📄 见招拆招《windows程序设计》(三) .txt
字号:
见招拆招《Windows程序设计》(三)
作者:Zoologist 于2007-11-16上传
--------------------------------------------------------------------------------
本期内容非常多,我建议不要尝试一口气读完。过去有一种职业叫做“采珠女”,她们的工作就是潜入海底,捞取珍珠(这个职业多是妇女,据说妇女耐力好,能忍受低温,可以潜得更深……)。我们现在学习Win32汇编语言,和她们的工作差不多,每一次的学习过程好比下潜,每一次收获的知识则是我们的“珍珠”。在阅读本期的时候不妨在我们标记“总结”的地方休息一下,好比浮上来换气,也顺便盘点收获。
输出文字
在上一期,您看到了一个简单的Windows程序,它在窗口中央,或者更准确地说,在显示区域中央显示一行文字。正如我们学到的,显示区域是整个应用程序窗口中未被标题列、窗口边框,以及可选的菜单列、工具列、状态列和滚动条占据的部分。简而言之,显示区域是窗口中可以由程序任意书写和传递视觉信息的部分。
对于程序的显示区域,您几乎可以为所欲为,只不过您不能假定窗口大小是某一特定尺寸,或者在程序执行时其大小会保持不变。如果您不熟悉图形窗口环境的程序设计,这些限制可能会使您感到惊讶:不能再假设屏幕上的一行文字一定有80个字符了。您的程序必须与其它Windows程序共享显示器。Windows使用者控制程序窗口在屏幕上显示的方式。尽管可以建立固定大小的窗口(这对于计算器之类的应用是合理的),但在大多数情况下,使用者应该能够改变应用程序窗口的大小。您的程序必须能够接受指定给它的大小,并且合理地利用这一空间。
这有两种可能的情况。一种可能是,程序只有仅能显示「hello」的显示区域;还有另一种可能,即程序在一个大屏幕、高分辨率的系统上执行,其显示区域大得足以显示两整页文字。灵活地处理这两种极端是Windows程序设计的要点之一。
这一章,我们将讲述程序在显示区域显示信息的方式,但比上一期说明的显示方式更加复杂。当程序在显示区域显示文字或图形时,它经常要「绘制」它的显示区域。这里着重讲述绘制的方法。
尽管Windows为显示图形提供了强大的图形设备接口(GDI)函数,但在这一章中,我只介绍简单文字行的显示。我也将忽略Windows能够使用的不同字体外形及字体大小,仅使用Windows的内定系统字体。这看起来似乎是一种限制,其实不然,本章涉及和解决的问题适用于所有Windows程序设计。在混合显示文字和图形时,Windows内定字体的字符大小通常决定了图形的尺寸。
这期的内容表面上是讨论绘图的方法,实际上是讨论与设备无关的程序设计基础。Windows程序只能对显示区域大小甚至字符的大小做很少的假定,相反地,必须使用Windows提供的功能来取得关于程序执行环境的信息,形象地说就是“走一步看一步”“具体情况具体分析”。
绘制和更新
在文字模式环境下,程序可以在显示器的任意部分输出,程序输出到屏幕上的内容会停留在原处,不会神秘地消失。因此,程序可以丢掉重新生成屏幕显示时所需的信息。
在Windows中,只能在窗口的显示区域绘制文字和图形,而且不能确保在显示区域内显示的内容会一直保留到程序下一次有意地改写它时还保留在那里。例如,使用者可能会在屏幕上移动另一个程序的窗口,这样就可能覆盖您的应用程序窗口的一部分。Windows不会保存您的窗口中被其它程序覆盖的区域,当程序移开后,Windows会要求您的程序更新显示区域的这个部分。
Windows是一个消息驱动系统。它通过把消息投入应用程序消息队列中或者把消息发送给合适的窗口消息处理程序,将发生的各种事件通知给应用程序。Windows通过发送WM_PAINT消息通知窗口消息处理程序,窗口的部分显示区域需要绘制。
WM_PAINT消息
大多数Windows程序在WinMain中进入消息循环之前的初始化期间都要呼叫函数UpdateWindow。Windows利用这个机会给窗口消息处理程序发送第一个WM_PAINT消息。这个消息通知窗口消息处理程序:必须绘制显示区域。此后,窗口消息处理程序应在任何时刻都准备好处理其它WM_PAINT消息,必要的话,甚至重新绘制窗口的整个显示区域。在发生下面几种事件之一时,窗口消息处理程序会接收到一个WM_PAINT消息:
在使用者移动窗口或显示窗口时,窗口中先前被隐藏的区域重新可见。
使用者改变窗口的大小(如果窗口类别样式有着CS_HREDRAW和CS_VREDRAW位旗标的设定)。
程序使用ScrollWindow或ScrollDC函数滚动显示区域的一部分。
程序使用InvalidateRect或InvalidateRgn函数刻意产生WM_PAINT消息。
对于上面,我们可以做一下实验来验证:
打开Spy++,选中一个窗口,这个例子中,我选的是MasmPlus EXE模板直接建立的Win32 EXE,就是一个简单的窗口。使用“窗口查找程序工具”选定Gui窗口,
再切换到消息卡片上,清除全部,然后选定 WM_Paint 消息。
点确定之后就开始监视了:
我们试着将,Spy++窗口移动到Gui.exe上,并没有消息
但只要挪开一点点,就会产生消息
除此之外,还可以试试看:鼠标移动到Gui.EXE上是否会产生消息等等。
在某些情况下,显示区域的一部分被临时覆盖,Windows试图保存一个显示区域,并在以后恢复它,但这不一定能成功。在以下情况下,Windows可能发送WM_PAINT消息:
Windows擦除覆盖了部分窗口的对话框或消息框。
菜单下拉出来,然后被释放。
显示工具提示消息。
在某些情况下,Windows总是保存它所覆盖的显示区域,然后恢复它。这些情况是:
鼠标光标穿越显示区域。
图标拖过显示区域。
处理WM_PAINT消息要求程序写作者改变自己向显示器输出的思维方式。程序应该组织成可以保留绘制显示区域需要的所有信息,并且仅当「响应要求」-即Windows给窗口消息处理程序发送WM_PAINT消息时才进行绘制。如果程序在其它时间需要更新其显示区域,它可以强制Windows产生一个WM_PAINT消息。这看来似乎是在屏幕上显示内容的一种舍近求远的方法。但您的程序结构可以从中受益。
有效矩形和无效矩形
尽管窗口消息处理程序一旦接收到WM_PAINT消息之后,就准备更新整个显示区域,但它经常只需要更新一个较小的区域(最常见的是显示区域中的矩形区域)。显然,当对话框覆盖了部分显示区域时,情况即是如此。在擦除对话框之后,需要重画的只是先前被对话框遮住的矩形区域。(按照上面的方法,你可以试验一下,当GUI.EXE被一个窗口完全覆盖,它是不会收到消息的)
这个区域称为「无效区域」或「更新区域」。正是显示区域内无效区域的存在,才会让Windows将一个WM_PAINT消息放在应用程序的消息队列中。只有在显示区域的某一部分失效时,窗口才会接受WM_PAINT消息。
Windows内部为每个窗口保存一个「绘图信息结构」,这个结构包含了包围无效区域的最小矩形的坐标以及其它信息,这个矩形就叫做「无效矩形」,有时也称为「无效区域」。如果在窗口消息处理程序处理WM_PAINT消息之前显示区域中的另一个区域变为无效,则Windows计算出一个包围两个区域的新的无效区域(以及一个新的无效矩形),并将这种变化后的信息放在绘制信息结构中。Windows不会将多个WM_PAINT消息都放在消息队列中。
窗口消息处理程序可以通过呼叫InvalidateRect使显示区域内的矩形无效。如果消息队列中已经包含一个WM_PAINT消息,Windows将计算出新的无效矩形。否则,它将一个新的WM_PAINT消息放入消息队列中。在接收到WM_PAINT消息时,窗口消息处理程序可以取得无效矩形的坐标(我们马上就会看到这一点)。通过呼叫GetUpdateRect,可以在任何时候取得这些坐标。
在处理WM_PAINT消息处理期间,窗口消息处理程序在呼叫了BeginPaint之后,整个显示区域即变为有效。程序也可以通过呼叫ValidateRect函数使显示区域内的任意矩形区域变为有效。如果这呼叫具有令整个无效区域变为有效的效果,则目前队列中的任何WM_PAINT消息都将被删除。
总结:上面一段叙述让人感觉非常枯燥,相比之下我还是喜欢用代码说话。如果这一段看不明白不要紧,下面还会再提到。
GDI 简介
要在窗口的显示区域绘图,可以使用Windows的图形设备接口(GDI)函数。Windows提供了几个GDI函数,用于将字符串输出到窗口的显示区域内。我们已经在上一期看过DrawText函数,但是目前使用最为普遍的文字输出函数是TextOut。该函数的格式如下:
TextOut PROTO hdc:HDC,nXStart:DWORD,nYStart:DWORD,lpString:DWORD,cbString:DWORD
HDC :Dc的handle
nXStart :开始位置的X坐标
nYStart :开始位置的Y坐标
lpString :要显示的字符串
cbString :字符数量
TextOut向窗口的显示区域写入字符串。lpString参数是指向字符串的指针,cbString 是字符串的长度。nXStart和nYStart参数定义了字符串在显示区域的开始位置。hdc参数是「设备内容句柄」,它是GDI的重要部分。实际上,每个GDI函数都需要将这个句柄作为函数的第一个参数。
设备内容
读者可能还记得,句柄只不过是一个数值,Windows以它在内部使用对象。程序写作者从Windows取得句柄,然后在其它函数中使用该句柄。设备内容句柄是GDI函数的窗口「通行证」,有了这种设备内容句柄,程序写作者就能自如地在显示区域上绘图,使图形如自己所愿地变得好看或者难看。
设备内容(简称为「DC」)实际上是GDI内部保存的数据结构。设备内容与特定的显示设备(如显示器或打印机)相关。对于显示器,设备内容总是与显示器上的特定窗口相关。
设备内容中的有些值是图形「属性」,这些属性定义了GDI绘图函数工作的细节。例如,对于TextOut,设备内容的属性确定了文字的颜色、文字的背景色、x坐标和y坐标映像到窗口的显示区域的方式,以及显示文字时Windows使用的字体。
当程序需要绘图时,它必须先取得设备内容句柄。在取得了该句柄后,Windows用内定的属性值填入内部设备内容结构。在后面您会看到,可以通过呼叫不同的GDI函数改变这些默认值。利用其它的GDI函数可以取得这些属性的目前值。当然,还有其它的GDI函数能够在窗口的显示区域真正地绘图。
当程序在显示区域绘图完毕后,它必须释放设备内容句柄。句柄被程序释放后就不再有效,且不能再被使用。程序必须在处理单个消息处理期间取得和释放句柄。除了呼叫CreateDC建立的设备内容之外,程序不能在两个消息之间保存其它设备内容句柄。
Windows应用程序一般使用两种方法来取得设备内容句柄,以备在屏幕上绘图。
取得设备内容句柄:方法一
在处理WM_PAINT消息时,使用这种方法。它涉及BeginPaint和EndPaint两个函数,这两个函数需要窗口句柄(作为参数传给窗口消息处理程序)和PAINTSTRUCT结构的变量(在Windows.inc中定义)的地址为参数。Windows程序写作者通常把这一结构变量命名为ps并且在窗口消息处理程序中定义它:
PAINTSTRUCT ps ;
在处理WM_PAINT消息时,窗口消息处理程序首先呼叫BeginPaint。BeginPaint函数一般在准备绘制时导致无效区域的背景被擦除。该函数也填入ps结构的字段。BeginPaint传回的值是设备内容句柄,这一传回值通常被保存在叫做hdc的变量中。它在窗口消息处理程序中的定义如下:
HDC hdc ;
HDC数据型态定义为32位的无正负号整数。然后,程序就可以使用需要设备内容句柄的TextOut等GDI函数。呼叫EndPaint即可释放设备内容句柄。
一般地,处理WM_PAINT消息的形式如下:
.if eax == WM_PAINT ;eax为 uMsg
Invoke BeginPaint,hWnd,addr stPS
; 使用GDI函数
Invoke EndPaint,hWnd,addr stPS
Xor eax,eax
ret
.endif
在处理WM_PAINT消息时,必须成对地呼叫BeginPaint和EndPaint。如果窗口消息处理程序不处理WM_PAINT消息,则它必须将WM_PAINT消息传递给Windows中DefWindowProc(内定窗口消息处理程序)。DefWindowProc以下列代码处理WM_PAINT消息:
.if eax == WM_PAINT ;eax为 uMsg
Invoke BeginPaint,hWnd,addr stPS
Invoke EndPaint,hWnd,addr stPS
Xor eax,eax
ret
.endif
这两个BeginPaint和EndPaint呼叫之间中没有任何叙述,仅仅使先前无效区域变为有效。但以下方法是错误的:
case WM_PAINT:
return 0 ; // WRONG !!!
.if eax == WM_PAINT ;eax为 uMsg
Ret
.endif
Windows将一个WM_PAINT消息放到消息队列中,是因为显示区域的一部分无效。如果不呼叫BeginPaint和EndPaint(或者ValidateRect),则Windows不会使该区域变为有效。相反,Windows将发送另一个WM_PAINT消息,且一直发送下去。使用Spy++可以看到,潮水一般的 WM_PAINT 不断发出来,并且CPU占用率也一直很高(我是双核CPU,一个CPU占满了,因此右下角的CPU使用率一直在50%左右,如果是单核CPU就是100%了)。
绘图信息结构
前面提到过,Windows为每个窗口保存一个「绘图信息结构」,这就是PAINTSTRUCT,定义如下:
PAINTSTRUCT STRUCT
hdc DWORD ?
fErase DWORD ?
rcPaint RECT <>
fRestore DWORD ?
fIncUpdate DWORD ?
rgbReserved BYTE 32 dup(?)
PAINTSTRUCT ENDS
在程序呼叫BeginPaint时,Windows会适当填入该结构的各个字段值。使用者程序只使用前三个字段,其它字段由Windows内部使用。hdc字段是设备内容句柄。在旧版本的Windows中,BeginPaint的传回值也曾是这个设备内容句柄。在大多数情况下, fErase被标志为FALSE(0),这意味着Windows已经擦除了无效矩形的背景。这最早在BeginPaint函数中发生(如果要在窗口消息处理程序中自己定义一些背景擦除行为,可以自行处理WM_ERASEBKGND消息)。
比如,在MasmPlus GUI模板程序中处理WM_ERASEBKGND消息,如下:
.elseif uMsg == WM_PAINT
invoke BeginPaint,hWin,addr ps
invoke EndPaint, hWin,addr ps
.elseif uMsg == WM_ERASEBKGND
mov eax,0
.elseif uMsg == WM_DESTROY
invoke PostQuitMessage,NULL
就会使得这个窗口的行为看起来非常怪异。当系统特别烦忙的时候,我们的窗口就会出现类似的“病症”。下图是用记事本程序划过这个窗口之后的景象。
Windows使用WNDCLASS结构的hbrBackground字段指定的画刷来擦除背景,这个WNDCLASS结构是程序在WinMain初始化期间登录窗口类别时使用的。许多Windows程序使用白色画刷。以下叙述设定窗口类别结构字段值:
invoke GetStockObject,WHITE_BRUSH
mov wndclass.hbrBackground,EAX
不过,如果程序通过呼叫Windows函数InvalidateRect使显示区域中的矩形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在呼叫完BeginPaint后PAINTSTRUCT结构的fErase字段将为TRUE(非零)。
PAINTSTRUCT结构的rcPaint字段是RECT型态的结构。您已经在前面中看到,RECT结构定义了一个矩形,其四个字段为left、top、right和bottom。PAINTSTRUCT结构的rcPaint字段定义了无效矩形的边界,如图4-1所示。这些值均以图素为单位,并相对于显示区域的左上角。无效矩形是应该重画的区域。
图4-1 无效矩形的边界
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -