工具列(Toolbar)與狀態列 是 MFC 框架視窗架構中最基礎也最重要的控制條。本文基於 MFC 原始碼,深度剖析 CControlBar、CToolBar、CStatusBar 的類別體系、視窗建立流程、按鈕/分格管理、泊位/浮動佈局算法,以及框架視窗的重新佈局機制。
ToolbarWindow32,狀態列:msctls_statusbar32,ToolTip:tooltips_class32。InitCommonControls() 或 AfxDeferRegisterClass(AFX_WNDCOMMCTLS_REG) 確保 DLL 載入並註冊類別。CCS_TOP(頂部)、CCS_BOTTOM(底部)、TBSTYLE_TOOLTIPS(啟用提示)。
CObject
└── CCmdTarget
└── CWnd
└── CControlBar (基類)
├── CToolBar
├── CStatusBar
├── CDockBar
├── CDialogBar
└── COleResizeBar
CControlBar 實作共性:對齊父視窗邊緣、子項目管理(HWND 或非 HWND)、CBRS_* 樣式(CBRS_TOP/BOTTOM/LEFT/RIGHT, CBRS_ALIGN_ANY, CBRS_FLOAT_MULTI, CBRS_TOOLTIPS, CBRS_FLYBY, CBRS_SIZE_DYNAMIC)。
BOOL CToolBar::Create(CWnd* pParentWnd, DWORD dwStyle, UINT nID)
{
ASSERT_VALID(pParentWnd);
ASSERT(!((dwStyle & CBRS_SIZE_FIXED) && (dwStyle & CBRS_SIZE_DYNAMIC)));
m_dwStyle = dwStyle; // 保存 CBRS_* 樣式
if (nID == AFX_IDW_TOOLBAR) m_dwStyle |= CBRS_HIDE_INPLACE;
dwStyle &= ~CBRS_ALL; // 移除 CBRS_* 避免干擾視窗樣式
dwStyle |= CCS_NOPARENTALIGN | CCS_NOMOVEY | CCS_NODIVIDER | CCS_NORESIZE;
VERIFY(AfxDeferRegisterClass(AFX_WNDCOMMCTLS_REG)); // 確保通用控制項類別已註冊
CRect rect; rect.SetRectEmpty();
return CWnd::Create(TOOLBARCLASSNAME, NULL, dwStyle, rect, pParentWnd, nID);
}
關鍵點:分離視窗樣式與 CBRS_* 控制條樣式,使用 ToolbarWindow32 系統類別建立視窗。
CControlBar::PreCreateWindow 強制加入 WS_CLIPSIBLINGS 防止重繪閃爍;在 Win4+ 系統上根據對齊方向自動轉換邊框樣式(CBRS_BORDER_3D)。
int CControlBar::OnCreate(LPCREATESTRUCT lpcs)
{
if (CWnd::OnCreate(lpcs) == -1) return -1;
if (m_dwStyle & CBRS_TOOLTIPS) EnableToolTips(); // 啟用 ToolTip
CFrameWnd* pFrame = (CFrameWnd*)GetParent();
if (pFrame->IsFrameWnd()) {
m_pDockSite = pFrame;
pFrame->AddControlBar(this); // 加入框架視窗控制條列表
}
return 0;
}
BOOL CToolBar::OnNcCreate(LPCREATESTRUCT lpCreateStruct)
{
if (!CControlBar::OnNcCreate(lpCreateStruct)) return FALSE;
if (m_hWndOwner) DefWindowProc(TB_SETPARENT, (WPARAM)m_hWndOwner, 0);
DefWindowProc(TB_BUTTONSTRUCTSIZE, sizeof(TBBUTTON), 0); // 設定按鈕結構大小
return TRUE;
}
流程:尋找 RT_TOOLBAR 資源 → 鎖定記憶體 → 解析 CToolBarData(版本、寬高、按鈕數、命令 ID 陣列) → SetButtons 建立按鈕 → SetSizes 設定按鈕/圖片尺寸 → LoadBitmap 載入工具列位圖。
BOOL CToolBar::SetButtons(const UINT* lpIDArray, int nIDCount)
{
// 1. 刪除現有按鈕
int nCount = (int)DefWindowProc(TB_BUTTONCOUNT, 0, 0);
while (nCount--) VERIFY(DefWindowProc(TB_DELETEBUTTON, 0, 0));
// 2. 新增按鈕
TBBUTTON btn; memset(&btn, 0, sizeof(TBBUTTON));
int iImage = 0;
for (int i = 0; i < nIDCount; i++) {
btn.fsState = TBSTATE_ENABLED;
if ((btn.idCommand = *lpIDArray++) == 0) { // ID=0 為分隔條
btn.fsStyle = TBSTYLE_SEP;
btn.iBitmap = 8; // 8 像素寬度
} else {
btn.fsStyle = TBSTYLE_BUTTON;
btn.iBitmap = iImage++;
}
if (!DefWindowProc(TB_ADDBUTTONS, 1, (LPARAM)&btn)) return FALSE;
}
m_nCount = (int)DefWindowProc(TB_BUTTONCOUNT, 0, 0);
m_bDelayedButtonLayout = TRUE;
return TRUE;
}
CStatusBar::Create 使用 msctls_statusbar32 類別,ID 為 AFX_IDW_STATUS_BAR (0xE801)。SetIndicators 設定分格 ID 陣列(類似工具列按鈕 ID),內部發送 SB_SETPARTS 訊息。CControlBar 覆寫 DestroyWindow、PostNcDestroy、OnDestroy 統一處理銷毀邏輯;m_bAutoDelete = TRUE 時動態建立的控制條自動 delete this。
虛擬函數,在視窗建立、WM_SIZE、初始化時被呼叫。分兩種情況:
CMiniFrameWnd, FWS_SNAPTOBARS):計算邊框矩形後 SetWindowPos。RepositionBars(0, 0xFFFF, AFX_IDW_PANE_FIRST, reposExtra, &m_rectBorder)。void CWnd::RepositionBars(UINT nIDFirst, UINT nIDLast, UINT nIDLeftOver,
UINT nFlags, LPRECT lpRectParam, LPCRECT lpRectClient, BOOL bStretch)
{
AFX_SIZEPARENTPARAMS layout;
layout.hDWP = (nFlags != reposQuery) ? ::BeginDeferWindowPos(8) : NULL;
layout.rect = lpRectClient ? *lpRectClient : ClientRect;
layout.bStretch = bStretch;
// 1. 發送 WM_SIZEPARENT 給每個控制條,讓其從客戶區扣除自身高度/寬度
for (HWND hWndChild = ::GetTopWindow(m_hWnd); hWndChild; hWndChild = ::GetNextWindow(hWndChild, GW_HWNDNEXT)) {
UINT nIDC = _AfxGetDlgCtrlID(hWndChild);
if (nIDC == nIDLeftOver) { hWndLeftOver = hWndChild; continue; }
if (nIDC >= nIDFirst && nIDC <= nIDLast) {
::SendMessage(hWndChild, WM_SIZEPARENT, 0, (LPARAM)&layout);
}
}
// 2. 剩餘區域給 View (nIDLeftOver)
if (hWndLeftOver) {
CWnd* pLeft = CWnd::FromHandle(hWndLeftOver);
if (nFlags == reposExtra && lpRectParam) { /* 套用邊框保留區 */ }
pLeft->CalcWindowRect(&layout.rect);
AfxRepositionWindow(&layout, hWndLeftOver, &layout.rect);
}
// 3. 統一提交視窗位置
if (layout.hDWP) ::EndDeferWindowPos(layout.hDWP);
}
控制條收到 WM_SIZEPARENT 時,呼叫 CalcDynamicLayout 或 CalcFixedLayout 計算自身所需矩形,從 layout.rect 扣除,並視 layout.hDWP 決定是否立即定位。
WM_SETMESSAGESTRING / WM_POPMESSAGESTRING:MFC 內部訊息,透過框架視窗在狀態列第 0 格顯示命令提示文字(資源 ID 對應字串表)。CFrameWnd::SaveBarState / LoadBarState:將工具列泊位狀態、可見性存入註冊表/INI,實現使用者自訂佈局持久化。// CMainFrame::OnCreate 典型代碼
if (!m_wndToolBar.Create(this) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) return -1;
if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, ...)) return -1;
m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
MFC 控制條架構以 CControlBar 為核心,封裝了 Windows 通用控制項的建立、樣式轉換、佈局協商、持久化等複雜細節。深入理解 Create/PreCreateWindow/OnCreate/OnNcCreate 建立鏈、LoadToolBar/SetButtons 按鈕管理、RecalcLayout/RepositionBars/OnSizeParent 佈局三部曲,對於客製化工具列、修復泊位異常、實現動態增減控制條具有決定性指導意義。