本节书摘来自异步社区出版社《C++多线程编程实战》一书中的第1章,第1.8节,作者: 【黑山共和国】Milos Ljumovic(米洛斯 留莫维奇),更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.8 事件处理器和消息传递接口
许多程序都要响应一些事件,例如,当用户按下按键或输入一些文本时。事件处理或程序能响应用户的动作是一种非常重要的机制。如果要在用户按下按键时处理这个事件,就要创建某种监听器,监听按键事件(即,按下的动作)。
事件处理器是操作系统调用的一个函数,每次都发送某种类型的消息。例如,在按下按键时发送“已按下”,在文本输入时发送“接收到一个字符”。
事件处理器非常重要。计时器是经过某段时间后触发的事件。当用户按下键盘上的一个按键,操作系统就引发“按下按键”事件,等等。
对我们而言,窗口的事件处理器至关重要。大多数应用程序都有窗口或窗体。每个窗口都要有自己的事件处理器,一旦在窗口中发生事件都要调用事件处理器。例如,如果创建一个带多个按钮和文本框的窗口,则必须有一个与该窗口相关的窗口过程来处理这些事件。
Windows操作系统以窗口过程的形式提供了这样一种机制,通常命名为WndProc
(也可以叫其他名称)。每次指定窗口发生事件时,操作系统就会调用该过程。在下面的例子中,我们将创建第1个Windows应用程序(即创建一个窗口),并解释窗口过程的用法。
准备就绪
确定安装并运行了Visual Studio
。
操作步骤
执行下面的步骤。
1.创建一个新的C++ Win32项目,命名为GUIProject
,单击右下方的【确定】。在弹出的向导窗口中单击【下一步】,在附加选项中勾选【空项目】,然后单击【完成】。现在,在【解决方案资源管理器】中右键单击【源文件】,选择【添加】,然后左键单击【新建项】。在弹出的窗口中选择【C++文件(.cpp)】,命名为main
。然后,单击窗口右下方的【添加】。
2. 现在创建代码。首先,添加所需的头文件:#include <windows.h>
大多数API都需要windows.h
头文件才能处理一些视觉特性,如窗口、控件、枚举和样式。在创建一个应用程序入口点之前,必须先声明一个窗口过程的原型才能在窗口结构中使用它,如下代码所示:
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);```
我们稍后实现`WndProc`,现在有声明就够了。接下来,需要一个应用程序入口点。Win32应用程序和控制台应用程序的`main`函数原型稍有不同,如下代码所示:
int WINAPI WinMain(HINSTANCE hThis, HINSTANCE hPrev, LPSTR szCmdLine, int iCmdShow)`
注意,在返回类型(int)后面有一个WINAPI宏,它表示一种调用约定(calling convention)
。
WINAPI
或stdcall
意味着栈的清理工作由被调函数来完成。WinMain
是函数名,该函数必须有4个参数,而且参数的顺序要与声明中的顺序相同。第1个参数hThis
是应用程序当前实例的句柄。第2个参数hPrev
是应用程序上一个实例的句柄。如果查阅MSDN
文档(http://msdn.microsoft.com/en-us/library/windows/desktop/ms633559%28v=vs.85%29.aspx)可以看到,hPrev
参数一定是NULL
。我猜应该是为了兼容旧版本的Windows操作系统,所以没有写明当前版本的值。第3个参数是szCmdLine
或应用程序的命令行,包括该程序的名称。最后一个参数控制如何显示窗口。
可以用OR(|)运算符组合多个位值(欲了解详细内容,请参阅MSDN)。
接下来,在WinMain的函数体中,用UNREFERENCED_RARAMETER宏告诉编译器不使用某些参数,方便编译器进行一些额外的优化。如下代码所示:
UNREFERENCED_PARAMETER( hPrev );
UNREFERENCED_PARAMETER( szCmdLine );```
然后,实例化`WNDCLASSEX`窗口结构。该对象中储存了待生成窗口的细节,如栈大小、当前应用程序实例的句柄、窗口样式、窗口颜色、图标和鼠标指针。`WNDCLASSEX`窗口结构的实例化代码如下所示:
`
WNDCLASSEX wndEx = { 0 };`
下面的代码定义了在实例化窗口类后分配的额外字节数:
`
wndEx.cbClsExtra = 0;`
下面的代码定义了窗口结构的大小(以字节为单位):
wndEx.cbSize = sizeof( wndEx );
下面的代码定义了实例化窗口实例后分配的额外字节数:
`
wndEx.cbWndExtra = 0;`
下面的代码定义了窗口类背景画刷的句柄:
`
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);`
下面的代码定义了窗口类光标的句柄:
wndEx.hCursor = LoadCursor( NULL, IDC_ARROW );
下面的代码定义了窗口类图标的句柄:
wndEx.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wndEx.hIconSm = LoadIcon( NULL, IDI_APPLICATION );`
下面的代码定义了包含窗口过程的实例句柄:
`
wndEx.hInstance = hThis;`
下面的代码定义了指向窗口过程的指针:
`
wndEx.lpfnWndProc = WndProc;`
下面的代码定义了指向以空字符结尾的字符串或原子的指针:
`
wndEx.lpszClassName = TEXT("GUIProject");`
下面的代码定义了指向以空字符结尾的字符串的指针,该字符串指定了窗口类菜单的资源名:
`
wndEx.lpszMenuName = NULL;`
下面的代码定义了窗口类的样式:
`
wndEx.style = CS_HREDRAW | CS_VREDRAW;`
下面的代码注册一个窗口类,供CreateWindow
或CreateWindowEx
函数稍后使用:
if ( !RegisterClassEx( &wndEx ) )
{
return -1;
}```
`CreateWindowAPI`创建一个重叠、弹出的窗口或子窗口。它指定该窗口类、窗口标题、窗口样式、窗口的初始位置和大小(可选的)。该函数还指定了窗口的父窗口或所有者(如果有的话),以及窗口的菜单。如下代码所示:
HWND hWnd = CreateWindow( wndEx.lpszClassName, TEXT("GUI Project"), WS_OVERLAPPEDWINDOW,
200, 200, 400, 300, HWND_DESKTOP,NULL, hThis, 0 );
if ( !hWnd )
{
return -1;
}`
如果指定窗口的更新域未被填满,UpdateWindow
函数就向窗口发送一条WM_PAINT
消息,更新指定窗口的客户区。该函数绕过应用程序的消息队列,向指定窗口的窗口过程直接发送一条WM_PAINT
消息。如下代码所示:
UpdateWindow( hWnd );
下面的代码设置指定窗口的显示状态:
`
ShowWindow( hWnd, iCmdShow );`
我们还需要一个MSG结构的实例来表示窗口消息。
`
MSG msg = { 0 };`
接下来,进入一个消息循环。Windows
中的应用程序是事件驱动的,它们不会显式调用函数(如,C运行时库调用)来获得输入,而是等待系统把输入传递给它们。系统把所有的输入传递给应用程序的不同窗口。每个窗口都有一个叫做窗口过程的函数,当有输入需要传递给窗口时,系统调用会调用该函数。窗口过程处理输入,并把控制权返回系统。GetMessageAPI
从主调线程的消息队列中检索信息,如下代码所示:
while ( GetMessage( &msg, NULL, NULL, NULL ) )
{
// 把虚拟键消息翻译成字符消息
TranslateMessage(&msg );
// 分发一条消息给窗口过程
DispatchMessage(&msg );
}```
当关闭应用程序或发送一些触发其退出的命令时,系统会释放应用程序消息队列。这意味着该应用程序不会再有消息,而且`while`循环也将结束。`DestroyWindowAPI`销毁指定的窗口。该函数向指定窗口发送`WM_DESTROY`和`WM_NCDESTROY`消息,使窗口无效并移除其键盘焦点(keyboard focus)。此外,该函数还将销毁指定窗口的菜单,清空线程的消息队列,销毁与窗口过程相关的计时器,解除窗口对剪切板的所有权,如果该窗口在查看器链的顶端,还将打断剪切板的查看器链。
`
DestroyWindow( hWnd );`
下面的函数注销窗口类,释放该类占用的内存:
`
UnregisterClass( wndEx.lpszClassName, hThis );`
下面的return函数从应用程序消息队列中返回一个成功退出代码或最后一个消息代码,如下代码所示:
`
return (int) msg.wParam;`
以上,我们逐行讲解了`WinMain`函数。接下来,要实现窗口过程或应用程序主事件处理器。作为第1个实例,先创建一个简单的`WndProc`,它只有一个处理关闭窗口的功能。该窗口过程返回64位有符号长整型值,有4个参数:`hWnd`结构(表示窗口标识符)、`uMsg`无符号整数(表示窗口消息代码)、`wParam`无符号64位长整型数(传递应用程序定义的数据)、`lParam`有符号64位长整型数(也用于传递应用程序定义的数据)。
LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{`
消息代码负责处理消息,如默认消息(该例中是WM_CLOSE
),即正在关闭应用程序时系统发送的消息。然后,调用PostQuitMessageAP
I释放系统资源,并安全关闭该应用程序。
switch ( uMsg )
{
case WM_CLOSE:
{
PostQuitMessage( 0 );
break;
}
default:
{```
最后,调用默认窗口过程(`DefWindowProc`)处理应用程序未处理的窗口消息。该函数确保每个消息都被处理,如下所示:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}`
虽然本节介绍的窗口应用程序示例非常简单,但是它完整地反映了事件驱动系统特性和事件处理机制。在后面的章节中,我们将频繁地使用事件处理,所以理解这些基本过程非常重要。