分类:
2008-10-14 14:51:12
实现类似Excel和Visual C++里文件夹式样的标签控制(一)
——实现基本功能
编译/N
众所周知,Excel中一个工作簿可以有多个工作表单(worksheet),每个表单可以通过左下角的标签控制灵活切换(如图一),Visual C++也有类似的控制,如在Output窗口中设置有:Build,Debug,Find in Files和Results等标签控制(如图二)。
图一Excel中的标签控制
图二 Visual C++中的标签控制
我们将这种界面称为文件夹式样的标签控制,以下简称标签控制,而将MFC中的Tab
Control称为标签控件。那么标签控制是如何实现的呢?MFC中有没有现成的控件可以利用?
论坛中有很多人提出过这个问题。看了本文以后,我想这个问题应该有一个圆满的答案。MFC固然给编程带来了极大的方便,但是它并不能代替程序员的编程,MFC只是提供了一个编程框架,应用的实质性代码还是必须由程序员自己来写。同时,MFC的问题也是显而易见的,那就是其GUI素材太丰富,以至于程序员们过分依赖MFC,当想要实现MFC中没有的GUI特性时便不知所措。
下面我们就来看看如何实现图一和图二所示的文件夹式样的标签控制界面。有人可能想到了从现成的标签控件(Tab
Control)入手。但是经验证明:为了使用的方便性和更好的可重用性起见,还是不要采取这种方法。我是一个热衷于可重用性的家伙,但是这方面我们在自己的代码中做得还很不够。所以我宁愿自己创建一个窗口类,这样做还有一个好处是你能完全控制代码的修改,不必顾及因现有控件版本的变化而对自己的代码造成的巨大影响和麻烦。我想微软的家伙肯定也希望你这么做。如果你用Spy++查看一下Excel和Visual C++的界面就会发现其文件夹式样的标签控制并不是SysTabControl32s,而是另外创建的窗口类。为什么我们不也来创建一个呢?
请看图三所示的画面,这就是我编写的一个程序FldrTab,它实现了我们所要的界面功能。实现这个UI的C++类是我自己创建的,它叫CFolderTabCtrl。
图三 FldrTab 程序运行画面
有关CFolderTabCtrl的实现细节请参考源代码。其头文件为Ftab.h,实现文件为Ftab.cpp。在分析CFolderTabCtrl的实现原理之前,让我先来说明一下这个类的使用方法。当FldrTab程序的InitInstance函数获得控制权时,它创建一个主对话框的实例,并运行这个对话框:
BOOL CApp::InitInstance() { CMyDialog dlg; m_pMainWnd = &dlg; dlg.DoModal(); return FALSE; }CMyDialog有两个控制:一个是m_wndStaticInfo,另一个是m_wndFolderTab。顾名思义,第一个控制为一个静态文本窗口,它显示选中的标签,第二个是标签控制本身,即CFolderTabCtrl实例。通过调用SubclassDlgItem,CMyDialog::OnInitDialog以常规方式子类化静态文本,遗憾的是它不能子类化标签控制,因为对话框中并没有实际的标签控制窗口。此外也没有办法借助COM技术将此标签控制实现为一个带运行时接口的定制控件。我的办法是在对话框想要放置标签控制的地方创建一个静态文本控件。如图五所示:
m_wndFolderTab.CreateFromStatic(IDC_FOLDERTAB, this);CFolderTabCtrl::CreateFromStatic 在静态文本控件的位置上创建一个标签控制,然后删除静态文本控件。这是我创建特殊对话框控制常用的绝招,我认为这个诀窍是超一流的。在调用Create之前,CreateFromStatic调用CFolderTab::GetDesiredHeight来获得控制的高度,而忽略静态文本控件的高度。在非对话框应用中不能调用CreateFromStatic;而是要直接调用CFolderTab::Create。创建了标签控制后,接下来你必须设置标签名字。这里是在CMyDialog中调用现成的Load函数。
m_wndFolderTab.Load(IDR_FOLDERTABS);IDR_FOLDERTABS是串资源的ID,它是一个包含新行指示符(“\n”)分割的标签名(“在线杂志第一期\n在线杂志第二期\n……”),一旦创建了控制并调用Load,那么你的标签控制就完全happy了。它看起来就象图三所示的那样。
void CMyDialog::OnChangedTab(NMFOLDERTAB* nmtab,LRESULT* pRes) { CString s; s.Format(_T("选中 %d: %s"),nmtab->iItem,nmtab->pItem->GetText()); m_wndStaticInfo.SetWindowText(s); }NMFOLDERTAB 结构在FTab.h. 文件中定义。
struct NMFOLDERTAB : public NMHDR { int iItem; // 项目索引 const CFolderTab* pItem; // 标签 };这个结构除了NMHDR所包含的成员之外,还有项目索引和指向当前标签CFolderTab的指针,它与CFolderTabCtrl有所不同,从CFolderTab中你可以得到标签的文本。以上就是CFolderTabCtrl的使用方法。
int CFolderTabCtrl::AddItem(LPCTSTR lpszText) { m_lsTabs.AddTail(new CFolderTab(lpszText)); return m_lsTabs.GetCount() - 1; }就这么简单,创建一个新的CFolderTab对象并将它添加到一个列表中。与AddItem相对的是RemoveItem函数,它们的实现都在Ftab.cpp文件中,这两个函数分别负责动态添加和删除标签页,而不是存取资源串。然后是GetItem和GetItemCount函数,一看它们的名字你就应该明白它们的作用,前者用来获取CFolderTab标签的索引号(从0开始),后者则返回m_lsTabs.GetCount,即总共有多少标签。此外,你一定想需要有个函数来获取和设置标签文本,没问题,每一个CFolderTab对象都有一个m_sText成员变量来存储标签名,存取方法是GetText和SetText,我想你闭着眼睛都能写出这些代码!
int x = 0; for (int i=0; iRecomputeLayout为每一个标签调用CFolderTab的成员函数ComputeRgn。ComputeRgn计算出标签的梯形大小并返回算出的宽度,RecomputeLayout将它与当前x轴坐标相加,然后作为下一个标签的起始x轴坐标进行参数传递,最后减去形状修饰因子CXOFFSET,使得它们看起来有重叠的效果。之所以这么做是因为给定的标签只能决定其大小,不能决定其绝对位置,它需要更多的x轴信息。 一旦ComputeRgn有了x轴坐标,它就可以计算出一个足够大的梯形来容纳标签文本,注意要加一些边空,使文本的显示不会产生混乱。用DT_CALCRECT 调用CDC::DrawText计算文本所占的矩形,然后用结果计算梯形的大小。私有函数GetTrapezoid计算与文本矩形相配的梯形。用象素进行计算确实是一件麻烦和头疼的事情,我不想让你为此也痛苦一番。当CFolderTab::ComputeRgn计算出梯形的坐标,它调用CRgn::CreatePolygonRgn函数强行创建一个多边形区域。ComputeRgn(dc, x) - CXOFFSET; }
int CFolderTab::ComputeRgn(CDC& dc, int x) { CRect& rc = m_rect; dc.DrawText(m_sText, &rc, DT_CALCRECT); // tweak rc to add margins …… CPoint pts[4]; GetTrapezoid(rc, pts); m_rgn.CreatePolygonRgn(pts, 4, WINDING); return rc.Width(); }当标签的区域确定后,绘制这些标签的工作使你又陷入另一个艰难的象素处理环境。CFolderTab::Draw对选中标签(选中和未选中状态)要显示的颜色和字体进行处理。因为标签将梯形数据存储在CRgn中,因此只要调用CDC::FillRgn即可绘制标签。然后用MoveTos 和LineTos以适当的颜色绘制线条。最后调用DrawText绘制文本。