Introduction
Writing custom control classes the old fashioned way (no .NET or COM classes) is a lost art, but it is not too difficult to learn. Compiling new controls in a DLL produces a fast, efficient method of distributing controls. Then they can be created using the CreateWindowEx
API in any programming language which allows you to access the Windows API. What's the best language to write such controls?
In my opinion, the PowerBASIC compiler produces very fast, small DLLs, and the language is compatible with standard Microsoft Basic dialects (i.e., Visual Basic, QuickBASIC), but with a lot of goodies added to it (see: http://powerbasic.com).
I don't work for PowerBASIC, but I am a developer of third party add-ons for use with PowerBASIC, so I have a lot of experience writing custom controls this way. I have written such controls as Masked Edit (superclass of edit class), Shape/Hot Spot control, Turtle Graphics control (vector graphics), Canvas control, MCI control, and currently, I am working on an OpenGL Canvas control.
Writing a custom control
So where to start? There are three ways to produce a custom control:
- Subclassing
If all you need to do is add a minor feature or two to an existing control, then you simply subclass the control, which basically creates a hook into the control's original Window procedure, and then intercept and process any messages that you need to modify. For example, you could process the WM_ERASEBKGND
message to draw a different type of background rather than have Windows simply color it. You could trap certain key-presses and prevent them from coming through, by processing the WM_KEYDOWN
/WM_KEYUP
messages.
- Superclassing
Superclassing is where you actually create a new window class based on an existing one. You get information about the existing window class (e.g., Button
), which includes its window procedure address, and then set the address to your custom control's window procedure. In the control's window procedure, you process many of the messages, but when you want to use the features of the original control window class, you simply pass the messages on to it by using the CallWindowProc
function.
Superclassing has the advantage of being able to add extra window bytes (a form of window data storage for each instance of the window) for storing information about your control, plus you have access to the control's WM_CREATE
message (not available when subclassing).
Superclassing is an excellent way of building custom controls.
- Create a new window class!
You can also build a custom control from scratch. You create a totally new window class and process its messages in a window procedure. This is the hardest route, since you must process more messages to make the control do something. While the DefWindowProc
API function does many things for you and many messages can simply be passed to it, you will have to process a number of messages to make your control do anything valuable. On the other hand, this form of custom control is the most powerful, since you decide how everything works.
If you would like an example of a simple custom control, you can download my freeware custom control sample program (full source code) at http://cwsof.com/page1.htm. (The code is also provided below.)
There aren't a lot of good books on writing custom controls. Plus, most new books deal with things like ActiveX, which is not what you want for PowerBASIC. The only good book I could find may be out of print, but you might find a copy (used) somewhere. The book is: "Windows Custom Controls", by William Smith and Robert Ward, published by R&D Technical Books Copyright 1993.
The book is about 16 bit Windows programming, but few things have changed between 32 bit and 16 bit Windows programming of custom controls. Some of the API stuff may be slightly different, but the basic concept is still the same. It would be best to check each API function used in the book with an up to date API reference to see if it has changed at all in 32 bit Windows.
I learned a lot from this book, since it explained some of the basic concepts quite well. So, let's look at some working code!
Here is a very simple custom control which demonstrates a very important technique for a control which has background persistence. In this case, the background image is stored in a bitmap and then BitBlt
to the screen during the WM_PAINT
message.
Here is the source code for the simple custom control:
Note: The code can be compiled using any version of the PowerBASIC Windows compiler from versions 6.0 to 9.0.
#DIM ALL
#DEBUG ERROR OFF
#COMPILE DLL
#INCLUDE "win32api.inc"
%ControlClassExtraData = 5 Custom Window Classes
GetControlData function
$ControlClassName = "MY_CUSTOM_CTRL1"
GLOBAL DLL_Instance&
DECLARE FUNCTION GetControlLong(BYVAL hWnd AS LONG, BYVAL N&) AS LONG
DECLARE SUB SetControlLong(BYVAL hWnd AS LONG, BYVAL N&, BYVAL V&)
DECLARE SUB RegisterControlClass()
DECLARE SUB ControlClassPaint(BYVAL hWnd AS LONG)
DECLARE SUB ControlClassDraw(BYVAL hWnd AS LONG)
DECLARE SUB BuildBitmap(BYVAL hWnd AS LONG, BYVAL CFlag&)
%MY_CUSTOM_MESSAGE = %WM_USER+100
FUNCTION LIBMAIN(BYVAL hInstance AS LONG, _
BYVAL fwdReason AS LONG, _
BYVAL lpvReserved AS LONG) EXPORT AS LONG
SELECT CASE fwdReason
CASE %DLL_PROCESS_ATTACH DLL_Instance&=hInstance
RegisterControlClass
CASE %DLL_THREAD_ATTACH
CASE %DLL_THREAD_DETACH
CASE %DLL_PROCESS_DETACH CASE ELSE
END SELECT
LIBMAIN=1
END FUNCTION
SUB RegisterControlClass()
LOCAL windowclass AS WndClassEx
LOCAL szClassName AS ASCIIZ * 80
szClassName = $ControlClassName+CHR$(0)
windowclass.cbSize = SIZEOF(windowclass)
windowclass.style = %CS_HREDRAW OR %CS_VREDRAW OR %CS_PARENTDC
OR %CS_DBLCLKS OR %CS_GLOBALCLASS
windowclass.lpfnWndProc = CODEPTR(ControlClassWndProc)
windowclass.cbClsExtra = 0
windowclass.cbWndExtra = %ControlClassExtraData*4
windowclass.hInstance = DLL_Instance&
windowclass.hIcon = %NULL
windowclass.hCursor = LoadCursor( %NULL, BYVAL %IDC_ARROW )
windowclass.hbrBackground = GetStockObject( %WHITE_BRUSH )
windowclass.lpszMenuName = %NULL
windowclass.lpszClassName = VARPTR( szClassName )
windowclass.hIconSm = %NULL
RegisterClassEx windowclass
END SUB
FUNCTION ControlClassWndProc(BYVAL hWnd AS LONG, _
BYVAL Msg AS LONG, _
BYVAL wParam AS LONG, _
BYVAL lParam AS LONG) EXPORT AS LONG
LOCAL RV&, hParent AS LONG
SELECT CASE Msg
CASE %MY_CUSTOM_MESSAGE
SetControlLong hWnd, 5, wParam
ControlClassDraw hWnd
InvalidateRect hWnd, BYVAL %NULL, %TRUE
FUNCTION=RV&
EXIT FUNCTION
CASE %WM_PAINT
ControlClassPaint hWnd
FUNCTION=0
EXIT FUNCTION
CASE %WM_ERASEBKGND
FUNCTION=0
EXIT FUNCTION
CASE %WM_SETCURSOR
CASE %WM_LBUTTONDBLCLK
hParent=GetParent(hWnd)
IF hParent<>0 THEN
SendMessage hParent, %WM_COMMAND,
MAKLNG(GetWindowLong(hWnd,%GWL_ID),%STN_DBLCLK), hWnd
END IF
CASE %WM_LBUTTONDOWN
hParent=GetParent(hWnd)
IF hParent<>0 THEN
SendMessage hParent,
%WM_COMMAND,MAKLNG(GetWindowLong(hWnd,%GWL_ID),%STN_CLICKED), hWnd
END IF
CASE %WM_ENABLE
CASE %WM_GETDLGCODE
CASE %WM_CREATE
SetControlLong hWnd, 5, RGB(255,255,0)
BuildBitmap hWnd, 1
CASE %WM_DESTROY
BuildBitmap hWnd, -1
CASE %WM_SIZE
BuildBitmap hWnd, 0
InvalidateRect hWnd, BYVAL %NULL, %TRUE
CASE ELSE
END SELECT
FUNCTION = DefWindowProc(hWnd,Msg,wParam,lParam)
END FUNCTION
SUB BuildBitmap(BYVAL hWnd AS LONG, BYVAL CFlag&)
LOCAL R AS RECT, hDC AS LONG, hBmp AS LONG, hDC2 AS LONG
LOCAL hOldBmp AS LONG, W&, H&
IF CFlag&<=0 THEN
hDC=GetControlLong(hWnd, 1)
hBmp=GetControlLong(hWnd, 2)
DeleteObject hBmp
DeleteDC hDC
SetControlLong hWnd, 1, 0
SetControlLong hWnd, 2, 0
SetControlLong hWnd, 3, 0
SetControlLong hWnd, 4, 0
END IF
IF CFlag&>=0 THEN
hDC2=GetDC(hWnd)
hDC=CreateCompatibleDC(hDC2)
GetClientRect hWnd, R
W&=R.nRight-R.nLeft
H&=R.nBottom-R.nTop
hBmp=CreateCompatibleBitmap(hDC2, W&, H&)
SelectObject hDC, hBmp
ReleaseDC hWnd, hDC2
SetControlLong hWnd, 1, hDC
SetControlLong hWnd, 2, hBmp
SetControlLong hWnd, 3, W&
SetControlLong hWnd, 4, H&
ControlClassDraw hWnd
END IF
END SUB
SUB ControlClassDraw(BYVAL hWnd AS LONG)
LOCAL hDC AS LONG, W&, H&, L&, ATL&, tbuffer$, CFlag&
LOCAL hParent AS LONG, hBrush AS LONG, OldhBrush AS LONG, C&
IF IsWindow(hWnd) THEN
hDC=GetControlLong(hWnd, 1)
W&=GetControlLong(hWnd, 3)
H&=GetControlLong(hWnd, 4)
C&=GetControlLong(hWnd, 5)
hBrush=CreateSolidBrush(C&)
OldhBrush=SelectObject(hDC, hBrush)
PatBlt hDC, 0,0, W&, H&, %PATCOPY
SelectObject hDC, OldhBrush
DeleteObject hBrush
END IF
END SUB
SUB ControlClassPaint(BYVAL hWnd AS LONG)
LOCAL PS AS PAINTSTRUCT
LOCAL hDC AS LONG, R AS RECT, tbuffer$, L&, ATL&
LOCAL memDC AS LONG
IF IsWindow(hWnd) THEN
hDC=BeginPaint(hWnd, PS)
memDC=GetControlLong(hWnd, 1)
BitBlt hDC, PS.rcPaint.nLeft, PS.rcPaint.nTop, _
PS.rcPaint.nRight-PS.rcPaint.nLeft,
PS.rcPaint.nBottom-PS.rcPaint.nTop, _
memDC, PS.rcPaint.nLeft, PS.rcPaint.nTop, %SRCCOPY
EndPaint hWnd, PS
END IF
END SUB
FUNCTION GetControlLong(BYVAL hWnd AS LONG, BYVAL N&) AS LONG
LOCAL I&, RV&
RV&=0
IF N&>=1 AND N&<=%ControlClassExtraData THEN
I&=(N&-1)*4
IF IsWindow(hWnd) THEN
RV&=GetWindowLong(hWnd, I&)
END IF
END IF
FUNCTION=RV&
END FUNCTION
SUB SetControlLong(BYVAL hWnd AS LONG, BYVAL N&, BYVAL V&)
LOCAL I&
IF N&>=1 AND N&<=%ControlClassExtraData THEN
I&=(N&-1)*4
IF IsWindow(hWnd) THEN
SetWindowLong hWnd, I&, V&
END IF
END IF
END SUB
This code demonstrates a number of things.
- How to register a new control (window) class.
Note: This code registers the new control class in DLLMain
, but this should only be done if the DLL will be loaded using the LoadLibrary
API function. If you plan on linking the DLL directly into your applications (using include files), then you really should call the register function directly (export it). Windows has some restrictions of what APIs can be called in DLLmain
if not loaded using LoadLibrary
.
- How to use the window
Long
s for data storage per each instance of the control. Windows 95 is limited to 10 Long
s (40 bytes), but later versions of Windows can handle more.
- How to create a memory DC, bitmap, and then use it to
BltBit
to the screen during WM_PAINT
.
This example may seem very simple, but it demonstrates many of the core techniques in writing a custom window class.
Conclusion
Are you tired of COM and .NET?
Try writing some custom controls the old fashioned way. In many ways, it is more efficient and more powerful. At the very least, your controls will be much smaller in size and likely faster since they work the way Windows works under the hood.
Enjoy!