{************************************************************************
*
*	Custom Controls Unit
*
*	WRITTEN BY:		Shawn Aubrey Baker (aka sab)
*
*	COMPUSERVE ID:	76450,22
*
*	CREDITS:			This code started out being based on the work of
*						Robert Norton, who uploaded a bitmap button unit to
*						Compuserve. Thanks Robert, it helped a lot. Also, the
*						code from the example unit (BITBTN.PAS) that came with
*						Turbo Pascal for Windows was a big help. Thanks Borland.
*
*	USE:				As you wish. Please send any comments and/or bug fixes
*						via mail to the above ID. IF IT DIES IT'S YOUR PROBLEM.
*
*	NOTES:			This file uses tabs = 3
*
*	THE PROBLEM:	The first time I tried this the custom bitmap buttons
*						worked fine in a TWindow but died a horrible death in
*						a TDlgWindow. This is because Windows creates the
*						actual controls instead of OWL. OWL provides little
*						(read NO) support for custom controls and assumes that
*						any control from a resource is fully created by the time
*						that OWL gets to its child window creation code. This
*						means that OWL doesn't try to create the window (good!)
*						but that it has installed set the window procedure to
*						its standard initialization proc (bad!). This procedure
*						(InitWndProc for those with the OWL source) depends on
*						having a global variable (CreationWindow) set that points
*						to the object being created. Since this variable hasn't
*						been set the routine goes off into la-la land. Also, the
*						InitResource method sets the DefaultProc pointer to nil,
*						which again causes OWL to go astray.
*
*	THE SOLUTION:	The method I've used to get around this is to override
*						the window procedure pointer in the TWndClass structure
*						to point my own procedure (InitCustom). Unfortunately,
*						the InitCustom procedure needs to get a pointer to the
*						object being initialized in order to get the real window
*						procedure out of the Instance variable. Therefore, I've
*						had to create a collection of custom controls and get
*						the pointers out of there. Again, this only applies to
*						controls from resources, so the object is added to the
*						collection in InitResource and removed from it in
*						InitCustom. It is possible to get into problems with this
*						if you Init 2 dialogs with custom controls before you
*						ExecDialog either of them. If there is a TCustom control
*						with the same ID in the two resources then there is no
*						telling which one will get picked out of the collection.
*						It's simple, DON'T DO THIS!!!
*
*						As far as the TWindow.InitResource problem goes, I simply
*						call TWindow.Init as it does and then set everything
*						except the DefaultProc pointer in the same way as
*						TWindow.InitResource does.
*
*	THE END.
*
************************************************************************}

unit	Custom;

interface

uses	WinTypes,WinProcs,WObjects,Strings;

type

		PCustom=^TCustom;
		TCustom=object(TWindow)
			constructor	Init(AParent:PWindowsObject; AnId:integer;
									ATitle:PChar; X,Y,W,H:integer);
			constructor	InitResource(AParent:PWindowsObject; AnID:word);
			procedure	SetupWindow; virtual;
			function		GetClassName:PChar; virtual;
			procedure	GetWindowClass(var AWndClass:TWndClass); virtual;
			end;

		PCustomButton=^TCustomButton;
		TCustomButton=object(TCustom)
			OwnMouse		:	boolean;		{ Is the mouse held captive?			}
			Pressed		:	boolean;		{ Is the button currently pressed?	}
			Default		:	boolean;		{ Is this the default button?			}

			constructor	Init(AParent:PWindowsObject; AnID:integer;
									AText:PChar; X,Y,W,H:integer; IsDefault:boolean);
			constructor	InitResource(AParent:PWindowsObject; AnID:integer);
			procedure	SetupWindow; virtual;

			procedure	WMMouseMove(var Msg:TMessage);
													virtual wm_First + wm_MouseMove;

			procedure	WMLButtonDown(var Msg:TMessage);
													virtual wm_First + wm_LButtonDown;
			procedure	WMLButtonUp(var Msg:TMessage);
													virtual wm_First + wm_LButtonUp;

			procedure	WMSetFocus(var Msg:TMessage);
													virtual wm_First + wm_SetFocus;
			procedure	WMKillFocus(var Msg:TMessage);
													virtual wm_First + wm_KillFocus;

			procedure	WMKeyDown(var Msg:Tmessage);
													virtual wm_First + wm_KeyDown;
			procedure	WMKeyUp(var Msg:Tmessage);
													virtual wm_First + wm_KeyUp;

			procedure	WMGetDlgCode(var Msg:Tmessage);
													virtual wm_First + wm_GetDlgCode;
			procedure	BMSetStyle(var Msg:Tmessage);
													virtual wm_First + bm_SetStyle;
			end;

		PBitButton=^TBitButton;
		TBitButton=object(TCustomButton)
			UpBits		:	HBitMap;
			FocUpBits	:	HBitMap;
			DownBits		:	HBitMap;
			UpName		:	PChar;
			FocUpName	:	PChar;
			DownName		:	PChar;
			bmWidth		:	integer;
			bmHeight		:	integer;

			constructor	Init(AParent:PWindowsObject; AnID,X,Y:integer;
									AUpName,AFocUpName,ADownName:PChar;
									IsDefault:boolean);
			constructor	InitResource(AParent:PWindowsObject; AnID:integer;
												AUpName,AFocUpName,ADownName:PChar);
			destructor	Done; virtual;
			procedure	SetupWindow; virtual;
			function		GetClassName:PChar; virtual;
			procedure	Paint(DC:HDC; var PaintInfo:TPaintStruct); virtual;
			end;

implementation

{------------------------------------------------------------------------
-------------------------------------------------------------------------
----									TCustom Object									----
-------------------------------------------------------------------------
------------------------------------------------------------------------}

{************************************************************************
*
*	Name:			InitCustom
*
*	Purpose:		Called with the first message for a custom control. This
*					routine sets the window procedure to the one pointed to
*					by Instance in the Custom object. The object is stored in
*					the Customs collection by the Init/InitResource routine
*					and it is removed here. This list is only needed in order
*					to find the actual object.
*
*	Parameters:	Message	- the first message (should be wm_NCCreate)
*					WParam	- more message info
*					LParam	- even more message info
*
*	Return:		window procedure return value (depends on the message command)
*
************************************************************************}

var	Customs:PCollection;		{ collection of custom controls }
		ACustom:PWindowsObject;	{ current custom control }

function InitCustom(HWindow:HWND; Message,WParam:word; LParam:longint):
							longint; export;

var	ID:longint;

		{ finds the Custom object in the Customs collection }

		function FindID(Custom:PCustom):boolean; far;
		begin
		FindID:=Custom^.GetID = ID;
		end;

begin

{ find the Custom object, delete it from the collection }

ID:=GetWindowWord(HWindow,gww_ID);
ACustom:=Customs^.FirstThat(@FindID);
Customs^.Delete(ACustom);

{ set the window proc to the instance proc }

SetWindowLong(HWindow,gwl_WndProc,longint(ACustom^.Instance));

{ call the instance proc to handle the message }

asm
	PUSH	HWindow
	PUSH	Message
	PUSH	WParam
	PUSH	LParam.Word[2]
	PUSH	LParam.Word[0]
	MOV	AX,DS
	LES	DI,ACustom
	CALL	ES:[DI].TWindowsObject.Instance
end;

end;

{************************************************************************
*
*	Name:			TCustom.Init
*
*	Purpose:		Initializes a custom control.
*
*	Parameters:	AParent		- parent window
*					AnID			- button ID
*					ATitle		- control title
*					X,Y,W,H		- position and size
*
*	Return:		None
*
************************************************************************}

constructor TCustom.Init(AParent:PWindowsObject; AnId:integer;
									ATitle:PChar; X,Y,W,H:integer);
begin
TWindow.Init(AParent,ATitle);
Attr.Id:=AnId;
Attr.X:=X;
Attr.Y:=Y;
Attr.W:=W;
Attr.H:=H;
Attr.Style:=ws_Child or ws_Visible or ws_Group or ws_TabStop;
end;

{************************************************************************
*
*	Name:			TCustom.InitResource
*
*	Purpose:		Initializes a custom control from a resource and enables
*					data transfer.
*
*	Parameters:	AParent		- parent window
*					AnID			- button ID
*
*	Return:		None
*
************************************************************************}

constructor TCustom.InitResource(AParent:PWindowsObject; AnID:word);
begin

{ replacement code for TWindow.InitResource, needed		}
{ because the TWindow routine sets DefaultProc to nil,	}
{ wherease TWindow.Init sets it to the routine we want	}

TWindow.Init(AParent,nil);
SetFlags(wb_FromResource,true);
FillChar(Attr,SizeOf(Attr),0);
Attr.ID:=AnID;

{ must pre-register because Windows creates controls from resources }

if not Register then Fail;
EnableTransfer;

{ add it to the Customs collection so that the InitCustom proc can find it }

Customs^.Insert(@self);
end;

{************************************************************************
*
*	Name:			TCustom.SetupWindow
*
*	Purpose:		Sets up the window and gets the attributes if the window
*					is from a resource.
*
*	Parameters:	None
*
*	Return:		None
*
************************************************************************}

procedure TCustom.SetupWindow;
var	Rect:TRect;
		Pt:TPoint;
begin
TWindow.SetupWindow;

{ if it's from a resource then set the attributes }

if IsFlagSet(wb_FromResource) then
	begin

	{ get the client rect in screen co-ordinates }

	GetWindowRect(HWindow,Rect);
	Pt.X:=Rect.Left;
	Pt.Y:=Rect.Top;

	{ make the position relative to the parent window }

	ScreenToClient(GetWindowWord(HWindow,gww_HWndParent),Pt);
	Attr.X:=Pt.X;
	Attr.Y:=Pt.Y;

	{ get the client rect and set the window size }

	GetClientRect(HWindow,Rect);
	Attr.W:=Rect.Right-Rect.Left;
	Attr.H:=Rect.Bottom-Rect.Top;

	{ get the style info }

	Attr.Style:=GetWindowWord(HWindow,gwl_Style);
	Attr.ExStyle:=GetWindowWord(HWindow,gwl_ExStyle);
	end;
end;

{************************************************************************
*
*	Name:			TCustom.GetClassName
*
*	Purpose:		Abstract virtual method that gets the class name for a
*					custom control. Generates a run-time error to ensure
*					that the descendants override the method with their
*					own class name.
*
*	Parameters:	None
*
*	Return:		None
*
************************************************************************}

function TCustom.GetClassName:PChar;
begin
Abstract;
end;

{************************************************************************
*
*	Name:			TCustom.GetWindowClass
*
*	Purpose:		Sets the class info for a custom control. Overrides the
*					TPW startup procedure to use code that will find the
*					object in our "Customs" collection.
*
*	Parameters:	AWndClass - class information
*
*	Return:		None
*
************************************************************************}

procedure TCustom.GetWindowClass(var AWndClass:TWndClass);
begin
TWindow.GetWindowClass(AWndClass);

if IsFlagSet(wb_FromResource) then
	AWndClass.lpfnWndProc:=@InitCustom;
end;

{------------------------------------------------------------------------
-------------------------------------------------------------------------
----								TCustomButton Object								----
-------------------------------------------------------------------------
------------------------------------------------------------------------}

{************************************************************************
*
*	Name:			TCustomButton.Init
*
*	Purpose:		Initializes a custom button.
*
*	Parameters:	AParent		- parent window
*					AnID			- button ID
*					AText			- button text (or nil)
*					X,Y,W,H		- position and size
*					IsDefault	- default button ?
*
*	Return:		None
*
************************************************************************}

constructor	TCustomButton.Init(AParent:PWindowsObject; AnID:integer;
											AText:PChar; X,Y,W,H:integer;
											IsDefault:boolean);
begin
TCustom.Init(AParent,AnID,nil,X,Y,10,10);
if IsDefault then
	Attr.Style:=Attr.Style or bs_DefPushButton
else
	Attr.Style:=Attr.Style or bs_PushButton;
end;

{************************************************************************
*
*	Name:			TCustomButton.InitResource
*
*	Purpose:		Initializes a custom button from a resource.
*
*	Parameters:	AParent	- parent window
*					AnID		- button ID
*
*	Return:		None
*
************************************************************************}

constructor	TCustomButton.InitResource(AParent:PWindowsObject; AnID:integer);
begin
TCustom.InitResource(AParent,AnID);
DisableTransfer;
end;

{************************************************************************
*
*	Name:			TCustomButton.SetupWindow
*
*	Purpose:		Sets up the window and initializes the state variables.
*
*	Parameters:	None
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.SetupWindow;
begin
TCustom.SetupWindow;

Pressed:=false;
OwnMouse:=false;
Default:=Attr.Style and bs_DefPushButton = bs_DefPushButton;
end;

{************************************************************************
*
*	Name:			TCustomButton.WMLButtonDown
*
*	Purpose:		repaint the button in the down position when the left
*					mouse button is pressed.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMLButtonDown(var Msg:TMessage);
begin

{ if not already pressed then set state to pressed }

if not Pressed then
	begin
	if GetFocus <> hWindow then
		SetFocus(hWindow);
	Pressed:=true;
	OwnMouse:=true;
	SetCapture(hWindow);
	end;

{ trigger repaint }

InvalidateRect(hWindow,nil,false);
end;

{************************************************************************
*
*	Name:			TCustomButton.WMLButtonUp
*
*	Purpose:		If the left mouse button is pressed and then released
*					over the button then repaint it as unpressed and notify
*					the parent window.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMLButtonUp(var Msg:TMessage);
begin
if OwnMouse then
	begin
	ReleaseCapture;
	OwnMouse:=false;
	if Pressed then		{ trigger repaint and notify parent }
		begin
		Pressed:=false;
		InvalidateRect(hWindow,nil,false);
		PostMessage(Parent^.hWindow,wm_Command,Attr.Id,longint(hWindow));
		end;
	end;
end;

{************************************************************************
*
*	Name:			TCustomButton.WMMouseMove
*
*	Purpose:		Repaints the button when the mouse is pressed and moves
*					into and outof the button window.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMMouseMove(var Msg:TMessage);
var	BtnRect:TRect;
		MousePt:TPoint;
begin

{ get window rectangle and mouse point }

GetClientRect(hWindow,BtnRect);
MousePt.X:=integer(Msg.lParamLo);
MousePt.Y:=integer(Msg.lParamHi);

{ if the mouse is over the button }

if PtInRect(BtnRect,MousePt) then
	begin

	{ if the mouse is moved into the button area }

	if OwnMouse and (not Pressed) then
		begin
		Pressed:=true;
		InvalidateRect(hWindow,nil,false);
		end;
	end

{ if the mouse is moved out of the button area }

else if Pressed then
	begin
	Pressed:=false;
	InvalidateRect(hWindow,nil,false);
	end;
end;

{************************************************************************
*
*	Name:			TCustomButton.WMSetFocus
*
*	Purpose:		Forces repaint if the focus is set to the button.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMSetFocus(var Msg:TMessage);
begin
InvalidateRect(hWindow,nil,false);
end;

{************************************************************************
*
*	Name:			TCustomButton.WMKillFocus
*
*	Purpose:		Forces repaint if the focus is taken away from the button.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMKillFocus(var Msg:TMessage);
begin
InvalidateRect(hWindow,nil,false);
end;

{************************************************************************
*
*	Name:			TCustomButton.WMKeyDown
*
*	Purpose:		Repaints the button in the down position if the space
*					bar is pressed on the button.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMKeyDown(var Msg:Tmessage);
begin
if (Msg.wParam = $20) and not Pressed and not OwnMouse then
	begin
	Pressed:=true;
	InvalidateRect(hWindow,nil,false);
	end;
end;

{************************************************************************
*
*	Name:			TCustomButton.WMKeyUp
*
*	Purpose:		Repaints the button in the up position and notifies the
*					parent window if the space bar is pressed on the button.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMKeyUP(var Msg:Tmessage);
begin
if (Msg.wParam = $20) and Pressed and not OwnMouse then
	begin
	Pressed:=false;
	InvalidateRect(hWindow,nil,false);
	PostMessage(Parent^.hWindow,wm_Command,Attr.Id,longint(hWindow));
	end;
end;

{************************************************************************
*
*	Name:			TCustomButton.WMGetDlgCode
*
*	Purpose:		Gets whether or not the button is the default.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.WMGetDlgCode(var Msg:Tmessage);
begin
if Default then
	Msg.Result:=dlgc_DefPushButton
else
	Msg.Result:=dlgc_UndefPushButton;
end;

{************************************************************************
*
*	Name:			TCustomButton.BMSetStyle
*
*	Purpose:		Sets the button style to either default or not.
*
*	Parameters:	Msg - a message
*
*	Return:		None
*
************************************************************************}

procedure TCustomButton.BMSetStyle(var Msg:Tmessage);
var	OldDefault:boolean;
begin
OldDefault:=Default;
Default:=Msg.WParam = bs_DefPushButton;
if Default <> OldDefault then
	InvalidateRect(hWindow,nil,false);
end;

{------------------------------------------------------------------------
-------------------------------------------------------------------------
----									TBitButton Object								----
-------------------------------------------------------------------------
------------------------------------------------------------------------}

{************************************************************************
*
*	Name:			TBitButton.Init
*
*	Purpose:		Initializes a button.
*
*	Parameters:	AParent		- parent window
*					AnID			- button ID
*					X,Y			- position
*					IsDefault	- default button ?
*					AUpName		- name of resource for up bitmap
*					AFocUpName	- name of resource for up bitmap when focused
*					ADownName	- name of resource for down bitmap
*
*	Return:		None
*
************************************************************************}

constructor TBitButton.Init(AParent:PWindowsObject; AnID,X,Y:integer;
										AUpName,AFocUpName,ADownName:PChar;
										IsDefault:boolean);
begin
TCustomButton.Init(AParent,AnID,nil,X,Y,10,10,IsDefault);

UpName:=AUpName;
FocUpName:=AFocUpName;
DownName:=ADownName;
end;

{************************************************************************
*
*	Name:			TBitButton.InitResource
*
*	Purpose:		Initializes a button from a resource.
*
*	Parameters:	AParent		- parent window
*					AnID			- button ID
*					AUpName		- name of resource for up bitmap
*					AFocUpName	- name of resource for up bitmap when focused
*					ADownName	- name of resource for down bitmap
*
*	Return:		None
*
************************************************************************}

constructor TBitButton.InitResource(AParent:PWindowsObject; AnID:integer;
										AUpName,AFocUpName,ADownName:PChar);
begin
TCustomButton.InitResource(AParent,AnID);

UpName:=AUpName;
FocUpName:=AFocUpName;
DownName:=ADownName;
end;

{************************************************************************
*
*	Name:			TBitButton.Done
*
*	Purpose:		Destroys the button.
*
*	Parameters:	None
*
*	Return:		None
*
************************************************************************}

destructor TBitButton.Done;
begin
DeleteObject(UpBits);
DeleteObject(FocUpBits);
DeleteObject(DownBits);
TCustomButton.Done;
end;

{************************************************************************
*
*	Name:			TBitButton.SetupWindow
*
*	Purpose:		Loads the bitmaps for a button, resizes the window
*					accordingly, and initializes the state variables.
*
*	Parameters:	None
*
*	Return:		None
*
************************************************************************}

procedure TBitButton.SetupWindow;
var	bm:TBitMap;
begin
TCustomButton.SetupWindow;

{ load the bitmaps }

UpBits:=LoadBitmap(hInstance,UpName);
FocUpBits:=LoadBitmap(hInstance,FocUpName);
DownBits:=LoadBitmap(hInstance,DownName);

{ resize the window to fit the bitmaps }

GetObject(DownBits,SizeOf(bm),@bm);
MoveWindow(HWindow,Attr.X,Attr.Y,bm.bmWidth+2,bm.bmHeight+2,false);
bmWidth:=bm.bmWidth;
bmHeight:=bm.bmHeight;
end;

{************************************************************************
*
*	Name:			TBitButton.GetClassName
*
*	Purpose:		Gets the class name for a bitmap button.
*
*	Parameters:	None
*
*	Return:		pointer to the class name
*
************************************************************************}

function TBitButton.GetClassName;
begin
GetClassName:='BitButton';
end;

{************************************************************************
*
*	Name:			TBitButton.Paint
*
*	Purpose:		Paints one of the bitmaps into the window depending on
*					the current state.
*
*	Parameters:	DC				- device context to paint into
*					PaintInfo	- painting information
*
*	Return:		None
*
************************************************************************}

procedure TBitButton.Paint(DC:HDC; var PaintInfo:TPaintStruct);
var	BitsDC:HDC;
		OldBitmap:HBitMap;
		OldBrush:HBrush;
begin

{ draw the border }

if Default then
	OldBrush:=SelectObject(DC,GetStockObject(Black_Brush))
else
	OldBrush:=SelectObject(DC,GetStockObject(White_Brush));
PatBlt(DC,0,0,Attr.W,1,PatCopy);
PatBlt(DC,0,0,1,Attr.H,PatCopy);
PatBlt(DC,0,Attr.H-1,Attr.W,1,PatCopy);
PatBlt(DC,Attr.W-1,0,1,Attr.H,PatCopy);
SelectObject(DC,OldBrush);

{ draw the button }

BitsDC:=CreateCompatibleDC(DC);
if Pressed then
	OldBitmap:=SelectObject(BitsDC,DownBits)
else if GetFocus = hWindow then
	OldBitmap:=SelectObject(BitsDC,FocUpBits)
else
	OldBitmap:=SelectObject(BitsDC,UpBits);
BitBlt(DC,1,1,bmWidth,bmHeight,BitsDC,0,0,SrcCopy);
SelectObject(BitsDC,OldBitmap);
DeleteDC(BitsDC);
end;

{------------------------------------------------------------------------
-------------------------------------------------------------------------
----									Unit initialization							----
-------------------------------------------------------------------------
------------------------------------------------------------------------}

begin
New(Customs,Init(40,10));
end.
