窗口类

每个窗口都必须和一个窗口类(非C++意义的类)相关联,窗口类在运行时向系统注册,注册窗口类使用WNDCLASS结构:

1
2
3
4
5
6
7
8
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";

WNDCLASS wc = { };

wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;

其中:

  • lpfnWndProc 是指向应用程序定义的函数(窗口过程)的指针,窗口过程定义窗口大部分的行为.
  • hInstance 是应用程序实例的句柄.
  • lpszClassName 是标识窗口类的字符串.

类名需要在进程中唯一,且不能与标准Windows控件类名(例如Button)冲突.

其他成员可以填充为0.

然后将该结构变量的地址传递给RegisterClass函数,来注册到操作系统:

1
RegisterClass(&wc);

创建窗口

使用CreateWindowEx函数创建窗口的新实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HWND CreateWindowEx(
[in] DWORD dwExStyle,
[in, optional] LPCSTR lpClassName,
[in, optional] LPCSTR lpWindowName,
[in] DWORD dwStyle,
[in] int X,
[in] int Y,
[in] int nWidth,
[in] int nHeight,
[in, optional] HWND hWndParent,
[in, optional] HMENU hMenu,
[in, optional] HINSTANCE hInstance,
[in, optional] LPVOID lpParam
);
  • dwExStyle 制定扩展窗口样式,例如透明窗口.默认设置为0即可.
  • lpClassName 窗口类名,定义要创建的窗口的类型.
  • lpWindowName 窗口名称,如果窗口样式指定标题栏,则其将显示在标题栏中.
  • dwStyle 窗口样式,它是一组标志,用于定义窗口的一些外观.
  • X 窗口初始的水平坐标,CW_USEDEFAULT表示使用默认值(仅对重叠窗口有效,如果为弹出窗口或子窗口指定,则X和Y参数设置为零).
  • Y 同上类似.
  • nWidth,nHeight 窗口的宽度,高度,以设备单位为单位,CW_USEDEFAULT为默认值.
  • hWndParent 正在创建的窗口的父窗口或所有者窗口的句柄,对于顶级窗口,设置为NULL.
  • hMenu 菜单句柄,或指定子窗口标识符,具体取决于窗口样式.可以设为NULL.
  • hInstance 如前所述,为实例句柄.
  • lpParam void*的任意数据的指针,使用此值来将数据结构传递到窗口过程.

CreateWindowEx返回新窗口的句柄,如果失败则返回0.

如果要显示窗口,则将返回的句柄传递给ShowWindow函数:

1
ShowWindow(hwnd, nCmdShow);

其中nCmdShow可用于最小化或最大化窗口,操作系统通过wWinMain函数将此值传递给程序(?)

消息循环

事件与消息

有两种事件:

  • 来自用户的事件: 用户与程序交互的所有方式:鼠标单击,击键等.
  • 来自操作系统的事件:"程序外部"的任何可能影响程序行为方式的内容,例如插入新硬件设备等.

事件在程序运行的任何时间,几乎任何顺序发生,为了处理这样无法预测的事件,Windows使用消息传递模型,即操作系统通过向应用程序窗口传递消息来与应用程序窗口通信,消息只是指定特定事件的数字代码.

例如当按下左键时,窗口将收到如下消息代码:

1
#define WM_LBUTTONDOWN    0x0201

某些消息具有与之关联的数据,例如鼠标光标的x,y坐标.

要将消息传递到窗口,操作系统会调用为窗口注册的窗口过程.

获取消息与消息循环

应用程序在运行时会收到大量消息,同时应用程序可能会有多个窗口,每个窗口都有自己的窗口过程,应用程序需要一个循环来检索消息并将其调度到正确的窗口.

对每个线程,操作系统都会为其消息窗口创建一个队列,用于保存在该线程上创建的所有窗口的消息.该队列本身对程序隐藏,使用GetMessage函数来操作该队列:

1
2
3
4
5
6
BOOL GetMessage(
[out] LPMSG lpMsg,
[in, optional] HWND hWnd,
[in] UINT wMsgFilterMin,
[in] UINT wMsgFilterMax
);

其中:

  • lpMsg 为指向MSG结构的指针,该结构用于从消息队列中接受消息信息.

  • hWnd 要检索其消息的窗口的句柄,一般为NULL(检索属于当前线程的任何窗口的消息以及当前线程的消息队列上hwnd值为NULL的任何消息)—处理窗口消息和线程消息.

    如果为-1,则仅检索当前线程的消息队列上hwnd值为NULL的任何消息或PostMessage发布的线程消息.

  • wMsgFilterMin 要检索的最低消息值的整数值.

  • wMsgFilterMax 要检索的最高消息值的整数值.

  • 如果wMsgFilterMin和wMsgFilterMax均为零,则GetMessage将返回所有可用消息,即不执行范围筛选.

如果检索WM_QUIT以外的消息,返回非零;检索WM_QUIT返回零;出现错误则返回-1.

GetMessage函数从队列的头部删除一条消息,若队列为空则该函数阻塞(但不会使程序无响应),因此后台处理需要创建另一个线程.

例如:

1
2
MSG msg;
GetMessage(&msg, NULL, 0, 0);

几乎不需要手动检查MSG结构,只需要将其传递给如下函数即可:

1
2
TranslateMessage(&msg); // 与键盘输入相关,将击键(上,下)转换为字符.在DispatchMessage之前调用
DispatchMessage(&msg); // 指示操作系统调用窗口的窗口过程

需要一个循环来不断从队列中拉取消息并调度它们,如果需要退出应用程序并中断消息循环,调用PostQuitMessage函数:

1
PostQuitMessage(0);

该函数将WM_QUIT消息置于消息队列中,导致GetMessage返回零,标志着消息循环结束,例如:

1
2
3
4
5
6
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

已发布消息与已发送消息

操作系统有时会绕过消息队列直接调用窗口过程.

  • 发布消息意味着消息进入消息队列.
  • 发送消息意味着消息跳过队列,操作系统直接调用窗口过程.

如果应用程序在窗口之间通信,则可能有所不同,详见 消息和消息队列.

编写窗口过程

如前所述,DispatchMessage函数调用窗口的窗口过程,该窗口是消息的目标.

窗口过程有以下签名:

1
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

其中:

  • hwnd 是窗口的句柄.

  • uMsg 是消息代码.

  • wParam和lParam 包含与消息相关的其他数据,具体含义依赖于消息代码(数值/指针).对于每条消息,都需要根据消息代码并将其强转为正确的数据类型.

  • LRESULT 是程序返回到Windows的整数值,包含程序对特定消息的响应,具体含义同样取决于消息代码.

例如处理WM_SIZE消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SIZE:
{
int width = LOWORD(lParam); // Macro to get the low-order word.
int height = HIWORD(lParam); // Macro to get the high-order word.

// Respond to the message:
OnSize(hwnd, (UINT)wParam, width, height);
}
break;
}
}

void OnSize(HWND hwnd, UINT flag, int width, int height)
{
// Handle resizing
}

LOWORD和`HIWORD是2个宏,用于获取16位数据(参考MSDN文档).


如果不在窗口消息中处理特定消息,请将消息参数直接传递到DefWindowProc函数,此函数对消息执行默认操作,因消息类型而异:

1
return DefWindowProc(hwnd, uMsg, wParam, lParam);

注意:当窗口过程执行时,它会阻止在同一线程上创建的窗口的任何其他消息.

因此,请不要在窗口过程中进行冗长处理,例如TCP连接并无限等待,这会让窗口在完成该任务前无法做出其他任何响应,甚至无法关闭!

相反,请将该工作移动到另一个线程,该操作可以使用Windows如下多任务工具的一种来实现:

  • 创建新进程
  • 使用线程池
  • 使用异步I/O调用
  • 使用异步过程调用

绘制窗口

到此为止已经创建窗口,接下来想要在其内绘制一些东西,Windows术语叫做绘制窗口.换句话说,窗口是一个空白的画布,它等待被绘制(填充).

有时,程序会启动绘制以更新窗口的外观。 在其他时候,操作系统会通知你必须重新绘制窗口的一部分。 发生这种情况时,操作系统会向窗口发送WM_PAINT消息。 必须绘制的窗口部分称为 更新区域。

首次显示消息时,必须绘制窗口的整个工作区,即在显示窗口时,始终会收到至少一条WM_PAINT消息.

显示窗口更新区域的插图

你只负责绘制工作区.外面的框架(包括标题栏)由操作系统自动绘制.完成绘制后,清除更新区域,这会通知操作系统,在发生更改前,其不需要发送另一条WM_PAINT消息.(?)


假设用户移动了窗口,使其遮挡了窗口的一部分,当遮挡部分再次可见时,该部分将添加到更新区域,并且窗口会收到另一条WM_PAINT消息:

显示当两个窗口重叠时更新区域如何更改的插图

同样,如果用户拉伸窗口,新的区域将添加到更新区域:

显示调整窗口大小时更新区域如何更改的插图

绘制窗口时,先使用BeginPaint函数来启动绘制操作, 此函数使用有关重画请求的信息填充PAINTSTRUCT结构:

1
2
3
4
HDC BeginPaint(
[in] HWND hWnd,
[out] LPPAINTSTRUCT lpPaint
);

其中:

  • hWnd 是要求重新绘制的窗口的句柄.
  • lpPaint 是指向接收绘制信息的PAINTSTRUCT结构的指针.
  • 如果函数成功,则返回指定窗口的显示设备上下文的句柄;如果函数失败,则返回值为 NULL,指示没有可用的显示设备上下文.

BeginPaint 的每个调用都必须具有对 EndPaint 函数的相应调用!

然后可以使用FillRect函数来绘制矩形,此函数包括矩形的左和上边框,不包括右和下边框:

1
2
3
4
5
int FillRect(
[in] HDC hDC,
[in] const RECT *lprc,
[in] HBRUSH hbr
);

其中:

  • hDC 是设备上下文的句柄.
  • lprc 是指向RECT结构的指针,该结构包含矩形的逻辑坐标.
  • hbr 是画笔句柄,其可以是逻辑画笔的句柄,也可以是颜色值.
  • 如果成功,则返回非零值;否则返回零.

完成绘制后,(必须)调用EndPaint函数,该函数清除更新区域,向Windows发出窗口已完成绘制本身的信号:

1
2
3
4
BOOL EndPaint(
[in] HWND hWnd,
[in] const PAINTSTRUCT *lpPaint
);
  • hWnd 是已重新绘制的窗口的句柄.
  • lpPaint 是指向PAINTSTRUCT结构的指针.
  • 返回值始终为非零值.

上述使用的PAINTSTRUCT结构体包含应用程序的信息,可用于绘制该应用程序拥有的窗口的工作区:

1
2
3
4
5
6
7
8
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;

其中:

  • hdc: 要用于绘制的显示 DC 的句柄。

  • fErase: 指示是否必须擦除背景。 如果应用程序应擦除背景,则此值为非零值。 如果创建窗口类时没有背景画笔,则应用程序负责擦除背景。 有关详细信息,请参阅 WNDCLASS 结构的 hbrBackground 成员的说明。

  • rcPaint: RECT结构,指定请求绘制的矩形的左上角和右下角,以相对于工作区左上角的设备单位表示。

  • fRestore: 保留;由系统内部使用。

  • fIncUpdate: 保留;由系统内部使用。

  • rgbReserved[32]: 保留;由系统内部使用。


例如,如下例子使用纯色(用户定义的系统背景色)来填充工作区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (uMsg)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);

// All painting occurs here, between BeginPaint and EndPaint.

FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

EndPaint(hwnd, &ps);
}
return 0;
}