WinLamb: 使用 C++11 Lambda 处理 Win32 消息






4.83/5 (35投票s)
介绍 WinLamb,一个现代 C++11 面向对象库,用于编写原生 Windows 程序
目录
1. 剧透
首先,本文假设读者熟悉原生 Win32 编程和 C++11。
在解释它是什么之前,我将首先展示一个 Win32 程序使用 WinLamb 的样子。以下是一个具有单个窗口的完整程序。请注意,没有消息循环、没有窗口类注册、没有 switch 语句或消息映射。并且处理了两个消息,每个消息都带有一个 C++11 lambda。
// Declaration: SimpleMainWindow.h
#include "winlamb/window_main.h"
class SimpleMainWindow : public wl::window_main {
public:
SimpleMainWindow();
};
// Implementation: SimpleMainWindow.cpp
#include "SimpleMainWindow.h"
RUN(SimpleMainWindow);
SimpleMainWindow::SimpleMainWindow()
{
setup.wndClassEx.lpszClassName = L"SOME_CLASS_NAME";
setup.title = L"This is my window";
setup.style |= WS_MINIMIZEBOX;
on_message(WM_CREATE, [&](wl::params p)->LRESULT
{
set_text(L"A new title for the window");
return 0;
});
on_message(WM_LBUTTONDOWN, [&](wl::params p)->LRESULT
{
set_text(L"Window clicked!");
return 0;
});
}
要编译此代码,您需要一个 C++11 编译器。本文中的所有示例均已使用 Visual C++ 2017 编译和测试。
2. 入门
2.1. 概念
Charles Petzold 在其经典著作 Programming Windows 中深入描述了创建原生 Windows C 程序的原始和通常方式。从那时起,许多面向对象的库——如 MFC 和 WTL——都被编写出来,提供了 C++ 方法来处理原生 Windows 编程。
WinLamb——一个 Windows 和 lambda 的无灵感缩写——是另一个面向对象的 C++ 库。它是一个仅头文件的库,除了纯 Win32 和 C++ 标准模板库之外,不依赖任何东西,并且它大量依赖于现代 C++11 特性(实际上,也依赖 C++14 和 C++17)。
WinLamb
是 Win32 API 之上的一薄层。它可以分为三个主要部分:
- 窗口管理:允许您创建各种类型窗口并使用 lambda 处理其消息的基础设施——这是库最重要的部分。
- 大多数原生 Windows 控件(如编辑框、列表视图等)的基本包装器。
- 实用程序类,如文件 I/O、设备上下文、互联网下载、COM 包装器等。
在上述列表中,(2) 和 (3) 是可选的。如果您有其他库或您自己的一组类,则可以使用它们代替。
2.2. 技术考量
WinLamb
完全使用 Unicode Win32 实现,随处可见 wchar_t
和 std::wstring
。
安装非常简单:由于该库是仅头文件,您只需下载文件并将其 #include
到您的项目中。它们应该立即生效。所有类都包含在 wl
命名空间中。
由于它大量依赖 C++11 甚至一些 C++14 和 C++17 功能,因此您需要一个兼容的 C++ 编译器。当前版本是使用 Visual C++ 2017 开发和测试的。
错误通过抛出异常来报告。WinLamb
没有任何自定义异常,它只抛出普通 STL 异常。所有异常都继承自 std::exception
,因此如果您捕获它,就保证能捕获 WinLamb
中的任何可能异常。
最后但同样重要的是:WinLamb
不是不学习 Win32 的借口——在使用 WinLamb
之前,我强烈建议读者学习如何用纯 C 编写 Win32 程序的基础知识。
WinLamb
源代码可在 GitHub 上获得,作为开源项目,采用 MIT 许可证。
2.3. 设置新项目
WinLamb
是一个仅头文件的 C++ 库。最新代码可以在 GitHub 上找到,您可以克隆存储库或直接下载文件。
将库添加到您的项目中最简单的方法是将所有文件保存在一个名为“winlamb”的子文件夹中,然后在您的源代码中 #include
它们。
这里,我们将从头开始,介绍如何使用 Visual C++ IDE 创建一个全新的 WinLamb
Win32 项目。您可以跳过本节,直接查看代码。
首先,创建新项目:
选择“Windows 桌面向导”。“.NET Framework”选项无关紧要,因为我们正在编写一个纯 Win32 程序,不使用 .NET Framework。这里,我将项目命名为“example1
”。
选择“Windows 应用程序”。除了“空项目”之外,取消选中所有选项。这将为我们创建一个完全空的项目。
最后,创建一个名为“winlamb”的子目录,并将 WinLamb
文件放在其中。然后正常创建您的源文件;您现在应该能够包含 WinLamb
文件了。
请注意,该库有一个“internals”子目录。这是所有内部库文件所在的位置;您应该不需要接触这些文件。
3. 创建和使用窗口
项目准备就绪后,是时候使用 WinLamb
来包装我们的 Windows 了。
3.1. 创建主窗口
在最常见的情况下,创建 Win32 程序时必须设计的第一个东西是主窗口。因此,让我们从主窗口类的声明开始——在 WinLamb
中,每个窗口都有一个类。为每个窗口设置一个头文件和(至少)一个源文件是个好主意。技术上,主窗口不需要头文件,但为了保持一致性,我们还是写一个。
所有 WinLamb
库类都属于 wl
命名空间。我们的主窗口类将继承自 window_main
类。我们还要声明构造函数:
// Declaration: MyWindow.h
#include "winlamb/window_main.h"
class MyWindow : public wl::window_main {
public:
MyWindow();
};
对于程序入口点,如果您愿意,可以编写您的 WinMain 函数并手动实例化 MyWindow
。但是,如果您在 WinMain
中没有做任何特殊的事情,您可以简单地使用 WinLamb
的 RUN
宏——我保证这是整个库中唯一的宏——,它将简单地展开为 WinMain
调用,在堆栈上实例化您的类。这是宏调用:
RUN(MyWindow);
然后实现类构造函数。因此,到目前为止,我们的源文件中有:
// Implementation: MyWindow.cpp
#include "MyWindow.h"
RUN(MyWindow); // will generate a WinMain function
MyWindow::MyWindow()
{
}
如果您编译并运行此代码,窗口将无法显示,因为我们没有指定窗口类名。当实例化该类时,基类 window_main
将在内部调用 RegisterClassEx,并且它将使用 WNDCLASSEX 结构,其中包含一些预定值——然而,这些值没有指定要注册的类名。
基类提供了一个 setup
成员变量,该变量包含类的所有初始化值。对于经验丰富的 Win32 程序员来说,此结构的成员将非常熟悉:它们是 CreateWindowEx 函数的参数,加上 wndClassEx
成员,后者是传递给 RegisterClassEx
的 WNDCLASSEX
结构。然而,此 wndClassEx
成员隐藏了 WinLamb
在内部设置的成员。
因此,我们必须在 lpszClassName
成员中定义类名,并在构造函数中完成此操作:
// Implementation: MyWindow.cpp
#include "MyWindow.h"
RUN(MyWindow);
MyWindow::MyWindow()
{
setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
}
wndClassEx
成员具有 style
,setup
本身具有 style
和 exStyle
。这两个字段已填充默认标志,由 WinLamb
指定:
setup.wndClassEx.style = CS_DBLCLKS;
setup.style = WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN | WS_BORDER;
当然,您可以覆盖这些值。然而,它们在大多数窗口中都很常见,而且大多数时候,您只想添加一个标志。例如,如果您希望主窗口可调整大小和可最小化,您只需:
setup.style |= (WS_SIZEBOX | WS_MINIMIZEBOX);
因此,再加上窗口标题,我们有:
// Implementation: MyWindow.cpp
#include "MyWindow.h"
RUN(MyWindow);
MyWindow::MyWindow()
{
setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
setup.title = L"My first window";
setup.style |= (WS_SIZEBOX | WS_MINIMIZEBOX);
}
现在程序应该可以正常编译和运行了。是的,这是一个功能齐全的 Win32 程序,包括窗口类注册、窗口创建、消息循环分发和最终清理——所有这些基础设施代码都是透明的。
注意:WinLamb
类使用 Set/GetWindowLongPtr 和 GWLP_USERDATA
及 DWLP_USER
标志来存储上下文数据。既然您的窗口有一个类,您可以在其中拥有所有想要的成员,我真的想不出在您的代码中使用这些标志的理由,但我还是提醒您以防万一:不要使用 GWLP_USERDATA
和 DWLP_USER
来存储您的数据。
3.2. 处理消息
处理窗口消息的传统方式是在 WNDPROC 函数内使用一个巨大的 switch 语句。一些库定义宏以避免“巨大的 switch”。
在我们的程序中,使用 WinLamb
,我们将使用 C++11 lambda。window_main
基类提供了 on_message
成员函数,它接收两个参数:要处理的消息和一个处理该消息的函数。
void on_message(UINT message, std::function<LRESULT(wl::params)>&& func);
最简单的方法是内联传递一个匿名 lambda 函数。例如,让我们在主窗口类构造函数中处理 WM_CREATE 消息。
MyWindow::MyWindow()
{
setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
on_message(WM_CREATE, [](wl::params p)->LRESULT
{
return 0;
});
}
lambda 接收一个 params
参数,它包含 WPARAM
和 LPARAM
成员——稍后会详细介绍。请注意,lambda 必须返回一个 LRESULT
值,就像任何普通的 WNDPROC
消息处理一样。
注意:在普通窗口中,您必须处理 WM_DESTROY 以便调用 PostQuitMessage。WinLamb
为一些消息实现了默认消息处理,使用默认行为,因此您不必担心它们。但是,如果您需要特定的功能,它们可以被覆盖——在上面的示例中,如果我们为 WM_DESTROY
编写一个处理程序,默认库代码将完全被绕过。稍后会详细介绍。
现在,如果您恰好有两个消息需要相同的处理,on_message
也接受一个 initializer_list 作为第一个参数。
on_message({WM_LBUTTONUP, WM_RBUTTONUP}, [](wl::params p)->LRESULT
{
UINT currentMsg = p.message;
return 0;
});
这在功能上等同于:
switch (LOWORD(wParam))
{
case WM_LBUTTONUP:
case WM_RBUTTONUP:
// some code...
return 0;
}
提示:如果您的窗口处理的消息太多,类构造函数可能会变得相当庞大且难以理解。在这种情况下,将处理程序分解为成员函数会很有帮助。
// Declaration: MyWindow.h
class MyWindow : public wl::window_main {
public:
MyWindow();
private:
void attachHandlers();
void evenMoreHandlers();
};
// Implementation: MyWindow.cpp
#include "MyWindow.h"
RUN(MyWindow);
MyWindow::MyWindow()
{
setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
attachHandlers();
evenMoreHandlers();
}
void MyWindow::attachHandlers()
{
on_message(WM_CREATE, [](wl::params p)->LRESULT
{
return 0;
});
on_message(WM_CLOSE, [](wl::params p)->LRESULT
{
return 0;
});
}
void MyWindow::evenMoreHandlers()
{
on_message(WM_SIZE, [](wl::params p)->LRESULT
{
WORD width = LOWORD(p.lParam);
return 0;
});
}
为了最终使用窗口,可以使用 hwnd
成员函数来检索窗口的 HWND
。
on_message(WM_CREATE, [this](wl::params p)->LRESULT
{
SetWindowText(hwnd(), L"New window title");
return 0;
});
但是窗口中有一个 set_text
方法可用,所以:
on_message(WM_CREATE, [this](wl::params p)->LRESULT
{
set_text(L"New window title");
return 0;
});
3.3. 对话框作为主窗口
在前面的示例中,我们创建了一个普通的主窗口——在纯 Win32 中,它相当于调用 RegisterClassEx 和 CreateWindowEx,以及其他过程。
但是,也可以将对话框作为程序的主窗口。如果您将主窗口继承自 dialog_main
类,则 WinLamb
中涵盖了这种可能性:
// Declaration: FirstDialog.h
#include "winlamb/dialog_main.h"
class FirstDialog : public wl::dialog_main {
public:
FirstDialog();
};
使用对话框时,您无需直接处理 WNDCLASSEX
,也无需注册窗口类名。这就是为什么 setup
成员变量没有 wndClassEx
成员,而是允许您指定要加载的对话框资源的 ID。
通常,对话框资源是使用资源编辑器(例如 Visual Studio 的资源编辑器)创建的。对话框创建的示例可以在这里看到。现在,假设对话框资源已经创建,让我们使用对话框 ID:
// Implementation: FirstDialog.cpp
#include "FirstDialog.h"
#include "resource.h" // contains the dialog resource ID
RUN(FirstDialog);
FirstDialog::FirstDialog()
{
setup.dialogId = IDD_MY_FIRST_DIALOG; // specify dialog ID
on_message(WM_INITDIALOG, [this](wl::params p)->INT_PTR
{
set_text(L"A new title for the dialog");
return TRUE;
});
}
这里,on_message
有一些细微的差异,以遵循对话框 DLGPROC 消息处理,例如 INT_PTR
返回类型,以及返回 TRUE
而不是零。
3.4. 使用控件
在前面的示例中,假设对话框资源有一个编辑框,其资源 ID 为 IDC_EDIT1
。WinLamb
有 textbox
类,它封装了一个编辑框。要使用它,请将 textbox
对象声明为父类的成员。
// Declaration: FirstDialog.h
#include "winlamb/dialog_main.h"
#include "winlamb/textbox.h"
class FirstDialog : public wl::dialog_main {
private:
wl::textbox edit1; // our control object
public:
FirstDialog();
};
在控件对象上,调用 assign
方法,该方法将调用 GetDlgItem 并将 HWND
存储在对象内部。之后,该小部件就可以使用了。
// Implementation: FirstDialog.cpp
#include "FirstDialog.h"
#include "resource.h" // contains the dialog resource IDs
RUN(FirstDialog);
FirstDialog::FirstDialog()
{
setup.dialogId = IDD_MY_FIRST_DIALOG;
on_message(WM_INITDIALOG, [this](wl::params p)->INT_PTR
{
edit1.assign(this, IDC_EDIT1)
.set_text(L"This is the edit box.")
.set_focus();
return TRUE;
});
}
请注意,assign
和 set_text
方法都返回对象本身的引用,因此可以链式调用其他方法。这是 WinLamb
中大多数对象的常见行为。
3.5. 一个模态弹出对话框
模态弹出对话框是通过 DialogBoxParam 创建的窗口。让我们实现一个带有头文件和源文件的模态对话框。然后,我们将在父窗口中实例化这个模态对话框。
这是我们模态对话框的头文件,它继承自 dialog_modal
类:
// Declaration: MyModal.h
#include "winlamb/dialog_modal.h"
class MyModal : public wl::dialog_modal {
public:
MyModal();
};
实现方式与任何其他对话框窗口非常相似——您必须告知要加载的对话框资源的 ID——但请记住,模态对话框是通过调用 EndDialog 销毁的,该函数的第二个参数是原始对话框调用的返回值。
// Implementation: MyModal.cpp
#include "MyModal.h"
#include "resource.h" // contains dialog resource ID
MyModal::MyModal()
{
setup.dialogId = IDD_DIALOG2;
on_message(WM_COMMAND, [this](wl::params p)->INT_PTR
{
if (LOWORD(p.wParam) == IDCANCEL) // the ESC key
{
EndDialog(hwnd(), 33); // modal will return 33, see the next example
return TRUE;
}
return FALSE;
});
}
现在让我们重新审视主窗口的实现,它现在将通过实例化对象并调用 show
方法来使用模态对话框。由于对话框是模态的,show
方法将阻塞执行,并且只有在对话框关闭后才会返回。
// Implementation: MainWindow.cpp
#include "MainWindow.h"
#include "MyModal.h" // our modal dialog header
RUN(MainWindow);
MainWindow::MainWindow()
{
on_message(WM_COMMAND, [this](wl::params p)->LRESULT
{
if (LOWORD(p.wParam) == IDC_SHOWMODAL) // some button to open the modal
{
MyModal modalDlg;
int retVal = modalDlg.show(this); // blocks until return; retVal receives 33
return 0;
}
return DefWindowProc(hwnd(), p.message, p.wParam, p.lParam);
});
}
如果模态对话框要求用户输入,它通常会返回诸如 IDOK
或 IDCANCEL
等常量。
模态对话框也可以像任何类一样在构造函数中接收任何参数,并且可以有 public
方法来返回一些东西。
class MyModal : public wl::dialog_modal {
public:
MyModal(std::wstring name, int number);
std::wstring getName();
};
然后由父窗口进行实例化,这里有一个更详细的例子:
on_message(WM_COMMAND, [this](wl::params p)->LRESULT
{
if (LOWORD(p.wParam) == IDC_BTNSHOWMODAL)
{
MyModal modalDlg(L"Hello modal", 800); // instantiate the modal
if (modalDlg.show(this) != IDCANCEL) {
std::wstring foo = modalDlg.getName();
}
return 0;
}
return DefWindowProc(hwnd(), p.message, p.wParam, p.lParam);
});
模态对话框也可以弹出其他模态对话框。
3.6. 一个非模态弹出对话框
非模态弹出对话框与模态对话框不同,因为非模态对话框是通过 CreateDialogParam 创建的。这是一个声明示例:
// Declaration: MyModeless.h
#include "winlamb/dialog_modeless.h"
class MyModeless : public wl::dialog_modeless {
public:
MyModeless();
};
以及实现,与前面的例子非常相似:
// Implementation: MyModeless.cpp
#include "MyModeless.h"
#include "resource.h"
MyModeless::MyModeless()
{
setup.dialogId = IDD_DIALOG3;
on_message(WM_INITDIALOG, [](wl::params p)->INT_PTR
{
return TRUE;
});
}
非常重要:一旦创建了非模态对话框,它将与其父窗口共存——非模态对话框本身没有消息循环。因此,必须注意声明范围。非模态对象必须声明为其父对象的成员。
// Declaration: MainWindow.h
#include "winlamb/window_main.h"
#include "MyModeless.h" // our modeless dialog header
class MainWindow : public wl::window_main {
private:
MyModeless mless; // modeless as a member
public:
MainWindow();
};
这样,在我们创建它之后,变量在调用函数返回后不会超出作用域。
// Implementation: MainWindow.cpp
#include "MainWindow.h"
RUN(MainWindow);
MainWindow::MainWindow()
{
on_message(WM_CREATE, [this](wl::params p)->LRESULT
{
mless.show(this); // modeless dialog is now alive
return 0;
});
}
如果我们在 on_message
的 lambda 中声明了 mless
对象——就像我们之前在模态对话框示例中做的那样——那么在 lambda 返回后,mless
将立即超出作用域,从而在非模态窗口仍然存活时被销毁。然后,当非模态窗口处理其第一个消息时,mless
对象将不再存在。
对于 lambda 来说,作用域非常重要。
非模态对话框通过调用 DestroyWindow 销毁。默认情况下,WinLamb
使用 DestroyWindow
调用处理 WM_CLOSE
,因此如果您向非模态对话框发送 WM_CLOSE
,它将立即被销毁。
现在,如果您使用纯 Win32 使用过非模态窗口,您可能会想知道它们在窗口消息分派中引入的问题。不用担心:WinLamb
旨在内部处理这些问题——这个痛苦已经消失了。
3.7. 一个自定义控件
自定义控件是一个设计为另一个窗口子窗口的窗口。它将继承自 window_control
。
// Declaration: MyWidget.h
#include "winlamb/window_control.h"
class MyWidget : public wl::window_control {
public:
MyWidget();
};
实现可以像这样:
// Implementation: MyWidget.cpp
#include "MyWidget.h"
MyWidget::MyWidget()
{
setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_WIDGET";
setup.wndClassEx.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
setup.exStyle |= WS_EX_CLIENTEDGE;
setup.style |= (WS_TABSTOP | WS_GROUP | WS_HSCROLL);
on_message(WM_PAINT, [this](wl::params p)->LRESULT
{
PAINTSTRUCT ps{};
HDC hdc = BeginPaint(hwnd(), &ps);
EndPaint(hwnd(), &ps);
return 0;
});
on_message(WM_ERASEBKGND, [](wl::params p)->LRESULT
{
return 0;
});
}
出于上述非模态对话框示例的同样原因,您必须将子窗口声明为父窗口的成员。
// Declaration: ParentWindow.h
#include "winlamb/window_main.h"
#include "MyWidget.h" // our custom control header
class ParentWindow : public wl::window_main {
private:
MyWidget widgetFoo1, widgetFoo2; // let’s have two of them
public:
ParentWindow();
};
要创建控件,父级必须调用其 create
成员函数,该函数接收:父级的 this
指针,我们希望赋予它的控件 ID,控件大小的 SIZE
,以及父级内位置的 POINT
。
// Implementation: ParentWindow.cpp
#include "ParentWindow.h"
RUN(ParentWindow);
#define WIDG_FIRST 40001
#define WIDG_SECOND WIDG_FIRST + 1
ParentWindow::ParentWindow()
{
setup.wndClassEx.lpszClassName = L"BEAUTIFUL_PARENT";
on_message(WM_CREATE, [this](wl::params p)->LRESULT
{
widgetFoo1.create(this, WIDG_FIRST, {10,10}, {150,100});
widgetFoo2.create(this, WIDG_SECOND, {10,200}, {150,320});
return 0;
});
}
这些是 window_control
的预定义 style
值:
setup.wndClassEx.style = CS_DBLCLKS;
setup.style = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
3.8. 对话框作为控件
也可以将对话框嵌入为窗口的子项,或者作为另一个对话框的子项。要构建这样的子对话框,请将其继承自 dialog_control
类:
// Declaration: MyDlgWidget.h
#include "winlamb/dialog_control.h"
class MyDlgWidget : public wl::dialog_control {
public:
MyDlgWidget();
};
为了正常工作,控件对话框必须具有一些特定的样式——这是 Win32 的要求,而不是 WinLamb
的要求。使用 Visual Studio 资源编辑器,这些样式是:
边框:无;
控制:真;
样式:子;
可见:真
(否则将以不可见状态启动);客户端边缘
:如果要边框则为真
(将添加WS_EX_CLIENTEDGE
)
鉴于前面的例子,我认为控制对话框的实现现在是微不足道的了。
4. 消息解析
除了提供使用 lambda 处理消息的功能外,WinLamb
还提供了处理消息内容的功能。
4.1. 命令和通知处理
到目前为止,我们只使用 on_message
来处理窗口和对话框中的 Windows 消息。除此之外,还有另外两个专门的方法来专门处理 WM_COMMAND 和 WM_NOTIFY 消息。
void on_command(WORD cmd, std::function<INT_PTR(wl::params)>&& func);
void on_notify(UINT_PTR idFrom, UINT code, std::function<INT_PTR(wl::params)>&& func);
这些是简写形式,与在 WM_COMMAND
和 WM_NOTIFY
消息中手动切换具有相同的效果。
on_command(IDOK, [this](wl::params p)->INT_PTR
{
set_text(L"OK button clicked.");
return TRUE;
});
根据 NMHDR 结构,WM_NOTIFY
标识符接收两个参数——控件的 ID
和通知代码。
on_notify(IDC_LISTVIEW1, LVN_DELETEITEM, [this](wl::params p)->INT_PTR
{
set_text(L"Item deleted from list view.");
return TRUE;
});
这两个函数也接受一个 initializer_list,用于一次处理多条消息。
4.2. 消息参数解包
处理消息时,您的 lambda 总是接收一个单独的 wl::params
参数。
on_message(WM_MENUSELECT, [](wl::params p)->LRESULT
{
return 0;
});
wl::params
是一个简单的 struct
,包含 3 个成员,这些成员对于任何编写过 Win32
程序的人来说都非常熟悉。这是 WinLamb
中可以找到的声明:
struct params {
UINT message;
WPARAM wParam;
LPARAM lParam;
};
然而,对于几乎所有 Windows 消息,WPARAM
和 LPARAM
成员都包含打包数据,这些数据根据所处理的消息而变化。例如,对于 WM_MENUSELECT,它们携带的是:
WPARAM
,低位字 – 菜单项索引WPARAM
,高位字 – 项状态标志LPARAM
– 指向被点击菜单的句柄
要检索这些数据,您必须执行类型转换,提取位标志,并确保您正在做的事情是正确的。
为了减轻这个负担,WinLamb
为(希望是)所有已文档化的 Windows 消息提供了解包器。这些解包器只是从 wl::params
派生出来的 struct
,并添加了解包方法。它们被封装在 wl::wm
命名空间中。
例如,我们以 WM_MENUSELECT
为例。在以下示例中,wl::params
的声明被替换为 wl::wm::menuselect
,这就是您需要做的全部。请注意在 p
上调用的方法。
on_message(WM_MENUSELECT, [](wl::wm::menuselect p)->LRESULT
{
if (p.is_checked() || p.has_bitmap()) {
HMENU hMenu = p.hmenu();
WORD itemIndex = p.item();
}
return 0;
});
这在功能上等同于费力地手动解包数据,像这样:
on_message(WM_MENUSELECT, [](wl::params p)->LRESULT
{
if ((HIWORD(p.wParam) & MF_CHECKED) || (HIWORD(p.wParam) & MF_BITMAP)) {
HMENU hMenu = reinterpret_cast<HMENU>(p.lParam);
WORD itemIndex = LOWORD(p.wParam);
}
return 0;
});
尽可能多地使用消息解析器。它们更安全,可以节省您的时间,而且在 IntelliSense 中看起来很棒。
4.3. 解包通用控件通知
通用控件通过 WM_NOTIFY 消息发送通知,其方法与普通消息不同。对于通用控件,数据打包到 NMHDR struct
或包含它的 struct
中。
WinLamb
也有这些通知的解析器,它们封装在 wl::wmn
命名空间中。请注意这里的区别:
- 普通的 Windows 消息属于
wl::wm
命名空间 WM_NOTIFY
通知属于wl::wmn
命名空间
在 wl::wmn
中,每个通用控件都有一个嵌套命名空间,因此每个控件的通知都保持分离。这些命名空间以与通知本身相同的约定命名。例如,列表视图通知,前缀为 LVN_
,属于 wl::wmn::lvn
命名空间。
例如,我们这样解析 LVN_INSERTITEM 通知:
on_notify(IDC_LIST1, LVN_INSERTITEM, [](wl::wmn::lvn::insertitem p)->INT_PTR
{
int newId = p.nmhdr().iItem;
return TRUE;
});
请注意,p
有一个 nmhdr
成员函数。此函数将根据通知返回确切的类型;在上面的示例中,nmhdr
将返回对 NMLISTVIEW 结构的引用,该结构包含 NMHDR
。IntelliSense 将列出所有成员。
5. 子类化控件
控件子类化通常借助 SetWindowSubclass Win32 函数完成。您提供一个 SUBCLASSPROC 回调函数,它与 WNDPROC
和 DLGPROC
非常相似,并从那里处理特定消息。
5.1. 安装和消息处理
WinLamb
的控件子类化方法是实例化一个 subclass
类型的对象,然后将其附加到现有控件。例如,重新以上述 编辑控件示例 为例,让我们子类化编辑控件。首先,我们向类中添加一个 subclass
成员:
// Declaration: FirstDialog.h
#include "winlamb/dialog_main.h"
#include "winlamb/textbox.h"
#include "winlamb/subclass.h"
class FirstDialog : public wl::dialog_main {
private:
wl::textbox edit1; // our control object
wl::subclass edit1sub; // edit subclasser object
public:
FirstDialog();
};
为了处理子类中的方法,我们只需在 subclass
对象上调用 on_message
。它的工作方式与窗口和对话框相同,只是我们正在处理来自 SUBCLASSPROC
回调过程的消息。
edit1sub.on_message(WM_RBUTTONDOWN, [](wl::params p)->LRESULT
{
// subclass code...
return 0;
});
添加消息后,我们将有一个填充了处理程序的 subclass
对象,但它仍然什么都不做。我们在通过调用 assign
初始化控件之后安装子类。这是完整的实现:
// Implementation: FirstDialog.cpp
#include "FirstDialog.h"
#include "resource.h"
RUN(FirstDialog);
FirstDialog::FirstDialog()
{
setup.dialogId = IDD_MY_FIRST_DIALOG;
edit1sub.on_message(WM_RBUTTONDOWN, [](wl::params p)->LRESULT
{
// subclass code for edit1...
return 0;
});
on_message(WM_INITDIALOG, [this](wl::params p)->INT_PTR
{
edit1.assign(this, IDC_EDIT1); // init control
edit1sub.install_subclass(edit1); // subclass installed and ready
return TRUE;
});
}
请注意,在此示例中,我们是在 WM_INITIDALOG
之前添加 edit1sub
处理程序的,但这并非必需。由于 lambda 是异步调用的,因此我们附加它们的顺序并不重要。我们也可以在 WM_INITDIALOG
之后以任何顺序附加 edit1sub
消息。
subclass
对象还具有 on_command
和 on_notify
成员函数,并且可以通过调用 remove_subclass
随时将其分离。
6. 最终主题
6.1. 窗口类型总结
总结一下,这些是您的窗口可以继承的所有 WinLamb
窗口基类:
window_main
– 通过 CreateWindowEx 创建程序的主窗口,封装消息循环、窗口注册和窗口创建。dialog_main
– 通过 CreateDialogParam 使用对话框创建程序主窗口,并处理消息循环。dialog_modal
– 通过 DialogBoxParam 创建模态对话框。dialog_modeless
– 通过CreateDialogParam
创建无模式对话框。window_control
– 通过CreateWindowEx
创建自定义子窗口,作为父窗口中的控件使用。dialog_control
– 使用CreateDialogParam
创建的对话框,用作父窗口中的控件。
WinLamb
还封装了许多原生控件,例如 listview
、textbox
、combobox
等。
窗口子类化也可以自动化。
6.2. 默认消息处理
窗口类为某些消息提供了默认处理。如果您编写处理程序来处理这些消息之一,默认处理将被覆盖。以下是具有默认处理的消息列表及其功能:
window_main
– WM_NCDESTROY(调用 PostQuitMessage)dialog_main
– WM_CLOSE(调用 DestroyWindow),WM_NCDESTROY
(调用PostQuitMessage
)dialog_modal
–WM_CLOSE
(调用 EndDialog)dialog_modeless
–WM_CLOSE
(调用DestroyWindow
)window_control
和dialog_control
– WM_NCPAINT(绘制控件边框,如果有的话)
6.3. 下一步是什么?
据我所知,大约在 2002 年,我开始将所有 Win32 例程封装到类中,以便自己重复使用,节省时间。这些年来,它逐渐形成了一个真正的库,一个基于纯 Win32 的薄抽象层。看到它的人经常评论说它很好,所以在 2017 年,我决定在 GitHub 上发布它。
是的,虽然它是一个现代的 C++11 库,但实际上在我第一次发布它的时候,它已经有 15 年的历史了。
由于 WinLamb
是我个人程序所使用的库,它很可能会随着时间继续发展。重构肯定会发生,但目前的架构已经稳定多年,不太可能出现大的破坏性更改。
我在 GitHub 上有一些完整的真实世界项目,并打上了标签。所有内容均在 MIT 许可证下共享。
7. 文章历史
我将努力使本文与最新的 WinLamb
版本保持同步
- 2018.12.31 – 作用域枚举样式,更新 MSDN 链接
- 2017.11.19 – 库重构,原生控件包装器合并
- 2017.04.26 – 第一个公开发布版本