65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 PowerBASIC 编写自定义控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (5投票s)

2010年7月2日

CPOL

5分钟阅读

viewsIcon

30080

用老式方法(不使用 .NET 或 COM 类)编写自定义控件类是一门失传的技艺,但学习起来并不难。

引言

用老式方法(不使用 .NET 或 COM 类)编写自定义控件类是一门失传的技艺,但学习起来并不难。在 DLL 中编译新控件是分发控件的一种快速、高效的方法。然后,可以使用 `CreateWindowEx` API 在任何允许访问 Windows API 的编程语言中创建它们。编写此类控件的最佳语言是什么?

在我看来,PowerBASIC 编译器生成的 DLL 非常快速、小巧,而且该语言与标准的 Microsoft Basic 方言(例如 Visual Basic、QuickBASIC)兼容,但增加了许多附加功能(参见:http://powerbasic.com)。

我不为 PowerBASIC 工作,但我是一名开发用于 PowerBASIC 的第三方附加组件的开发者,因此我在编写自定义控件方面拥有丰富的经验。我编写过诸如掩码编辑(edit 类的超类)、形状/热点控件、海龟绘图控件(矢量图形)、画布控件、MCI 控件等控件,并且目前我正在开发一个 OpenGL 画布控件。

编写自定义控件

那么从哪里开始呢?有三种方法可以生成自定义控件:

  1. 子类化

    如果您只需要为现有控件添加一两个小功能,那么您只需对其进行子类化(subclassing)。这基本上是在控件的原始窗口过程中创建一个挂钩,然后拦截并处理您需要修改的任何消息。例如,您可以处理 `WM_ERASEBKGND` 消息来绘制不同类型的背景,而不是让 Windows 简单地为其着色。您可以通过处理 `WM_KEYDOWN`/`WM_KEYUP` 消息来捕获某些按键并阻止它们通过。

  2. 超类化(Superclassing)

    超类化(Superclassing)是指您实际上创建一个基于现有窗口类的新窗口类。您获取有关现有窗口类(例如 `Button`)的信息,包括其窗口过程地址,然后将其设置为您自定义控件的窗口过程地址。在控件的窗口过程中,您处理许多消息,但当您想使用原始控件窗口类的功能时,您只需使用 `CallWindowProc` 函数将消息传递给它。

    超类化的优点是可以添加额外的窗口字节(每实例窗口数据存储的一种形式)来存储有关您控件的信息,并且您可以访问控件的 `WM_CREATE` 消息(子类化时不可用)。

    超类化是构建自定义控件的绝佳方法。

  3. 创建新的窗口类!

    您还可以从头开始构建自定义控件。您创建一个全新的窗口类,并在窗口过程中处理其消息。这是最困难的路线,因为您必须处理更多的消息才能使控件执行某些操作。虽然 `DefWindowProc` API 函数为您做了很多事情,并且许多消息可以简单地传递给它,但您将不得不处理许多消息才能使您的控件执行任何有价值的操作。另一方面,这种形式的自定义控件是最强大的,因为您可以决定一切如何工作。

如果您想要一个简单的自定义控件示例,您可以在 http://cwsof.com/page1.htm 下载我的免费自定义控件示例程序(完整的源代码)。(代码也提供在下方。)

关于编写自定义控件的好书不多。而且,大多数新书都涉及 ActiveX 之类的内容,这对于 PowerBASIC 来说并不是您想要的。我能找到的唯一一本好书可能已经绝版,但您或许能在某处找到一本(二手)副本。这本书是:“Windows Custom Controls”,作者 William Smith 和 Robert Ward,由 R&D Technical Books 出版,版权所有 1993。

这本书主要讲的是 16 位 Windows 编程,但 32 位和 16 位 Windows 编程在自定义控件方面几乎没有变化。一些 API 内容可能略有不同,但基本概念仍然相同。最好使用最新的 API 参考检查书中使用的每个 API 函数,看看它在 32 位 Windows 中是否有所更改。

我从这本书中学到了很多东西,因为它很好地解释了一些基本概念。所以,让我们看一些实际的代码吧!

这是一个非常简单的自定义控件,它演示了一种对于具有背景持久性的控件非常重要的技术。在这种情况下,背景图像存储在位图中,然后在 `WM_PAINT` 消息期间使用 `BitBlt` 绘制到屏幕上。

以下是简单自定义控件的源代码

注意:可以使用 PowerBASIC Windows 编译器从 6.0 到 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

' ------------------------------------------------------------------------

此代码演示了许多内容。

  1. 如何注册一个新的控件(窗口)类。

    注意:此代码在 `DLLMain` 中注册新控件类,但这仅应在 DLL 使用 `LoadLibrary` API 函数加载时执行。如果您计划将 DLL 直接链接到您的应用程序(使用 include 文件),那么您应该直接调用注册函数(导出它)。Windows 对在未通过 `LoadLibrary` 加载的情况下可以在 `DLLmain` 中调用的 API 存在一些限制。

  2. 如何使用窗口 Longs 来为每个控件实例存储数据。Windows 95 限制为 10 个 Longs(40 字节),但后来的 Windows 版本可以处理更多。
  3. 如何创建内存 DC、位图,然后在 `WM_PAINT` 期间使用它将 `BltBit` 到屏幕。

这个例子可能看起来很简单,但它演示了编写自定义窗口类的许多核心技术。

结论

您是否厌倦了 COM 和 .NET?

尝试用老式方法编写一些自定义控件。在很多方面,它更有效率,也更强大。至少,您的控件在大小上会小得多,并且可能更快,因为它们的工作方式符合 Windows 底层的运行方式。

尽情享用!

© . All rights reserved.