您的位置:首页 > 其它

Programming Windows程式开发设计指南->第四章 输出文本

2007-06-17 11:45 519 查看
4. 输出文本
在前一章,您看到了一个简单的Windows 98程序,它在窗口中央,或者更准确地说,在客户区中央显示一行文本。正如我们学到的,客户区是整个应用程序窗口中未被标题列、窗口边框,以及可选的菜单列、工具栏、状态列和滚动条占据的部分。简而言之,客户区是窗口中可以由程序任意书写和传递视觉信息的部分。
对于程序的客户区,您几乎可以为所欲为,只不过您不能假定窗口大小是某一特定尺寸,或者在程序运行时其大小会保持不变。如果您不熟悉图形窗口环境的程序设计,这些限制可能会使您感到惊讶:不能再假设屏幕上的一行文本一定有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消息。
 
在某些情况下,客户区的一部分被临时覆盖,Windows试图保存一个客户区,并在以后恢复它,但这不一定能成功。在以下情况下,Windows可能发送WM_PAINT消息:
· Windows擦除覆盖了部分窗口的对话框或消息框(方块)。
 
· 菜单下拉出来,然后被释放。
 
· 显示工具提示消息。
 
在某些情况下,Windows总是保存它所覆盖的客户区,然后恢复它。这些情况是:
· 鼠标光标穿越客户区。
 
· 图标拖过客户区。
 
处理WM_PAINT消息要求程序写作者改变自己向显示器输出的思维方式。程序应该组织成可以保留绘制客户区需要的所有信息,并且仅当「回应要求」-即Windows给窗口消息处理程序发送WM_PAINT消息时才进行绘制。如果程序在其他时间需要更新其客户区,它可以强制Windows产生一个WM_PAINT消息。这看来似乎是在屏幕上显示内容的一种舍近求远的方法。但您的程序结构可以从中受益。
有效矩形和无效矩形 
尽管窗口消息处理程序一旦接收到WM_PAINT消息之后,就准备更新整个客户区,但它经常只需要更新一个较小的区域(最常见的是客户区中的矩形区域)。显然,当对话框覆盖了部分客户区时,情况即是如此。在擦除对话框之后,需要重画的只是先前被对话框遮住的矩形区域。
这个区域称为「无效区域」或「更新区域」。正是客户区内无效区域的存在,才会让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 (hdc, x, y, psText, iLength) ;

TextOut向窗口的客户区写入字符串。psText参数是指向字符串的指针,iLength是字符串的长度。x和y参数定义了字符串在客户区的开始位置(不久会讲述关于它们的详细情况)。hdc参数是「设备环境句柄」,它是GDI的重要部分。实际上,每个GDI函数都需要将这个句柄作为函数的第一个参数。
设备环境 
读者可能还记得,句柄只不过是一个数值,Windows以它在内部使用对象。程序写作者从Windows取得句柄,然后在其他函数中使用该句柄。设备环境句柄是GDI函数的窗口「通行证」,有了这种设备环境句柄,程序写作者就能自如地在客户区上绘图,使图形如自己所愿地变得好看或者难看。
设备环境(简称为「DC」)实际上是GDI内部保存的数据结构。设备环境与特定的显示设备(如视频显示器或打印机)相关。对于视频显示器,设备环境总是与显示器上的特定窗口相关。
设备环境中的有些值是图形「属性」,这些属性定义了GDI绘图函数工作的细节。例如,对于TextOut,设备环境的属性确定了文本的颜色、文本的背景色、x座标和y座标映射到窗口的客户区的方式,以及显示文本时Windows使用的字体。
当程序需要绘图时,它必须先取得设备环境句柄。在取得了该句柄后,Windows用默认的属性值填入内部设备环境结构。在后面的章节中您会看到,可以通过调用不同的GDI函数改变这些预设值。利用其他的GDI函数可以取得这些属性的目前值。当然,还有其他的GDI函数能够在窗口的客户区真正地绘图。
当程序在客户区绘图完毕后,它必须释放设备环境句柄。句柄被程序释放后就不再有效,且不能再被使用。程序必须在处理单个消息处理期间取得和释放句柄。除了调用CreateDC(函数,在本章暂不讲述)建立的设备环境之外,程序不能在两个消息之间保存其他设备环境句柄。
Windows应用程序一般使用两种方法来取得设备环境句柄,以备在屏幕上绘图。
取得设备环境句柄:方法一 
在处理WM_PAINT消息时,使用这种方法。它涉及BeginPaint和EndPaint两个函数,这两个函数需要窗口句柄(作为参数传给窗口消息处理程序)和PAINTSTRUCT结构的变量(在WINUSER.H头文件中定义)的地址为参数。Windows程序写作者通常把这一结构变量命名为ps并且在窗口消息处理程序中定义它:
PAINTSTRUCT ps ;

在处理WM_PAINT消息时,窗口消息处理程序首先调用BeginPaint。BeginPaint函数一般在准备绘制时导致无效区域的背景被擦除。该函数也填入ps结构的栏位。BeginPaint传回的值是设备环境句柄,这一传回值通常被保存在叫做hdc的变量中。它在窗口消息处理程序中的定义如下:
HDC hdc ;

HDC数据类型定义为32位的无正负号整数。然后,程序就可以使用需要设备环境句柄的TextOut等GDI函数。调用EndPaint即可释放设备环境句柄。
一般地,处理WM_PAINT消息的形式如下:
case  WM_PAINT:

hdc = BeginPaint (hwnd, &ps) ;

使用GDI函数

EndPaint (hwnd, &ps) ;

return 0 ;

在处理WM_PAINT消息时,必须成对地调用BeginPaint和EndPaint。如果窗口消息处理程序不处理WM_PAINT消息,则它必须将WM_PAINT消息传递给Windows中DefWindowProc(默认窗口消息处理程序)。DefWindowProc以下列代码处理WM_PAINT消息:
case WM_PAINT:

BeginPaint (hwnd, &ps) ;

EndPaint (hwnd, &ps) ;

return 0 ;

这两个BeginPaint和EndPaint调用之间中没有任何叙述,仅仅使先前无效区域变为有效。但以下方法是错误的:
case WM_PAINT:

return 0 ;   // WRONG !!!

Windows将一个WM_PAINT消息放到消息伫列中,是因为客户区的一部分无效。如果不调用BeginPaint和EndPaint(或者ValidateRect),则Windows不会使该区域变为有效。相反,Windows将发送另一个WM_PAINT消息,且一直发送下去。
绘图信息结构 
前面提到过,Windows为每个窗口保存一个「绘图信息结构」,这就是PAINTSTRUCT,定义如下:
typedef struct tagPAINTSTRUCT

{

HDC       hdc ;

BOOL         fErase ;

RECT         rcPaint ;

BOOL     fRestore ;

BOOL     fIncUpdate ;

BYTE     rgbReserved[32] ;

} PAINTSTRUCT ;

在程序调用BeginPaint时,Windows会适当填入该结构的各个栏位值。用户程序只使用前三个栏位,其他栏位由Windows内部使用。hdc栏位是设备环境句柄。在旧版本的Windows中,BeginPaint的传回值也曾是这个设备环境句柄。在大多数情况下, fErase被标志为FALSE(0),这意味著Windows已经擦除了无效矩形的背景。这最早在BeginPaint函数中发生(如果要在窗口消息处理程序中自己定义一些背景擦除行为,可以自行处理WM_ERASEBKGND消息)。Windows使用WNDCLASS结构的hbrBackground栏位指定的画刷来擦除背景,这个WNDCLASS结构是程序在WinMain初始化期间登录窗口类别时使用的。许多Windows程序使用白色画刷。以下叙述设定窗口类别结构栏位值:
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;

不过,如果程序通过调用Windows函数InvalidateRect使客户区中的矩形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在调用完BeginPaint后PAINTSTRUCT结构的fErase栏位将为TRUE(非零)。
PAINTSTRUCT结构的rcPaint栏位是RECT类型的结构。您已经在第三章中看到,RECT结构定义了一个矩形,其四个栏位为left、top、right和bottom。PAINTSTRUCT结构的rcPaint栏位定义了无效矩形的边界,如图4-1所示。这些值均以像素为单位,并相对于客户区的左上角。无效矩形是应该重画的区域。



图4-1 无效矩形的边界
PAINTSTRUCT中的rcPaint矩形不仅是无效矩形,它还是一个「剪取」矩形。这意味著Windows将绘图操作限制在剪取矩形内(更确切地说,如果无效矩形区域不为矩形,则Windows将绘图操作限制在这个区域内)。
在处理WM_PAINT消息时,为了在更新的矩形外绘图,可以使用如下调用:
InvalidateRect (hwnd, NULL, TRUE) ;

该调用在BeginPaint调用之前进行,它使整个客户区变为无效,并擦除背景。但是,如果最后一个参数等于FALSE,则不擦除背景,原有的东西将保留在原处。
通常这是Windows程序在无论何时收到WM_PAINT消息而不考虑rcPaint结构的情况下简单地重画整个客户区最方便的方法。例如,如果在客户区的显示输出中包括了一个圆,但是只有圆的一部分落到了无效矩形中,它就使仅绘制圆的无效部分变得没有意义。这需要画整个圆。在您使用从BeginPaint传回的设备环境句柄时,Windows不会绘制rcPaint矩形外的任何部分。
在第三章的HELLOWIN程序中,我们并不关心处理WM_PAINT消息时的无效矩形。如果文本客户区恰巧在无效矩形内,则由DrawText恢复之。否则,在处理DrawText调用的某个时刻,Windows会确定它无须向显示器上输出。不过,这一决定需要时间。关心程序性能和速度的程序写作者希望在处理WM_PAINT期间使用无效矩形范围,以避免不必要的GDI调用。如果绘制时需要存取例如位图这样的磁片文件,则这就显得尤其重要。
取得设备环境句柄:方法二 
虽然最好是在处理WM_PAINT消息处理期间更新整个客户区,但是您也会发现在处理非WM_PAINT消息处理期间绘制客户区的某个部分也是非常有用的。或者您需要将设备环境句柄用于其他目的,如取得设备环境的信息。
要得到窗口客户区的设备环境句柄,可以调用GetDC来取得句柄,在使用完后调用ReleaseDC:
hdc = GetDC (hwnd) ;

使用GDI函数

ReleaseDC (hwnd, hdc) ;

与BeginPaint和EndPaint一样,GetDC和ReleaseDC函数必须成对地使用。如果在处理某消息时调用GetDC,则必须在退出窗口消息处理程序之前调用ReleaseDC。不要在一个消息中调用GetDC却在另一个消息调用ReleaseDC。
与从BeginPaint传回设备环境句柄不同,GetDC传回的设备环境句柄具有一个剪取矩形,它等于整个客户区。可以在客户区的某一部分绘图,而不只是在无效矩形上绘图(如果确实存在无效矩形)。与BeginPaint不同,GetDC不会使任何无效区域变为有效。如果需要使整个客户区有效,可以调用
ValidateRect (hwnd, NULL) ;

一般可以调用GetDC和ReleaseDC来对键盘消息(如在字处理程序中)和鼠标消息(如在画图程序中)作出反应。此时,程序可以立刻根据用户的键盘或鼠标输入来更新客户区,而不需要考虑为了窗口的无效区域而使用WM_PAINT消息。不过,一旦确实收到了WM_PAINT消息,程序就必须要收集足够的信息后才能更新显示。
与GetDC相似的函数是GetWindowDC。GetDC传回用于写入窗口客户区的设备环境句柄,而GetWindowDC传回写入整个窗口的设备环境句柄。例如,您的程序可以使用从GetWindowDC传回的设备环境句柄在窗口的标题列上写入文本。然而,程序同样也应该处理WM_NCPAINT (「非客户区绘制」)消息。
TextOut:细节 
TextOut是用于显示文本的最常用的GDI函数。语法是:
TextOut (hdc, x, y, psText, iLength) ;

以下将详细地讨论这个函数。
第一个参数是设备环境句柄,它既可以是GetDC的传回值,也可以是在处理WM_PAINT消息时BeginPaint的传回值。
设备环境的属性控制了被显示的字符串的特徵。例如,设备环境中有一个属性指定文本颜色,默认颜色为黑色;默认设备环境还定义了白色的背景。在程序向显示器输出文本时,Windows使用这个背景色来填入字符周围的矩形空间(称为「字符框」)。
该文本背景色与定义窗口类别时设置的背景并不相同。窗口类别中的背景是一个画刷,它是一种纯色或者非纯色组成的画刷,Windows用它来擦除客户区,它不是设备环境结构的一部分。在定义窗口类别结构时,大多数Windows应用程序使用WHITE_BRUSH,以便默认设备环境中的默认文本背景颜色与Windows用以擦除客户区背景的画刷颜色相同。
psText参数是指向字符串的指针,iLength是字符串中字符的个数。如果psText指向Unicode字符串,则字符串中的字节数就是iLength值的两倍。字符串中不能包含任何ASCII控制字符(如回车、换行、制表或退格),Windows会将这些控制字符显示为实心块。Text0ut不识别作为字符串结束标志的内容为零的字节(对于Unicode,是一个短整数类型的0),而需要由nLength参数指明长度。
TextOut中的x和y定义客户区内字符串的开始位置,x是水平位置,y是垂直位置。字符串中第一个字符的左上角位于座标点(x,y)。在默认的设备环境中,原点(x和y均为0的点)是客户区的左上角。如果在TextOut中将x和y设为0,则将从客户区左上角开始输出字符串。
当您阅读GDI绘图函数(例如TextOut)的文件时,就会发现传递给函数的座标常常被称为「逻辑座标」。在第五章会详细地解释这种情况。现在请注意,Windows有许多「座标映射模式」,它们用来控制GDI函数指定的逻辑座标转换为显示器的实际像素座标的方式。映射模式在设备环境中定义,默认映射模式是MM_TEXT(使用WINGDI.H中定义的标帜符)。在MM_TEXT映射模式下,逻辑单位与实际单位相同,都是像素;x的值从左向右递增,y的值从上向下递增(参看图4-2)。MM_TEXT座标系与Windows在PAINTSTRUCT结构中定义无效矩形时使用的座标系相同,这为我们带来了很多方便(但是,其他映射模式并非如此)。

 



图4-2 MM_TEXT映射方式下的x座标和y座标
设备环境也定义了一个剪裁区域。您已经看到,对于从GetDC取得的设备环境句柄,默认剪裁区域是整个客户区;而对于从BeginPaint取得的设备环境句柄,则为无效区域。Windows不会在剪裁区域之外的任何位置显示字符串。如果一个字符有一部分在剪裁区域外,则Windows将只显示此区域内的那部分。要想将输出写到窗口的客户区之外不是那么容易的,所以不用担心会无意间出现这种事情。
系统字体 
设备环境还定义了在您调用TextOut显示文本时Windows使用的字体。默认字体为「系统字体」,或用Windows头文件中的标帜符,即SYSTEM_FONT。系统字体是Windows用来在标题列、菜单和对话框中显示字符串的默认字体。
在Windows的早期版本中,系统字体是等宽(fixed-pitch)字体,这意味著所有字符均具有同样的宽度,非常类似于打字机。然而,从Windows 3.0开始,系统字体成为一种变宽(variable-pitch)字体,这意味著不同的字符具有不同的大小,比如,「W」要比「i」宽。变宽字体比等宽字体好读,这已经是公认的事实。不过,可以想见,这一转变使很多原来的Windows程序码不再适用,从而要求程序写作者学习一些使用字体的新技术。
系统字体是一种「点阵字体」,这意味著字符被定义为像素块(在第十七章,将讨论TrueType字体,它是由轮廓定义的)。至于确切的大小,系统字体的字符大小取决于视频显示器的大小。系统字体设计为至少能在显示器上显示25行80列文本。
字符大小 
要用TextOut显示多行文本,就必须确定字体的字符大小,可以根据字符的高度来定位字符的后续行,以及根据字符的宽度来定位字符的后续列。
系统字体的字符高度和平均宽度是多少?这个问题取决于视频显示器的像素大小。Windows需要的最小显示大小是640×480,但是许多用户更喜欢800×600或1024×768的显示大小。另外,对于这些较大的显示尺寸,Windows允许用户选择不同大小的系统字体。
程序可以调用GetSystemMetrics函数以取用户界面上各类视觉元件大小的信息,调用GetTextMetrics取得字体大小。GetTextMetrics传回设备环境中目前选取的字体信息,因此它需要设备环境句柄。Windows将文本大小的不同值复制到在WINGDI.H中定义的TEXTMETRIC类型的结构中。TEXTMETRIC结构有20个栏位,我们只使用前七个:
typedef struct tagTEXTMETRIC

{

LONG tmHeight ;

LONG tmAscent ;

LONG tmDescent ;

LONG tmInternalLeading ;

LONG tmExternalLeading ;

LONG tmAveCharWidth ;

LONG tmMaxCharWidth ;

其他结构栏位

}

TEXTMETRIC, * PTEXTMETRIC ;
这些栏位值的单位取决于选定的设备环境映射模式。在默认设备环境下,映射模式是MM_TEXT,因此值的大小是以像素为单位。
要使用GetTextMetrics函数,需要先定义一个结构变量(通常称为tm):
TEXTMETRIC tm ;
在需要确定文本大小时,先取得设备环境句柄,再调用GetTextMetrics:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
ReleaseDC (hwnd, hdc) ;
此后,您就可以查看文本尺寸结构中的值,并有可能保存其中的一些以备将来使用。
文本大小:细节 
TEXTMETRIC结构提供了关于目前设备环境中选用的字体的丰富信息。但是,字体的纵向大小只由5个值确定,其中4个值如图4-3所示。

 



图4-3 定义字体中纵向字元大小的4个值
最重要的值是tmHeight,它是tmAscent和tmDescent的和。这两个值表示了基准线上下字符的最大纵向高度。「间距」(leading)指打印机在两行文本间插入的空间。在TEXTMETRIC结构中,内部的间距包括在tmAscent中(因此也在tmHeight中),并且它经常是重音符号出现的地方。tmInternalLeading栏位可被设成0,在这种情况下,加重音的字母会稍稍缩短以便容纳重音符号。
TEXTMETRIC结构还包括一个不包含在tmHeight值中的栏位tmExternalLeading。它是字体设计者建议加在横向字符之间的空间大小。在安排文本行之间的空隙时,您可以接受设计者建议的值,也可以拒绝它。在系统字体中tmExternalLeading可以为0,因此我没有在图4-3中显示它。(尽管我不想告诉你们,图4-3确实就是Windows在640×480的显示解析度中使用的系统字体。)
TEXTMETRICS结构包含有描述字符宽度的两个栏位,即tmAveCharWidth(小写字母加权平均宽度)和tmMaxCharWidth(字体中最宽字符的宽度)。对于定宽字体,这两个值是相等的(图4-3中这些值分别为7和14)。
本章的范例程序还需要另一种字符宽度,即大写字母的平均宽度,这可以用tmAveCharWidth乘以150%大致计算出来。
必须认识到,系统字体的大小取决于Windows所运行的视频显示器的解析度,在某些情况下,取决于用户选取的系统字体的大小。Windows提供了一个与设备无关的图形界面,但程序写作者还是有事情要处理的。不要想当然耳地猜测字体大小来写作Windows程序,也不要把值定死,您可以使用GetTextMetrics函数取得这一信息。
格式化文本 
Windows启动后,系统字体的大小就不会发生改变,所以在程序运行过程中,程序写作者只需要调用一次GetTexMetrics。最好是在窗口消息处理程序中处理WM_CREATE消息时进行此调用,WM_CREATE消息是窗口消息处理程序接收的第一个消息。在WinMain中调用CreateWindow时,Windows会以一个WM_CREATE消息调用窗口消息处理程序。
假设要编写一个Windows程序,在客户区显示几行文本,这需要先取得字符宽度和高度。您可以在窗口消息处理程序默认义两个变量来保存平均字符宽度(cxChar)和总的字符高度(cyChar):
static int cxChar, cyChar ;
变量名的字头c代表「count」,在这里指像素数,与x和y结合,分别指宽和高。这些变量定义为static静态变量,因为它们在窗口消息处理程序中处理其他消息(如WM_PAINT)时也应该是有效的。如果变量在函数外面定义,则不需要定义为static。
下面是取得系统字体的字符宽度和高度的WM_CREATE程序码:
case WM_CREATE:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;

cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight + tm.tmExternalLeading ;

ReleaseDC (hwnd, hdc) ;
return 0 ;
注意我在计算cyChar时包括了tmExternalLeading栏位,虽然该栏位在系统字体中为0,但是因为它使得文本的可读性更好,所以还是应该把它包括进去。沿著窗口向下每隔cyChar像素就会显示一行文本。
您会发现常常需要显示格式化的数字跟简单的字符串。我在第二章讲到过,您不能使惯用的工具(可爱的printf函数)来完成这项工作,但是可以使用sprintf和Windows版的sprintf-wsprintf。这些函数与printf相似,只是把格式化字符串放到字符串中。然后,可以用TextOut将字符串输出到显示器上。非常方便的是,从sprintf和wsprintf传回的值就是字符串的长度。您可以将这个值传递给TextOut作为iLength参数。下面的程序码显示了wsprintf与TextOut的典型组合:
int iLength ;
TCHAR szBuffer [40] ;
其他行程序
iLength = wsprintf (szBuffer, TEXT ("The sum of %i and %i is %i"),
iA, iB, iA + iB) ;
TextOut (hdc, x, y, szBuffer, iLength) ;
对于这样简单的情况,可以将nLength的定义值与TextOut放在同一条叙述中,从而无需定义iLength:
TextOut (hdc, x, y, szBuffer,
wsprintf (szBuffer, TEXT ("The sum of %i and %i is %i"),
iA, iB, iA + iB)) ;
虽然这样子写起来不好看,但是功能与前者是一样的。
综合使用 
现在,我们似乎已经具备了在屏幕上显示多行文本所需要的所有知识。我们知道如何在WM_PAINT消息处理期间取得一个设备环境句柄,如何使用TextOut函数以及如何根据字符大小来安排字距,剩下的就是显示一点有意义的东西了。
在前一章里,我们大概知道从Windows的GetSystemMetrics函数中取得的信息是很有意义的,该函数传回Windows中不同视觉元件的大小信息,如图标、光标、标题列和滚动条等。它们的大小因显示卡和驱动程序的不同而有所不同。GetSystemMetrics是在程序中完成与设备无关图形输出的重要函数。
该函数需要一个参数,叫做「索引」,在Windows头文件定义了75个整数索引标帜符(标帜符的数量随著每个版本的Windows的发布而不断地增加,在Windows 1.0的程序写作者文件中仅列出了26个)。GetSystemMetrics传回一个整数,这个整数通常就是参数中指定的图形元件大小。
让我们来编写一个程序,显示一些可以从GetSystemMetrics调用中取得的信息,显示格式为每种视觉元件一行。如果我们建立一个头文件,在头文件中定义一个结构阵列,此结构包含GetSystemMetrics索引对应的Windows头文件标帜符和调用所传回的每个值对应的字符串,这样处理起来要容易一些。头文件名为SYSMETS.H,如程序4-1所示。
程序4-1 SYSMETS.H
/*---------------------------------------------------------
SYSMETS.H -- System metrics display structure
-----------------------------------------------------------*/
#define NUMLINES ((int) (sizeof sysmetrics / sizeof sysmetrics [0]))
struct
{
int Index ;
TCHAR * szLabel ;
TCHAR * szDesc ;
}
sysmetrics [] =
{
SM_CXSCREEN, TEXT ("SM_CXSCREEN"),
TEXT ("Screen width in pixels"),
SM_CYSCREEN, TEXT ("SM_CYSCREEN"),
TEXT ("Screen height in pixels"),
SM_CXVSCROLL, TEXT ("SM_CXVSCROLL"),
TEXT ("Vertical scroll width"),
SM_CYHSCROLL, TEXT ("SM_CYHSCROLL"),
TEXT ("Horizontal scroll height"),
SM_CYCAPTION, TEXT ("SM_CYCAPTION"),
TEXT ("Caption bar height"),
SM_CXBORDER, TEXT ("SM_CXBORDER"),
TEXT ("Window border width"),
SM_CYBORDER, TEXT ("SM_CYBORDER"),
TEXT ("Window border height"),
SM_CXFIXEDFRAME,TEXT ("SM_CXFIXEDFRAME"),
TEXT ("Dialog window frame width"),
SM_CYFIXEDFRAME,TEXT ("SM_CYFIXEDFRAME"),
TEXT ("Dialog window frame height"),
SM_CYVTHUMB, TEXT ("SM_CYVTHUMB"),
TEXT ("Vertical scroll thumb height"),
SM_CXHTHUMB, TEXT ("SM_CXHTHUMB"),
TEXT ("Horizontal scroll thumb width"),
SM_CXICON, TEXT ("SM_CXICON"),
TEXT ("Icon width"),
SM_CYICON, TEXT ("SM_CYICON"),
TEXT ("Icon height"),
SM_CXCURSOR, TEXT ("SM_CXCURSOR"),
TEXT ("Cursor width"),
SM_CYCURSOR, TEXT ("SM_CYCURSOR"),
TEXT ("Cursor height"),
SM_CYMENU, TEXT ("SM_CYMENU"),
TEXT ("Menu bar height"),
SM_CXFULLSCREEN,TEXT ("SM_CXFULLSCREEN"),
TEXT ("Full screen client area width"),
SM_CYFULLSCREEN,TEXT ("SM_CYFULLSCREEN"),
TEXT ("Full screen client area height"),
SM_CYKANJIWINDOW,TEXT ("SM_CYKANJIWINDOW"),
TEXT ("Kanji window height"),
SM_MOUSEPRESENT, TEXT ("SM_MOUSEPRESENT"),
TEXT ("Mouse present flag"),
SM_CYVSCROLL, TEXT ("SM_CYVSCROLL"),
TEXT ("Vertical scroll arrow height"),
SM_CXHSCROLL, TEXT ("SM_CXHSCROLL"),
TEXT ("Horizontal scroll arrow width"),
SM_DEBUG, TEXT ("SM_DEBUG"),
TEXT ("Debug version flag"),
SM_SWAPBUTTON, TEXT ("SM_SWAPBUTTON"),
TEXT ("Mouse buttons swapped flag"),
SM_CXMIN, TEXT ("SM_CXMIN"),
TEXT ("Minimum window width"),
SM_CYMIN, TEXT ("SM_CYMIN"),
TEXT ("Minimum window height"),
SM_CXSIZE, TEXT ("SM_CXSIZE"),
TEXT ("Min/Max/Close button width"),
SM_CYSIZE, TEXT ("SM_CYSIZE"),
TEXT ("Min/Max/Close button height"),
SM_CXSIZEFRAME, TEXT ("SM_CXSIZEFRAME"),
TEXT ("Window sizing frame width"),
SM_CYSIZEFRAME, TEXT ("SM_CYSIZEFRAME"),
TEXT ("Window sizing frame height"),
SM_CXMINTRACK, TEXT ("SM_CXMINTRACK"),
TEXT ("Minimum window tracking width"),
SM_CYMINTRACK, TEXT ("SM_CYMINTRACK"),
TEXT ("Minimum window tracking height"),
SM_CXDOUBLECLK, TEXT ("SM_CXDOUBLECLK"),
TEXT ("Double click x tolerance"),
SM_CYDOUBLECLK, TEXT ("SM_CYDOUBLECLK"),
TEXT ("Double click y tolerance"),
SM_CXICONSPACING,TEXT ("SM_CXICONSPACING"),
TEXT ("Horizontal icon spacing"),
SM_CYICONSPACING,TEXT ("SM_CYICONSPACING"),
TEXT ("Vertical icon spacing"),
SM_MENUDROPALIGNMENT, TEXT ("SM_MENUDROPALIGNMENT"),
TEXT ("Left or right menu drop"),
SM_PENWINDOWS, TEXT ("SM_PENWINDOWS"),
TEXT ("Pen extensions installed"),
SM_DBCSENABLED, TEXT ("SM_DBCSENABLED"),
TEXT ("Double-Byte Char Set enabled"),
SM_CMOUSEBUTTONS, TEXT ("SM_CMOUSEBUTTONS"),
TEXT ("Number of mouse buttons"),
SM_SECURE, TEXT ("SM_SECURE"),
TEXT ("Security present flag"),
SM_CXEDGE, TEXT ("SM_CXEDGE"),
TEXT ("3-D border width"),
SM_CYEDGE, TEXT ("SM_CYEDGE"),
TEXT ("3-D border height"),
SM_CXMINSPACING, TEXT ("SM_CXMINSPACING"),
TEXT ("Minimized window spacing width"),
SM_CYMINSPACING, TEXT ("SM_CYMINSPACING"),
TEXT ("Minimized window spacing height"),
SM_CXSMICON, TEXT ("SM_CXSMICON"),
TEXT ("Small icon width"),
SM_CYSMICON, TEXT ("SM_CYSMICON"),
TEXT ("Small icon height"),
SM_CYSMCAPTION, TEXT ("SM_CYSMCAPTION"),
TEXT ("Small caption height"),
SM_CXSMSIZE, TEXT ("SM_CXSMSIZE"),
TEXT ("Small caption button width"),
SM_CYSMSIZE, TEXT ("SM_CYSMSIZE"),
TEXT ("Small caption button height"),
SM_CXMENUSIZE, TEXT ("SM_CXMENUSIZE"),
TEXT ("Menu bar button width"),
SM_CYMENUSIZE, TEXT ("SM_CYMENUSIZE"),
TEXT ("Menu bar button height"),
SM_ARRANGE, TEXT ("SM_ARRANGE"),
TEXT ("How minimized windows arranged"),
SM_CXMINIMIZED, TEXT ("SM_CXMINIMIZED"),
TEXT ("Minimized window width"),
SM_CYMINIMIZED, TEXT ("SM_CYMINIMIZED"),
TEXT ("Minimized window height"),
SM_CXMAXTRACK, TEXT ("SM_CXMAXTRACK"),
TEXT ("Maximum draggable width"),
SM_CYMAXTRACK, TEXT ("SM_CYMAXTRACK"),
TEXT ("Maximum draggable height"),
SM_CXMAXIMIZED, TEXT ("SM_CXMAXIMIZED"),
TEXT ("Width of maximized window"),
SM_CYMAXIMIZED, TEXT ("SM_CYMAXIMIZED"),
TEXT ("Height of maximized window"),
SM_NETWORK, TEXT ("SM_NETWORK"),
TEXT ("Network present flag"),
SM_CLEANBOOT, TEXT ("SM_CLEANBOOT"),
TEXT ("How system was booted"),
SM_CXDRAG, TEXT ("SM_CXDRAG"),
TEXT ("Avoid drag x tolerance"),
SM_CYDRAG, TEXT ("SM_CYDRAG"),
TEXT ("Avoid drag y tolerance"),
SM_SHOWSOUNDS, TEXT ("SM_SHOWSOUNDS"),
TEXT ("Present sounds visually"),
SM_CXMENUCHECK, TEXT ("SM_CXMENUCHECK"),
TEXT ("Menu check-mark width"),
SM_CYMENUCHECK, TEXT ("SM_CYMENUCHECK"),
TEXT ("Menu check-mark height"),
SM_SLOWMACHINE, TEXT ("SM_SLOWMACHINE"),
TEXT ("Slow processor flag"),
SM_MIDEASTENABLED, TEXT ("SM_MIDEASTENABLED"),
TEXT ("Hebrew and Arabic enabled flag"),
SM_MOUSEWHEELPRESENT, TEXT ("SM_MOUSEWHEELPRESENT"),
TEXT ("Mouse wheel present flag"),
SM_XVIRTUALSCREEN, TEXT ("SM_XVIRTUALSCREEN"),
TEXT ("Virtual screen x origin"),
SM_YVIRTUALSCREEN, TEXT ("SM_YVIRTUALSCREEN"),
TEXT ("Virtual screen y origin"),
SM_CXVIRTUALSCREEN, TEXT ("SM_CXVIRTUALSCREEN"),
TEXT ("Virtual screen width"),
SM_CYVIRTUALSCREEN, TEXT ("SM_CYVIRTUALSCREEN"),
TEXT ("Virtual screen height"),
SM_CMONITORS, TEXT ("SM_CMONITORS"),
TEXT ("Number of monitors"),
SM_SAMEDISPLAYFORMAT, TEXT ("SM_SAMEDISPLAYFORMAT"),
TEXT ("Same color format flag")
} ;
显示信息的程序命名为SYSMETS1。SYSMETS1.C的源代码如程序4-2所示。现在大多数程序码看起来都很熟悉。WinMain中的程序码实际上与HELLOWIN中的程序码相同,并且WndProc中的大部分程序码都已经讨论过了。
程序4-2 SYSMETS1.C
/*------------------------------------------------------------------
SYSMETS1.C -- System Metrics Display Program No. 1
(c) Charles Petzold, 1998
----------------------------------------------------------------*/
#include <windows.h>
#include "sysmets.h"

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("SysMets1") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;

return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 1"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxChar, cxCaps, cyChar ;
HDC hdc ;
int i ;
PAINTSTRUCT ps ;
TCHAR szBuffer [10] ;
TEXTMETRIC tm ;

switch (message)
{
case WM_CREATE:
hdc = GetDC (hwnd) ;

GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
cyChar = tm.tmHeight + tm.tmExternalLeading ;

ReleaseDC (hwnd, hdc) ;
return 0 ;

case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;

for (i = 0 ; i < NUMLINES ; i++)
{
TextOut (hdc, 0, cyChar * i,
sysmetrics[i].szLabel,
lstrlen (sysmetrics[i].szLabel)) ;

TextOut (hdc, 22 * cxCaps, cyChar * i,
sysmetrics[i].szDesc,
lstrlen (sysmetrics[i].szDesc)) ;

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
TextOut (hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer,
wsprintf (szBuffer, TEXT ("%5d"),
GetSystemMetrics (sysmetrics[i].iIndex))) ;
SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
图4-4显示了在标准VGA上运行的SYSMETS1。在程序客户区的前两行可以看到,屏幕宽度是640个像素,屏幕高度是480个像素,这两个值以及程序所显示的其他值可能会因视频显示器类型的不同而不同。

 



图4-4 SYSMETS1的显示
SYSMETS1.C窗口消息处理程序 
SYSMETS1.C程序中的WndProc窗口消息处理程序处理三个消息:WM_CREATE、WM_PAINT和WM_DESTROY。WM_DESTROY消息的处理方法与第三章的HELLOWIN程序相同。
WM_CREATE消息是窗口消息处理程序接收到的第一个消息。在CreateWindow函数建立窗口时,Windows产生这个消息。在处理WM_CREATE消息时,SYSMETS1调用GetDC取得窗口的设备环境,并调用GetTextMetrics取得默认系统字体的文本大小。SYSMETS1将平均字符宽度保存在cxChar中,将字符的总高度(包括外部间距)保存在cyChar中。
SYSMETS1还将大写字母的平均宽度保存在静态变量cxCaps中。对于固定宽度的字体, cxCaps等于cxChar。对于可变宽度字体,cxCaps设定为cxChar乘以150%。对于可变宽度字体,TEXTMETRIC结构中的tmPitchAndFamily栏位的低位为1,对于固定宽度字体,该值为0。 SYSMETS1使用这个位从cxChar计算cxCaps:
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
SYSMETS1在处理WM_PAINT消息处理期间完成所有窗口建立工作。通常,窗口消息处理程序先调用BeginPaint取得设备环境句柄,然后用一道for叙述对SYSMETS.H中定义的sysmetrics结构的每一行进行回圈。三列文本用三个TextOut函数显示,对于每一列,TextOut的第三个参数都设定为:
cyChar * i
这个参数指示了字符串顶端相对于客户区顶部的像素位置。
第一条TextOut叙述在第一列显示了大写标帜符。TextOut的第二个参数是0,这是说文本从客户区的左边缘开始。文本的内容来自sysmetrics结构的szLabel栏位。我使用Windows函数lstrlen来计算字符串的长度,它是TextOut需要的最后一个参数。
第二条TextOut叙述显示了对系统尺寸值的描述。这些描述存放在sysmetrics结构的szDesc栏位中。在这种情况下,TextOut的第二个参数设定为:
22 * cxCaps
第一列显示的最长的大写标帜符有20个字符,因此第二列必须在第一列文本开头向右20 × cxCaps处开始。我使用22,以在两列之间加一点多余的空间。
第三条TextOut叙述显示从GetSystemMetrics函数取得的数值。变宽字体使得格式化向右对齐的数值有些棘手。从0到9的数字具有相同的宽度,但是这个宽度比空格宽度大。数值可以比一个数字宽,所以不同的数值应该从不同的横向位置开始。
那么,如果我们指定字符串结束的像素位置,而不是指定字符串的开始位置,以此向右对齐数值,是否会容易一些呢?用SetTextAlign函数就可以做到这一点。在SYSMETS1调用:
SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
之后,传给后续TextOut函数的座标将指定字符串的右上角,而不是左上角。
显示列数的TextOut函数的第二个参数设定为:
22 * cxCaps + 40 * cxChar
值40*cxChar包含了第二列的宽度和第三列的宽度。在TextOut函数之后,另一个对SetTextAlign的调用将对齐方式设定回普通方式,以进行下次回圈。
空间不够 
在SYSMETS1程序中存在著一个很难处理的问题:除非您有一个大屏幕跟高解析度的显示卡,否则就无法看到系统尺度列表的最后几行。如果窗口太窄,甚至根本看不到值。
SYSMETS1不知道这个问题。否则我们就会显示一个消息框(方块)说「抱歉!」程序甚至不知道它的客户区有多大,它从窗口顶部开始输出文本,并仰赖Windows裁剪超出客户区底部的内容。
显然,这很不理想。为了解决这个问题,我们的第一个任务是确定程序在客户区内能输出多少内容。
客户区的大小 
如果您使用过现有的Windows应用程序,可能会发现窗口的尺寸变化极大。窗口最大化时(假定窗口只有标题列并且没有菜单),客户区几乎占据了整个屏幕。这一最大化了的客户区的尺寸可以通过以SM_CXFULLSCREEN和SM_CYFULLSCREEN为参数调用GetSystemMetrics来获得。窗口的最小尺寸可以很小,有时甚至不存在,更不用说客户区了。
在最近一章,我们使用GetClientRect函数来取得客户区的大小。使用这个函数没有什么不好,但是在您每次要使用信息时就去调用它一遍是没有效率的。确定窗口客户区大小的更好方法是在窗口消息处理程序中处理WM_SIZE消息。在窗口大小改变时,Windows给窗口消息处理程序发送一个WM_SIZE消息。传给窗口消息处理程序的lParam参数的低字组中包含客户区的宽度,高字组中包含客户区的高度。要保存这些尺寸,需要在窗口消息处理程序中定义两个静态变量:
static int cxClient, cyClient ;
与cxChar和cyChar相似,这两个变量在窗口消息处理程序默认义为静态变量,因为在以后处理其他消息时会用到它们。处理WM_SIZE的方法如下:
case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
return 0 ;
实际上您会在每个Windows程序中看到类似的程序码。LOWORD和HIWORD宏在Windows头文件WINDEF.H中定义。这些宏的定义看起来像这样:
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
这两个宏传回WORD值(16位的无正负号整数,范围从0到0xFFFF)。一般,将这些值保存在32位有号整数中。这就不会牵扯到任何转换问题,并使得这些值在以后需要的任何计算中易于使用。
在许多Windows程序中,WM_SIZE消息必然跟著一个WM_PAINT消息。为什么呢?因为在我们定义窗口类别时指定窗口类别样式为:
CS_HREDRAW | CS_VREDRAW
这种窗口类别样式告诉Windows,如果水平或者垂直大小发生改变, 则强制更新客户区。
用如下公式计算可以在客户区内显示的文本的总行数:
cyClient / cyChar
如果客户区的高度太小以至无法显示一个完整的字符,这个公式的结果可以为0。类似地,在客户区的水平方向可以显示的小写字符的近似数目为:
cxClient / cxChar
如果在处理WM_CREATE消息处理期间取得cxChar和cyChar,则不用担心在这两个计算公式中会出现被0除的情况。在WinMain调用CreateWindow时,窗口消息处理程序接收一个WM_CREATE消息。在WinMain调用ShowWindow之后接收到第一个WM_CREATE消息,此时cxChar和cyChar已经被赋予正的非零值了。
如果客户区的大小不足以容纳所有的内容,那么,知道窗口客户区的大小只是为用户提供了在客户区内滚动文本的第一步。如果您对其他有类似需求的Windows应用程序很熟悉,就很可能知道,这种情况下,我们需要使用「滚动条」。

滚动条
 

滚动条是图形用户界面中最好的功能之一,它很容易使用,而且提供了很好的视觉回馈效果。您可以使用滚动条显示任何东西--无论是文本、图形、表格、数据库记录、图像或是网页,只要它所需的空间超出了窗口的客户区所能提供的空间,就可以使用滚动条。
滚动条既有垂直方向的(供上下移动),也有水平方向的(供左右移动)。用户可以使用鼠标在滚动条两端的箭头上或者在箭头之间的区域中点一下,这时,「滚动框(方块)」在滚动条内的移动位置与所显示的信息在整个文件中的近似相关位置成比例。用户也可以用鼠标拖动滚动框(方块)到特定的位置。图4-5显示了垂直滚动条的建议用法。

 



图4-5 垂直卷动列
有时,程序写作者对滚动概念很难理解,因为他们的观点与用户的观点不同:用户向下滚动是想看到文件较下面的部分;但是,程序实际上是将文件相对于显示窗口向上移动。Windows文件和头文件标帜符是依据用户的观点:向上滚动意味著朝文件的开头移动;向下滚动意味著朝文件尾部移动。
很容易在应用程序中包含水平或者垂直的滚动条,程序写作者只需要在CreateWindow的第三个参数中包括窗口样式(WS)标帜符WS_VSCROLL(垂直滚动)和/或WS_HSCROLL(水平滚动)即可。这些滚动条通常放在窗口的右部和底部,伸展为客户区的整个长度或宽度。客户区不包含滚动条所占据的空间。对于特定的显示驱动程序和显示解析度,垂直滚动条的宽度和水平滚动条的高度是恒定的。如果需要这些值,可以使用GetSystemMetrics调用来取得(如前面的程序那样)。
Windows负责处理对滚动条的所有鼠标操作,但是,窗口滚动条没有自动的键盘界面。如果想用光标键来完成滚动功能,则必须提供这方面的程序码(我们将在下一章另一个版本的SYSMETS程序中做到这一点)。
滚动条的范围和位置 
每个滚动条均有一个相关的「范围」(这是一对整数,分别代表最小值和最大值)和「位置」(它是滚动框(方块)在此范围内的位置)。当滚动框(方块)在滚动条的顶部(或左部)时,滚动框(方块)的位置是范围的最小值;在滚动条的底部(或右部)时,滚动框(方块)的位置是范围的最大值。
在默认情况下,滚动条的范围是从0(顶部或左部)至100(底部或右部),但将范围改变为更方便于程序的数值也是很容易的:
SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;
参数iBar为SB_VERT或者SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画滚动条,则设置bRedraw为TRUE(如果在调用SetScrollRange后,调用了影响滚动条位置的其他函数,则应该将bRedraw设定为FALSE以避免过多的重画)。
滚动框(方块)的位置总是离散的整数值。例如,范围为0至4的滚动条具有5个滚动框(方块)位置,如图4-6所示。

 



图4-6 具有5个卷动方块位置的卷动列
您可以使用SetScrollPos在滚动条范围内设置新的滚动框(方块)位置:
SetScrollPos (hwnd, iBar, iPos, bRedraw) ;
参数iPos是新位置,它必须在iMin至iMax的范围内。Windows提供了类似的函数(GetScrollRange和GetScrollPos)来取得滚动条的目前范围和位置。
在程序内使用滚动条时,程序写作者与Windows共同负责维护滚动条以及更新滚动框(方块)的位置。下面是Windows对滚动条的处理:
处理所有滚动条鼠标事件
 
当用户在滚动条内单击鼠标时,提供一种「反相显示」的闪烁
 
当用户在滚动条内拖动滚动框(方块)时,移动滚动框(方块)
 
为包含滚动条窗口的窗口消息处理程序发送滚动条消息
 
以下是程序写作者应该完成的工作:
初始化滚动条的范围和位置
 
处理窗口消息处理程序的滚动条消息
 
更新滚动条内滚动框(方块)的位置
 
更改客户区的内容以回应对滚动条的更改
 
像生活中的大多数事情一样,在我们看一些程序码时这些会显得更加有意义。
滚动条消息 
在用鼠标单击滚动条或者拖动滚动框(方块)时,Windows给窗口消息处理程序发送WM_VSCROLL(供上下移动)和WM_HSCROLL(供左右移动)消息。在滚动条上的每个鼠标动作都至少产生两个消息,一条在按下鼠标按钮时产生,一条在释放按钮时产生。
和所有的消息一样, WM_VSCROLL和WM_HSCROLL也带有wParam和lParam消息参数。对于来自作为窗口的一部分而建立的滚动条消息,您可以忽略lParam;它只用于作为子窗口而建立的滚动条(通常在对话框内)。
wParam消息参数被分为一个低字组和一个高字组。wParam的低字组是一个数值,它指出了鼠标对滚动条进行的操作。这个数值被看作一个「通知码」。通知码的值由以SB(代表「scroll bar(滚动条)」)开头的标帜符定义。以下是在WINUSER.H中定义的通知码:
#define SB_LINEUP 0
#define SB_LINELEFT 0
#define SB_LINEDOWN 1
#define SB_LINERIGHT 1
#define SB_PAGEUP 2
#define SB_PAGELEFT 2
#define SB_PAGEDOWN 3
#define SB_PAGERIGHT 3
#define SB_THUMBPOSITION 4
#define SB_THUMBTRACK 5
#define SB_TOP 6
#define SB_LEFT 6
#define SB_BOTTOM 7
#define SB_RIGHT 7
#define SB_ENDSCROLL 8
包含LEFT和RIGHT的标帜符用于水平滚动条,包含UP、DOWN、TOP和BOTTOM的标帜符用于垂直滚动条。鼠标在滚动条的不同区域单击所产生的通知码如图4-7所示。

 



图4-7 用於卷动列讯息的wParam值的识别字
如果在滚动条的各个部位按住鼠标键,程序就能收到多个滚动条消息。当释放鼠标键后,程序会收到一个带有SB_ENDSCROLL通知码的消息。一般可以忽略这个消息,Windows不会去改变滚动框(方块)的位置,而您可以在程序中调用SetScrollPos来改变滚动框(方块)的位置。
当把鼠标的光标放在滚动框(方块)上并按住鼠标键时,您就可以移动滚动框(方块)。这样就产生了带有SB_THUMBTRACK和SB_THUMBPOSITION通知码的滚动条消息。在wParam的低字组是SB_THUMBTRACK时,wParam的高字组是用户在拖动滚动框(方块)时的目前位置。该位置位于滚动条范围的最小值和最大值之间。在wParam的低字组是SB_THUMBPOSITION时,wParam的高字组是用户释放鼠标键后滚动框(方块)的最终位置。对于其他的滚动条操作,wParam的高字组应该被忽略。
为了给用户提供回馈,Windows在您用鼠标拖动滚动框(方块)时移动它,同时您的程序会收到SB_THUMBTRACK消息。然而,如果不通过调用SetScrollPos来处理SB_THUMBTRACK或SB_THUMBPOSITION消息,在用户释放鼠标键后,滚动框(方块)会迅速跳回原来的位置。
程序能够处理SB_THUMBTRACK或SB_THUMBPOSITION消息,但一般不同时处理两者。如果处理SB_THUMBTRACK消息,在用户拖动滚动框(方块)时您需要移动客户区的内容。而如果处理SB_THUMBPOSITION消息,则只需在用户停止拖动滚动框(方块)时移动客户区的内容。处理SB_THUMBTRACK消息更好一些(但更困难),对于某些类型的数据,您的程序可能很难跟上产生的消息。
WINUSER.H头文件还包括SB_TOP、SB_BOTTOM、SB_LEFT和SB_RIGHT通知码,指出滚动条已经被移到了它的最小或最大位置。然而,对于作为应用程序窗口一部分而建立的滚动条来说,永远不会接收到这些通知码。
在滚动条范围使用32位的值也是有效的,尽管这不常见。然而,wParam的高字组只有16位的大小,它不能适当地指出SB_THUMBTRACK和SB_THUMBPOSITION操作的位置。在这种情况下,需要使用GetScrollInfo函数(在下面描述)来得到信息。
SYSMETS中加入滚动功能 
前面的说明已经很详尽了,现在,要将那些东西动手做做看了。让我们开始时简单些,从垂直滚动著手,因为我们实在太需要垂直滚动了,而暂时还可以不用水平滚动。SYSMET2如程序4-3所示。这个程序可能是滚动条的最简单的应用。
程序4-3 SYSMETS2.C
/*------------------------------------------------------------------
SYSMETS2.C -- System Metrics Display Program No. 2
(c) Charles Petzold, 1998
------------------------------------------------------------------*/
#include <windows.h>
#include "sysmets.h"

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("SysMets2") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}

hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 2"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxChar, cxCaps, cyChar, cyClient, iVscrollPos ;
HDC hdc ;
int i, y ;
PAINTSTRUCT ps ;
TCHAR szBuffer[10] ;
TEXTMETRIC tm ;
switch (message)
{
case WM_CREATE:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
cyChar = tm.tmHeight + tm.tmExternalLeading ;

ReleaseDC (hwnd, hdc) ;
SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ;
SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;
return 0 ;

case WM_SIZE:
cyClient = HIWORD (lParam) ;
return 0 ;

case WM_VSCROLL:
switch (LOWORD (wParam))
{
case SB_LINEUP:
iVscrollPos -= 1 ;
break ;

case SB_LINEDOWN:
iVscrollPos += 1 ;
break ;

case SB_PAGEUP:
iVscrollPos -= cyClient / cyChar ;
break ;

case SB_PAGEDOWN:
iVscrollPos += cyClient / cyChar ;
break ;

case SB_THUMBPOSITION:
iVscrollPos = HIWORD (wParam) ;
break ;

default :
break ;
}

iVscrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ;
if (iVscrollPos != GetScrollPos (hwnd, SB_VERT))
{
SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;
InvalidateRect (hwnd, NULL, TRUE) ;
}
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
for (i = 0 ; i < NUMLINES ; i++)
{
y = cyChar * (i - iVscrollPos) ;
TextOut (hdc, 0, y,
sysmetrics[i].szLabel,
lstrlen (sysmetrics[i].szLabel)) ;

TextOut (hdc, 22 * cxCaps, y,
sysmetrics[i].szDesc,
lstrlen (sysmetrics[i].szDesc)) ;

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer,
wsprintf (szBuffer, TEXT ("%5d"),
GetSystemMetrics (sysmetrics[i].iIndex))) ;
SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;

case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
新的CreateWindow调用在第三个参数中包含了WS_VSCROLL窗口样式,从而在窗口中加入了垂直滚动条,其窗口样式为:
WS_OVERLAPPEDWINDOW | WS_VSCROLL
WndProc窗口消息处理程序在处理WM_CREATE消息时增加了两条叙述,以设置垂直滚动条的范围和初始位置:
SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ;
SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;
sysmetrics结构具有NUMLINES行文本,所以滚动条范围被设定为0至NUMLINES-1。滚动条的每个位置对应于在客户区顶部显示的一个文本行。如果滚动框(方块)的位置为0,则第一行会被放置在客户区的顶部。如果位置大于0,其他行就会出现在客户区的顶部。当位置为NUMLINES-1时,则最后一行文本出现在客户区的顶部。
为了有助于处理WM_VSCROLL消息,在窗口消息处理程序中定义了一个静态变量iVscrollPos,这一变量是滚动条内滚动框(方块)的目前位置。对于SB_LINEUP和SB_LINEDOWN,只需要将滚动框(方块)调整一个单位的位置。对于SB_PAGEUP和SB_PAGEDOWN,我们想移动一整面的内容,或者移动cyClient /cyChar个单位的位置。对于SB_THUMBPOSITION,新的滚动框(方块)位置是wParam的高字组。SB_ENDSCROLL和SB_THUMBTRACK消息被忽略。
在程序依据收到的WM_VSCROLL消息计算出新的iVscrollPos值后,用min和max宏来调整iVscrollPos,以确保它在最大值与最小值之间。程序然后将iVscrollPos与调用GetScrollPos取得的先前位置相比较,如果滚动位置发生了变化,则使用SetScrollPos来进行更新,并且调用InvalidateRect使整个窗口无效。
InvalidateRect调用产生一个WM_PAINT消息。SYSMETS1在处理WM_PAINT消息时,每一行的y座标计算公式为:
cyChar * i
在SYSMETS2中,计算公式为:
cyChar * (i - iVscrollPos)
回圈仍然显示NUMLINES行文本,但是对于非零值的iVscrollPos是负数。程序实际上在客户区以外显示这些文本行。当然,Windows不会显示这些行,因此屏幕显得乾净和漂亮。
前面说过,我们一开始不想弄得太复杂,这样的程序码很浪费,效率很低。下面我们对此加以修改,但是先要考虑在WM_VSCROLL消息之后更新客户区的方法。
绘图程序的组织 
在处理完滚动条消息后,SYSMETS2不更新客户区,相反,它调用InvalidateRect使客户区失效。这导致Windows将一个WM_PAINT消息放入消息伫列中。
最好能使Windows程序在回应WM_PAINT消息时完成所有的客户区绘制功能。因为程序必须在一接收到WM_PAINT消息时就更新整个客户区,如果在程序的其他部分也绘制的话,将很可能使程序码重复。
首先,您可能对这种拐弯抹角的方式感到厌烦。在Windows的早期,因为这种方式与文本模式的程序设计差别太大,程序写作者感到这种概念很难理解。并且,程序要不断地通过马上绘制画面来回应键盘和鼠标。这样做既方便又有效,但是在很多情况下,这完全不必要。当您掌握了在回应WM_PAINT消息时积累绘制客户区所需要的全部信息的原则之后,会对这种结果感到满意的。
如同SYSMETS2示范的,程序仍然需要在处理非WM_PAINT消息时更新特定的客户区,使用InvalidateRect就很方便,您可以用它使客户区内的特定矩形或者整个客户区失效。
只将窗口客户区标记为无效以产生WM_PAINT消息,对于某些应用程序来说也许不是完全令人满意的选择。在调用InvalidateRect之后,Windows将WM_PAINT消息放入消息伫列中,最后由窗口消息处理程序处理它。然而,Windows将WM_PAINT消息当成低优先顺序消息,如果系统有许多其他的动作正在发生,那么也许会让您等待一会儿工夫。这时,当对话框消失时,将会出现一些空白的「洞」,程序仍然等待更新它的窗口。
如果您希望立即更新无效区域,可以在调用InvalidateRect之后调用UpdateWindow:
UpdateWindow (hwnd) ;
如果客户区的任一部分无效,则UpdateWindow将导致Windows用WM_PAINT消息调用窗口消息处理程序(如果整个客户区有效,则不调用窗口消息处理程序)。这一WM_PAINT消息不进入消息伫列,直接由Windows调用窗口消息处理程序。窗口消息处理程序完成更新后立即退出,Windows将控制传回给程序中UpdateWindow调用之后的叙述。
您可能注意到,UpdateWindow与WinMain中用来产生第一个WM_PAINT消息的函数相同。最初建立窗口时,整个客户区内容变为无效,UpdateWindow指示窗口消息处理程序绘制客户区。

建立更好的滚动
 

SYSMETS2动作良好,但它只是模仿其他程序中的滚动条,并且效率很低。很快我将示范一个新的版本,改进它的不足。也许最有趣的是这个新版本不使用目前所讨论的四个滚动条函数。相反,它将使用Win32 API中才有的新函数。
滚动条信息函数 
滚动条文件(在/Platform SDK/User Interface Services/Controls/Scroll Bars中)指出SetScrollRange、SetScrollPos、GetScrollRange和GetScrollPos函数是「过时的」,但这并不完全正确。这些函数在Windows 1.0中就出现了,在Win32 API中升级以处理32位参数。它们仍然具有良好的功能。而且,它们不与Windows程序设计中新函数相冲突,这就是我在此书中仍使用它们的原因。
Win32 API介绍的两个滚动条函数称作SetScrollInfo和GetScrollInfo。这些函数可以完成以前函数的全部功能,并增加了两个新特性。
第一个功能涉及滚动框(方块)的大小。您可能注意到,滚动框(方块)大小在SYSMETS2程序中是固定的。然而,在您可能使用到的一些Windows应用程序中,滚动框(方块)大小与在窗口中显示的文件大小成比例。显示的大小称作「页面大小」。演算法为:

 

可以使用SetScrollInfo来设置页面大小(从而设置了滚动框(方块)的大小),如将要看到的SYSMETS3程序所示。
GetScrollInfo函数增加了第二个重要的功能,或者说它改进了目前API的不足。假设您要使用65,536或更大单位的范围,这在16位Windows中是不可能的。当然在Win32中,函数被定义为可接受32位参数,因此是没有问题的。(记住如果使用这样大的范围,滚动框(方块)的实际物理位置数仍然由滚动条的像素大小限制)。然而,当使用SB_THUMBTRACK或SB_THUMBPOSITION通知码得到WM_VSCROLL或WM_HSCROLL消息时,只提供了16位数据来指出滚动框(方块)的目前位置。通过GetScrollInfo函数可以取得真实的32位值。
SetScrollInfo和GetScrollInfo函数的语法是
SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;
像在其他滚动条函数中那样,iBar参数是SB_VERT或SB_HORZ,它还可以是用于滚动条控制的SB_CTL。SetScrollInfo的最后一个参数可以是TRUE或FALSE,指出了是否要Windows重新绘制计算了新信息后的滚动条。
两个函数的第三个参数是SCROLLINFO结构,定义为:
typedef struct tagSCROLLINFO
{
UINT cbSize ; // set to sizeof (SCROLLINFO)
UINT fMask ; // values to set or get
int nMin ; // minimum range value
int nMax ; // maximum range value
UINT nPage ; // page size
int nPos ; // current position
int nTrackPos ;// current tracking position
}
SCROLLINFO, * PSCROLLINFO ;
在程序中,可以定义如下的SCROLLINFO结构类型:
SCROLLINFO si ;
在调用SetScrollInfo或GetScrollInfo之前,必须将cbSize栏位设定为结构的大小:
si.cbSize = sizeof (si) ;

si.cbSize = sizeof (SCROLLINFO) ;
逐渐熟悉Windows后,您就会发现另外几个结构像这个结构一样,第一个栏位指出了结构大小。这个栏位使将来的Windows版本可以扩充结构并添加新的功能,并且仍然与以前编译的版本兼容。
把fMask栏位设定为一个以上以SIF字头开头的旗标,并且可以使用C的位操作OR运算子(|)组合这些旗标。
SetScrollInfo函数使用SIF_RANGE旗标时,必须把nMin和nMax栏位设定为所需的滚动条范围。GetScrollInfo函数使用SIF_RANGE旗标时,应把nMin和nMax栏位设定为从函数传回的目前范围。
SIF_POS旗标也一样。当通过SetScrollInfo使用它时,必须把结构的nPos栏位设定为所需的位置。可以通过GetScrollInfo使用SIF_POS旗标来取得目前位置。
使用SIF_PAGE旗标能够取得页面大小。用SetScrollInfo函数把nPage设定为所需的页面大小。GetScrollInfo使用SIF_PAGE旗标可以取得目前页面的大小。如果不想得到比例化的滚动条,就不要使用该旗标。
当处理带有SB_THUMBTRACK或SB_THUMBPOSITION通知码的WM_VSCROLL或WM_HSCROLL消息时,通过GetScrollInfo只使用SIF_TRACKPOS旗标。从函数的传回中,SCROLLINFO结构的nTrackPos栏位将指出目前的32位的滚动框(方块)位置。
在SetScrollInfo函数中仅使用SIF_DISABLENOSCROLL旗标。如果指定了此旗标,而且新的滚动条参数使滚动条消失,则该滚动条就不能使用了(下面会有更多的解释)。
SIF_ALL旗标是SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的组合。在WM_SIZE消息处理期间设置滚动条参数时,这是很方便的(在SetScrollInfo函数中指定SIF_TRACKPOS后,它会被忽略)。这在处理滚动条消息时也是很方便的。
滚动范围 
在SYSMETS2中,滚动范围设置最小为0,最大为NUMLINES-1。当滚动条位置是0时,第一行信息显示在客户区的顶部;当滚动条的位置是NUMLINES-1时,最后一行显示在客户区的顶部,并且看不见其他行。
可以说SYSMETS2滚动范围太大。事实上只需把信息最后一行显示在客户区的底部而不是顶部即可。我们可以对SYSMETS2作出一些修改以达到此点。当处理WM_CREATE消息时不设置滚动条范围,而是等到接收到WM_SIZE消息后再做此工作:
iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ;
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;
假定NUMLINES等于75,并假定特定窗口大小是:50(cyChar除以cyClient)。换句话说,我们有75行信息但只有50行可以显示在客户区中。使用上面的两行程序码,把范围设置最小为0,最大为25。当滚动条位置等于0时,程序显示0到49行。当滚动条位置等于1时,程序显示1到50行;并且当滚动条位置等于25(最大值)时,程序显示25到74行。很明显需要对程序的其他部分做出修改,但这是可行的。
新滚动条函数的一个好的功能是当使用与滚动条范围一样大的页面时,它已经为您做掉了一大堆杂事。可以像下面的程序码一样使用SCROLLINFO结构和SetScrollInfo:
si.cbSize = sizeof (SCROLLINFO) ;
si.cbMask = SIF_RANGE | SIF_PAGE ;
si.nMin = 0 ;
si.nMax = NUMLINES - 1 ;
si.nPage = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
这样做之后,Windows会把最大的滚动条位置限制为si.nMax - si.nPage +1而不是si.nMax。像前面那样做出假设:NUMLINES等于75 (所以si.nMax等于74),si.nPage等于50。这意味著最大的滚动条位置限制为74 - 50 + 1,即25。这正是我们想要的。
当页面大小与滚动条范围一样大时,会发生什么情况呢?在这个例子中,就是nPage等于75或更大的情况。Windows通常隐藏滚动条,因为它并不需要。如果不想隐藏滚动条,可在调用SetScrollInfo时使用SIF_DISABLENOSCROLL,Windows只是让那个滚动条不能被使用,而不隐藏它。
SYSMETS
 
SYSMETS3-此章中最后的SYSMETS程序版本-显示在程序4-4中。此版本使用SetScrollInfo和GetScrollInfo函数,添加左右滚动的水平滚动条,并能更有效地重画客户区。
程序4-4 SYSMETS3
SYSMETS3.C
/*------------------------------------------------------------------
SYSMETS3.C -- System Metrics Display Program No. 3
(c) Charles Petzold, 1998
----------------------------------------------------------------*/
#include <windows.h>
#include "sysmets.h"

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("SysMets3") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;

wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc= WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("Program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}

hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ;
HDC hdc ;
int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ;
PAINTSTRUCT ps ;
SCROLLINFO si ;
TCHAR szBuffer[10] ;
TEXTMETRIC tm ;

switch (message)
{
case WM_CREATE:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
cyChar = tm.tmHeight + tm.tmExternalLeading ;

ReleaseDC (hwnd, hdc) ;
// Save the width of the three columns
iMaxWidth = 40 * cxChar + 22 * cxCaps ;
return 0 ;

case WM_SIZE:
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;

// Set vertical scroll bar range and page size
si.cbSize = sizeof (si) ;
si.fMask = SIF_RANGE | SIF_PAGE ;
si.nMin = 0 ;
si.nMax = NUMLINES - 1 ;
si.nPage = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
// Set horizontal scroll bar range and page size
si.cbSize = sizeof (si) ;
si.fMask = SIF_RANGE | SIF_PAGE ;
si.nMin = 0 ;
si.nMax = 2 + iMaxWidth / cxChar ;
si.nPage = cxClient / cxChar ;
SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
return 0 ;

case WM_VSCROLL:
// Get all the vertical scroll bar information
si.cbSize = sizeof (si) ;
si.fMask = SIF_ALL ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
// Save the position for comparison later on
iVertPos = si.nPos ;
switch (LOWORD (wParam))
{
case SB_TOP:
si.nPos = si.nMin ;
break ;

case SB_BOTTOM:
si.nPos = si.nMax ;
break ;

case SB_LINEUP:
si.nPos - = 1 ;
break ;

case SB_LINEDOWN:
si.nPos += 1 ;
break ;

case SB_PAGEUP:
si.nPos -= si.nPage ;
break ;

case SB_PAGEDOWN:
si.nPos += si.nPage ;
break ;

case SB_THUMBTRACK:
si.nPos = si.nTrackPos ;
break ;

default:
break ;
}
// Set the position and then retrieve it. Due to adjustments
// by Windows it may not be the same as the value set.

si.fMask = SIF_POS ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
GetScrollInfo (hwnd, SB_VERT, &si) ;

// If the position has changed, scroll the window and update it
if (si.nPos != iVertPos)
{
ScrollWindow ( hwnd, 0, cyChar * (iVertPos - si.nPos),
NULL, NULL) ;
UpdateWindow (hwnd) ;
}
return 0 ;
case WM_HSCROLL:
// Get all the vertical scroll bar information
si.cbSize = sizeof (si) ;
si.fMask = SIF_ALL ;

// Save the position for comparison later on
GetScrollInfo (hwnd, SB_HORZ, &si) ;
iHorzPos = si.nPos ;

switch (LOWORD (wParam))
{
case SB_LINELEFT:
si.nPos -= 1 ;
break ;

case SB_LINERIGHT:
si.nPos += 1 ;
break ;

case SB_PAGELEFT:
si.nPos -= si.nPage ;
break ;

case SB_PAGERIGHT:
si.nPos += si.nPage ;
break ;

case SB_THUMBPOSITION:
si.nPos = si.nTrackPos ;
break ;

default :
break ;
}
// Set the position and then retrieve it. Due to adjustments
// by Windows it may not be the same as the value set.

si.fMask = SIF_POS ;
SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ;
GetScrollInfo (hwnd, SB_HORZ, &si) ;

// If the position has changed, scroll the window

if (si.nPos != iHorzPos)
{
ScrollWindow ( hwnd, cxChar * (iHorzPos - si.nPos), 0,
NULL, NULL) ;
}
return 0 ;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
// Get vertical scroll bar position
si.cbSize = sizeof (si) ;
si.fMask = SIF_POS ;
GetScrollInfo (hwnd, SB_VERT, &si) ;
iVertPos = si.nPos ;

// Get horizontal scroll bar position
GetScrollInfo (hwnd, SB_HORZ, &si) ;
iHorzPos = si.nPos ;
// Find painting limits
iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ;
iPaintEnd = min ( NUMLINES - 1,
iVertPos + ps.rcPaint.bottom / cyChar) ;

for (i = iPaintBeg ; i <= iPaintEnd ; i++)
{
x = cxChar * (1 - iHorzPos) ;
y = cyChar * (i - iVertPos) ;

TextOut (hdc, x, y,
sysmetrics[i].szLabel,
lstrlen (sysmetrics[i].szLabel)) ;

TextOut (hdc, x + 22 * cxCaps, y,
sysmetrics[i].szDesc,
lstrlen (sysmetrics[i].szDesc)) ;

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,
wsprintf (szBuffer, TEXT ("%5d"),
GetSystemMetrics (sysmetrics[i].iIndex))) ;

SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
}

EndPaint (hwnd, &ps) ;
return 0 ;

case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
这个版本的程序仰赖Windows保存滚动条信息并做边界检查。在WM_VSCROLL和WM_HSCROLL处理的开始,它取得所有的滚动条信息,根据通知码调整位置,然后调用SetScrollInfo设置其位置。程序然后调用GetScrollInfo。如果该位置超出了SetScrollInfo调用的范围,则由Windows来纠正该位置并且在GetScrollInfo调用中传回正确的值。
SYSMETS3使用ScrollWindow函数在窗口的客户区中滚动信息而不是重画它。虽然该函数很复杂(在新版本的Windows中已被更复杂的ScrollWindowEx所替代),SYSMETS3仍以相当简单的方式使用它。函数的第二个参数给出了水平滚动客户区的数值,第三个参数是垂直滚动客户区的数值,单位都是像素。
ScrollWindow的最后两个参数设定为NULL,这指出了要滚动整个客户区。Windows自动把客户区中未被滚动操作覆盖的矩形设为无效。这会产生WM_PAINT消息。再也不需要InvalidateRect了。注意ScrollWindow不是GDI函数,因为它不需设备环境句柄。它是少数几个非GDI的Windows函数之一,它可以改变窗口的客户区外观。很特殊但不方便,它是随滚动条函数一起记载在文件中。
WM_HSCROLL处理拦截SB_THUMBPOSITION通知码并忽略SB_THUMBTRACK。因而,如果用户在水平滚动条上拖动滚动框(方块),在用户释放鼠标按钮之前,程序不会水平滚动窗口的内容。
WM_VSCROLL的方法与之不同:程序拦截SB_THUMBTRACK消息并忽略SB_THUMBPOSITION。因而,程序随用户在垂直滚动条上拖动滚动框(方块)而垂直地滚动内容。这种想法很好,但应注意:一旦用户发现程序会立即回应拖动的滚动框(方块),他们就会不断地来回拖动滚动框(方块)。幸运的是现在的PC快得可以胜任这种严酷的测试。但是在较慢的机器上,可以考虑为GetSystemMetrics使用SB_SLOWMACHINE参数来替代这种处理。
加快WM_PAINT处理的一个方法由SYSMETS3展示:WM_PAINT处理程序确定无效区域中的文本行并仅仅重画这些行。当然,程序码复杂一些,但速度很快。
不用鼠标怎么办 
在Windows的早期,有大量的用户不喜欢使用鼠标,而且,Windows自身也不要求必须有鼠标。虽然,没有鼠标的PC现在走上了单色显示器和点阵打印机的没落之路,但我仍然建议您编写可以使用键盘来产生与鼠标操作相同效果的程序,尤其对于像滚动条这样的基本操作对象更是如此。因为我们的键盘有一组光标移动键,所以应该实作同样的操作。
在下一章,您将学习使用键盘和在SYSMETS3中增加键盘界面的方法。您可能会注意到,SYSMETS3似乎在通知码等于SB_TOP和SB_BOTTOM时处理了WM_VSCROLL消息。前面已经提到过,窗口消息处理程序不从滚动条接收这些消息,所以,目前这是多余的程序码。当我们在下一章再次回到这个程序时,您将会明白这样做的原因。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息