Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Writing Custom Controls Using PowerBASIC

0.00/5 (No votes)
30 Jun 2010 1  
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:

  1. 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.

  2. 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.

  3. 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.

  1. 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.

  2. How to use the window Longs for data storage per each instance of the control. Windows 95 is limited to 10 Longs (40 bytes), but later versions of Windows can handle more.
  3. 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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here