Writing Custom Controls Using PowerBASIC






4.88/5 (5 votes)
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.
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 theWM_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 theCallWindowProc
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.
' ---------------------------------------------------------------------------
' Copyright Christopher R. Boss, 2010
' All Rights Reserved
' This code may be used ROYALTY FREE !
' You may use this code in any commercial application
' freely. You may also distribute the code to others
' and may post it on a web site for others to use,
' as long as the copyright is left in the code.
' ---------------------------------------------------------------------------
#DIM ALL
#DEBUG ERROR OFF
#COMPILE DLL
#INCLUDE "win32api.inc"
' ---------------------------------------------------------------------------
' Custom Class ControlClass Constants and Types
' ---------------------------------------------------------------------------
%ControlClassExtraData = 5 ' # of Extra Data Items (Long) for All
Custom Window Classes
' Data Items will be indexed from 1 in
GetControlData function
' ---------------------------------------------------------------------------
$ControlClassName = "MY_CUSTOM_CTRL1"
' ---------------------------------------------------------------------------
' ---------------------------------------------------------------------------
' Universal Global Variables
' ---------------------------------------------------------------------------
GLOBAL DLL_Instance&
' ---------------------------------------------------------------------------
' EZGUI Custom Control Library Declares
' ---------------------------------------------------------------------------
DECLARE FUNCTION GetControlLong(BYVAL hWnd AS LONG, BYVAL N&) AS LONG
DECLARE SUB SetControlLong(BYVAL hWnd AS LONG, BYVAL N&, BYVAL V&)
' ---------------------------------------------------------------------------
' Custom Control Control Class Declares
' ---------------------------------------------------------------------------
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
' ---------------------------------------------------------------------------
' DLL Entrance - LibMain
' ---------------------------------------------------------------------------
FUNCTION LIBMAIN(BYVAL hInstance AS LONG, _
BYVAL fwdReason AS LONG, _
BYVAL lpvReserved AS LONG) EXPORT AS LONG
SELECT CASE fwdReason
CASE %DLL_PROCESS_ATTACH ' =1 - Where DLL starts
DLL_Instance&=hInstance
RegisterControlClass
CASE %DLL_THREAD_ATTACH
CASE %DLL_THREAD_DETACH
CASE %DLL_PROCESS_DETACH ' =0 - Where DLL exits
CASE ELSE
END SELECT
LIBMAIN=1
END FUNCTION
' ---------------------------------------------------------------------------
' Custom Control ControlClass Functions / Subs
' ---------------------------------------------------------------------------
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
' If message is processed then set FUNCTION=0 and then EXIT FUNCTION
SELECT CASE Msg
CASE %MY_CUSTOM_MESSAGE
' New Control Color passed in wParam
' You could pass almost any type of info about your control using
' this technique. Even a pointer to a complex UDT could be passed.
' A single Long value representing a RGB color is just an example
' used for this sample project.
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
' Uses the Standard Static control message
SendMessage hParent, %WM_COMMAND,
MAKLNG(GetWindowLong(hWnd,%GWL_ID),%STN_DBLCLK), hWnd
END IF
CASE %WM_LBUTTONDOWN
hParent=GetParent(hWnd)
IF hParent<>0 THEN
' Uses the Standard Static control message
SendMessage hParent,
%WM_COMMAND,MAKLNG(GetWindowLong(hWnd,%GWL_ID),%STN_CLICKED), hWnd
END IF
CASE %WM_ENABLE
CASE %WM_GETDLGCODE
CASE %WM_CREATE
' --------------------------------------
' Set Default Color
SetControlLong hWnd, 5, RGB(255,255,0)
' Set any Default values for control here
' --------------------------------------
BuildBitmap hWnd, 1
CASE %WM_DESTROY
BuildBitmap hWnd, -1
CASE %WM_SIZE
BuildBitmap hWnd, 0
' ControlClassDraw hWnd
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&
' CFlag&=1 for Creation, =0 for Resize, -1 for destroy
IF CFlag&<=0 THEN
' Delete Previous DC and Bitmap
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)
' ------------------------------------
' Draw your control here into the memory DC (not the window DC).
' The memory DC has a Bitamp associated with it already.
' The code below is just sample code.
' ------------------------------------
' In this example lParam is simply an RGB color value
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
' TYPE PAINTSTRUCT
' hdc AS LONG
' fErase AS LONG
' rcPaint AS Rect
' fRestore AS LONG
' fIncUpdate AS LONG
' rgbReserved(1 TO 32) AS BYTE
' END TYPE
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
' -----------------------------------------------------------------------
' -----------------------------------------------------------------------
' EZGUI Custom Control Library Code (free to use)
' -----------------------------------------------------------------------
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 theLoadLibrary
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 inDLLmain
if not loaded usingLoadLibrary
. - How to use the window
Long
s for data storage per each instance of the control. Windows 95 is limited to 10Long
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 duringWM_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!