无效矩形
需要重绘的区域被称为“无效区域”,windows会计算出一个包含无效区域的最小矩形,这个最小矩形被称为“无效矩形”,调用windows函数可以获取到这个无效矩形。
当窗口中出现无效区域时,windows把WM_PAINT消息发送到窗口消息队列。消息队列中不会出现多条WM_PAINT消息,windows会根据新出现的无效区域,计算一个新的无效矩形。
设备环境句柄
1、BeginPaint和EndPaint
调用GDI函数基本都需要一个设备环境句柄参数。获取设备环境句柄的一个方法是调用BeginPaint,需要和EndPaint搭配使用。BeginPaint会使无效区域变成有效,所以BeginPaint一般在处理WM_PAINT消息中使用,否则windows会一直发送WM_PAINT消息。
除了设备环境句柄,BeginPaint还通过输出参数获取一个PAINTSTRUCT变量:
typedef struct tagPAINTSTRUCT {
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
- hdc:设备环境句柄。
- fErase:指示BeginPaint是否擦除了无效区域背景,0表示背景被擦除,非0则表示未擦除。
// 令整个客户区无效,并且其后的BeginPaint会发送WM_ERASEBKGND来擦除背景,fErase将是0
InvalidateRect(hwnd, NULL, TRUE);
// BeginPaint不会擦除背景,fErase将是非0
InvalidateRect(hwnd, NULL, TRUE);
- rcPaint:无效矩形。使用BeginPaint返回的设备环境句柄进行绘图时,绘制被限制在这个裁剪矩形中。
2、GetDC和ReleaseDC
使用GetDC返回的设备环境句柄可以在整个客户区内进行绘图,但是GetDC不会使无效区域有效化。因此GetDC一般不在WM_PAINT消息处理中使用,而用于处理键盘消息和键盘消息。
3、GetWindowDC和ReleaseDC
使用GetWindowDC返回的设备环境句柄可以在整个窗口内进行绘图。如果需要在非客户区进行绘图,需要处理WM_NCPAINT消息。
4、CreateDC和DeleteDC
根据名字获取特定设备的设备环境句柄。(BeginPaint、GetDC和GetWindowDC都是根据窗口句柄来获取特定窗口的设备环境句柄)
5、设备环境是和特定区域绑定的,绘图范围被限制在这个区域内。BeginPaint返回的和无效矩形关联,GetDC返回的和客户区关联,GetWindowDC返回的和整个窗口区域关联。
6、必须在同一条消息处理中获取和释放设备环境句柄,不能在消息之间传递设备环境句柄(CreateDC是例外)。
显示多行(列)文本
显示多行(列)文本时,一个需要解决的问题是如何确定文本位置。windows提供的TEXTMETRIC结构体记录了当前设备环境相关的字符尺寸信息和文本显示格式,利用这些尺寸信息可以计算出文本显示位置。
typedef struct tagTEXTMETRICW
{
LONG tmHeight; // 字符高度
LONG tmAscent;
LONG tmDescent;
LONG tmInternalLeading;
LONG tmExternalLeading; // 外部间距,可以理解为行间距
LONG tmAveCharWidth; // 字符平均宽度
LONG tmMaxCharWidth; // 字符最大宽度
LONG tmWeight;
LONG tmOverhang;
LONG tmDigitizedAspectX;
LONG tmDigitizedAspectY;
WCHAR tmFirstChar;
WCHAR tmLastChar;
WCHAR tmDefaultChar;
WCHAR tmBreakChar;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmPitchAndFamily; // 1:变宽字体 0:等宽字体
BYTE tmCharSet;
} TEXTMETRICW, *PTEXTMETRICW, NEAR *NPTEXTMETRICW, FAR *LPTEXTMETRICW;
TEXTMETRIC几个纵向尺寸间关系 |
---|
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;
// 对于变宽字符,字符宽度取1.5 * 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, cyChar * i, sysmetrics[i].szLabel, lstrlen(sysmetrics[i].szLabel));
TextOut(hdc, 22 * cxCaps, cyChar * i, sysmetrics[i].szDesc, lstrlen(sysmetrics[i].szDesc));
// 设置右对齐,TextOut的x参数指定最后一个字符位置
SetTextAlign(hdc, TA_RIGHT | TA_TOP);
// 这里利用了函数参数从右向左入栈的规则(函数调用约定)
TextOut(hdc, 22 * cxCaps + 40 * cxCaps, 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);
}
滚动条
窗口无法显示所有文本内容时,可以给窗口添加一个滚动条。添加滚动条最直接的方法是改变窗口风格:垂直滚动条WS_VSCROLL和水平滚动条WS_HSCROLL。
使用滚动条,我们需要负责更新滚动条的位置以及更新显示的内容,如果有需要还可以设置滚动条范围。
操作滚动条时,Windows向窗口过程发送WM_VSCROLL或WM_HSCROLL,其wParam的低位字表示滚动条的动作,称为“通知码”。使用宏HIWORD或LOWORD可以取到消息参数的高位字或低位字。
// 滚动条通知码
#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 // 拖动滑块结束,松开鼠标时,wParam的高位字表示滑块最终位置
#define SB_THUMBTRACK 5 // 鼠标拖动滑块移动时,wParam的高位字表示滑块当前位置
#define SB_TOP 6
#define SB_LEFT 6
#define SB_BOTTOM 7
#define SB_RIGHT 7
#define SB_ENDSCROLL 8
相关API:SetScrollRange、SetScrollPos、GetScrollPos、GetScrollRange
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 (int 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));
// 设置右对齐,TextOut的x参数指定最后一个字符位置
SetTextAlign(hdc, TA_RIGHT | TA_TOP);
// 这里利用了函数参数从右向左入栈的规则(函数调用约定)
TextOut(hdc, 22 * cxCaps + 40 * cxCaps, 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);
}
更好的滚动条
SetScrollInfo和GetScrollInfo也可以用于设置、获取滚动条信息,能够提供SetScrollRange、SetScrollPos、GetScrollPos、GetScrollRange相同甚至更好的功能。SetScrollInfo和GetScrollInfo通过SCROLLINFO 结构体来操作滚动条。
typedef struct tagSCROLLINFO
{
UINT cbSize; // 结构体大小
UINT fMask; // 描述了需要设置、获取哪些值的标志
int nMin; // 滚动条范围
int nMax; // 滚动条范围
UINT nPage; // 页面大小
int nPos; // 滑块当前位置
int nTrackPos; // 拖动中的滑块位置
} SCROLLINFO, FAR *LPSCROLLINFO;
typedef SCROLLINFO CONST FAR *LPCSCROLLINFO;
// fMask可以是下列值及其"位或"的组合
#define SIF_RANGE 0x0001
#define SIF_PAGE 0x0002
#define SIF_POS 0x0004
#define SIF_DISABLENOSCROLL 0x0008 // 禁用滚动条
#define SIF_TRACKPOS 0x0010
#define SIF_ALL (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS)
- nPage:默认情况,滚动条滑块大小是固定的。一个好的滑块应该能反映当前显示内容在全部内容中的占比。通过修改页面大小可以改变滑块大小。
- nTrackPos:通知码为SB_THUMBPOSITION或SB_THUMBTRACK的滚动条消息,wParam的高位字指示了滑块位置,只有16位。SCROLLINFO中的nTrackPos提供了32位的滑块位置信息。