A number of years back I realized that custom controls would be an excellent way of modularizing or ‘componentising’ some aspect of GUI behavior. I really didn’t know how to go about it though until I found a tutorial on custom controls by Chris Boss. I will shortly provide a link to this tutorial which I believe Chris still maintains. While the code I am providing below on building a custom control bears little resemblance to Chris’ original code, it was nonetheless inspired by Chris’ work. What I did was add some messaging functionality to Chris’ idea.
The specific thing I wanted to learn how to do was create specialized data entry grids that would allow for the very rapid and efficient entry of large volumes of data. In developing this rather simple minded example I am presenting to you below, I was able to figure out the underlying messaging architecture that would allow me to create my more advanced grid controls. I am providing this code in the hope some of you might also find it useful.
The first program below – CustomControlClient.bas, contains four controls. There are three buttons with labels of blue, green, and red. The fourth control is a rather silly custom control that is nothing but a window painted some particular color. When you click one of the three buttons the color printed on the button’s label is painted in the custom control’s window. While its silly, it does show the underlying architecture of a custom control in simple enough detail to be reasonably easily understood. Finally, when you click on the custom control, the control sends a message back to the host and a message box coded in the host displays all the ‘state’ information contained in the custom control. I used PowerBASIC 9.0 for this project and Jose’s Includes, but it should work with PowerBASIC’s standard Includes too.
Finally, at some future date I intend to provide code showing how to turn this control into an ActiveX Control. And I can state with a fair degree of accuracy when that will occur: It will occur when I have finally figured out how to do it or someone shows me how to do it!
CustomControlClient.bas
#Compile Exe 'This app hosts a simple custom control housed in dllCustomControlServer.dll.
#Dim All 'This is a fairly simple control that does not even export any functions, and
#Include "Win32api.inc" 'all communication between the control and this app is through Windows messaging.
%IDC_BUTTON1 =1300 'Control ID For Blue' Button
%IDC_BUTTON2 =1305 'Control ID For Green' Button
%IDC_BUTTON3 =1310 'Control ID For Red' Button
%FORM1_CUSTOM1 =1315 'Control ID For Custom Control
%MY_CUSTOM_MESSAGE =1500 'Custom User Defined Message For Custom Control
%MY_CUS_MSG_LBUTTON_DOWN =1505 'and another
Global hDll As Dword 'When Microsoft created their suite of 'Common Controls' such as the.........
Type WndEventArgs '...SysMonthCal control, Tab control, etc., they realized that more complicated
wParam As Long 'information needed to be passed between the host of the control and the control
lParam As Long 'itself than could be handled by the wParam and lParam parameters to the Window
hWnd As Dword 'procedure that are such a basic part of the Windows messaging mechanism used by
hInst As Dword 'simpler controls such as edit boxes and listboxes. What they did was create a
End Type 'new WM_ message named WM_NOTIFY that the custom control would send to its host...
Type WindowColor 'Associated with this WM_NOTIFY message is a wParam and lParam just like with all
lpnmh As NMHDR 'messages. However, the lParam will hold a pointer to a large type/structure whose
szColor As Asciiz*16 'fields are unique to whatever functionality the custom control exposes. Since this
End Type 'apps's custom control only manifests itself as a colored window, it only exposes...
Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
Local pCreateStruct As CREATESTRUCT Ptr
Local hButton As Dword '...a simple type/structure with a field for window color, i.e., szColor (see type WindowColor
Local dwStyle As Dword 'left above). But the standard created by Microsoft is that the first member of this user
Local hCus As Dword 'createdtype/structure should be a Microsoft defined structure named NMHDR (New Msg Handler).
Register i As Long 'It looks like this...
pCreateStruct=wea.lParam 'Type NMHDR
wea.hInst=@pCreateStruct.hInstance ' hwndFrom As Dword
hDLL= _ ' idfrom As Dword
LoadLibrary _ ' code As Long
( _ 'End Type continued below...
"dllCustomControlServer.dll" _
)
if hDLL Then
hButton=CreateWindowEx(0,"button","Blue",%WS_CHILD Or %WS_VISIBLE,8,10,80,25,wea.hWnd,%IDC_BUTTON1,wea.hInst,ByVal 0)
hButton=CreateWindowEx(0,"button","Green",%WS_CHILD Or %WS_VISIBLE,8,40,80,25,wea.hWnd,%IDC_BUTTON2,wea.hInst,ByVal 0)
hButton=CreateWindowEx(0,"button","Red",%WS_CHILD Or %WS_VISIBLE,8,70,80,25,wea.hWnd,%IDC_BUTTON3,wea.hInst,ByVal 0)
dwStyle=%WS_CHILD Or %WS_VISIBLE Or %WS_THICKFRAME
hCus= _
CreateWindowEx _ '...Interupting for a moment our discussion of NMHDR, note that just left is the CreateWindow
( _ 'call that instantiates our custom control located in the dll. A custom control is created just
%WS_EX_CLIENTEDGE, _ 'like any other control - through a CreateWindow() call. Note that the 2nd parameter of the
"Custom_Control", _ 'CreateWindowEx() call is the registered class name of the custom control in the dll - here
"", _ 'Custom_Control. I'll discuss that issue in the dll code. But returning again to NMHDR, there
dwStyle, _ 'will be a window procedure within the dll for the custom control. This window procedure will
100, _ 'receive Windows messages such as mouse movements, button clicks, etc., just like any window
12, _ 'procedure. These windows messages received within the window procedure of the custom control
325, _ 'in the dll will be transferred/sent back to the host app in slightly modified form through this
80, _ 'NMHDR structure. It might be instructive at this point to examine the WM_LBUTTONDOWN code in
wea.hWnd, _ 'ControlClassWndProc in the dllCustomControlServer.bas file to see what happens when a LButtonDown
%FORM1_CUSTOM1, _ 'occurs over the custom control. First off you may note that a local variable of type WindowColor
wea.hInst, _ 'is declared at the top of the window procedure. This type contains as its 1st member a NMHDR
ByVal 0 _ 'structure named lpnmh. The 2nd member is an asciiz buffer to hold the color of the custom control.
) 'When a left button down occurs the NMHDR is filled out with the hWnd and CtrlID of the custom,
Else 'as well as a member we have not discussed yet named 'code'. What goes here is a custom control
MsgBox("Dll Load Failure!")
PostQuitMessage 0 'defined message that will inform the host of what exactly happened in the custom control.
Function=-1 'In this case a WM_LBUTTONDOWN occurred. A custom message equate is defined in both the dll and the
Exit Function 'host for this scenerio, e.g., %MY_CUS_MSG_LBUTTON_DOWN = 1505. This gets assigned to the
End If 'wc.lpnmh.code member. The next thing that happens is that the cbWndExtra bytes (0-3) of the
fnWndProc_OnCreate=0 'custom control's Window Class structure are queried for the RGB value stored there. Four bytes
End Function 'of cbWndExtra storage were allocated in the Window Class structure for this purpose of storing...
'Event Handler For Click Of 'Blue' Button '...the custom control's current color. Select Case logic then translates the
Sub btnBlue_Click(wea As WndEventArgs) 'retrieved RGB value into an Asciiz string such as 'Red', 'Green', 'Blue', etc.
Call SendMessage _ 'This string is then assigned to the wc.szColor member of the WindowColor type.
( _ 'Then something critical happens. This SendMessage() call is made...
GetDlgItem(wea.hWnd,%FORM1_CUSTOM1), _ '
%MY_CUSTOM_MESSAGE, _ 'SendMessage(GetParent(hWnd), %WM_NOTIFY, GetDlgCtrlID(hWnd), VarPtr(wc))
RGB(0,0,255),0 _ '
) 'The 1st parameter will obtain the parent of the custom control which is the
End Sub 'client or this host app. The 2nd parameter is the message to be sent to .....
'Event Handler For Click Of 'Green' Button '...this app, and that would be the WM_NOTIFY message we have been discussing all
Sub btnGreen_Click(wea As WndEventArgs) 'along. When this app receives a WM_NOTIFY message it will know that a message
Call SendMessage _ 'from some custom control has arrived. The 3rd parameter (wParam) of the
( _ 'SendMessage() call is the control id of the particular custom control sending
GetDlgItem(wea.hWnd,%FORM1_CUSTOM1), _ 'the message. The 4th parameter is the most important. This would be the address
%MY_CUSTOM_MESSAGE, _ 'of the wc WindowColor variable in the Window Procedure of the custom control. And
RGB(0,255,0), _ 'it is this variable that currently holds all the unique custom control 'state'
0 _ 'information of which the custom control wishes to make its host aware. Note that
) 'the Varptr(wc) was transferred back through this lParam parameter which is in
End Sub 'keeping with Windows typical use of this variable which is after all ultimately
'Event Handler For Click Of 'Red' Button '...defined as a pointer variable in various windows header files. So that explains
Sub btnRed_Click(wea As WndEventArgs) 'what happens in the dll when you click the mouse button over the custom control.
Call SendMessage _ 'What needs to be explained here now in the host app is what happens when the buttons
( _ 'just to the left of the custom control are clicked, and how the WM_NOTIFY handler
GetDlgItem(wea.hWnd,%FORM1_CUSTOM1), _ 'here works. We'll start with the former being as the button click handlers are
%MY_CUSTOM_MESSAGE, _ 'just to the left of these words you are reading. First realize that the three
RGB(255,0,0), _ 'buttons aren't part of the custom control. They are located within this app and
0 _ 'are children of this app's main form. When one of them is clicked this main form's
) 'Window Procedure receives its typical WM_COMMAND message and logic then routes
End Sub 'program execution to one of these three button message handlers just to the left.
'Event Handler For %WM_COMMAND Messages 'What then happens is another SendMessage() Api call but this time the message is
Function fnWndProc_OnCommand(wea As WndEventArgs) As Long 'being sent to the custom control. And the message being sent is
Select Case LoWrd(wea.wParam) 'another custom control defined message - not a standard windows message. The
Case %IDC_BUTTON1 'message is defined both here and in the dll as follows - %MY_CUSTOM_MESSAGE=1500.
Call btnBlue_Click(wea) 'The third parameter (wParam) of the SendMessage() call is the RGB value that the
Case %IDC_BUTTON2 'host (this app) wants the custom control to draw itself, i.e., blue, green, or
Call btnGreen_Click(wea) 'red. Referring back to the Window Procedure within the dll in
Case %IDC_BUTTON3 'dllCustomControlServer.bas, you'll note a case - Case %MY_CUSTOM_MESSAGE - and
Call btnRed_Click(wea) 'this SendMessage() call from here will be 'picked up' there. What then happens
End Select 'is the RGB value placed here in the host in the wParam of the SendMessage() call
fnWndProc_OnCommand=0 'is stored there in the custom control in its previously allocated cbWndExtra
End Function 'bytes. Then an InvalidateRect() call is finally made and this forces a WM_PAINT...
Function fnWndProc_OnNotify(wea As WndEventArgs) As Long '..within the custom control where the RGB value is retrieved from
Local wc As WindowColor Ptr 'the cbWndExtra bytes, a colored brush of the desired color created,
wc=wea.lParam 'and the window finally painted the desired color. Well, that was a
Select Case As Long @wc.lpnmh.idFrom 'mouthful but it brings us to fnWndProc_OnNotify() just to the left.
Case %FORM1_CUSTOM1 'That worked out pretty well I'd say! That over there to the left
Select Case As Long @wc.lpnmh.code 'looks like a complicated mess of Select Case logic, doesn't it? Well,
Case %MY_CUS_MSG_LBUTTON_DOWN 'its not all that complicated. In fact, if you would remove every
MsgBox _ 'remnant of the select case statements and just leave the MsgBox() the
( _ 'program would still work. What the Select Case logic does is determine
"wParam = " & Str$(wea.wParam) & Chr$(13) & Chr$(10) & _ 'the specific custom control ID the
"@wc.lpnmh.hwndFrom = " & Str$(@wc.lpnmh.hwndFrom) & Chr$(13) & Chr$(10) & _ 'WM_NOTIFY is coming from, and the
"@wc.lpnmh.idFrom = " & Str$(@wc.lpnmh.idFrom) & Chr$(13) & Chr$(10) & _ 'particular custom message that has
"@wc.lpnmh.code = " & Str$(@wc.lpnmh.code) & Chr$(13) & Chr$(10) & _ 'just arrived. In this unique case
"@wc.szColor = " & @wc.szColor _ 'we only have one instance of the custom control on the main form, and
) 'that particular custom control only generates one message, but in
End Select 'general custom controls should be designed so that any number of them
End Select 'can be instantiated on a form/dialog, and many custom controls generate
fnWndProc_OnNotify=0 'many different messages. Think of a grid for example. A grid might
End Function 'generate a message when a char key is pressed, when a cursor motion...
'Event Handler For %WM_CLOSE Message '...key is pressed, a button clicked, etc. That's why in general Select
Function fnWndProc_OnClose(wea As WndEventArgs) As Long 'Case logic will be used. Note also the variable declaration. There is
Call FreeLibrary(hDll) 'only one - 'Local wc As WindowColor Ptr' Don't forget that when the
Call PostQuitMessage(0) 'custom control SendMessag()'ed us the %MY_CUS_MSG_LBUTTON_DOWN message,
fnWndProc_OnClose=0 'the lParam parameter was loaded with a pointer to a WindowColor Type,
End Function 'i.e., Varptr(wc). That is why pointer notation is being used to....
Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) Export As Long
Local wea As WndEventArgs '...dereference the pointer variable. Finally note how the message box
Select Case wMsg 'faithfully reports to us here in the host app the 'state' of the variables
Case %WM_CREATE 'in the custom control.
wea.wParam=wParam:wea.lParam=lParam:wea.hWnd=hWnd
fnWndProc=fnWndProc_OnCreate(wea) '
Exit Function
Case %WM_COMMAND
wea.wParam=wParam:wea.lParam=lParam:wea.hWnd=hWnd
fnWndProc=fnWndProc_OnCommand(wea)
Exit Function
Case %WM_NOTIFY
wea.wParam=wParam:wea.lParam=lParam:wea.hWnd=hWnd
fnWndProc=fnWndProc_OnNotify(wea)
Exit Function
Case %WM_CLOSE
wea.wParam=wParam:wea.lParam=lParam:wea.hWnd=hWnd
fnWndProc=fnWndProc_OnClose(wea)
Exit Function
End Select
fnWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function
Function blnAppInitialize(hIns As Long,szClassName As Asciiz) As Dword
Local szAppName As Asciiz*16
Local wcl As WndClassEx
wcl.cbSize=SizeOf(wcl)
wcl.style=%CS_HREDRAW Or %CS_VREDRAW
wcl.lpfnWndProc=CodePtr(fnWndProc)
wcl.cbClsExtra=0
wcl.cbWndExtra=0
wcl.hInstance=hIns
wcl.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
wcl.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
wcl.hbrBackground=%COLOR_BTNFACE+1
wcl.lpszMenuName=%NULL
wcl.lpszClassName=VarPtr(szClassName)
wcl.hIconSm=LoadIcon(hIns,ByVal %IDI_APPLICATION)
If IsFalse(RegisterClassEx(wcl)) Then
blnAppInitialize=%FALSE
Exit Function
End If
Function=%TRUE
End Function
Function WinMain(ByVal hIns As Long, ByVal hPrevIns As Long,ByVal lpCmdLine As Asciiz Ptr, ByVal iShow As Long) As Long
Local dwStyle,hMainWnd As Dword
Local szAppName As Asciiz*32
Local Msg As tagMsg
szAppName="Custom Control Test"
If blnAppInitialize(hIns,szAppName) Then 'Register App Class In blnAppInitialize()
dwStyle=%WS_OVERLAPPEDWINDOW Xor %WS_MAXIMIZEBOX
hMainWnd=CreateWindowEx(0,szAppName,szAppName,dwStyle,200,100,440,140,%HWND_DESKTOP,0,hIns,ByVal 0)
Call ShowWindow(hMainWnd,iShow)
Call UpdateWindow(hMainWnd)
While GetMessage(Msg,%NULL,0,0)
TranslateMessage Msg
DispatchMessage Msg
Wend
End If
Function=msg.wParam
End Function
And here is the code for dllCustomControlServer.bas that needs to be compiled into a dll that is used by CustomControlClient.exe
#Compile Dll "dllCustomControlServer.dll" 'Here is the code for te custom control contained in dllCustomControlServer.dll.
#Dim All 'I'd recommend you not use anything but local variables in the custom control
#Include "win32api.inc" 'code so that multiple instances of the control can be created. Otherwise,
%MY_CUSTOM_MESSAGE =1500 'you'll likely end up with one control's memory messing up another's.
%MY_CUS_MSG_LBUTTON_DOWN =1505
Type WindowColor 'A pointer to a variable of this type will be sent to the parent or container window
lpnmh As NMHDR 'when a %WM_LBUTTONDOWN message is intercepted by the custom control. Just for the
szColor As Asciiz*16 'purpose of exposition, the cbWndExtra bytes will be interogated by a Call To
End Type 'GetWindowLong() to obtain the RGB color stored there. This will be sent to Parent.
'Window Procedure for the custom control.
Function ControlClassWndProc(ByVal hWnd As Long,ByVal Msg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
Local hDC,hNewBrush As Dword
Local wc As WindowColor
Local ps As PAINTSTRUCT
Select Case Msg
Case %WM_CREATE
Call SetWindowLong(hWnd,0,RGB(255,255,0)) 'Set default start up color to yellow and store RGB value
Function=0 'of yellow in bytes 0 - 3 of allocated cbWndExtra bytes.
Exit Function
Case %WM_PAINT 'When the custom control's window becomes invalid for
hDC=BeginPaint(hWnd,ps) 'whatever reason a WM_PAINT will be received and this code
hNewBrush=CreateSolidBrush(GetWindowLong(hWnd,0)) 'will run. Note that a Brush will be
'hNewBrush=CreateHatchBrush(%HS_CROSS,GetWindowLong(hWnd,0)) 'created of the color retrieved from
Call FillRect(hDC,ps.rcPaint,hNewBrush) 'the controls cbWndExtra bytes. That is...
Call DrawText(hDC, "Click Me!",-1,ps.rcPaint,%DT_SINGLELINE Or %DT_CENTER Or %DT_VCENTER)
Call DeleteObject(hNewBrush) 'what the GetWindowLong(hWnd,0) is doing, i.e., retrieving
Call EndPaint(hWnd,ps) 'the RGB value stored in cbWndExtra bytes.
Function=0
Exit Function
Case %WM_LBUTTONDOWN 'If the particular custom control receives a left button
wc.lpnmh.hwndFrom=hWnd 'down message a WindowColor variable is filled out with
wc.lpnmh.idFrom=GetDlgCtrlID(hWnd) 'such items of information as the control's hWnd, its
wc.lpnmh.code=%MY_CUS_MSG_LBUTTON_DOWN 'Ctrl ID, and the present color of the control retrieved
Select Case GetWindowLong(hWnd,0) 'from the cbWndExtra bytes. Also, the wc.lpnmh.code
Case RGB(255,0,0) 'red 'member is set to %MY_CUS_MSG_LBUTTONDOWN to let the
wc.szColor="Red" 'host know what happened in the custom control. Finally,
Case RGB(0,255,0) 'green 'a pointer to the WindowColor type/struct is SendMessage()'ed
wc.szColor="Green" 'back to the host. The message that the host will actually
Case RGB(255,255,0) 'yellow 'receive is WM_NOTIFY. The host will then dereference and
wc.szColor="Yellow" 'tear apart the WindowColor type to get at the sent data.
Case RGB(0,0,255) 'blue
wc.szColor="Blue"
End Select
Call SendMessage(GetParent(hWnd),%WM_NOTIFY,GetDlgCtrlID(hWnd),VarPtr(wc)) 'Send info back to parent.
Function=0
Exit Function
Case %MY_CUSTOM_MESSAGE 'This is the message the host sends to the custom control
Call SetWindowLong(hWnd,0,wParam) 'to let it know it wants it to do something, i.e., paint
Call InvalidateRect(hWnd,ByVal %NULL,%TRUE) 'its window some particular color. The host sends an RGB
Function=0 'value in here through the wParam of the message, and here
Exit Function 'that wParam (containing an RGB value) is stored immediately
Case %WM_SIZE 'in the cbWndExtra bytes. Then an InvalidateRect() call
Call InvalidateRect(hWnd,ByVal %NULL,%TRUE) 'forces a WM_PAINT where the RGB value is retrieved and
Function=0 'the window painted.
Exit Function
End Select
Function=DefWindowProc(hWnd,Msg,wParam,lParam)
End Function
Function RegisterControlClass() As Word 'Many knowledgeable folks state that a custom control
Local windowclass As WndClassEx 'should export some kind of Initialize() function the
Local szClassName As Asciiz*16 'calling of which by the host app will cause the control's
'class to be registered by a function such as directly
szClassName="Custom_Control" 'at left. At the risk of being savaged by such
windowclass.cbSize=SizeOf(windowclass) 'knowledgeable folks I have not done that here. When
windowclass.style=%CS_HREDRAW Or %CS_GLOBALCLASS Or %CS_DBLCLKS 'a dll is loaded its DllMain function is called with the
windowclass.lpfnWndProc=CodePtr(ControlClassWndProc) 'fwdReason parameter set to DLL_PROCESS_ATTACH. I use
windowclass.cbClsExtra=0 'this message to register the custom control's class.
windowclass.cbWndExtra=4 'Four extra bytes to store RGB color. 'The Windows documentation states that problems in
windowclass.hInstance=GetModuleHandle(ByVal %NULL) 'dll load order can in some situations occur if GDI or
windowclass.hIcon=%NULL 'user functions are called during a dll's DLL_PROCESS_ATTACH
windowclass.hCursor=LoadCursor(%NULL,ByVal %IDC_ARROW) 'message. Kernel functions are OK but GDI functions can
windowclass.hbrBackground=GetStockObject(%WHITE_BRUSH) 'be problematic according to the docs. RegisterClassEx()
windowclass.lpszMenuName=%NULL 'is a user32 function that returns an ATOM (Atoms are in
windowclass.lpszClassName=VarPtr(szClassName) 'kernel), and I have never had problems with this.
windowclass.hIconSm=%NULL 'However, if it causes you any concern simply Export
Function=RegisterClassEx(windowclass) 'RegisterControlClass() and call it from WinMain()
End Function 'or WM_CREATE. In that case it wouldn't be necessary...
Function DllMain(ByVal hInstance As Long,ByVal fwdReason As Long,ByVal lpvReserved As Long) As Long
If fwdReason=%DLL_PROCESS_ATTACH Then
If RegisterControlClass()=%FALSE Then '...to LoadLibrary() the Dll, as implicit loading through
Function=0 'the Declare would be performed by PowerBASIC.
Exit Function
End If
End If
DllMain=1
End Function