拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CButtonST:MFC自绘按钮增强库的完整解析与实战避坑

简介CButtonST是MFC框架下常用的增强型按钮控件类压缩包内提供完整源码与示例工程面向需要丰富界面表现、实现图标按钮与多状态反馈的Windows开发者可替代标准CButton使用。压缩包共一百二十个文件大小约2.28MB核心由23个h头文件和22个cpp源文件组成另有35个ico图标与8个bmp位图用作按钮素材还带有dsp、dsw、rc等工程配置文件和可运行的Demo程序便于直接编译观察效果。目前已有171人学习浏览。通过这份资源开发者可以学习将CButton替换为CButtonST、设置按钮颜色边框、加载图标、配置热键与文本对齐方式以及处理扩展点击事件等关键用法CButtonST_Demo示例中的Sky、Face等位图与图标组合能直观展示不同外观效果适合中高级MFC界面开发者参考改进。 说起MFC界面开发我估计不少老工程师和我一样脑子里会立刻出现CButtonST这个名字。Paolo Messina在90年代末放出的这个开源按钮类几乎成了VC6时代“好看按钮”的代名词。那个年代用标准CButton做按钮体验确实很痛苦只能显示文字想加个图标得自己处理BM_SETIMAGE想做一个悬停变色的工具栏按钮更是得自己折腾WM_DRAWITEM自绘。我当时在一个播放器项目里连做五个控制按钮光自绘代码就写了一千多行还弄得各种状态错乱。后来换成CButtonST几行代码搞定整个人都清爽了。CButtonST基于CButton扩展把按钮的图标、位图背景、悬停状态、按下状态、禁用状态、工具提示这些能力全部封装成了可调用的成员函数。它解决的是MFC标准按钮在视觉表现力上的不足同时把自绘按钮的复杂度收拢在一个类里。对于还在维护MFC老项目的团队、刚学Win32/MFC自绘原理的开发者以及想从经典源码里吸收状态机和绘制逻辑的人来说这个类都有很高的参考价值。1. 标准CButton的视觉困境与CButtonST的破局1.1 系统按钮在表现力上的天然短板MFC的标准CButton本质上是Win32按钮控件的薄封装系统内置的按钮绘制逻辑非常朴素画一个矩形边框、铺一层灰色底色、放上文字再根据焦点和按下状态切换几种预置外观。想在按钮上放一张图要么用BM_SETIMAGE挂一个位图或图标要么自己接管绘制。但BM_SETIMAGE只解决“有没有图”的问题图标怎么对齐、透明区域怎么处理、悬停时要不要换图它一概不关心。更麻烦的是禁用状态。标准按钮禁用时系统会自动把文字变灰但带图标的按钮如果图标颜色偏深禁用后依然看得清清楚楚视觉上非常别扭。你需要自己捕获WM_ENABLE消息换成一套灰色图标。这个逻辑如果写在主对话框的消息处理里一旦按钮多起来对话框类就会膨胀得不像话。还有悬停反馈。系统按钮在鼠标悬停时几乎没有任何响应除非整个桌面开启了视觉样式否则按钮就是一块呆板的灰色方块。而多媒体播放器、工具箱面板上那种“鼠标一移上去就变色”的按钮在当时只有两条路自己写自绘或者用第三方类库。CButtonST就是在这样的背景下被大量项目选中的方案。1.2 一个类把复杂度全部收口CButtonST的定位非常明确在延续CButton原有使用习惯的前提下把常用的按钮增强能力做成成员函数。开发者不需要理解自绘细节不需要关心消息路由只要把一个按钮控件和CButtonST的实例绑定再调几个Set方法效果就出来了。它对代码侵入性也很小。原有对话框代码不用伤筋动骨把成员变量的类型从CButton换成CButtonST在OnInitDialog里加上初始化调用就完成了接入。这种“小成本、大效果”的体验在今天的组件生态里可能很常见但在那个年代是非常难得的设计。也正因为它足够简单才会被收录进无数代码库成为MFC项目里最常见的第三方控件类之一。2. 核心能力拆解图标、位图与状态切换CButtonST最有价值的地方是把按钮的状态拆分成了普通、悬停、按下、禁用四种并针对不同状态分别处理。这个“状态驱动绘制”的模型后来我在很多现代UI框架里都见到了相似的设计。2.1 四态图标按钮一次SetIcon切换全部状态用CButtonST做图标按钮最简单的调用是这样m_btnPlay.SetIcon(IDI_ICON_PLAY);就这一行按钮就会在普通状态下显示IDI_ICON_PLAY这个图标。如果还想区分鼠标悬停、按下和禁用时的图标可以用带状态标志的重载m_btnPlay.SetIcon(IDI_ICON_PLAY, CButtonST::ST_ICON_NORMAL); m_btnPlay.SetIcon(IDI_ICON_PLAY_HOT, CButtonST::ST_ICON_HOT); m_btnPlay.SetIcon(IDI_ICON_PLAY_PRESSED, CButtonST::ST_ICON_PRESSED); m_btnPlay.SetIcon(IDI_ICON_PLAY_DISABLED, CButtonST::ST_ICON_DISABLED);内部实现会把四个图标句柄分别保存在DrawItem里根据当前按钮状态选一个绘制。图标的对齐方式、绘制模式也提供了接口可以控制图标放在按钮左侧、居中还是右侧。对于工具栏按钮和媒体控制按钮这套方案已经足够完整。我通常会把普通态和悬停态做成同一张图的不同配色按下态做成压暗版本禁用态直接换灰色系图标视觉层次一下就出来了。2.2 位图背景与透明绘制除了图标CButtonST还支持为按钮普通状态和按下状态分别设置位图背景。这个功能常用于做不规则形状的按钮比如圆形播放键、圆角导航条。m_btnPlay.SetBitmaps(IDB_BTN_PLAY_UP, IDB_BTN_PLAY_DOWN);按下时切换到位图松开时切回反馈非常直接。如果项目里用的是带Alpha通道的32位位图按钮的透明区域能很好地和对话框背景融合。透明绘制是另一个高频能力调用DrawTransparent(TRUE)之后按钮在普通态不再绘制系统默认的灰色背景而是直接沿用对话框的背景色看起来像是按钮“长”在了面板上。这个能力对深色背景的对话框尤其重要否则一块灰底在界面上会特别突兀。2.3 扁平风格与气泡提示CButtonST里的SetFlat(TRUE)可以做出类似网页链接的扁平按钮没有边框鼠标移上去才出现细边框和变色效果。听起来很普通但在当时这是让工具栏看起来不那么笨重的关键手段。气泡提示则由SetTooltipText一行搞定内部自动创建CToolTipCtrl并绑定到按钮窗口上把悬停文本显示出来。这个方法太常用了我几乎在每一个用到CButtonST的项目里都会调用。需要注意工具提示默认延迟和系统一致如果嫌提示出现太慢可以用SetTooltipDelay调整延迟时间这个参数决定鼠标停在按钮上多久后才弹出提示对体验影响很大。3. 手把手集成CButtonST到MFC项目下面以Visual Studio加MFC对话框工程为例讲一下从引入源码到按钮正常显示增强效果的完整过程。这里只说实际项目里最常用的路径踩过的坑放在后面单独讲。3.1 源码准备与工程引入CButtonST是开放源码的网上能找到包含CButtonST.h和CButtonST.cpp的压缩包。把这两个文件拷贝到项目源码目录然后在解决方案资源管理器里添加现有项将文件加进项目。记得确认工程的字符集设置老版本代码大量使用TCHAR宏并混用LPSTR如果工程设置为Unicode编译时可能会报类型不匹配。把字符集设为“使用Unicode字符集”一般没问题个别转换报错手动加一下强制转换就能解决。另外由于原版代码写于VC6时代在VS2010以上的编译环境里会出现不少C4996类警告比如关于std::string和指针转换的提示。我的建议是把项目警告级别调成4但不要设置为“警告视为错误”先让编译通过再逐个清理真正的风险告警。这样既保留老代码可用性也不至于让新接手的人被一堆warn吓住。3.2 按钮控件与CButtonST实例的绑定方式第一种在对话框类里把成员变量声明为CButtonST然后通过DDX_Control绑定void CMainDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_BTN_PLAY, m_btnPlay); }第二种在OnInitDialog里手动调用SubclassDlgItemm_btnPlay.SubclassDlgItem(IDC_BTN_PLAY, this);第三种动态创建按钮m_btnPlay.Create(_T(), WS_CHILD | WS_VISIBLE, CRect(10, 10, 50, 30), this, IDC_BTN_PLAY);无论采用哪种方式CButtonST都会在PreSubclassWindow中自动补上BS_OWNERDRAW样式。这一点非常关键——自绘按钮的起点就是BS_OWNERDRAW它告诉Windows这个按钮的所有绘制都由程序自己负责。3.3 常用API速查整理一份我常用的API清单方便按需调用功能调用方式设置普通态图标SetIcon(IDI_ICON_XXX)设置四态图标SetIcon(IDI_XXX, CButtonST::ST_ICON_HOT)设置位图背景SetBitmaps(IDB_UP, IDB_DOWN)开启透明背景DrawTransparent(TRUE)扁平风格SetFlat(TRUE)气泡提示SetTooltipText(_T(提示文本))提示延迟SetTooltipDelay(500)文字颜色SetColor(CButtonST::BTNST_COLOR_FG, RGB(255,255,255))渐变背景SetGradientColor(RGB(70,130,180), RGB(30,60,120))SetGradientColor是后期版本加入的渐变背景能力做媒体播放器音量条、进度按钮时很好用。需要说明SetColor里的BTNST_COLOR_FG指文字前景色直接传RGB颜色值即可如果想改按钮底色用BTNST_COLOR_BK。拿到类之后先看一眼头文件里的枚举再开始写代码能少走很多弯路。4. 源码级原理自绘按钮背后发生了什么用了半天CButtonST如果不理解它的自绘逻辑遇到绘制异常时基本无从下手。这段把关键机制拆开讲。4.1 BS_OWNERDRAW与DrawItem的调用链当按钮带上BS_OWNERDRAW样式时Windows不再负责按钮的显示而是在需要绘制时向父窗口发送WM_DRAWITEM消息。MFC对这条消息做了反射处理会把它路由回按钮自身的DrawItem虚函数这就是CButtonST真正绘制的地方。CButtonST在DrawItem里拿到DRAWITEMSTRUCT结构体其中包含了按钮矩形区域、当前状态位ODS_SELECTED表示按下、ODS_DISABLED表示禁用、ODS_FOCUS表示获得焦点以及控件ID。类内部根据这些状态位选择一个绘制分支先画图标或位图再填充背景色然后画文字和焦点框。所有状态切换的处理逻辑集中在同一个函数里所以才能做到状态与表现一一对应、不重不漏。每次状态变化导致的重绘本质上就是重新进入同一个绘制函数读一遍状态位然后画出对应外观。4.2 鼠标状态是怎么被跟踪的为了让按钮在悬停和按下时切换外观CButtonST重载了OnMouseMove、OnMouseLeave、OnLButtonDown等消息处理函数。鼠标进入按钮时内部把hover状态置为TRUE然后调用Invalidate触发重绘鼠标离开时置为FALSE同样触发重绘。所有鼠标进入和离开的反馈都是由这些消息处理驱动的。这里有个容易忽略的细节Windows不会主动通知控件“鼠标已经离开”控件必须调用TrackMouseEvent来注册跟踪离开时才会收到WM_MOUSELEAVE。CButtonST对“是否一直跟踪”有一个开关如果SetAlwaysTrack传FALSE则鼠标在按钮内部移动时不会频繁刷新只有进入和真正移出时才更新如果传TRUE则每次状态变化都会立刻重绘。早期版本在鼠标快速移过按钮时偶尔会出现状态残留改进了TrackMouseEvent的调用时机之后才解决。4.3 消息反射机制与控件自包含设计用MFC写过自定义控件的人会知道MFC控件有消息反射机制父窗口收到WM_DRAWITEM、WM_CTLCOLOR等消息后会通过ReflectLastMsg把消息反射回控件自身处理。CButtonST正是在这个机制下工作的。因为这个封装CButtonST既能作为对话框子控件运行也能在FormView或属性页里运行兼容性相当好。这个设计给后来写自绘控件的人一个很好的示范不要把绘制逻辑都堆在父窗口里而应该让控件自己响应自己的绘制消息。绘制、状态、资源管理都在控件内部闭环外部只暴露简单的接口。代码的复用性和隔离性都会好很多。读CButtonST源码最大的收获其实就在这里。5. 踩坑实录那些文档里不会写的事CButtonST用起来简单但深入项目后容易踩到一些隐蔽的问题。我把实际遇到过的坑都列出来如果你正在用它建议提前避雷。5.1 GDI句柄泄漏图标资源必须自己管理SetIcon有两个重载一个传资源ID一个传HICON。传资源ID时CButtonST内部通过AfxGetApp()-LoadIcon加载的是程序共享图标这类资源由框架统一管理不需要也没办法单独释放。但如果你用LoadImage加载一个独立的图标句柄再调用SetIcon(HICON)这个句柄必须由你自己负责释放。我之前在一个皮肤切换功能里每次切换皮肤都重新加载一套图标旧句柄没有释放任务管理器里GDI对象数一路涨到系统极限按钮直接画不出来。排查了半天根因就是图标句柄泄漏。建议养成习惯如果SetIcon传的是HICON在按钮退出或重建前调用DestroyIcon释放或者利用CButtonST提供的句柄所有权机制把句柄交出去让析构时统一销毁。别以为对话框关闭后就万事大吉短生命周期的对话框在运行期反复创建销毁泄漏一样会累积到系统崩溃。5.2 透明背景在主题样式下变成灰色块DrawTransparent(TRUE)在Windows XP之前很好使但当系统启用视觉样式之后按钮的绘制背景会优先使用系统主题的按钮背景色导致透明效果失效出现一块灰底或蓝色渐变底非常难看。解决办法是在按钮初始化时调用SetWindowTheme把视觉样式关掉#include uxtheme.h #pragma comment(lib, uxtheme.lib) SetWindowTheme(m_btnPlay.GetSafeHwnd(), L, L);关掉主题后按钮所有绘制都走自绘逻辑透明背景才能正确计算。我一般在DrawTransparent之后调用SetWindowTheme实测下来背景最干净。现在维护MFC老项目的工程师遇到类似问题第一反应应该都是这个开关。如果忘了加最容易看到的现象就是设计器里预览一切正常编译运行后按钮多出一块系统主题色。5.3 高DPI下图标和按钮尺寸明显偏小VC6时代的控件尺寸大多按96 DPI设计CButtonST本身也不做DPI缩放。在高分屏上图标会显得很小按钮点击热区也偏小用户体验很差。严格说这不是CButtonST的bug而是所有老代码在新时代的共同挑战。我的做法是在OnInitDialog里根据GetDpiForWindow或GetDeviceCaps计算缩放系数手动调整按钮矩形和图标大小。如果图标是用资源ID加载的可以用LoadImage传入合适的cx和cy参数让图标按目标尺寸加载。千万不要在SetIcon之后直接用SetWindowPos拉伸按钮那样图标位置会错位视觉上很糟糕。正确顺序是先算好按钮尺寸再设置图标最后根据按钮矩形调整图标对齐。5.4 与DDX绑定的初始化时序问题很多项目用DDX_Control绑定CButtonST后在OnInitDialog里调用SetIcon一切正常。但如果你把初始化写在OnCreate里或者在没有完成子本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门