第四章 输出文字

  显示区域是被整个应用程式视窗中未被标题列、视窗边框,以及可选的功能表列、工具列、状态列和卷动列占据的部分。简而言之,显示区域是视窗中可以由程式任意书写和传递视觉资讯的部分。
  在混合显示文字和图形时,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讯息之前显示区域中的另一个区域变为无效,则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, iLenght);

  TextOut向视窗的显示区域写入字串。psText参数是指向字串的指针,iLength是字串的长度。x和y参数定义了字串在显示区域的开始位置。hdc参数是【装置内容代号】,它是GDI的重要部分。实际上,每个GDI函式都需要将这个代号作为函式的第一个参数。

装置内容

  装置内容(简称DC)实际上是GDI内部保存的资料结构。装置内容与特定的显示设备(如视讯显示器或印表机)相关。对于视讯显示器,装置内容总是与显示器上的特定视窗相关。
  装置内容中的有些值是图形属性,这些属性定义了GDI绘图函式工作的细节。例如,对于TextOut,装置内容的属性确定了文字的颜色、文字的背景色、x坐标和y坐标映射到视窗的显示区域的方式,以及显示文字时Windows使用的字体。当程式需要绘图时,它必须首先取得装置的内容代号。在取得了该代号之后,Windows用内定的属性值填入内部装置内容结构。可以呼叫不同的GDI函式可以改变这些预设值。利用其他的GDI函式可以取得这些属性的目前值。当然,还有其他的GDI函式可以真正地绘图。
  当程式在显示区域绘图完毕后,它必须释放装置内容代号。代号被程式释放后就不在有效,且不能被使用。程式必须在处理单个讯息处理期间取得和释放代号。除了呼叫CreateDC建立的装置内容之外,程式不能在两个讯息之间保存其他装置内容代号。
  Windows应用程式一般使用两种方法取得装置内容代号,以备在荧幕上绘图。

取得装置内容代号:方法一

  在处理WM_PAINT讯息时,使用这种方法。它涉及BeginPaint和EndPaint两个函式,这两个函式需要视窗代号(作为参数传递给视窗讯息处理程式)和PAINTSTRUCT结构的变数(在WINUSER.H表头档案中定义)的地址为参数。
  在处理WM_PAINT时,视窗讯息处理程式首先呼叫BeginPaint。BeginPaint函式一般在准备绘制时导致无效区域的背景被擦除。该函式也填入PAINTSTRUCT结构的栏位。BeginPaint传回的值是装置内容代号,这一传回值通常被保存在hdc变数中。它在视窗讯息处理程式中的定义如下:

HDC hdc;

  HDC资料型态定义为32位元正负号整数。然后,程式就可以使用需要装置内容代号的TextOut等GDI函式。呼叫EndPaint即可释放装置内容代号。在处理WM_PAINT讯息时,必须成对地呼叫BeginPaint和EndPaint。如果视窗讯息处理程式不处理WM_PAINT讯息,则它必须将WM_PAINT讯息传递给Windows中DefWindProc。DefWindowProc以下代码处理WM_PAINT讯息:

case WM_PAINT:
    BeginPaint(hwnd, &ps);
    EndPaint(hwnd, &ps);
    return 0;

  Windows将一个WM_PAINT讯息放到讯息伫列中,是因为显示区域的一部分无效。如果不呼叫BeginPaint和EndPaint,则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初始化期间登陆视窗类别时使用的。

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栏位定义了无效矩形的边界,这些值均以图素为单位,并相对于显示区域的左上角。无效矩形是应该重画的区域。
  PAINTSTRUCT中的rcPaint矩形不仅是无效矩形,它还是一个【剪取】矩形。这意味着Windows将绘图操作限制在剪取矩形内(更确切的说,如果无效矩形区域不为矩形,则Windows将绘图操作限制在这个区域内)。
  在处理WM_PAINT讯息时,为了在更新的举行外绘图,可以使用如下呼叫:

InvalidateRect(hwnd, NULL, TRUE);

  该呼叫在BeginPaint呼叫之前进行,它使整个显示区域变为无效,并擦除背景,但是,如果最后一个参数等于FALSE,则不擦除背景,原有的东西将保留在原处。

取得装置内容代号:方法二

  要得到视窗显示区域的装置内容代号,可以呼叫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_PAINT(非显示区域绘制)讯息。

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会将这些控制字元显示为实心块。TextOut不识别作为字串结束标志的内容为零的位元组(对于Unicode,是一个短整数型态的0),而需要由nLength参数指明长度。
  TextOut中的x和y定义显示区域内字串的开始位置,x是水平位置,y是垂直位置。字串中第一个字元的左上角位于坐标点(x,y)。在内定的装置内容中,原点(x和y均为0的点)是显示区域的左上角。如果在TextOut中将x和y设为0,则将从显示区域左上角开始输出字串。
  Windows有许多坐标映射方式,传递给函式的坐标常常被称为逻辑坐标,它们用来控制GDI函式指定的逻辑坐标转换为显示器的实际图素坐标的方式。映射方式在装置内容中定义,内定映射方式是MM_TEXT(使用WINGDI.H中定义的识别字)。在MM_TEXT映射方式下,逻辑单位与实际单位相同,都是图素;x的值从左向右递增,y的值从上向下递增。MM_TEXT坐标与Windows在PAINTSTRUCT结构中定义无效矩形时使用的坐标系相同,这为我们带来了很多方便。
  装置内容也定义了一个剪裁区域。您已经看到,对于GetDC取得的装置内容代号,内定剪裁区域是整个显示区域;而对于从BeginPaint取得的装置内容代号,则为无效区域。Windows不会在剪裁区域外的任何位置显示字串。如果一个字元有一部分在剪裁区域外,则Windows将只显示此区域内的那部分。要想将输出写到视窗的显示区域之外不是那么容易的,所以不用担心会无意间出现这种事情。

系统字体

  装置内容还定义了在您呼叫TextOut显示文字时Windows使用的字体。内定字体为【系统字体】,或用Windows表头档案中的识别字,即SYSTEM_FONT。系统字体是Windows用来在标题列、功能表和对话方块中显示字串的内定字体。
系统字体是一种点阵字体,这意味着字元被定义为图素块。至于确切的大小,系统字体的字元大小取决于视讯显示器的大小。系统字体设计为至少在显示器上显示25行80列文字。

字元大小

  要使用TextOut显示多行文字,就必须确定字体的字元大小,可以根据字元的高度来定位字元的后续行,以及根据字元的宽度来定义字元的后续列。
  程式可以呼叫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个值确定。
  tmHeight是tmAscent和tmDescent的和。这两个值表示了基准线上下字元的最大纵向高度。【间距】leading指印表机在两行文字间插入的空间。在TEXTMETRIC结构中,内部的间距包括在tmAscent中(因此也在tmHeight中),并且它经常是重音符号出现的地方。tmInternalLeading栏位可被设置成0,在这种情况下,加重音的字母会稍稍缩短以便容纳重音符号。
  TEXTMETRIC结构还包括一个不包含在tmHeight值中的栏位tmExternalLeading。它是字体设计者建议加在横向字元之间的空间大小。在安排文字行之间的空隙时,您可以接受设计者的建议的值,也可以拒绝它。
  TEXTMETRICS结构包含有描述字元宽度的两个栏位,即tmAveCharWidth(小写字母加权平均宽度)和tmMaxCharWidth(字体中最宽字元的宽度)。对于定宽字体,这两个值是相等的。大写字母的平均宽度可以用tmAveCharWidth乘以150%大致计算出来。

格式化文字

  Windows启动后,系统字体的大小就不会发生改变,所以在程式执行过程中,程式写作者只需要呼叫一次GetTextMetrics。最好是在视窗讯息处理程式中处理WM_CREATE讯息时进行此次呼叫,WM_CREATE讯息是视窗讯息处理程式收到的第一个讯息。在WinMain中呼叫CreateWindow时,Windows会以一个WM_CREATE讯息呼叫视窗讯息处理程式。
  假设要编写一个Windows程式,在显示区域显示几行文字,这需要先取得字元宽度和高度。您可以在视窗讯息处理程式内定义两个变数来保存平均字元宽度(cxChar)和总的字元高度(cyChar):

static int cxChar, cyChar;

  变量名的首字母c代表【count】,这里指图素数,与x和y结合,分别指宽和高度。这些变数定义为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;

  您会发现常常需要显示格式化的数字跟简单的字串。此时不能使用惯用的工具来完成这项工作,但是可以使用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);

  对于这样简单的情况,可以将iLength的定义值与TextOut放在同一条叙述中,从而无需定义iLength:

TextOut(hdc, x, y, szBuffer, wsprintf(szBuffer, TEXT("The sum of %i and %i is %i"), iA, iB, iA + iB));

综合使用

  让我们来编写一个程式,显示一些可以从GetSystemMetrics呼叫中取得的资讯,显示格式为每种视觉元件一行。
SystemMetrics.h源码如下:

#ifndef _SystemMetrics_H_
#define _SystemMetrics_H_

#include <windows.h>

#define NUMLINES ((int)(sizeof sysmetrics / sizeof sysmetrics[0]))

struct
{
    int iIndex;
    TCHAR *p_szLabel;
    TCHAR *p_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")
};

#endif // _SystemMetrics_H_

main.cpp源码如下:

#include <windows.h>
#include "SystemMetrics.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("SystemMetrics");
    HWND hwnd;
    WNDCLASS wndclass;
    MSG msg;

    wndclass.style          = CS_HREDRAW | CS_VREDRAW;
    wndclass.cbClsExtra     = 0;
    wndclass.cbWndExtra     = 0;
    wndclass.lpfnWndProc    = WndProc;
    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,
                        szAppName,
                        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;
    TEXTMETRIC tm;
    PAINTSTRUCT ps;
    HDC hdc;
    TCHAR szBuffer[100];

    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 (int i = 0; i < NUMLINES; i++)
        {
            TextOut(hdc, 0, i * cyChar, sysmetrics[i].p_szLabel, lstrlen(sysmetrics[i].p_szLabel));
            TextOut(hdc, 22 * cxCaps, i * cyChar, sysmetrics[i].p_szDesc, lstrlen(sysmetrics[i].p_szDesc));
            SetTextAlign(hdc, TA_RIGHT | TA_TOP);
            TextOut(hdc, 22 * cxCaps + 40 * cxChar, i * cyChar, szBuffer, wsprintf(szBuffer, TEXT("%d"), 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);
}

SystemMetrics视窗处理程式

  SystemMetrics程式中的WndProc视窗讯息处理程式处理三个讯息:WM_CREATE、WM_PAINT和WM_DESTROY。WM_DESTROY讯息的处理方法与第三章的HelloWin程式相同。WM_CREATE讯息是视窗讯息处理程式接收到的第一个讯息。在CreateWindow函式建立视窗时,Windows产生这个讯息。在处理WM_CREATE讯息时,SystemMetrics呼叫GetDC取得视窗的装置内容,并呼叫GetTextMetrics取得内定系统字体的文字大小。SystemMetrics将平均字元宽度保存在cxChar中,将字元的总高度(包括外部间距)保存在cyChar中。
  SystemMetrics还将大写字母的平均宽度保存在静态变数cxCaps中。对于固定宽度的字体,cxCaps等于cxChar。对于可变宽度字体,cxCaps设定为cxChar乘以150%。对于可变宽度字体,TEXTMETRIC结构中的tmPitchAndFamily栏位的低位元为1,对于固定宽度字体,该值为0.SystemMetrics使用这个位元从cxChar计算cxCaps:

cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;

  SystemMetrics在处理WM_PAINT讯息处理期间完成所有视窗建立工作。通常,视窗讯息处理程式先呼叫BeginPaint取得装置内容代号,然后用一道for叙述对SystemMetrics.H中定义的systemmetrics结构的每一个进行回圈。三列文字用三个TextOut函式显示,对于每一列,TextOut的第三个参数都设定为:

cyChar * i;

  这个参数指示字符串顶端相对于显示区域顶部的图素位置。
  第一条TextOut叙述在第一列显示了大写识别字。TextOut的第二个参数是0,这是说文字从显示区域的左边缘开始。文字的内容来自systemmetrics结构的szLabel栏位。我使用Windows函式lstrlen来计算字符串的长度,它是TextOut需要的最后一个参数。
  第二条TextOut叙述显示了对系统尺寸值的描述。这些描述存放在systemmetrics结构的szDesc栏位中。这种情况下,TextOut的第二个参数设定为:

22 * cxCaps;

  第一列显示的最大长的大写识别字有20字元,因此第二列必须在第一列文字开头向右20cxCaps处开始。我使用22,以在两列之间加一点多余的空间。第三条TextOut叙述显示从GetSystemMetrics函式取得的数值。变宽字体使得格式化向右对齐的数值有些棘手。从0到9的数字具有相同的宽度,但是这个宽度比空格宽度大。数值可以比一个变宽数字宽,所以不同的数值应该从不同的横向位置开始。
  那么如果我们指定字串结束的图素位置,而不是指定字串的开始位置,以此向右对齐数值,是否会容易一些呢?用SetTextAlign函式就可以做到这一点。在SystemMetrics中呼叫:

SetTextAlign(hdc, TA_RIGHT | TA_TOP);

  之后,传给后续TextOut函式的坐标将指定字串的右上角,而不是左上角。
  显示列数的TextOut函式的第二个参数设定为:

22 * cxCaps + 40 * cxChar;

  值40 * cxChar包含了第二列的宽度和第三列的宽度。在TextOut函式之后,另一个对SetTextAlign的呼叫将对齐方式设定为普通方式,以进行下一次回圈。

空间不够

  在SystemMetrics程式中存在者一个很难处理的问题:除非您有一个大荧幕跟高解析度的显示卡,否则就无法看到系统尺度列表的最后几行。如果视窗太窄,甚至根本看不到值。
  SystemMetrics不知道这个问题。否则我们就会显示一个讯息方块说【抱歉!】程式甚至不知道它的显示区域有多大,它从视窗顶部开始输出文字,并仰赖Windows剪裁超出显示区域底部的内容。
  显然,者很不理想。为了解决这个问题,我们的第一个任务是确定程式在显示区域内能输出多少内容。

显示区域的大小

  如果您使用过现有的Windows应用程式,可能会发现视窗的尺寸变化极大。视窗最大化时(假定视窗只有标题列且没有功能表),显示区域几乎占据了整个荧幕。这一最大化了的显示区域的尺寸可以通过以SM_CXFULLSCREEN和SM_CYFULLSCREEN为参数呼叫GetSystemMetrics来获得。视窗的最小尺寸可以很小,有时甚至不存在,更不用说显示区域了。
  在最近一章,我们使用GetClientRect函式来取得显示区域的大小。使用这个函式没有什么不好,但是您每次要使用资讯时就去呼叫它一遍是没有效率的。确定视窗显示区域大小的更好办法是在视窗讯息处理程式中处理WM_SIZE讯息。在视窗大小改变时,Windows给视窗讯息处理程式发送一个WM_SIZE讯息。传给视窗讯息处理程式的lParam参数的低字组中包含显示区域的宽度,高字组中包含显示区域的高度。要保存这些尺寸,需要在视讯处理程式中定义两个静态变数:

static int cxClient, cyClient;

  与cxChar和cyChar相似,这两个变数在视窗讯息处理程式内定义为静态变数,因为在以后处理其他讯息时会用到它们。处理MW_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

  用如下公式可以计算出在显示区域内显示的文字的总行数:

cyClient / cyChar

  类似地,在显示区域内的水平方向可以显示的小写字元的近似数目为:

cxClient / cxChar

卷动列

  卷动列是图形使用者界面中最好的功能之一,它很容易使用,而且提供了很好的视觉回馈效果。您可以使用卷动列显示任何东西–无论是文字、图形、表格、资料库记录、图像或是网页,只要它所需的空间超过了视窗的显示区域所能提供的空间,就可以使用卷动列。
  卷动列既有垂直方向的,也有水平方向的。使用者可以使用滑鼠在卷动列两端的箭头上或者在箭头之间的区域中点一下,这时,【卷动方块】在卷动列内的移动位置与所显示的资讯在整个文件中的近似相关位置成比例。使用者也可以用滑鼠拖动卷动方块到特定的位置。

卷动列的范围和位置

  每个卷动列均有一个相关的【范围】(这是一对整数,分别代表最小值和最大值)和【位置】(它是卷动列方块在此范围内的位置)。当卷动方块在卷动列的顶部时,卷动方块的位置是范围的最小值;在卷动列的底部(或右部)时,卷动方块的位置是范围的最大值。
  在内定情况下,卷动列的范围是从0到100,但将范围改变为更方便于程式的数值也是很容易的:

SetScrollRange(hwnd, iBar, iMin, iMax, bRedraw);

  参数iBar为SB_VERT或者SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画卷动列,则设置bRedraw为TRUE(如果在呼叫SetScrollRange后,呼叫了影响卷动列位置的其他函式,则应将bRedraw设定为FALSE以避免过多的重画)。
  卷动列方块的位置总是离散的整数值。例如,范围为0~4的卷动列具有5(0,1,2,3,4)个卷动列方块的位置。
  您可以使用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_LINUP            0
#define SB_LINELEFT         0
#define SB_LINEDOWN         1
#define SB_LINGRIGHT        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

  如果在卷动列的各个部位按住滑鼠键,程式就能收到多个卷动列讯息。当释放滑鼠键后,程式会收到一个带有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函式来得到资讯。

在SystemMetrics中加入卷动功能

#include <windows.h>
#include "..\SystemMetrics\SystemMetrics.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("SystemMetrics1");
    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,
                 szAppName,
                 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, cxClient, 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:
        cxClient = LOWORD(lParam);
        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 (int i = 0; i < NUMLINES; i++)
        {
            y = cyChar * (i - iVscrollPos);
            TextOut(hdc, 0, y, sysmetrics[i].p_szLabel, lstrlen(sysmetrics[i].p_szLabel));
            TextOut(hdc, 22 * cxCaps, y, sysmetrics[i].p_szDesc, lstrlen(sysmetrics[i].p_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_OVELAPPEDWINDOW | WS_VSCROLL

  WndProc视窗讯息处理程式在处理WM_CREATE讯息时增加了两条叙述,以设置垂直卷动列的范围和初始位置:

SetScrollRange(hwnd, SB_VERT, 0, NUMLINES - 1, FALSE);
SetScrollPos(hwnd, SB_VERT, iVscrollPos, TURE);

  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讯息。SystemMetrics在处理WM_PAINT讯息时,每一行的y坐标计算公式为:

cyChar * i

  在SystemMetircs1中,计算公式为:

cyChar * (i - iVscrollPos)

  回圈仍显示NUMLINES行文字,但是对于非零的iVscrollPos是负数。程式实际上在显示区域以外显示这些文字行。当然,Windows不会显示这些行,因此荧幕显得干净漂亮。
  前面说过,我们一开始不想弄得太复杂,这样的程式码很浪费,效率很低。下面我们对此加以修改,但是先考虑在WM_VSCROLL讯息之后更新显示区域的方法。

绘图程式的组织

  在处理完卷动列讯息后,SystemMetrics1不更新显示区域,相反,它呼叫InvalidateRect使显示区域失效。这导致Windows将一个WM_PAINT讯息放入讯息伫列中。
  最好能使Windows程式在回应WM_PAINT讯息时完成所有的显示区域绘制功能。因为程式必须在一接收到WM_PAINT讯息时就更新显示区域,如果在程式的其他部分也绘制的话,将很可能使程式码重复。
  首先,您可能对这种拐弯抹角的方式感到厌烦。在Windows早期,因为这种方式与文字模式的程式设计差别太大,程式写作者感到这种概念很难理解。并且,程式要不断地通过马上绘制画面来回应键盘和滑鼠。这样即方便又有效,但是在很多情况下,这完全没必要。当您掌握了在回应WM_PAINT讯息时积累绘制显示区域所需要的全部资讯的原则之后,会对这种结果感到满意。
  如SystemMetrics1示范的,程式仍然需要处理非WM_PAINT讯息时更新特定的显示区域,使用InvalidateRect哈桑你hi就很方便,您可以用它使显示区域的特定矩形或者整个显示区域失效。
  只将视窗显示区域标记为无效以产生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指示视窗讯息处理程式绘制显示区域。

建立更好的滚动

卷动列资讯函式

  Win32API介绍的两个卷动列函式称作SetScrollInfo和GetScrollInfo。这些函式可以完成以前函式的全部功能,并增加了两个新的特性。
  第一个功能涉及卷动方块的大小。您可能注意到,卷动方块大小在SystemMetrics1程式是固定的。然而,在您可能使用道德一些Windows应用程式中,卷动方块大小与在视窗中的文件大小成比例。显示的大小称作【页面大小】。公式为:

卷动方块大小      页面大小      显示的文件数量
——————————  = ———————————— = ————————————————
  滚动长度         范围          文件的总大小

  可以使用SetScrollInfo来设置页面大小(从而设置了卷动方块的大小),如将要看到的SystemMetrics2程式所示。
GetScrollInfo函式增加了第二个重要的功能,或者说它改进了目前API的不足。假设您要使用65535或更大单位的范围,这在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 ;在呼叫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后,它会被忽略)。这在处理卷动列讯息时也是很方便的。

卷动范围

  在SystemMetrics1中,卷动范围设置为最小为0,最大为NUMLINES-1.当卷动列位置是0时,第一行资讯显示在显示区域的顶部;当卷动列的位置是NUMLINES-1时,最后一行显示在显示区域的顶部,并且看不见其他行。
  可以说SystemMetrics2卷动范围太大。事实上只需把资讯最后一行显示在显示区域的底部而不是顶部即可。我们可以对SystemMetrics1做出一些修改以达到此点。当处理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只是让那个卷动列不被使用,而不是隐藏它。

新的SystemMetrics

#include <windows.h>
#include "../SystemMetrics/SystemMetrics.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
    static TCHAR szAppName[] = TEXT("SystemMetrics2");
    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,
                        szAppName,
                        WS_OVERLAPPEDWINDOW | WS_HSCROLL | 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, cxClient, cyClient, iMaxWidth;
    int x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd;
    HDC hdc;
    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);
        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(SCROLLINFO);
        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(SCROLLINFO);
        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(SCROLLINFO);
        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 horizontal scroll bar information
        si.cbSize = sizeof(SCROLLINFO);
        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);
            UpdateWindow(hwnd);
        }
        return 0;

    case WM_PAINT:
        hdc = BeginPaint(hwnd, &ps);
        // Get vertical scroll bar position
        si.cbSize = sizeof(SCROLLINFO);
        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 (int i = iPaintBeg; i <= iPaintEnd; i++)
        {
            x = cxChar * (1 - iHorzPos);
            y = cyChar * (i - iVertPos);
            TextOut(hdc, x, y, sysmetrics[i].p_szLabel, lstrlen(sysmetrics[i].p_szLabel));
            TextOut(hdc, x + 22 * cxCaps, y, sysmetrics[i].p_szDesc, lstrlen(sysmetrics[i].p_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呼叫中传回正确的值。
  SystemMetrics2使用ScrollWindow函式在视窗的显示区域中卷动资讯而不是重画它。虽然程式很复杂(在新版本的Windows中已被更复杂的ScrollWindowEX所替代),SystemMetrics2仍以相当简单的方式使用它。函式的第二个参数给出了水平卷动显示区域的数值,第三个参数是垂直卷动显示区域的数值,单位都是图素。
  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处理的一个方法由SystemMetrics2展示:WM_PAINT处理程式确定无效区域中的文字行并仅仅重画这些行。当然,程式码复杂一些,但速度很快。

猜你喜欢

转载自blog.csdn.net/flyingsbird/article/details/80075127