简介面向微软视窗平台使用C语言的开发者这套增强型微软基础类库按钮组件以动态按钮实现为核心弥补了标准按钮控件在文字实时更新、图片随状态切换、自定义绘制样式以及多状态反馈等方面的不足。压缩包非常轻量仅包含两个文件一个是头文件用于声明类接口和样式宏另一个是源文件封装了按钮绘制和消息处理细节整个资源大小约十七KB可直接添加到工程使用学习成本极低。目前已有123人浏览学习。这个按钮类支持文字动态更新、图像替换、圆角渐变背景、多状态明暗反馈以及扩展交互回调同时针对多语言应用做了文本适配并对绘制流程进行性能优化。开发者只需继承或直接实例化该按钮类即可快速获得高颜值、响应及时的交互控件减少大量自绘按钮的模板代码把精力集中在业务逻辑上是增强应用界面友好度和开发效率的实用工具。1. 方便好用的MFC按钮类从“能点就行”到“拿过来就能用”做上位机工具或者维护老式桌面程序时对话框里总少不了一排按钮启动、停止、复位、参数保存每个都要带图标、带状态色按下要有“陷下去”的手感鼠标悬停还要有点反馈。原生CButton只能做到“能点就行”图标和文字不能同显按下状态是系统画死的换肤时更是一块灰扑扑的补丁。搜“方便好用的MFC按钮类”拿到的答案大多只有零散代码没有封装思路改个圆角半径都要翻半天。这篇笔记要做的就是把这个“方便好用”拆开它到底封装了哪些能力、自绘函数怎么写到能直接复用、参数怎么设、最常见的坑在哪。适合正在维护 MFC 项目、做设备上位机、或想给老界面换一套统一视觉的开发者直接参考落地。2. 为什么自绘按钮类比原生 CButton 好用封装思路与选型2.1 原生按钮的三大局限图标文字不能同显、四态不全、视觉难统一原生CButton在对话框里最常用的是BS_PUSHBUTTON风格。文字显示没问题但想放图标就得用BS_ICON或BS_BITMAP而这两种风格一旦贴上图文字要么不显示要么连文字带图标挤在一起间距完全不可控这是第一个局限。第二个局限是状态反馈不完整。系统画的按钮在按下时有一个“沉下去”的效果但鼠标悬停、获得焦点、禁用这三个状态没有明显差异化尤其在 Windows 经典主题下禁用态和普通态只差一点灰度用户根本不知道当前能不能点。对于设备操作界面来说这种反馈缺失很容易引发误操作。第三个局限在视觉统一上。老 MFC 程序经常用自绘对话框做深色背景原生CButton用的是系统按钮颜色深浅不搭边框和圆角也改不了。想让它和旁边的编辑框、下拉框风格一致只能走自绘这条路。把这三个局限合起来看一个“方便好用”的按钮类本质就是把原生控件缺的能力补齐同时对外接口做到改参数就行、不用碰绘制细节。2.2 自绘路线怎么选BS_OWNERDRAW 与拦截 WM_PAINT 的取舍MFC 里自绘按钮有两条常见路线。一条是给按钮设置BS_OWNERDRAW风格让系统在需要绘制时发送WM_DRAWITEM消息MFC 的CButton收到后调用虚函数DrawItem你在子类里重写这个函数就行。这是最正统、消息路径最清晰的方案代码量也最少。另一条是重写PreSubclassWindow在里面自己拦截WM_PAINT、WM_ERASEBKGND完全手工绘制。这条路能做到的事情更多比如绘制异形按钮、响应不规则点击区域但代码量和维护成本明显增加要处理的消息也比DrawItem多一些对“方便好用”这个定位来说属于杀鸡用牛刀。我一般会选择第一条路线理由是状态分发不用自己操心。DrawItem收到的LPDRAWITEMSTRUCT里已经带了itemState按下、禁用、聚焦、悬停都分别有对应的标志位不用自己记鼠标状态再重绘系统会帮你算好什么时候触发绘制。第二条路线适合按钮数量少、形状特殊的场景比如圆形电源键、仪表盘上的异形按键。下面的实现也以第一条路线为主它的核心代码更集中容易让新手跟着做通。2.3 一个“方便好用”的按钮类必须包含的五个能力给“方便好用”定个可验收的标准我认为至少要有五个能力第一图标文字同显且图标大小、文字间距可调。这解决的是资源按钮最常见的使用场景工具条按钮、状态操作按钮都要这个能力。第二四态完整普通、悬停、按下、禁用四套颜色独立可控悬停色和按压缩放由代码自动派生也可以但至少要留出强制指定的接口。第三圆角和边框可设。圆角半径一个参数边框颜色一个参数内部用绘制路径解决裁剪外部使用者不用关心区域怎么算。第四键盘焦点我能看见。自绘按钮最容易把焦点框画丢键盘用户按 Tab 过来之后完全不知道自己选中了哪个按钮所以焦点虚线框必须保留。第五双击不误触发。设备界面上“复位”按钮如果被双击触发两次后果可能是严重的安全问题。这个能力来自对WM_LBUTTONDBLCLK的处理虽然不是纯绘制功能但作为按钮类的一部分很必要。有了这五个能力这个类放在任何老 MFC 项目里都能直接替换原有按钮而不破坏现有逻辑。3. 手写一个可复用的 MFC 自绘按钮类类骨架与绘制流程3.1 类声明与状态管理成员变量一多绘制逻辑就清晰了先写类声明。这个类继承CButton因为DrawItem是CButton的虚函数子类重写后系统会自动调用消息映射里只需要处理鼠标悬停和按下抬起这几个非绘制消息整体代码结构比较干净。头文件里的成员变量对应第 2 章定义的五种能力图标句柄、三种状态的颜色、圆角半径、间距。// MyButton.h - 自绘按钮类声明 #pragma once #include afxwin.h class CMyButton : public CButton { public: CMyButton() {} virtual ~CMyButton() {} // 对外接口调用方只需关心这几个方法 void SetIcon(HICON hIcon, int nIconSize 16); // 设置图标默认16x16 void SetTextColor(COLORREF clrNormal, // 文字颜色 COLORREF clrHover 0, // 悬停色0表示自动派生 COLORREF clrPressed 0); // 按下色0表示自动派生 void SetBackColor(COLORREF clrBg, COLORREF clrBgHover 0, COLORREF clrBgPressed 0); void SetRoundRect(int nRadius 3); // 圆角半径0为直角 void SetContentSpacing(int nSpacing 4); // 图标与文字间距 protected: virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); // 系统回调的绘制函数 afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnMouseLeave(); afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); DECLARE_MESSAGE_MAP() private: HICON m_hIcon NULL; // 图标句柄 int m_nIconSize 16; // 图标渲染尺寸 int m_nRadius 3; // 圆角半径 int m_nSpacing 4; // 图标与文字间距 COLORREF m_clrText RGB(51, 51, 51); COLORREF m_clrTextHover RGB(0, 0, 0); COLORREF m_clrTextPressed RGB(0, 0, 0); COLORREF m_clrBg RGB(240, 240, 240); COLORREF m_clrBgHover RGB(220, 230, 250); COLORREF m_clrBgPressed RGB(200, 210, 230); BOOL m_bHover FALSE; // 当前是否悬停 };成员变量的设计遵循一个原则绘制函数只读状态、不维护状态。m_bHover是鼠标消息处理函数维护的DrawItem里只根据它和lpDrawItemStruct-itemState决定画哪套颜色。这样做的目的是让消息处理和绘制逻辑解耦改颜色时只需要动SetBackColor函数不会牵扯到DrawItem内部的分支判断。3.2 DrawItem 绘画函数背景、边框、图标、文字一条龙DrawItem是这个类的核心所在。它做四件事保存 DC 状态、根据 itemState 选择颜色、绘制带圆角的背景和边框、绘制图标和文字。这里有一个经常被忽略的细节lpDIS-rcItem是整个按钮的客户区但实际可点击区域比它略小绘制圆角路径时需要把矩形缩小一个像素避免边框溢出后产生锯齿边。// MyButton.cpp - DrawItem 核心绘制实现 void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDIS) { CDC* pDC CDC::FromHandle(lpDIS-hDC); if (!pDC) return; // 保存DC状态绘制结束后恢复避免影响调用方的GDI对象 int nSavedDC pDC-SaveDC(); CRect rc lpDIS-rcItem; UINT state lpDIS-itemState; // 按当前状态选择颜色优先取itemState系统标志其次取鼠标悬停标志 COLORREF clrText m_clrText; COLORREF clrBg m_clrBg; if (state ODS_DISABLED) { clrText RGB(160, 160, 160); clrBg RGB(235, 235, 235); } else if (state ODS_SELECTED) { clrText m_clrTextPressed; clrBg m_clrBgPressed; } else if (m_bHover || (state ODS_HOTLIGHT)) { clrText m_clrTextHover; clrBg m_clrBgHover; } // 用内存DC做双缓冲避免按钮重绘时闪烁 CDC memDC; memDC.CreateCompatibleDC(pDC); CBitmap bmp; bmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height()); CBitmap* pOldBmp memDC.SelectObject(bmp); // 绘制带圆角的背景区域 CRect rcDraw rc; rcDraw.DeflateRect(1, 1); // 缩小1像素防止边框溢出按钮边界 CBrush bgBrush(clrBg); if (m_nRadius 0) { HRGN hRgn CreateRoundRectRgn(rcDraw.left, rcDraw.top, rcDraw.right 1, rcDraw.bottom 1, m_nRadius, m_nRadius); memDC.FillRgn(hRgn, bgBrush); DeleteObject(hRgn); } else { memDC.FillRect(rcDraw, bgBrush); } // 绘制边框 CPen borderPen(PS_SOLID, 1, RGB(180, 180, 180)); CPen* pOldPen memDC.SelectObject(borderPen); HBRUSH hOldBrush (HBRUSH)SelectObject(memDC, GetStockObject(NULL_BRUSH)); if (m_nRadius 0) { memDC.RoundRect(rcDraw, CPoint(m_nRadius, m_nRadius)); } else { memDC.Rectangle(rcDraw); } memDC.SelectObject(pOldPen); SelectObject(memDC, hOldBrush); // 设置文字背景透明 memDC.SetBkMode(TRANSPARENT); memDC.SetTextColor(clrText); // 计算图标和文字的绘制位置 int nTextWidth 0; if (!GetWindowText().IsEmpty()) { CSize szText memDC.GetTextExtent(GetWindowText()); nTextWidth szText.cx; } // 总内容宽度 图标 间距 文字按这个宽度居中偏移 int nTotalWidth (m_hIcon ? m_nIconSize m_nSpacing : 0) nTextWidth; int nLeft rc.left (rc.Width() - nTotalWidth) / 2; int nCenterY rc.top rc.Height() / 2; // 绘制图标 if (m_hIcon) { int nIconTop nCenterY - m_nIconSize / 2; memDC.DrawState(CPoint(nLeft, nIconTop), CSize(m_nIconSize, m_nIconSize), m_hIcon, DSS_NORMAL, (HBRUSH)NULL); nLeft m_nIconSize m_nSpacing; } // 绘制文字 if (!GetWindowText().IsEmpty()) { CRect rcText rc; rcText.left nLeft; memDC.DrawText(GetWindowText(), rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } // 焦点状态绘制虚线框键盘用户依赖这个才能看到焦点位置 if (state ODS_FOCUS) { CRect rcFocus rc; rcFocus.DeflateRect(3, 3); memDC.DrawFocusRect(rcFocus); } // 将内存DC内容整体拷贝到窗口DC一次完成无闪烁 pDC-BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); // 恢复DC状态与GDI对象 memDC.SelectObject(pOldBmp); pDC-RestoreDC(nSavedDC); }代码里最关键的是CreateRoundRectRgn和memDC.FillRgn这一对组合。FillRgn能保证背景只在圆角区域内生效区域外保持原样这样不会出现“圆角外有一圈杂色”的常见问题。DrawState函数比DrawIcon更适合这里因为它可以按指定的目标尺寸绘制m_nIconSize改了图标就跟着缩放。SetBkMode(TRANSPARENT)是文字绘制的基础不做这一步文字背后会有一个矩形白底在深色按钮上特别难看。3.3 鼠标悬停与按下抬起用 _TrackMouseEvent 补足交互状态DrawItem只能拿到绘制时的状态但m_bHover这个标志位需要消息处理代码来维护。关键函数是PreSubclassWindow在这里设置BS_OWNERDRAW风格让按钮进入自绘模式。按钮从对话框模板创建出来后PreSubclassWindow的时机正好能改风格。// MyButton.cpp - 风格设置与鼠标状态维护 void CMyButton::PreSubclassWindow() { CButton::PreSubclassWindow(); // 设置自绘风格DrawItem 才会被系统调用 ModifyStyle(0, BS_OWNERDRAW); } void CMyButton::OnMouseMove(UINT nFlags, CPoint point) { // 鼠标进入按钮区域则标记悬停状态并启动跟踪 if (!m_bHover) { m_bHover TRUE; Invalidate(FALSE); // 触发一次重绘让悬停颜色生效 TRACKMOUSEEVENT tme { 0 }; tme.cbSize sizeof(tme); tme.dwFlags TME_LEAVE; // 只需要离开通知 tme.hwndTrack m_hWnd; _TrackMouseEvent(tme); } CButton::OnMouseMove(nFlags, point); } void CMyButton::OnMouseLeave() { m_bHover FALSE; Invalidate(FALSE); // 离开后恢复普通态颜色 CButton::OnMouseLeave(); } void CMyButton::OnLButtonDown(UINT nFlags, CPoint point) { // 按下时立即重绘一次用户能马上看到按下反馈 Invalidate(FALSE); CButton::OnLButtonDown(nFlags, point); } void CMyButton::OnLButtonUp(UINT nFlags, CPoint point) { // 抬起时如果鼠标还在按钮上保持悬停态否则恢复普通态 CRect rc; GetClientRect(rc); if (rc.PtInRect(point)) { m_bHover TRUE; } Invalidate(FALSE); CButton::OnLButtonUp(nFlags, point); }_TrackMouseEvent是这里的关键点。如果不调用它鼠标移出窗口后OnMouseLeave永远不会触发按钮就会一直停留在悬停高亮状态这是自绘按钮最常见的交互 bug。Invalidate(FALSE)的FALSE参数意思是只重绘客户区、不擦除背景在自绘按钮场景下既能更新颜色又不会闪屏。按下抬起逻辑里我没有调用SetCapture因为普通按钮在按下后系统已经自动捕获了鼠标松开时也会收到消息只有需要处理“按下去后拖出再松开”的复杂场景才需要手动捕获。4. 参数设计与接入对话框怎么调才顺手4.1 对外接口这样设计使用者只需要关心四类参数“方便好用”的关键在调用方视角。我把接口收敛成四类图标、颜色、形状、间距。SetIcon传入HICON和尺寸颜色接口是两组COLORREFSetRoundRect只收一个半径SetContentSpacing收一个像素值。调用方不需要理解DrawItem的任何一个细节也不需要处理ODS_*标志位。颜色接口的默认参数0代表“自动派生”。例如只设置普通态背景色为深蓝悬停色留空类内部会自动把颜色往亮的方向偏移 10%。这个设计对实际使用很友好不用每次都写全套颜色大部分项目只需要两三个参数就能有统一的视觉。void CMyButton::SetBackColor(COLORREF clrBg, COLORREF clrBgHover, COLORREF clrBgPressed) { m_clrBg clrBg; // 传入0时按普通色自动派生悬停色和按下色 m_clrBgHover (clrBgHover 0) ? LightenColor(clrBg, 1.15f) : clrBgHover; m_clrBgPressed (clrBgPressed 0) ? LightenColor(clrBg, 0.85f) : clrBgPressed; }LightenColor是一个内部辅助函数把 RGB 三个分量分别乘系数并钳制到 0~255。为什么要用乘法而不是直接加减因为白色按钮加减会直接越界乘法后依然保持原有色相。外围颜色变暗 15%、变亮 15%对大多数场景已经足够。如果项目有严格的视觉规范直接用非零参数强行指定即可。4.2 图标尺寸、圆角半径、颜色亮度的默认值参考默认参数是从实际项目数据里归纳出来的不是拍脑袋定值。我整理了一张表接入时可以直接照抄。参数默认值适合场景备注图标尺寸16x16工具栏、表格内嵌按钮高DPI下建议按DPI缩放图标与文字间距4像素大多数按钮文字较长时可加到6圆角半径3像素经典风格、容器内部主操作按钮可用6~8普通态文字颜色RGB(51,51,51)浅色背景按钮深色背景下改成白色悬停亮度×1.15通用浅色按钮更明显按下亮度×0.85通用模拟“陷下去”手感边框颜色RGB(180,180,180)默认禁用态建议单独处理这里有一个使用习惯值得说明圆角半径设为 3 是为了和大多数 MFC 编辑框、列表控件的默认圆角保持一致。如果某个界面只要主按钮用大圆角渐变风格其他按钮保持方形也是分实例设不同值就行类内部不共享任何静态状态。高 DPI 场景我建议图标尺寸传入时要看GetDeviceCaps(LOGPIXELSX)的结果不要写死 16。4.3 用 DDX 绑定还是动态 Create两种接入方式怎么选MFC 对话框接入自绘按钮有两种常见姿势按项目形态选即可。第一种是对话框资源里先放好普通按钮然后在DoDataExchange里用DDX_Control绑定成员变量之后调用接口设置样式。这种方式适合界面布局固定的老项目改动量最小。void CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_BTN_START, m_btnStart); } BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); m_btnStart.SetIcon(m_hIconStart, 20); m_btnStart.SetBackColor(RGB(30, 120, 210)); m_btnStart.SetRoundRect(6); return TRUE; }第二种是动态创建适合按钮数量不固定、布局按数据生成的场景。先new出实例然后调用Create指定位置和风格最后手动SetWindowText。注意动态创建时按钮的资源 ID 要保证是唯一的否则点击事件的ON_BN_CLICKED会串到错误处理函数里。动态创建还需要在对话框析构时主动删除对象资源绑定方式则不需要。// 动态创建示例在面板上生成3个操作按钮 CMyButton* pBtn new CMyButton(); CString strText _T(保存); pBtn-Create(strText, WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, CRect(10, 10, 90, 36), this, IDC_BTN_SAVE_BASE i); pBtn-SetIcon(hIconSave, 16); pBtn-SetBackColor(RGB(240, 240, 240));两种方式都要注意BS_OWNERDRAW风格。资源绑定方式在PreSubclassWindow里统一处理了动态创建方式必须在Create的第二个参数里加上BS_OWNERDRAW否则按钮不会走自绘流程。5. 自绘按钮排坑指南5 个最容易翻车的地方5.1 图标不显示或花屏先查句柄、再查 DC 状态最后查尺寸现象很直白按钮上只有文字图标一直不出来或者图标像被撕裂了一样有黑块。查问题的顺序我一般固定是三步。第一步确认HICON本身有效很多情况是LoadIcon返回 NULL却没有做有效性判断。第二步确认DrawState的目标尺寸m_nIconSize如果传了 0绘制函数拿到的目标区域是空的图标自然不显示。第三步要查 DC 的文本背景色DrawState绘制图标时会受到SetBkColor的影响如果背景色和图标透明色不一致就会出现黑底方块。解决方式是在绘制图标前再单独执行一次SetBkColor(RGB(255,255,255))配合透明色保证边缘干净。5.2 Pressed 后出现一圈黑边区域裁剪与背景擦除的顺序按钮按下后背景变深在圆角边缘外却出现一圈黑色或暗边这是自绘按钮非常典型的翻车现场。原因有两个叠加DrawItem重绘时没有先擦除旧背景或者擦除时用的是系统画刷DeflateRect(1,1)没有生效导致圆角路径的像素和上一次重绘的边缘像素交叠。解决方式是在DrawItem开头先用双缓冲填满整个客户区再在圆角路径内画背景色。我在代码里用的memDC.FillRgn恰好能覆盖这种情况但要注意FillRgn的区域必须比控件边框小至少 1 像素否则边缘锯齿依然存在。检查时可以用全屏截图放大到 200% 看按钮边缘锯齿和黑边一目了然。5.3 双击按钮触发了两次单击用 GetDoubleClickTime 做区分设备界面里“复位”“删除”这类按钮双击触发两次会带来实际损失。原生CButton对单击和双击的区分依赖鼠标消息序列第一击的WM_LBUTTONDOWN后如果快速来了WM_LBUTTONDBLCLK系统其实是把第二击作为双击消息发的但按钮类的OnLButtonDown两次都会进入。解决方式是在处理OnLButtonDown时记录GetTickCount()配合GetDoubleClickTime()判断两次点击间隔如果小于系统双击时间阈值就丢弃第二次点击的触发逻辑。// 双击保护在 OnLButtonDown 中记录时间戳 DWORD dwLastClickTime 0; if (GetTickCount() - dwLastClickTime (DWORD)GetDoubleClickTime()) { return; // 判断为双击忽略本次按下事件 } dwLastClickTime GetTickCount();这个方案对快速操作有影响——用户连点两次启动第二个点击会被吞掉。实际上“吞掉第二次点击”正是预期行为双击在这里被定义为无意义操作业务层只处理单击所以只触发一次是最安全的。5.4 高 DPI 屏幕上文字和图标发虚字号跟点阵走位置跟像素走Windows 缩放 150% 时自绘按钮最容易出现图标偏小、文字边缘发虚的问题。根因是 MFC 的DrawText默认使用对话框的字号而对话框字体是点阵逻辑单位缩放后物理像素变大但图标尺寸还是写死的 16 像素。解决方式是字号跟随系统缩放图标跟像素同步放大。在OnInitDialog里做一次GetDeviceCaps(LOGPIXELSX) / 96.0的缩放因子计算把m_nIconSize乘一下再用CreatePointFont重建按钮字体。如果项目已经开启了进程级 DPI 感知还要注意CreateCompatibleBitmap创建的内存 DC 尺寸也要用当前 DPI 下的客户区像素值否则绘制区域和窗口实际尺寸不一致拷回窗口时只能拉伸画面必然发虚。判断是否踩了这个坑的方法是把窗口拖到第二块高缩放比的显示器上按钮如果明显比同高度文字模糊就说明绘制位图和窗口像素尺寸没对齐。5.5 换肤对话框把按钮底色涂成黑块从 WM_ERASEBKGND 根源解决对话框重绘时按钮背景经常先被系统默认的WM_ERASEBKGND擦成灰色或黑色再走DrawItem绘制但两者之间会有几毫秒的时间差人眼就会看到按钮区域闪黑块。解决方式分两层第一层在按钮类里重写WM_ERASEBKGND直接返回 TRUE告诉系统不用擦背景让自绘代码完全接管第二层在对话框的OnEraseBkgnd里对按钮区域不做擦除或者用RedrawWindow的RDW_UPDATENOW参数强制同步刷新。BOOL CMyButton::OnEraseBkgnd(CDC* pDC) { // 禁止系统擦除按钮背景全部交给 DrawItem 绘制防止闪烁 return TRUE; }这一步做完换肤时的黑块现象基本消失。优先级上要先解决WM_ERASEBKGND再考虑双缓冲很多闪屏问题其实根子都在这里双缓冲只是兜底方案。6. 把按钮类用到生产环境三个验证技巧与一个性能习惯6.1 用 SPY 验证窗口风格与消息路径接入后第一件事不是看视觉而是验证消息路径。打开 Visual Studio 自带的 SPY 工具找到按钮窗口确认窗口风格里包含BS_OWNERDRAW。如果风格没有生效检查你是在PreSubclassWindow里改的还是Create参数里加的两种方式漏掉任何一种都不会走自绘流程。再开启消息日志把鼠标移入按钮看有没有WM_MOUSEMOVE之后紧接着的WM_PRINTCLIENT或绘制消息再移出按钮看WM_MOUSELEAVE是否触发。这一步能确认 3.3 节的_TrackMouseEvent是否正常工作。6.2 键盘路径和热区边界自绘按钮最容易漏掉的验收项视觉验收完成后用 Tab 键走一遍焦点顺序。每按一次 Tab焦点按钮上应该出现虚线焦点框高亮色和悬停色不能一样否则键盘用户分不清自己在哪。再测试一次热区边界按钮四周 2 像素内的区域点击是否有效。很多自绘按钮为了做圆角把实际点击区域也裁剪小了导致用户点按钮边缘没反应。验收标准是点击矩形客户区的任何一点都应该触发BN_CLICKED圆角只是视觉裁剪不应影响热区。6.3 缓存 GDI 对象把重复创建的画笔画刷收成静态成员每次DrawItem都创建CBrush和CPen按钮多时会造成频繁的 GDI 对象分配释放有一定性能损耗。生产环境里我现在习惯把固定颜色对应的画刷、画笔做成静态成员初始化一次DrawItem里直接查颜色值拿对象。// 静态GDI对象缓存示例 static CMapCOLORREF, COLORREF, CBrush*, CBrush* s_mapBrush; static CBrush* GetBrush(COLORREF clr) { CBrush* pBrush NULL; if (!s_mapBrush.Lookup(clr, pBrush)) { pBrush new CBrush(clr); s_mapBrush.SetAt(clr, pBrush); } return pBrush; }缓存的边界参数是颜色种类一般每个按钮四态颜色用完后只有 4~8 种画刷缓存后DrawItem里省去了反复创建销毁的损耗逻辑也更干净。我记得之前有个项目操作界面上有四十多个按钮未加缓存时频繁切换页面会有肉眼可见的卡顿加上缓存后明显顺滑了很多。现在我做任何DrawItem类控件都会默认先查一下有没有重复创建 GDI 对象的写法。这个习惯帮我在后面几个项目里省了不少排查时间希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?