【第四章 输出文本-1】
我们的网站有论坛了。60块一年的那种。很便宜吧?很爽啊!问题是人气太差。不过没关系。一切都会变好的。今天我受到很大的打击。可能我们网站的访问量有水分。实际访问量可能不到原来的十分之一。原来是23000多我倒~~~
学习笔记是我学习的方式。我是给自己解释。如果我能解释得很明白,就说明这一章我通过了。所以我写的学习笔记很长很长很长。简直比书上写的还长。
第四章有些错误让我给发现了,我是不是很牛啊?如下:
★注一★第76页的 图4-3 是错误的。不过第121页还有一个相同的图,那个图才是正确的。
★注二★第92页:下数第九行开始,书中把SB_THUMBPOSITION,SB_THUMBTRACK说成是消息,说什么 SB_THUMBTRACK消息,SB_THUMBPOSITION消息等,以SB为前缀的不是消息,而是通知码,这是概念问题。
第四章的内容很多,也很重要。所以写第四章的学习笔记对我来讲有很大的难度。我需要很长时间才能完成。文章可能很长,所以我分成两段了。跟前三章一样,在第四章您又会碰到很多看不懂的术语。这是没有办法的事情。多看几遍吧。
在前一章我们看到了一个简单的Windows程序HelloWin。它在客户区中央显示一行文本。客户区是整个应用程序中末被标题栏,窗口边框以及菜单栏,工具栏,状态栏和滚动条占据的部分。也就是说客户区是窗口中可以由程序任意书写和传递信息的部分。HelloWin的客户区是那一片白色的部分。
MS-DOS环境下的程序是单线程的。也就是说一次只能运行一个程序。而到了Windows时代情况变得复杂了。我们的程序必须与其它的程序共享一个屏幕。而且用户可以控制程序的显示方式,最大化,最小化等。而且大多数程序窗口都可以随意改变窗口尺寸。这对于最终用户来讲是很方便的,而对于程序员来说却是令人头痛的事情。这些看起来简单的功能实现起来很复杂。
当用户改变了窗口尺寸,当一个窗口被另一个窗口遮挡,当窗口中的内容需要更新,这一切都需要我们程序员来解决。我们并不知道用户的显示器有多大,是十四寸还是十九寸。也不知道用户的显示器是哪种分辨率,是800*600还是1024*768。我们的程序应该灵活地处理这些情况。
当然,如果您觉得这些太麻烦,我还有一个办法。那就是………您的程序可以设计成固定尺寸的窗口。就像计算器程序一样,使用户无法改变程序窗口的尺寸。这样以来就什么都解决了。不过身为二十一世纪的有志青年,我们怎么能用这么低能的办法呢?
这一章讲述的是程序在客户区显示信息的方式。但是比上一章说明的显示方式更加复杂。
Windows为显示图形提供了强大的图形设备接口(GDI)函数。不过现在才第四章,所以在这一章中只介绍简单文本行的显示。本章涉及和解决的问题适用于所有Windows程序设计。
本章讨论的是与设备无关的程序设计基础。Windows环境下的程序使用系统提供的功能来获取关于程序运行环境的信息。
Windows中可以同时打开很多窗口,有的时候一个窗口的一部分会被另一个窗口覆盖。其实在DOS下也有类似奈侍狻R訢OS下的WPS为例,当用户选中了一个下拉菜单后,菜单可能会覆盖一部分客户区。这个时候程序会保存被菜单覆盖的那部分内容。下拉菜单消失后再把保存的那部分搬回来。Windows的处理方法不是这样的。Windows不会保存被覆盖的部分。当被覆盖的部分重新露出来的时候Windows会要求程序刷新被覆盖过的部分,也就是重新绘制。因为Windows保留了重新绘制窗口所需的全部信息。
DOS下的WPS虽然也是图形界面,但是WPS的界面一但遭到破坏,就无法恢复了。如果程序不另外保存被覆盖的部分,那么等到下拉菜单消失以后原先被覆盖的那个地方什么都不会留下。等于彻底擦掉了。
而在Windows下就不一样了。Windows事先已经保留了有关窗口的所有信息。所以就算屏幕上的窗口全部被别的窗口覆盖,也照样能恢复。
Windows是一个消息驱动系统。它通过消息把各种事件通知给程序。当窗口过程从Windows收到一个WM_PAINT消息的时候窗口的部分客户区需要重新绘制。
大多数Windows程序在进入消息循环之前的初始化期间都要调用函数UpdateWindow()。Windows利用这个机会给窗口过程发送第一个WM_PAINT消息。这个消息通知窗口过程必须绘制客户区。此后窗口过程应该在任何时候都准备好处理其它WM_PAINT消息。如果有必要,甚至重新绘制整个客户区。
在发生下面几种事件之一时窗口过程会接收到一个WM_PAINT消息。
◆用户移动窗口或显示窗口时,先前被隐藏的部分重新可见
◆用户改变窗口尺寸--如果窗口类风格具有CS_HREDRAW CS_VREDRAW位设置
◆使用ScrollWindow()或ScrollDC()滚动客户区的一部分
◆使用InvalidateRect()或InvalidateRgn()显式产生一个WM_PAINT消息。
下面的情况下Windows可能发送WM_PAINT消息。
◆Windows擦除覆盖了部分窗口的对话框或消息框。
◆菜单下拉出来,然后被释放
◆显示工具提示
在某些情况下Windows总是保存它所覆盖的显示区域,然后恢复它。这些情况是:
◆鼠标光标穿越客户区
◆图标拖过客户区
窗口过程接收到WM_PAINT消息之后就准备更新整个客户区。有的时候窗口过程只需要更新一个较小的区域。最常见的是客户区中的矩形区域。当对话框覆盖了部分客户区时情况即是如此。擦除对话框之后需要重画的只是先前被对话框遮住的矩形区域。
这个区域被称为“无效区域”或“更新区域”。客户区内出现无效区域后Windows就会发送一个WM_PAINT消息。只有在客户区的某一部分变得无效时窗口才会收到WM_PAINT消息。
Windows内部为每个窗口保存一个“绘图信息结构”,这个结构包含了包围无效区域的最小矩形的坐标以及其它信息。这个矩形就叫做“无效矩形”,有时也称为“无效区域”。如果在窗口过程处理WM_PAINT消息之前客户区中的另一个区域也变为无效,Windows计算出包围两个区域的新的无效区域,并将这种变化后的信息放在绘图信息结构中。Windows不会将多个WM_PAINT消息都放在消息队列中。
InvalidateRect()的功能是使得客户区内的部分矩形变为无效。我们可以利用这个函数强制性地使某部分客户区变为无效。在接收到WM_PAINT消息时窗口过程可以获取无效矩形的坐标。通过调用GetUpdateRect()可以在任何时候获取这些坐标。
在处理WM_PAINT消息期间窗口过程在调用了BeginPaint()之后整个客户区即变得有效。程序也可以调用ValidateRect()使客户区内的任意矩形区域变为有效。如是调用该函数使得整个客户区全部变为有效,则当前消息队列中的所有WM_PAINT消息都会被删除。
想要在窗口客户区绘图,可以使用Windows的图形设备接口(GDI)函数。Windows提供了几个GDI函数用于将文本串输出到窗口客户区内。上一章的HelloWin程序用的是DrawText()。但是目前最普遍的文本输出函数是TextOut()。该函数的格式如下:
TextOut(hdc, x, y, psText, iLength);
TextOut()向窗口的客户区写入字符串。psText参数是指向字符串的指针,iLength是字符串的长度。x和y参数定义了字符串在客户区的开始位置。hdc参数是“设备描述表句柄”,它是GDI的重要部分。实际上每个GDI函数都需要将这个句柄作为函数的第一个参数。
在第三章我简单地介绍过句柄。句柄是一个数值,以十六进制表示。句柄代表一个对象。设备描述表句柄代表的是一个设备对象。GDI函数中的设备描述表句柄代表一个窗口对象。有了这种句柄程序员就能自如地在客户区绘图。
设备描述表实际上是GDI内部保存的数据结构。设备描述表与特定的显示设备相关。对于视频显示器来说设备描述表总是与显示器上的特定窗口相关。Do You Understand?
设备描述表中的有些值是图形化的属性。这些属性定义了一些GDI绘图函数工作情况的特殊内容。例如,对于TextOut(),设备描述表的属性确定了文本的颜色,文本的背景色,TextOut()的x坐标和y坐标映射到窗口的客户区的方式,以及显示文本时使用的字体。
当程序需要绘图时必须先获取设备描述表句柄。在获取了该句柄后Windows用默认的属性值填充内部设备描述表结构。我们可以调用各种GDI函数改变这些默认值。也可以利用其它的一些GDI函数获取这些属性的当前值。
当程序在客户区绘图完毕后它必须释放设备描述表句柄。句柄被程序释放后就不再有效,且不能再被使用。程序必须在处理单个消息期间获取和释放句柄。除了调用CreateDC()创建的设备描述表句柄
之外程序不能在两个消息之间保存其它设备描述表句柄。
Windows应用程序一般使用两种方法获取设备描述表句柄。在处理WM_PAINT消息时使用时采用第一种方法。这种方法涉及到BeginPaint()和EndPaint()。这两个函数需要窗口句柄和PAINTSTRUCT结构的变量的地址做为参数。
在处理WM_PAINT消息时窗口过程首先调用BeginPaint()。BeginPaint()一般在准备绘制时将无效区域的背景擦除。该函数返回的值是设备描述表句柄。这一返回值通常被保存在HDC结构的变量中。HDC数据类型定义为32位的无符号整数。
然后程序可以使用需要设备描述表句柄的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消息传递给DefWindowProc()。该项函数依次调用BeginPaint()和EndPaint()。它仅仅使无效区域变得有效。
case WM_PAINT:
BeginPaint(hwnd, &ps);
EndPaint(hwnd, &ps);
return 0;
但是以下的方法是错误的。
case WM_PAINT:
return 0;
Windows发送WM_PAINT消息是因为客户区的一部分变得无效。如果不调用BeginPaint()或ValidateRect(),则无效区域永远也不会变得有效。这样以来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内部使用。如果该窗口的窗口类有背景刷,BeginPaint()返回前用刷子擦除无效区域的背景。HelloWin程序中的窗口类中就有背景刷:
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
在大多数情况下fErase被标志为FALSE,这意味着BeginPaint()已经擦除了无效矩形的背景。想在窗口过程中自己定义一些背景的擦除,可以处理WM_ERASEBKGN消息。当fErase被标记为TRUE时BeginPaint()返回前向窗口发出WM_ERASEBKGN消息。
如果程序通过调用InvalidateRect()使客户区中的矫形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE,则Windows将不会擦除背景,并且在调用完BeginPaint()后PAINTSTRUCT结构的fErase域设置为TRUE。
PAINTSTRUCT结构的rcPaint域是RECT类型的结构。在第三章我们已经看到RECT结构定义了一个矩形,它的四个域是left,top,right,bottom。rcPaint定义了无效矩形的边界。这些值以像素点为单位,并相对于客户区的左上角。
PAINTSTRUCT结构中的rcPaint矩形不仅是无效矩形,它还是个“剪取”矩形。意思是说Windows将绘图操作限制在剪取矩形内。如果无效矩形区域不是矩形,Windows将绘图操作限制在这个区域内。
想在处理WM_PAINT消息时在无效矩形外绘图,您可以在调用BeginPaint()之前使用如下调用:
InvalidateRect(hwnd, NULL, TRUE);
该函数使整个客户区变为无效,并擦除背景。如果最后一个参数是FALSE,则不擦除背景,原有的东东将保留在原处。
通常这是Windows程序收到WM_PAINT消息时不考虑rcPaint结构的情况下简单地重画整个客户区的最方便的方法。例如,如果在客户区的显示输出中包括了一个圆,但是只有圆的一部分落到了无效矩形中,这个时候仅仅重新绘制圆的无效部分没有意义。我们需要画的是整个圆。
使用从BeginPaint()返回的设备描述表句柄时Windows不会绘制rcPaint矩形外的任何部分。在第三章的HelloWin程序中我们并不关心处理WM_PAINT消息时的无效矩形。如果文本显示区域恰巧在无效区域内,则由DrawText()重新把字符串显示在客户区中央。程序会检查有没有必要调用DrawText()向显示器输出字符串。不过这一检查过程需要时间。关心程序性能和速度的程序员希望在处理WM_PAINT消息期间使用无效矩形范围,以避免不必要的GDI调用。如果绘制时需要访问例如位图这样的磁盘文件,这就显得尤其重要。
以后我们会发现在处理非WM_PAINT消息期间绘制客户区的某个部分也是非常有用的。或者我们需要将设备描述表句柄用于其它目的。
想要得到窗口客户区的设备描述表句柄,可以调用GetDC()来获取句柄,在使用完后调用ReleaseDC():
hdc = GetDC(hwnd);
[使用GDI函数]
ReleaseDC(hwnd, hdc);
与BeginPaint()和EndPaint()一样,GetDC()和ReleaseDC()函数必须成对使用。如果在处理某消息时调用GetDC(),则必须在退出窗口过程之前调用ReleaseDC()。
GetDC()返回的设备描述表句柄和BeginPaint()返回的设备描述表句柄不一样。GetDC()返回的设备描述表句柄具有一个包含整个客户区的剪取区域。也就是说可以在客户区的任意部分绘图,而不只是局限于无效矩形。GetDC()不会使任何无效区域变为有效。如果需要使整个客户区有效,可以调用
ValidateRect(hwnd, NULL);
一般可以调用GetDC()和ReleaseDC来对键盘消息(如在字处理程序中)或鼠标消息(如在画图程序中)做出反应。此时程序可以立刻根据用户的键盘或鼠标输入更新客户区,而不需要考虑为了窗口的无效区域而使用WM_PAINT消息。
与GetDC()相似的函数是GetWindowDC()。GetDC()返回用于写入窗口客户区的设备描述表句柄,而GetWinodwDC()返回的设备描述表句柄在窗口的标题栏写入文字。
TextOut()是用于显示文本的最常用的GDI函数。我们有必要了解一下该函数。语法是:
TextOut(hdc, x, y, psText, iLength);
第一个参数是设备描述表句柄。设备描述表句柄的属性控制了被显示文本串的特征。例如,设备描述表中有一个属性指定了文本的颜色,默认颜色为黑色。默认设备描述表还定义了白色的文本背景。程序向显示器输出文本时Windows使用这个背景色来填充字符周围的矩形空间。这个矩形空间被称为“字符框”。
该文本背景色与定义窗口类时设置的背景并不相同。窗口类的背景是一个刷子,它是一种纯色或者非纯色组成的刷子,Windows用来擦除客户区,它不是设备描述表结构的一部分。在定义窗口类结构时,大多数Windows应用程序使用WHITE_BRUSH,使得与设备描述表中的默认文本背景颜色相同。字符的背景和客户区的背景是两码事。
psText参数是指向字符串的指针,该字符串就是显示在客户区的内容。iLength是字符串中的字符个数。串中不能包含任何ASCII控制字符,如回车、换行、制表、退格等。Windows将这些控制字符显示为实心块。字符串是以“/0”为结束标记的,但是TextOut()没有检测字符串结束标志的功能。所以需要iLength参数指明字符串的长度。
x和y定义客户区内字符串的开始位置,x是水平位置,y是垂直位置。字符串中第一个字符的左上角位于坐标点(x,y)。在默认设备描述表中原点(x,y均为0的点)是客户区的左上角。如果在TextOut()中将x和y置为0,则从客户区的左上角开始输出字符串。
传递给函数的坐标常常被称为“逻辑坐标”。有关逻辑坐标的详细情况我们将在第五章讲解。现在只要记住,Windows有多种不同的“映射方式”,它们用来控制逻辑坐标转换为显示器的物理像素坐标的方式。映射方式在设备描述表中定义。默认的映射方式是MM_TEXT,在WINGDI.H头文件中定义。在MM_TEXT映射方式下逻辑坐标和物理坐标相同。x的值从左向右递增,y的值从上向下递增(图4-2)。
MM_TEXT坐标系与PAINTSTRUCT结构中定义无效矩形时使用的坐标系相同。这为我们带来了很多方便。但是其它映射方式不是这个样子。
设备描述表还定义了一个剪取区域。从GetDC()获取的设备描述表句柄默认的剪取区域是整个客户区。而从BeginPaint()获取的设备描述表句柄默认的剪取区域为无效区域。由此可以看出剪取区域和无效区域是两个完全不同的两个概念。Windows不会在剪取区域外的任何位置显示字符串。如果一个字符有一部分在剪取区域外,那么Windows将只显示剪取区域那部分的内容。
设备描述表还定义了调用TextOut()显示文本时使用的字体。默认字体是“系统字体”,或用头文件中的标识符即SYSTEM_FONT。系统字体是Windows用来在标题栏,菜单和对话框中显示文本串的默认字体。
Windows的早期版本中系统字体是等宽字体,这意味着所有字符都具有同样的宽度,非常类似于打字机的字体。但是从Windows3.0开始系统字体成为一种变宽字体,这意味着不同的字符具有不同的尺寸。比如,“W”要比“I”宽。变宽字体比等宽字体好读。不过这一转变使得很多原来的程序代码不再适用。从而要求程序员学习一些使用字体的新技术。
系统字体是一种“点阵字体”,这意味着字符被定义为像素块。系统字体的字符尺寸依赖于视频显示器的尺寸。系统字体设计为至少能在显示器上显示25行,80列字符文本。
想用TextOut()显示多行文本,就必须确定字体的字符尺寸。可以根据字符的高度定位下一行字符的显示坐标。以及根据字符的宽度定位下一个字符的显示位置。
系统字体的字符高度和平均宽度取决于视频显示器的像素大小。Windows需要的最小显示大小是640*480,但是许多用户更喜欢800*600或1024*768的显示模式。对于这些较大的显示尺寸Windows允许用户选择不同尺寸的系统字体。
程序可以调用GetSystemMetrics()来确定关于用户界面构件大小的信息。调用GetTextMetrics()确定字体大小。该函数返回制备描述表中当前选定的字体信息,因此它需要设备描述表句柄。Windows将字体信息付值到TEXTMETRIC结构中。该结构在WINGDI.H中定义,有20个域,我们只使用前7个:
typedef struct tagTEXTMETRIC
{
LONG tmHeight;
LONG tmAscent;
LONG tmDescent;
LONG tmInternalLeading;
LONG tmExternalLeading;
LONG tmAveCharWidth;
LONG tmMaxCharWidth;
[在此省略其它结构域…]
}TEXTMETRIC, *PTEXTMETRIC;
这些字段值的单位取决于选定的设备描述表映射方式。在默认的设备描述表下,映射方式是MM_TEXT,因此值的大小是以像素为单位的。
想使用GetTextMetrics(),需要先定义TEXTMETRIC结构的变量:
TEXTMETRIC tm;
需要确定文本尺寸时先获取设备描述表句柄,再调用GetTextMetrics():
hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);
ReleaseDC(hwnd, hdc);
★我再强调一次★书中第76页的 图4-3 是错误的。不过第121页还有一个相同的图,那个图才是正确的。
TEXTMETRIC结构提供了关于当前设备描述表中选用的字体的丰富信息。字体的纵向尺寸只由五个值确定,其中的4个值如 图。这个图是正确的
最重要的值是tmHeight,它等于tmAscent + tmDescent。这两个值表示了基线上下字符的最大纵向高度。术语“间距(leading)”指打印机在两行文本间插入的空间。TEXTMETRIC结构中内部的间距包括在tmAscent中,它经常是重音符号出现的地方。tmInternalLeading字段能被置为0,在这种情况下加重音的字母会稍稍缩短,以便容纳重音符号。
TEXTMETRIC结构还包括一个不包含在tmHeight值中的字段tmExternalLeading。它是字体设计者建议加在行与行之间的空间的尺寸。在系统字体中tmExternalLeading是0,因此没有在图中显示它。图4-3是Windows在640*480分辨率中使用的精确系统字体。
TEXTMETRIC结构中包含有描述字符宽度的两字段,即tmAveCharWidth(小写字母加权平均宽度)和tmMaxCharWidth(字体中最宽字符的宽度)。对于等宽字体这两个值是相等的。
本章的示例程序还需要另一种字符宽度,即大写字母平均宽度。这可以用tmAveCharWidth乘以150%大致辞计算。
必须认识到,系统字体的尺寸取决于Windows所运行的视频显示器的分辨率。在某些情况下取决于用户选取的系统字体的大小。Windows提供了一个与设备无关的图形界面,但还有很多事情需要程序员解决。
由于在一次Windows对话期间系统字体的尺寸不会发生改变,因此在程序运行过程中我们只需要调用一次GetTextMetrics。最好是在窗口过程中处理WM_CREATE消息时进行此调用。WM_CREATE消息是窗口过程接收的第一个消息。当在WinMain()中调用CreateWindow()时,Window向窗口过程发送一个WM_CREATE消息。
想在客户区显示几行文本,这需要先获取字符宽度和高度。我们可以在窗口过程内定义两个变量来保存平均字符宽度(cxChar)和字符高度(cyChar):
static int cxChar, cyChar;
变量名的前缀c代表“count”,在这里指像素数,与x和y结合,分别指宽和高。这些变量定义为static,因为它们在窗口过程中处理其它消息时也应该是有效的。如果变量在函数外面定义,则不需要定义为static。
下面是获取系统字体的字符宽度和高度的WM_CREATE代码:
case WM_CREATE:
hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cyChar = tm.tmHeight + tm.ExternalLeading;
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);
现在我们似乎已经具备了在屏幕上显示多行文本所需要的所有知识。我们知道如何在WM_PAINT消息期间获取一个设备描述表句柄,如何使用TextOut(),以及如何根据字符大小来安排字距,剩下的就是显示一点有意义的东东了。
在第三章,我们知道了从Windows的GetSystemMetrics()中获取的信息是很有意义的。该函数返回Windows中不同图形构件的大小信息。如图标,光标,标题栏和滚动条等。它们的尺寸因显示适配器和驱动程序的不同而有所不同。GetSystemMetrics()是在程序中完成与设备无关的图形输出的重要函数。
该函数需要一个参数。这个参数决定函数获取什么样的信息。比如说,当这个参数是SM_CXSCREEN时:
GetSystemMetrics(SM_CXSCREEN);
这个函数返回的是显示器的宽度,单位是像素。比如说我现在的显示分辨率是1024*768。所以上面那条语句返回的将是1024。而:
GetSystemMetrics(SM_CYSCREEN);
返回的则是屏幕的高度,单位是像素。我这里返回的是768。
这些以SM为前缀的参数选项都是Windows头文件中定义的。GetSystemMetrics()返回类型是整型。让我们来编写一个程序,显示一些可以从GetSystemMetrics()调用中获取的信息,显示格式为每条信息占一行。书中已经建立了一个头文件,头文件中定义了一个结构数组。结构数组中包含了GetSystemMetrics()所有的参数选项。
终于可以看程序了。这个程序比较简单。只是为了后面的程序做的准备活动。第四章一共有三个程序。
WinMain()部分和第三章的HelloWin没什么区别。所以我们直接进入WndProc()。
SYSMETS1.c程序中程序中的WndProc()处理三个消息。其中WM_DESTROY消息的处理方法与第三章的HelloWin相同。
WM_CREATE消息是窗口过程接收到的第一个消息。当CreateWindow()创建窗口时Windows产生这个消息。在处理WM_CREATE消息时程序调用GetDC()获取窗口的设备描述表,并调用GetTextMetrics()获取默认系统字体的信息。程序将平均字符宽度保存在cxChar中,将字符的高度保存在cyChar中。
程序还将大写字母的平均宽度保存在静态变量cxCaps中。如果是等宽字体,cxCaps等于cxChar。如果是变宽字体,cxCaps设置为cxChar乘以150%。TEXTMETRIC结构中的tmPitchAndFamily域。如果是变宽字体,该域取值1,如果是等宽字体,该域取值是0。程序中作用这个域计算cxCaps:
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar/2;
程序在处理WM_PAINT消息期间向客户区显示多行文本。窗口过程先调用BeginPaint()获取设备描述表句柄,然后用一个for语句对程序中定义的sysmetrics结构的所有行进行循环。三列文本通过三个TextOut()调用显示。对于每一列,TextOut()的第三个参数都设置为:
cyChar*i
这个参数指示了字符串左上角相对于客户区左上角的像素点位置。
第一个TextOut()语句在第一列显示了大写标识符。TextOut()的第二个参数是0,所以第一列的显示位置是客户区的左边缘。文本的内容来自sysmetrics结构的szLabel字段。程序中使用Windows函数lstrlen()来计算字符串的长度。
第二条TextOut()语句显示了对系统尺寸值的描述。可惜都是用英文,所以我们看不懂。这些描述存放在sysmetrics结构的szDesc字段中。在这种情况下,TextOut()的第二个参数设置为:
22*cxCaps
第一列显示的最长的大写标识符有20个字符,因此第二列必须在第一列文本开头向右20*cxCaps处开始。我使用22,这样以来第一列和第二列之间就有一些空隙了。
第三条TextOut()语句显示从GetSystemMetrics()获取数值。注意,第三列显示方式是右对齐,TextOut()中指定的字符串显示位置也是文本右上角的坐标,这一点与前两列不同。第三列显示的一数字。TextOut()中指定的显示位置正是数字右上角的坐标。SetTextAlign()可以改变文本的对齐方式。
SetTextAlign(hdc, TA_RIGHT | TA_TOP);
第三个TextOut()的第二个参数设置为:
(22*cxCaps) + (40*cxChar)
在TextOut()之后另一个SetTextAlign()将对齐方式重新设置成普通的左对齐方式以进行下一次循环。
在SYSMETS1程序中存在着一个很难处理的问题。如果没有足够大的显示器,我们将无法看到最下面几行。如果用户故意将客户区调得很小,我们甚至看不到后面两列。
程序没有考虑这样的问题。我们的程序甚至不知道客户区的尺寸。它只是从窗口顶部开始输出文本。它才不管用户是否能看到下面的内容。这很不理想。想要解决这个问题,我们第一个任务就是让程序知道客户区的尺寸,站它知道客户区能显示多少行多少列内容。
使用过Windows程序的用户应该清楚,窗口的尺寸变化是很大的。我们可以随心所欲地改变窗口的尺寸。在第三章我们使用GetClientRect()获取客户区的大小。使用这个函数没有什么错,但是频繁调用它是没有效率的。确定窗口客户区大小的量更好办法是在窗口过程中处理WM_SIZE消息。每当窗口改变大小时Windows给窗口过程发送一个WM_SIZE消息。WM_SIZE消息的lParam域的低十六位包含客户区宽度信息,高十六位包含客户区高度。想保存这些尺寸,需要在窗口过程中定义两个静态变量:
static int cxClient, cyClient;
MSG结构的第四个域lParam,同时也是WndProc()的第四个参数是一个32位整数。在WM_SIZE中lParam域的低16位包含客户区的宽度,高16位包含客户区的高度。须要把这些尺寸保存到两个静态变量:
case WM_SIZE:
cxClient = LOWORD(lParam);//客户区的宽度
cyClient = HIWORD(lParam);//客户区的高度
return 0;
WORD是16位的无符号整数。在WINDEF.H中LOWORD和HIWORD的定义如下:
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
所以LOWORD(lParam) 就等于WORD(lParam)。因为WORD是16位的,而lParam是32位的,所强类型转换之后lParam高16位部分就丢失了。留下来的就只有低16位,即客户区的宽度。
DWORD是无符号长整型,即32位。右移16位之后低16位部分丢失,原来的高16位成了低16位,而高16位则是十六个0。然后再和0xFFFF(即1111111111111111)按位与,结果是什么也不变。然后再把32位的lParam进行强类型转换,这样以来就获得了客户区的高度。
如果您看不懂LOWORD和HIWORD宏,说明您的C语言基础不好,请先恶补C语言基础。
在许多Windows程序中WM_SIZE消息必然跟着一个WM_PAINT消息。在定义窗口类的时候指定窗口风格为:
CS_HREDRAW | CS_VREDRAW
这种类风格告诉Windows如果客户区的水平或者垂直尺寸发生改变,则强行刷新客户区。
用如下公式计算可以在客户区内显示的文本的总行数:
cyClient/cyChar
在客户区的水平方向可以显示的小写字符的近似数目为:
cxClient/cyChar
有一点数学常识的人们都知道除数不能为零。如果在处理WM_CREATE消息期间确定cxChar和cyChar,则不用担心在这两个计算公式中出现被0除的情况。
如果客户区的大小不足以容纳所有的内容,那么,知道窗口客户区的大小只是为用户提供了在客户区内移动文本的第一步。使用过其它Windows程序的用户们应该十分清楚,这种情况下我们需要“滚动条”。
滚动条是图形用户界面中最好的功能之一,它很容易使用,而且提供了很好的视觉反馈。您可以使用滚动条显示任何东东,无论是文本,图形,表格,数据库记录,图像,还是Web页面,只要所需的空间超出了窗口客户区所能提供的空间,就可以使用滚动条。
滚动条既有垂直方向的,也有水平方向的。用户可以使用鼠标在滚动条两端的箭头上或者箭头之间的区域中点一下,这时“滚动框”在滚动条内的移动模拟了所显示的信息在整个文档中的近似位置。用户也可以用鼠标拖动滚动框到特定的位置。
有时程序员对滚动条概念很难理解,因为程序员的观点与用户的观点不同。用户向下滚动是想看到文档稍下的部分,但是程序实际上是将文档相对于显示窗口向上移动。Windows文档和头文件标识符是基于用户的观点。向上滚动意味着朝文档的开头移动,向下意味着向文档尾部移动。
很容易在应用程序中包含水平或者垂直的滚动条。程序员只需要在CreateWindow()的第三个参数中包括窗口风格标识符WS_VSCROLL(垂直滚动)和WS_HSCROLL(水平滚动)即可。客户区不包含滚动条所占据的空间。
垂直滚动条的宽度和水平滚动条的高度是恒定的。如果需要这些值,可以使用GetSystemMetrics()来获取。
Windows负责处理对滚动条的所有鼠标操作,但是滚动条没有自动的键盘界面。如果想用光标来完成滚动功能,则必须由程序员写这方面的代码。
每个滚动条均有一个相关的范围和位置。当滚动框在滚动条的最顶部或最左边时滚动框的位置是范围的最小值。在滚动条的底部或最右边时滚动框的位置是范围的最大值。
在默认情况下滚动条的范围是从0到100之间。但是将范围改为更方便于程序的数值是很容易的:
SetScrollRange(hwnd, iBar, iMin, iMax, bRedraw);
参数iBar为SB_VERT或者SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画滚动条,则设置bRedraw为TRUE。如果在调用SetScrollRange()后调用了影响滚动条位置的其它函数,则应该将bRedraw设置为FALSE,以避免过多的重画。
滚动条的位置总是离散的整数值。例如,范围为0 -- 4的滚动条具有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消息参数被分为一个低位字和一个高位字。低位字是一个数值,它指出了一个鼠标对滚动条进行的操作。这个数值叫作“通知码”。通知码的值由SB开头的标识符定义。以下是在WINUSER.H中定义的通知码:
#define SB_LINEUP 0
#define SB_LINELEFT 0
#define SB_LINEDOWN 1
#define SB_LINERIGHT 1
#defint 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的标识符用于垂直滚动条。鼠标在滚动条的不同区域单击所产生的通知码如92页图4-7所示。
当把鼠标的光标放在滚动框上并按住鼠标键时,我们就可以移动滚动框。这样就产生了带有SB_THUMBTRACK和SB_THUMBPOSITION通知码的滚动条消息。
wParam的低位字是SB_THUMBTRACK时,高位字是用户在拖动滚动框时的当前位置。
wParam的低位字是SB_THUMBPOSITION时,高位字是用户释放鼠标键后滚动框的最终位置。对于其它的滚动条操作wParam的高位字被忽略。
为了给用户提供反馈,当我们用鼠标拖动滚动框时Windows会移动滚动框。同时我们的程序会收到SB_THUMBTRACK通知码。然而,如果不通过调用SetScrollPos()来处理SB_THUMBTRACK或SB_THUMBPOSITION通知码,在用户释放鼠标键后滚动框会迅速跳回原来的位置。
有一点要搞清楚。即使我们用SetScrollPos()改变了滚动框的位置,客户区的内容不会自动跟着滚动条一起改变。程序员必须自己动手改变客户区的内容。
程序能够处理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()来获取信息。