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

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

2017年4月26日

CPOL

18分钟阅读

viewsIcon

45061

downloadIcon

19

介绍 WinLamb,一个现代 C++11 面向对象库,用于编写原生 Windows 程序

目录

  1. 剧透
  2. 入门
    1. 概念
    2. 技术考量
    3. 设置新项目
  3. 创建和使用窗口
    1. 创建主窗口
    2. 处理消息
    3. 对话框作为主窗口
    4. 使用控件
    5. 一个模态弹出对话框
    6. 一个非模态弹出对话框
    7. 一个自定义控件
    8. 对话框作为控件
  4. 消息解析
    1. 命令和通知处理
    2. 消息参数解包
    3. 解包通用控件通知
  5. 子类化控件
    1. 安装和消息处理
  6. 最终主题
    1. 窗口类型总结
    2. 默认消息处理
    3. 下一步是什么?
  7. 文章历史

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 程序的原始和通常方式。从那时起,许多面向对象的库——如 MFCWTL——都被编写出来,提供了 C++ 方法来处理原生 Windows 编程。

WinLamb——一个 Windows 和 lambda 的无灵感缩写——是另一个面向对象的 C++ 库。它是一个仅头文件的库,除了纯 Win32 和 C++ 标准模板库之外,不依赖任何东西,并且它大量依赖于现代 C++11 特性(实际上,也依赖 C++14 和 C++17)。

WinLamb 是 Win32 API 之上的一薄层。它可以分为三个主要部分:

  1. 窗口管理:允许您创建各种类型窗口并使用 lambda 处理其消息的基础设施——这是库最重要的部分。
  2. 大多数原生 Windows 控件(如编辑框、列表视图等)的基本包装器。
  3. 实用程序类,如文件 I/O、设备上下文、互联网下载、COM 包装器等。

在上述列表中,(2) 和 (3) 是可选的。如果您有其他库或您自己的一组类,则可以使用它们代替。

2.2. 技术考量

WinLamb 完全使用 Unicode Win32 实现,随处可见 wchar_tstd::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 中没有做任何特殊的事情,您可以简单地使用 WinLambRUN 宏——我保证这是整个库中唯一的宏——,它将简单地展开为 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 成员,后者是传递给 RegisterClassExWNDCLASSEX 结构。然而,此 wndClassEx 成员隐藏了 WinLamb 在内部设置的成员。

因此,我们必须在 lpszClassName 成员中定义类名,并在构造函数中完成此操作:

// Implementation: MyWindow.cpp

#include "MyWindow.h"
RUN(MyWindow);

MyWindow::MyWindow()
{
  setup.wndClassEx.lpszClassName = L"HAPPY_LITTLE_CLASS_NAME";
}

wndClassEx 成员具有 stylesetup 本身具有 styleexStyle。这两个字段已填充默认标志,由 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/GetWindowLongPtrGWLP_USERDATADWLP_USER 标志来存储上下文数据。既然您的窗口有一个类,您可以在其中拥有所有想要的成员,我真的想不出在您的代码中使用这些标志的理由,但我还是提醒您以防万一:不要使用 GWLP_USERDATADWLP_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 参数,它包含 WPARAMLPARAM 成员——稍后会详细介绍。请注意,lambda 必须返回一个 LRESULT 值,就像任何普通的 WNDPROC 消息处理一样。

注意:在普通窗口中,您必须处理 WM_DESTROY 以便调用 PostQuitMessageWinLamb 为一些消息实现了默认消息处理,使用默认行为,因此您不必担心它们。但是,如果您需要特定的功能,它们可以被覆盖——在上面的示例中,如果我们为 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 中,它相当于调用 RegisterClassExCreateWindowEx,以及其他过程。

但是,也可以将对话框作为程序的主窗口。如果您将主窗口继承自 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_EDIT1WinLambtextbox 类,它封装了一个编辑框。要使用它,请将 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;
  });
}

请注意,assignset_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);
  });
}

如果模态对话框要求用户输入,它通常会返回诸如 IDOKIDCANCEL 等常量。

模态对话框也可以像任何类一样在构造函数中接收任何参数,并且可以有 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_COMMANDWM_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_COMMANDWM_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 消息,WPARAMLPARAM 成员都包含打包数据,这些数据根据所处理的消息而变化。例如,对于 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 回调函数,它与 WNDPROCDLGPROC 非常相似,并从那里处理特定消息。

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_commandon_notify 成员函数,并且可以通过调用 remove_subclass 随时将其分离。

6. 最终主题

6.1. 窗口类型总结

总结一下,这些是您的窗口可以继承的所有 WinLamb 窗口基类:

WinLamb 还封装了许多原生控件,例如 listviewtextboxcombobox 等。

窗口子类化也可以自动化。

6.2. 默认消息处理

窗口类为某些消息提供了默认处理。如果您编写处理程序来处理这些消息之一,默认处理将被覆盖。以下是具有默认处理的消息列表及其功能:

  • window_mainWM_NCDESTROY(调用 PostQuitMessage
  • dialog_mainWM_CLOSE(调用 DestroyWindow),WM_NCDESTROY(调用 PostQuitMessage
  • dialog_modalWM_CLOSE(调用 EndDialog
  • dialog_modelessWM_CLOSE(调用 DestroyWindow
  • window_controldialog_controlWM_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 – 第一个公开发布版本
© . All rights reserved.