《C++多线程编程实战》——1.8 事件处理器和消息传递接口

简介:

本节书摘来自异步社区出版社《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)

WINAPIstdcall意味着栈的清理工作由被调函数来完成。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;`
下面的代码注册一个窗口类,供CreateWindowCreateWindowEx函数稍后使用:

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),即正在关闭应用程序时系统发送的消息。然后,调用PostQuitMessageAPI释放系统资源,并安全关闭该应用程序。

switch ( uMsg ) 
{ 
  case WM_CLOSE: 
  { 
    PostQuitMessage( 0 ); 
    break; 
  } 
  default: 
  {```
最后,调用默认窗口过程(`DefWindowProc`)处理应用程序未处理的窗口消息。该函数确保每个消息都被处理,如下所示:

     return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
  }
  return 0;
}`
虽然本节介绍的窗口应用程序示例非常简单,但是它完整地反映了事件驱动系统特性和事件处理机制。在后面的章节中,我们将频繁地使用事件处理,所以理解这些基本过程非常重要。

相关文章
|
2天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
24 6
|
1月前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
39 1
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
1月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
41 7
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
235 10
|
1月前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
1月前
|
消息中间件 存储 安全
|
2月前
SDL事件处理以及线程使用(2)
SDL库中事件处理和多线程编程的基本概念和示例代码,包括如何使用SDL事件循环来处理键盘和鼠标事件,以及如何创建和管理线程、互斥锁和条件变量。
34 1
SDL事件处理以及线程使用(2)
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
26 3