书籍:《Visual C++ 2017从入门到精通》的2.3.8 Win32控件编程
环境:visual studio 2022
内容:例2.17】复选按钮的使用
说明:以下内容大部分来自腾讯元宝。
问题:勾选复选按钮时,内容没有同步显示,需要等好一会儿才刷新。
原因:在InvalidateRect()之前没有调用GetClientRect(hWnd, &rt)获取当前窗口客户区的尺寸导致。
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
int wmEvent = HIWORD(wParam);
/* 功能:获取窗口客户区(除去标题栏、边框等)的矩形区域,用于定位绘图坐标。
参数说明:
hWnd:窗口句柄,标识目标窗口。
& rt:接收客户区坐标的RECT结构体指针。
应用场景:
在WM_PAINT中确定提示文本“选择你爱吃的水果:”的显示位置(rt初始值)。
在WM_COMMAND中标记整个客户区为无效区域,触发重绘*/
GetClientRect(hWnd, &rt); //获取窗口客户区矩形坐标
// 分析菜单选择:
switch (wmId)
{
/* 功能:通过BM_GETCHECK消息获取按钮的选中状态(BST_CHECKED或BST_UNCHECKED)。
参数说明:
(HWND)lParam:复选按钮的句柄。
0:忽略参数,仅查询状态。
状态更新逻辑:
若按钮被选中,将对应缓冲区(如szbuf1)填充为“西瓜”。
若取消选中,清空对应缓冲区内容,确保显示结果与实际状态一致。*/
case IDC_CHECK1:
iCheckFlag = (int)SendMessage((HWND)lParam, BM_GETCHECK, 0, 0);
if(iCheckFlag)
{
_tcscpy_s(szbuf1, _T("西瓜"));
}
else
{
_tcscpy_s(szbuf1, _T(""));
}
/* 功能:将指定矩形区域标记为无效,触发WM_PAINT消息以重绘该区域。
参数说明:
hWnd:窗口句柄。
& rt:需重绘的矩形区域(此处为整个客户区)。
TRUE:重绘时擦除背景(默认背景色为白色)。
实现逻辑:
每次复选按钮状态变化时,调用InvalidateRect标记客户区无效。
系统将WM_PAINT消息加入消息队列,待当前消息处理完成后执行重绘。
重绘时,WM_PAINT通过GetClientRect重新获取最新客户区坐标,确保绘制位置准确。*/
InvalidateRect(hWnd, &rt, TRUE);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
1. GetClientRect() 的作用
GetClientRect()用于获取窗口客户区的矩形坐标,即窗口中除去标题栏、边框等非客户区后的可绘制区域。其核心功能是将客户区的左上角和右下角坐标存储到RECT结构体中。
代码中的具体作用:
在处理菜单命令(WM_COMMAND)时,通过GetClientRect(hWnd, &rt)获取当前窗口客户区的尺寸,确保后续重绘操作基于最新的客户区范围。
示例中,rt变量存储了客户区的坐标信息,后续通过InvalidateRect()标记该区域为无效,触发重绘。
2. InvalidateRect() 的作用
InvalidateRect()用于标记指定矩形区域为无效,从而触发系统发送WM_PAINT消息,强制窗口重绘该区域。其参数含义如下:
**hWnd**:窗口句柄。
**lpRect**:指向要标记为无效的矩形区域(若为NULL,则整个客户区无效)。
**bErase**:是否擦除背景(TRUE表示擦除,FALSE保留原背景)。
代码中的具体作用:
每当菜单项(如IDC_CHECK1、IDC_CHECK2)状态变化时,更新对应的字符串变量(如szbuf1、szbuf2)。
调用InvalidateRect(hWnd, &rt, TRUE)将整个客户区标记为无效,并擦除背景,触发WM_PAINT消息,强制重绘界面以反映最新的文本内容。
注意:此处每次更新都标记整个客户区为无效,可能效率较低。更优的做法是仅标记文本变化的具体区域,减少重绘范围。
总结
GetClientRect():获取客户区尺寸,确保重绘区域准确。
InvalidateRect():强制重绘客户区,更新界面显示。
代码逻辑:通过菜单操作修改字符串后,立即标记客户区无效并重绘,实现界面动态更新。
在InvalidateRect()之前未调用GetClientRect()
如果在调用 InvalidateRect() 之前没有调用 GetClientRect(),InvalidateRect() 中的 lpRect 参数(即 &rt)将指向一个未初始化或错误的 RECT 结构体。这种情况下会导致以下结果:
1. 行为异常
未初始化的 RECT 结构体:
如果 rt 变量未通过 GetClientRect() 初始化,它可能包含随机的内存值(垃圾数据)。此时 InvalidateRect(hWnd, &rt, TRUE) 会将一个无效的矩形区域标记为无效,导致系统发送 WM_PAINT 消息时传入错误的坐标范围。
潜在后果:
若 rt 的坐标超出窗口范围,可能触发无效的重绘操作(例如尝试绘制到屏幕外)。
若 rt 的坐标完全错误(如负数或极大值),可能导致程序崩溃或出现不可预测的图形错误。
默认行为(lpRect = NULL):
如果代码中 rt 未被初始化但传递给 InvalidateRect() 的 lpRect 参数为 NULL(例如写错为 InvalidateRect(hWnd, NULL, TRUE)),则整个窗口客户区会被标记为无效。这与正确调用 GetClientRect() 后的效果类似,但缺乏针对性:
优点:仍能触发重绘以更新界面。
缺点:效率较低,因为重绘范围是整个客户区,而非仅需要更新的局部区域。
2. 具体代码中的影响
在您提供的代码中:
GetClientRect(hWnd, &rt); // 获取窗口客户区坐标
// ...
InvalidateRect(hWnd, &rt, TRUE); // 标记整个客户区为无效
如果没有调用 GetClientRect(),则 rt 未被初始化。假设 rt 是一个全局变量或静态变量,其内容可能是上次使用时的残留值,甚至是随机的未初始化内存。这将导致以下问题:
不可预测的重绘范围:InvalidateRect() 可能基于错误的矩形区域触发重绘,导致界面显示异常(如部分区域不刷新或闪烁)。
程序崩溃风险:如果 rt 的值恰好指向无效内存地址(如 NULL 或越界地址),程序可能崩溃。
3. 正确做法
必须调用 GetClientRect():
在调用 InvalidateRect() 之前,始终需要通过 GetClientRect() 获取当前窗口客户区的准确坐标,确保重绘范围正确。
GetClientRect(hWnd, &rt); // 获取正确的客户区矩形
InvalidateRect(hWnd, &rt, TRUE); // 标记该区域为无效
优化局部重绘(可选):
如果只需要更新窗口中的特定部分(如某个按钮或文本框),可以缩小 InvalidateRect() 的参数范围,减少重绘开销:
// 例如仅更新某个子区域
InvalidateRect(hWnd, &specificRect, FALSE); // 不擦除背景
总结
未调用 GetClientRect() 的直接后果:
InvalidateRect() 使用的 RECT 参数无效,可能导致程序崩溃或界面显示异常。
实际代码中的表现:
如果原代码中 rt 已被正确初始化(例如在其他地方调用过 GetClientRect()),则可能暂时正常运行,但这属于巧合,并非可靠做法。
最佳实践:
始终在调用 InvalidateRect() 前通过 GetClientRect() 获取最新的客户区坐标,确保重绘范围的正确性和程序的稳定性。