MFC 程序员的 WTL 教程 ( 7 ) — 分割条窗口
内容
简介自从 Windows 95 的资源管理器以其文件系统的双窗格视图粉墨登场以来,分割条窗口就成了一个流行的 UI 元素。MFC 中有一个复杂而强大的分割条窗口类,但是学会如何使用它却有点困难,而且它关联于文档/视图框架。在本部分里,我将探讨 WTL 的分隔条窗口,与 MFC 的分割条窗口相比没那么复杂。尽管 WTL 分隔条的实现没有 MFC 的特性丰富,但它却极其易于使用和扩展。 本章的示例工程是对 ClipSpy 的重新实现,当然,是使用了 WTL 而不是 MFC。如果你对该程序不熟悉,现在可以先浏览一下相关的文章,因为在这儿我会复制 ClipSpy 中的功能,但不会提供其工作原理的深入解释。本文主要聚焦于分割条窗口而不是剪贴板。
WTL 分割条窗口在头文件 atlsplit.h 中包括了所有的 WTL 分割条窗口类。共有三个类: 类
最后, 创建一个分割条因为
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
// ...
m_wndSplit.Create ( *this, rcDefault );
m_hWndClient = m_wndSplit;
}
创建分割条之后,你可以把窗口关联到它的窗格上,并进行其它必要的初始化工作。 基本方法bool SetSplitterPos(int xyPos = -1, bool bUpdate = true) 调用 bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE) 调用 DWORD SetSplitterExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0) DWORD GetSplitterExtendedStyle() 分割条窗口还有扩展的风格位,用来在整个分割条窗口改变大小时控制分割条的移动方式。可用的风格有:
如果没有指定上述的三个风格之一,那分割条缺省就是左或者上对齐的。如果你将 还有一个另外的风格可以控制用户是否可以移动分割条:
缺省的扩展风格是 bool SetSplitterPane(int nPane, HWND hWnd, bool bUpdate = true) void SetSplitterPanes(HWND hWndLeftTop, HWND hWndRightBottom, bool bUpdate = true) HWND GetSplitterPane(int nPane) 调用 你可以用 bool SetActivePane(int nPane) int GetActivePane()
bool ActivateNextPane(bool bNext = true) 如果分隔条处于单窗格模式下,焦点会被设置到可见窗格上。否则, bool SetDefaultActivePane(int nPane) bool SetDefaultActivePane(HWND hWnd) int GetDefaultActivePane() 使用 void GetSystemSettings(bool bUpdate)
分割条在创建时会自动调用此函数,所以你不必亲自调用它。不过,你的主框架应该处理 数据成员分割条的另外一些特性是通过设置
开始示例工程我们已经有了非常坚实的基础了,现在来看看怎样才能搞出一个包含分隔条的框架窗口来。用 WTL AppWizard 开始一个新的工程,在第一页里,选中 SDI Application 并点击 Next,在第二页,不要选择 Toolbar,也不要选择 Use a view window,就象这样: 我们不需要视图窗口,因为分割条以及其窗格会成为“视图”。在
class CMainFrame : public ...
{
//...
protected:
CSplitterWindow m_wndVertSplit;
};
然后在
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
// Create the splitter window
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
// 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;
}
请注意,在设置分隔条的位置之前你需要设置 取代调用 如果你现在就运行此应用,你就可以看到分割条在工作。即使不在窗格里创建任何东西,基本的功能也仍然可用。你可以拖动分割条,或者通过双击把分割条移到正中。 为了演示管理窗格窗口的不同方法,我将使用一个
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()
//...
};
如果你从之前的系列文章一路读来,那你阅读此类就不应该有任何问题。它处理 由于窗格窗口在应用的生命期内一直存在,所以我们也可以为它们在
class CMainFrame : public ...
{
//...
protected:
CSplitterWindow m_wndVertSplit;
CClipSpyListCtrl m_wndFormatList;
CRichEditCtrl m_wndDataViewer;
};
在窗格中创建窗口现在我们已经有了用于分隔条和窗格的成员变量,填充分隔条就是一件简单的事情了。分隔条窗口创建之后,我们就可以把分隔条作为父窗口来创建两个子窗口了:
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
// Create the splitter window
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
// Create the left pane (list of clip formats)
m_wndFormatList.Create ( m_wndVertSplit, rcDefault );
// Create the right pane (rich edit ctrl)
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;
}
可以看到,两个 最后一步是把窗格的
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;
}
下面就是结果,列表控件里已经添加了几列: 注意,分隔条并不限制什么窗口才能放到窗格里,不像 MFC 总是假定你使用的是 WS_EX_CLIENTEDGE 的影响接下来的少许不太重要的话题是关于把
消息路由现在,因为我们在主框架和窗格窗口之间又有了另外的一个窗口,你可能会惊讶通知消息是怎么工作的。尤其是,主框架怎样才能接收到
BEGIN_MSG_MAP()
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_SIZE, OnSize)
CHAIN_MSG_MAP(baseClass)
FORWARD_NOTIFICATIONS()
END_MSG_MAP()
最后面的 所有这些的结果就是发送于主框架和列表间的通知消息并不会因为有分隔条窗口的存在而受到影响。这就使得添加或者去处分隔条都相当的容易,因为子窗口类根本不需要为了能使其消息处理可以继续工作而作任何改动。 窗格容器WTL 还支持一种窗口部件(widget),就好比在资源管理器的左窗格中的那个一样,称为窗格容器。此控件提供了一个带有文字的题头区域,以及一个可选的关闭按钮: 如同分隔条窗口管理着两个窗格窗口一样,窗个容器管理了一个子窗口。在容器被改变大小时,其子窗口也自动改变大小以匹配容器内部的空间。 类窗格容器的实现有两个类,都在 atlctrlx.h 里: 基本方法
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)
创建 DWORD SetPaneContainerExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0) DWORD GetPaneContainerExtendedStyle()
此扩展风格的缺省值为 0,也即意味着是一个带有关闭按钮的水平容器。 HWND SetClient(HWND hWndClient) HWND GetClient() 调用 BOOL SetTitle(LPCTSTR lpstrTitle) BOOL GetTitle(LPTSTR lpstrTitle, int cchLength) int GetTitleLength() 调用 BOOL EnableCloseButton(BOOL bEnable) 如果窗格容器具有关闭按钮,你可以使用 在分割条窗口中使用窗格容器为了演示如何向一个现存的分割条中添加窗格容器,我们来向 ClipSpy 的分割条的左窗格里添加一个容器。我们不把列表控件关联到左窗格上,而代之以窗格容器。然后再把列表关联到窗格容器上。下面是在
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
m_wndVertSplit.Create ( *this, rcDefault );
// Create the pane container.
m_wndPaneContainer.Create ( m_wndVertSplit, IDS_PANE_CONTAINER_TEXT );
// 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 );
注意,列表控件的父是 关闭按钮以及消息处理当用户点击了关闭按钮时,窗格容器会向其父窗口发送
高级分割条特性在这一节里,我会介绍如何使用 WTL 的分割条来做一些常见的高级 UI 技巧。 嵌套分割条如果你计划写一个类似于邮件客户端或者 RSS 阅读器的应用,最终你就可能会使用到嵌套分割条,一个水平的和一个垂直的。用 WTL 的分割条来做这件事相当容易 – 只需把一个分割条创建为另一个的子窗口。 出于演示目的,我们要为 ClipSpy 添加一个水平分割条。水平分割条处于最顶级,垂直分割条将嵌套于其中。在添加完名为
LRESULT CMainFrame::OnCreate()
{
//...
// Create the splitter windows.
m_wndHorzSplit.Create ( *this, rcDefault );
m_wndVertSplit.Create ( m_wndHorzSplit, rcDefault );
//...
// 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 );
//...
}
下面就是成果: 在窗格中使用 ActiveX 控件在分割条窗格中掌控 ActiveX 控件与在对话框中掌控控件类似。你可以在运行时使用
// 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 );
特殊绘制如果你要为分割条提供不同的外观,比如说要在其上绘制纹理,你可以从
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;
下面就是结果,分隔条被拓宽了,以便能更容易地看到效果: 窗格容器中的特殊绘制
class CMyPaneContainer :
public CPaneContainerImpl<CMyPaneContainer>
{
public:
DECLARE_WND_CLASS_EX(_T("My_PaneContainer"), 0, -1)
//... overrides here ...
};
更有意思的几个方法是: void CalcSize()
HFONT GetTitleFont() 此方法返回一个用于绘制题头文字的 BOOL GetToolTipText(LPNMHDR lpnmh) 覆盖此方法可以提供鼠标悬停于关闭按钮时所需的工具提示文字。此方法其实是 void DrawPaneTitle(CDCHandle dc) 你可以覆盖此方法以提供题头区域的自绘制。可以使用
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 );
}
示例工程演示了覆盖这些方法中的几个,其结果显示在这儿: 如上所示,示例工程有一个 Splitters 菜单,允许你切换分隔条以及窗格容器的几种特殊绘制,以便你可以看到其间的区别。你还可以锁定分割条,这是通过切换 奖励:状态栏中的进度条正如我早在上几篇文章之前所允诺过的,这一新的 ClipSpy 会演示如何在状态栏上创建进度条。此工作与 MFC 版本一样 – 所需的步骤有:
可以到 下一步在第八部分里,我将介绍有关属性表和向导的话题。 参考资料WTL Splitters and Pane Containers – Ed Gadziemski 修订历史2003 年 7 月 9 日:首次发布 |

近期评论