Programming Windows 第五版读书笔记 第四章 输出文字

Programming Windows 5th Edition Chapter 4 输出文字

1. 本章更进一步的阐述显示文字的注意点,包括如何根据字体的大小来计算输出坐标,在不同的情况下如何取得设备句柄并开始绘图,如何在程序中加入滚动条等。

2. 首先需要介绍的很重要,那就是什么情况下windows会发送WM_PAINT消息给我们的程序,如下:

(1) 以下情况发生一种就一定会发送WM_PAINT消息:

在使用者移动窗口或显示窗口时,窗口中先前被隐藏的区域重新可见。
使用者改变窗口的大小(如果窗口类别样式有着CS_HREDRAW和CS_VREDRAW位旗标的设定)。
程序使用ScrollWindow或ScrollDC函数滚动显示区域的一部分。
程序使用InvalidateRect或InvalidateRgn函数刻意产生WM_PAINT消息。

(2) 以下情况时,windows试图帮我们保存一个显示区域,这样试图不用让我们的程序来负责重绘,但windows的保存动作不一定成功,也就是说,以下情况windows可能会发送WM_PAINT消息:

Windows擦除覆盖了部分窗口的对话框或消息框。
菜单下拉出来,然后被释放。
显示工具提示消息。

(3) 以下情况windows总是会保存被覆盖的显示区域,然后恢复,也就是说,不会发送WM_PAINT消息:

鼠标光标穿越显示区域。
图标拖过显示区域。


3. 有效矩形和无效矩形。Windows在发送WM_PAINT消息的时候,都会把需要重绘的显示区域保存到一个PAINTSTURCTURE中,从这里我们 的程序就可以知道哪部分内容需要重绘。Windows不会保存多个WM_PAINT消息在消息队列中,因为在添加WM_PAINT消息的时候,如果发现此 时队列中已经有了一个WM_PAINT消息,那么windows会将上一个WM_PAINT消息中的无效矩形取出,和目前的这个矩形合并,形成一个新的矩 形,然后让我们的消息处理函数处理,windows不会保存多个WM_PAINT消息在消息队列中。在前几章,我们已经可以看到,在调用 BeginPaint的时候,需要传入一个PAINTSTRUCT的结构,windows会将无效矩形就填充到这个数据结构中。

无效矩形的概念很重要。消息处理函数在处理涉及到重绘的消息时,必须将无效矩形申明成有效矩形,否则windows会认为我们的重绘工作没有完成,其结果就是--windows一直给我们的程序发送WM_PAINT消息!
调 用BeginPaint函数,可以使整个显示区域变成有效,或调用ValidateRect函数,可以使显示区域内任意矩形区域变为有效。一旦矩形变成有 效,任何涉及到该矩形区域的WM_PAINT消息都将被删除。所以,在我们的代码中,即使我们在WM_PAINT的消息处理中,什么事都不干,也要调用 BeginPaint, EndPaint,使无效矩形变成有效,否则WM_PAINT将一直被发送。


4. 设备内容(DC)。在前面我们看到,在调用BeginPaint后,会返回一个hdc,这其实就是DC的一个句柄,DC实际上就是windows GDI内部保存的数据结构,有了hdc,我们才具备了向特定显示设备输出东西的能力。DC中大多保存了一些图形属性的数据,比如TextOut的时 候,DC中保存了文字的颜色,文字的背景色,xy坐标映射到窗口显示区域的方式,使用的字体等等。所以,要修改文字输出的格式,颜色等,就要对DC进行操 作,然后用修改过的DC来绘图,才能出现我们想要的结果。

5. OK,既然绘图,输出文字的第一步是要取得HDC,那么,现在开始讲解取得HDC的两种办法。注:除了调用CreateDC建立的DC之外,程序不能在两个消息之间保存其他的DC句柄(HDC)。

(1) 方法1。该方法在处理WM_PAINT消息的时候使用。就是调用BeginPaint, EndPaint函数对。前面说过了,处理WM_PAINT消息,什么都不做,也要调用这两个函数,否则WM_PAINT消息会一直被发送(见上面的描 述)。在调用BeginPaint的时候,我们还需要填入一个PAINTSTRUCT,她的结构如下:

Code: Select all
typedef struct tagPAINTSTRUCT
{
     HDC       hdc ;
     BOOL      fErase ;
     RECT      rcPaint ;
     BOOL      fRestore ;
     BOOL      fIncUpdate ;
     BYTE      rgbReserved[32] ;
} PAINTSTRUCT ;


BeginPaint 会为我们在这个结构中填入数据。其中,我们只需要关注前三个字段,后面的是给windows用的。hdc不说了;fErase被BeginPaint标志 为FALSE,表示Windows已经用背景擦除了无效矩形(背景定义在注册窗口类别的时候,前面说过了,BeginPaint调用的时 候,windows会为我们擦除无效矩形的内容),如果我们不想让windows为我们擦除无效矩形,比如很多要求显示迅速的绘图,那么我们可以处理 WM_ERASEBKGND消息,在里面加入我们的代码。不过,如果我们的程序通过调用InvalidateRect来触发WM_PAINT消息的 话,InvalidateRect函数的最后一个参数可以用来指定是否擦除无效区域,如果这个参数为FALSE,那么windows将不会擦除无效区域, 此时fErase这个字段的值就是TRUE了;最后第三个字段是rcPaint,这就是无效矩形的定义了,我们可以通过这个取到目前需要重绘哪部分,而 且,事实上,windows会自动裁减我们的绘图操作,不在这个矩形区域内的绘图操作将被ignore!如果我们真的需要在这个矩形外面绘制的话,可以在 调用BeginPaint之前,调用InvalidateRect(hwnd, NULL, TRUE),这样就可以使整个显示区域都变成无效矩形。出现这种情况,一般就是我们不管三七二十一,将整个显示区域全部重绘一遍的做法了。

GetUpdateRect。 这个方法也可以用来获得当前的无效矩形区域。其实BeginPaint已经可以了,所以这个方法我认为用处不大。需要注意的是,如果在调用了 BeginPaint之后,来调用这个GetUpdateRect,那么得到的矩形将是empty rect,因为前面说过了,BeginPaint会把整个显示区域都申明成有效区域。

(2) 方法2。方法1适用于在WM_PAINT消息处理函数中使用,事实上,很多时候我们在其他消息处理的时候,也需要进行绘制工作,这个时候怎么取得hdc 呢?很简单,调用GetDC方法即可。GetDC函数简单一些,只需传入一个窗口句柄hwnd,就会返回一个hdc,和BeginPaint, EndPaint一样,GetDC必须和ReleaseDC配对使用。GetDC返回的hdc中包含了一个无效矩形,这个矩形就是整个显示区域(因为这不 是在WM_PAINT消息中)。GetDC方法一般在相应键盘鼠标消息的时候使用(比如一个用鼠标绘图的程序),此时我们可以根据鼠标的输入,立即更新显 示区域,而不用等到WM_PAINT消息中去处理。

GetDC, ReleaseDC方法不会将区域申明成有效矩形区域。我们可以通过调用ValidateRect函数来实现这一目的。

与 GetDC类似的函数有GetWindowDC。GetDC传回用于写入窗口显示区域的设备内容句柄,而GetWindowDC传回写入整个窗口的设备内 容句柄。例如,您的程序可以使用从GetWindowDC传回的设备内容句柄在窗口的标题列上写入文字。这种情况下,程序同样也应该处理 WM_NCPAINT (「非显示区域绘制」)消息。

6. TextOut函数。这个函数原型如下:

TextOut (hdc, x, y, psText, iLength);

几个注意点:

(1) psText字符串中不要包含回车,换行,删除等控制字符,这些字符将被显示成方块。
(2) iLength是字符的个数,不是字节数。
(3) x,y坐标被称为逻辑坐标。和hdc中定义的坐标映射模式有关。默认是MM_TEXT, 此时表示逻辑坐标和实际坐标相同,即左上角是0, 0,x向右增长,y向下增长。根据需要,可以定义不同的坐标模式。下一章会讲解。

7. 系统字体。字体必需要解释,因为在具体绘图的时候,我们必须知道字体的宽度,高度等信息,才能将文字正确的绘制到我们想要的地方。这里只讲述 windows自带的标准系统字体system font,其他的字体自己研究。在Windows的早期版本中,系统字体是等宽字体,就是所有的字母都是一个宽度,现在不是了,比如W和i占用的宽度就不 一样,因为非等宽字体更利于阅读。系统字体是一种点阵字体,也就是说,字符图形被定义成象素块,第十七章会讲到TrueType(这是由轮廓定义的字 体)。

OK, 和GetSystemMetrics函数一样,我们用GetTextMetrics就可以取道字体的信息。GetTextMetrics填充TEXTMETRIC结构,这个结构有20个字段,我们只关心前七个:

Code: Select all
typedef struct tagTEXTMETRIC
{
     LONG tmHeight ;
     LONG tmAscent ;
     LONG tmDescent ; 
     LONG tmInternalLeading ;
     LONG tmExternalLeading ;
     LONG tmAveCharWidth ;
     LONG tmMaxCharWidth ;
          [other structure fields]
}
TEXTMETRIC, * PTEXTMETRIC ;


// Get the text metrics
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
ReleaseDC (hwnd, hdc) ;

这些字段的含义参考 附件1

在我们日常编程的过程中,我们只需要关注这么几个关键点就可以了(计算字符的宽度和高度,以方便我们在TextOut的时候确定绘制坐标):

(1) 小写字符一般取tmAveCharWidth来确定宽度
(2) 大写字符的宽度:tm.tmPitchAndFamily & 1 ? 3 : 2) * tmAveCharWidth / 2,也就是首先看tmPitchAndFamily是0还是1,是0表示大写字符宽度和小写字符一样,取1,大写字符的宽度设成小写字符的平均宽度的 1.5倍
(3) 字符的高度取成:tm.tmHeight + tm.tmExternalLeading


8. 我们可以把确定字符宽度和高度的代码放在WM_CREATE消息中初始化,然后在使用TextOut的时候,可以考虑使用wsprintf函数来格式化字 符串,而且wsprintf函数非常棒的是会返回格式化后的字符串的字符个数,这个值正好用于TextOut的iLength参数,如下:

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

9. 然后书中给出了一个例子,可以看练习代码。例子很好理解,里面的SetTextAlign(hdc, TA_RIGHT|TA_TOP); 指明了TextOut的x,y坐标是从字符右上角的坐标,而不是默认的左上角,从而实现了字符串右对齐。

10. 上面的代码例子SysMets1有一个很明显的缺点就是显示空间不够显示我们的绘制,所以自然就引入了滚动条的概念。本章中共讲述了两种滚动条的做法,第 二种做法是目前常用的做法,也是科学的做法,第一种做法有缺陷,而且滚动条方块的大小也是固定的。我们来一个一个讲述,都很有价值。

11. 无论那种滚动条做法,我们都需要在窗口大小发生改变的时候,设置滚动范围和输出的文字,所以,下面的代码中都有对WM_SIZE的消息处理。我们在WM_SIZE消息中可以获得当前窗口的大小:

Code: Select all
case WM_SIZE:
   cxClient = LOWORD (lParam) ;
   cyClient = HIWORD (lParam) ;
   return 0 ;


在WM_SIZE消息中,lParam的低16位表示当前显示区域(不是整个窗口哦)的宽度,高16位表示当前显示区域的高度。用windows提供给我们的LOWORD和HIWORD两个宏可以很方面的取出这两个数值。

12. 要在窗口中加入滚动条很简单,在CreateWindow的第三个参数,定义窗口样式的时候,加入WS_VSCROLL, WS_HSCROLL的风格就可以在窗口中加入纵向和横向滚动条了。首先我们来看第一种滚动条实现的方法,他是通过这么几个关键函数来做到的:

// iBar设成SB_VERT或SB_HORZ,表示纵向和横向滚动条
// iMin, iMax表示滚动条的取值范围
SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;

// 设置滚动条当前的位置
SetScrollPos (hwnd, iBar, iPos, bRedraw) ;

在 用户单击了滚动条之后,windows给我们发送WM_VSCROLL或WM_HSCROLL消息,这两个消息中,很自然有wParam和lParam两 个消息参数。对于作为窗口的一部分而建立的滚动条,可以忽略lParam参数,因为lParam只有当滚动条在子窗口中才有意义(通常在一个对话框内)。

OK, 来看wParam。wParam的低16位是一个数值,表示当前鼠标对滚动条进行的操作-也就是一个通知码。滚动条的通知码有这么一些(定义在WINUSER.H中):

附件2

一 般情况下,我们可以忽略SB_ENDSCROLL的通知码,因为我们会在相应的滚动条通知码中已经设置了滚动条的当前位置,不需要在 SB_ENDSCROLL的时候再处理了。这里面有意思的是SB_THUMBTRACK和SB_THUMBPOSITION,这两个其实就是我们点住滚动 条的方块进行操作的时候发送的通知码。如果我们处理SB_THUMBTRACK,那么很明显,我们要不停的对窗口进行重绘,因为用户只要拖一下滚动条的方 块,我们就会收到这个通知,如果我们处理SB_THUMBPOSITION,那只有在放开了滚动条方块的时候我们才会处理,此时的效果就是我们在拖动滚动 条的时候,看不到任何反应,只有当放开滚动条方块的时候,显示区域才会刷新。一般情况下,我们只需要处理这两个通知码中的一个就可以了。

除了上述的通知码之外,WINUSER.H中还定义了SB_TOP,SB_BOTTOM,SB_LEFT和SB_RIGHT通知码,指出滚动条已经被移到了它的最小或最大位置。然而,对于作为应用程序窗口一部分而建立的滚动条来说,永远不会接收到这些通知码。

13. 查看最后一个附件能看到上述的含滚动条的程序SysMets2,这个程序中,每单击一下滚动条的两端箭头按钮,每次滚动一行信息,整个程序中,出于性能考 虑,我们没有响应SB_THUMBTRACK,该而响应SB_THUMBPOSITION,而且调用了InvalidateRect函数,从而产生 WM_PAINT消息,然后重绘了显示区域,如果响应SB_THUMBTRACK,我们可以先调用InvalidateRect,产生无效矩形区域,然后 立刻调用UpdateWindow,使windows将WM_PAINT消息不入队,直接调用消息处理函数中对WM_PAINT消息的处理,从而实现显示 区域内的内容快速重绘的目的。这个程序工作的不错,但是回顾一下,这种使用滚动条的方法有这么几个问题:

(1) 在响应WM_VSCROLL, WM_HSCROLL消息的时候,我们在wParam中取出滚动条当前的位置,但是发现没有,这个wParam中只有16 bit用来表示滚动条的位置,这就限制了滚动条的最大设置范围
(2) 这种样子实现的滚动条,滚动方块永远是一个大小,滚动方块不会根据我们能滚动的区域来动态改变大小,而目前的windows程序滚动条方块都是能根据篇幅大小自动改变大小的。

14. 现在让我们来看看更好的滚动条实现,能解决上述的问题。如果我们在MSDN中查看SetScrollRange, SetScrollPos, GetScrollRange, GetScrollPos函数,会被告知这些函数是过时的函数,其实不然,这些函数从windows 1.0开始就有了,而且在32位windows中升级成了32位的参数,但是的确有更好的滚动条处理函数,那就是现在说的“滚动条信息函 数”--SetScrollInfo, GetScrollInfo。

这两个函数可以完成上面那些函数的所有功能,并解决了上述的两个问题。

SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;

这两个函数的第三个参数变成了一个Struct,名为ScrollInfo:

Code: Select all
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 ;


这就是关键所在了:

cbSize -- 一般设置成si.cbSize = sizeof(si); or si.cbSize = sizeof(SCROLLINFO); 以后会发现windows中很多struct都有这样的字段,这样的字段可以使将来的windows版本可以扩充该结构并添加新的功能,同时能和以前写的 代码兼容。

fMask -- 这是关键。fMask是一个flag,定义成一堆以SIF开头的常量,这些常量可以用 | 来组合。他们具体有:

在SetScrollInfo函数中,如果fMask设定成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方法。此时fMask应设成SIF_TRACKPOS旗标。从函数的传回中,SCROLLINFO结构的nTrackPos字 段将指出目前的32位的卷动方块位置。

还有一个fMask是SIF_DISABLENOSCROLL旗标,只能给SetScrollInfo用,表示禁用滚动条。

SIF_ALL旗标是SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的组合。在WM_SIZE消息处理期间设 置滚动条参数时,这是很方便的,这在处理滚动条消息时也是很方便的。因为设定了SIF_ALL之后,ScrollInfo中各个字段就都有值了。

所以,从上面可以看出,首先,牵涉到滚动条position和range的参数都是32位的了,没有了上述16位的限制;其次,多出了一个 Page的东西,这个Page定义了一个Page(页面)能显示的范围,这样,Page和nMin,nMax结合起来,滚动条方块就能根据这个比例来显示 出不同的大小了。不过这里也要注意,有点绕,比如我们设定nMin=0, nMax=75, nPage=50,那么此时滚动条其实只有25个可滚动单元了哦,而不是75个哦,因为一屏(一个Page)就能显示50条,那么点25下就能来到最后一 行了哦!

15. OK,针对上面写出的SysMets3,是个最完善的带滚动条的程序了,将纵向和横向滚动条都加上了。可以仔细看里面的代码,说几点:

(1) 当nPage大于或等于nMax的时候,表明目前一屏足以显示所有的数据了,此时windows会隐藏滚动条,如果不想隐藏,可以自己用SetScrollInfo设置SIF_DISABLENOSCROLL,此时滚动条将不能使用,而不会隐藏。

(2) 在响应WM_SIZE的时候,设置了滚动条的range和page,这是很自然的做法。对于page的设置,设成了当前显示区域的高度除以一行字符的高度,表示一屏能显示多少行字符。

(3) 在响应滚动消息的时候,首先我们设置了新的滚动条位置,然后用将新的位置取出来,和滚动前的位置对比,如果发生了变化,那么,调用 ScorllWindow,这是函数很复杂,目前已被ScrollWindowEx代替,用了这个函数,就不需要InvalidateRect了,因为这 个函数也会产生WM_PAINT,用这个函数表明当前滚动的大小,第二个参数是水平滚动的改变,第三个参数是垂直滚动的大小。后两个NULL表示更新整个 显示区域。

(4) 在垂直滚动方面,我们处理了SB_THUMBTRACK,在水平滚动方面,我们响应了SB_THUMBPOSITION。

(5) 在WM_PAINT中,我们根据无效区域的大小,选择性的重绘了显示区域,这样带来了更好的性能。

(6) 今后可以看看ScrollWindowEx,看做了哪些改进,目前的这个程序,鼠标滚轮是无法滚动窗口的,换成ScrollWindowEx,是否就可以了呢?


紧接着上一贴的最后一个问题:

为什么我们要用ScrollWindow, ScrollWindowEx而不是使用InvalidateRect来产生WM_PAINT呢?

网上搜来的一些回答:

ScrollWindowEx()执行的操作流程是先将位图块在屏幕DC上移动(硬件操作),然后对新暴露出的区域发送WM_ERASEBKGND(可选)并对该窗口设置新的无效区域(你在WM_PAINT中可以得到这个区域)。

也就是说,windows首先通过图形的方式帮我们把一些仍然需要显示的东西搬到屏幕的适当位置,然后把新出现的需要我们重绘的部分设成无效矩形区域,然后我们在WM_PAINT中取得这个区域后,就只需要重绘整个区域了,的确高效很多啊。

如果用InvalidateRect,由于我们很难判断滚动的区域然后做象素级的操作,所以我们只能将整个显示区域设成无效,这样WM_PAINT的时候就要重绘整个显示区域,对于拖着滚动条方块进行的滚动操作,这样的重绘方式性能会非常低。
 

原文地址:https://www.cnblogs.com/super119/p/2011321.html