今天,我们将一起学习“文本输出”章节,一看标题,我以为用一章的篇幅来介绍“文本输出”内容,是否有些大惊小怪的成分?但细细翻看里边的内容,发现即便是简单的“文本输出”,也牵涉到了很多知识点,最重要的一点是介绍了与设备无关的编程基础。哈哈,一不小心被“外表”所欺骗,算是很美丽的教训。。开始正题。
1 绘制与重绘
在字符模式环境下,程序可以输出到显示器的任何位置,程序输出到显示器上的内容也不会神秘消失。在Windows中,程序只能在自己窗口的客户区中显示文本和图形。你不能保证程序重新输出到哪里之前还会留在哪里。例如,当用户在屏幕上移动其他程序的窗口时,可能会遮住应用程序的部分窗口。Windows并不会保留被遮住的部分窗口。当其他程序的窗口被移开时,Windows会要求你的程序重新绘制刚才被遮住的客户区。
Windows是一个消息驱动的系统,它使用两种方式把各种事件通知到应用程序,把消息放在应用程序的消息队列中,或者向适当的窗口过程直接发送消息。
1.1 WM_PAINT消息
在Chapter03 章节,我们介绍了WM_PAINT消息的产生机制,本节再次补充之前内容,更完整地介绍WM_PAINT消息的产生场景。
在以下任何一个事件发生时,窗口过程都会收到一个WM_PAINT消息:
>用户移动一个窗口,导致一个被遮盖的部分窗口暴露出来。
>用户调整窗口的大小(当窗口的类型设置为CS_HREDRAW和CS_VREDRAW时)。
>程序调用ScrollWindow或ScrollDC函数滚动客户区。
>程序调用InvalidateRect或InvadiateRgn函数显式生成WM_PAINT消息。
在某些情况下,当客户区的一部分被临时覆盖时,Windows会试图保存被覆盖的这部分,以便将来恢复时使用。这并不是每次都能成功。在以下情况下,Windows有时会发送一条WM_PAINT消息:
>Windows关闭了一个覆盖了部分窗口的对话框或者消息框。
>下拉菜单被拉下然后收回。
>显示提示信息
在少数情况下,Windows总是会保存被覆盖部分的显示内容,然后再恢复。这些情况如下:
>鼠标指针在客户区内移动。
>在客户区内拖动图标。
另外,如果需要在其他时候更新客户区,可以强制Windows生成WM_PAINT消息。
1.2 有效矩形和无效矩形
尽管窗口过程必须能够在收到WM_PAINT消息时更新整个客户区,但通常它只需要更新其中的一部分,最常见的是更新其中的一个矩形区域。需要重新绘制的部分被称为“无效区域”或“更新区域”。在客户区中有一个无效区域将导致Windows在应用程序的消息队列中放置一条WM_PAINT消息。只有当程序客户区的一部分失效时,窗口过程才会接收到WM_PAINT消息。
Windows内部为每一个窗口都保存了一个“绘制信息结构”。这个结构保存着一个可以覆盖该无效区域的最小矩形和一些其他信息。这个最小矩形被称为“无效矩形”。如果在窗口过程处理一条等候的WM_PAINT消息之前,客户区中的另一部分也失效了,那么Windows将计算出一个覆盖这两个失效部分的新的无效区域和无效矩形,并更新绘制信息结构中的数据。Windows不会在消息队列中放置多条WM_PAINT消息(注意哦)。
窗口过程可以通过调用InvalidateRect函数来强制使自己的客户区中的一个矩形失效,如果消息队列中已经有一条WM_PAINT消息,Windows就会计算出一个新的无效矩形,否则,Windows会在消息队列中放置一条WM_PAINT消息。当窗口过程收到WM_PAINT消息时,它可以获得无效矩形的坐标。而在其他任何时候,可以通过GetUpdateRect函数来获取这些坐标。
窗口过程在处理WM_PAINT消息时,在调用BeginPaint函数后,整个客户区会变成有效的。程序也可以通过调用ValidateRect函数来使客户区中任意的矩形变得有效。如果该函数调用的结果是让整个无效区域都有效,那么当前消息队列中的WM_PAINT消息就会被删除。
2 GDI简介
GDI,即图形设备接口,提供了一系列用于绘图及相关操作的函数。很多GDI函数都需要一个设备环境句柄作为参数。
2.1 设备环境
设备环境(也称DC, Device Context)实际上是GDI内部维护的一个数据结构,设备环境与特定的显示设备关联。对于视频显示,设备环境通常与屏幕上的一个特定的窗口关联。设备环境中的某些值是图形“属性”。这些属性决定了GDI绘图函数的工作细节,例如在TextOut函数中,设备环境的属性决定了文本的颜色、文本背景的颜色、函数的参数x和y如何映射到窗口的客户区,以及Windows用什么字体显示文本。
程序在绘制前必须获取一个设备环境句柄。在获取句柄后,Windows会在内部的设备环境结构中填入默认的属性值。当程序完成对客户区的绘制后,它必须释放设备环境句柄。在释放之后,这个句柄不再有效并且不能再被使用。程序必须在处理同一条消息的过程中获取句柄和释放句柄,你不能在两条消息中间传递一个设备环境句柄,唯一的例外是通过调用CreateDC函数创建的设备环境句柄。接下来介绍绘制信息结构和获取设备环境句柄的通用两种方法。
2.2 绘制信息结构
该结构声明如下:
第一个参数hdc,即表示用于绘制的设备环境句柄。typedef struct tagPAINTSTRUCT { HDC hdc; BOOL fErase; RECT rcPaint; BOOL fRestore; BOOL fIncUpdate; BYTE rgbReserved[32]; } PAINTSTRUCT, *PPAINTSTRUCT;
第二个参数fErase,表示窗口背景是否需要被擦除。InvalidateRect函数的最后一个参数是指定背景是否被擦除,如果是FALSE,则表示Windows将不会擦除背景。在调用BeginPaint函数后,fErase将是TRUE。
第三个参数rcPaint,表示需要被绘制的矩形区域(相对客户区左上角而言)。
第四个、五个、六个参数供系统内部使用。
2.3 获取设备环境句柄:方法一
这种方法在处理WM_PAINT消息时使用。这涉及到两个函数:BeginPaint和EndPaint,其中BeginPaint函数用于获取设备环境句柄,而EndPaint函数用于释放设备环境句柄。注意:在WM_PAINT消息处理过程中,如果不使用BeginPaint和EndPaint函数对(或者使用ValidateRect函数),Windows不会将无效区域有效化,因此Windows将会不停地发送WM_PAINT消息,直到永远。
2.4 获取设备环境句柄:方法二
相对于第一种方法只能适用于WM_PAINT消息处理过程中,方法二可以用于处理非WM_PAINT消息以及其他用途。方法二通过使用函数对GetDC函数和ReleaseDC函数,分别达到获取和释放设备环境句柄的目的:
与BeginPaint、EndPaint函数对类似之处,GetDC、ReleaseDC函数对必须成对使用,例如需要在一个消息内成对使用,而不能在一个消息内获取,在另外一个消息内释放。 与hdc = GetDC(hwnd); [GDI Operation] ReleaseDC(hwnd, hdc);
BeginPaint、EndPaint函数对不同之处,从GetDC返回的设备环境句柄的绘制矩形是整个客户区,同时GetDC不会将无效区域有效化。如果需要使无效区域有效化,则需要调用ValidateRect函数(或者其他类似功能的函数)。
通常GetDC和ReleaseDC函数对用于处理键盘消息或者鼠标消息。使用这两个函数,程序可以在收到用户的键盘或者鼠标输入时及时得绘制客户区,而不必为了生成WM_PAINT消息去可以使客户区的一部分无效化。
另一个与GetDC类似的函数是GetWindowDC,不同之处是GetWindowDC返回的时整个窗口的设备环境句柄。例如,程序可以使用GetWindowDC返回的设备环境句柄在标题栏上输出,相应的,程序也必须要处理WM_NCPAINT(非客户区绘制)消息。
2.5 TextOut函数详解
TextOut函数是显示文本的最重要的GDI函数,它的声明如下:
第一个参数hdc,表示设备环境句柄。BOOL TextOut( HDC hdc, // handle to DC int nXStart, // x-coordinate of starting position int nYStart, // y-coordinate of starting position LPCTSTR lpString, // character string int cbString // number of characters);
第二个、三个参数nXStart,nYStart,表示输出字符串相对于参考点的逻辑坐标X位置和Y位置。(默认参考点是客户区左上角)
第四个参数lpString,表示输出字符串。
第五个参数cbString,表示输出字符串的字符个数,
返回值为BOOL类型,当返回值为0时,表示函数失败,否则表示函数成功。
该函数在输出字符串过程中,涉及到了“映射模式”概念,它决定了如何将GDI绘图函数中的逻辑坐标转换成显示器上的物理像素坐标。这些映射模式是在设备环境中定义。默认的映射模式是MM_TEXT,在该模式下,逻辑单位和物理单位都是像素,坐标是相对于客户区的左上角,X方向的值是从左到右增大,Y方向的值是从上到下增大。另外该函数输出字符串到一个特定的裁剪区域内,这个区域由设备环境句柄指定,从GetDC得到的环境句柄中,该裁剪区域是整个客户区,从BeginPaint得到的环境句柄中,该裁剪区域是无效区域。
2.6 系统字体
设备环境同时还定义了在调用TextOut时Windows使用的字体,默认的字体称为“系统字体”或SYSTEM_FONT,系统字体是Windows在标题栏、菜单和对话框中使用的默认字体。在早期的Windows中,系统字体是一种等宽字体,从Windows 3.0开始,系统字体变成了变宽字体。系统字体是一种“点阵字体”:每个字符由像素点构成.(Chapter17节学习的TrueType字体,由轮廓定义字符)。在某种程度上,系统字体中字符的大小取决于显示器的大小:系统字体的设计要求能够在显示器上起码显示25行80列字符。
2.7 字符大小
为了用TextOut函数显示多行文本,需要知道字体中字符的尺寸,即由字符的高度可以决定下一行文字的垂直位置,由字符的平均宽度可以决定下一栏文本的水平位置。系统字体的尺寸与显示器类型有关,可以通过调用GetSystemMetrics函数来获取用户界面的尺寸,通过调用GetTextMetrics函数获取字体尺寸。GetTextMetrics函数需要一个设备环境句柄,该函数返回该设备环境中当前选定的字体的信息。Windows将把字符尺寸的各种值赋值到类型为TEXTMETRIC的结构中,该结构声明如下:
typedef struct tagTEXTMETRIC { LONG tmHeight; LONG tmAscent; LONG tmDescent; LONG tmInternalLeading; LONG tmExternalLeading; LONG tmAveCharWidth; LONG tmMaxCharWidth; [其他结构字段] } TEXTMETRIC, *PTEXTMETRIC;
这些字段的值的单位取决于设备环境中当前选定的映射模式,默认的映射模式是文本模式(MM_TEXT),相应这些字段的单位是以像素为单位的。具体各个字段标题字符的纵向尺寸大小参考下图:
字段tmHeight,表示字符的高度,是字段tmAscent和字段tmDescent的和,tmAscent和tmDescent分别是字符在基线之上和之下的最大高度。间距是两行文字之间的空间,在TEXTMETRICS结构中,内部间距(tmInternalLeading)包含在tmAscent中,该间距通常用于显示重音符号。字段tmInternalLeading的值可以设定为0,在这种情况下,带重音符号的字符会稍微短点,以便把重音符号包括在内。字段tmExternalLeading并不在tmHeight的值里,它是字体设计者建议在两行文字间留出的空间大小。在显示多行文字时,可以选择接受或者不理会这个建议。字段tmAvecharWidth是小写字符的加权平均宽度,大写字符的宽度一般是小写字符的1.5倍。字段tmMaxCharWidth是字体中最宽的字符的宽度。
2.8 文本格式化
Windows运行时,系统字体不会变化。因此应用程序只需要调用一次GetTextMetrics函数,最好的时机是在WM_CREATE消息处理时。在WM_CREATE消息处理过程中,我们可以使用静态变量或者全局变量cxChar,cyChar来保存字符的平均宽度和总高度,cxChar等于tm.tmAveCharWidth,cyChar等于tm.tmHeight和tm.tmExternalHeight之和。
Windows程序中可以使用sprintf或者wsprintf函数来格式化字符串。
2.9 综合使用
接下来,我们通过一个程序显示多行字符,运用以上所涉及的文本显示知识。该程序展示的内容是通过GetSystemMetrics函数获取WIndows各种图形项(例如鼠标、鼠标指针、标题栏和滚动条)的尺寸信息。该程序包含SYSMETS.H头文件和SYSMETS1.C文件。其中头文件中定义了一个结构数组,该数组中每个结构都包含了Windows头文件中用于GetSystemMetrics函数的索引,以及我们希望为每个返回的值而显示的文字。SYSMETS.H代码如下:
SYSMETS1.C代码如下:/*----------------------------------------------- SYSMETS.H -- System metrics display structure -----------------------------------------------*/ #define NUMLINES ((int) (sizeof sysmetrics / sizeof sysmetrics [0])) struct { int iIndex ; 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.C -- System Metrics Display Program No. 1 (c) Charles Petzold, 1998 ----------------------------------------------------*/ #define WINVER 0x0500 #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) ; }
2.10 SYSMETS1窗口过程
SYSMETS1.C的窗口过程WndProc处理三条消息:WM_CREATE、WM_PAINT和WM_DESTROY。其中WM_DESTROY的处理过程与Chapter03章节程序处理一致。WM_CREATE消息是窗口过程收到的第一条信息,在处理WM_CREATE消息时,SYSMETS1通过调用GetDC函数获取窗口的设备环境,并调用GetTextMetrics函数获取默认系统字体的尺寸。SYSMETS1将平均字符宽度保存在变量cxChar,将字符的总高度保存在变量cyChar中,将大写字符的平均宽度保存在静态变量cxCaps中。在等宽字体中,cxCaps等于cxChar。在变宽字体中,cxCaps设为cxChar的1.5倍。在TEXTMETRICS结构中,tmPitchAndFamily字段的低位决定是否为等宽字体:1表示变宽字体,0表示等宽字体。在WM_PAINT消息中,该程序完成了所有的绘制工作,在显示文本过程中,程序使用SetTextAlign函数设置数字对齐格式为右对齐,文本对齐格式为左对齐。
2.11 空间不够
SYSMETS1程序有一个麻烦的问题:除非你有一个大屏幕的高清晰显卡,否则系统尺寸列表的很多行是看不到的,如果把窗口变窄,你甚至连输出的数值也看不到。解决方案是在WM_SIZE消息处理中动态捕获窗口尺寸的大小,其中WM_SIZE消息中lParam字段的低位字是客户区的宽度(LOWORD(lParam)),而高位字是高度(HIWORD(lParam))。除了获取窗口客户区的大小外,我们还需要一个滚动条来实现浏览所有内容。接下来我们将继续介绍。
3 滚动条
滚动条是图形用户界面中最好的特性之一,包括水平方向滚动条和垂直方向滚动条两种,为用户提供了很好的视觉效果。在应用程序的窗口中包括滚动条相当容易,只需要在CreateWindow的第三个参数中包括窗口风格标识符WS_VSCROLL或者WS_VSCROLL,或者同时包括两者。在CreateWindow函数中指定的滚动条总是出现在窗口的右边和底部,而且总是伸展到整个客户区的宽度和高度。客户区并不包括滚动条所占用的时间。对于特定的显示驱动和显示分辨率,垂直滚动条的宽度和水平滚动条的高度是固定的,如果需要,可以通过调用GetSystemMetrics函数来获取它们的值。
Windows负责处理滚动条中的所有鼠标消息,但是,滚动条并没有自动对应额键盘接口,如果需要则必须显示地提供相应的对应关系(在Chapter05章节中,我们将在另一个版本的SYSMETS程序中演示具体做法)。
3.1 滚动条的范围和位置
每个滚动条都有相应的“范围”和“位置”。滚动条的范围是一对整数,分别代表滚动条的最小值和最大值。滚动条的位置是指滑块在范围中所处的值,是一个离散的整数值。当滑块在滚动条的顶端(或最左)时,滑块的位置是范围的最小值。当滑块在滚动条的底端(或者最右)时,位置是范围的最大值。在默认状态下,滚动条的范围是0—100.滚动条的范围可以通过SetScrollRange函数来设置,该函数的声明如下:
BOOL SetScrollRange( HWND hWnd, int nBar, int nMinPos, int nMaxPos,BOOL bRedraw );
第一个参数hWnd,指定一个滚动条或者一个包含标准滚动条窗口的句柄。第二个参数nBar,指定即将设置的滚动条类型,如下表所示:
第三个、四个参数nMinPos、nMaxPos,分别指定滚动条范围的最小值和最大值。
Value Description SB_CTL 设置滚动条的范围,hWnd必须是滚动条的句柄 SB_HORZ 设置窗口水平滚动条的范围,hWnd必须是窗口的句柄 SB_VERT 设置窗口垂直滚动条的范围,hWnd必须是窗口的句柄 第五个参数bRedraw,指定滚动条是否重绘来反映滚动条的范围变化。通过SetScrollPos函数设置滑块在滚动条范围的位置,函数声明如下:
该函数参数与SetScrollRange函数参数功能类似。int SetScrollPos( HWND hWnd, int nBar, int nPos, BOOL bRedraw );
另外,Windows提供了GetScrollRange函数和GetScrollPos来获取滚动条的范围和位置。
在程序中使用滚动条时,程序需要和Windows共同负责维护滚动条以及滑块在滚动条的位置。Windows负责如下任务:
- 处理滚动条中的所有鼠标消息。
- 当用户单击滚动条时,提供一种反向显示的闪烁。
- 当用户拖动滑块时,在滚动条内移动滑块。
- 向拥有滚动条的窗口的窗口过程发送滚动条消息。
程序需要负责如下任务:
- 初始化滚动条的范围和位置。
- 处理传送给窗口过程的滚动条消息。
- 更新滑块的位置。
- 根据滚动条的变化更新客户区的内容。
3.2 滚动条消息
当用户单击滚动条或者拖动滑块时,Windows向窗口过程发送WM_HSCROLL消息(水平滚动)或者WM_VSCROLL消息(垂直滚动),在滚动条的任何鼠标动作都会产生至少两条消息:一条在鼠标键按下时,一条在鼠标键松开时。就像所有消息一样,WM_HSCROLL和WM_VSCROLL消息都伴随着wParam和lParam消息。当滚动条是窗口的一部分时,可以忽略lParam参数:它只适用于滚动条是子窗口时,通常是在对话框中。wParam参数被分为低位字和高位字,wParam的低位字代表了鼠标在滚动条上的动作,这个值称为“通知码”,由一个以SB开头的标识符定义,通知码定义如下:
#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的标识符用于垂直滚动条中。
如果在滚动条的不同部分按住鼠标键不放,程序可能收到多条滚动条消息。当松开鼠标键时,程序会收到一条带有SB_ENDSCROLL通知码的消息。程序通常可以忽略带有SB_ENDSCROLL通知码的消息,Windows不会自己改变滑块的位置,应用程序需要通过SetScrollPos函数来改变它。
将鼠标放在滑块上然后按下鼠标键,可以移动滑块,这将会产生带SB_THUMBTRACK和SB_THUMBPOSITION通知码的滚动条消息。当wParam的低位字是SB_THUMBTRACK时,wParam的高位字是用户拖动滑块的当前位置。当wParam的低位字是SB_THUMBPOSITION时,wParam的高位字是用户松开鼠标键时滑块的最终位置。对于其他的滚动条操作,wParam的高位字应该被忽略。
为了给用户一些反馈,当用户用鼠标拖动滚动条滑块时,Windows会在你的程序中收到SB_THUMBTRACK通知码消息的同时移动滑块。但是除非程序在处理SB_THUMBTRACK或者SB_THUMBPOSITION时调用SetScrollPos函数,否则滑块会在用户松开鼠标键的同时回到原来的位置。程序可以处理SB_THUMBTRACK和SB_THUMBPOSITION消息,但很少会同时处理两者。如果处理SB_THUMBTRACK消息,则需要在用户拖动滑块时移动客户区的内容。而如果处理SB_THUMBPOSITION消息,则只需要在用户停止拖动滑块时更新客户区的内容。相对而言处理SB_THUMBTRACK消息更好些,当然实现也很困难些;在某些情况下,你的程序可能会来不及处理新产生的消息。
通知码SB_TOP、SB_BOTTOM、SB_LEFT和SB_RIGHT表示滚动条已经到了最小或者最大的位置。但是,如果滚动条是应用程序窗口的一部分,就不会收到这些通知码。
使用32位整数作为滚动条的范围虽然不常见,但是是完全可行的。但是由于wParam的高位字是一个16位的整数,在SB_THUMBTRACK和SB_THUMBPOSITION消息中它就不能正确表示滑块的位置,这时,就需要通过调用GetScrollInfo函数来获取这个消息(将在本章后续详细描述这点)。
附图,显示滚动条消息的wParam值的标识符:
3.3 加入滚动条的SYSMETS
接下来,我们将会把滚动条知识应用到SYSMETS程序中,先从垂直滚动条开始,SYSMETS2程序代码如下:/*---------------------------------------------------- SYSMETS2.C -- System Metrics Display Program No. 2 (c) Charles Petzold, 1998 ----------------------------------------------------*/ #define WINVER 0x0500 #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) ; }
SYSMETS程序中,在应用程序窗口增加垂直滚动条的方法是在CreateWindow函数的第三个参数中设置了WS_VSCROLL风格。在窗口过程处理WM_CREATE消息过程中通过SetScrollRange函数和SetScrollPos函数分别设置垂直滚动条的范围和初始化位置。在处理WM_VSCROLL消息过程中,根据消息通知码更新当前滑块位置,通过GetScrollPos函数查询滑块位置是否变化,如果变化,则调用SetScrollPos函数更新滑块位置,然后调用InvalidateRect函数使整个窗口无效。在移动滑块位置时,鉴于WM_PAINT消息是低优先级消息,在设置完滑块位置后,窗口过程可能会登上一段时间才会收到WM_PAINT消息。如果需要立即更新无效区域,可以在调用InvalidateRect函数后调用UpdateWindow(hwnd)函数,使窗口过程立即收到WM_PAINT消息,及时更新客户区。
4 效果更好的滚动条
SYSMETS2的功能良好,但是它的效率太差,不足以成为其他程序的范本。接下来,我们将一起学习一个更有效的程序。它将不使用之前使用的4个滚动条函数,而是采用Win32 API中独有的一些新函数。
4.1 滚动条信息函数
在Win32 API中新引入的两个滚动条函数是GetScrollInfo和SetScrollInfo,除了具有之前滚动条函数的功能外,还具有两个新功能,具体描述如下:功能1:关于滑块的大小。在SYSMETS2中滑块的大小是固定的,然而在你用过的一些Windows应用程序中,滑块的大小与显示在窗口中的文档多少是成比例的。在窗口中显示的文档数量称为“页面大小”,下面的公式表明了它们的关系:
滑块大小/滚动条长度 ≈页面大小/页面范围≈文档显示的数量/文档的总大小
在程序中可以通过SetScrollInfo函数来设定页面大小。
功能2:解决滑块位置的大小。假设滑块的范围是65535或者更大,在16位Windows操作系统中,这是不可能的。在Win32中函数定义为可以接受32位的参数,实际上也确实如此(当滚动条滑块的范围有这么大时,滑块的实际位置还受限于滚动条的物理尺寸,即像素)。但是,当收到通知码为SB_THUMBTRACK或者SB_THUMBPOSITION的WM_HSCROLL或者WM_VSCROLL消息时,只有16位被指定滑块的位置,而GetScrollInfo函数可以得到实际的32位值。两个新滚动条函数的声明如下:参数hwnd,fnBar,fRedraw的含义与之前滚动条函数参数相同。我们重点介绍SCROLLINFO结构体的声明:int SetScrollInfo( HWND hwnd, int fnBar, LPSCROLLINFO lpsi, BOOL fRedraw ); BOOL GetScrollInfo( HWND hwnd, int fnBar, LPSCROLLINFO lpsi );
参数介绍如下:typedef struct tagSCROLLINFO { UINT cbSize; UINT fMask; int nMin; int nMax; UINT nPage; int nPos; int nTrackPos; } SCROLLINFO; typedef SCROLLINFO FAR* LPSCROLLINFO;
- 字段cbSize,表示SCROLLINFO结构体的大小。
- 字段fMask,表示设置或者读取滚动条的信息类型,由一个或者多个以“SIF”为前缀的标志,用或运算符”|“组合在一起。
- 字段nMin和nMax,表示滚动条的最小值和最大值。
- 字段nPage,表示页面大小
- 字段nPos,表示滚动条滑块的当前位置
- 字段nTrackPos,表示当前追踪位置。
其中,常用的SIF前缀标志类型如下:
- SIF_RANGE,表示滚动条的范围,在SetScrollInfo函数中指定该标志时,必须在nMin和nMax中指定滚动条的范围。在GetScrollInfo函数中指定该标志时,nMain和nMax中返回滚动条的范围。
- SIF_POS,表示滚动条滑块的位置,在SetScrollInfo函数中指定该标志时,必须在nPos中指定滚动条滑块的位置。在GetScrollInfo函数中指定该标志时,nPos中返回滚动条滑块的位置。
- SIF_PAGE,表示页面的大小,在SetScrollInfo函数中指定该标志时,必须在nPage中指定页面大小。在GetScrollInfo函数中指定该标志时,nPage返回页面大小。
- SIF_TRACKPOS,表示当前滑块的位置,该标志只能用于GetScrollInfo函数中,而且只在处理通知码是SB_THUMBTRACK或者SB_THUMBPOSITION的WM_HSCROLL或者WM_VSCROLL消息中。
- SIF_DISABLENOSCROLL标志只用在SetScrollInfo中,当设定了这个标志,原来让滚动条不显示的设置这时将禁用滚动条。
- SIF_ALL标志以SIF_RANGE、SIF_POS、SIF_PAGE、SIF_TRACKPOS的组合,在处理WM_SIZE消息,该标志可以使得设置滚动条参数更方便。同样,处理滚动条消息也变得方便。另外在SetScrollInfo函数中,SIF_TRACKPOS标志将会被忽略。
4.2 最远可以卷动到哪里?
在SYSMETS2中,滚动条的范围是0和NUMLINES-1之间。当滚动条位置是0时,第一行显示在客户区的最上方。当滚动条的位置是NUMLINES-1时,最后一行显示在客户区的最上方,而其他行都不可见。这时我们会觉得程序滚动得太多了,实际上,它只需要滚动到最后一行显示在客户区的最下方即可。通过修改SYSMETS2可以做到这一点,与其在WM_CREATE消息处理时设定滚动条的范围,不如在WM_SIZE消息处理过程中再来确定滚动条的范围。代码如下:
iVscrollMax = max(0, NUMLINES-cyClient/cyChar); SetScrollRange(hwnd, SB_VERT,0,iVscrollMax,TRUE);
新的滚动条函数的另一个好处是:当使用滚动条页面大小时,Windows自动处理了上面的很多事情。结合SCROLLINFO结构和SetScrollInfo函数,只需要做如下的代码:
si.cbSize = sizeof(SCROLLINFO); si.cbMax = 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 + ,而不是si.nMax。
当页面大小和滚动条范围一样大时,会怎么样?Windows很聪明地隐藏了滚动条,因为这是并不需要滚动条。如果你不想隐藏滚动条,只需要在调用SetScrollInfo函数时设定SIF_DISABLENOSCROLL,以让Windows显示一个被禁用的滚动条。
4.3 新的SYSMETS
结合上面讲到的SetScrollInfo和GetScrollInfo函数,同时增加水平滚动条,我们生成了终结版的SYSMETS程序,程序代码如下:
/*---------------------------------------------------- SYSMETS3.C -- System Metrics Display Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------*/ #define WINVER 0x0500 #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 vertial 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 vertial 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函数设置位置。程序然后使用SetScrollInfo,如果在调用SetScrollInfo时位置超出了范围,Windows会自动修正位置,并通过GetScrollInfo调用返回正确的位置。
SYSMETS3使用ScrollWindow函数来滚动窗口客户区的内容,而不是重绘,尽管这个函数相对复杂(在新的Windows版本中,被更复杂的ScrollWindowEx取代),在SYSMETS3中的应用确实是非常简单的。该函数的第二个参数指定了客户区水平滚动多少像素,第三个参数是客户区垂直滚动得像素。ScrollWindow的最后两个参数设置为NULL,表示需要滚动整个客户区。Windows自动将新滚动出现的地方无效化,从而产生一条WM_PAINT消息。这里并不需要InvalidateRect函数。需要注意的时ScrollWindow函数不是GDI函数,因此不需要设备环境句柄作为参数,这是少数几个能够改变窗口的客户区显示的非GDI函数之一。
WM_HSCROLL消息处理截获了SB_THUMBPOSITION通知码,但是忽略了SB_THUMBTRACK通知码,这样,如果用户拖动水平滚动条的滑块,程序只有在用户松开了鼠标键时才会水平滚动窗口的内容。
WM_VSCROLL消息处理截获了SB_THUMBTRACK通知码,但是忽略了SB_THUMBPOSITION通知码。这样,当用户拖动垂直滚动条时,程序会相应地滚动窗口的内容,这样更符合用户的期待。
另一个加快WM_PAINT处理的方法是程序确定了哪些行落在了无效矩形中,并且只重绘拿些行。这样的代码更复杂,但是更快。
4.4 可我不想用鼠标
当我们不想使用鼠标来控制滚动条,而是想通过键盘上的一组方向键来控制时,我们需要在程序中加入键盘接口。在SYSMETS3程序中,WM_VSCROLL消息处理似乎处理了SB_TOP和SB_BOTTOM通知码。我们之前提到过,窗口过程不会从滚动条收到这些消息,所以目前看这些代码是多余的。但是在Chapter06中,当我们回过头来再看这个程序时,就会明白这么做的原因。
5 总结
这次,我们花了很大篇幅内容来介绍文本输出,里面涉及到了文本输出涉及到的设备环境相关控制、滚动条控制,为今后设备环境无关的操作奠定了基础。接下来Chapter05章节,我们将一起学习绘图基础,其中将学到GDI部分知识。
Bye ^_^