简介:CButtonST V3.9修正版是基于微软基础类库(MFC)的经典按钮增强控件资源包,特意针对Visual Studio 2012编译环境做了兼容性修正,主要面向仍在用VS2012维护Windows桌面程序的C++开发人员。原版V3.9由作者在Visual C++ 6.0时代编写,直接升级到新IDE时会遇到旧版API不再支持、编译器语法检查更严格、库文件版本不匹配等编译问题;压缩包已解决这些障碍,同时保留了原有多种按钮样式、自定义鼠标键盘事件、状态外观反馈、文字与图标自由布局以及多语言支持等实用特性。压缩包共93个文件,大小约617KB,核心为35个ico图标、18个h头文件、16个cpp源文件,还包含bmp位图、wav音效、rc资源脚本、sln与vcxproj工程文件以及完整Demo演示程序,便于阅读源码、修改界面设计并直接编译运行。已有366人学习下载,适合希望继续沿用CButtonST功能又不愿退回旧IDE的MFC开发者,拿到手即可在VS2012中打开工程,快速恢复常用按钮控件的开发效率。
1. 二十年前的按钮类,升级 VS2012 时把项目卡了一周
接手一个从 VS2008 时代传下来的老 MFC 工程,用 VS2012 一编译,几十条报错几乎全指向同一个文件:CButtonST V3.9 修正版。这个自绘按钮类我很熟,贴位图、悬停变色、按下换图全靠它,可 VS2012 的编译器换了脾气:默认 Unicode、新 SDK 要求自己声明系统版本、旧代码里 DWORD 存句柄的写法被警告得抬不起头。这篇笔记把“在 VS2012 下编译通过”这件事拆开讲:先说清它为什么挂,再给一套能直接复现的工程配置和最小使用代码,最后把运行期高频问题和泄漏验证方法都摆出来。适合正在升级老项目、或者想用成熟自绘按钮又不想碰 CMFCButton 的人。
2. CButtonST V3.9 在 VS2012 下编译失败的根源:字符集、SDK 版本与消息映射
2.1 CButtonST 的核心职责:它到底替你干了什么
CButtonST 是对 CButton 的 owner-draw 封装。普通按钮由窗口系统自己画,而 CButtonST 把 BS_OWNERDRAW 接管过来,把按钮的点击、悬停、按下、禁用状态翻译成位图、文字和背景色的组合。这个类最实用的地方在于,它把按钮的视觉状态机暴露成了几个成员函数,调用方不需要自己去处理 WM_MOUSEMOVE、WM_LBUTTONDOWN、WM_ENABLE 这一整套状态跟踪。
我最早用这个类是为了做一个软件顶部的“部署”大按钮,那种带渐变位图、鼠标移过去就换图的效果。如果自己写,至少要在 OnMouseMove 里记录坐标,在 OnMouseLeave 里清状态,还要在 OnDrawItem 里区分四种状态;而 CButtonST 把这些全部收进内部实现。这也是为什么它明明很老,至今还有工程在用——在 MFC 世界里,自绘按钮的轮子不必再造。
不过要注意,CButtonST 不注册窗口类,也不是 ActiveX 控件,它只是一个 C++ 类,通过 Create 或 SubclassDlgItem 的方式工作。这一点决定了它在 VS2012 下失败时,错误信息不会指向某个 DLL,而是一堆编译期报错:头文件缺失、类型不匹配、宏未定义。所以排查思路不能停留在“换个库版本”,要从工程配置和编译器假设入手。
2.2 VS2012 的编译器与旧代码之间有三道坎
第一道坎是字符集。VS2012 新建的 MFC 工程默认 Unicode,而老代码里大量使用 char*、char 数组,和 CString 做隐式转换。在 MBCS 时代,CString 与 char* 互转很随意;切到 Unicode 后,CString 对应的是 CStringW,把 char* 直接塞给它,编译器会给出 C2664 这类参数不匹配的报错。CButtonST 的源码里,保存按钮文字、工具提示文本的地方,没少用这种写法。
第二道坎是 SDK 版本宏缺失。新版 Windows SDK 的很多接口、消息、结构体,比如 TrackMouseEvent、WM_MOUSELEAVE、TME_LEAVE,在 WinUser.h 里是被 _WIN32_WINNT 宏包着的。如果工程里没人定义 _WIN32_WINNT,这些符号根本不会出现在编译器的视野里。十年前那批类库代码,往往继承自更早的工程设置,stdafx.h 里根本没有这些预处理宏。于是鼠标悬停跟踪这个功能点,在 VS2012 下会先死在前置声明阶段。
第三道坎是编译器收紧了类型转换。VS2012 的编译器对句柄相关类型、DWORD 与指针之间的隐式转换变得更加敏感。CButtonST 早期版本把 HBITMAP、HICON 这些句柄用 DWORD 成员变量保存,在 32 位下可能只是警告,到 VS2012 下配合 64 位编译选项时,这类代码会直接编译不过,或者运行期句柄截断。此外,MFC 消息映射展开后要求处理函数签名与新版 MFC 头文件完全一致,只要返回类型和参数类型对不上,就会报 C2440。
这三道坎叠加起来的效果是:你拿到的 CButtonST 如果没有针对 VS2012 做过适配,编译报错会横跨预编译头、类声明、消息映射和实现文件四个区域,看起来像源代码整个坏了。实际上类逻辑没有变,坏的是构建假设。
2.3 修正版到底修了什么:源码里值得确认的三个点
拿到一个自称“VS2012 修正版”的 CButtonST 源码,先不要急着编译,打开源文件确认三个地方。
第一,stdafx.h 或者 CButtonST.h 里有没有定义 _WIN32_WINNT 和 WINVER。如果没有,自己在工程预编译头里补上:
#ifndef WINVER #define WINVER 0x0601 // Windows 7 级别 API #endif #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0601 // 让 TrackMouseEvent 等声明可见 #endif这段代码解决的是 2.2 里的第二道坎。0x0601 对应 Windows 7 / Server 2008 R2,如果你的目标环境是 Windows 10,也可以直接写 0x0A00,但 MFC 老代码跟着提升到 0x0601 通常已经足够。注意,宏要在 include <afxwin.h> 之前生效,所以放在 stdafx.h 最上面,不要放到某个 .cpp 文件里,否则预编译头已经展开,宏就不生效了。
第二,消息映射里有没有处理 WM_MOUSELEAVE。修正版一般会在类里增加 ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave) 映射,并在实现文件里调用 TrackMouseEvent。你可以搜索源文件里有没有 “ON_MESSAGE(WM_MOUSELEAVE” 这个字符串。没有的话,悬停状态基本是残废的,鼠标离开按钮后按钮不会恢复常态。
第三,用 HBITMAP 还是 DWORD 保存位图句柄。全局搜 DWORD 与 Bitmap 相邻的成员声明,凡是存句柄的,修正版应该已经改成 HBITMAP 或 ULONG_PTR。不然你即使 32 位编译过,x64 构建也容易翻车。这三条都过了,再进 VS2012 编译,报错数量会大幅下降。
3. 在 VS2012 里跑通 CButtonST V3.9:工程配置与最小使用代码
3.1 最小修正工程:用动态创建绕开资源模板依赖
把 CButtonST 接到项目里,最稳妥的方式不是去资源编辑器里改控件类,而是动态创建。原因是 CButtonST 不是注册窗口类,你在 .rc 文件里把某个 Button 的类名改成 CButtonST,VS2012 的资源编辑器不会帮你生成对应的 DDX_Control 绑定,运行时大概率得到一个无法初始化的控件变量。直接往 OnInitDialog 里 new 一个,又快又可控。
下面是最小工程里的一段标准代码,注意在对话框类的头文件里先声明成员指针:
// CMyDialog.h class CMyDialog : public CDialogEx { public: CButtonST* m_pBtnDeploy; // 自绘按钮指针,初始化为 NULL BOOL OnInitDialog() override; void OnDestroy() override; };实现部分:
// CMyDialog.cpp BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); m_pBtnDeploy = new CButtonST; CRect rcBtn(20, 20, 160, 64); m_pBtnDeploy->Create(_T(""), WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, rcBtn, this, IDC_BTN_DEPLOY); m_pBtnDeploy->SetBitmaps(IDB_BTN_DEPLOY_N, IDB_BTN_DEPLOY_H, IDB_BTN_DEPLOY_P, IDB_BTN_DEPLOY_D); return TRUE; }Create 的参数说明:第一个参数是按钮文字,这里留空,因为位图已经能表达含义;第二个参数要显式加 BS_OWNERDRAW;第三、四个是按钮矩形和父窗口;最后一个 IDC_BTN_DEPLOY 是控件 ID,必须和你的资源编号对得上。SetBitmaps 的前三个参数分别对应常态、悬停、按下三个状态的位图 ID,第四个参数是禁用状态位图,传 0 表示沿用常态图。
为什么强调 BS_OWNERDRAW?因为 CButtonST 的绘制逻辑完全挂在 WM_DRAWITEM 上,如果去掉这个样式,窗口系统会按普通按钮去画,位图根本不会出现。常见的错误是,把 Create 的样式写成 WS_CHILD | WS_VISIBLE 忘了 BS_OWNERDRAW,运行起来发现按钮还是默认样式,才回头改。
3.2 设置 Bitmap 与 Hover 效果的必改参数
在最小工程跑通后,做一套完整按钮效果需要几个调用。把它们都集中在 OnInitDialog 里,很容易观察参数:
m_pBtnDeploy->SetMouseHover(TRUE); // 开启悬停换图 m_pBtnDeploy->SetFlat(TRUE); // 平面化,去掉系统边框 m_pBtnDeploy->SetTransparent(TRUE); // 位图透明背景生效 m_pBtnDeploy->SetColor(RGB(255, 255, 255)); // 文字颜色 m_pBtnDeploy->SetTooltipText(_T("一键同步")); // 悬停提示SetMouseHover(TRUE) 的副作用是:内部会在第一次 WM_MOUSEMOVE 时主动调用 TrackMouseEvent,所以这个方法必须在窗口句柄有效之后调用,不能放在 CButtonST 构造函数里。如果你发现按钮的悬停效果时灵时不灵,优先检查这一句有没有在 Create 之后才执行。
SetTransparent(TRUE) 的透明色默认取的是位图左上角第一个像素的颜色值。所以做图的人需要把位图放在一张背景色与对话框一致的画布上,四个角涂成同样的颜色,否则按钮边缘会出现一个长方形的底色块。这是自绘按钮最常见的“没透明成功”案例,后面 4.3 还会展开。
SetTooltipText 内部维护了一个 CToolTipCtrl,但不是所有版本都会自动给按钮加 BS_NOTIFY。如果你的提示始终不弹出来,给 Create 的样式里补上 BS_NOTIFY,或者调用 SetTooltipText 之前确认控件 ID 非零。这五个参数是按“常见做法”补全的,具体命名以你手上的源码为准。
3.3 决定生死的三个编译开关:字符集、平台工具集与 MBCS 补丁
工程配置层面有三个开关决定 CButtonST 能不能编译通过,缺一不可。
第一个是平台工具集。VS2012 默认对应 v110。在项目属性 -> 配置属性 -> 常规 -> 平台工具集里选择 v110。如果要部署到 Windows XP,需要升级到 VS2012 Update 1 之后选择 v110_xp,它会链接适合 XP 的运行时。用错工具集不会导致 CButtonST 本身报错,但整个 MFC 程序在目标机器上启动时会报缺少运行时 DLL,排查方向完全不同。
第二个是字符集。在项目属性 -> 常规 -> 字符集里选“使用 Unicode 字符集”。CButtonST 修正版内部已经尽量统一使用 CString 和 _T() 宏,Unicode 字符集下编译是安全的。如果你的老工程仍然用 MBCS,请先装 VS2012 的 MBCS 补丁包,再把工程字符集设为 MBCS,两条路只能选一条,不要试图在 Unicode 字符集下保留大段 char* 硬编码。
第三个是预编译头。老工程多半开了 /Yu 预编译头,stdafx.h 的内容会被缓存。CButtonST 的头文件一定要 include 在 stdafx.h 的 afxwin.h、afxext.h 之后,顺序反了会出现“CButton 未定义”之类的错误。建议直接把头文件加入 stdafx.h 末尾,而不是散落在各个对话框 .cpp 里:
#include <afxwin.h> #include <afxext.h> #include "CButtonST.h" // 修正版头文件这里把 CButtonST.h 放在预编译头里,所有编译单元都可见,代价是改一次 CButtonST.h,全工程重编译。我的习惯是放预编译头,毕竟这个类几乎每个对话框都可能用。
4. CButtonST 常见问题的 5 个排查样例:现象、原因与处理
4.1 按钮不显示位图只显示大写字符串
现象:运行起来按钮还在,能点击,文字也正常,就是位图不显示,连背景色都没有。
原因有几种,按优先级排查。最常见的是 Create 样式里漏了 BS_OWNERDRAW,窗口系统按标准按钮渲染,压根不调用 OnDrawItem。其次,SetBitmaps 调用时机如果发生在 Create 之前,内部 LoadBitmap 虽然能加载,但 DrawItem 时窗口句柄还没准备好,绘图函数可能被静默跳过。还有一种情况是位图资源被误存成了 PNG 或其他格式,MFC 的 LoadBitmap 只认 BITMAP 资源,和 CImage 那套加载路径完全不同。
解决:先给 Create 显式加 BS_OWNERDRAW,再把 SetBitmaps 挪到 Create 之后,最后在 SetBitmaps 后面加一行临时校验:
CBitmap bmp; BOOL bOK = bmp.LoadBitmap(IDB_BTN_DEPLOY_N); ASSERT(bOK);如果断言失败,去资源视图里检查 IDB_BTN_DEPLOY_N 的类型,应该是 BITMAP 而不是 PNG 或 RCDATA。注意 CBitmap 的临时对象只是为了验证资源,不要把它再传给 CButtonST,否则两个对象管理同一个位图,析构时会重复删除。
4.2 鼠标移出后按钮一直停留在按下状态
现象:按住按钮不松开,鼠标拖出按钮区域,按钮保持“按下”的高亮状态;把鼠标移回来又重新响应。
原因:CButtonST 的悬停状态机依赖 WM_MOUSELEAVE。这个系统消息不是自动产生的,必须由程序调用 TrackMouseEvent 注册。注册动作通常在 OnMouseMove 里第一次移动鼠标时执行。如果 _WIN32_WINNT 宏没定义,TrackMouseEvent 根本无法编译;修正版虽然补了代码,但你的工程预编译头仍然可能把它挡在外面。
解决:首先确认 stdafx.h 里 _WIN32_WINNT 不低于 0x0400,建议直接用 0x0601。然后检查 SetMouseHover(TRUE) 是否在 Create 之后调用。如果跟踪代码被某种条件编译分支屏蔽了,可以在 CButtonST 的 OnMouseMove 里补一段:
// 第一次移动鼠标时注册离开通知 if (!m_bMouseTracking) { TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 }; ::TrackMouseEvent(&tme); m_bMouseTracking = TRUE; }在对应 OnMouseLeave 里把 m_bMouseTracking 重置为 FALSE,同时把按钮状态改回普通态并 Invalidate。这样每次鼠标重新进入按钮,都会重新注册一次离开通知,不会出现“只跟踪第一次”的问题。
4.3 背景色变成纯黑或纯白块,视觉样式下很难看
现象:位图本身没问题,但按钮四周有一个矩形背景块,颜色和对话框不一致;在开启了视觉样式的 Windows 上尤其扎眼。
原因:两个来源。第一个是透明色参数与位图左上角像素不一致。CButtonST 的透明实现是从位图左上角取一个参考色,凡是接近这个颜色的像素全部画成背景色。如果位图左上角那一个点是纯蓝,而位图边缘其实是深蓝渐变,边缘一圈就会被填成背景色,形成黑边。第二个来源是系统视觉样式在按钮区域画了一层主题背景,把自绘内容顶到前面。
解决:做素材时把位图四角统一填充为对话框背景的 RGB 值,不要在角落留渐变;同时在 OnInitDialog 里给按钮关闭主题接管:
#include <uxtheme.h> #pragma comment(lib, "uxtheme.lib") ::SetWindowTheme(m_pBtnDeploy->GetSafeHwnd(), L"", L"");这行代码的作用是告诉系统,这个按钮不要绘制主题背景,交给自绘逻辑。需要注意的是,关闭主题后按钮的鼠标悬停渐变也没了,只画你自绘的内容,如果 UI 里需要保留系统反馈,要权衡使用。
4.4 64 位编译时位图句柄丢失或崩溃
现象:同样的代码,Win32 平台编译正常;切到 x64 编译,按钮要么空白,要么点击时崩溃,崩溃点在 OnDrawItem 或析构函数里。
原因:典型根因是把 HBITMAP 或 HICON 存进了 DWORD 成员。32 位下句柄正好 32 位,强转没事;64 位下句柄是 64 位,存进 DWORD 直接截断,DrawItem 时拿到的句柄是垃圾地址。如果你拿的版本号称修正版但源码里仍大量出现 (DWORD)LoadBitmap 之类的写法,那就说明没修彻底。
解决:全局替换,把保存句柄的成员从 DWORD 改成 HBITMAP、HICON 对应的真实类型。改完后用命令行快速扫描一轮,确认没有漏网:
findstr /n /i "DWORD.*Bmp DWORD.*Bitmap DWORD.*Icon" CButtonST.h CButtonST.cpp输出为空的健康状态。如果还有结果,顺带检查 DWORD 里存的“控件状态”“按钮风格”这类真正的整数,不要无差别替换,否则编译都过不了。CButtonST 内部也有不少整型成员,比如 m_nState、m_bMouseTracking,这些不能动。
4.5 子类化对话框按钮后退出时崩溃
现象:在 OnInitDialog 里用 SubclassDlgItem 把某个 CButton 替换成 CButtonST,功能正常;关闭对话框时,在 CButtonST 析构里崩溃,有时报 Debug Assertion Failed,有时直接访问违例。
原因:资源双重释放。CButtonST 的析构函数会删除 SetBitmaps 创建的资源;如果按钮是子类化来的,对话框销毁时会自动销毁按钮窗口并触发 CButtonST 析构,但某些代码又在外层手动 delete 了一次按钮对象,或者对话框成员里有同名指针,导致同一个对象被 delete 两次。
解决:坚持“谁创建,谁释放”的原则。动态创建的按钮,在对话框 OnDestroy 里只释放一次:
if (m_pBtnDeploy != NULL) { if (::IsWindow(m_pBtnDeploy->GetSafeHwnd())) m_pBtnDeploy->DestroyWindow(); delete m_pBtnDeploy; m_pBtnDeploy = NULL; }采用 SubclassDlgItem 的,则在 OnDestroy 里调用 UnsubclassDlgItem 取消子类化,让资源模板里的原始按钮接管生命周期。实践里我更喜欢动态创建,因为 Subclass 这条路在 VS2012 下还涉及资源模板中按钮 ID 与 DDX 映射的先后问题,调试成本高出一个量级。
5. 修正版还能继续压榨的 3 个实用技巧
5.1 用 CButtonST 做菜单按钮:不引入 CMFCButton 也能有下拉
老界面里经常需要“主按钮 + 下拉菜单”的组合。VS2012 的 CMFCButton 能嵌菜单,但要为一个小按钮引入整套 CMFC 框架,有点重。CButtonST 自带位图和悬停效果,配合 TrackPopupMenu 一样能实现,而且依赖更轻。
写法:给按钮的点击事件写菜单弹出。由于按钮的 click 事件在父窗口处理,直接这样写:
void CMyDialog::OnBnClickedDeployMenu() { CMenu menu; if (!menu.LoadMenu(IDR_MENU_DEPLOY)) return; CMenu* pPopup = menu.GetSubMenu(0); if (pPopup == NULL) return; CRect rcBtn(0, 0, 0, 0); m_pBtnDeploy->GetWindowRect(&rcBtn); pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_TOPALIGN, rcBtn.left, rcBtn.bottom, this); }要点:GetWindowRect 返回屏幕坐标,TrackPopupMenu 需要的也是屏幕坐标,所以两者直接匹配,不需要 ScreenToClient。如果你用 ClientToScreen 再转一次,菜单位置会偏出去一倍。按钮下方的摆放一般取 rcBtn.left(菜单左边缘和按钮左对齐)和 rcBtn.bottom(按钮底边)。
这个技巧的价值在于,按钮本身还能继续用 SetBitmaps 表达“下拉”语义,比如位图右侧画一个小箭头,hover 时换白底箭头,交互反馈比静态按钮好。如果你需要“点击按钮主体执行默认动作、点击右侧箭头展开菜单”,那就拆成两个控件或者自己拦截 WM_LBUTTONUP 判断坐标,CButtonST 本身不提供拆分区域,但拦截消息在 MFC 里依然是通用的那套。
5.2 多状态切换按钮:把 Radio 的选中态做进位图里
CButtonST 内置状态只有 Normal、Hover、Pressed,没有“Checked”选中态。做工具栏式切换按钮,常见办法是自维护一个 m_bChecked,并在点击时手动切状态。
我一般会在 CButtonST 子类里加一个方法:
void CMyButtonST::SetChecked(BOOL bChecked) { m_bChecked = bChecked; Invalidate(FALSE); }同时在点击处理里反转状态:
void CMyDialog::OnBnClickedToggleMode() { m_bChecked = !m_bChecked; m_pToggle->SetChecked(m_bChecked); // 这里切换业务逻辑 }绘制端怎么切图?先在 SetBitmaps 阶段准备好四张位图:普通、悬停、按下、选中。选中可以复用按下图,也可以单独做一张高亮图。在 OnDrawItem 重写里,判断 m_bChecked 为 TRUE 时,跳过系统的三状态分支,直接贴选中图;为 FALSE 时走原来的分支。这样按钮外观是连续的,逻辑上仍然是一个普通的 push button,不用把对话框控件改成 radio 或 checkbox。
边界注意:做了自绘选中态之后,按钮的键盘交互和辅助功能状态不会自动跟随,需要在 DrawItem 里把选中状态同步到视觉状态。这不是 CButtonST 特有的问题,是所有自绘开关按钮的通病。项目里没有键盘走查要求可以暂时忽略,但代码里最好保留注释,防后人踩坑。
5.3 自绘按钮的资源生命周期管理
自绘按钮的资源管理最容易翻车,因为窗口销毁和 C++ 对象析构是两套生命周期。CButtonST 内部会管理它加载进来的位图,这个没问题;坑的是使用方自己又存了一份句柄。比如某个对话框初始化时,为了判断位图是否存在,用 CBitmap::LoadBitmap 加载了一次,然后把 HBITMAP 赋给 CButtonST。结果两个对象都认为自己拥有这个 GDI 对象,析构时先后调用 DeleteObject,第二次删除就会断言。
我给自己定的规矩是:所有位图资源只允许 CButtonST 内部加载一次,外部不要重复持有句柄。如果要统一制作一组按钮,可以做一个管理结构,存放资源 ID 而不是 HBITMAP:
struct ButtonRes { UINT nNormal; // 常态位图 ID UINT nHover; // 悬停位图 ID UINT nPressed; // 按下位图 ID UINT nDisabled; // 禁用位图 ID };CButtonST 在需要时再去 LoadBitmap,避免句柄共享带来的双重释放。动态创建的按钮统一在对话框 OnDestroy 里 delete,不要依赖 MFC 自动清理。这条习惯在 VS2012 上特别重要,因为它的调试运行时对重复删除 GDI 对象会让断言直接中断,而发布版里表现为偶发崩溃,很难定位。
6. 用 GDI 对象计数验证修正版没有资源泄漏
自绘控件最怕的不是编译期报错,而是运行期偷偷泄漏 GDI 对象。CButtonST 修正版在你手上编译过了,不代表它没留下资源管理隐患。我每拿到一个版本,都会先做一次 GDI 对象计数体检。
做法很简单。对话框的 OnInitDialog 里取一次进程 GDI 对象数,OnDestroy 里再取一次,正常情况下差值稳定在预期范围。反复开关对话框 20 次,如果每次打开关闭都多出几个对象,说明某处创建了 GDI 对象但没释放。
#include <winuser.h> DWORD GetProcessGdiCount() { return ::GetGuiResources(::GetCurrentProcess(), GR_GDIOBJECTS); }在 OnInitDialog 结尾和 OnDestroy 开头各取一次,差值输出到 Debug 输出窗口。用任务管理器里的“GDI 对象”列做辅助观察也可以,但进程级计数器更直接。验证流程通常配合命令行循环:
for ($i = 0; $i -lt 20; $i++) { Start-Process .\Debug\MyApp.exe Start-Sleep -Milliseconds 800 Stop-Process -Name MyApp }如果曲线平稳,说明 CButtonST 内部位图资源和悬停状态都正常;如果每次增加 3 到 5 个,重点检查 OnDrawItem 里有没有自己创建画笔、画刷、字体而没有 DeleteObject。除了 GDI 计数,还要做一次 x64 回归:把平台切到 x64,重复上面的循环。这一步能兜住句柄截断问题。
我拿到手的一个版本,在 Win32 下跑了一整天没异常,切到 x64 后第一次点击就崩,就是 DWORD 存句柄的经典翻车。从那以后,凡是沾自绘控件的升级,GDI 计数加 x64 回归都是我固定保留的两个检查动作。后来另一个项目里我把一个含十几个自绘按钮的界面反复开关 200 次,靠这个办法定位到 OnDrawItem 里忘记删除临时画刷的泄漏点。希望帮到你。
本文还有配套的精品资源,点击获取