IE浏覽器的多標簽模式已日趨占據浏覽器市場的主流模式。回憶IE6.0時代的單文檔多實例年代,那浏覽多網頁是何等的痛苦。原本有限的空間就要被那些煩瑣的網頁所占據,要從這些煩瑣的網頁中切換到自己目的網頁更是何等的不便。現在很多第三方IE浏覽器對IE浏覽器所顯示出來的弊病虎視眈眈許久,多標簽浏覽器也應運而生。遨游、世界之窗、TT等如今都是拜多標簽浏覽器之福,早早占領了市場,占據了一席之地。而如今微軟也知道自己浏覽器帝國的根基也岌岌可危,其怎可示弱,IE7.0也就相繼問世。
IE多標簽欄的主要特點是:單實例多文檔模式,文檔間的切換是通過標簽實現。一個實例就可以包容多個文檔,察看網頁是何其的方便。
構成IE多標簽欄的界面要素包括工具欄和標簽。工具欄采用CDialogBar做為標簽的容器,標簽采用自繪按鈕來實現,CtabCtrl做標簽不容易實現自繪(自繪的時候有灰色的border出現)。
在CDialogBar的寬度發生改變的時候,其上面的按鈕標簽應做適當的調整。當然在足夠容納按鈕標簽的時,可以給一個設定值。若空間有限的話,則就適當縮小標簽的大小,其上的內容通過提示框提示。所以只要響應CdialogBar的WM_SIZE來調整標簽的擺放方式。至於標簽欄的自繪通過響應WM_PAINT消息就可以做到。
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++)
{
CRect rcBtn;
TABINFO* pTabInfo=(TABINFO*)m_ptrArray.GetAt(i);
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++)
{
CRect rcBtn;
TABINFO* pTabInfo=(TABINFO*)m_ptrArray.GetAt(i);
pTabInfo->pTabButton->GetClientRect(&rcBtn);
pTabInfo->pTabButton->MoveWindow(m_nWidth*i,
(rcClient.Height()-rcBtn.Height())/2,
m_nWidth, m_nHeight, FALSE);
}
}
Invalidate();
}
當選中標簽後,鼠標移動到標簽右邊的時候,則會出現一個關閉按鈕,用來關閉文檔。所以這個裡面涉及到按鈕的自繪原理,這樣的文章比較多,這裡就不多做說明。不過俺想提個問題,對於按鈕的0nDrawItem與DrawItem,是否有什麼區別,如果將下面換成OnDrawItem消息響應函數是否可以?實在不懂得的話可以去跟蹤CButton的源代碼,就知道了。
void CTabButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
ASSERT(lpDrawItemStruct->CtlType== ODT_BUTTON);
CRect rcItem = lpDrawItemStruct->rcItem;
HWND hWnd = lpDrawItemStruct->hwndItem;
UINT nState = lpDrawItemStruct->itemState;
HDC hDC = lpDrawItemStruct->hDC;
CDC dc;
dc.Attach(hDC);
dc.SetBkMode(TRANSPARENT);
CString strTitle;
GetWindowText(strTitle);
if (m_bSel)
{
if(!m_bmpBmpBkgnd.IsNull())
{
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()));
}
if(!m_bmpIcon.IsNull())
{
m_bmpIcon.Draw(dc.m_hDC, CRect(4, 4, 4+m_bmpIcon.GetWidth(), 4+m_bmpIcon.GetHeight()),
CRect(0,0,m_bmpIcon.GetWidth(), m_bmpIcon.GetHeight()));
}
CRect rcLabel;
rcLabel.left=rcItem.left+4*2+16;
rcLabel.top = rcItem.top;
rcLabel.right=rcItem.right - (4*2+16);
rcLabel.bottom=rcItem.bottom;
dc.SetTextColor(RGB(0xff,0xff,0xff));
dc.DrawText(strTitle, &rcLabel, DT_SINGLELINE | DT_VCENTER | DT_WORD_ELLIPSIS);
if(m_bOverCloseButton)
{
if(!m_bmpClose.IsNull())
{
m_bmpClose.Draw(dc.m_hDC,
CRect(rcItem.Width()-m_bmpClose.GetWidth()/3-5,
(rcItem.Height()-m_bmpClose.GetHeight())/2,rcItem.Width()-2,
(rcItem.Height()-m_bmpClose.GetHeight())/2+m_bmpClose.GetHeight()),
CRect(m_bmpClose.GetWidth()/3, 0, m_bmpClose.GetWidth()/3*2, m_bmpClose.GetHeight()));
}
}
else
{
if(!m_bmpClose.IsNull())
{
m_bmpClose.Draw(dc.m_hDC, CRect(rcItem.Width()-m_bmpClose.GetWidth()/3-5,
(rcItem.Height()-m_bmpClose.GetHeight())/2,rcItem.Width()-2,
(rcItem.Height()-m_bmpClose.GetHeight())/2+m_bmpClose.GetHeight()),
CRect(0, 0, m_bmpClose.GetWidth()/3, m_bmpClose.GetHeight()));
}
}
}
else
{
if(!m_bmpBmpBkgnd.IsNull())
{
m_bmpBmpBkgnd.Draw(dc.m_hDC, CRect(0,0,10, m_bmpBmpBkgnd.GetHeight()),
CRect(0, 0, 10, m_bmpBmpBkgnd.GetHeight()));
m_bmpBmpBkgnd.Draw(dc.m_hDC, CRect(10,0,rcItem.right-10, m_bmpBmpBkgnd.GetHeight()),
CRect(10, 0, m_bmpBmpBkgnd.GetWidth()/4-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-10, 0, m_bmpBmpBkgnd.GetWidth()/4, m_bmpBmpBkgnd.GetHeight()));
}
if(!m_bmpIcon.IsNull())
{
m_bmpIcon.Draw(dc.m_hDC, CRect(4, 4, 4+m_bmpIcon.GetWidth(), 4+m_bmpIcon.GetHeight()),
CRect(0,0,m_bmpIcon.GetWidth(), m_bmpIcon.GetHeight()));
}
CRect rcLabel;
rcLabel.left=rcItem.left + 4*2+16;
rcLabel.top = rcItem.top;
rcLabel.right=rcItem.right - (4*2+16);
rcLabel.bottom=rcItem.bottom;
dc.SetTextColor(RGB(0xff,0xff,0xff));
dc.DrawText(strTitle, &rcLabel, DT_SINGLELINE | DT_VCENTER | DT_WORD_ELLIPSIS);
}
dc.Detach();
}
值得注意的是,CdialogBar的按鈕命令消息是先發給主框架窗口處理,如果主框架窗口沒有為其提供相應的響應函數,則命令的消息路由就中斷,按鈕則會出現disabled狀態。或者你也可以獲取到這個命令消息後,重載OnCmdMsg再將這個命令消息發往其他窗口。然後就可以通過按鈕的消息反射機制,使按鈕能夠自己處理自己的事件,比較貼近面向對象的設計。由於命令消息響應函數裡面沒有將發命令消息的對象傳到CtabBar中,這樣CtabBar如果靠按鈕ID來設置主鍵唯一標志一個標簽按鈕的話,那那就會占用很多的ID。本設計並沒有這麼做,在給ctabbar提供相的相關通知,是在裡面發送一個通知消息到ctabbar的,其中LPARAM就可以保存按鈕標簽對象。這樣標簽欄就可以實現對底下標簽的操作。以下就是按鈕發送通知消息給標簽欄的實現細節。
#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;
}
總體的設計思想是:標簽欄應該用一個數組來存放標簽,但由於一個標簽又是跟一個CchildFrame一一對應起來,兩者應該可以相互訪問,效率比較高的話可以用cmap映射來實現。本設計采用一個結構體來存儲這兩個關鍵要素。typedef struct _TABINFO
{
CChildFrame* pChildFrame;
CTabButton* pTabButton;
}TABINFO, *PTABINFO;
這個標簽欄就可以通過一個數組維護這樣的結構。至於CchildFrame和CtabButton的對應關系可以通過下面來實現,不過是個輪詢過程,的確很浪費時間。CTabButton* CTabBar::GetTabButtonByChildFrame(CChildFrame* pChildFrame)
{
int nCount=m_ptrArray.GetCount();
for(int i=0; i<nCount; i++)
{
TABINFO* pTabInfo=(TABINFO*)m_ptrArray.GetAt(i);
if(pTabInfo->pChildFrame==pChildFrame)
{
return pTabInfo->pTabButton;
}
else
{
continue;
}
}
return NULL;
}
CChildFrame* CTabBar::GetChildFrameByTabButton(CTabButton* pTabButton)
{
int nCount=m_ptrArray.GetCount();
for(int i=0; i<nCount; i++)
{
TABINFO* pTabInfo=(TABINFO*)m_ptrArray.GetAt(i);
if(pTabInfo->pTabButton==pTabButton)
{
return pTabInfo->pChildFrame;
}
else
{
continue;
}
}
return NULL;
}
以下是我實現的浏覽器的一個截圖,並附有一個vs2003的demo,具體細節可以察看這個demo。
本文配套源碼