Windows 编程/消息循环架构
用 C 语言编写 Windows 程序与大多数人习惯的有所不同。首先,没有 main() 函数,而是使用 _tWinMain() 函数作为程序的入口点。_tWinMain() 在 tchar.h 中定义为宏,如下所示
#ifdef _UNICODE #define _tWinMain wWinMain #else #define _tWinMain WinMain #endif
这意味着 Windows 函数可以轻松地用 Unicode 或 ASCII 编写。除了函数名不同外,_tWinMain 还有不同于标准 main 函数的参数
int WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, // LPWSTR for wWinMain int iCmdShow);
HINSTANCE 对象是程序实例的引用。hThisInstance 是当前程序的引用,而 hPrevInstance 是之前运行的相同程序的引用。但是,在 Windows NT 中,hPrevInstance 数据对象始终为 NULL。这意味着我们无法使用 hPrevInstance 值来确定系统中是否运行了其他相同程序的副本。检查系统中是否运行了其他相同程序副本的方法有很多,我们将在稍后讨论这些方法。
lpszArgument 本质上包含 argc 和 argv 变量过去显示的所有信息,但命令行参数不会被拆分成向量。这意味着,如果你想从命令行中排序单个参数,则需要自己拆分它们,或者调用可用函数中的某个函数来为你拆分它们。我们将在稍后讨论这些函数。
最后一个参数 "int iCmdShow" 是一个 int 类型(整数)的数据项,用于确定是否立即显示图形窗口,或者程序是否应最小化运行。
WinMain 有许多不同的任务
- 注册程序要使用的窗口类
- 创建程序使用的任何窗口
- 运行消息循环
我们将更详细地解释每个任务。
你在屏幕上看到的每个图形细节都被称为“窗口”。每个带有边框的程序、每个按钮、每个文本框都被称为窗口。实际上,所有这些不同的对象都是以相同的方式创建的。这意味着为了从相同方法中获得像文本框和滚动条这样不同的对象,必须有大量的选项和自定义内容。每个窗口都有一个关联的“窗口类”,需要在系统中注册,以指定窗口的不同属性。这可以通过以下 2 个步骤完成
- 填写 WNDCLASS 数据对象的字段
- 将 WNDCLASS 对象传递给 RegisterClass 函数。
此外,还有一种“扩展”版本,可以执行类似的操作
- 填写 WNDCLASSEX 对象的字段
- 将 WNDCLASSEX 对象传递给 RegisterClassEx 函数。
这两种方法都可以用来注册类,但 -Ex 版本有一些额外的选项。
注册类后,你可以丢弃 WNDCLASS 结构,因为你不再需要它。
可以使用 CreateWindow 或 CreateWindowEx 函数创建窗口。两者执行相同的通用任务,但 -Ex 版本也有更多选项。你将一些具体内容传递给 CreateWindow 函数,例如窗口大小、窗口位置(X 和 Y 坐标)、窗口标题等。CreateWindow 将返回一个 HWND 数据对象,即新创建窗口的句柄。接下来,大多数程序会将此句柄传递给 ShowWindow 函数,以使窗口出现在屏幕上。CreateWindow() 创建窗口的方式如下
hwnd=CreateWindowEx(
WS_EX_CLIENTEDGE,
g_szClassName,
"The title of my window",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,240,120,
NULL,NULL,hInstance,NULL);
第一个参数 WS_EX_CLIENTEDGE 是扩展窗口样式。接下来是类名 g_szClassName,它告诉系统要创建哪种窗口。由于我们要从刚刚注册的类中创建窗口,因此我们使用该类的名称。之后,我们指定窗口名称或标题,即在标题或窗口上的标题栏中显示的文本。作为参数的 WS_OVERLAPPEDWINDOW 是窗口样式参数。有很多这样的参数,你应该查阅它们并进行试验,以找出它们的作用。接下来的四个参数(CW_USEDEFAULT,CW_USEDEFAULT,240,120)是窗口左上角的 X 和 Y 坐标,以及窗口的宽度和高度。X 和 Y 坐标被设置为 CW_USEDEFAULT,以让窗口选择在屏幕上的哪个位置放置窗口。接下来,(NULL,NULL,hInstance,NULL),我们有父窗口句柄、菜单句柄、应用程序实例句柄和指向窗口创建数据的指针句柄。在 Windows 中,屏幕上的窗口按父窗口和子窗口的层次结构排列。当看到窗口上的按钮时,按钮是子窗口,它包含在其父窗口中。在本例中,父窗口句柄为 NULL,因为我们没有父窗口,这是我们的主窗口或顶级窗口。现在菜单为 NULL,因为我们还没有菜单。实例句柄设置为作为 WinMain() 的第一个参数传递的值。可以用来向正在创建的窗口发送额外数据的创建数据为 NULL。
窗口创建后,窗口将通过 消息 与系统中的其他部分进行交互。系统向窗口发送消息,窗口也向系统发送消息。实际上,大多数程序只做一件事,就是读取消息并响应消息!
消息以 MSG 数据类型的形式出现。此数据对象被传递给 GetMessage() 函数,该函数从消息队列中读取消息,或者等待系统发送的新消息。接下来,消息被发送到 TranslateMessage 函数,该函数处理一些简单的任务,例如转换为 Unicode 或不转换为 Unicode。最后,使用 DispatchMessage 函数将消息发送到窗口进行处理。
以下是一个示例
MSG msg; BOOL bRet; while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0) { if (bRet == -1) { // handle the error and possibly exit } else { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam;
最后一行将在稍后解释。
窗口过程可以命名为任何你想要的名称,但其通用原型如下所示
LRESULT CALLBACK WinProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT 数据类型是一个通用的 32 位数据对象,可以将其类型转换为包含任何 32 位值(包括指针)。hwnd 参数是窗口本身的句柄。msg 数据值包含来自操作系统的当前消息,而 WPARAM 和 LPARAM 值包含该消息的参数。例如,如果按下键盘上的某个按钮,msg 字段将包含消息 WM_KEYDOWN,而 WPARAM 字段将包含实际按下的字母(例如 'A'),而 LPARAM 字段将包含有关 CTRL、ALT 或 SHIFT 按钮是否按下以及是否已触发类型化重复功能的信息。已定义了一些宏,在将 WPARAM 和 LPARAM 分离成不同大小的块时非常有用
- LOWORD(x)
- 返回 32 位参数的低 16 位
- HIWORD(x)
- 返回 32 位参数的高 16 位
- LOBYTE(x)
- 返回 16 位参数的低 8 位
- HIBYTE(x)
- 返回 16 位参数的高 8 位
例如,要访问 wParam 字段中的第二个字节,我们将使用如下所示的宏
HIBYTE(LOWORD(wParam));
由于窗口过程只有两个可用参数,因此这些参数通常包含数据。这些宏在将这些信息分离成不同的字段时非常有用。
以下是一个一般窗口过程的示例,我们将进行解释
LRESULT CALLBACK MyWinProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc (hwnd, msg, wParam, lParam); } return 0; }
大多数窗口过程只包含一个简单的循环,该循环搜索来自系统的特定消息,然后对它们进行操作。在本例中,我们只在查找 WM_DESTROY 消息,这是内核在窗口需要关闭时发送给窗口的消息。响应 WM_DESTROY 消息,窗口调用 PostQuitMessage 函数,该函数将 WM_QUIT 消息(定义为 0)放入消息队列。当消息循环(如上所述)接收到 WM_QUIT 消息时,它将从循环中退出,并返回来自 PostQuitMessage 函数的值。
任何未被循环处理的消息都应(必须)传递给 DefWindowProc 函数。DefWindowProc 将根据收到的消息执行一些默认操作,但它不会做任何有趣的事情。如果你希望你的程序执行一些操作,你需要自己处理这些消息。
我们将在后面讨论其他一些消息
- WM_CREATE
- 您的窗口只在第一次创建时收到此消息。使用此消息执行需要在开始时处理的任务,例如初始化变量、分配内存或创建子窗口(按钮和文本框)。
- WM_PAINT
- 此消息指示程序需要重新绘制自身。使用图形函数重新绘制窗口上应该显示的内容。如果你没有绘制任何内容,那么窗口将是无聊的白色(或灰色)背景,或者如果背景没有被擦除,将保留显示在上面的任何图像(这看起来不稳定)。
- WM_COMMAND
- 这是一个通用消息,指示用户在您的窗口上做了什么。用户要么单击了一个按钮,要么选择了菜单项,要么按下了特殊的“加速键”序列。WPARAM 和 LPARAM 字段将包含有关发生的事情的一些描述,以便您可以找到一种对它做出反应的方式。如果您没有处理 WM_COMMAND 消息,用户将无法单击任何按钮或选择任何菜单项,这将非常令人沮丧。
- WM_CLOSE
- 用户决定关闭窗口,因此内核发送 WM_CLOSE 消息。这是保存窗口的最后机会 - 如果你不想完全关闭它,你应该处理 WM_CLOSE 消息并确保它不会销毁窗口。如果 WM_CLOSE 消息传递给 DefWindowProc,那么窗口将接下来接收 WM_DESTROY 消息。
- WM_DESTROY
- WM_DESTROY 指示给定窗口从屏幕中移除并将从内存中卸载。通常,您的程序可以通过调用 PostQuitMessage() 将 WM_QUIT 消息发布到程序以退出程序。
这些是一些最基本的消息,我们将根据需要讨论其他消息。
- 提示 SendMessage(hwnd,MACRO,NULL,NULL) 可用于发送用户定义的消息。