使用 PowerBASIC 编写自定义控件






4.88/5 (5投票s)
用老式方法(不使用 .NET 或 COM 类)编写自定义控件类是一门失传的技艺,但学习起来并不难。
引言
用老式方法(不使用 .NET 或 COM 类)编写自定义控件类是一门失传的技艺,但学习起来并不难。在 DLL 中编译新控件是分发控件的一种快速、高效的方法。然后,可以使用 `CreateWindowEx` API 在任何允许访问 Windows API 的编程语言中创建它们。编写此类控件的最佳语言是什么?
在我看来,PowerBASIC 编译器生成的 DLL 非常快速、小巧,而且该语言与标准的 Microsoft Basic 方言(例如 Visual Basic、QuickBASIC)兼容,但增加了许多附加功能(参见:http://powerbasic.com)。
我不为 PowerBASIC 工作,但我是一名开发用于 PowerBASIC 的第三方附加组件的开发者,因此我在编写自定义控件方面拥有丰富的经验。我编写过诸如掩码编辑(edit 类的超类)、形状/热点控件、海龟绘图控件(矢量图形)、画布控件、MCI 控件等控件,并且目前我正在开发一个 OpenGL 画布控件。
编写自定义控件
那么从哪里开始呢?有三种方法可以生成自定义控件:
- 子类化
如果您只需要为现有控件添加一两个小功能,那么您只需对其进行子类化(subclassing)。这基本上是在控件的原始窗口过程中创建一个挂钩,然后拦截并处理您需要修改的任何消息。例如,您可以处理 `WM_ERASEBKGND` 消息来绘制不同类型的背景,而不是让 Windows 简单地为其着色。您可以通过处理 `WM_KEYDOWN`/`WM_KEYUP` 消息来捕获某些按键并阻止它们通过。
- 超类化(Superclassing)
超类化(Superclassing)是指您实际上创建一个基于现有窗口类的新窗口类。您获取有关现有窗口类(例如 `Button`)的信息,包括其窗口过程地址,然后将其设置为您自定义控件的窗口过程地址。在控件的窗口过程中,您处理许多消息,但当您想使用原始控件窗口类的功能时,您只需使用 `CallWindowProc` 函数将消息传递给它。
超类化的优点是可以添加额外的窗口字节(每实例窗口数据存储的一种形式)来存储有关您控件的信息,并且您可以访问控件的 `WM_CREATE` 消息(子类化时不可用)。
超类化是构建自定义控件的绝佳方法。
- 创建新的窗口类!
您还可以从头开始构建自定义控件。您创建一个全新的窗口类,并在窗口过程中处理其消息。这是最困难的路线,因为您必须处理更多的消息才能使控件执行某些操作。虽然 `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
' ------------------------------------------------------------------------
此代码演示了许多内容。
- 如何注册一个新的控件(窗口)类。
注意:此代码在 `DLLMain` 中注册新控件类,但这仅应在 DLL 使用 `LoadLibrary` API 函数加载时执行。如果您计划将 DLL 直接链接到您的应用程序(使用 include 文件),那么您应该直接调用注册函数(导出它)。Windows 对在未通过 `LoadLibrary` 加载的情况下可以在 `DLLmain` 中调用的 API 存在一些限制。
- 如何使用窗口
Long
s 来为每个控件实例存储数据。Windows 95 限制为 10 个Long
s(40 字节),但后来的 Windows 版本可以处理更多。 - 如何创建内存 DC、位图,然后在 `WM_PAINT` 期间使用它将 `BltBit` 到屏幕。
这个例子可能看起来很简单,但它演示了编写自定义窗口类的许多核心技术。
结论
您是否厌倦了 COM 和 .NET?
尝试用老式方法编写一些自定义控件。在很多方面,它更有效率,也更强大。至少,您的控件在大小上会小得多,并且可能更快,因为它们的工作方式符合 Windows 底层的运行方式。
尽情享用!