Windows程式开发设计指南--输出文字
2012-08-31 23:33
501 查看
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向视窗的显示区域写入字串。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并且在视窗讯息处理程式中定义它:
在处理WM_PAINT讯息时,视窗讯息处理程式首先呼叫BeginPaint。BeginPaint函式一般在准备绘制时导致无效区域的背景被擦除。该函式也填入ps结构的栏位。BeginPaint传回的值是装置内容代号,这一传回值通常被保存在叫做hdc的变数中。它在视窗讯息处理程式中的定义如下:
HDC资料型态定义为32位元的无正负号整数。然後,程式就可以使用需要装置内容代号的TextOut等GDI函式。呼叫EndPaint即可释放装置内容代号。
一般地,处理WM_PAINT讯息的形式如下:
在处理WM_PAINT讯息时,必须成对地呼叫BeginPaint和EndPaint。如果视窗讯息处理程式不处理WM_PAINT讯息,则它必须将WM_PAINT讯息传递给Windows中DefWindowProc(内定视窗讯息处理程式)。DefWindowProc以下列代码处理WM_PAINT讯息:
这两个BeginPaint和EndPaint呼叫之间中没有任何叙述,仅仅使先前无效区域变为有效。但以下方法是错误的:
Windows将一个WM_PAINT讯息放到讯息伫列中,是因为显示区域的一部分无效。如果不呼叫BeginPaint和EndPaint(或者ValidateRect),则Windows不会使该区域变为有效。相反,Windows将发送另一个WM_PAINT讯息,且一直发送下去。
绘图资讯结构
前面提到过,Windows为每个视窗保存一个「绘图资讯结构」,这就是PAINTSTRUCT,定义如下:
在程式呼叫BeginPaint时,Windows会适当填入该结构的各个栏位值。使用者程式只使用前三个栏位,其他栏位由Windows内部使用。hdc栏位是装置内容代号。在旧版本的Windows中,BeginPaint的传回值也曾是这个装置内容代号。在大多数情况下, fErase被标志为FALSE(0),这意味著Windows已经擦除了无效矩形的背景。这最早在BeginPaint函式中发生(如果要在视窗讯息处理程式中自己定义一些背景擦除行为,可以自行处理WM_ERASEBKGND讯息)。Windows使用WNDCLASS结构的hbrBackground栏位指定的画刷来擦除背景,这个WNDCLASS结构是程式在WinMain初始化期间登录视窗类别时使用的。许多Windows程式使用白色画刷。以下叙述设定视窗类别结构栏位值:
不过,如果程式通过呼叫Windows函式InvalidateRect使显示区域中的矩形失效,则该函式的最後一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在呼叫完BeginPaint後PAINTSTRUCT结构的fErase栏位将为TRUE(非零)。
PAINTSTRUCT结构的rcPaint栏位是RECT型态的结构。您已经在第三章中看到,RECT结构定义了一个矩形,其四个栏位为left、top、right和bottom。PAINTSTRUCT结构的rcPaint栏位定义了无效矩形的边界,如图4-1所示。这些值均以图素为单位,并相对於显示区域的左上角。无效矩形是应该重画的区域。
PAINTSTRUCT中的rcPaint矩形不仅是无效矩形,它还是一个「剪取」矩形。这意味著Windows将绘图操作限制在剪取矩形内(更确切地说,如果无效矩形区域不为矩形,则Windows将绘图操作限制在这个区域内)。
在处理WM_PAINT讯息时,为了在更新的矩形外绘图,可以使用如下呼叫:
该呼叫在BeginPaint呼叫之前进行,它使整个显示区域变为无效,并擦除背景。但是,如果最後一个参数等於FALSE,则不擦除背景,原有的东西将保留在原处。
通常这是Windows程式在无论何时收到WM_PAINT讯息而不考虑rcPaint结构的情况下简单地重画整个显示区域最方便的方法。例如,如果在显示区域的显示输出中包括了一个圆,但是只有圆的一部分落到了无效矩形中,它就使仅绘制圆的无效部分变得没有意义。这需要画整个圆。在您使用从BeginPaint传回的装置内容代号时,Windows不会绘制rcPaint矩形外的任何部分。
在第三章的HELLOWIN程式中,我们并不关心处理WM_PAINT讯息时的无效矩形。如果文字显示区域恰巧在无效矩形内,则由DrawText恢复之。否则,在处理DrawText呼叫的某个时刻,Windows会确定它无须向显示器上输出。不过,这一决定需要时间。关心程式性能和速度的程式写作者希望在处理WM_PAINT期间使用无效矩形范围,以避免不必要的GDI呼叫。如果绘制时需要存取例如点阵图这样的磁片档案,则这就显得尤其重要。
取得装置内容代号:方法二
虽然最好是在处理WM_PAINT讯息处理期间更新整个显示区域,但是您也会发现在处理非WM_PAINT讯息处理期间绘制显示区域的某个部分也是非常有用的。或者您需要将装置内容代号用於其他目的,如取得装置内容的资讯。
要得到视窗显示区域的装置内容代号,可以呼叫GetDC来取得代号,在使用完後呼叫ReleaseDC:
与BeginPaint和EndPaint一样,GetDC和ReleaseDC函式必须成对地使用。如果在处理某讯息时呼叫GetDC,则必须在退出视窗讯息处理程式之前呼叫ReleaseDC。不要在一个讯息中呼叫GetDC却在另一个讯息呼叫ReleaseDC。
与从BeginPaint传回装置内容代号不同,GetDC传回的装置内容代号具有一个剪取矩形,它等於整个显示区域。可以在显示区域的某一部分绘图,而不只是在无效矩形上绘图(如果确实存在无效矩形)。与BeginPaint不同,GetDC不会使任何无效区域变为有效。如果需要使整个显示区域有效,可以呼叫
一般可以呼叫GetDC和ReleaseDC来对键盘讯息(如在字处理程式中)和滑鼠讯息(如在画图程式中)作出反应。此时,程式可以立刻根据使用者的键盘或滑鼠输入来更新显示区域,而不需要考虑为了视窗的无效区域而使用WM_PAINT讯息。不过,一旦确实收到了WM_PAINT讯息,程式就必须要收集足够的资讯後才能更新显示。
与GetDC相似的函式是GetWindowDC。GetDC传回用於写入视窗显示区域的装置内容代号,而GetWindowDC传回写入整个视窗的装置内容代号。例如,您的程式可以使用从GetWindowDC传回的装置内容代号在视窗的标题列上写入文字。然而,程式同样也应该处理WM_NCPAINT (「非显示区域绘制」)讯息。
TextOut:细节
TextOut是用於显示文字的最常用的GDI函式。语法是:
以下将详细地讨论这个函式。
第一个参数是装置内容代号,它既可以是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结构中定义无效矩形时使用的座标系相同,这为我们带来了很多方便(但是,其他映射方式并非如此)。
装置内容也定义了一个剪裁区域。您已经看到,对於从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个栏位,我们只使用前七个:
这些栏位值的单位取决於选定的装置内容映射方式。在内定装置内容下,映射方式是MM_TEXT,因此值的大小是以图素为单位。
要使用GetTextMetrics函式,需要先定义一个结构变数(通常称为tm):
在需要确定文字大小时,先取得装置内容代号,再呼叫GetTextMetrics:
此後,您就可以查看文字尺寸结构中的值,并有可能保存其中的一些以备将来使用。
文字大小:细节
TEXTMETRIC结构提供了关於目前装置内容中选用的字体的丰富资讯。但是,字体的纵向大小只由5个值确定,其中4个值如图4-3所示。
最重要的值是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):
变数名的字首c代表「count」,在这里指图素数,与x和y结合,分别指宽和高。这些变数定义为static静态变数,因为它们在视窗讯息处理程式中处理其他讯息(如WM_PAINT)时也应该是有效的。如果变数在函式外面定义,则不需要定义为static。
下面是取得系统字体的字元宽度和高度的WM_CREATE程式码:
注意我在计算cyChar时包括了tmExternalLeading栏位,虽然该栏位在系统字体中为0,但是因为它使得文字的可读性更好,所以还是应该把它包括进去。沿著视窗向下每隔cyChar图素就会显示一行文字。
您会发现常常需要显示格式化的数字跟简单的字串。我在第二章讲到过,您不能使惯用的工具(可爱的printf函式)来完成这项工作,但是可以使用sprintf和Windows版的sprintf-wsprintf。这些函式与printf相似,只是把格式化字串放到字串中。然後,可以用TextOut将字串输出到显示器上。非常方便的是,从sprintf和wsprintf传回的值就是字串的长度。您可以将这个值传递给TextOut作为iLength参数。下面的程式码显示了wsprintf与TextOut的典型组合:
对於这样简单的情况,可以将nLength的定义值与TextOut放在同一条叙述中,从而无需定义iLength:
虽然这样子写起来不好看,但是功能与前者是一样的。
综合使用
现在,我们似乎已经具备了在萤幕上显示多行文字所需要的所有知识。我们知道如何在WM_PAINT讯息处理期间取得一个装置内容代号,如何使用TextOut函式以及如何根据字元大小来安排字距,剩下的就是显示一点有意义的东西了。
在前一章里,我们大概知道从Windows的GetSystemMetrics函式中取得的资讯是很有意义的,该函式传回Windows中不同视觉元件的大小资讯,如图示、游标、标题列和卷动列等。它们的大小因显示卡和驱动程式的不同而有所不同。GetSystemMetrics是在程式中完成与装置无关图形输出的重要函式。
该函式需要一个参数,叫做「索引」,在Windows表头档案定义了75个整数索引识别字(识别字的数量随著每个版本的Windows的发布而不断地增加,在Windows 1.0的程式写作者文件中仅列出了26个)。GetSystemMetrics传回一个整数,这个整数通常就是参数中指定的图形元件大小。
让我们来编写一个程式,显示一些可以从GetSystemMetrics呼叫中取得的资讯,显示格式为每种视觉元件一行。如果我们建立一个表头档案,在表头档案中定义一个结构阵列,此结构包含GetSystemMetrics索引对应的Windows表头档案识别字和呼叫所传回的每个值对应的字串,这样处理起来要容易一些。表头档案名为SYSMETS.H,如程式4-1所示。
显示资讯的程式命名为SYSMETS1。SYSMETS1.C的原始码如程式4-2所示。现在大多数程式码看起来都很熟悉。WinMain中的程式码实际上与HELLOWIN中的程式码相同,并且WndProc中的大部分程式码都已经讨论过了。
图4-4显示了在标准VGA上执行的SYSMETS1。在程式显示区域的前两行可以看到,萤幕宽度是640个图素,萤幕高度是480个图素,这两个值以及程式所显示的其他值可能会因视讯显示器型态的不同而不同。
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:
SYSMETS1在处理WM_PAINT讯息处理期间完成所有视窗建立工作。通常,视窗讯息处理程式先呼叫BeginPaint取得装置内容代号,然後用一道for叙述对SYSMETS.H中定义的sysmetrics结构的每一行进行回圈。三列文字用三个TextOut函式显示,对於每一列,TextOut的第三个参数都设定为:
这个参数指示了字串顶端相对於显示区域顶部的图素位置。
第一条TextOut叙述在第一列显示了大写识别字。TextOut的第二个参数是0,这是说文字从显示区域的左边缘开始。文字的内容来自sysmetrics结构的szLabel栏位。我使用Windows函式lstrlen来计算字串的长度,它是TextOut需要的最後一个参数。
第二条TextOut叙述显示了对系统尺寸值的描述。这些描述存放在sysmetrics结构的szDesc栏位中。在这种情况下,TextOut的第二个参数设定为:
第一列显示的最长的大写识别字有20个字元,因此第二列必须在第一列文字开头向右20 × cxCaps处开始。我使用22,以在两列之间加一点多余的空间。
第三条TextOut叙述显示从GetSystemMetrics函式取得的数值。变宽字体使得格式化向右对齐的数值有些棘手。从0到9的数字具有相同的宽度,但是这个宽度比空格宽度大。数值可以比一个数字宽,所以不同的数值应该从不同的横向位置开始。
那么,如果我们指定字串结束的图素位置,而不是指定字串的开始位置,以此向右对齐数值,是否会容易一些呢?用SetTextAlign函式就可以做到这一点。在SYSMETS1呼叫:
之後,传给後续TextOut函式的座标将指定字串的右上角,而不是左上角。
显示列数的TextOut函式的第二个参数设定为:
值40*cxChar包含了第二列的宽度和第三列的宽度。在TextOut函式之後,另一个对SetTextAlign的呼叫将对齐方式设定回普通方式,以进行下次回圈。
空间不够
在SYSMETS1程式中存在著一个很难处理的问题:除非您有一个大萤幕跟高解析度的显示卡,否则就无法看到系统尺度列表的最後几行。如果视窗太窄,甚至根本看不到值。
SYSMETS1不知道这个问题。否则我们就会显示一个讯息方块说「抱歉!」程式甚至不知道它的显示区域有多大,它从视窗顶部开始输出文字,并仰赖Windows裁剪超出显示区域底部的内容。
显然,这很不理想。为了解决这个问题,我们的第一个任务是确定程式在显示区域内能输出多少内容。
显示区域的大小
如果您使用过现有的Windows应用程式,可能会发现视窗的尺寸变化极大。视窗最大化时(假定视窗只有标题列并且没有功能表),显示区域几乎占据了整个萤幕。这一最大化了的显示区域的尺寸可以通过以SM_CXFULLSCREEN和SM_CYFULLSCREEN为参数呼叫GetSystemMetrics来获得。视窗的最小尺寸可以很小,有时甚至不存在,更不用说显示区域了。
在最近一章,我们使用GetClientRect函式来取得显示区域的大小。使用这个函式没有什么不好,但是在您每次要使用资讯时就去呼叫它一遍是没有效率的。确定视窗显示区域大小的更好方法是在视窗讯息处理程式中处理WM_SIZE讯息。在视窗大小改变时,Windows给视窗讯息处理程式发送一个WM_SIZE讯息。传给视窗讯息处理程式的lParam参数的低字组中包含显示区域的宽度,高字组中包含显示区域的高度。要保存这些尺寸,需要在视窗讯息处理程式中定义两个静态变数:
与cxChar和cyChar相似,这两个变数在视窗讯息处理程式内定义为静态变数,因为在以後处理其他讯息时会用到它们。处理WM_SIZE的方法如下:
实际上您会在每个Windows程式中看到类似的程式码。LOWORD和HIWORD巨集在Windows表头档案WINDEF.H中定义。这些巨集的定义看起来像这样:
这两个巨集传回WORD值(16位元的无正负号整数,范围从0到0xFFFF)。一般,将这些值保存在32位元有号整数中。这就不会牵扯到任何转换问题,并使得这些值在以後需要的任何计算中易於使用。
在许多Windows程式中,WM_SIZE讯息必然跟著一个WM_PAINT讯息。为什么呢?因为在我们定义视窗类别时指定视窗类别样式为:
这种视窗类别样式告诉Windows,如果水平或者垂直大小发生改变, 则强制更新显示区域。
用如下公式计算可以在显示区域内显示的文字的总行数:
如果显示区域的高度太小以至无法显示一个完整的字元,这个公式的结果可以为0。类似地,在显示区域的水平方向可以显示的小写字元的近似数目为:
如果在处理WM_CREATE讯息处理期间取得cxChar和cyChar,则不用担心在这两个计算公式中会出现被0除的情况。在WinMain呼叫CreateWindow时,视窗讯息处理程式接收一个WM_CREATE讯息。在WinMain呼叫ShowWindow之後接收到第一个WM_CREATE讯息,此时cxChar和cyChar已经被赋予正的非零值了。
如果显示区域的大小不足以容纳所有的内容,那么,知道视窗显示区域的大小只是为使用者提供了在显示区域内卷动文字的第一步。如果您对其他有类似需求的Windows应用程式很熟悉,就很可能知道,这种情况下,我们需要使用「卷动列」。
卷动列
卷动列是图形使用者介面中最好的功能之一,它很容易使用,而且提供了很好的视觉回馈效果。您可以使用卷动列显示任何东西--无论是文字、图形、表格、资料库记录、图像或是网页,只要它所需的空间超出了视窗的显示区域所能提供的空间,就可以使用卷动列。
卷动列既有垂直方向的(供上下移动),也有水平方向的(供左右移动)。使用者可以使用滑鼠在卷动列两端的箭头上或者在箭头之间的区域中点一下,这时,「卷动方块」在卷动列内的移动位置与所显示的资讯在整个文件中的近似相关位置成比例。使用者也可以用滑鼠拖动卷动方块到特定的位置。图4-5显示了垂直卷动列的建议用法。
有时,程式写作者对卷动概念很难理解,因为他们的观点与使用者的观点不同:使用者向下卷动是想看到文件较下面的部分;但是,程式实际上是将文件相对於显示视窗向上移动。Windows文件和表头档案识别字是依据使用者的观点:向上卷动意味著朝文件的开头移动;向下卷动意味著朝文件尾部移动。
很容易在应用程式中包含水平或者垂直的卷动列,程式写作者只需要在CreateWindow的第三个参数中包括视窗样式(WS)识别字WS_VSCROLL(垂直卷动)和/或WS_HSCROLL(水平卷动)即可。这些卷动列通常放在视窗的右部和底部,伸展为显示区域的整个长度或宽度。显示区域不包含卷动列所占据的空间。对於特定的显示驱动程式和显示解析度,垂直卷动列的宽度和水平卷动列的高度是恒定的。如果需要这些值,可以使用GetSystemMetrics呼叫来取得(如前面的程式那样)。
Windows负责处理对卷动列的所有滑鼠操作,但是,视窗卷动列没有自动的键盘介面。如果想用游标键来完成卷动功能,则必须提供这方面的程式码(我们将在下一章另一个版本的SYSMETS程式中做到这一点)。
卷动列的范围和位置
每个卷动列均有一个相关的「范围」(这是一对整数,分别代表最小值和最大值)和「位置」(它是卷动方块在此范围内的位置)。当卷动方块在卷动列的顶部(或左部)时,卷动方块的位置是范围的最小值;在卷动列的底部(或右部)时,卷动方块的位置是范围的最大值。
在内定情况下,卷动列的范围是从0(顶部或左部)至100(底部或右部),但将范围改变为更方便於程式的数值也是很容易的:
参数iBar为SB_VERT或者SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画卷动列,则设置bRedraw为TRUE(如果在呼叫SetScrollRange後,呼叫了影响卷动列位置的其他函式,则应该将bRedraw设定为FALSE以避免过多的重画)。
卷动方块的位置总是离散的整数值。例如,范围为0至4的卷动列具有5个卷动方块位置,如图4-6所示。
您可以使用SetScrollPos在卷动列范围内设置新的卷动方块位置:
参数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中定义的通知码:
包含LEFT和RIGHT的识别字用於水平卷动列,包含UP、DOWN、TOP和BOTTOM的识别字用於垂直卷动列。滑鼠在卷动列的不同区域单击所产生的通知码如图4-7所示。
如果在卷动列的各个部位按住滑鼠键,程式就能收到多个卷动列讯息。当释放滑鼠键後,程式会收到一个带有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所示。这个程式可能是卷动列的最简单的应用。
新的CreateWindow呼叫在第三个参数中包含了WS_VSCROLL视窗样式,从而在视窗中加入了垂直卷动列,其视窗样式为:
WndProc视窗讯息处理程式在处理WM_CREATE讯息时增加了两条叙述,以设置垂直卷动列的范围和初始位置:
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座标计算公式为:
在SYSMETS2中,计算公式为:
回圈仍然显示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将导致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函式的语法是
像在其他卷动列函式中那样,iBar参数是SB_VERT或SB_HORZ,它还可以是用於卷动列控制的SB_CTL。SetScrollInfo的最後一个参数可以是TRUE或FALSE,指出了是否要Windows重新绘制计算了新资讯後的卷动列。
两个函式的第三个参数是SCROLLINFO结构,定义为:
在程式中,可以定义如下的SCROLLINFO结构型态:
在呼叫SetScrollInfo或GetScrollInfo之前,必须将cbSize栏位设定为结构的大小:
或
逐渐熟悉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讯息後再做此工作:
假定NUMLINES等於75,并假定特定视窗大小是:50(cyChar除以cyClient)。换句话说,我们有75行资讯但只有50行可以显示在显示区域中。使用上面的两行程式码,把范围设置最小为0,最大为25。当卷动列位置等於0时,程式显示0到49行。当卷动列位置等於1时,程式显示1到50行;并且当卷动列位置等於25(最大值)时,程式显示25到74行。很明显需要对程式的其他部分做出修改,但这是可行的。
新卷动列函式的一个好的功能是当使用与卷动列范围一样大的页面时,它已经为您做掉了一大堆杂事。可以像下面的程式码一样使用SCROLLINFO结构和SetScrollInfo:
这样做之後,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函式,添加左右卷动的水平卷动列,并能更有效地重画显示区域。
这个版本的程式仰赖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讯息。前面已经提到过,视窗讯息处理程式不从卷动列接收这些讯息,所以,目前这是多余的程式码。当我们在下一章再次回到这个程式时,您将会明白这样做的原因。
在前一章,您看到了一个简单的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 无效矩形的边界 |
在处理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座标 |
系统字体
装置内容还定义了在您呼叫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个值 |
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程式中的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 垂直卷动列 |
很容易在应用程式中包含水平或者垂直的卷动列,程式写作者只需要在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 (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_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讯息。前面已经提到过,视窗讯息处理程式不从卷动列接收这些讯息,所以,目前这是多余的程式码。当我们在下一章再次回到这个程式时,您将会明白这样做的原因。
相关文章推荐
- Windows程式开发设计指南(十七)文字和字体
- Windows程式开发设计指南(二十二)声音与音乐
- Windows程式开发设计指南(八)计时器
- Windows程式开发设计指南(二十三)领略Internet
- Windows程式开发设计指南(十五)与装置无关的点阵图
- Windows程式开发设计指南(九)子视窗控制项
- Windows程式开发设计指南(十六)调色盘管理器
- Windows程式开发设计指南(十)功能表及其他资源
- Windows程式开发设计指南(十八)Metafile
- Windows程式开发设计指南--开始
- Windows程式开发设计指南(十一)对话方块
- Windows程式开发设计指南(十四)点阵图和Bitblt
- Windows程式开发设计指南--Unicode简介
- Windows程式开发设计指南--视窗和讯息
- Windows程式开发设计指南(十二)剪贴簿
- Windows程式开发设计指南(十三)使用印表机
- Windows程式开发设计指南--图形基础
- Windows程式开发设计指南(十九)多重文件介面
- Windows程式开发设计指南(六) 键盘
- Windows程式开发设计指南(二十一)动态连结程式库