
簡介這份資源面向在 VS2008SP1 環(huán)境下使用 VC 開發(fā)界面的程序員聚焦復(fù)選下拉框CCheckComboBox的實現(xiàn)與使用。核心文件 CheckComboBox.h 與 CheckComboBox.cpp 封裝了復(fù)選下拉框的完整功能可直接集成到 MFC 項目中。作者 margin 在使用中發(fā)現(xiàn)該控件在模態(tài)子對話框中多次進(jìn)入后會出現(xiàn)無法正常選擇的問題并給出改用非模態(tài)子對話框即可規(guī)避的排錯思路對遇到同類界面交互問題的開發(fā)者有實際參考價值。資源包共 22 個文件以 8 個 h 頭文件、6 個 cpp 源文件為主另含 sln、vcproj 工程文件及 ico、rc、rc2 等資源文件整體約 136KB工程結(jié)構(gòu)完整便于直接編譯調(diào)試。目前已有 950 人學(xué)習(xí)下載適合需要快速掌握復(fù)選下拉框用法并規(guī)避模態(tài)對話框陷阱的 VC 開發(fā)者參考。1. 復(fù)選下拉框一個被低估的 MFC 控件缺口如果你在 VS2008SP1 的 VC 環(huán)境里做過配置面板、篩選器或者權(quán)限勾選界面大概率遇到過同一個尷尬MFC 自帶的CComboBox只能單選想多選就得自己拼一個「編輯框 列表框 彈出按鈕」的組合或者干脆退化成一組 CheckBox 堆在對話框上。前者交互別扭后者占地方。CCheckComboBox這個資源解決的正是這個缺口——它把下拉列表里的每一項變成帶復(fù)選框的條目點一下切換勾選狀態(tài)收起后還能把已選項拼成文本回顯到編輯區(qū)。適合誰正在維護(hù) VS2008SP1 老項目、不想引入第三方 UI 庫、又需要多選下拉交互的 VC 開發(fā)者。下面我按「它怎么長出來的 → 怎么塞進(jìn)你的工程 → 哪些地方會翻車」拆一遍。2. 拆開看CCheckComboBox 的繼承鏈與消息模型2.1 為什么是「繼承 CComboBox」而不是「組合 CComboBox」常見做法有兩種一種是把CComboBox當(dāng)成員變量組合進(jìn)來另一種是直接繼承。這個資源走的是繼承路線原因很實際——繼承之后你原來的DDX_Control綁定、GetDlgItem調(diào)用、EnableWindow這些代碼幾乎不用改控件在對話框資源里仍然是一個普通的 ComboBox只是類換成了CCheckComboBox。組合方案則要在外層再包一層窗口過程消息轉(zhuǎn)發(fā)寫起來啰嗦還容易在WM_COMMAND反射上踩坑。繼承鏈大致是CCheckComboBox : public CComboBox。它沒有去動 ComboBox 的繪制底層而是利用 ComboBox 的CBS_DROPDOWNLIST或CBS_DROPDOWN風(fēng)格在列表彈出時接管列表項的繪制和點擊。核心思路是ComboBox 的列表部分本質(zhì)上是一個 ListBox通過GetComboBoxInfo拿到列表窗口句柄然后對列表窗口做自繪Owner Draw或者子類化處理。2.2 復(fù)選框狀態(tài)存在哪一個容易被忽略的設(shè)計點多選控件最怕狀態(tài)丟失。CCheckComboBox通常用一個DWORD或std::vectorBOOL來保存每一項的勾選狀態(tài)索引與 ComboBox 的 item index 一一對應(yīng)。這里有個關(guān)鍵約束一旦你調(diào)用ResetContent或DeleteString狀態(tài)數(shù)組必須同步維護(hù)否則索引錯位勾選會「漂移」到別的條目上。我見過不止一個項目在這里翻車——動態(tài)刷新列表后用戶明明勾了 A結(jié)果 B 被選上了。狀態(tài)與顯示是分離的列表項文本只負(fù)責(zé)展示勾選狀態(tài)由獨(dú)立數(shù)組管理。收起時控件遍歷狀態(tài)數(shù)組把已勾選項的文本用分隔符默認(rèn)逗號拼起來SetWindowText到編輯區(qū)。這個「拼文本」的邏輯通常暴露一個SetSeparator之類的接口方便你換成中文頓號或分號。2.3 消息映射里必須處理的三個通知要讓復(fù)選框真正響應(yīng)點擊消息映射里至少得掛上這幾類處理// 在 CCheckComboBox 的消息映射中 BEGIN_MESSAGE_MAP(CCheckComboBox, CComboBox) ON_CONTROL_REFLECT(CBN_DROPDOWN, CCheckComboBox::OnDropDown) ON_CONTROL_REFLECT(CBN_SELCHANGE, CCheckComboBox::OnSelChange) ON_CONTROL_REFLECT(CBN_CLOSEUP, CCheckComboBox::OnCloseUp) END_MESSAGE_MAP()CBN_DROPDOWN列表即將彈出此時適合做列表窗口的子類化或自繪準(zhǔn)備。CBN_SELCHANGE用戶在列表中移動選擇項。注意這里不是「勾選」只是高亮移動。真正的勾選切換通常要在CBN_SELCHANGE里判斷并翻轉(zhuǎn)狀態(tài)然后立刻刷新列表項顯示。CBN_CLOSEUP列表收起觸發(fā)文本回顯邏輯把當(dāng)前勾選狀態(tài)拼成字符串。提示CBN_SELCHANGE在鍵盤上下鍵移動時也會觸發(fā)如果你在這里直接翻轉(zhuǎn)勾選鍵盤導(dǎo)航會變成「一路走一路勾」。常見做法是區(qū)分鼠標(biāo)點擊和鍵盤選擇或者干脆用NM_CLICK在列表窗口上單獨(dú)處理鼠標(biāo)。2.4 自繪列表項DrawItem 與 MeasureItem如果走 Owner Draw 路線列表的DrawItem和MeasureItem必須成對實現(xiàn)。MeasureItem決定每一項的高度復(fù)選框圖標(biāo)通常 16x16加上文字行高一般設(shè) 18~20 像素。DrawItem里先畫復(fù)選框狀態(tài)勾選/未勾選/禁用再畫文本最后處理高亮背景。void CCheckComboBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC dc; dc.Attach(lpDrawItemStruct-hDC); CRect rcItem lpDrawItemStruct-rcItem; int nIndex lpDrawItemStruct-itemID; // 根據(jù)狀態(tài)數(shù)組決定復(fù)選框是否勾選 BOOL bChecked m_arrCheck[nIndex]; // 繪制背景選中項高亮 if (lpDrawItemStruct-itemState ODS_SELECTED) dc.FillSolidRect(rcItem, ::GetSysColor(COLOR_HIGHLIGHT)); else dc.FillSolidRect(rcItem, ::GetSysColor(COLOR_WINDOW)); // 繪制復(fù)選框方框與勾 CRect rcCheck rcItem; rcCheck.left 2; rcCheck.right rcCheck.left 16; dc.DrawFrameControl(rcCheck, DFC_BUTTON, bChecked ? DFCS_BUTTONCHECK | DFCS_CHECKED : DFCS_BUTTONCHECK); // 繪制文本 CString strText; GetLBText(nIndex, strText); dc.SetBkMode(TRANSPARENT); dc.SetTextColor((lpDrawItemStruct-itemState ODS_SELECTED) ? ::GetSysColor(COLOR_HIGHLIGHTTEXT) : ::GetSysColor(COLOR_WINDOWTEXT)); rcItem.left rcCheck.right 4; dc.DrawText(strText, rcItem, DT_LEFT | DT_VCENTER | DT_SINGLELINE); dc.Detach(); }邏輯說明lpDrawItemStruct-itemID是當(dāng)前繪制的列表項索引直接用來查狀態(tài)數(shù)組。ODS_SELECTED表示該項當(dāng)前被高亮不是被勾選——高亮和勾選是兩回事別混。DrawFrameControl是 Win32 自帶的復(fù)選框繪制省得自己畫勾。參數(shù)說明DFCS_BUTTONCHECK畫空框加DFCS_CHECKED畫勾選態(tài)。如果你要支持三態(tài)灰選還得加DFCS_BUTTON3STATE和DFCS_INACTIVE但多數(shù)業(yè)務(wù)場景用不上。3. 塞進(jìn) VS2008SP1 工程從添加文件到對話框綁定3.1 文件清單與工程配置拿到資源后通常包含CheckComboBox.h和CheckComboBox.cpp兩個文件。在 VS2008SP1 里右鍵工程 → 添加 → 現(xiàn)有項把這兩個文件加進(jìn)去。如果你的工程用了預(yù)編譯頭stdafx.h確保.cpp文件第一行包含它否則會報一堆a(bǔ)fxwin.h找不到的錯。文件作用是否必須CheckComboBox.h類聲明、消息映射、接口定義是CheckComboBox.cpp繪制、狀態(tài)管理、消息處理實現(xiàn)是CheckComboBoxDemo示例對話框工程否參考用注意VS2008SP1 的 MFC 版本對ON_CONTROL_REFLECT的支持沒問題但如果你從更高版本VS2010拿到的代碼可能用了nullptr、override這些 C11 關(guān)鍵字直接編譯會報錯。常見做法是手動替換成NULL和去掉override。3.2 對話框資源里的替換步驟假設(shè)你已經(jīng)在對話框資源里放了一個 ComboBoxID 為IDC_COMBO_FILTER。右鍵 → 添加變量變量類型填CCheckComboBox變量名比如m_comboFilter。如果類列表里沒有CCheckComboBox說明頭文件沒被包含進(jìn)對話框的頭文件手動加一行#include CheckComboBox.h。然后確認(rèn) ComboBox 的屬性Type 設(shè)為Dropdown或Drop ListOwner Draw設(shè)為Fixed或Variable。如果資源里沒設(shè) Owner Draw自繪不會生效列表項還是默認(rèn)的單行文本。// 在對話框的 OnInitDialog 中初始化 BOOL CMyDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 添加帶復(fù)選框的條目 m_comboFilter.AddString(_T(按狀態(tài)篩選)); m_comboFilter.AddString(_T(按時間篩選)); m_comboFilter.AddString(_T(按關(guān)鍵字篩選)); // 默認(rèn)勾選第一項 m_comboFilter.SetCheck(0, TRUE); // 設(shè)置分隔符為中文頓號 m_comboFilter.SetSeparator(_T(、)); return TRUE; }邏輯說明AddString是繼承自CComboBox的方法添加條目的同時CCheckComboBox內(nèi)部會把狀態(tài)數(shù)組擴(kuò)容并默認(rèn)置為未勾選。SetCheck是新增接口第一個參數(shù)是索引第二個是勾選狀態(tài)。SetSeparator決定收起后文本的拼接方式。參數(shù)說明SetCheck的索引必須小于條目總數(shù)否則可能越界。如果你在AddString之前調(diào)用SetCheck狀態(tài)數(shù)組還沒分配會直接崩。順序不能反。3.3 讀取勾選結(jié)果別用 GetCurSel這是最容易踩的坑。CComboBox::GetCurSel返回的是當(dāng)前高亮項不是勾選項。多選場景下高亮項可能一個都沒勾也可能勾了好幾個但高亮停在別處。正確做法是遍歷狀態(tài)數(shù)組// 獲取所有已勾選項的索引 std::vectorint CMyDlg::GetCheckedIndices() { std::vectorint vecChecked; int nCount m_comboFilter.GetCount(); for (int i 0; i nCount; i) { if (m_comboFilter.GetCheck(i)) vecChecked.push_back(i); } return vecChecked; }邏輯說明GetCheck(i)返回第 i 項的勾選狀態(tài)。遍歷全部條目收集為真的索引。如果你需要文本而不是索引把GetCheck換成先判斷再GetLBText。參數(shù)說明GetCount返回條目總數(shù)循環(huán)邊界用它不要硬編碼。如果列表是動態(tài)增刪的每次讀取前重新GetCount別緩存舊值。4. 避坑與排查五個真實翻車現(xiàn)場4.1 現(xiàn)象列表項點擊后勾選狀態(tài)閃一下就沒了原因在CBN_SELCHANGE里翻轉(zhuǎn)了狀態(tài)但緊接著 ComboBox 默認(rèn)處理又把選擇項重置或者DrawItem里讀的狀態(tài)數(shù)組和寫入的不是同一份。常見于狀態(tài)數(shù)組用了局部變量或者拷貝構(gòu)造沒處理好。解決確認(rèn)狀態(tài)數(shù)組是類的成員變量且在AddString重寫里同步擴(kuò)容。翻轉(zhuǎn)狀態(tài)后調(diào)用Invalidate刷新列表不要依賴 ComboBox 的默認(rèn)重繪。4.2 現(xiàn)象收起后編輯區(qū)顯示的是最后點擊項不是所有勾選項原因文本回顯邏輯寫在了CBN_SELCHANGE里而不是CBN_CLOSEUP。CBN_SELCHANGE每次高亮移動都觸發(fā)此時拼文本只能拿到當(dāng)前項。解決把拼文本的邏輯挪到CBN_CLOSEUP或者單獨(dú)寫一個UpdateText函數(shù)在勾選狀態(tài)變化后和收起時都調(diào)用。4.3 現(xiàn)象VS2008SP1 編譯報錯「error C2065: nullptr 未聲明的標(biāo)識符」原因資源代碼來自更高版本的 Visual Studio用了 C11 關(guān)鍵字。VS2008SP1 默認(rèn)只支持到 C03。解決全局搜索nullptr替換為NULL搜索override直接刪除搜索auto檢查是否用了類型推導(dǎo)VS2008 不支持auto推導(dǎo)。如果代碼里用了std::unique_ptr換成std::auto_ptr或者裸指針加手動delete。4.4 現(xiàn)象下拉列表彈出來后復(fù)選框畫出來了但點擊沒反應(yīng)原因列表窗口沒有被子類化鼠標(biāo)點擊消息被 ComboBox 默認(rèn)的列表窗口過程吃掉了沒走到你的處理函數(shù)。解決在OnDropDown里通過GetComboBoxInfo拿到hwndList然后SubclassWindow或者SetWindowLongPtr掛上自己的窗口過程。注意保存舊的窗口過程指針處理完不關(guān)心的消息要轉(zhuǎn)發(fā)回去。4.5 現(xiàn)象動態(tài)增刪條目后勾選狀態(tài)錯位原因DeleteString刪除條目時狀態(tài)數(shù)組沒有同步刪除對應(yīng)索引后面的狀態(tài)整體前移導(dǎo)致原本勾選第 3 項變成了勾選第 2 項。解決重寫DeleteString在調(diào)用基類刪除之前先m_arrCheck.erase(m_arrCheck.begin() nIndex)。如果一次性清空用ResetContent重寫把狀態(tài)數(shù)組也clear。5. 進(jìn)階把勾選結(jié)果持久化到配置與一個驗證習(xí)慣多選控件的價值最終要落到「用戶選了什么下次打開還在」。我一般會在對話框的OnOK里把勾選索引序列化成字符串寫進(jìn)注冊表或 ini 文件。序列化格式用逗號分隔的索引列表比如0,2,5讀取時解析回std::vectorint再逐項SetCheck。// 保存勾選狀態(tài)到 ini void CMyDlg::SaveCheckedState() { std::vectorint vecChecked GetCheckedIndices(); CString strValue; for (size_t i 0; i vecChecked.size(); i) { CString strIndex; strIndex.Format(_T(%d), vecChecked[i]); if (!strValue.IsEmpty()) strValue _T(,); strValue strIndex; } WritePrivateProfileString(_T(Filter), _T(Checked), strValue, _T(.\\config.ini)); } // 從 ini 恢復(fù)勾選狀態(tài) void CMyDlg::LoadCheckedState() { TCHAR szBuf[256] {0}; GetPrivateProfileString(_T(Filter), _T(Checked), _T(), szBuf, 256, _T(.\\config.ini)); CString strBuf(szBuf); int nPos 0; CString strToken strBuf.Tokenize(_T(,), nPos); while (!strToken.IsEmpty()) { int nIndex _ttoi(strToken); if (nIndex 0 nIndex m_comboFilter.GetCount()) m_comboFilter.SetCheck(nIndex, TRUE); strToken strBuf.Tokenize(_T(,), nPos); } }邏輯說明Tokenize按逗號切分_ttoi轉(zhuǎn)成整數(shù)邊界檢查防止 ini 被手改后越界。SetCheck只置勾選不改變高亮項所以恢復(fù)后編輯區(qū)文本需要手動調(diào)一次UpdateText或者觸發(fā)CBN_CLOSEUP的邏輯。參數(shù)說明WritePrivateProfileString和GetPrivateProfileString是 Win32 API路徑用相對路徑時注意工作目錄建議用GetModuleFileName拼絕對路徑避免從不同快捷方式啟動時讀寫到不同文件。驗證方法我習(xí)慣在SetCheck之后加一個斷言確認(rèn)索引在合法范圍內(nèi)在GetCheckedIndices返回后打印一遍索引列表和界面上肉眼看到的勾選做比對。這個習(xí)慣幫我逮住過好幾次「狀態(tài)數(shù)組和列表項數(shù)量不一致」的玄學(xué)問題——表面看勾選正常實際數(shù)組已經(jīng)越界讀到了臟數(shù)據(jù)。從那以后我每次集成這類自繪控件都強(qiáng)制走一遍「添加條目 → 勾選 → 收起 → 重新打開 → 動態(tài)刪除條目 → 再勾選」的完整鏈路確認(rèn)狀態(tài)數(shù)組和界面始終對齊。希望幫到你。本文還有配套的精品資源點擊獲取