简介: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里手动调用SubclassDlgItem:
m_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里,或者在没有完成子
本文还有配套的精品资源,点击获取