MFC按钮不显示?深入解析CButton::Create与样式组合
最近在带一个老项目的维护,看到不少人卡在“按钮显示不出来”这种最基础的问题上,而且搜索引擎翻半天也找不到正经答案。仔细一问,多半是对CButton::Create的几个参数理解不透,尤其是 Button Styles 和 Window Styles 到底怎么组合、区别在哪,完全是一笔糊涂账。这篇文章就把这块彻底讲透,先从窗口创建的本质说起,再逐项拆解函数参数和样式位,最后给出一套可以直接抄的实操流程和排查清单。适合刚接触 MFC 或者从 Win32 转过来的开发者,也适合老手快速检索核对。
1. 搞清楚按钮到底是个什么东西
很多人把按钮当成一个“控件”,但在 Windows 的世界里,按钮就是一个窗口。它有自己的窗口句柄,有自己的窗口过程,也要遵守窗口的创建规则。这句话看起来简单,却是理解一切后续问题的关键。
CButton是 MFC 对系统按钮窗口类的封装,内部就是CreateWindowEx那套机制。当你调用CButton::Create时,MFC 会去注册或复用系统预定义的按钮类名,也就是"BUTTON",然后创建出一个子窗口。既然是子窗口,就必须依附于某个父窗口存在,并且要指明它在父窗口客户区中的位置和大小。
这好比往墙上挂一幅画,墙上得有钉子,画框上得有挂绳,两个条件缺一不可。父窗口是墙,WS_CHILD就是挂绳,WS_VISIBLE就是你有没有把画布掀开。很多人按钮不显示,要么是墙没砌好,要么是挂绳没系上,要么是布没掀开。
另一个容易忽视的点是:父窗口必须先创建成功,才能去创建子窗口。如果你在OnCreate里直接调Create,此时主窗口的HWND已经有效,但因为窗口还在创建过程中,某些初始化顺序不对,照样会出问题。最佳实践是放在OnInitDialog或WM_CREATE消息响应完之后再创建按钮,这时候布局已经稳定,坐标计算也靠谱。
2. CButton::Create 逐个参数的拆解
先看函数原型,这是 MFC 封装的版本:
BOOL Create(LPCTSTR lpszCaption, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);一共五个参数,下面一个一个说。
2.1 文本内容
lpszCaption是按钮上显示的文字,常见比如"确定"、"取消"、"浏览..."。没什么玄机,但要注意两点。
第一,如果项目使用 Unicode 字符集(现在默认都是),_T("确定")这种写法没问题;如果硬编码成LPSTR字符串会出现乱码或者编译警告。第二,自绘按钮(BS_OWNERDRAW)绘制时不会自动处理文字,需要你自己在DrawItem里用DrawText绘制,文本参数在这里只是存着一个字面量。
2.2 样式组合
dwStyle是整个函数里最核心、最容易出错的参数。它是 32 位整数,由“窗口样式”和“按钮样式”两部分按位或组合而成。窗口样式管这个按钮如何作为窗口存在,按钮样式管这个按钮长得像什么、行为像什么。
最常见的组合是这样的:
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON这里WS_CHILD表示这是个子窗口,WS_VISIBLE表示创建后立即显示,BS_PUSHBUTTON表示这是个普通按压式按钮。三者在代码里通常是缺一不可的。
如果少了WS_VISIBLE,按钮创建成功但不可见,你得再调用ShowWindow(SW_SHOW)才能显示。如果少了WS_CHILD,按钮作为一个顶层窗口创建,位置和父窗口完全脱钩,画面上可能根本找不到它在哪,而且消息也无法正常路由到父窗口。如果BS_PUSHBUTTON缺失但样式是 0,按钮可能仍然创建成功,但形态和消息行为会变得不可预期。
2.3 矩形区域
rect是RECT结构,描述按钮在父窗口客户区中的坐标。这个坐标系是父窗口客户区左上角为原点的,不是屏幕坐标,也不是父窗口的窗口坐标。很多人第一次写代码时直接把屏幕坐标塞进去,结果按钮跑到了意料之外的位置,或者干脆看不见。
举一个实际例子:
CRect rc(10, 10, 130, 50);这表示按钮左上角位于父窗口客户区坐标 (10, 10),右下角位于 (130, 50)。宽度 120 像素,高度 40 像素。你可以在OnInitDialog里用GetClientRect拿到父窗口客户区矩形,然后根据它计算按钮位置,这样按钮就能做到最基本的相对布局。
2.4 父窗口指针
pParentWnd指向按钮的父窗口,通常传入this。父窗口作用是接收按钮的通知消息,比如点击消息BN_CLICKED会通过WM_COMMAND发给父窗口。
这里有个细节容易被忽略:按钮的父窗口必须是消息路由链路的一部分。如果你把按钮创建在一个临时控件上,或者父窗口对象提前被销毁,那按钮就会和父窗口一起销毁,产生悬空指针或者点击无响应的问题。
2.5 控件 ID
nID是按钮的标识符。在一个对话框里,这个 ID 必须唯一,因为它决定了消息路由和控件操作时的索引。如果你有两个按钮 ID 相同,消息处理会出现混乱,比如点击一个按钮,另一个按钮也在响应。
建议使用资源文件里定义的 ID 宏,比如IDC_BTN_SUBMIT,而不是直接写死一个数字,这样阅读代码时语义清晰,还不容易冲突。
3. Button Styles 深度解析
按钮样式决定按钮的视觉和行为形态。我把常用的整理成一张表格,方便对照。
| 样式位 | 含义 | 典型场景 |
|---|---|---|
BS_PUSHBUTTON | 普通按压按钮 | 最常见的“确定”“取消” |
BS_DEFPUSHBUTTON | 默认按钮 | 对话框里按回车触发的按钮 |
BS_CHECKBOX | 复选框,点击不自动改变状态 | 需要手动维护选中状态 |
BS_AUTOCHECKBOX | 复选框,点击自动切换选中状态 | 最常用的勾选项 |
BS_3STATE | 三态复选框,不自动切换 | 需要手动控制第三态 |
BS_AUTO3STATE | 三态复选框,自动切换 | 部分选中、全选、未选 |
BS_RADIOBUTTON | 单选按钮,不自动互斥 | 需要手动管理互斥逻辑 |
BS_AUTORADIOBUTTON | 单选按钮,自动互斥 | 最常用的单选项组 |
BS_GROUPBOX | 分组框,只画边框和标题 | 视觉分组,不处理点击 |
BS_OWNERDRAW | 自绘按钮 | 自定义外观的按钮 |
BS_LEFTTEXT | 文字显示在按钮左侧 | 配合复选框和单选按钮使用 |
BS_LEFT/BS_RIGHT | 文字左对齐/右对齐 | 调整文字排布 |
3.1 普通按钮:BS_PUSHBUTTON 与 BS_DEFPUSHBUTTON
BS_PUSHBUTTON是默认的普通按钮,点击时按钮会凹陷再弹起,同时给父窗口发送BN_CLICKED通知。
BS_DEFPUSHBUTTON只用在对话框环境中,它的外观比普通按钮多了一个黑色粗边框,表示这是默认按钮。它最大的作用是:用户在对话框里按下回车键时,系统会模拟一次点击这个按钮的键盘消息。如果没有这个样式,按下回车会走默认的逻辑,比如回车直接关闭对话框或无响应,和你的预期可能差距很大。
如果你在OnInitDialog里创建了两个按钮,一个BS_DEFPUSHBUTTON一个BS_PUSHBUTTON,按回车只会触发默认按钮,这是符合用户习惯的设计。
3.2 复选框:BS_CHECKBOX 与 BS_AUTOCHECKBOX
BS_CHECKBOX只是个外观上带方框的按钮,点击不会自动改变选中状态。要切换选中状态,你需要自己处理BN_CLICKED消息,调用GetCheck获取当前状态,再调用SetCheck设置新状态。
BS_AUTOCHECKBOX则完全不同。你只需要处理BN_CLICKED消息,在里面用GetCheck读状态,系统已经自动把方框的勾选状态切换好了。大多数场景下你永远不需要BS_CHECKBOX,直接用BS_AUTOCHECKBOX就好。
注意一点:GetCheck和SetCheck是CButton的成员函数,内部通过BM_GETCHECK和BM_SETCHECK消息实现。状态值 0 表示未选中,1 表示选中,2 表示不确定态。
3.3 单选按钮:BS_RADIOBUTTON 与 BS_AUTORADIOBUTTON
单选按钮比复选框复杂一点,核心问题是互斥。同一时间一个组里只能有一个按钮处于选中状态。
BS_RADIOBUTTON很原始,它不会自动处理互斥。你不得不自己写逻辑:点击其中一个就遍历同组所有按钮,设置选中或取消选中。
BS_AUTORADIOBUTTON则不一样,它会在点击时自动取消同组其他按钮的选择状态。这里“同组”的判定依赖于按钮的 Z 序和WS_GROUP样式,稍后在窗口样式部分详细讲。
我实际遇到过一个情况:用BS_AUTORADIOBUTTON创建了三个单选按钮,不设置WS_GROUP,默认按创建顺序第一个拥有WS_GROUP,此时系统会把它作为一组。如果你创建的顺序比较乱,或者中间夹了其他控件,互斥分组就会乱套,两个按钮可以同时选中。
3.4 分组框:BS_GROUPBOX
BS_GROUPBOX不是真正意义上的按钮,它绘制一个矩形边框和左上角的标题文字,用来给其他控件做视觉分组。
它仍然继承自CButton,所以你可以创建一个CButton对象并把样式设为BS_GROUPBOX,然后它会画出一个框。
有一点要注意:分组框本身不拦截鼠标消息,但它作为窗口存在,可能会挡住它下面的控件接收点击。如果发现分组框内部的其他控件点击失灵,检查一下分组框的绘制范围和 Z 序,通常把分组框放在底层即可。
3.5 自绘按钮:BS_OWNERDRAW
BS_OWNERDRAW是把按钮绘制权交给你的样式。设置之后按钮不再绘制任何默认外观,所有绘制逻辑都由你在DrawItem虚函数中实现。
自绘按钮适合做皮肤、异形按钮、动态渐变等效果,但代价是你得自己处理所有状态:正常、按下、悬停、禁用、聚焦时都要有对应的绘制分支。开发经验不足的话容易出现“按钮没反应”或者“所有状态一个样”的问题。
我的建议是:除非视觉设计确实需要,否则不要轻易上自绘。很多需求用纯色块加文字就能解决,考虑用WM_CTLCOLOR_BTN改变按钮背景色即可,没必要把整个绘制流程接管过来。
4. Window Styles:决定按钮怎么“活”在这个窗口里
窗口样式同样是 32 位的标志组合,它决定按钮作为一个窗口的通用行为,和按钮具体长什么样无关。这些样式在底层是基于CreateWindowEx的参数映射的,所以任何窗口类都适用。
4.1 WS_CHILD 与 WS_POPUP
WS_CHILD表示这是一个子窗口,必须依附在父窗口上,位置相对于父窗口客户区。按钮几乎总是设置这个样式,因为按钮在 UI 中绝大多数时候都是作为某个窗口的子元素存在的。
WS_POPUP表示这是一个弹出窗口,没有父窗口或父窗口不限制它的位置。正常情况下按钮不用这个样式,如果你在按钮上误加了WS_POPUP,它会变成一个不规则的浮层,可能跑到屏幕任意位置,还不受父窗口裁剪。
WS_CHILD和WS_POPUP是互斥的,两者不能同时设置。
4.2 WS_VISIBLE:按钮不显示的罪魁祸首
这个样式太容易被漏掉了。创建按钮时如果不设置WS_VISIBLE,按钮窗口是存在的,但WS_VISIBLE位为 0,系统不会绘制它。常见表现是:
- 按钮看不见
- 控件在资源编辑器里是可见的
- 程序运行后却找不到按钮
解决办法有两个:创建时带上WS_VISIBLE,或者创建后调用ShowWindow(SW_SHOW)。前者更直接。
有一点需要特别提醒:WS_VISIBLE继承特性。如果你在父窗口尚未可见时就创建了子窗口,之后调用父窗口的ShowWindow(SW_SHOW),这个子窗口的可见性状态可能不会自动变为 “可见”。这与WS_VISIBLE的设置时机有关,稳妥的做法是等父窗口可见后再去创建或显式ShowWindow子窗口。实战中我遇到过窗口显示后按钮一片空白,排查后是这个原因。
4.3 WS_TABSTOP 与 WS_GROUP
这两个样式影响键盘导航。
WS_TABSTOP表示控件可以通过 Tab 键获得焦点。对于按钮,如果希望用户按 Tab 能依次切换焦点,同时空格键能触发点击,那就要设置它。
不设置WS_TABSTOP的按钮仍然可以用鼠标点击,但键盘无法聚焦,失去可访问性。用户习惯了 Tab 导航,这在对话框多控件场景中体验差异很明显。
WS_GROUP表示该控件开始一个新的控件组。它和WS_TABSTOP配合使用,常用来把一组单选按钮定义为一个整体。上一节提到BS_AUTORADIOBUTTON的互斥分组依赖WS_GROUP,具体规则是:
- 从设置了
WS_GROUP的控件开始,到下一个设置了WS_GROUP的控件之前(不包含),所有控件属于同一组。 - 如果没有任何控件设置
WS_GROUP,默认第一个创建的子窗口被视为组起始。
所以如果你创建了三个单选按钮,想让它们互斥,正确的创建顺序是这样的:
// 第一个按钮带 WS_GROUP,表示分组开始 CButton btn1; btn1.Create(_T("选项A"), WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP, CRect(10, 20, 100, 40), this, IDC_RADIO_A); // 第二、第三个按钮不需要 WS_GROUP CButton btn2; btn2.Create(_T("选项B"), WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, CRect(10, 50, 100, 70), this, IDC_RADIO_B); CButton btn3; btn3.Create(_T("选项C"), WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, CRect(10, 80, 100, 100), this, IDC_RADIO_C);这样三个按钮就是一个互斥组。如果每个按钮都错误地加上了WS_GROUP,那么它们各自独立成组,互相不互斥,用户可以同时选中多个。
这也是一个典型的“样式叠加顺序影响行为”的例子。
4.4 其他常用窗口样式
WS_DISABLED表示创建一个被禁用的按钮。禁用后按钮变灰,不响应鼠标和键盘输入。动态控制可用性时可以用EnableWindow(FALSE/TRUE)来切换,WS_DISABLED只是创建时的初始状态。
WS_BORDER给按钮加一条细边框。普通按钮默认有边框,但如果你用FLAT风格或自绘时可能需要视情况调整。
WS_CLIPSIBLINGS表示裁剪兄弟窗口区域,防止子窗口绘制时越过边界覆盖到同级的其他窗口。在某些复杂布局和高频重绘的情况下,设置它可以减少绘制闪烁,但不是必须的。
5. 实操:在窗口中完整地创建并处理一个按钮
说了那么多理论,现在走一遍完整流程。以基于对话框的 MFC 程序为例(这是最典型的场景),假设我们有一个CMyDlg类,要把一个“提交”按钮放到对话框中。
5.1 在对话框类中声明控件对象
class CMyDlg : public CDialogEx { public: CMyDlg(CWnd* pParent = nullptr); enum { IDD = IDD_MY_DIALOG }; protected: CButton m_btnSubmit; CFont m_font; virtual BOOL OnInitDialog(); afx_msg void OnBnClickedSubmit(); DECLARE_MESSAGE_MAP() };把按钮作为成员变量持有,能在整个对话框生命周期内安全地访问和操作它。如果你只创建而不保存对象,按钮会随着临时对象析构而销毁。
5.2 在 OnInitDialog 中创建按钮
BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 先在逻辑上确定按钮的位置 CRect rcClient; GetClientRect(&rcClient); CRect rcBtn(rcClient.right - 150, rcClient.bottom - 40, rcClient.right - 20, rcClient.bottom - 10); m_btnSubmit.Create(_T("提交"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, rcBtn, this, IDC_BTN_SUBMIT); // 按钮创建失败时要做错误处理 if (m_btnSubmit.GetSafeHwnd() == NULL) { AfxMessageBox(_T("按钮创建失败")); return FALSE; } // 设置一个好看一点的字体 m_font.CreatePointFont(90, _T("微软雅黑")); m_btnSubmit.SetFont(&m_font); return TRUE; }按钮创建完成后,立刻判断GetSafeHwnd()是否为NULL,这是比较稳妥的验证方式。
SetFont的作用很重要,否则按钮会用系统的默认字体,在 DPI 缩放场景下可能显得偏小或偏大。字体对象m_font必须是成员变量,不能是局部变量,否则按钮获得的字体在变量析构后会失效,绘制文字会变成随机表现。
5.3 添加消息响应
在消息映射中添加BN_CLICKED处理:
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx) ON_BN_CLICKED(IDC_BTN_SUBMIT, &CMyDlg::OnBnClickedSubmit) END_MESSAGE_MAP() void CMyDlg::OnBnClickedSubmit() { AfxMessageBox(_T("提交按钮被点击了")); }消息路由按这样的链路工作:按钮被点击 -> 系统向父窗口发送WM_COMMAND,wParam高 8 位是BN_CLICKED,低 16 位是控件 ID -> MFC 的消息映射找到ON_BN_CLICKED对应项并调用处理函数。
控件 ID 必须是按钮创建时传入的nID,而且不能和对话框内其他控件 ID 重复。如果发现点击一个按钮会触发另一个按钮的处理函数,首选检查 ID 是否重复。这是我在实际项目中亲历过的问题:两个按钮 ID 都是IDC_BTN_OK,点击任何一个都会触发同一个函数,排查了好久才定位到 ID 冲突。
5.4 销毁与生命周期
按钮的窗口会随着父窗口的销毁而自动销毁,你不需要手动调用DestroyWindow。但如果你在对话框运行期间动态创建了按钮,并且需要提前移除,正确的做法是调用DestroyWindow(),然后把对象指针置空。
这里有一个容易犯错的点:CButton对象的析构函数会自动销毁关联的窗口,但如果你先调用了DestroyWindow(),之后CButton对象析构时不会再重复销毁,这没问题。反之,如果你让局部CButton对象在函数结束时自动析构,同时窗口尚未销毁,那这个按钮窗口也会被销毁,可能出现“刚创建完按钮就没了”的灵异现象。
6. 常见问题与排查技巧实录
这些是我在实际开发和维护里积累下来的高频问题,整理成速查表。
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 按钮完全不可见 | 缺少WS_VISIBLE | 检查创建样式,或在创建后调用ShowWindow(SW_SHOW) |
| 按钮位置不对,飘到其他地方 | rect使用了屏幕坐标 | 换算成父窗口客户区坐标,或调用ScreenToClient |
| 按钮创建失败,返回 FALSE | 父窗口无效,或样式组合严重错误 | 检查父窗口指针和WS_CHILD,加GetSafeHwnd()判断 |
| 按钮点击无反应 | 控件 ID 冲突或消息映射缺失 | 检查 ID 是否唯一,确认ON_BN_CLICKED映射存在 |
| 按钮文字显示为乱码 | 项目字符集和字符串字面量不匹配 | 全部写成_T("..."),不要直接用"..."和L"..."混用 |
| 单选按钮能同时选中多个 | 分组错误,缺少或不合理的WS_GROUP | 按照“第一个带WS_GROUP,后续不带”的规则重新排布 |
| 按钮默认触发主窗口关闭 | 按钮误设为BS_DEFPUSHBUTTON | 评估是否需要默认按钮样式,普通按钮用BS_PUSHBUTTON |
| 按钮重复闪烁 | 缺少WS_CLIPSIBLINGS | 在复杂子窗口布局中补充该样式 |
| 按钮字体偏小偏大 | 未设置字体或字体未持久化 | 持有字体对象为成员,调用SetFont |
6.1 一个典型的“按钮不显示”案例
有一次调试一个老模块,界面无论如何都少了一个“保存”按钮。代码里Create调用确实执行了,返回值也是TRUE,但界面上就是看不到。
逐项排查后,发现问题出在这里:创建按钮的代码放在了一个自定义初始化函数中,这个函数在父窗口的WM_CREATE消息处理里被调用,而父窗口此时尚未真正完成创建流程;按钮创建后虽然WS_VISIBLE已设置,但父窗口尺寸是 0,按钮位置计算成负数,被裁切到了窗口之外。
解决方法是把创建按钮的代码移到OnInitDialog或WM_SIZE之后再执行,等父窗口尺寸有效后再进行布局。
这也印证了一个重要经验:按钮创建失败时,你很容易想到检查返回值;但按钮创建成功却不显示时,问题往往出在创建时机和布局坐标上,而不是样式本身。
6.2 字符集与文本问题的排查
MFC 项目通常有“使用 Unicode 字符集”和“使用多字节字符集”两个选项。现代项目几乎都是 Unicode,但老代码迁移时偶尔会遗漏字符串写法。
如果按钮的文字出现乱码或丢失,首先要检查代码里是否同时混用了_T("")、L""和直接字符串。统一用_T宏是最稳妥的,它会根据项目字符集自动选择char或wchar_t。
还有另一个细节:按钮文字过长时不会自动换行,而是会截断或者溢出。如果你的按钮文字包含多个单词,考虑在适当位置插入\n来换行,或者拉伸按钮宽度。
6.3 使用 Spy++ 辅助定位
当你实在找不到按钮问题时,用 Visual Studio 自带的 Spy++ 工具查看窗口树是最直接的办法。启动 Spy++,定位到目标对话框,展开子窗口列表。如果按钮存在,但WS_VISIBLE未勾选,你立刻就能看出问题。
如果按钮根本没出现在窗口树中,说明创建代码根本没执行成功,或者父窗口不对。这种可视化定位常常比读代码高效得多。
7. 再聊一些实用技巧与设计建议
按钮创建这件事本身不难,但放在一个完整的 UI 工程里,考虑的问题会多一点。
7.1 布局和 DPI 缩放
如果你在OnInitDialog中写死CRect,在高 DPI 屏幕上按钮会显得很小。较好的做法是结合GetClientRect动态计算,或者把按钮放在对话框模板资源中,让系统自动处理布局。
动态创建按钮时,建议通过父窗口客户区高度和宽度比例来计算位置,而不是写死 10、10、120、50 这类绝对坐标。我一般用一个小函数来统一布局:
void PlaceButton(CWnd* pParent, CButton& btn, UINT id, int rightMargin, int bottomMargin, int width, int height, LPCTSTR text) { CRect rc; pParent->GetClientRect(&rc); CRect rcBtn(rc.right - rightMargin - width, rc.bottom - bottomMargin - height, rc.right - rightMargin, rc.bottom - bottomMargin); btn.Create(text, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, rcBtn, pParent, id); }每个参数都有意义,调用处一目了然,布局调整也方便。实测下来,这种方式在应对不同分辨率窗口时比写死坐标稳得多。
7.2 按钮 ID 的管理
动态创建按钮时,ID 通常是#define定义的常量或者enum枚举。我习惯把同一组功能按钮的 ID 放在一起,比如:
enum { IDC_BTN_SEARCH = 1001, IDC_BTN_RESET = 1002, IDC_BTN_EXPORT = 1003, };只要不冲突,数字范围随意。但不要直接用1001这种裸数字,否则代码可读性很差,维护时想改一个 ID 要在整个文件中反复搜索替换。
7.3 按钮颜色和背景处理
默认按钮外观是系统主题决定的,但有些个性化界面需要改变按钮背景色和文字颜色。此时可以处理WM_CTLCOLOR_BTN消息,返回一个画刷,MFC 会根据画刷重绘按钮背景。
这个方法在工作量上比自绘按钮小得多,但只适用于纯色背景的场景。如果要做渐变和图片背景,还是老老实实用BS_OWNERDRAW自绘。
我个人的经验是:先用主题按钮把功能逻辑跑通,最后再考虑外观美化。很多项目翻车就翻在“先做皮肤再做功能”这个顺序上,功能按钮放到后面做,位置、ID、文字全都乱了,调试成本直线上升。
7.4 关注兄弟控件的坐标关系
动态创建多个按钮并排排列时,坐标计算最容易出错。我的速度技巧是:先创建第一个按钮并保存其rect,后续按钮根据第一个按钮的rect偏移得到,不要每个按钮都独立重新计算一遍边距,这样既少算还不容易错。
CRect rcFirst(10, 10, 110, 40); m_btn1.Create(_T("按钮1"), dwStyle, rcFirst, this, IDC_1); CRect rcSecond = rcFirst; rcSecond.OffsetRect(110, 0); // 水平偏移 110 像素 m_btn2.Create(_T("按钮2"), dwStyle, rcSecond, this, IDC_2);OffsetRect是现成的 API,用起来非常顺手。等宽排列、垂直排列都基于这个思路改造即可。
7.5 关于老代码迁移的提醒
接手老项目时,经常会遇到 VC6 时代写的老式BYTE类型和DWORD样式混用的代码,风格比较混乱。曾经有段代码在样式位里写了WS_CHILD|WS_VISIBLE|BS_OWNERDRAW但没实现DrawItem,结果按钮渲染出来是一块空白区域,点击无反馈。
定位的过程花了些时间,但原理很简单:BS_OWNERDRAW意味着系统把绘制责任全部丢给程序员,没实现绘制就是一片空白。如果你不想自绘但又想自定义外观,去掉BS_OWNERDRAW就行。
这类问题提醒我们:看到样式位不仅要“知道这个样式是什么”,还得想清楚“不实现配套逻辑会怎样”。BS_OWNERDRAW是最典型的需要特别留意的样式,因为它把窗口绘制流程从系统接管转为用户接管,后续所有绘制都要负责到底。