ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC自绘复选下拉框CCheckComboBox实现详解

MFC自绘复选下拉框CCheckComboBox实现详解 简介面向需要在VC程序中实现复选下拉框的开发者基于VS2008 SP1环境编写核心为CheckComboBox.h与CheckComboBox.cpp两个文件提供了可直接使用的CCheckComboBox组件。针对作者在模态对话框使用中遇到的多次进入后无法正常选择的棘手问题资源给出了改用非模态子对话框的解决方案帮助读者避开这一陷阱。压缩包共22个文件除核心源码外还包含6个cpp源文件、8个头文件以及工程配置、资源文件、说明文档等整体仅136KB结构清晰便于直接集成或参考。资源附带的完整示例工程演示了在对话框中使用该组件的具体流程可帮助理解消息映射与界面刷新逻辑。目前已有949人学习使用适合正在处理复杂界面交互的VC开发者尤其是遇到下拉复选功能异常时可作排错参考。通过阅读示例工程与源码可快速掌握复选下拉框的正确用法及其在对话框编程中的注意事项提升开发效率。1. 项目概述与核心需求做MFC桌面开发的兄弟十有八九会遇到这种需求界面上有个下拉框里面列了十几个选项用户需要勾选其中好几个然后程序再根据勾选结果干活。你用默认的CComboBox拉下来一次只能选一项根本顶不住你用一堆CheckBox控件平铺在界面上十几个又占地方又难看你用ListBox加多选模式界面风格又和下拉框不搭。这就是我这次要聊的——在VC里实现一个自带复选框的下拉框我习惯把它叫做CCheckComboBox本质就是对CComboBox做二次扩展让下拉列表里的每一项前面都有一个CheckBox。这个控件能解决什么问题很直接让“多选”这个操作在视觉上和操作上都保持下拉框的轻量感不额外占据对话框空间同时用户看到勾选框就知道可以多选交互心智零成本。它适合谁来参考如果你正在用VC 6.0到Visual Studio 2017甚至更新的版本做MFC开发碰到过类似的“下拉多选”需求或者你想研究一下CComboBox的Owner Draw机制到底怎么玩这篇文章的完整实现思路和踩坑记录都能直接用上。先交代一下实现路径我们不引入任何第三方库就是基于MFC的CComboBox派生一个自定义类重写几个关键虚函数和处理几个通知消息然后把它当普通下拉框一样拖到对话框上用。核心原理说穿了就两件事第一让下拉框进入自绘模式Owner Draw这样每一项的内容由我们自己画第二在每一项前面根据该项的选中状态画一个CheckBox图案点击的时候切换状态并刷新显示。听起来不复杂但真正落地的时候有几个坑不踩一遍根本想不到。2. 方案分析与界面设计思路2.1 为什么选择继承CComboBox而不是从零写控件有人可能会问直接拿CListBox加LBS_MULTIPLECHECK风格再嵌到一个下拉框外形里不就行了这事我试过实际做出来之后交互很别扭。CListBox自带的复选列表风格LBS_MULTIPLECHECK画出来的勾选框样式比较老气而且它不归CComboBox管你得自己处理下拉弹出、收起、失去焦点时的行为等于同时维护两个控件窗口的联动状态。还有一版我试过用CComboBox的下拉列表直接SetDroppedWidth然后自绘但那个方案对行高的控制很死勾选框坐标也容易算歪。最后稳定下来的方案还是从CComboBox派生利用它自带的DropDown列表框架配合Owner Draw机制逐项自绘。这样下拉框的弹出、滚动、选中高亮、键盘操作这些底层行为都是CComboBox自带的我们只负责画内容和处理点击工作量小一半还不容易出Bug。这里的核心机制叫Owner Draw也就是自绘模式。CComboBox创建的时候带上CBS_OWNERDRAWFIXED风格系统就不再画每一行的默认文本了而是通过WM_DRAWITEM消息回调通知我们说“这一项需要你来画”。对应到MFC里就是重写虚函数DrawItem。同时还有一个MeasureItem用来告诉系统每一行的行高是多少这直接决定了下拉列表的行距。DrawItem和MeasureItem这两个虚函数就是整个复选下拉框的基石。在自绘模式下CComboBox的文本显示区平时没拉下来时显示当前文字的那个编辑框或静态区域也会走DrawItem。这意味着你要自己画“收缩状态”下的显示内容。我的做法是平时把已勾选的项的文字用逗号拼起来显示如果一项都没勾就显示一个占位符比如“请选择...”。这个逻辑写在一个私有的UpdateText成员函数里每次勾选状态发生变化时调用。2.2 数据结构设计与状态存储复选状态需要存储。CComboBox有一个非常方便的配套机制每一项都可以通过SetItemData绑定一个DWORD_PTR类型的附加数据用GetItemData取出来。这个附加数据原本常用的用途是存放指针或者索引值我这里直接拿它当布尔状态用0代表未勾选1代表已勾选。为什么不用一个额外的BOOL数组因为CComboBox的下拉项是可以被动态增删的如果维护一个独立数组AddString、DeleteString、InsertString、ResetContent这些函数全部都要同步维护数组哪一次忘了就出隐蔽Bug。用SetItemData绑定增删项的时候状态跟着项走自动同步省心。当然如果你需要存储“半选”状态或者更复杂的业务标识一个DWORD_PTR不够用那就应该改成存储一个堆指针指向你自己的结构体。这里先按下不表我们基础版本用0/1就够。代码骨架长这样头文件里类的声明// CCheckComboBox.h #pragma once class CCheckComboBox : public CComboBox { DECLARE_DYNAMIC(CCheckComboBox) public: CCheckComboBox(); virtual ~CCheckComboBox(); void SetCheck(int nIndex, BOOL bChecked); BOOL GetCheck(int nIndex) const; void SelectAll(BOOL bCheckAll); CString GetCheckedItemsText() const; protected: afx_msg void OnDropdown(); afx_msg void OnCloseup(); afx_msg LRESULT OnSelEndOK(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnSelChange(WPARAM wParam, LPARAM lParam); virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); BOOL PreTranslateMessage(MSG* pMsg); DECLARE_MESSAGE_MAP() private: CString m_strPlaceholder; BOOL m_bDropped; void UpdateText(); void ToggleCheck(int nIndex); };这里有几个值得聊的设计点。第一我用m_bDropped标记下拉是否展开。为什么要这个变量因为DrawItem在下拉展开和收缩两种状态下都会被调用画法不同而DrawItem的参数里没有直接告诉你当前是不是展开状态所以要在OnDropdown下拉框展开通知和OnCloseup收起通知里手动维护。第二OnSelEndOK和OnSelChange为什么用消息映射而不是普通虚函数因为我们要拦截的是CBN_SELENDOK和CBN_SELCHANGE这两个通知消息它们在用户在下拉列表里选中某一项时触发父窗口会把它们以WM_COMMAND的形式发给CComboBox自己MFC通过消息映射表能够反射处理。简单说消息反射机制让控件自己处理自己的子控件通知父对话框不用插一手。第三PreTranslateMessage用来拦截鼠标和键盘消息后面讲坑的时候会细说。3. 核心流程与关键步骤实现3.1 创建控件与样式设定关键的样式设定必须在CreateWindow时一步到位。比如你要让下拉框自绘就要有CBS_OWNERDRAWFIXED要让下拉列表是列表框而不是编辑框风格就不要CBS_DROPDOWN而是CBS_DROPDOWNLIST。CBS_DROPDOWNLIST意味着用户不能手动输入文字只能点开下拉选择这对我们的复选场景是合适的因为用户不需要输入任意文本。在对话框模板里动态创建的话这样写// 在对话框初始化时动态创建复选下拉框 CRect rcCombo(10, 10, 250, 30); m_checkCombo.Create( WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS, rcCombo, this, IDC_CHECK_COMBO);注意CBS_HASSTRINGS。这个风格告诉系统下拉项的数据以字符串为主这样GetLBText等函数才能正常工作而且系统内部会管理字符串内存。MeasureItem和DrawItem中需要的行高、文本输出都会依赖这个风格不能省。如果你打算直接在对话框资源编辑器里放置一个Combo Box控件再在类向导里给控件关联变量并手动改成CCheckComboBox类型也可以但必须记得在对话框的OnInitDialog里给控件动态加上CBS_OWNERDRAWFIXED风格。因为对话框模板里预创建的控件是没有这个风格的缺了它DrawItem就不会被调用。补风格代码如下// 如果控件是在资源模板中定义的需要在这里补上自绘风格 m_checkCombo.ModifyStyle(0, CBS_OWNERDRAWFIXED);3.2 MeasureItem行高的精确控制MeasureItem这个函数的作用是告诉系统每一行占多高。这里有个细节很多人忽略返回的行高是下拉列表弹出来时每一项的高度但DrawItem里绘画区域的高度也是它。如果行高给太小勾选框和文字挤在一起给太大十几项的下拉列表会变得很高很难看。我的经验值是用当前字体信息动态计算行高文字高度的基础上加6到8像素的上下留白这样勾选框能宽松地放进去。void CCheckComboBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // 根据字体动态计算项高度 TEXTMETRIC tm { 0 }; HDC hDC GetDC(); CFont* pFont GetFont(); CFont* pOldFont (CFont*)hDC-SelectObject(pFont); GetTextMetrics(hDC, tm); hDC-SelectObject(pOldFont); ReleaseDC(hDC); // 行高 字符高度 上下各4像素留白 1像素底边线 lpMeasureItemStruct-itemHeight tm.tmHeight 9; }这套写法在DPI变化的环境中表现相对稳定因为字体信息是动态查的。在高DPI显示器125%、150%缩放下系统字体会变大行高跟着变不会出现文字被截断的问题。3.3 DrawItem绘制勾选框、高亮和文本DrawItem是整份代码的灵魂。在这个函数里你要处理三件事背景绘制、勾选框绘制、文本绘制。先说背景。当下拉列表里鼠标停留在一项上时系统会设置ODS_SELECTED标志这时候需要自己画高亮背景色通常用COLOR_HIGHLIGHT。但要注意如果你整行都填充了高亮色再画一个勾选框在上面勾选框的区域就会显得很怪——所以我处理的方式是先整行填充背景高亮再画勾选框的白色方形区域最后在方形区域中心画一个对勾符号。勾选框的绘制不要自己去画那些复杂的3D边框效果直接调用DrawFrameControl函数用DFC_BUTTON | DFBS_CHECKBOX参数系统会以当前主题样式画出带圆角的、符合Windows风格的勾选框。选中状态则在DrawFrameControl后再覆盖一个DFCS_CHECKED状态标志。这是最简单也最不容易画歪的办法比用RectangleLine手画稳定得多。核心绘制代码如下void CCheckComboBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); CRect rcItem(lpDrawItemStruct-rcItem); int nIndex lpDrawItemStruct-itemID; // 1. 画背景色选中高亮优先 UINT nState lpDrawItemStruct-itemState; COLORREF clrBk, clrText; if (nState ODS_SELECTED) { clrBk GetSysColor(COLOR_HIGHLIGHT); clrText GetSysColor(COLOR_HIGHLIGHTTEXT); } else { clrBk GetSysColor(COLOR_WINDOW); clrText GetSysColor(COLOR_WINDOWTEXT); } pDC-FillSolidRect(rcItem, clrBk); // 2. 计算勾选区域左侧留8像素边距宽度等于行高 CRect rcCheck rcItem; rcCheck.left 8; rcCheck.right rcCheck.left rcItem.Height() - 8; rcCheck.top 4; rcCheck.bottom - 4; // 3. 用系统API绘制勾选框 BOOL bChecked GetCheck(nIndex); UINT uCheckState DFCS_BUTTONCHECK; if (bChecked) uCheckState | DFCS_CHECKED; ::DrawFrameControl(pDC-GetSafeHdc(), rcCheck, DFC_BUTTON, uCheckState); // 4. 绘制文字区域 CRect rcText rcItem; rcText.left rcCheck.right 6; rcText.right - 4; CString strText; GetLBText(nIndex, strText); pDC-SetBkMode(TRANSPARENT); pDC-SetTextColor(clrText); pDC-DrawText(strText, rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); }两个绘制细节值得单独说。第一个细节是rcItem在列表展开时是客户区整行但在收缩状态时可能只有一部分宽度——如果勾选框算出来的rcCheck超出了rcItem的可视范围DrawFrameControl会自动裁剪没有关系。第二个细节是绘制文字时一定要用DT_VCENTER | DT_SINGLELINE的组合不要用DT_END_ELLIPSIS以外的额外标志否则文字在部分字体下会出现像素级的垂直偏移。还有一点文字底色我设置了TRANSPARENT因为背景已经自己填充过了再让DrawText去填充底色会闪一下而且会和选中高亮色之间出现一条缝。3.4 鼠标交互与通知消息处理光画出来还不够用户点一下勾选框状态要能切换。这里有个特别隐蔽的坑鼠标点击下拉列表某一行的时候系统会默认发出一个选中的通知紧接着下拉框就会收起。如果我只处理CBN_SELCHANGE消息去切换勾选状态用户会发现点一下勾选框变了但下拉框也关了想勾第二项还得重新打开体验很差。解决这个问题的标准做法是在下拉框展开期间拦截CBN_SELENDOK消息并且在这个消息处理里不让它关闭下拉列表。具体来说MFC里可以在OnSelEndOK里直接返回一个非零值但这个返回值并不能阻止关闭。真正的做法是在OnDropdown之后给自己的控件发送一个CB_SHOWDROPDOWN消息把它重新强制弹开——这招虽然暴力但是有效很多老项目里都这么干。还有一种做法是重写PreTranslateMessage在鼠标按下时判断点击位置是否落在某一行上如果落在该项的勾选区或者整行就调用ToggleCheck切换状态然后返回TRUE吞掉这条鼠标消息让它不进入默认的下拉收起流程。我自己的实现选了第二种因为它不打断系统原本的窗口关闭时机逻辑更干净。参考代码如下BOOL CCheckComboBox::PreTranslateMessage(MSG* pMsg) { // 动态创建的下拉列表是弹出的需要单独判断 if (pMsg-message WM_LBUTTONDOWN m_bDropped) { CWnd* pDropList GetDroppedControl(); if (pDropList ! NULL pDropList-GetSafeHwnd() pMsg-hwnd) { // 把客户区坐标换算成列表项索引 CPoint pt(GET_X_LPARAM(pMsg-lParam), GET_Y_LPARAM(pMsg-lParam)); pDropList-ScreenToClient(pt); // 注意需要判断按钮点击是否落在勾选区域或整行这里简化成整行切换 int nIndex pDropList-HitTest(pt); if (nIndex 0 nIndex GetCount()) { ToggleCheck(nIndex); UpdateText(); // 通知父窗口状态变化 GetParent()-SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CBN_CHECKCHANGE), (LPARAM)GetSafeHwnd()); return TRUE; // 吞掉消息不让列表收起 } } } return CComboBox::PreTranslateMessage(pMsg); }注意这里GetDroppedControl()的用法。CComboBox内部的下拉列表框是一个独立的控件窗口它收到的鼠标消息hwnd和CComboBox自身的hwnd不是同一个。所以拦截鼠标消息必须判断pMsg-hwnd是否是下拉列表框的句柄坐标转换也得分清楚。ToggleCheck里边做的是取反当前项的存取数据然后强制触发重绘void CCheckComboBox::ToggleCheck(int nIndex) { if (nIndex 0 || nIndex GetCount()) return; BOOL bChecked GetCheck(nIndex); SetCheck(nIndex, !bChecked); Invalidate(); }OnDropdown和OnCloseup用来维护m_bDropped状态void CCheckComboBox::OnDropdown() { m_bDropped TRUE; CComboBox::OnDropdown(); } void CCheckComboBox::OnCloseup() { m_bDropped FALSE; CComboBox::OnCloseup(); }3.5 收缩状态文本更新前面提到过收缩状态下要显示“已选中的项名称”拼起来的文本。这个文本不能直接SetWindowText因为CComboBox在CBS_DROPDOWNLIST风格下文本是通过SetCurSel控制的。如果我们重写了DrawItem那么SetWindowText的效果也会被自绘覆盖掉所以干脆也是用自己的成员变量保存一个显示字符串再调用Invalidate触发重绘绘制时直接输出这个字符串。这样最简单。void CCheckComboBox::UpdateText() { CString strText GetCheckedItemsText(); if (strText.IsEmpty()) { // 没有勾选任何项时显示占位符 m_strPlaceholderText _T(请选择...); SetWindowText(m_strPlaceholderText); } else { m_strPlaceholderText.Empty(); SetWindowText(strText); } Invalidate(); }GetCheckedItemsText就是把所有选项遍历一遍勾了的取文字拼起来用逗号分隔。如果选中项太多显示区域有限绘制代码里已经加了DT_END_ELLIPSIS文本超出部分会显示省略号不会把控件撑爆。这里有一个性能上的小建议如果选项数量非常少比如不到10个每次UpdateText时全量遍历没有任何问题如果选项有成百上千个就不要用这种全量拼接的方式了建议维护一个“已勾选索引集合”的成员变量用CArray或者std::vector来存增删时同步维护UpdateText只遍历这个集合效率会高得多。4. 对话框集成与事件通知机制4.1 在对话框中放置控件并关联变量动态创建的方案前面已经给了完整代码但大多数MFC项目还是倾向于在资源编辑器里拖一个Combo Box摆好位置。这种情况下除了在OnInitDialog里补上CBS_OWNERDRAWFIXED风格之外还需要把控件的变量类型改成CCheckComboBox。具体操作是用类向导给控件添加变量变量类型填CCheckComboBox然后在对话框的头文件里确保包含“CCheckComboBox.h”。如果你用的Visual Studio版本比较老可能类向导的下拉列表里没有这个类那就直接在头文件里手动声明// CMyDialog.h #include CCheckComboBox.h class CMyDialog : public CDialogEx { ... protected: CCheckComboBox m_ctrlCheckCombo; ... };然后在DoDataExchange里加一句DDX_Controlvoid CMyDialog::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_CHECK_COMBO, m_ctrlCheckCombo); }这样在OnInitDialog里就能直接给控件加项了BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 补自绘风格模板创建的控件需要 m_ctrlCheckCombo.ModifyStyle(0, CBS_OWNERDRAWFIXED); // 添加选项 m_ctrlCheckCombo.AddString(_T(篮球)); m_ctrlCheckCombo.AddString(_T(足球)); m_ctrlCheckCombo.AddString(_T(羽毛球)); m_ctrlCheckCombo.AddString(_T(乒乓球)); // 默认勾选第一项 m_ctrlCheckCombo.SetCheck(0, TRUE); m_ctrlCheckCombo.UpdateText(); return TRUE; }4.2 向父窗口发送自定义通知消息很多时候对话框需要知道用户在下拉框里勾选了哪一项以便联动刷新其他界面元素。MFC的控件通知一般通过WM_COMMAND消息发送对于标准控件有CBN_SELCHANGE、CBN_EDITCHANGE这些。但我们的复选交互是自定义的没有现成的通知码所以我在ToggleCheck之后给父窗口发了一个自定义通知用CBN_CHECKCHANGE。这个通知码要用一个不冲突的值通常可以在WM_USER上面做文章或者直接用0x1000以下没被占用的通知码。这里有个坑通知码的取值空间是被系统定义的CBN_系列占了一部分的取值范围是0x0300到0x030F附近自定义时建议取一个大的比如0x1000然后定义成#define CBN_CHECKCHANGE 0x1000父窗口里如果用的是ON_CONTROL_REFLECT可以在子控件类内部自己处理如果想让对话框响应就在对话框的消息映射里加ON_CONTROL(CBN_CHECKCHANGE, IDC_CHECK_COMBO, CMyDialog::OnCheckComboChange)这样在OnCheckComboChange里可以安全地遍历所有项把勾选状态读出来做后续的业务逻辑。不要试图在PreTranslateMessage里直接调用对话框的业务函数那会让控件类和对话框耦合很重后面维护起来难受。4.3 键盘操作支持鼠标操作搞定之后千万别把键盘用户忘了。下拉框展开时用户可能用上下方向键移动高亮项然后按空格键做勾选切换。这个交互我现在已经用得很顺手了一定要在PreTranslateMessage里加对VK_SPACE的处理。逻辑很简单当下拉框展开时当前高亮项由GetCurSel()拿到如果是合法的索引就ToggleCheck它然后返回TRUE。if (pMsg-message WM_KEYDOWN m_bDropped) { if (pMsg-wParam VK_SPACE) { int nCur GetCurSel(); if (nCur 0) { ToggleCheck(nCur); UpdateText(); return TRUE; } } }加上这个之后整个控件用键盘操作起来就和普通下拉框一样顺手空格勾选回车或Esc收起Tab切换焦点无障碍访问的键盘路径就通了。5. 常见问题与排查技巧实录5.1 下拉列表点击无效勾选框不响应这个问题主要是鼠标消息被父窗口截胡了。你要在PreTranslateMessage里拦截WM_LBUTTONDOWN但前提是GetDroppedControl()拿到的句柄要和pMsg-hwnd匹配。我自己调试时遇到过的经典场景是把判断写成了if (pMsg-hwnd GetSafeHwnd())结果点的永远是下拉列表窗口永远进不了分支。排查办法其实很简单在PreTranslateMessage开头加一个TRACE(_T(hwnd: 0x%X, drop: 0x%X\n), pMsg-hwnd, GetDroppedControl()-GetSafeHwnd());跑一次就知道该怎么办了。另外要确认自己确实加了CBS_OWNERDRAWFIXED风格。没有这个风格DrawItem根本不执行勾选框压根不会画出来这种情况下点哪里都没反应。5.2 勾选框画出来但特别小或者文字对不齐行高和字体有关。MeasureItem里如果用固定的像素值比如写死24在125%缩放的屏幕上就会出问题文字底部会被截断。解决办法就是我前面说的在MeasureItem里动态获取当前字体的TEXTMETRIC以此为基础计算行高。这个做法在DPI变化时能自适应不用额外去处理WM_DPICHANGED。还要留意下拉框和对话框的字体如果设置了不同大小MeasureItem拿到的GetFont返回的是CComboBox自身的字体不是父窗口的。如果两者字体不一致行高会按错的字体来算。建议在OnInitDialog里统一给控件SetFont(GetFont())。5.3 下拉框点击后立刻收起无法连续勾选这是复选下拉框最经典的问题本质原因是CComboBox的默认交互逻辑是“点一项就选定并关闭”。我在3.4节里给的方案是吞掉鼠标消息。但如果你在项目里不方便重写PreTranslateMessage还有一个备选方案在OnCloseup消息里判断本次是否发生了勾选变化如果变化了重新弹开下拉列表。这个方案有一个副作用下拉框在收起时会有一次肉眼可见的闪烁和跳动。相比之下我还是推荐PreTranslateMessage的做法它从根源上阻止了鼠标消息触发默认的“选中并关闭”流程动画上更平滑。这里再强调一句吞掉鼠标消息后高亮项依然会跟着鼠标移动因为高亮是由系统对WM_MOUSEMOVE的处理完成的我们拦截的只是WM_LBUTTONDOWN不会影响视觉反馈。5.4 下拉框收起后显示的文本不更新如果你发现UpdateText里调用了SetWindowText但界面上显示还是老样子先检查是不是自绘模式下的收缩状态绘制里输出了硬编码的旧文本。我在早期版本里犯过这个错在DrawItem函数里当nIndex -1时我直接输出变量m_strSelectedText但UpdateText更新的是另一个成员变量两边没同步。排查起来很浪费时间的。建议把“当前显示文本”统一收敛成一个成员变量所有会修改它的地方都走同一个函数不要多个地方各改各的。5.5 常见问题速查表现象直接原因排查与修复下拉项全部空白不显示文字缺少CBS_HASSTRINGS风格创建时加上CBS_HASSTRINGS或ModifyStyle动态添加DrawItem不被调用控件没有CBS_OWNERDRAWFIXED风格动态创建时带上该风格模板创建的在OnInitDialog里ModifyStyle勾选框画出来但不是Windows主题样式使用了自己画的矩形加对勾改用DrawFrameControl(DFC_BUTTON / DFBS_CHECKBOX)点击列表项后下拉框立刻关闭默认选中交互被触发在PreTranslateMessage拦截WM_LBUTTONDOWN并吞掉键盘空格无法切换勾选没处理WM_KEYDOWN的空格键在下拉展开状态下拦截VK_SPACE并调用ToggleCheck文字被截断行高不足或字体不一致在MeasureItem里动态获取TEXTMETRIC并计算行高收缩后显示文本不及时更新更新文本逻辑没有统一入口用UpdateText统一处理并在勾选变化后调用父窗口收不到变化通知没发送自定义通知码勾选状态变化后SendMessage(WM_COMMAND, MAKEWPARAM(ID, CBN_CHECKCHANGE), ...)6. 扩展思路与实际使用体会这个CCheckComboBox基础版做完之后如果你愿意还可以往几个方向继续打磨。比如支持“全选”和“清空”的上下文菜单右键点击下拉框弹出一个菜单提供全选/全不选这在选项特别多的业务场景里很好用。再比如支持只读模式某些情况下不允许用户改勾选状态只需要展示可以在DrawItem里根据只读标志把勾选框画成灰色禁用状态。还可以支持在收缩状态下显示“已选3项”而不是把每项都拼出来这种显示策略适合选项名称特别长的场景。我用了好几年这个控件最大的体会是自绘控件并不是什么高深的技术核心就是理解DrawItem、MeasureItem、消息反射、窗口句柄归属这几套机制剩下的事情就是反复调试细节。尤其是窗口句柄的归属问题CComboBox本身是一个复合控件父窗口和内部弹出的下拉列表框是两个不同的HWND很多诡异的Bug都是因为混淆了这两个窗口导致的。调试这类问题最有效的办法就是TRACE输出各个句柄一目了然。如果你要把这个控件实际用在自己的项目里我最后再给你一个建议一定要在项目一开始就把DPI感知打开并且用动态字体计算行高否则后面屏幕适配会让你改到怀疑人生。另外如果你用的是Visual Studio 2017或更高版本记得在项目属性里把字符集设成“使用Unicode字符集”所有字符串都用_T宏包一下这样代码在VC 6.0的老工程和现代工程之间移植都不需要大改。复选下拉框这个需求很常见把这个控件做扎实了你的工具箱里又多了一件称手的武器。本文还有配套的精品资源点击获取
返回列表