拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC 鼠标忙碌状态设置:C++ 消息响应与光标切换实战

MFC 鼠标忙碌状态设置:C++ 消息响应与光标切换实战 1. 从一次误点说起MFC 里鼠标忙碌状态到底解决什么问题在 MFC/C 桌面应用里鼠标忙碌状态指的是程序正在执行耗时操作时把光标切换成沙漏或转圈样式同时让界面暂时不响应误操作。它适合谁适合所有用 MFC 写工具类软件、工业上位机、数据导入导出界面的开发者。能做什么一句话让用户知道“程序在忙别乱点”并且真的点不动。我见过太多项目点一下“开始计算”界面直接卡死用户以为程序崩了疯狂点按钮结果消息队列里堆了一串 WM_COMMAND等耗时操作结束这些点击像洪水一样涌进来弹出一堆重复对话框。这不是用户手贱是程序没给反馈。MFC 自带的CWaitCursor只能改光标挡不住消息。真正要落地鼠标忙碌状态得同时处理三件事光标切换、消息屏蔽、以及耗时操作期间的重绘。核心机制绕不开WM_SETCURSOR和OnSetCursor重写再配合一个 RAII 风格的等待光标类。这篇就按实际项目顺序走先讲消息响应原理再给可复制的类封装和消息映射代码然后编译运行验证最后把常见的坑一个个填掉。你跟着做十分钟内能在自己的对话框工程里看到效果。2. 前置准备TaoToken 接入与工程环境确认在动手改 MFC 代码之前先把两件事准备好一个是你的开发环境另一个是后面调试模型辅助代码时用到的 TaoToken 接入。环境方面你需要 Visual Studio 2017 及以上安装“使用 C 的桌面开发”工作负载MFC 组件勾选上。新建一个基于对话框的 MFC 项目名字随意我这边叫WaitCursorDemo。确认stdafx.h或pch.h存在后面加头文件要用。TaoToken 这边如果你打算在写代码时让模型帮你补全消息映射或者排查编译错误可以先去控制台拿一个 API Key。入口在 TaoToken 控制台登录后创建 Key复制保存。接入文档在 TaoToken 接入文档里面有 base_url 和请求示例。API 地址是https://taotoken.net/api注意这个不带 UTM直接填就行。提示Key 只显示一次丢了就重新建一个。别把 Key 硬编码进提交到仓库的代码里用环境变量或者本地配置文件。如果你只是想先验证模型能不能正常返回可以打开 模型对话 页面随便问一句“MFC 里 OnSetCursor 怎么重写”看响应是否正常。这一步不是必须但能帮你确认网络和 Key 没问题省得后面把模型问题和编译问题混在一起排查。3. 可复制配置消息映射、OnSetCursor 重写与等待光标类3.1 理解 WM_SETCURSOR 与 OnSetCursor 的调用时机Windows 在鼠标移动、窗口激活、或者光标需要重绘时会向窗口发送WM_SETCURSOR。MFC 把这个消息映射到CWnd::OnSetCursor默认实现会调用DefWindowProc让系统根据窗口类决定光标。关键点OnSetCursor的返回值决定是否继续处理。返回TRUE表示你已经设置了光标系统不再处理返回FALSE则交给默认处理。所以重写它的时候先判断当前是否处于忙碌状态是就SetCursor成等待光标并返回TRUE否则调用基类。// WaitCursorDemoDlg.h 中声明 protected: BOOL m_bBusy; HCURSOR m_hWaitCursor; virtual BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);// WaitCursorDemoDlg.cpp 构造函数里初始化 CWaitCursorDemoDlg::CWaitCursorDemoDlg(CWnd* pParent /*nullptr*/) : CDialogEx(IDD_WAITCURSORDEMO_DIALOG, pParent) { m_hIcon AfxGetApp()-LoadIcon(IDR_MAINFRAME); m_bBusy FALSE; m_hWaitCursor AfxGetApp()-LoadStandardCursor(IDC_WAIT); }// 重写 OnSetCursor BOOL CWaitCursorDemoDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { if (m_bBusy) { ::SetCursor(m_hWaitCursor); return TRUE; } return CDialogEx::OnSetCursor(pWnd, nHitTest, message); }这里用LoadStandardCursor(IDC_WAIT)加载系统沙漏光标。如果你想用自己的 .cur 资源改成LoadCursor加资源 ID 即可比如AfxGetApp()-LoadCursor(IDC_MYWAIT)前提是你在资源视图里导入了一个光标资源。3.2 封装一个带消息屏蔽的等待光标类光改光标不够耗时操作期间用户点击按钮消息还是会进队列。下面这个类用 RAII 思路构造时进入忙碌状态、设置光标析构时恢复光标、清空消息队列里积压的鼠标和键盘消息。// MyWaitCursor.h #pragma once class CMyWaitCursor { public: CMyWaitCursor(BOOL* pBusyFlag); void Restore(); ~CMyWaitCursor(); private: BOOL* m_pBusyFlag; };// MyWaitCursor.cpp #include pch.h #include MyWaitCursor.h CMyWaitCursor::CMyWaitCursor(BOOL* pBusyFlag) : m_pBusyFlag(pBusyFlag) { if (m_pBusyFlag) *m_pBusyFlag TRUE; AfxGetApp()-BeginWaitCursor(); } void CMyWaitCursor::Restore() { AfxGetApp()-RestoreWaitCursor(); } CMyWaitCursor::~CMyWaitCursor() { if (m_pBusyFlag) *m_pBusyFlag FALSE; // 清空消息队列中积压的输入消息避免忙碌期间的点击在结束后集中触发 MSG msg; while (::PeekMessage(msg, NULL, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE)) { // 鼠标消息直接丢弃 } while (::PeekMessage(msg, NULL, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)) { // 键盘消息直接丢弃 } AfxGetApp()-EndWaitCursor(); }注意这里和网上流传的版本有个区别很多老代码用PeekMessage(msg, NULL, 0, 0, PM_REMOVE)清空所有消息但这样会把WM_PAINT也吃掉导致界面不重绘。更稳妥的做法是只移除鼠标和键盘输入消息保留WM_PAINT让系统正常绘制。3.3 在按钮响应里使用void CWaitCursorDemoDlg::OnBnClickedButtonStart() { CMyWaitCursor wait(m_bBusy); // 模拟耗时操作 for (int i 0; i 50; i) { Sleep(100); // 如果耗时操作里有消息泵需要调用 Restore 恢复光标 // 这里没有消息泵所以不需要 } // 析构时自动恢复 }如果你在耗时操作中间弹了对话框或者调用了MessageBox光标会被系统改回去这时候要手动调wait.Restore()。这是Restore存在的意义。4. 验证请求与成功结果编译运行后的观察步骤代码写完后按下面步骤验证。第一步编译。在 VS 里按CtrlShiftB确认没有错误。常见错误是pch.h名字不对VS2019 之后默认是pch.h老项目是stdafx.h按你的项目改。第二步运行。按F5启动主对话框出现鼠标是正常箭头。第三步点击“开始”按钮。立刻把鼠标移到对话框上光标应该变成沙漏。在 5 秒的Sleep期间疯狂点击按钮和其他区域观察两点光标是否保持沙漏操作结束后是否只执行了一次没有堆积的弹窗或重复响应。第四步验证消息屏蔽。在按钮响应里加一句AfxMessageBox(_T(done));放在CMyWaitCursor析构之后。忙碌期间狂点结束后应该只弹一次。如果弹了多次说明消息没清干净检查PeekMessage的范围参数。第五步验证重绘。把对话框拖到屏幕外再拖回来或者用另一个窗口遮挡再移开界面应该正常重绘没有花屏。如果花屏说明你把WM_PAINT也移除了改回只移除鼠标键盘消息。成功的结果是沙漏光标稳定显示误点被吞掉界面不卡死重绘正常。这时候你可以把Sleep换成真实的耗时逻辑比如数据库查询、文件解析、图像处理。5. 本篇常见错排查5.1 光标不变成沙漏最常见的原因是OnSetCursor没被调用。检查三点消息映射里有没有ON_WM_SETCURSOR()函数签名是不是BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)m_bBusy有没有在构造CMyWaitCursor时被置为TRUE。还有一种情况鼠标不在对话框客户区而在标题栏或边框上。WM_SETCURSOR的nHitTest会是HTCAPTION之类你的重写如果只处理客户区就会漏掉。上面的代码没有区分nHitTest所以整个窗口都会变沙漏这通常够用。5.2 忙碌期间点击仍然触发如果你用的是 MFC 自带的CWaitCursor它只改光标不清消息点击当然会进队列。换成第 3.2 节的CMyWaitCursor并且确认析构里的PeekMessage范围是WM_MOUSEFIRST到WM_MOUSELAST。有些老代码写msg.message ! WM_PAINT来判断逻辑反了会把非WM_PAINT的消息全丢掉包括一些系统消息导致界面异常。5.3 耗时操作里弹窗后光标恢复失败MessageBox会触发WM_SETCURSOR系统把光标改回箭头。这时候在弹窗之后调用wait.Restore()让 MFC 重新设置等待光标。注意Restore要在弹窗关闭之后调用不是之前。5.4 多线程场景下的光标问题如果你的耗时操作放在工作线程里主线程的消息泵还在跑OnSetCursor依然会被调用但m_bBusy的读写要考虑线程安全。简单做法是用std::atomicbool替代BOOL或者只在 UI 线程改这个标志。工作线程不要直接调AfxGetApp()-BeginWaitCursor()MFC 的等待光标机制不是线程安全的。5.5 编译报错 C2065 找不到标识符检查头文件包含顺序。MyWaitCursor.h里如果用到BOOL需要包含afxwin.h或者放在pch.h之后。VS 的预编译头机制要求pch.h必须是每个 cpp 的第一行包含顺序错了会报一堆莫名其妙的错。6. 继续深入把忙碌状态接到你的编码工作流到这里鼠标忙碌状态的核心代码已经能跑了。但实际项目里你可能还会遇到更复杂的场景比如耗时操作需要显示进度条、需要支持取消、或者要在忙碌期间保持某个动画。这些都可以在CMyWaitCursor的基础上扩展。如果你在写这些扩展代码时想让模型帮你补全消息映射、解释WM_SETCURSOR的返回值语义或者排查一个诡异的编译错误可以用 TaoToken API Keys 拿到的 Key 接到你的编辑器里。长期做 MFC 或者 Agent 类编码工作的可以看看 Coding Plan按需选用。最后留一个实用技巧把CMyWaitCursor的析构里加一行日志输出记录本次忙碌期间丢弃了多少条消息。这个数字能帮你判断用户的误点频率如果特别高说明你的按钮该加禁用状态了而不是只靠光标提示。
返回列表