基于CDialogBar的IE多标签栏的实现

来源:岁月联盟 编辑:zhu 时间:2009-03-12

  本文源代码下载地址:

  http://flashview.ddvip.com/2009_02/TabBarDemo.rar

  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。

基于CDialogBar的IE多标签栏的实现