展實(shí)戰(zhàn):CToolBarEx在Visual C++中的集成與避坑指南)
簡(jiǎn)介這份資源圍繞MFC擴(kuò)展工具欄類CToolBarEx在Visual C中的應(yīng)用展開(kāi)面向具備一定MFC基礎(chǔ)的Windows客戶端開(kāi)發(fā)者幫助解決標(biāo)準(zhǔn)CToolBar功能單一、界面定制受限的問(wèn)題。壓縮包共30個(gè)文件約43KB以h頭文件與cpp源文件為主體配合ico圖標(biāo)、bmp位圖、rc資源腳本及dsp、dsw工程文件構(gòu)成一套可直接編譯運(yùn)行的完整示例工程。示例程序演示了自定義按鈕圖像、按鈕與下拉菜單關(guān)聯(lián)、圖標(biāo)文字并存以及禁用、隱藏、分組等狀態(tài)管理并展示了AddButton添加按鈕、消息映射綁定、SetButtonInfo調(diào)整樣式等關(guān)鍵用法。已有93人學(xué)習(xí)適合希望提升界面交互性與美觀度的開(kāi)發(fā)者參考通過(guò)閱讀源碼可快速掌握CToolBarEx的集成方式與擴(kuò)展思路。1. 從 TB.rar 里翻出 CToolBarEx一個(gè)被低估的 MFC 工具欄擴(kuò)展類如果你手頭有一個(gè)叫TB.rar的壓縮包里面躺著一份CToolBarEx的源碼而你的開(kāi)發(fā)環(huán)境又是 Visual C不管是 VC6 還是 VS 各版本那你大概率正卡在一個(gè)很具體的需求上標(biāo)準(zhǔn)CToolBar太素了想給它加下拉按鈕、想讓它記住用戶拖拽后的位置、想讓它支持 24 位色圖標(biāo)還不閃。CToolBarEx就是干這個(gè)的——它是對(duì) MFC 原生CToolBar的派生擴(kuò)展把工具欄從“能用”推到“好用”。這篇筆記不聊虛的就按“拿到 TB.rar 之后怎么讀、怎么編、怎么改、怎么避坑”的順序走一遍。適合兩類人一是剛接手老 MFC 項(xiàng)目、被工具欄需求逼到墻角的二是想給現(xiàn)有 VC 工程換一套更靈活工具欄控件的。下面所有步驟都按 Visual C 的常規(guī)工程結(jié)構(gòu)來(lái)不依賴任何外部包管理器。2. CToolBarEx 到底擴(kuò)展了什么從 CToolBar 的四個(gè)硬傷說(shuō)起2.1 原生 CToolBar 在真實(shí)項(xiàng)目里為什么不夠用MFC 自帶的CToolBar從 VC6 時(shí)代就沒(méi)怎么大改過(guò)它的設(shè)計(jì)目標(biāo)只是“把一排按鈕貼到窗口頂部”。一旦項(xiàng)目進(jìn)入真實(shí)交付階段四個(gè)硬傷會(huì)依次暴露。第一按鈕狀態(tài)和圖標(biāo)管理是割裂的SetButtonStyle改的是行為L(zhǎng)oadBitmap管的是外觀想動(dòng)態(tài)換圖標(biāo)得自己維護(hù)CToolBarCtrl的 image list。第二??縟ocking行為依賴CFrameWnd的EnableDocking但工具欄自身的“記憶位置”能力幾乎為零用戶把工具欄拖到左邊下次啟動(dòng)又彈回頂部。第三自定義繪制owner draw支持很弱想做扁平化、hover 高亮、禁用態(tài)灰度得重寫DrawItem并處理NM_CUSTOMDRAW代碼量不小。第四多工具欄并存時(shí)CToolBar之間的換行、伸縮、隱藏邏輯要手動(dòng)算RecalcLayout經(jīng)常算不準(zhǔn)。CToolBarEx的價(jià)值就在于它把這四件事收進(jìn)一個(gè)派生類里。它通常繼承自CToolBar或CToolBarCtrl內(nèi)部持有一個(gè)CImageList管理普通態(tài)、熱態(tài)、禁用態(tài)三套圖標(biāo)重寫OnPaint或DrawItem做自繪并暴露SaveState/LoadState接口把??课恢脤戇M(jìn)注冊(cè)表或 ini。你拿到TB.rar后第一件事不是急著編譯而是先確認(rèn)它繼承自誰(shuí)、用了哪些消息映射。2.2 讀源碼前先確認(rèn)三件事基類、消息映射、資源依賴打開(kāi)TB.rar解壓后的目錄通常能看到ToolBarEx.h、ToolBarEx.cpp可能還有ToolBarEx.rc片段或一張bmp位圖。先看頭文件里的類聲明// ToolBarEx.h 典型結(jié)構(gòu) class CToolBarEx : public CToolBar { DECLARE_DYNAMIC(CToolBarEx) public: CToolBarEx(); virtual ~CToolBarEx(); BOOL CreateEx(CWnd* pParentWnd, DWORD dwCtrlStyle TBSTYLE_FLAT, DWORD dwStyle WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP, CRect rcBorders CRect(1, 1, 1, 1), UINT nID AFX_IDW_TOOLBAR); void SetImageList(UINT nBitmapID, int nButtonWidth, int nButtonHeight); void EnableDockingState(BOOL bSave); // ... protected: CImageList m_imgList; BOOL m_bSaveState; afx_msg void OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };看到public CToolBar就說(shuō)明它是標(biāo)準(zhǔn)派生路線消息映射里大概率有ON_NOTIFY_REFLECT(NM_CUSTOMDRAW, CToolBarEx::OnCustomDraw)。這一步確認(rèn)完你才知道后面改繪制邏輯該動(dòng)哪個(gè)函數(shù)。如果它繼承的是CToolBarCtrl那CreateEx的參數(shù)和??窟壿嫊?huì)不一樣得走CFrameWnd::CreateEx那套。資源依賴也要提前看SetImageList里引用的位圖 ID 是否在你工程的resource.h里存在不存在就得先補(bǔ)資源否則編譯能過(guò)、運(yùn)行直接斷言。2.3 把 TB.rar 掛進(jìn) Visual C 工程的最小步驟假設(shè)你用的是 VS2019/2022工程是 MFC 對(duì)話框或 SDI。步驟不復(fù)雜但順序錯(cuò)了會(huì)報(bào)一堆鏈接錯(cuò)。第一步把ToolBarEx.h和ToolBarEx.cpp復(fù)制到工程目錄在“解決方案資源管理器”里右鍵頭文件/源文件分別添加現(xiàn)有項(xiàng)。第二步打開(kāi)stdafx.h或pch.h確認(rèn)#include afxext.h和#include afxcmn.h已經(jīng)存在CToolBar和CImageList都靠它們。第三步在MainFrm.h里把原來(lái)的CToolBar m_wndToolBar;換成CToolBarEx m_wndToolBar;并#include ToolBarEx.h。第四步改MainFrm.cpp的OnCreate// MainFrm.cpp 中替換原有工具欄創(chuàng)建 if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS, CRect(1, 1, 1, 1), AFX_IDW_TOOLBAR) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0(Failed to create toolbar\n); return -1; } m_wndToolBar.SetImageList(IDB_TOOLBAR24, 24, 24); // 換成你的位圖資源 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(m_wndToolBar);這里CreateEx的dwCtrlStyle傳TBSTYLE_FLAT是為了扁平外觀CBRS_GRIPPER讓用戶能拖動(dòng)CRect(1,1,1,1)是邊框內(nèi)縮避免貼邊太死。SetImageList的 24 是圖標(biāo)寬高必須和位圖里每格實(shí)際像素一致否則圖標(biāo)會(huì)錯(cuò)位或拉伸。第五步編譯。如果報(bào)LNK2001: unresolved external symbol九成是.cpp沒(méi)加進(jìn)工程或者ToolBarEx.cpp里用了某個(gè)你沒(méi)包含的 MFC 頭。提示VC6 工程遷移到高版本 VS 時(shí)TBSTYLE_FLAT等常量仍在但CImageList::Create的ILC_COLOR32支持更好建議在SetImageList內(nèi)部把ILC_COLOR24改成ILC_COLOR32否則高 DPI 下圖標(biāo)邊緣會(huì)有鋸齒。3. 讓 CToolBarEx 跑起來(lái)創(chuàng)建、圖標(biāo)、??颗c狀態(tài)保存3.1 CreateEx 的參數(shù)怎么設(shè)才不翻車CreateEx是CToolBarEx的入口參數(shù)設(shè)錯(cuò)的表現(xiàn)通常是工具欄不顯示、顯示但按鈕全灰、或者一拖動(dòng)就崩。逐個(gè)說(shuō)。pParentWnd傳this主框架別傳NULL。dwCtrlStyle常用TBSTYLE_FLAT想要透明背景加TBSTYLE_TRANSPARENT但后者在 XP 以后視覺(jué)風(fēng)格下容易和主題沖突我一般只保留TBSTYLE_FLAT。dwStyle里WS_CHILD | WS_VISIBLE必須有CBRS_TOP決定初始停靠邊CBRS_GRIPPER給拖拽手柄CBRS_TOOLTIPS開(kāi)提示CBRS_SIZE_DYNAMIC允許用戶改大小。rcBorders四個(gè)值分別是左、上、右、下內(nèi)邊距默認(rèn)(1,1,1,1)夠用如果圖標(biāo)是 32×32建議改成(2,2,2,2)留呼吸感。nID用AFX_IDW_TOOLBAR或自定義唯一 ID多個(gè)工具欄時(shí)不能重復(fù)。一個(gè)常見(jiàn)翻車點(diǎn)CreateEx返回FALSE但沒(méi)打TRACE你以為是資源問(wèn)題其實(shí)是父窗口還沒(méi)創(chuàng)建完。確保在OnCreate里、CFrameWnd的OnCreate返回 0 之前調(diào)用。另一個(gè)dwStyle里漏了CBRS_ALIGN_TOP只寫CBRS_TOP在部分 VS 版本里??啃袨闀?huì)異常兩個(gè)都寫上最穩(wěn)。3.2 三態(tài)圖標(biāo)與 CImageList 的綁定細(xì)節(jié)CToolBarEx通常要求你提供一張包含三組圖標(biāo)的位圖普通態(tài)、熱態(tài)hover、禁用態(tài)。位圖橫向排列每組 N 個(gè)圖標(biāo)每個(gè)圖標(biāo)寬高一致。SetImageList內(nèi)部會(huì)m_imgList.Create然后Add或直接FromHandle。如果你拿到的TB.rar里SetImageList只處理了普通態(tài)那 hover 高亮就得自己補(bǔ)void CToolBarEx::SetImageList(UINT nBitmapID, int nW, int nH) { CBitmap bmp; bmp.LoadBitmap(nBitmapID); BITMAP bm; bmp.GetBitmap(bm); int nCount bm.bmWidth / nW; // 總圖標(biāo)數(shù) int nNormal nCount / 3; // 假設(shè)三等分 m_imgList.Create(nW, nH, ILC_COLOR32 | ILC_MASK, nNormal, 1); m_imgList.Add(bmp, RGB(255, 0, 255)); // 品紅做透明色 GetToolBarCtrl().SetImageList(m_imgList); GetToolBarCtrl().SetHotImageList(m_imgList); // 熱態(tài) GetToolBarCtrl().SetDisabledImageList(m_imgList); bmp.DeleteObject(); }ILC_COLOR32比ILC_COLOR24多一個(gè) alpha 通道PNG 轉(zhuǎn) BMP 時(shí)保留透明更干凈。RGB(255,0,255)是傳統(tǒng)遮罩色如果你的位圖背景是白色改成RGB(255,255,255)。SetHotImageList和SetDisabledImageList在CToolBarCtrl上可用但前提是TBSTYLE_FLAT已設(shè)否則熱態(tài)不觸發(fā)。參數(shù)nW、nH必須和位圖實(shí)際格子一致差一個(gè)像素就會(huì)整體偏移這種問(wèn)題肉眼很難第一時(shí)間發(fā)現(xiàn)建議先用畫(huà)圖工具量一下。3.3 ??繝顟B(tài)保存寫注冊(cè)表還是 ini用戶拖完工具欄下次啟動(dòng)要還原這是CToolBarEx相對(duì)原生最大的體驗(yàn)提升。常見(jiàn)做法是在MainFrm的OnClose里調(diào)m_wndToolBar.SaveState(Software\\MyApp\\ToolBar)在OnCreate里調(diào)LoadState。SaveState內(nèi)部一般用AfxGetApp()-WriteProfileInt或CRegKey寫m_dockBar的m_pDockBar-GetDockState。如果你拿到的源碼沒(méi)實(shí)現(xiàn)可以自己補(bǔ)一個(gè)輕量版void CToolBarEx::SaveState(LPCTSTR lpszSection) { if (!m_bSaveState) return; CString str; str.Format(_T(%d,%d,%d,%d), m_dockBar ? m_dockBar-m_dockStyle : 0, m_rect.left, m_rect.top, m_rect.right); AfxGetApp()-WriteProfileString(lpszSection, _T(BarState), str); } void CToolBarEx::LoadState(LPCTSTR lpszSection) { CString str AfxGetApp()-GetProfileString(lpszSection, _T(BarState)); if (str.IsEmpty()) return; int a, b, c, d; _stscanf_s(str, _T(%d,%d,%d,%d), a, b, c, d); // 根據(jù) a 恢復(fù)??窟卋/c/d 恢復(fù)位置 }m_bSaveState是個(gè)開(kāi)關(guān)默認(rèn)FALSE在EnableDockingState(TRUE)后置真避免調(diào)試時(shí)反復(fù)寫注冊(cè)表。_stscanf_s在 VS2005 以后才安全VC6 用_stscanf。寫注冊(cè)表路徑別用HKEY_LOCAL_MACHINE用HKEY_CURRENT_USER否則非管理員權(quán)限下靜默失敗。如果你更傾向 ini把WriteProfileString換成WritePrivateProfileString路徑用GetModuleFileName拼。4. 避坑與排查CToolBarEx 在 Visual C 里最容易翻車的五件事4.1 現(xiàn)象工具欄顯示但按鈕全是灰的點(diǎn)不動(dòng)原因通常是LoadToolBar加載的資源 ID 和SetImageList的位圖 ID 不匹配或者CToolBarCtrl的按鈕狀態(tài)被SetState誤設(shè)成TBSTATE_INDETERMINATE。解決先確認(rèn)IDR_MAINFRAME對(duì)應(yīng)的TOOLBAR資源里按鈕數(shù)量和位圖圖標(biāo)數(shù)一致多一個(gè)少一個(gè)都會(huì)導(dǎo)致后續(xù)按鈕錯(cuò)位并變灰。然后在OnCreate末尾加m_wndToolBar.GetToolBarCtrl().SetState(ID_XXX, TBSTATE_ENABLED)強(qiáng)制啟用。如果還不行檢查MainFrm的OnUpdate里有沒(méi)有pCmdUI-Enable(FALSE)把命令全禁了。4.2 現(xiàn)象拖動(dòng)工具欄后松手窗口布局錯(cuò)亂或工具欄消失這是RecalcLayout和EnableDocking調(diào)用順序問(wèn)題。必須在DockControlBar之前對(duì)主框架調(diào)EnableDocking(CBRS_ALIGN_ANY)且CToolBarEx自己的EnableDocking也要調(diào)。順序反了m_pDockBar為空SaveState里訪問(wèn)m_dockBar-m_dockStyle直接崩。解決嚴(yán)格按CreateEx→LoadToolBar→SetImageList→EnableDocking→DockControlBar的順序。如果已經(jīng)錯(cuò)亂刪掉注冊(cè)表里保存的狀態(tài)鍵重啟工程。4.3 現(xiàn)象高 DPI 下圖標(biāo)模糊或按鈕間距異常MFC 老工程默認(rèn)不感知 DPICToolBarEx里寫死的 24×24 在 150% 縮放下會(huì)被系統(tǒng)拉伸。解決在MainFrm構(gòu)造函數(shù)里加SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)需windows.h和 Win10 SDK然后把SetImageList的寬高改成MulDiv(24, GetDpiForWindow(), 96)。如果不想動(dòng) DPI至少把ILC_COLOR24換成ILC_COLOR32并讓位圖本身是 32 位模糊會(huì)減輕。注意SetProcessDpiAwarenessContext必須在任何窗口創(chuàng)建前調(diào)用放在InitInstance最前面。4.4 現(xiàn)象編譯報(bào)error C2065: TBSTYLE_FLAT : undeclared identifier這是頭文件包含順序問(wèn)題。TBSTYLE_FLAT定義在commctrl.h里MFC 工程通常由afxcmn.h間接引入。如果你在stdafx.h里先包含了ToolBarEx.h再包含afxcmn.h就會(huì)報(bào)未聲明。解決把#include afxcmn.h提到ToolBarEx.h之前或者直接在ToolBarEx.h頂部加#include afxcmn.h。VC6 下TBSTYLE_FLAT需要_WIN32_IE 0x0300在stdafx.h里#define _WIN32_IE 0x0500再包含。4.5 現(xiàn)象Release 版正常Debug 版一運(yùn)行就斷言ASSERT(::IsWindow(m_hWnd))CToolBarEx的某個(gè)成員函數(shù)在窗口句柄還沒(méi)創(chuàng)建時(shí)被調(diào)用了常見(jiàn)于SetImageList在CreateEx之前執(zhí)行或者LoadState在OnCreate里調(diào)得太早。解決把SetImageList和LoadState都挪到CreateEx成功返回之后。如果LoadState里需要訪問(wèn)m_dockBar確保DockControlBar已經(jīng)執(zhí)行。Debug 斷言是幫你定位順序問(wèn)題的別用#ifdef NDEBUG繞過(guò)去繞了 Release 下就是隨機(jī)崩。5. 進(jìn)階給 CToolBarEx 加下拉按鈕和動(dòng)態(tài)換膚的實(shí)操技巧下拉按鈕是CToolBarEx最常被要求加的功能原生CToolBar做起來(lái)很別扭。思路是給某個(gè)按鈕設(shè)TBSTYLE_DROPDOWN然后在MainFrm里處理ON_NOTIFY(TBN_DROPDOWN, ...)彈出菜單。具體做法在LoadToolBar之后用GetToolBarCtrl().SetButtonStyle或SetButtonInfo把目標(biāo)按鈕的 style 改成TBSTYLE_DROPDOWN | TBSTYLE_FLAT然后在MainFrm消息映射里加ON_NOTIFY(TBN_DROPDOWN, AFX_IDW_TOOLBAR, CMainFrame::OnToolbarDropdown)。處理函數(shù)里用TrackPopupMenu在按鈕下方彈菜單坐標(biāo)從pNMHDR的rc取。注意TBN_DROPDOWN的idFrom是工具欄 ID不是按鈕 ID按鈕 ID 在pnm-hdr.idFrom里別搞混。動(dòng)態(tài)換膚是另一個(gè)高頻需求。CToolBarEx如果已經(jīng)重寫了OnCustomDraw換膚就簡(jiǎn)單在OnCustomDraw里根據(jù)當(dāng)前主題變量決定clrText、clrBk、clrBtnFace。我一般會(huì)加一個(gè)SetTheme(int nTheme)接口內(nèi)部改幾個(gè)COLORREF成員然后Invalidate()。如果源碼沒(méi)走NM_CUSTOMDRAW而是走DrawItem那就重寫DrawItem并調(diào)CDC::FillSolidRect和DrawState。換膚時(shí)最容易忽略的是禁用態(tài)圖標(biāo)SetDisabledImageList如果沒(méi)跟著換禁用按鈕還是舊主題的灰度圖視覺(jué)上很割裂。建議把三態(tài)圖標(biāo)都做成資源換膚時(shí)一起SetImageList。驗(yàn)證換膚是否生效別只看主界面把工具欄拖成浮動(dòng)窗口再看一遍浮動(dòng)狀態(tài)下m_pDockBar為空部分繪制分支會(huì)走不同路徑。我自己的習(xí)慣是每次改完CToolBarEx的繪制邏輯先跑 Debug 版拖三次頂部、左側(cè)、浮動(dòng)再跑 Release 版拖三次確認(rèn)沒(méi)有斷言、沒(méi)有殘影、沒(méi)有圖標(biāo)錯(cuò)位。這套流程幫我省過(guò)至少兩次上線后的緊急回滾。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取