IE7+ 採用單進程多標籤架構 (Single Process, Multiple Tabs),以 CDialogBar 作為標籤容器、自繪 CButton 實現標籤視覺效果,是 MFC 實現瀏覽器標籤欄的經典方案。本文完整解析佈局調整、自繪邏輯、關閉按鈕熱區、命令訊息路由、標籤與 CChildFrame 映射管理。
WM_SIZE 調整佈局WM_PAINT、滑鼠進入/離開關閉區域、點擊通知CTabButton* ↔ CChildFrame*void CTabBar::OnSize(UINT nType, int cx, int cy) {
CDialogBar::OnSize(nType, cx, cy);
CRect rcClient; GetClientRect(rcClient);
int nBarWidth = rcClient.Width();
int nTabWidth = nBarWidth - 120; // 預留工具列空間
int nCount = m_ptrArray.GetCount();
if (nCount * m_nWidth > nTabWidth) {
// 空間不足:平均分配
int nAveWidth = nTabWidth * 1.0 / nCount;
for (int i = 0; i < nCount; i++) {
TABINFO* pTabInfo = (TABINFO*)m_ptrArray.GetAt(i);
CRect rcBtn; pTabInfo->pTabButton->GetClientRect(&rcBtn);
pTabInfo->pTabButton->MoveWindow(
nAveWidth * i,
(rcClient.Height() - rcBtn.Height()) / 2,
nAveWidth, m_nHeight, FALSE);
}
} else {
// 空間充足:固定寬度
for (int i = 0; i < nCount; i++) {
TABINFO* pTabInfo = (TABINFO*)m_ptrArray.GetAt(i);
CRect rcBtn; pTabInfo->pTabButton->GetClientRect(&rcBtn);
pTabInfo->pTabButton->MoveWindow(
m_nWidth * i,
(rcClient.Height() - rcBtn.Height()) / 2,
m_nWidth, m_nHeight, FALSE);
}
}
Invalidate(); // 觸發重繪
}
void CTabButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) {
ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);
CRect rcItem = lpDrawItemStruct->rcItem;
HDC hDC = lpDrawItemStruct->hDC;
CDC dc; dc.Attach(hDC);
dc.SetBkMode(TRANSPARENT);
CString strTitle; GetWindowText(strTitle);
if (m_bSel) { // 選中狀態:三段式背景 (左中右)
m_bmpBmpBkgnd.Draw(dc.m_hDC, CRect(0,0,10, m_bmpBmpBkgnd.GetHeight()),
CRect(m_bmpBmpBkgnd.GetWidth()/4*2, 0, m_bmpBmpBkgnd.GetWidth()/4*2+10, m_bmpBmpBkgnd.GetHeight()));
m_bmpBmpBkgnd.Draw(dc.m_hDC, CRect(4,0,rcItem.right-10, m_bmpBmpBkgnd.GetHeight()),
CRect(m_bmpBmpBkgnd.GetWidth()/4*2+10, 0, m_bmpBmpBkgnd.GetWidth()/4*3-10, m_bmpBmpBkgnd.GetHeight()));
m_bmpBmpBkgnd.Draw(dc.m_hDC, CRect(rcItem.right-10,0,rcItem.right, m_bmpBmpBkgnd.GetHeight()),
CRect(m_bmpBmpBkgnd.GetWidth()/4*3-10, 0, m_bmpBmpBkgnd.GetWidth()/4*3, m_bmpBmpBkgnd.GetHeight()));
// 圖示 + 白色文字
// 關閉按鈕熱區圖示 (滑鼠懸停顯示不同狀態)
} else { // 未選中狀態:單一背景
// 類似繪製,文字白色
}
dc.Detach();
}
關鍵點:三段式位圖 (左端帽、中間重複、右端帽) 適應任意寬度;關閉按鈕僅在 m_bOverCloseButton 時顯示高亮狀態。
// CTabButton::OnMouseMove
// 計算關閉按鈕矩形區域 (右側 16x16 等)
// 設定 m_bOverCloseButton,呼叫 Invalidate() 觸發重繪
問題:CDialogBar 上按鈕的 BN_CLICKED 命令優先發送給主框架 CMainFrame,若主框架無處理函數,按鈕顯示 Disabled 狀態。
解決方案:按鈕自發自訂通知訊息 NM_TBSEL (WM_USER+1) 給 CTabBar,附帶 LPARAM = this (CTabButton*)。
#define NM_TBSEL WM_USER+1
void CTabButton::OnBnClicked() {
CTabBar* pTabBar = (CTabBar*)GetParent();
pTabBar->SendMessage(NM_TBSEL, 0, (LPARAM)this);
}
HRESULT CTabBar::OnNmTbSel(WPARAM wParam, LPARAM lParam) {
CTabButton* pTabButton = (CTabButton*)lParam;
SetCurSel(pTabButton); // 內部更新選中狀態
CChildFrame* pChildFrame = GetChildFrameByTabButton(pTabButton);
pChildFrame->MDIActivate();
pChildFrame->MDIMaximize();
return TRUE;
}
struct TABINFO {
CChildFrame* pChildFrame;
CTabButton* pTabButton;
};
CTabButton* CTabBar::GetTabButtonByChildFrame(CChildFrame* pChildFrame) {
for (int i = 0; i < m_ptrArray.GetCount(); i++) {
TABINFO* pTabInfo = (TABINFO*)m_ptrArray.GetAt(i);
if (pTabInfo->pChildFrame == pChildFrame) return pTabInfo->pTabButton;
}
return NULL;
}
CChildFrame* CTabBar::GetChildFrameByTabButton(CTabButton* pTabButton) {
for (int i = 0; i < m_ptrArray.GetCount(); i++) {
TABINFO* pTabInfo = (TABINFO*)m_ptrArray.GetAt(i);
if (pTabInfo->pTabButton == pTabButton) return pTabInfo->pChildFrame;
}
return NULL;
}
優化建議:大量標籤時改用 CMap<CChildFrame*, CTabButton*> O(1) 查找。

基於 CDialogBar + 自繪 CButton 實現 IE 風格標籤欄,核心在於:
OnSize 動態計算標籤寬度與位置DrawItem 三段式背景 + 關閉按鈕熱區NM_TBSEL 直接送達標籤欄,避免主框架命令路由導致按鈕 DisabledTABINFO 結構維護標籤與子視窗對應此架構可直接擴展至現代瀏覽器標籤功能:拖拽排序、固定標籤、靜音圖示、分組等。