真正理解微软Windows程序运行机制——窗口机制(第三部分)

微软技术分享 微软最有价值专家
全栈领域优质创作者
博客专家认证
2024-02-12 09:46:29

我是荔园微风,作为一名在IT界整整25年的老兵,今天说说Windows程序的运行机制。经常被问到MFC到底是一个什么技术,为了解释这个我之前还写过帖子,但是很多人还是不理解。其实这没什么,我在学生时代也被这个问题困绕过。而且那个时间学习资料没有那么丰富,网上也没有什么资料,周围也没有懂的人,那个时候理解MFC更困难。甚至在我看来,理解这个比理解人工神经网络更难。

我认为造成这种现象的根本原因就是没有搞清楚Windows程序的运行机制,因为不理解Windows程序的运行机制,所以给理解MFC带来了很大的困难。我决定带所有微软开发技术的初学者一起攻破这个问题,但是一篇文章肯定是讲不清楚的,我们要分好几章来说。需要你有足够的耐心,一起来吧。我们这次来搞清楚什么是Windows程序的窗口机制。

下面我们来创建一个窗口,并在该窗口中响应键盘及鼠标消息,程序实现的步骤我们分三个部分,分别用三篇文章来讲清楚。

第一部分我们先写出WinMain()函数的定义,然后创建一个窗口。第二部分我们再通过消息循环来处理消息,最后第三部分我们编写窗口过程函数。为什么最后要编写窗口过程函数,主要是为了处理传过来的消息,这个就是我们今天要讲的内容。只有以上三个部分完成了,我们就真正能做到创建一个窗口,并在该窗口中响应键盘及鼠标消息,不然少一步都做不到。

现在我们就来介绍如何编写窗口过程函数

在完成之前两篇帖子的内容后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。一个 Windows应用程序的主要代码部分就集中在窗口过程函数中。在 MSDN中可以查到窗口过程函数的声明形式,如下所示:

LRESULT CALLBACK WindowProc(
  HWND hwnd,        //handle to window
  UINT uMsg,        //message identifier
  WPARAM wParam,    //first message parameter
  LPARAM lParam     //second message parameter
);

窗口过程函数的名字可以随便取,但函数定义的形式必须和上述声明的形式相同。系统通过窗口过程函数的地址(指针)来调用窗口过程函数,而不是名字。

WindowProc函数的4个参数分别对应消息的窗口句柄、消息代码、消息代码的两个附加参数。一个程序可以有多个窗口,窗口过程函数的第1个参数hwnd就标识了接收消息的特定窗口。在窗口过程函数内部使用switch/case语句来确定窗口过程接收的是什么消息,以及如何对这个消息进行处理。

我们看下面的代码WinMain.cpp:

1. LRESULT CALLBACK WindowsProc(
2.  HWND hwnd,  // handle to window
3.  UINT uMsg,  // message identifier
4.  WPARAM wParam,  // first message parameter
5.  LPARAM IParam  // second message parameter
6.)
7.{
8.  switch(uMsg)
9.  {
10.  case WM_CHAR:
11.      char szChar[20];
12.      sprintf_s(szChar, sizeof(szChar), "char is &d", wParam);
13.      MessageBox(hwnd, szChar, "char",0);
14.      break;
15.  case WM_LBUTTONDOWN:
16.      MessageBox(hwnd,"mouse clicked","message",O);
17.      HDC hdc;
18.      hdc=GetDC(hwnd);
19.      TextOut (hdc,0,50,"csdn"strlen("csdn") ;
20.      ReleaseDC(hwnd,hdc);
21.      break;
22.  case WM_PAINT:
23.      HDC hDC;
24.      PAINTSTRUCT ps;
25.      hDC=BeginPaint(hwnd, &ps);
26.       TextOut(hDC,0,0,"www.csdn.net", strlen("www.csdn.net");
27.      EndPaint(hwnd,&ps);
28.      break;
29.  case WM_CLOSE:
30.      if (IDYES=MessageBox (hwnd,"是否结束?""message", MB_YESNO)
31.      {
32.          DestroyWindow(hwnd);
33.      }
34.      break;
35.  case WM_DESTROY:
36.      PostQuitMessage(0);
37.      break;
38.  default:
39.      return DefWindowProc(hwnd, uMsg, wParam,1Param);
40.  }
41.  return 0;
42.}

当年我看到这个程序时,不知道是什么样的意思,下面我来解释一下:

10~14行代码:当用户在窗口中按下一个字符键,程序将得到一条WM_CHAR消息(通过调用TranslateMessage函数转换得到),在其 wParam 参数中含有字符的ASCII码值。MessageBox函数弹出一个包含了显示信息的消息框,如果我们按下字母“c”键(注意大小写),程序将弹出消息框,内容是char is 99。

15-21行代码:当用户在窗口中按下鼠标左键时,将产生WM_LBUTTONDOWN 消息。我们在 WM_LBUTTONDOWN消息的响应代码中,调用MessageBox函数弹出一个提示信息,告诉用户“mouse clicked”。接下来,我们在窗口中(0,50)的位置处输出一行文字“csdn”。下面这句话各位初学者一定要牢记:

要在窗口中输出文字或者显示图形,需要用到设备描述表(Device Context),简称DC。DC.是一个包含设备(物理输出设备,如显示器,以及设备驱动程序)信息的结构体,在Windows平台下,所有的图形操作都是利用DC来完成的。

https://img-blog.csdnimg.cn/img_convert/b57f5db20af29200821fd9f8308c8527.jpeg

关于 DC,我们来打个比方。我们让各种AIGC应用画一幅皮卡丘的图像,有的AI采用素描,有的AI采用水彩画,有的AI采用油画,每个AI所画的图都是皮卡丘,然而表现形式却各不相同。如果让我们来画图,指定了一种画法(例如用素描),我们就要去学习它,然后才能按照要求画出图形。如果画法(工具)经常变换,我们就要花大量的时间和精力去学习和掌握画法。在这里,画法就相当于计算机中的图形设备及其驱动程序。我们要想画一幅图,就要掌握我们所用平台的图形设备和驱动程序,调用驱动程序的接口来完成图形的显示。不同图形设备的驱动程序是不一样的,对于程序员来说,要掌握各种不同的驱动程序,工作量就太大了。

因此,Windows就给我们提供了一个DC,让我们只要下命令去画皮卡丘这幅图,由DC去和设备驱动程序打交道,就能完成图形的绘制。至于图形的效果,就要由所使用的图形设备来决定了。

对于我们来说,只要画出的是皮卡丘图像就可以了。

对于程序员来说,只需要获取DC(DC也是一种资源)的句柄,利用这个句柄去作图就可以了。使用DC,程序不用为图形的显示与打印输出做分别处理了。

无论是显示,还是打印,都直接在DC上操作,然后由DC映射到这些物理设备上。

第17行代码:定义了一个类型为HDC的变量hdc。

第18行代码:用hdc保存GetDC函数返回的与特定窗口相关联的DC的句柄。

为什么DC要和窗口相关联呢?我们在作图时需要有画布,而利用计算机作图,窗口就相当于画布,因此,在获取DC的句柄时,总是和一个指定的窗口相关联。

第19行代码:TextOut函数利用得到的DC句柄在指定的位置(x坐标为0,y坐标为50)输出一行文字。

第20行代码:在执行图形操作时,如果使用GetDC函数来得到DC的句柄,那么在完成图形操作后,必须调用ReleaseDC函数来释放DC所占用的资源,否则会引起内存泄漏。

好了,在继续研究代码前,我们必须了解一下消息,叫做WM_PAINT 消息,我们来看一看:

WM_PAINT是Windows窗口系统中一条重要的消息,应用程序通过处理该消息实现在窗口上的绘制工作。

问题1. 系统何时发送WM_PAINT消息?系统会在多个不同的时机发送WM_PAINT消息:当第一次创建一个窗口时,当改变窗口的大小时,当把窗口从另一个窗口背后移出时,当最大化或最小化窗口时等等,这些动作都是由系统管理的,应用只是被动地接收该消息,在消息处理函数中进行绘制操作;大多数的时候应用也需要能够主动引发窗口中的绘制操作,比如当窗口显示的数据改变的时候,这一般是通过InvalidateRect和 InvalidateRgn函数来完成的。InvalidateRect和InvalidateRgn把指定的区域加到窗口的Update Region中,当应用的消息队列没有其他消息时,如果窗口的Update Region不为空时,系统就会自动产生WM_PAINT消息。

系统为什么不在调用Invalidate时发送WM_PAINT消息呢?又为什么非要等应用消息队列为空时才发送WM_PAINT消息呢?这是因为系统把在窗口中的绘制操作当作一种低优先级的操作,于是尽可能地推后做。不过这样也有利于提高绘制的效率:两个WM_PAINT消息之间通过InvalidateRect和InvaliateRgn使之失效的区域就会被累加起来,然后在一个WM_PAINT消息中一次得到更新,不仅能避免多次重复地更新同一区域,也优化了应用的更新操作。这种通过InvalidateRect和InvalidateRgn来使窗口区域无效,依赖于系统在合适的时机发送WM_PAINT消息的机制实际上是一种异步工作方式,也就是说,在无效化窗口区域和发送WM_PAINT消息之间是有延迟的;有时候这种延迟并不是我们希望的,这时我们当然可以在无效化窗口区域后利用SendMessage 发送一条WM_PAINT消息来强制立即重画,但不如使用Windows GDI为我们提供的更方便和强大的函数:UpdateWindow和RedrawWindow。UpdateWindow会检查窗口的Update Region,当其不为空时才发送WM_PAINT消息;RedrawWindow则给我们更多的控制:是否重画非客户区和背景,是否总是发送WM_PAINT消息而不管Update Region是否为空等。

问题2. 什么是BeginPaint?BeginPaint和WM_PAINT消息紧密相关。试一试在WM_PAINT处理函数中不写BeginPaint会怎样?程序会像进入了一个死循环一样达到惊人的CPU占用率,你会发现程序总在处理一个接一个的WM_PAINT消息。这是因为在通常情况下,当应用收到WM_PAINT消息时,窗口的Update Region都是非空的(如果为空就不需要发送WM_PAINT消息了),BeginPaint的一个作用就是把该Update Region置为空,这样如果不调用BeginPaint,窗口的Update Region就一直不为空,如前所述,系统就会一直发送WM_PAINT消息。BeginPaint和WM_ERASEBKGND消息也有关系。当窗口的Update Region被标志为需要擦除背景时,BeginPaint会发送WM_ERASEBKGND消息来重画背景,同时在其返回信息里有一个标志表明窗口背景是否被重画过。当我们用InvalidateRect和InvalidateRgn来把指定区域加到Update Region中时,可以设置该区域是否需要被擦除背景,这样下一个BeginPaint就知道是否需要发送WM_ERASEBKGND消息了。另外要注意的一点是,BeginPaint只能在WM_PAINT处理函数中使用。

另外,需要知道的是:1.WM_PAINT是一个被动消息,不能通过普通的方法简单的 sendmessage WM_PAINT了事,这是不行的;但通过消息由程序员引发不是不可能;通过几个特殊的常数可以做到。2.sendmessage 可以将消息发送到消息队列;但windows会自动判断是否存在无效的画图区域; 如果存在无效的画图区域,则可能会重画,反之则弃用该消息。3.可以使用 InvalidateRect 等几个APi将屏幕上任意一个个矩形区域设置为无效区域,在UpdateWindow后调用后,windows会自动查找是否存在无效,并重画,该矩形区。

好了,我们继续分析代码。

第22~28行代码:对WM_PAINT消息进行处理。当窗口客户区的一部分或者全部变为“无效”时,系统会发送WM_PAINT消息,通知应用程序重新绘制窗口。当窗口刚创建的时候,整个客户区都是无效的。因为这个时候程序还没有在窗口上绘制任何东西,当调用 UpdateWindow 函数时,会发送 WM PAINT消息给窗口过程,对窗口进行刷新。当窗口从无到有、改变尺寸、最小化后再恢复,窗口的客户区都将变为无效,此时系统会给应用程序发送WM_PAINT消息,通知应用程序重新绘制。窗口大小发生变化时是否发生重绘,取决于WNDCLASS结构体中style成员是否设置了CS_HREDRAW和CS_VREDRAW标志。

第25行,调用BeginPaint函数得到DC的句柄。BeginPaint函数的第1个参数是窗口的句柄,第二个参数是PAINTSTRUCT结构体的指针,该结构体对象用于接收绘制的信息。在调用 BeginPaint时,如果客户区的背景还没有被擦除,那么BeginPaint 会发送WM_ERASEBKGND消息给窗口,系统就会使用 WNDCLASS结构体的 IhbrBackground成员指定的画刷来擦除背景。

第26行,调用TextOut函数在(0,0)的位置输出一个网址“http://www.csdn.net".xn--,-lq6ap3a41lvc21ch9jmykfkjre306dbybe0bn51jenezut46q1w5bib7arndqx5a.xn--,textout,www-449fl587clta654b7wbobx0fu3ujqa83a053cna7803eltn0ndy9il81bga8083j1p2a0pnvf4drc7f.csdn.xn--net-9o0a.xn--,,-zv2cke90ee2puiey2lroak1q5hjc001c34dbae82f132c5rala116doo2gfag508fhad199gm9pnad5740bxf9dmuknos2o6aua57d99d76d/ WM_PAINT消息的代码中输出的文字或图形始终保持在窗口中。换句话说,如果我们想要让某个图形始终在窗口中显示,就应该将图形的绘制操作放到响应WM_PAINT消息的代码中。

那么系统为什么不直接保存窗口中的图形数据,而要由应用程序不断地进行重绘呢?这主要是因为在图形环境中涉及的数据量太大,为了节省内存的使用,提高效率,而采用了重绘的方式。

在响应 WM_PAINT消息的代码中,要得到窗口的 DC,必须调用 BeginPaint函数。BeginPaint 函数也只能在 WM_PAINT 消息的响应代码中使用,在其他地方,只能使用GetDC来得到DC的句柄。另外, BeginPaint 函数得到的DC,必须用 EndPaint函数去释放。

29~34行代码:当用户单击窗口上的关闭按钮时,系统将给应用程序发送一条WM_CLOSE消息。在这段消息响应代码中,我们首先弹出一个消息框,让用户确认是否结束。如果用户选择“否”,则什么也不做;如果用户选择“是”,则调用DestroyWindow函数销毁窗口,DestroyWindow 函数在销毁窗口后会向窗口过程发送 WM_DESTROY 消息。注意,此时窗口虽然销毁了,但应用程序并没有退出。

有不少初学者错误地在WM_DESTROY消息的响应代码中提示用户是否退出,而此时窗口已经销毁了,即使用户选择不退出,也没有什么意义了。

所以如果你要控制程序是否退出,应该在WM_CLOSE消息的响应代码中完成。对WM CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给 DefWindowProc函数,那么 DefWindowProc函数则调用 DestroyWindow 函数来响应这条 WM_CLOSE消息。

35-37行代码:DestroyWindow函数在销毁窗口后,会给窗口过程发送 WM_DESTROY消息,我们在该消息的响应代码中调用PostQuitMessage函数。PostQuitMessage函数向应用程序的消息队列中投递一条WM_QUIT消息并返回。GetMessage函数只有在收到 WM_QUIT消息时才返回 0,此时消息循环才结束,程序退出。要想让程序正常退出,我们必须响应WM_DESTROY消息,并在消息响应代码中调用PostQuitMessage,向应用程序的消息队列中投递 WM_QUIT消息。传递给 PostQuitMessage函数的参数值将作为 WM_QUIT 消息的wParam参数,这个值通常用作 WinMain函数的返回值。

38-39行代码:DefWindowProc函数调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理。对于大多数的消息,应用程序都可以直接调用 DefWindowProc函数进行处理。在编写窗口过程时,应该将 DefWindowProc 函数的调用放到 default 语句中,并将该函数的返回值作为窗口过程函数的返回值。

如果在程序中不用DefWindowProc函数,就会导致窗口不显示。

在运行之后,你可以在Windows中启动任务管理器(同时按下键盘上的“Ctrl+Alt+Del”键),切换到进程标签,查看程序是否运行。

各位小伙伴,下次我们再深入研究windows程序的运行机制。


文章来源: https://blog.csdn.net/wang2015cn/article/details/131160044
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


...全文
55 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

460

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 微软技术分享
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧