WTL for MFC Programmers, Part VII - Splitter Windows

原作 :Michael Dunn [英文原文]
翻译 :Orbit(桔皮干了) [http://www.winmsg.com/cn/orbit.htm]

下载演示程序代码

本章内容

介绍

随着使用两个分隔的视图管理文件系统的资源管理器在Windows 95中第一次出现,分隔窗口逐渐成为一种流行的界面元素。MFC也有一个复杂的功能强大的分隔窗口类,但是要掌握它的用法确实有点难,并且它和文档/视图框架联系紧密。在第七章我将介绍WTL的分隔窗口,它比MFC的分隔窗口要简单一些。WTL的分隔窗口没有MFC那么多特性,但是易于使用和扩展。

本章的例子工程是用WTL重写的ClipSpy,如果你对这个程序不太熟悉,现在可以快速浏览一下本章内容,因为我只是复制了ClipSpy的功能而没用深入的解释它是如何工作的,毕竟这篇文章的重点是分隔窗口,不是剪贴板。

WTL 的分隔窗口

头文件atlsplit.h含有所有WTL的分隔窗口类,一共有三个类:CSplitterImpl,CSplitterWindowImpl和CSplitterWindowT,不过你通常只会用到其中的一个。下面将介绍这些类和它们的基本方法。

相关的类

CSplitterImpl是一个有两个参数的模板类,一个是窗口界面类的类名,另一个是布尔型变量表示分隔窗口的方向:true表示垂直方向,false表示水平方向。CSplitterImpl类包含了几乎所有分隔窗口的实现代码,它的许多方法是可重载的,重载这些方法可以自己绘制分隔条的外观或者实现其它的效果。CSplitterWindowImpl类是从CWindowImpl和CSplitterImpl两个类派生出来的,但是它的代码不多,有一个空的WM_ERASEBKGND消息处理函数和一个WM_SIZE处理函数用于重新定位分隔窗口。

最后一个是CSplitterWindowT类,它从CSplitterImpl类派生,它的窗口类名是“WTL_SplitterWindow”。还有两个自定义数据类型通常用来取代上面的三个类:CSplitterWindow用于垂直分隔窗口,CHorSplitterWindow用于水平分隔窗口。

创建分割窗口

由于CSplitterWindow是从CWindowImpl类派生的,所以你可以像创建其他子窗口那样创建分隔窗口。分隔窗口将存在于整个主框架窗口的生命周期,应该在CMainFrame类添加一个CSplitterWindow类型的变量。在CMainFrame::OnCreate()函数内,你可以将分隔窗口作为主窗口的子窗口创建,然后将其设置为主窗口的客户区窗口:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
// ...
const DWORD dwSplitStyle = WS_CHILD | WS_VISIBLE | 
                             WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
            dwSplitExStyle = WS_EX_CLIENTEDGE;
 
    m_wndSplit.Create ( *this, rcDefault, NULL, 
                        dwSplitStyle, dwSplitExStyle );
 
    m_hWndClient = m_wndSplit;
}

创建分隔窗口之后,你就可以为每个窗格指定窗口或者做其他必要的初始化工作。

基本方法

bool SetSplitterPos(int xyPos = -1, bool bUpdate = true)
int GetSplitterPos()

可以调用SetSplitterPos()函数设置分隔条的位置,这个位置表示分割条距离分隔窗口的上边界(水平分隔窗口)或左边界(垂直分隔窗口)有多少个象素点。你可以使用默认值-1将分隔条设置到分隔窗口的中间,使两个窗格大小相同,通常传递true给bUpdate参数表示在移动分隔条之后相应的改变两个窗格的大小。GetSplitterPos()返回当前分隔条的位置,这个位置也是相对于分隔窗口的上边界或左边界。

bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE)
int GetSinglePaneMode()

调用SetSinglePaneMode()函数可以改变分隔窗口的模式使单窗格模式还是双窗格模式,在单窗格模式下,只有一个窗格使可见的并且隐藏了分隔条,这和MFC的动态分隔窗口相似(只是没有那个小钳子形状的手柄,它用于重新分隔分隔窗口)。对于nPane参数可用的值是SPLIT_PANE_LEFT,SPLIT_PANE_RIGHT,SPLIT_PANE_TOP,SPLIT_PANE_BOTTOM,和SPLIT_PANE_NONE,前四个指示显示那个窗格(例如,使用SPLIT_PANE_LEFT参数将显示左边的窗格,隐藏右边的窗格),使用SPLIT_PANE_NONE表示两个窗格都显示。GetSinglePaneMode()返回五个SPLIT_PANE_*值中的一个表示当前的模式。

DWORD SetSplitterExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetSplitterExtendedStyle()

分隔窗口有自己的样式用于控制当整个分隔窗口改变大小时如何移动分隔条。有以下几种样式:

如果既没有指定SPLIT_PROPORTIONAL,也没有指定SPLIT_RIGHTALIGNED/SPLIT_BOTTOMALIGNED,则分隔窗口会变成左对齐或上对齐。如果将SPLIT_PROPORTIONAL和SPLIT_RIGHTALIGNED/SPLIT_BOTTOMALIGNED一起使用,则优先选用SPLIT_PROPORTIONAL样式。

还有一个附加的样式用来控制分隔条是否可以被用户移动:

扩展样式的默认值是 SPLIT_PROPORTIONAL。

bool SetSplitterPane(int nPane, HWND hWnd, bool bUpdate = true)
void SetSplitterPanes(HWND hWndLeftTop, HWND hWndRightBottom, bool bUpdate = true)
HWND GetSplitterPane(int nPane)

可以调用SetSplitterPane()为分隔窗口的窗格指派子窗口,nPane是一个SPLIT_PANE_*类型的值,表示设置拿一个窗格。hWnd是子窗口的窗口句柄。你可以使用SetSplitterPane()将一个子窗口同时指定给两个窗格,对于bUpdate参数通常使用默认值,也就是告诉分隔窗口立即调整子窗口的大小以适应窗格的大小。可以调用GetSplitterPane()得到某个窗格的子窗口句柄,如果窗格没有指派子窗口则GetSplitterPane()返回NULL。

bool SetActivePane(int nPane)
int GetActivePane()

SetActivePane()函数将分隔窗口中的某个子窗口设置为当前焦点窗口,nPane是SPLIT_PANE_*类型的值,表示需要激活哪个窗格,这个函数还可以设置默认的活动窗格(后面介绍)。GetActivePane()函数查看所有拥有焦点的窗口,如果拥有焦点的窗口是窗格或窗格的子窗口就返回一个SPLIT_PANE_*类型的值,表示是哪个窗格。如果当前拥有焦点的窗口不是窗格的子窗口,那么GetActivePane()返回SPLIT_PANE_NONE。

bool ActivateNextPane(bool bNext = true)

如果分隔窗口是单窗格模式,焦点被设到可见的窗格上,否则的话,ActivateNextPane()函数将调用GetActivePane()查看拥有焦点的窗口。如果一个窗格(或窗格内的子窗口)拥有检点,分隔窗口就将焦点设给另一个窗格,否则ActivateNextPane()将判断bNext的值,如果是true就激活left/top窗格,如果是false则激活right/bottom窗格。

bool SetDefaultActivePane(int nPane)
bool SetDefaultActivePane(HWND hWnd)
int GetDefaultActivePane()

调用SetDefaultActivePane()函数可以设置默认的活动窗格,它的参数可以是SPLIT_PANE_*类型的值,也可以是窗口的句柄。如果分隔窗口自身得到的焦点,可以通过调用SetFocus()将焦点转移给默认窗格。GetDefaultActivePane()函数返回SPLIT_PANE_*类型的值表示哪个窗格是当前默认的活动窗格。

void GetSystemSettings(bool bUpdate)

GetSystemSettings()读取系统设置并相应的设置数据成员。分隔窗口在OnCreate()函数中自动调用这个函数,你不需要自己调用这个函数。当然,你的主框架窗口应该响应WM_SETTINGCHANGE并将它传递给分隔窗口, CSplitterWindow在WM_SETTINGCHANGE消息的处理函数中调用GetSystemSettings()。传递true给bUpdate参数,分隔窗口会根据新的设置重画自己。

数据成员

其他的一些特性可以通过直接访问CSplitterWindow的公有成员来设定,只要GetSystemSettings()被调用了,这些公有成员也会相应的被重置。

m_cxySplitBar:控制分隔条的宽度(垂直分隔条)和高度(水平分隔条)。默认值是通过调用GetSystemMetrics(SM_CXSIZEFRAME)(垂直分隔条)或GetSystemMetrics(SM_CYSIZEFRAME)(水平分隔条)得到的。

m_cxyMin:控制每个窗格的最小宽度(垂直分隔)和最小高度(水平分隔),分隔窗口不允许拖动比这更小的宽度或高度。如果分隔窗口有WS_EX_CLIENTEDGE扩展属性,则这个变量的默认值是0,否则其默认值是2*GetSystemMetrics(SM_CXEDGE)(垂直分隔)或2*GetSystemMetrics(SM_CYEDGE)(水平分隔)。

m_cxyBarEdge:控制画在分隔条两侧的3D边界的宽度(垂直分隔)或高度(水平分隔),其默认值刚好和m_cxyMin相反。

m_bFullDrag:如果是true,当分隔条被拖动时窗格大小跟着调整,如果是false,拖动时只显示一个分隔条的影子,直到拖动停止才调整窗格的大小。默认值是调用SystemParametersInfo(SPI_GETDRAGFULLWINDOWS)函数的返回值。

开始一个例子工程

既然我们已经对分隔窗口有了基本的了解,我们就来看看如何创建一个包含分隔窗口的框架窗口。使用WTL向导开始一个新工程,在第一页选择SDI Application并单击Next,在第二页,如下图所示取消工具条并选择不使用视图窗口:

 [AppWizard pg 2 - 22K]

我们不使用分隔窗口是因为分隔窗口和它的窗格将作为“视图窗口”,在CMainFrame类中添加一个CSplitterWindow类型的数据成员:

class CMainFrame : public ...
{
//...
protected:
    CSplitterWindow  m_wndVertSplit;
};

接着在OnCreate()中创建分隔窗口并将其设为视图窗口:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
    // Create the splitter window
const DWORD dwSplitStyle = WS_CHILD | WS_VISIBLE |
                           WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
            dwSplitExStyle = WS_EX_CLIENTEDGE;
 
    m_wndVertSplit.Create ( *this, rcDefault, NULL,
                            dwSplitStyle, dwSplitExStyle );
 
    // Set the splitter as the client area window, and resize
    // the splitter to match the frame size.
    m_hWndClient = m_wndVertSplit;
    UpdateLayout();
 
    // Position the splitter bar.
    m_wndVertSplit.SetSplitterPos ( 200 );
 
    return 0;
}

需要注意的是在设置分隔窗口的位置之前要先设置m_hWndClient并调用CFrameWindowImpl::UpdateLayout()函数,UpdateLayout()将分隔窗口设置为初始时的大小。如果跳过这一步,分隔窗口的大小将不确定,可能小于200个象素点的宽度,最终导致SetSplitterPos()出现意想不到的结果。还有一种不调用UpdateLayout()函数的方,就是先得到框架窗口的客户区坐标,然后使用这个客户区坐标替换rcDefault坐标创建分隔窗口。使用这种方式创建的分隔窗口一开始就在正确的初始位置上,随后对位置调整的函数(例如 SetSplitterPos())都可以正常工作。

现在运行我们的程序就可以看到分隔条,即使没有创建任何窗格窗口它仍具有基本的行为。你可以拖动分隔条,用鼠标双击分隔条使其移到窗口的中间位置。

 [Empty splitter - 4K]

为了演示分隔窗口的不同使用方法,我将使用一个CListViewCtrl派生类和一个简单的CRichEditCtrl,下面是从CClipSpyListCtrl类摘录的代码,我们在左边的窗格使用这个类:

typedef CWinTraitsOR<LVS_REPORT | LVS_SINGLESEL | LVS_NOSORTHEADER>
          CListTraits;
 
class CClipSpyListCtrl :
    public CWindowImpl<CClipSpyListCtrl, CListViewCtrl, CListTraits>,
    public CCustomDraw<CClipSpyListCtrl>
{
public:
    DECLARE_WND_SUPERCLASS(NULL, WC_LISTVIEW)
 
    BEGIN_MSG_MAP(CClipSpyListCtrl)
        MSG_WM_CHANGECBCHAIN(OnChangeCBChain)
        MSG_WM_DRAWCLIPBOARD(OnDrawClipboard)
        MSG_WM_DESTROY(OnDestroy)
        CHAIN_MSG_MAP_ALT(CCustomDraw<CClipSpyListCtrl>, 1)
        DEFAULT_REFLECTION_HANDLER()
    END_MSG_MAP()
//...
};

如果你看过前面的几篇文章就会很容易读懂这个类的代码。它响应WM_CHANGECBCHAIN消息,这样就可以知道是否启动和关闭了其它剪贴板查看程序,它还响应WM_DRAWCLIPBOARD消息,这样就可以知道剪贴板的内容是否改变。

由于分隔窗口窗格内的子窗口在程序运行其间一直存在,我们也可以将它们设为CMainFrame类的成员:

class CMainFrame : public ...
{
//...
protected:
    CSplitterWindow  m_wndVertSplit;
    CClipSpyListCtrl m_wndFormatList;
    CRichEditCtrl    m_wndDataViewer;
};

创建一个窗格内的窗口

既然已经有了分隔窗口和子窗口的成员变量,填充分隔窗口就是一件简单的事情了。先创建分隔窗口,然后创建两个子窗口,使用分隔窗口作为它们的父窗口:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
    // Create the splitter window
const DWORD dwSplitStyle = WS_CHILD | WS_VISIBLE |
                           WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
            dwSplitExStyle = WS_EX_CLIENTEDGE;
 
    m_wndVertSplit.Create ( *this, rcDefault, NULL,
                            dwSplitStyle, dwSplitExStyle );
 
    // Create the left pane (list of clip formats)
    m_wndFormatList.Create ( m_wndVertSplit, rcDefault );
 
    // Create the right pane (rich edit ctrl)
const DWORD dwRichEditStyle = 
              WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
              ES_READONLY | ES_AUTOHSCROLL | ES_AUTOVSCROLL | ES_MULTILINE;
 
    m_wndDataViewer.Create ( m_wndVertSplit, rcDefault, 
                             NULL, dwRichEditStyle );
    m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
 
    // Set the splitter as the client area window, and resize
    // the splitter to match the frame size.
    m_hWndClient = m_wndVertSplit;
    UpdateLayout();
 
    m_wndVertSplit.SetSplitterPos ( 200 );
 
    return 0;
}

注意两个类的Create()函数都用m_wndVertSplit作为父窗口,RECT参数无关紧要,因为分隔窗口会重新调整它们的大小,所以可以使用CWindow::rcDefault。

最后就是将窗口的句柄传递给分隔窗口的窗格,这一步也需要在UpdateLayout()调用之前完成,这样最终所有的窗口都有正确的大小。

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
    m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
 
    // Set up the splitter panes
    m_wndVertSplit.SetSplitterPanes ( m_wndFormatList, m_wndDataViewer );
 
    // Set the splitter as the client area window, and resize
    // the splitter to match the frame size.
    m_hWndClient = m_wndVertSplit;
    UpdateLayout();
 
    m_wndVertSplit.SetSplitterPos ( 200 );
 
    return 0;
}

现在,list控件上增加了几栏,结果看起来是这个样子:

 [Splitter w/panes - 4K]

需要注意的是分隔窗口对放进窗格的窗口类型没有限制,不像MFC那样必须是CView的派生类。窗格窗口只要有WS_CHILD样式就行了,没有任何其他限制。

消息处理

由于在主框架窗口和我们的窗格窗口之间加了一个分隔窗口,你可能想知道现在通知消息是如何工作的,比如,主框架窗口是如何收到NM_CUSTOMDRAW通知消息并将它反射给list控件的?答案就在CSplitterWindowImpl的消息链中:

  BEGIN_MSG_MAP(thisClass)
    MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
    MESSAGE_HANDLER(WM_SIZE, OnSize)
    CHAIN_MSG_MAP(baseClass)
    FORWARD_NOTIFICATIONS()
  END_MSG_MAP()

最后的哪个FORWARD_NOTIFICATIONS()宏最重要,回忆一下第四章,有一些通知消息总是被发送的子窗口的父窗口,FORWARD_NOTIFICATIONS()就是做了这些工作,它将这些消息转发给分隔窗口的父窗口。也就是说,当list窗口发送一个WM_NOTIFY消息给分隔窗口时(它是list的父窗口),分隔窗口就将这个WM_NOTIFY消息转发给主框架窗口(它是分隔窗口的父窗口)。当主框架窗口反射回消息时会将消息反射给WM_NOTIFY消息的最初发送者,也就是list窗口,所以分隔窗口并没有参与消息反射。

在list窗口和主框架窗口之间的这些消息传递并不影响分隔窗口的工作,这使得在程序中添加和移除分隔窗口非常容易,因为子窗口不需要做任何改变就可以继续工作。

窗格容器

WTL还有一个被称为窗格容器的构件,它就像Explorer中左边的窗格那样,顶部有一个可以显示文字的区域,还有一个可选择是否显示的Close按钮:

 [Explorer pane container - 3K]

就像分隔窗口管理两个窗格窗口一样,这个窗格容器也管理一个子窗口,当容器窗口的大小改变时,子窗口也相应的改变大小以便能够填充容器窗口的内部空间。

相关的类

这个窗格容器的实现需要两个类:CPaneContainerImpl和CPaneContainer,它们都在atlctrlx.h中声明。CPaneContainerImpl是一个CWindowImpl派生类,它含有窗格容器的完整实现,CPaneContainer只是提供了一个类名,除非重载CPaneContainerImpl的方法或改变容器的外观,一般使用CPaneContainer就够了。

基本方法

HWND Create(
    HWND hWndParent, LPCTSTR lpstrTitle = NULL,
    DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
    DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)
HWND Create(
    HWND hWndParent, UINT uTitleID,
    DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
    DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)

创建一个CPaneContainer窗口和创建其它子窗口一样。有两个Create()函数,它们的区别仅仅是第二个参数不同。第一个函数需要传递一个字符串作为容器顶部区域显示的文字,第二个参数需要需要传一个字符串的资源ID,其他参数只要使用默认值就行了。

DWORD SetPaneContainerExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetPaneContainerExtendedStyle()

CPaneContainer还有一些扩展样式用来控制容器窗口上Close按钮的布局方式:

扩展样式的默认值是0,表示容器窗口是水平放置的,还有一个Close按钮。

HWND SetClient(HWND hWndClient)
HWND GetClient()

调用SetClient()可以将一个子窗口指派给窗格容器,这和调用CSplitterWindow类的SetSplitterPane()方法作用类似。SetClient()同时返回原来的客户区窗口句柄而调用GetClient()则可以得到当前的客户区窗口句柄。

BOOL SetTitle(LPCTSTR lpstrTitle)
BOOL GetTitle(LPTSTR lpstrTitle, int cchLength)
int GetTitleLength()

调用SetTitle()可以改变容器窗口顶部显示的文字,调用GetTitle()可以得到当前窗口顶部区域显示的文字,调用GetTitleLength()可以得到当前显示的文字的字符个数(不包括结尾的空字符)。

BOOL EnableCloseButton(BOOL bEnable)

如果窗格容器使用的Close按钮,你可以调用EnableCloseButton()来控制这个按钮的状态。

在分隔窗口中使用窗格容器

为了说明窗格容器的使用方法,我们将向ClipSpy的分隔窗口的左窗格添加一个窗格容器,我们将一个窗格容器指派给左窗格取代原来使用的list控件,而将list控件指派给窗格容器。下面是在CMainFrame::OnCreate()中为支持窗格容器而添加的代码。

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
    m_wndVertSplit.Create ( *this, rcDefault, NULL, dwSplitStyle, dwSplitExStyle );
 
    // Create the pane container.
    m_wndPaneContainer.Create ( m_wndVertSplit, IDS_LIST_HEADER );
 
    // Create the left pane (list of clip formats)
    m_wndFormatList.Create ( m_wndPaneContainer, rcDefault );
//...
    // Set up the splitter panes
    m_wndPaneContainer.SetClient ( m_wndFormatList );
    m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );

注意,现在list控件的父窗口是m_wndPaneContainer,同时m_wndPaneContainer被设定成分隔窗口的左窗格。

下面是修改后的左窗格的外观,由于窗格容器在顶部的文本区域自己画了一个三维边框,所以我还要稍微修改一下边框的样式。这样看起来不是很好看,你可以自己调整样式知道你满意为止。(当然,你需要在Windows XP 上测试一下哪个界面主题可以使得分隔窗口看起来“更有意思”。)

 [Pane container - 5K]

关闭按钮和消息处理

当用户用鼠标单击Close按钮时,窗格容器向父窗口发送一个WM_COMMAND消息,命令的ID是ID_PANE_CLOSE。如果你在分隔窗口中使用了窗格容器,你需要响应整个消息,调用SetSinglePaneMode()隐藏这个窗格。(但是,不要忘了提供用户一个重新显示窗格的方法!)

CPaneContainer的消息链也用到了FORWARD_NOTIFICATIONS()宏,和CSplitterWindow一样,窗格容器在客户窗口和它的父窗口之间传递通知消息。在ClipSpy这个例子中,在list控件和主框架窗口之间隔了两个窗口(窗格容器和分隔窗口),但是FORWARD_NOTIFICATIONS()宏可以确保所有的通知消息被送到主框架窗口。

高级功能

在这一节,我将介绍一些如何使用WTL的高级界面特性。

嵌套的分隔窗口

如果你要编写一个email的客户端程序,你可能需要使用嵌套的分隔条,一个水平的和一个垂直的分隔条。使用WTL很容易做到这一点:创建一个分隔窗口作为另一个分隔窗口的子窗口。

为了演示这种效果,我将为ClipSpy添加一个水平分隔窗口。首先,添加一个名为m_wndHorzSplitter的CHorSplitterWindow类型的成员,像创建垂直分隔窗口m_wndVertSplitter那样创建这个水平分隔窗口,使水平分隔窗口m_wndHorzSplitter成为顶层窗口,将m_wndVertSplitter创建成m_wndHorzSplitter的子窗口。最后将m_hWndClient设置为m_wndHorzSplitter,因为现在水平分隔窗口占据整个主框架窗口的客户区。

LRESULT CMainFrame::OnCreate()
{
//...
    // Create the splitter windows.
    m_wndHorzSplit.Create ( *this, rcDefault, NULL, 
                           dwSplitStyle, dwSplitExStyle );
 
    m_wndVertSplit.Create ( m_wndHorzSplit, rcDefault, NULL, 
                           dwSplitStyle, dwSplitExStyle );
//...
    // Set the horizontal splitter as the client area window.
    m_hWndClient = m_wndHorzSplit;

    // Set up the splitter panes
    m_wndPaneContainer.SetClient ( m_wndFormatList );
    m_wndHorzSplit.SetSplitterPane ( SPLIT_PANE_TOP, m_wndVertSplit );
    m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );
//...
}

最终的结果是这个样子的:

 [Horz splitter w/empty pane - 5K]

在窗格中使用ActiveX控件

在分隔窗口的窗格中使用ActiveX控件与在对话框中使用ActiveX控件类似,使用CAxWindow类的方法在运行是创建控件,然后将这个CAxWindow指定给分隔窗口的窗格。下面演示了如何在水平分隔窗口下面的窗格中使用浏览器控件:

    // Create the bottom pane (browser)
CAxWindow wndIE;
const DWORD dwIEStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |
                        WS_HSCROLL | WS_VSCROLL;

    wndIE.Create ( m_wndHorzSplit, rcDefault, 
                  _T("http://www.codeproject.com"), dwIEStyle );

    // Set the horizontal splitter as the client area window.
    m_hWndClient = m_wndHorzSplit;

    // Set up the splitter panes
    m_wndPaneContainer.SetClient ( m_wndFormatList );
    m_wndHorzSplit.SetSplitterPanes ( m_wndVertSplit, wndIE );
    m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );

特殊绘制

如果你想改变分隔条的外观,例如在上面使用一些材质,你可以从CSplitterWindowImpl派生新类并重载DrawSplitterBar()函数。如果你只是想调整一下分隔条的外观,可以复制CSplitterWindowImpl类的函数,然后稍做修改。下面的例子就在分隔条中使用了斜交叉线图案。

template <bool t_bVertical = true>
class CMySplitterWindowT : 
    public CSplitterWindowImpl<CMySplitterWindowT<t_bVertical>, t_bVertical>
{
public:
    DECLARE_WND_CLASS_EX(_T("My_SplitterWindow"), 
                         CS_DBLCLKS, COLOR_WINDOW)
 
    // Overrideables
    void DrawSplitterBar(CDCHandle dc)
    {
    RECT rect;
 
        if ( m_br.IsNull() )
            m_br.CreateHatchBrush ( HS_DIAGCROSS, 
                                    t_bVertical ? RGB(255,0,0) 
                                                : RGB(0,0,255) );
 
        if ( GetSplitterBarRect ( &rect ) )
        {
            dc.FillRect ( &rect, m_br );
 
            // draw 3D edge if needed
            if ( (GetExStyle() & WS_EX_CLIENTEDGE) != 0)
                dc.DrawEdge(&rect, EDGE_RAISED, 
                            t_bVertical ? (BF_LEFT | BF_RIGHT) 
                                        : (BF_TOP | BF_BOTTOM));
        }
    }
 
protected:
    CBrush m_br;
};
 
typedef CMySplitterWindowT<true>    CMySplitterWindow;
typedef CMySplitterWindowT<false>   CMyHorSplitterWindow;

这就是结果(将分隔条变宽是为了更容易看到效果):

 [custom drawn bars - 14K]

窗格容器内的特殊绘制

CPaneContainer也有几个函数可以重载,用来改变窗格容器的外观。你可以从CPaneContainerImpl派生新类并重载你需要的方法,例如:

class CMyPaneContainer :
    public CPaneContainerImpl<CMyPaneContainer>
{
public:
    DECLARE_WND_CLASS_EX(_T("My_PaneContainer"), 0, -1)
//... overrides here ...
};

一些更有意思的方法是:

void CalcSize()

调用CalcSize()函数只是为了设置m_cxyHeader,这个变量控制着窗格容器的顶部区域的宽度和高度。不过SetPaneContainerExtendedStyle()函数中有一个BUG,导致窗格从水平切换到垂直时没有调用派生类的CalcSize()方法,你可以将CalcSize()调用改为pT->CalcSize()来修补这个BUG。

HFONT GetTitleFont()

这个方法返回一个HFONT,它被用来画顶部区域的文字,默认的值是调用GetStockObject(DEFAULT_GUI_FONT)得到的字体,也就是MS Sans Serif。如果你想改称更现代的Tahoma字体,你可以重载GetTitleFont()方法,返回你创建的Tahoma字体。

BOOL GetToolTipText(LPNMHDR lpnmh)

重载这个方法提供鼠标移到Close按钮时弹出的提示信息,这个函数实际上是TTN_GETDISPINFO的相应函数,你可以将lpnmh转换成NMTTDISPINFO*,并设置这个数据结构内相应的成员变量。记住一点,你必须检查通知代码,它可能是TTN_GETDISPINFO或TTN_GETDISPINFOW,你需要有区别的访问这两个数据结构。

void DrawPaneTitle(CDCHandle dc)

你可以重载这个方法自己画顶部区域,你可以用GetClientRect()和m_cxyHeader来计算顶部区域的范围。下面的例子演示了在水平容器的顶部区域画一个渐变填充的背景:

void CMyPaneContainer::DrawPaneTitle ( CDCHandle dc )
{
RECT rect;
 
    GetClientRect(&rect);
 
TRIVERTEX tv[] = { 
    { rect.left, rect.top, 0xff00 },
    { rect.right, rect.top + m_cxyHeader, 0, 0xff00 } 
};
GRADIENT_RECT gr = { 0, 1 };
 
    dc.GradientFill ( tv, 2, &gr, 1, GRADIENT_FILL_RECT_H );
}

例子工程代码中演示了对这几个方法的重载,使得结果看起来是这个样子的:

 [Custom drawing in a pane cont. - 6K]

从上面的图中可以看到,这个演示程序有一个Splitters菜单,通过它可以在各种风格的分隔条(包括自画风格)和窗格容器之间切换,比较它们之间的异同。你还可以锁定分隔条的位置,这是通过设置和取消SPLIT_NONINTERACTIVE扩展风格来实现的。

在状态栏显示进度条

正如我在前几篇文章中做得保证那样,新的ClipSpy也演示了如何在状态条上创建进展条,它和MFC版本得功能一样,几个相关得步骤是:

  1. 得到状态条第一个窗格得坐标范围RECT
  2. 创建一个进展条作为状态条得子窗口,窗口大小就是哪个状态条窗格得大小
  3. 随着edit控件被填充的同时更新进展条的位置

这些代码在CMainFrame::CreateProgressCtrlInStatusBar()函数中。

继续

在第八章我将介绍属性页和向导对话框的用法

参考

WTL Splitters and Pane Containers by Ed Gadziemski

修改记录

July 9, 2003: 文章第一次发布。