在以下任何事件发生时,向窗口过程都会受到一条WM_PAINT消息
- 用户移动一个窗口,导致原来被遮盖的部分窗口暴露出来
- 用户调整窗口的大小(当窗口类型的设定为CS_HREDRAW和CS_VREDRAW时)
- 程序调用ScollWindow或ScollDC函数滚动客户区时
- 程序调用InvalidateRect或InvalidateRgn函数显式生成WM_PAINT消息
当客户区的一部分被临时覆盖时,Windows会试图保存被覆盖的这部分,以便将来恢复时使用,但并不是每次都会成功,在以下情况下有时会发送一条WM_PAINT消息
- Windows关闭一个覆盖了部分窗口的对话框或消息框
- 下拉菜单被拉下然后收回
- 显示提示消息
少数情况下,Windows总是保存并恢复被覆盖区域,而不再发送WM_PAINT消息
- 鼠标指针在客户区内移动
- 在客户区内拖动图标
需要重新绘制的部分被称为“无效区域”或“更新区域”。Windows内部为每一个窗口都保存了一个“绘制信息结构”。这个结构保存着一个可以覆盖该无效区域的最小矩形的坐标和一个其他的信息。如果在窗口过程处理一条等候处理的WM_PAINT消息之前,客户区中的另一部分也失效了,那么Windows将计算出一个覆盖这两个失效部分的新的无效区域和无效矩形,并更新绘制信息结构中的数据。Windows不会在消息队列中放置多条WM_PAINT消息。
InvalidateRect函数强制使客户区中的一个矩形失效,可以通过GetUpdateRect函数获取这些坐标,在BeginPaint函数调用后,整个客户区会变成有效的,也可以用ValidateRect函数使一个区域有效,如果ValidateRect函数的调用使得整个客户区变得有效,那么消息队列中的WM_PAINT消息会被删除。
设备环境DC实际上是GDI内部维护的一个数据结构,对于视频显示,设备环境通常与屏幕上的一个特定的窗口相关联。程序必须在同一条消息中获取并释放DC,并且不能在两条消息中间传递DC,唯一的例外是通过CreateDC创建出来的。
- 获取DC的方法1
在处理WM_PAINT消息时。窗口过程首先调用BeginPaint函数。这个函数通常会擦除无效区域的背景,并填充PAINTSTRUCT结构的各字段。函数的返回值就是DC。
对于PAINTSTRUCT,程序只能使用前三个字段hdc、fErase和rcPaint,其他的供Windows内部使用。大多数情况下,fErase被设置为false,意味着Windows在BeginPaint函数中已经擦除了无效区域的背景(如果想在窗口过程中自定义背景擦除方式,必需自己处理WM_ERASEBKGND消息)。Windows使用在窗口类型中定义的hbrBackground字段指定的画刷在擦除背景。
如果通过InvalidateRect函数来使矩形失效时,InvalidateRect函数的最后一个参数将指定背景是否擦除,如果设置为false,Windows将不擦除背景,同时在调用BeginPaint时,PAINTSTRUCT结构中的fErase被设置为true。
InvalidateRect中的fErase可理解为是否需要擦除背景,true时Windows会擦除。PAINTSTRUCT中的fErase可理解为通知用户背景是否需要擦除,如果是true,那么用户可以判断这个字段来手工擦除。则,在InvalidateRect中设置为true,即指定需要擦除,那么BeginPaint函数就会擦除,而通知用户会被设置为false,因为已经由Windows擦除了,所以不需要用户再次擦除。
PAINTSTRUCT结构的rcPaint字段是一个RECT,保存了无效矩形的坐标。

InvalidateRect(hwnd,NULL,TRUE);这个调用会使整个客户区无效,并使其后调用的BeginPaint擦除原有的背景。在使用从BeginPaint返回的DC时,Windows无论如何也不会在rcPaint定义的矩形之外绘制。
- 获取DC的方法2
通过GetDC和ReleaseDC来获取的DC与BeginPaint获取的DC值不同。从GetDC返回的设备环境句柄中的裁剪区域是整个客户区,与无效矩形没有任何关系,GetDC也不会将无效区域有效化,必需由用户调用ValidateRect(hwnd,NULL);来使得整个客户区都有效。
而通过GetWindowDC获取的则是整个窗口区域,包括了标题栏,相应的程序必需要处理WM_NCPAINT(非客户区绘制消息)消息。
对于TextOut(hdc, x, y, psText, iLength)
- 设备环境DC中的属性决定了文本显示的属性,包括颜色,字体,背景色等。文本的背景色与客户区的背景色不同,文本的背景色是指文本框的背景色。
- x,y文本框左上角是相对于客户区左上角的坐标。默认的映射模式是MM_TEXT,即逻辑单位都是像素。
- psText是字符串指针,iLength是字符串长度,TextOut不认为字符串以0结尾,如果psText是UNICODE,那么iLength是字符串字节数的一半。
- TextOut使用的系统字体,在Windows启动后就不会改变,系统字体要求能够在显示器上起码显示25行80列字符。
GetSystemMetrics函数用来获取用户界面的尺寸,GetTextMetrics从dc中获取字体信息TEXTMETRIC。
除了上图中的重要的TEXTMETRIC字段外,还有
tmExtenalLeading,表示字体设计者建议的两行文字之间的空间大小,一般是0像素。
tmAveCharWidth,表示小写字符的加权平均宽度。一般大写字符的平均宽度是tmAveCharWidth的1.5倍。
tmMaxCharWidth,表示字体中最宽的字符的宽度。
消息处理函数更新为
int cxChar, cxCaps, cyChar; char szBuffer[50]; 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) ; j = NUMLINES; for (i = 0 ; i < 3; i++) { TextOut (hdc, 0, cyChar * i, TEXT("NAME"), 4); TextOut (hdc, 22 * cxCaps, cyChar * i, TEXT("Description"), 11); SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer, wsprintf (szBuffer, TEXT ("Value"))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ;
其中tmPitchAndFamily的第1位表示该字体是否是等宽字体,1是等宽,0不是。
代码为TEXT(“NAME”)栏,保留了22个大写字母的宽度。
SetTextAlign (hdc, TA_RIGHT | TA_TOP)使得字符框的右上角与TextOut中指定的坐标右上角对齐,所以代码为TEXT(“Description”)和TEXT(“Value”)一共保留了40个小写字符的宽度。
最后SetTextAlign (hdc, TA_LEFT | TA_TOP)恢复默认对齐。
可以处理WM_SIZE消息来获取窗口大小的改变,此时消息的lParam的低位WORD是客户区宽度,高位WORD是高度。可以用WINDEF.H中的宏来获取。
#define LOWORD(l) ((WORD)(l)) #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0XFFFF))
两个宏都返回WORD值,即无符号整形,可以直接赋值给int类型以便使用。
在窗口长宽发生改变时会得到WM_SIZE消息,由于在窗口类型定义时指定CS_HREDRAW | CS_VREDRAW,所以在WM_SIZE后会有WM_PAINT消息。
static int cxClinet, cyClient; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0 ;
cyClient / cyChar 表示客户区能够容纳的行数
cxClient / cxChar 表示一行大概能够容纳的小写字符数
Windows文档和头文件中的标识符都是从用户角度出发,向上滚动意味着移向文档的开始,向下滚动意味着移向文档的结束。客户区不包含滚动条的空间,滚动条的大小是确定的,可以通过GetSystemMetrics获取。
滚动条默认范围是0~100,可以通过GetScollRange和SetScollRange来获取和设置,其中参数iBar只有SB_VERT和SB_HORZ两种取值。 滚动条滑块的位置可以通过GetScollPos和SetScollPos来获取和设置。
滚动的使用需要Windows和程序共同维护,Windows主要负责
- 处理滚动条中的所有鼠标消息
- 当用户单击滚动条时,提供一种反向显示的闪烁
- 当用户拖住滑块时,在滚动条内移动滑块
- 向拥有滚动条的窗口的窗口过程发送滚动条消息
程序需要负责:
- 初始化滚动条的范围和位置
- 处理传递给窗口过程的滚动条消息
- 更新滑块位置
- 根据滚动条的变化更新客户区的内容
用户单击滚动条或拖动滑块时,Windows向窗口过程发送WM_VSCROLL消息或者WM_HSCROLL消息,当滚动条是窗口的一部分时,可以忽略lParam参数,wParam参数的低位字代表鼠标在滚动条上的动作,称为“通知码”,由SB开头的标识符。
- 在滚动条的不同部分按住鼠标不放,程序可能收到多条滚动条消息,当松开鼠标键时,通知码为SB_ENDSCOLL,通常可以忽略此通知码的消息。Windows并不会自己改变滑块的位置,需要程序用SetScollPos来设置。
- 将鼠标放在滑块上然后按下鼠标键,可以移动滑块。此时通知码为SB_THUMBTRACK和SB_THUMBPOSITION,当通知码是SB_THUMBTRACK时,wParam的高位字是滑块的当前位置,当通知码是SB_THUMBPOSITION时,wParam高位字是用户松开鼠标时,滑块的最终位置,其他通知码时,wParam高位字无意义。
- Windows会在发送消息码为SB_THUMBTRACK的消息的同时移动滑块,但是在用户松开鼠标时回到原来位置,除非程序用SetScollPos设置最终位置。
- 一般程序只处理SB_THUMBTRACK和SB_THUMBPOSITION中的一个,处理SB_THUMBTRACK需要适时更新客户区,处理SB_THUMBPOSITION需要在用户停止移动后更新客户区。
- SB_TOP,SB_BOTTOM,SB_LEFT和SB_RIGHT表示滑块到达了最小或者最大值。
- 由于wParam的高位字是16位,所以如果滚动条范围用32位整数表示的话,那么可能会出现问题,此时应该用GetScollInfo函数来获取滑块位置。
#define WINVER 0x0500 #define NUMLINES 75 #include <windows.h> LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM); int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT("SysMets"); 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 = TEXT("MainWindow"); if (!RegisterClass(&wndclass)) { MessageBox (NULL, TEXT("Program requires Windows NT"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(TEXT("MainWindow"),szAppName, 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 UINT cxChar,cxCaps,cyChar,cxClients,cyClients,uiMaxWidth; HDC hdc; PAINTSTRUCT ps; SCROLLINFO si; TEXTMETRIC tm; int iVertPos, iHorzPos, iPaintBeg, iPaintEnd, i,x,y; TCHAR szBuffer[20]; switch(message) { case WM_CREATE: hdc = GetDC(hwnd); ZeroMemory(&tm,sizeof tm); GetTextMetrics(hdc,&tm); cxChar = tm.tmAveCharWidth; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2; cyChar = tm.tmHeight + tm.tmExternalLeading; ReleaseDC(hwnd,hdc); uiMaxWidth = 40 * cxChar + 22 * cxCaps; return 0; case WM_SIZE: cxClients = LOWORD(lParam); cyClients = HIWORD(lParam); ZeroMemory(&si,sizeof si); si.cbSize = sizeof si; si.fMask = SIF_RANGE | SIF_PAGE; si.nMin = 0; si.nMax = NUMLINES - 1; si.nPage = cyClients / cyChar; SetScrollInfo(hwnd,SB_VERT,&si,true); ZeroMemory(&si,sizeof si); si.cbSize = sizeof si; si.fMask = SIF_RANGE | SIF_PAGE; si.nMin = 0; si.nMax = uiMaxWidth / cxChar + 2; si.nPage = cxClients / cxChar; SetScrollInfo(hwnd,SB_HORZ,&si,true); return 0; case WM_VSCROLL: ZeroMemory(&si, sizeof si); si.cbSize = sizeof si; si.fMask = SIF_ALL; GetScrollInfo(hwnd,SB_VERT,&si); 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; } si.fMask = SIF_POS; SetScrollInfo(hwnd,SB_VERT,&si,true); GetScrollInfo(hwnd,SB_VERT,&si); if (si.nPos != iVertPos) { ScrollWindow(hwnd,0,cyChar * (iVertPos - si.nPos), NULL,NULL); UpdateWindow(hwnd); } return 0; case WM_HSCROLL: ZeroMemory(&si, sizeof si); si.cbSize = sizeof si; si.fMask = SIF_ALL; GetScrollInfo(hwnd,SB_HORZ,&si); iHorzPos = 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; } si.fMask = SIF_POS; SetScrollInfo(hwnd,SB_HORZ,&si,true); GetScrollInfo(hwnd,SB_HORZ,&si); if (si.nPos != iHorzPos) { ScrollWindow(hwnd,cxChar * (iHorzPos - si.nPos),0, NULL,NULL); UpdateWindow(hwnd); } return 0; case WM_PAINT: hdc = BeginPaint(hwnd,&ps); ZeroMemory(&si,sizeof si); si.fMask = SIF_POS; GetScrollInfo(hwnd,SB_VERT,&si); iVertPos = si.nPos; si.fMask = SIF_POS; GetScrollInfo(hwnd,SB_HORZ,&si); iHorzPos = si.nPos; 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, szBuffer,wsprintf (szBuffer, TEXT ("Name%d"),i)) ; TextOut (hdc, x + 22 * cxCaps, y, szBuffer,wsprintf (szBuffer, TEXT ("Description%d"),i)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,wsprintf (szBuffer, TEXT ("Value%d"),i)) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint(hwnd,&ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd,message,wParam,lParam); }
上面的代码几乎涵盖了文本输出中滚动条的各种用法。
- 指定WS_HSCROLL和WS_VSCROLL表明该窗口将使用竖直和水平滚动条。
hwnd = CreateWindow(TEXT("MainWindow"),szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT, NULL,NULL,hInstance,NULL);
-
程序监听WM_CREATE消息,并且在窗口创建时,获取当前设备环境中字体的相关设置。
程序监听WM_SIZE消息,并且根据窗口大小的变化,记录客户区的大小,并设置滚动条的新位置和滚动块的大小。
程序监听多种通知码的WM_VSCROLL和WM_HSCROLL消息,并且设置滚动块的新位置,通过ScrollWindow函数滚动客户使得被滚动的区域失效而产生一条WM_PAINT消息。 - iPaintBeg和iPaintEnd推算出需要被重绘的输出行,以此来加快重绘的过程。
iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar); iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar);
- x和y推算出正在被重绘的行的起始位置,可能出现负值。
x = cxChar * (1 - iHorzPos); y = cyChar * (i - iVertPos);





