DWinLib - 核心部分






4.96/5 (8投票s)
DWinLib 幕后工作原理简介!
目录
引言
以下是DWinLib
(一个针对Windows API的半简单封装器)内部工作原理的概述。如果您想了解更高级别的概述,请阅读DWinLib 6:漂亮的WinAPI集成。
DWinLib
很久以前就开始了,原因有两个。首先,我当时的编程工具(Borland Builder 4.0)随着我的项目变大而变得不稳定。浏览论坛,发现其他人也有同样的问题。此外,我的代码中某个地方有一个我无法找出的错误。我不知道是框架还是我的编程出了问题。所以我必须做出改变,而其他人报告的脆弱性促使我转向Visual Studio Express,这在当时似乎是最好的免费选择。
我不相信那个版本的VS包含MFC,但即使有,查看MFC示例也令人沮丧。它们与Borland VCL的干净代码相去甚远。我也不知道我是否能用那个框架修复我的错误,所以我决定学习底层的Windows API,并克服这个疑虑。
我很高兴地成功完成了我的任务,以下是我在此过程中创建的框架的概述。我希望您觉得这篇文档有用,即使只是出于求知欲。
最简单的窗口封装器
首先要克服的问题是弄清楚如何将程序化的Windows API程序变成面向对象的程序。这是一个简单的API程序示例,其中指出了各种步骤
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE hInst;
// **************************************
// Step 1: Create main program entrance:
// **************************************
int WINAPI WinMain(HINSTANCE instance, HINSTANCE , PSTR , int show) {
// ***********************************
// Step 2: Register the Window Class:
// ***********************************
static TCHAR appName[] = TEXT("MinApplication");
hInst = instance;
WNDCLASS wndclass;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = instance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = appName;
if (!RegisterClass(&wndclass)) {
MessageBox(NULL, TEXT("Oh no, Mr. Bill...."), appName, MB_ICONERROR);
return 0;
}
// ***************************
// Step 3: Create the window:
// ***************************
HWND hwnd = CreateWindowEx(WS_EX_APPWINDOW, appName, TEXT("MinApplication"),
WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_BORDER,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, instance, NULL);
ShowWindow(hwnd, show);
UpdateWindow(hwnd);
// ********************************
// Step 4: Enter the message loop:
// ********************************
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
// ******************************************
// Step 5: Handle the messages from Windows:
// ******************************************
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_DESTROY :
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
以上述方式编程一个非平凡应用程序的困难之一是,需要花费大量时间创建和管理静态的WndProc
过程。还需要将应用程序的逻辑和数据绑定到WndProc
过程,这对于非平凡应用程序来说可能是一项相当大的任务。
下面是使用DWinLib
所使用的相同基本机制重写的上一个程序。从代码上看,它比前一个程序大了近四倍,尽管编译后为52 KB,而非封装版本为48 KB。它可以剪切粘贴到一个空白项目中并按原样编译,但我相信您必须在Visual Studio中将配置属性 -> 系统 -> 链接器更改为“Windows”。
(虽然此处不适用,但您还需要将配置属性 -> C/C++ -> 语言 -> 符合性模式更改为“否”才能编译DWinLib
程序。)
在您仔细阅读代码时,请记住以下几点
代码通过创建一个Application
单元来保存主静态回调。BaseWin
单元被定义为虚拟基类,从中派生实际的窗口。(没有成员函数需要强制设置为纯虚拟函数,因此只有常识才能阻止您实例化BaseWin
对象。)
当BaseWin
对象实例化时,BaseWin
构造函数会调用Application
来注册自身。从BaseWin
派生的类会根据需要覆盖任何BaseWin
函数,以便以您希望的方式处理特定于Windows的消息(WM_PAINT
等)。您还可以向派生类添加自己的函数,以处理与窗口逻辑无关的自身逻辑(如果需要)。
在此示例中,只有MainWin
单元是从BaseWin
派生的。在DWinLib
应用程序中,一个名为MainAppWin
的等效MainWin
是必需的单元,并且包含您的应用程序的主窗口。在一个非平凡的应用程序中,您可能还有许多从BaseWin
派生的窗口。
总而言之,这是一种简单有效的方法。
您会注意到,之前指出的相同五个步骤已添加注释,尽管又添加了几条注释。
您还会注意到第五步已分为三个部分。第一部分(5 - 与之前的代码一样)是Application
单元的一部分的静态消息处理程序。第二部分(5a)是BaseWin
单元的非静态成员函数,用于定义窗口消息的默认处理程序。第三部分(5b)在默认处理程序无法完成您所需任务时,覆盖特定窗口消息处理程序以供您的窗口专用。
其他注释用于指示与概述步骤无关的程序结构。
#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400
#include <tchar.h>
#include <windows.h>
#include <stdlib.h>
class BaseWin;
// *************************************************************************
// Define an 'Application' class to handle the main static window procedure.
// There will only be one of these instantiated in the program as a global.
// *************************************************************************
class Application {
private:
static BaseWin * winBeingCreatedC;
HINSTANCE hInstC;
public:
Application(HINSTANCE hInst_, HINSTANCE, LPSTR, int);
LRESULT static CALLBACK WindowProc(HWND window, unsigned int msg,
unsigned int wParam, long lParam);
HINSTANCE hInst() { return hInstC; }
void setWinBeingCreated(BaseWin * win) { winBeingCreatedC = win; }
WPARAM run();
};
// **********************************************************
// Define a base window class for deriving real windows from
// **********************************************************
class BaseWin {
//For dealing with the window handle and window procedure
protected:
HWND hwndC;
public:
BaseWin();
~BaseWin();
//winProc can be overridden for each window, but it is easiest to just
//override the individual functions.
virtual LRESULT winProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam);
HWND hwnd() { return hwndC; }
void hwnd(HWND hwnd) { hwndC = hwnd; } //Only needed by Application::WindowProc
//Routines to be inherited by derived windows
public:
virtual LRESULT wClose() { return DefWindowProc(hwndC, WM_CLOSE, 0, 0); }
};
BaseWin * Application::winBeingCreatedC = NULL;
Application * gApplication; //A Global Application object for the entire program
// ************************************
// Now fill in the BaseWin definition
// ************************************
BaseWin::BaseWin() : hwndC(NULL) {
gApplication->setWinBeingCreated(this);
}
BaseWin::~BaseWin() {
if (IsWindow(hwndC) == TRUE) DestroyWindow(hwndC);
}
// ***************************************************************************
// Step 5a: Handle the messages from Windows in a non-static member function:
// ***************************************************************************
LRESULT BaseWin::winProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
wClose();
return 0;
}
return DefWindowProc(window, msg, wParam, lParam);
}
// ***************************************************
// Now define and fill in the guts to a 'real' window
// that uses the above BaseWin definitions.
// ***************************************************
class MainWin : public BaseWin {
//Regular class stuff
private:
void registerWinClass();
public:
MainWin();
//BaseWin overrides:
public:
virtual LRESULT wClose();
};
namespace { //An unnamed namespace for this unit to use
const TCHAR * winCaption = _T("Bare Windows Wrapper Example");
const TCHAR * winClassName = _T("BareWindowApp");
}
// ***********************************
// Step 2: Register the Window Class:
// ***********************************
void MainWin::registerWinClass() {
WNDCLASS wc;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = gApplication->WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = gApplication->hInst();
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject (GRAY_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = winClassName;
if (!RegisterClass (&wc)) abort();
}
MainWin::MainWin() {
static bool registered = false;
if (!registered) {
registerWinClass();
registered = true;
}
// ***************************
// Step 3: Create the window:
// ***************************
hwndC = CreateWindow(winClassName, winCaption, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, gApplication->hInst(),
NULL);
if (!hwndC) abort();
ShowWindow(hwndC, SW_SHOW);
UpdateWindow(hwndC);
}
// *************************************************
// Step 5b: Override any message handlers you want
// to handle differently than the BaseWin handler:
// *************************************************
LRESULT MainWin::wClose() {
PostQuitMessage(0);
return DefWindowProc(hwndC, WM_CLOSE, 0, 0);
}
// **********************************
// Now fill in the Application definition
// **********************************
Application::Application(HINSTANCE hInst_, HINSTANCE, LPSTR, int) :
hInstC(hInst_) {
gApplication = this;
}
WPARAM Application::run() {
MainWin win;
// ********************************
// Step 4: Enter the message loop:
// ********************************
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
// ******************************************
// Step 5: Handle the messages from Windows:
// ******************************************
LRESULT CALLBACK Application::WindowProc(HWND window, unsigned int msg, unsigned int wParam,
long lParam) {
BaseWin * win = reinterpret_cast<BaseWin*>(GetWindowLong(window, GWL_USERDATA));
if (win) return win->winProc(window, msg, wParam, lParam);
SetWindowLong(window, GWL_USERDATA, reinterpret_cast<long>(winBeingCreatedC));
winBeingCreatedC->hwnd(window);
return winBeingCreatedC->winProc(window, msg, wParam, lParam);
}
// ***************************************************************
// And finally, a WinMain that takes advantage of the above work:
// ***************************************************************
// **************************************
// Step 1: Create main program entrance:
// **************************************
int WINAPI WinMain(HINSTANCE inst1, HINSTANCE inst2, LPSTR str, int show) {
Application app(inst1, inst2, str, show);
return app.run();
}
以上代码是骨架式的;错误处理非常简单,所有无关消息都被忽略。之前的逻辑足以展示如何创建基本的Windows封装器。
需要注意的一点是窗口创建期间窗口类实例的存储方式。正如我在第二个代码块之前所说的,BaseWin
的构造函数会告诉全局Application
对象它正在被构造。如果您检查Application::WindowProc
函数,您会看到这是如何实际完成的,以及传入的窗口消息是如何路由到适当的成员函数的。DWinLib
处理方式略有不同,如下所示。
如果没有某种封装器,将应用程序的逻辑和数据绑定到纯API程序的WndProc
过程会非常痛苦。使用之前的框架,这不再困难:只需将其放在从BaseWin
派生的窗口的类定义中,并像使用任何C++类中的数据一样使用它。如果您熟悉面向对象技术,使用封装器编写的程序中的应用程序逻辑也会流畅得多。
如果您想看到上述方法在实际应用中,可以将之前的代码剪切粘贴到空白项目中。如果您想看一个更有趣的例子,这里有一个可以拆解的井字游戏示例。该程序比上述代码更完善,因为它处理了更多的消息,包含了异常处理,并且不像上述代码那样简单。它还以非常简洁的方式封装了几个Windows通用控件
尽管这是DWinLib
的核心框架,但DWinLib
比之前的代码先进得多。例如,这是当前的run()
函数
WPARAM dwl::Application::run() {
MSG msg;
int var;
try {
#ifdef DWL_MDI_APP
HWND mdiClient = gDwlGlobals->dwlMainWin->mdiClientHwnd();
HWND hwnd = gDwlGlobals->dwlMainWin->hwnd();
//Per Microsoft, all exceptions must be handled before returning control back to
//this message pump. For more info, read the last part of the article:
//http://www.microsoft.com/msj/archive/S204D.aspx. See DwlBaseApp::WndProc for the
//exception handler implemented for DWinLib.
accelTableC = accelC.table(); //The app can change this while running by calling
while ((var = GetMessage(&msg, NULL, 0, 0)) != 0) { //changeAccelTable(...)
if (var == -1) return var;
if (!TranslateMDISysAccel(mdiClient, &msg) &&
!TranslateAccelerator(hwnd, accelTableC, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
if (!::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE)) idlerC.wIdle();
}
}
#else
accelTableC = accelC.table();
while ((var = GetMessage(&msg, NULL, 0, 0)) != 0) {
if (var == -1) return var;
if (!TranslateAccelerator(gDwlGlobals->dwlMainWin->hwnd(), accelTableC, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
//Now check if we should go into idle processing:
if (!::PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE)) {
idleC = true;
//Do the idle processing:
while (idleC) idleC = wIdle();
}
}
}
#endif
}
catch (Exception & e) {
wString str = dwl::strings::msgPleaseReport();
str += L"\n" + dwl::strings::msgProgramming() + L"\n" + e.strC;
if (e.continuableC == Continuable::True)
str += L"\n" + dwl::strings::msgWillAttemptContinue();
else str += L"\n" + dwl::strings::msgProgramMustExit();
MessageBox(gDwlGlobals->dwlMainWin->hwnd(), str.c_str(),
dwl::strings::msgError().c_str(), MB_OK);
if (e.continuableC == Continuable::False) exit(EXIT_FAILURE);
}
catch (std::exception & e) {
wString str = dwl::strings::msgPleaseReport();
str += _T("\r\n");
str += dwl::strings::stdException();
str += _T("\r\n");
str += dwl::strings::msgError();
str += utils::strings::convertToApiString(e.what());
str += _T("\r\n");
str += dwl::strings::stdExceptionAbortQuery();
int wish = MessageBox(gDwlGlobals->dwlMainWin->hwnd(),
str.c_str(), dwl::strings::msgError().c_str(), MB_YESNO);
if (wish == IDYES) exit(EXIT_FAILURE);
}
catch (...) {
wString str = dwl::strings::msgUnknownException();
str += dwl::lastSysError();
str += dwl::strings::msgUnknownExceptionAbortQuery();
HWND parent = gDwlGlobals->dwlMainWin ? gDwlGlobals->dwlMainWin->hwnd() : NULL;
int wish = MessageBox(gDwlGlobals->dwlMainWin->hwnd(), str.c_str(),
dwl::strings::msgError().c_str(), MB_YESNO);
if (wish == IDYES) exit(EXIT_FAILURE);
}
return msg.wParam;
}
如您所见,它通过预处理器宏处理单文档界面(SDI)和多文档界面(MDI)程序。它还与程序范围的加速器表单元(响应“Ctrl + O”之类的“打开”命令)交互。内置了“空闲”机制,包含了错误处理,并使用了命名空间。
设计问题/解决方案
作为参考,如果您想回顾一下,这里是更大设计的简化插图。除了MainAppWin
之外,所有这些对象都在dwl
命名空间中
窗口查找
首先值得一提的问题是,感谢David Nash的精彩Win32++文章,DWinLib
取消了上面列出的GetWindowLong/SetWindowLong
方法,并将其替换为线程本地存储索引方法,结合应用程序map
来存储和查找各个窗口。当然,这样的尝试从来都不是一帆风顺的,如果您将他的方法与以下内容进行比较,您会注意到一些变化。
LRESULT CALLBACK dwl::Application::winProc(HWND window, UINT msg, WPARAM wParam,
LPARAM lParam) {
try {
BaseWin * win(nullptr);
{ //Scope the iterator to eliminate the possibility that the windowproc
//deletes the window and causes the iterator to become invalid,
//which will crash the program.
auto it = gDwlGlobals->dwlApp->windowsC.find(window);
if (it != gDwlGlobals->dwlApp->windowsC.end()) win = it->second;
}
if (win) return win->winProc(window, msg, wParam, lParam);
else {
BaseWin * tempWin =
static_cast<BaseWin*>(TlsGetValue(gDwlGlobals->dwlApp->tlsIndexC));
gDwlGlobals->dwlApp->windowsC.insert(std::make_pair(window, tempWin));
return tempWin->winProc(window, msg, wParam, lParam);
}
}
//exception handling...
}
TLS存储在createWindow
调用中设置,所有DWinLib
窗口都应该通过这些调用创建,除非您确保逻辑以某种方式在您自己的代码中复制
HWND dwl::Application::createWindow(BaseWin * winBeingCreated, const CREATESTRUCT & cs) {
TlsSetValue(tlsIndexC, reinterpret_cast<LPVOID>(winBeingCreated));
HWND ret = CreateWindowEx(cs.dwExStyle, cs.lpszClass, cs.lpszName, cs.style, cs.x, cs.y,
cs.cx, cs.cy, cs.hwndParent, cs.hMenu, cs.hInstance,
cs.lpCreateParams);
if (ret == NULL) {
MessageBox(NULL, dwl::lastSysError().c_str(), L"Error to report:", MB_OK);
}
return ret;
}
HWND dwl::Application::createMdiWindow(BaseWin * winBeingCreated, const CREATESTRUCT & cs) {
TlsSetValue(tlsIndexC, reinterpret_cast<LPVOID>(winBeingCreated));
HWND ret = CreateMDIWindow(cs.lpszClass, cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hInstance, NULL);
if (ret == NULL) {
MessageBox(NULL, dwl::lastSysError().c_str(), L"Error to report:", MB_OK);
}
return ret;
}
菜单
创建DWinLib
最大的痛苦之一是封装Windows的菜单API。微软的工程师们创造了一个递归的怪物,任何头脑清醒的人都不会想深入研究。但它们确实有效,而且比我不太喜欢的“ribbon”系统更好。(ribbons让事情更难找到,而不是更容易,除非您的整个程序只有一个“ribbon”功能。不过,后台代码可能更干净,这可能是微软弃用菜单的真正原因。)
基本上,DwlMenu
是Windows菜单函数的一个薄层封装。如果您对这种笨拙感兴趣,请随意查看“DwlMenu.cpp”及其头文件。您还可以通过深入研究下面的StupidSquares
示例代码来查看它的实际运行情况。
异常
在类概述图之前,我提到了错误处理,这是一个很好的时机来重复在另一篇文章中给出的一些信息。最初,由于我的C++经验,DWinLib
在构造函数中创建所有内容。但它们从未正确触发。
在我集成Francisco Campos的SWC框架时,我注意到他使用两步创建过程。构造函数从不做任何抛出异常的事情,而实际的创建步骤在instantiate
方法或等效方法中执行。这促使我意识到我原来的方法在堆栈展开过程中导致了更多的异常发生,这使得事情变得一团糟。查看MFC,我相信它也使用了两步过程。由于我的经验,我终于明白了这种安排的原因,并修改了DWinLib
以做同样的事情。
关于异常的另一点需要提及的是,它们不会跨越WindowProc
和Application::run()
函数之间的屏障。DispatchMesage
不会在消息处理程序中“捕获”异常。您必须在那里捕获它们,尽管我相信我在run()
函数中保留了一个try ... catch
块,这可能是不必要的。
SDI/MDI问题
严格来说,并没有真正的“问题”,只是一个需要注意的地方。
如果您创建一个SDI应用程序,您可以通过主窗口的hwndC
直接控制主应用程序窗口。对于MDI程序,DWinLib
会实例化一个MDI客户端窗口,您需要绘制它,而不是主应用程序的hwndC
。您可以逐步查看示例MainAppWin
单元中的逻辑。
将Windows消息与特定窗口关联
接下来要克服的障碍是将消息链接到正确的窗口。DWinLib
的解决方案与我见过的其他封装器略有不同。
如上所示,实际的转发发生在静态的Application::WndProc
(或者DWinLib
目前的winProc
)中。这是您所有程序窗口的主要回调。(请记住:标准Windows控件拥有自己的过程,因此它们不会在没有额外工作的情况下调用Application::winProc
。并且不推荐创建这样的代码。)前一个winProc
函数中,将消息路由到程序中适当窗口的神奇行是“if (win) return win->winProc(window, msg, wParam, lParam);
”。
此时,可执行文件会访问正确的dwl::BaseWin
对象的虚函数表,并执行其winProc
过程,同时发送消息参数。
这是DWinLib
在与某些封装器比较时闪光的一个领域。这些消息在winProc
过程中被“解封装”,使得处理它们的代码_大大_简化了。
这背后的技巧只是一堆繁琐的工作。此代码片段说明了该技术
LRESULT BaseWin::winProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) {
//Per Microsoft, all exceptions must be handled before returning control back to
//the message pump. For some information on this, see the bottom part of the article:
//http://www.microsoft.com/msj/archive/S204D.aspx
try {
wParamC = wParam; //The windows derived from BaseWin can access the most current
lParamC = lParam; //wParam and lParam variables through these class members
msgC = msg;
switch (msg) {
case WM_ACTIVATE:
return wActivate(LOWORD(wParam), HIWORD(wParam), (HWND)lParam);
case WM_CHAR:
return wChar(wParam, lParam);
case WM_CLOSE:
return wClose();
case WM_COMMAND:
return wCommand(HIWORD(wParam), LOWORD(wParam), (HWND)lParam);
//...
case WM_LBUTTONDOWN:
return wMouseDown(Button::Left, wParam, Point(GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam)));
//...
然后,wMouseDown
简单地定义为
virtual LRESULT wMouseDown(Button button, WPARAM flags, Point p);
尤里卡!您不再需要使用晦涩的“LOWORD
”和“HIWORD
”语法编写处理程序了。太棒了!“LeftButton
”肯定比“wParam
”好!
我想我应该简单说几句(或三句)关于匈牙利命名法的话。我讨厌它。
您可能会从之前的代码中得到不同的印象。但那些理解匈牙利命名法的人知道“MouseDown
”前面的“w
”并未以匈牙利方式使用。
相反,它仅仅表示该函数处理“Windows”消息。这个字母也可以表示所附变量与Windows紧密关联。要了解原因,请查看dwl::BaseWin
中处理的所有消息。试着想象一下,如果没有一个约定来区分这些函数,您的代码会是什么样子。您将有一个“enable
”例程单独漂浮在空中,几乎没有信息来传达其目的。天哪,您以后想将“enable
”这样的逻辑名称用于其他用途时该怎么办。
尽管DWinLib
封装在“dwl
”命名空间中,但在.cpp文件的顶部放置“using
”语句会再次混淆事物,所以我为了清晰起见保留了前缀“w
”。
我的另一个标准是类成员变量名末尾的大写“C”。我同意许多程序员的观点,指出这些是必需的,但前缀“m_
”只是一个丑陋的怪物,而且比“C
”更难输入。而且,成员变量的重要性并没有高到必须放在含义之前。换句话说,我的大脑更好地理解“我封装了wParam
的思想,而且我恰好是一个成员变量”,而不是“我是一个成员变量!它封装了wParam
的思想。”(加粗是为了表示“m_
”以这种方式跳出来给我看,并淹没了主要信息。)但也许我的灰色细胞与其他人不同。无论如何,主要的是逻辑被写出来,并且功能正常。
当然,问题就变成了“为什么‘w
’不在变量名之后呢?”那可能是匈牙利命名法影响的遗留。当我采用这个约定的时候,我并没有仔细考虑这个问题。我还使用‘g
’来表示‘全局变量’,而且只有这两个前缀,代码在我看来很干净。也许有一天,我会把所有这些都移到单词的末尾,但我必须承认我喜欢当前方案的字母顺序方面,所以也许我不会。
响应用户输入 - 回调
让我们回到代码,而不是理论。我们已经看到了消息是如何分派到适当的类的,以及用于解包WPARAM
和LPARAM
的机制,以简化编程任务。接下来要仔细研究的是DWinLib
的回调功能。当您在程序中按下按钮时,这就是将事件路由到相应处理程序的引擎。
无论有没有封装器,按钮的父窗口都会收到一个包含控件ID的WM_COMMAND
消息。在纯API代码中,该数字必须由您或您的资源编辑器分配。DWinLib
在幕后处理它们,您很少需要考虑ID。
在Win API代码中,只需在WM_COMMAND
逻辑中测试ID值并作出相应响应。这很简单,但当代码库变大时,整个过程会变得非常繁琐。
DWinLib
大大简化了这项任务。按钮和其他控件项都被封装在派生自dwl::ControlWin
的类中。该类包含一个dwl::CallbackItem
作为成员。每当创建这些对象之一时,它就会在DwlBaseApp
单元中实例化的程序范围的CallbackList
中注册自己。注册后,它会收到一个控件ID。您不再需要考虑这些数字。
解决问题的下一部分深受Borland Builder的影响。在其中,要让按钮调用类实例,只需分配一个“OnClick
”处理程序即可完成。除了函数的实际编码。
当我最初移植我的程序以解决我遇到的bug时,我想要一个类似于Builder的简单易用的回调机制,这样我就不必完全重做处理的这个方面以及所有其他方面。不可移植的扩展,例如它们的“__closure
”(或任何术语)是不可接受的。
我的第一个解决方案涉及static
函数和基类指针,我很自豪地说我让这个设计工作起来了,但我足够诚实地承认它并不漂亮。我已经忘记了细节,但非static
查找的方法,尽管可行,但我不会让你的纯洁眼睛看到。
实际上,翻出一些旧代码,它并没有我表现得那么糟糕。我改变主意了——你的眼睛能承受。这是很久以前一些代码中“重做”过程的开始
void MainWin::redo(WinObject * ) { //This is a static function
Performance * perf(gWinMain->perfKeeperC->activePerf());
if (perf) perf->redo();
}
如果正在响应按钮按下,则可以将“WinObject
”指针dynamic_cast
为DwlButton
,如果我需要额外的项,按钮有一个void *
“user”成员变量来保存或指向我想要的任何内容。我从不喜欢涉及到的void *
和static_casting
方面,但这种组合以最少的编码解决了我的问题。
最终,我发现了Sergey Ryazanov的Impossibly Fast C++ Delegates,经过一番思考,我发现它们最终将允许用户直接回调到实例化类,而无需通过静态函数并访问全局指针来启动实际的函数查找。三天后,一次错误的开始,这个更改在BCB、Dev-C++和VS上都奏效了。
解决方案涉及创建一个委托,其外观如下
namespace sr { //'sr' for 'Sergey Ryazanov', of course! (Even though I've made a few
//modifications to the class)
//There is also a plain 'Dwlegate'
//in DWinLib that doesn't require a <code>dwl::Object*</code>
//as an argument, which is why I've kept the 'Dwl' prefix here.
class DwlDelegate {
private:
#ifdef DWINLIB_C11_ENABLED
std::function<void (dwl::Object *)> funcC;
#endif
typedef void (*MemberFunctionAddress)(void* instanceOfObject, dwl::Object * arg);
MemberFunctionAddress functionAddressC;
void * instantiatedObjectC;
dwl::Object * userDwlObjC;
template <class T, void (T::*Method)(dwl::Object*)>
static void delegateFor(void* instantiatedObject, dwl::Object * userDwlVariable) {
T* p = static_cast<T*>(instantiatedObject);
return (p->*Method)(userDwlVariable);
}
public:
DwlDelegate() : instantiatedObjectC(0), userDwlObjC(0), functionAddressC(0),
#ifdef DWINLIB_C11_ENABLED
funcC(nullptr)
#endif
{ }
#ifdef DWINLIB_C11_ENABLED
DwlDelegate(const std::function<void (dwl::Object *)> & func, dwl::Object * obj) :
funcC(func), userDwlObjC(obj), functionAddressC(0),
instantiatedObjectC(nullptr) { }
#endif
~DwlDelegate() { }
template <class T, void (T::*Method)(dwl::Object*)>
static DwlDelegate create(T* instantiatedObject, dwl::Object * userDwlVariable) {
DwlDelegate d;
d.functionAddressC = &delegateFor<T, Method>;
d.instantiatedObjectC = instantiatedObject;
d.userDwlObjC = userDwlVariable;
return d;
}
void operator()() const {
#ifdef DWINLIB_C11_ENABLED
if (funcC) return funcC(userDwlObjC);
else return (*functionAddressC)(instantiatedObjectC, userDwlObjC);
#else
return (*functionAddressC)(instantiatedObjectC, userDwlObjC);
#endif
}
void object(dwl::Object * p) { userDwlObjC = p; }
void func(const std::function<void(dwl::Object*)> & func, dwl::Object * obj) {
funcC = func;
userDwlObjC = obj;
}
bool callable() { return instantiatedObjectC || funcC; }
};
}
这段代码相当接近Sergey最初的设计,剥离到其最基本的部分。不过,它远非Sergey的完整作品,后者允许创建多签名委托而无需更改代码库。BCB 4.0无法编译Sergey大师级创作的全部内容。以上修改允许您创建一个函数委托,其签名为“void SomeClass::Function(dwl::Object * sender);
”。
除了Delegate
类,还需要创建一个CallbackList
,它存储控件ID列表以及与每个ID关联的委托。
当创建一个控件时,它包含一个CallbackItem
,该项从CallbackList
“获取”下一个可用的ID号。CallbackItem
还负责在CallbackList
中注册和注销控件。
作为一点旁注,如果您需要一个分配数字、跟踪已分配数字并重新分配不再使用的数字的算法,请查看CallbackList::insert
和CallbackList::remove
背后的处理。我不能保证这是最快或最好的可用方法,但到目前为止,它还没有给我带来任何问题。
除此之外,BaseWin
窗口收到了以下wCommand
处理器
LRESULT BaseWin::wCommand(WORD notifyCode, WORD ctrlId, HWND hwnd) { //ctrlHWND
//Sometimes, during creation of a control, Windows sends WM_COMMANDS with 'notifyCode'
//== 1024 and other values. What these are doing is unknown, although it is suspected
//that they are 'commanding' the parent window to register this control in it's list
//of children inside of Windows itself.
if ((notifyCode == 0 || notifyCode == 1) && ctrlId < DWL_MDI_FIRSTWIN) {
if (gDwlGlobals->dwlApp->winCallBackListC.hasHandlerFor(ctrlId)) {
gDwlGlobals->dwlApp->winCallBackListC.performCallBack(ctrlId);
return 0;
}
return DefWindowProc(hwndC, WM_COMMAND, (notifyCode<<16)|ctrlId, (LRESULT)hwnd);
}
else {
#ifdef DWL_MDI_APP
if (wUseDefProcInMdiC) return DefWindowProc(hwndC, msgC, wParamC, lParamC);
return DefFrameProc(hwndC, gDwlGlobals->dwlMainWin->mdiClientHwnd(), msgC, wParamC,
lParamC);
#else
return 0;
#endif
}
}
你可以看到,当你的某个从这个类派生的窗口被Windows调用时,WinCallbackList
会自动被调用。正如你可能推断的,在不了解其工作原理之前,不要覆盖或更改这个wCommand
过程是个不明智的决定。
深入研究设计,CallbackList::performCallback
查找与传入控件ID关联的Delegate
,并告诉该委托执行自身。
示例应用程序展示了所有这些是如何工作的,但请允许我花一点时间将一些与按钮相关的代码纳入本文。希望程序员需要做的事情会变得清晰。
[意识到程序不包含按钮,于是奋力修复问题。并且,为了以防万一,按钮的“user
”变量已经“示例化”了。]
//First, add a button to ObjViewWin.h, and add a function matching the default
//callback signature to the class. Technically, the callback can be in another class,
//although you need to ensure an instantiated object pointer to that class is passed
//into the Delegate.
class ObjViewWin : public dwl::DockWindow {
private:
//...
std::unique_ptr<dwl::Button> buttonC;
public:
//...
void onButtonClick(dwl::Object * obj);
//...
//Then, in the .cpp file, initialize the button in the constructor with:
//...
buttonC.reset(new dwl::Button
(this, CreateDwlDelegate(ObjViewWin, onButtonClick, this, this)));
//...
//And instantiate it in 'instantiate':
void ObjViewWin::instantiate(dwl::DockManager * dockManager,
dwl::DockState state, dwl::DockEdge edge,
int dockDim, int minDockDim, int maxDockDim) {
//...
buttonC->instantiate(38, 174, 100, 24, _T("Button Test"));
buttonC->user = wString(_T("Howdy!"));
//...
//And the callback:
void ObjViewWin::onButtonClick(dwl::Object * obj) {
dwl::Button * test = d_cast<dwl::Button*>(obj);
//Set a breakpoint below here to see that 'test' is not nullptr.
//I could have used it below instead of the 'buttonC' (ie., test->user.isA<wString...)
//and got the same thing (just move the comment to try it).
dwl::msgBox(_T("Button Test is a success!"));
if (buttonC->user.isA<wString>()) {
//if (test->user.isA<wString>()) {
dwl::msgBox(buttonC->user.cast<wString>());
}
}
我相信之前的代码片段是相当不言自明的。一个按钮派生自dwl::Control
,并且所有dwl::Control
都有一个名为“user
”的“any
”对象。在这种情况下,“user
”持有一个wString
,它是一个根据Unicode定义封装了std::string
或std::wstring
的封装器。“ObjViewWin::onButtonClick
”被设置为处理按钮点击,并在MessageBox
中显示wString
。CreateDwlDelegate
部分是与Borland的VCL差异最大的方面,但总的来说,代码仍然清晰易懂。
这种方法的一个有趣替代方案是Sarah Thompson 的 Signal and Slot 实现。我从未使用过她的工作或任何其他 signal/slot 库,但它们听起来很有趣,尽管我不知道它们是否比前一种方法的最终结果更简洁。从我的阅读来看,如果您愿意,将信号链接在一起相当容易,这可能是一个优势。但是,同样的事情可能可以通过回调来实现,即调用一个函数,该函数又调用所有将被链接的函数。
另一个选择是使用Lambda函数。为了好玩,我将它们添加到了上面所示的Delegate
类中,所以如果您愿意,可以将CreateDwlDelegate
行替换为以下内容
buttonC.reset(new dwl::Button(this, [&](dwl::Object *) { onButtonClick(this); }));
停靠工具栏
接下来值得简要说明的是停靠工具栏。我不会深入探讨它们背后的机制,因为它们最初是根据Catch22网站上提供的优秀C语言教程开发的,我的代码相当不言自明。当我第一次编写此页面时,我偶然发现了Jeff Glatt的纯C语言停靠工具栏。由于其措辞,我以为这是James作品的直接改编,但在2005年8月1日为修订版编辑此文时,我有理由更深入地研究它,发现Jeff对代码本身做了一些实质性的更改(所有这些都非常好),结果我采用了他的方法来使停靠窗口可调整大小。
我要提一下,我大大改进了停靠工具栏的运行方式,并将其打磨到窗口调整大小时几乎没有(或根本没有)闪烁或跳动(在旧的Windows主题中)。据我所知,没有其他库实现了这一壮举。
简而言之,秘诀在于处理WM_ERASEBKGND
和WM_PAINT
消息,并将项目父级到MDI客户端而不是主窗口HWND
。当您从头开始创建控件时,也请这样做(当然,除了父级),并在WM_PAINT
处理程序中进行所有双缓冲绘制。
找出消除这些闪烁源所需的一切,并不是我生活中最有趣的经历之一,它让我对查看停靠窗口代码感到非常厌倦。为了克服所有相关困难,该部分经历了 *四* 次重大编辑。
然而,不要让上述吓到你。如果你决定从头开始创建一个无闪烁的控件(不是停靠工具栏,只是一个常规控件),那并不是一件痛苦的事情,只要你不是在封装一个Windows控件。(但如果你是,ControlWin
类将大大简化你的任务。)你可以在DWinLib子目录的StringGrid
单元中看到一个非WCC(非Wrapped Child Control)的例子。
回到停靠工具栏,我应该提到,尽管我为我的工作感到自豪,但它们仍然可以改进。如果工具栏能够自动停靠,而不是等待您在热区中释放鼠标按钮,那会很好。此外,David Nash框架中的机制也将是一个很好的补充。
如果您自己尝试停靠工具栏的实现,有一个小小的复杂性值得注意:有三种方法可以启动停靠窗口的销毁。第一种是手动调用相应停靠程序的析构函数。第二种是通过使用其“关闭”按钮(它会调用停靠程序的“wClose
”例程)在停靠程序浮动时关闭它。第三种是在停靠程序可见时终止程序。所有这些都有不同的控制流(最后一种从FloatingWindow::wNcDestroy
开始,或者如果您已覆盖,则从您的覆盖版本开始)。
所附的可执行文件(以及其他项目中的可执行文件)展示了这些方法的实际应用,并且停靠程序会正确地销毁自己。不过,需要注意的是。我尚未测试在工具栏窗口浮动时手动“删除”它。相反,我建议调用wClose
例程,我知道它会启动适当的事件链
void MainAppWin::toggleToolbar() {
if (toolbarC) {
toolbarC->wSendMessage(WM_CLOSE, 0, 0);
//Did not notice same problem as with objViewBar, but to ensure the MDI
//client gets refreshed correctly:
mdiClientC->invalidateRect();
}
else {
toolbarC = new ToolbarWin(this, _T("Toolbar"));
toolbarC->instantiate(&dockManagerC, DockState::Docked, dwl::DockEdge::Top, 40, 30, 100);
}
}
至此,我们的停靠窗口讨论就此结束!
子控件和事件
封装子控件(WCC - Windows标准控件,如编辑框、组合框等)带来了它们自己的问题,直到我的设计探索结束时才变得简单。
一开始,自然而然地为每种类型的控件单独封装,因为那是快速完成事情的方法。随着设计的进展,记住各个设计的细节变得笨拙且令人不安,这种厌恶促使我将所有内容整合到一个共同的winProc
中。最初的几次迭代令人恼火,因为它们不知何故充满了特殊情况,并继续让我头疼。
这个最新的迭代(6.04,2021年1月)最终通过一种集中、简单的方法克服了设计上的困境,而没有过去那些边缘情况的管理不善。
在其中,所有WCC都派生自ControlWin
类。该类拥有自己的winProc
方法,该方法实际上非常简单。并且被覆盖的控件处理它们自己的特殊需求,而不需要污染那个winProc
。
要实例化一个控件,创建一个派生自ControlWin
的类。使用构造函数设置其宽度、高度和位置,以及任何其他可以不抛出异常完成的操作。然后,在其“instantiate
”方法中,创建一个将创建所需控件类型的CREATESTRUCT
。将该CREATESTRUCT
通过引用传递给ControlWin::instantiate
,并带有一个错误字符串,以便在创建失败时使用。就这样!
ControlWin::instantiate
负责设置子类化的窗口过程。您可以通过检查ControlWin
代码以及DWinLib
中派生自ControlWin
的Button
、CheckBox
、编辑框和其他类来了解其工作原理。
您还可以检查这些派生类,了解如何与这些控件进行交互。有些操作需要通过父窗口的wCommand
函数来处理,因为Windows通常以这种方式通知它们。
如果您需要创建事件处理程序,StupidSquares
代码库中有一个示例可以作为您自己模型的参考。
我所说的“事件处理程序”,并不是指像wPaint
和其他Windows消息处理程序那样的常规例程。尽管它们确实是“事件处理程序”,但我指的是用户可设置的函数,您可以让窗口过程动态地钩入这些函数。就像Microsoft Access中的事件处理程序一样。
StupidSquares
在其ObjViewWin
中有一个文本框,用于显示与当前选定的“笨方块”相关的文本。您可以编辑该文本,当文本框失去焦点时,会触发textBoxKillFocusProc
例程。textBoxKillFocusProc
可以动态地由用户设置,如果需要,您可以通过程序逻辑选择五个或更多不同的void someFuncName(dwl::Object* arg)
函数。
这是通过ControlWin::winProc
过程中的一个Delegate
实现的,它与DwlDelegate
不同之处在于它不带任何参数,而DwlDelegate
带有一个dwl::Object*
。(为了克服Delegate
名称的污染,您可以将我对Sergey Ryazanov作品的覆盖替换为他的原始作品,或者使用Sergey Alexandrovich Kryukov的作品,后者添加了几个特性。)
class ControlWin : public Control {
//...
protected:
sr::DwlDelegate onKillFocusC;
public:
void onKillFocus(const sr::DwlDelegate & del) { onKillFocusC = del; }
protected:
std::function<LRESULT(WORD, WORD, HWND)> onCommandC = nullptr;
public:
void onCommand(const std::function<LRESULT(WORD, WORD, HWND)> & func) {
onCommandC = func;
}
//AN EXAMPLE USAGE:
//AFTER CONSTRUCTING A COMBOBOX:
//comboBoxC->onCommand([&](WORD notifyCode, WORD ctrlId, HWND hwnd) {
// return comboCommands(notifyCode, ctrlId, hwnd);
// } );
};
请注意,代码中实际上有两种不同的事件处理程序,还有一个onCommand
处理程序,我将省略这篇概述的其余部分(因为我已经阐明了lambda function
的用法,并且代码注释澄清了这种情况)。
这个onKillFocus
是在wKillFocus
过程(它处理WM_KILLFOCUS
消息)中触发的
LRESULT ControlWin::wKillFocus(HWND receiveFocusWin) {
if (onKillFocusC.callable()) {
onKillFocusC();
return 0;
}
return CallWindowProc(origProcC, hwndC, WM_KILLFOCUS, (WPARAM)receiveFocusWin, 0);
}
而这反过来又调用了在ObjViewWin
的构造函数中设置的函数
textBoxC->onKillFocus(CreateDwlDelegate
(ObjViewWin, textBoxKillFocusProc, this, textBoxC.get()));
非常简单!需要注意的一点是,您可能需要在应用程序窗口的wMouseDown
处理程序中添加一个SetFocus
命令,就像我在本示例中所做的那样,以便使编辑框的onKillFocus
事件触发。
我希望这能为您处理任何其他事件提供足够的参考。
空闲处理
如果您需要在程序空闲时做一些事情,您可以将一个BaseWin*
添加到DWinLib
的空闲处理列表中
gDwlGlobals->dwlApp->idler().addToIdleList(this);
一旦完成,DWinLib
将为该BaseWin
调用wIdle
进程。目前,这被设置为一次性事件——DWinLib
将继续调用该wIdle
函数,直到它返回false
。在此之前,您的程序将变得无响应。
要了解其工作原理,请查阅本文顶部的Application::run
列表。DwlApplication.cpp和头文件中的dwl::Idler
是负责该处理的小类。
在幕后,DWinLib
使用此功能来处理停靠工具栏的删除,因此您可以在DWinLib
本身中看到使用示例。
构建DWinLib程序
如果您正在Visual Studio中创建一个新的DWinLib
程序,最简单的方法可能是将其中一个示例程序目录复制到新名称。打开项目后,您需要删除非DWinLib
库项目中的所有文件,并通过将它们从新目录拖到解决方案资源管理器层次结构中来重新添加它们。如果您没有将新目录放在与示例程序相同的父目录中,您将需要更改配置属性 -> C/C++ -> 常规 -> 附加包含目录中的相对目录位置。然后右键单击“PrecompiledHeader.cpp”文件,将其配置属性 -> C/C++ -> 预编译头文件属性更改为“创建 (/Yc)”。然后您应该能够编译它并开始修改过程。
如果您想从头开始,可以创建一个空白的 C++ 项目。如果您不想使用 DWinLib
库,可以将 DWinLib
目录中的所有 DWinLib 文件(而不是 ReducedFlickerControlTests 子目录)拖到解决方案资源管理器中。此外,从示例中找到与您所需程序类型最相似的 C++ 文件,并将它们复制到一个新的工作目录,然后将它们拖到解决方案资源管理器中,最好拖到一个新的过滤器中,以便保持组织性。
接下来,您需要更改以下属性
- 配置属性 -> C/C++ -> 常规 -> 附加包含目录
- 配置属性 -> C/C++ -> 预处理器 -> 预处理器定义需要根据示例程序进行修改。
- 配置属性 -> C/C++ -> 语言 -> 符合性模式需要设置为“否”。
- 配置属性 -> C/C++ -> 预编译头需要为项目设置为“使用”,为PrecompiledHeaders.cpp文件设置为“创建”。
- 配置属性 -> 链接器 -> 系统 -> 子系统需要更改为“Windows (/SUBSYSTEM:WINDOWS)”。
- 如果您愿意,也可以在配置属性 -> 高级 -> 字符集设置中更改 Unicode / MBCS 字符集。
以及您希望的任何其他内容。
如果您想使用库,请遵循示例。许多以前的设置都需要在非库项目上设置。请确保库和主项目的值最终相同,否则您将遇到一些非常困难的调试问题。(库二进制布局将与主项目的预期二进制布局不符,因为宏展开差异。您会发誓您在某个地方“new”了某个东西并得到了一个指针值作为其位置,但在库中,它将具有不同的指针值,或NULL
。这极其令人沮丧!)
这些步骤已在更长的文章中详细说明,供希望获得更详细解释的人参考。
这是前面提到的StupidSquares
示例
- 下载源代码 - 135 KB(包括Release目录中的.exe)
结束语
以上是回顾DWinLib
开发时想到的重要事项。我希望您发现这篇文档对更好地理解Windows内部工作原理和/或以某种方式改进您自己的代码和/或出于求知欲而有所帮助。
一如既往,祝您编码愉快!
历史
1/16/2021
改进了dwl::ControlWin
控件,并改进了DWinLib
的许多方面。
2/13/13
完全改造了MDI例程,使其符合标准Windows方法。通过诸如CharArrayWrappers.cpp等单元改进了Unicode功能,因此如果wString
主要是美式英语且没有区域设置问题,则可以轻松地在string
/wstring
之间进行转换。(另请参阅DwlAsciiAndUnicodeFileWrapper.cpp以了解文件读写功能。)重写了整个DWinLib
系列文章,使其逻辑上从一个主题流向下一个主题。
8/1/05
在此向David Nash致以崇高的谢意。他花费时间让DWinLib
在Visual Studio .NET 2003中运行,并寄回了一份副本。这是一项艰巨的工作,我非常感激,同时也感谢他给我的出色反馈,帮助我进一步改进DWinLib
和本文。由于他的努力,我得以在Dev-C++上编译DWinLib
,因此如果您无法访问VS,仍然可以使用一个很棒的免费编译器来玩弄代码。
(在使用过Eclipse和Dev-C++之后,我对Dev-C++印象更深刻,因为它的项目管理似乎比Eclipse简单得多,也更容易直观理解。您不必在“工作区”中工作,而且Eclipse的“工作区”让我咒骂了大约两个小时,这可能是因为我的大脑迟钝,或者是Eclipse的工作区确实像我印象中那样糟糕。此外,我还遇到了一些其他问题,对于初学者来说并不容易理解。大约在2002年我尝试过之后,我放弃了。)
David还表示他曾尝试在VC6上使其工作,但由于该编译器不符合标准(并非由于非标准代码),他未能成功。我猜测这与到处使用“for (int i=0; i<something...
”有关,我拒绝回到不当作用域的变量。
另一个重大改变是DWinLib
现在启用了“Unicode”。再次感谢David引导我走上这条道路。
最后一次修改是针对回调部分中概述的回调机制。
DWinLib
和本文还有其他各种细微修改,我将不详细介绍。