WTL for MFC 程序员系列,第七部分 - 分割窗口






4.95/5 (83投票s)
2003年7月11日
20分钟阅读

407055

5407
一篇关于使用 WTL 分割窗口和窗格容器的教程。
目录
引言
自从 Explorer 在 Windows 95 中首次亮相,以其双窗格的文件系统视图以来,分割窗口一直是一种流行的 UI 元素。MFC 有一个复杂而强大的分割窗口类,然而它学习起来有些困难,并且与文档/视图框架耦合。在第七部分中,我将讨论 WTL 分割窗口,它比 MFC 的简单得多。虽然 WTL 的分割器实现比 MFC 的功能少,但它更容易使用和扩展。
本部分的示例项目将是 ClipSpy 的重写,当然是使用 WTL 而不是 MFC。如果您不熟悉该程序,请现在查看文章,因为我将在这里复制 ClipSpy 的功能,而不再深入解释它的工作原理。本文的重点是分割窗口,而不是剪贴板。
WTL 分割窗口
头文件 atlsplit.h 包含所有 WTL 分割窗口类。有三个类:CSplitterImpl
、CSplitterWindowImpl
和 CSplitterWindowT
。下面解释了这些类及其基本方法。
类
CSplitterImpl
是一个模板类,它接受两个模板参数:一个窗口接口类名和一个布尔值,指示分割器方向:true
表示垂直,false
表示水平。CSplitterImpl
几乎包含了分割器的所有实现,并且许多方法都可以被重写,以便您可以提供分割条的自定义绘制或其他效果。CSplitterWindowImpl
派生自 CWindowImpl
和 CSplitterImpl
,但代码不多。它有一个空的 WM_ERASEBKGND
处理程序,以及一个调整分割窗口大小的 WM_SIZE
处理程序。
最后,CSplitterWindowT
派生自 CSplitterImpl
并提供了一个窗口类名。如果您不需要进行任何自定义,可以使用两个方便的 typedef:CSplitterWindow
用于垂直分割器,CHorSplitterWindow
用于水平分割器。
创建分割器
由于 CSplitterWindow
派生自 CWindowImpl
,因此您可以像创建任何其他子窗口一样创建分割器。当分割器在主框架的整个生命周期中都存在时(就像在 ClipSpy 中一样),您可以将 CSplitterWindow
成员变量添加到 CMainFrame
中。在 CMainFrame::OnCreate()
中,您将分割器创建为框架的子窗口,然后将分割器设置为主框架的客户端窗口。
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs ) { // ... m_wndSplit.Create ( *this, rcDefault ); m_hWndClient = m_wndSplit; }
创建分割器后,您可以将窗口分配给其窗格,并进行任何其他必要的初始化。
基本方法
bool SetSplitterPos(int xyPos = -1, bool bUpdate = true) int GetSplitterPos()
调用 SetSplitterPos()
设置分割条的位置。该位置以像素表示,相对于分割窗口的顶部边缘(对于水平分割器)或左侧边缘(对于垂直分割器)。您可以使用默认值 -1 将分割条定位在中间,使两个窗格大小相同。您通常会为 bUpdate
传递 true
,以便分割器立即相应地调整窗格大小。GetSplitterPos()
返回分割条的当前位置,相对于分割窗口的顶部或左侧边缘。(如果分割器处于单窗格模式,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_NONINTERACTIVE
:分割条不能移动,也不响应鼠标
扩展样式的默认值为 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
是子窗口的窗口句柄。您可以使用 SetSplitterPanes()
同时将子窗口分配给两个窗格。您通常会使用 bUpdate
的默认值,它告诉分割器立即调整子窗口的大小以适应窗格。SetSplitterPane()
返回一个 bool
,但是只有在您为 nPane
传递无效值时才会返回 false
。
您可以使用 GetSplitterPane()
获取窗格中窗口的 HWND
。如果未将任何窗口分配给窗格,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()
检查具有焦点的窗口。如果一个窗格(或窗格的子级)具有焦点,则分割器将焦点设置到另一个窗格。否则,如果 bNext
为 true,ActivateNextPane()
激活左/上窗格;如果 bNext
为 false,则激活右/下窗格。
bool SetDefaultActivePane(int nPane) bool SetDefaultActivePane(HWND hWnd) int GetDefaultActivePane()
调用 SetDefaultActivePane()
,传入 SPLIT_PANE_*
值或窗口句柄,将该窗格设置为默认活动窗格。如果分割窗口本身通过 SetFocus()
调用获得焦点,它将依次将焦点设置到默认活动窗格。GetDefaultActivePane()
返回一个 SPLIT_PANE_*
值,指示当前的默认活动窗格。
void GetSystemSettings(bool bUpdate)
GetSystemSettings()
读取各种系统设置并相应地设置数据成员。为 bUpdate
传递 true,以便分割器立即使用新设置重绘自身。
分割器在创建时调用此方法,因此您无需自己调用。但是,您的主框架应处理 WM_SETTINGCHANGE
消息并将其传递给分割器;CSplitterWindow
在其 WM_SETTINGCHANGE
处理程序中调用 GetSystemSettings()
。
数据成员
一些其他分割器功能通过设置 CSplitterWindow
的公共成员来控制。当调用 GetSystemSettings()
时,这些成员都会被重置。
m_cxySplitBar
- 对于垂直分割器:控制分割条的宽度。默认值是
GetSystemMetrics(SM_CXSIZEFRAME)
返回的值。
对于水平分割器:控制分割条的高度。默认值是GetSystemMetrics(SM_CYSIZEFRAME)
返回的值。 m_cxyMin
- 对于垂直分割器:控制每个窗格的最小宽度。如果拖动分割条会导致任何一个窗格小于此像素数,则分割器不允许拖动。如果分割窗口具有
WS_EX_CLIENTEDGE
扩展窗口样式,则默认值为 0。否则,默认值为2*GetSystemMetrics(SM_CXEDGE)
。
对于水平分割器:控制每个窗格的最小高度。如果分割窗口具有WS_EX_CLIENTEDGE
扩展窗口样式,则默认值为 0。否则,默认值为2*GetSystemMetrics(SM_CYEDGE)
。 m_cxyBarEdge
- 对于垂直分割器:控制分割条两侧绘制的 3D 边缘的宽度。如果分割窗口具有
WS_EX_CLIENTEDGE
扩展窗口样式,则默认值为2*GetSystemMetrics(SM_CXEDGE)
,否则默认值为 0。
对于水平分割器:控制分割条两侧绘制的 3D 边缘的高度。如果分割窗口具有WS_EX_CLIENTEDGE
扩展窗口样式,则默认值为2*GetSystemMetrics(SM_CYEDGE)
,否则默认值为 0。 m_bFullDrag
- 如果此成员设置为
true
,则在拖动分割条时窗格会调整大小。如果为false
,则只绘制分割条的虚影,并且在用户释放分割条之前窗格不会调整大小。默认值是SystemParametersInfo(SPI_GETDRAGFULLWINDOWS)
返回的值。
启动示例项目
现在我们已经了解了基础知识,让我们看看如何设置一个包含分割器的框架窗口。使用 WTL AppWizard 启动一个新项目。在第一页,保持选择 SDI Application,然后单击下一步。在第二页,取消选中 Toolbar,然后取消选中 Use a view window,如下所示:
我们不需要视图窗口,因为分割器及其窗格将成为“视图”。在 CMainFrame
中,添加一个 CSplitterWindow
成员
class CMainFrame : public ... { //... protected: CSplitterWindow m_wndVertSplit; };
然后在 OnCreate()
中,创建分割器并将其设置为视图窗口
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; }
请注意,您需要在设置分割器位置之前设置 m_hWndClient
并调用 CFrameWindowImpl::UpdateLayout()
。UpdateLayout()
将分割窗口调整为其初始大小。如果您跳过该步骤,分割器的大小将不受您的控制,并且可能小于 200 像素宽。最终结果是 SetSplitterPos()
将不会产生您想要的效果。
替代调用 UpdateLayout()
的方法是获取框架窗口的客户端 RECT
,并在创建分割器时使用该 RECT
,而不是 rcDefault
。这样,您在初始位置创建分割器,所有后续处理位置的方法(如 SetSplitterPos()
)都将正常工作。
如果现在运行应用程序,您将看到分割器正在运行。即使没有为窗格创建任何内容,基本行为也存在。您可以拖动条,双击它会将其移动到中心。
为了演示管理窗格窗口的不同方法,我将使用一个派生自 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 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; }
请注意,两个 Create()
调用都使用 m_wndVertSplit
作为父窗口。RECT
参数并不重要,因为分割器会根据需要调整两个窗格窗口的大小,所以我们可以使用 CWindow::rcDefault
。
最后一步是将窗格的 HWND
传递给分割器。这也必须在 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; }
这是结果的样子,在列表控件添加了一些列之后:
请注意,分割器对窗格中可以放置的窗口没有任何限制,不像 MFC 中您必须使用 CView
。窗格窗口应该至少具有 WS_CHILD
样式,但除此之外,您基本上可以自由使用任何东西。
WS_EX_CLIENTEDGE 的影响
此处需要对 WS_EX_CLIENTEDGE
样式对分割器和窗格中窗口的影响进行一点旁注。我们可以在三个地方应用此样式:主框架、分割窗口或分割窗格中的窗口。WS_EX_CLIENTEDGE
在每种情况下都会创建不同的外观,因此我将在此处进行说明。
- 框架窗口上的
WS_EX_CLIENTEDGE
- 这是最不吸引人的选择,因为分割器边框有边缘,但分割条没有边缘。
- 分割窗口上的
WS_EX_CLIENTEDGE
- 当
CSplitterWindow
具有WS_EX_CLIENTEDGE
样式时,绘图代码会额外地沿着分割条的两侧绘制边框,以便每个窗格以及整个分割窗口都有一个边缘。 - 窗格窗口上的
WS_EX_CLIENTEDGE
- 每个窗格窗口都有边框,分割条与框架窗口的菜单和边框融合,没有任何间断。这在 XP 之前的 Windows(或 XP 关闭主题时)上更明显。在 XP 开启主题时,很难分辨那里是否有分割条,除非你用鼠标去寻找。
消息路由
既然我们现在在主框架和窗格窗口之间又多了一个窗口,您可能想知道通知消息是如何工作的。具体来说,主框架如何接收 NM_CUSTOMDRAW
通知,以便它可以将其反射到列表?答案可以在 CSplitterWindowImpl
消息映射中找到:
BEGIN_MSG_MAP() MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground) MESSAGE_HANDLER(WM_SIZE, OnSize) CHAIN_MSG_MAP(baseClass) FORWARD_NOTIFICATIONS() END_MSG_MAP()
末尾的 FORWARD_NOTIFICATIONS()
宏是重要的。回忆一下 第四部分,有一些通知消息总是发送到子窗口的父窗口。FORWARD_NOTIFICATIONS()
的作用是,将消息重新发送到 分割器的 父窗口。所以当列表将 WM_NOTIFY
消息发送给分割器(列表的父窗口)时,分割器又将 WM_NOTIFY
发送给主框架(分割器的父窗口)。当主框架反射消息时,它被发送回最初生成 WM_NOTIFY
的窗口,所以分割器不参与反射。
所有这些的结果是,在主框架和列表之间发送的通知消息不会受到分割窗口存在的影响。这使得添加或移除分割器变得相当容易,因为子窗口类根本不需要更改,它们的メッセージ处理就能继续工作。
窗格容器
WTL 还支持一个类似于 Explorer 左侧窗格中的小部件,称为“窗格容器”。此控件提供一个带有文本的标题区域,并可选择提供一个关闭按钮。
窗格容器管理一个子窗口,就像分割器管理两个窗格窗口一样。当容器调整大小时,子窗口会自动调整大小以匹配容器内的空间。
类
窗格容器的实现中有两个类,都在 atlctrlx.h 中:CPaneContainerImpl
和 CPaneContainer
。CPaneContainerImpl
是一个派生自 CWindowImpl
的类,包含完整的实现;CPaneContainer
只提供一个窗口类名。除非您想重写任何方法来更改容器的绘制方式,否则您将始终使用 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
具有附加的扩展样式,用于控制关闭按钮和容器的布局:
PANECNT_NOCLOSEBUTTON
:设置此样式以从标题中删除关闭按钮。PANECNT_VERTICAL
:设置此样式以使标题区域垂直,沿着容器窗口的左侧。
扩展样式的默认值为 0,这会生成一个带有关闭按钮的水平容器。
HWND SetClient(HWND hWndClient) HWND GetClient()
调用 SetClient()
将子窗口分配给窗格容器。这类似于 CSplitterWindow
中的 SetSplitterPane()
方法。SetClient()
返回旧客户端窗口的 HWND
。调用 GetClient()
获取当前客户端窗口的 HWND
。
BOOL SetTitle(LPCTSTR lpstrTitle) BOOL GetTitle(LPTSTR lpstrTitle, int cchLength) int GetTitleLength()
调用 SetTitle()
更改容器标题区域中显示的文本。调用 GetTitle()
检索当前标题文本,并调用 GetTitleLength()
获取当前标题文本的字符长度(不包括空终止符)。
BOOL EnableCloseButton(BOOL bEnable)
如果窗格容器有“关闭”按钮,您可以使用 EnableCloseButton()
启用和禁用它。
在分割窗口中使用窗格容器
为了演示如何向现有分割器添加窗格容器,我们将在 ClipSpy 分割器的左窗格中添加一个容器。我们不是将列表控件分配给左窗格,而是分配窗格容器。然后将列表分配给窗格容器。以下是 CMainFrame::OnCreate()
中用于设置窗格容器的更改行。
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 );
请注意,列表控件的父窗口是 m_wndPaneContainer
。此外,m_wndPaneContainer
被设置为分割器的左窗格。这是修改后的左窗格的样子。
关闭按钮和消息处理
当用户单击“关闭”按钮时,窗格容器会向其父级发送一个 WM_COMMAND
消息,命令 ID 为 ID_PANE_CLOSE
(在 atlres.h 中定义的常量)。当您在分割器中使用窗格容器时,通常的做法是调用 SetSinglePaneMode()
来隐藏包含窗格容器的分割器窗格。(但请记住提供一种方法让用户再次显示该窗格!)。
CPaneContainer
消息映射也具有 FORWARD_NOTIFICATIONS()
宏,就像 CSplitterWindow
一样,因此容器将其客户端窗口的通知消息传递给其父级。在 ClipSpy 的情况下,列表控件和主框架之间有两个窗口(窗格容器和分割器),但 FORWARD_NOTIFICATIONS()
宏确保列表的所有通知都到达主框架。
高级分割器功能
本节中,我将描述如何使用 WTL 分割器实现一些常见的用户界面高级技巧。
嵌套分割器
如果您计划编写一个应用程序,例如电子邮件客户端或 RSS 阅读器,您可能会使用嵌套分割器 - 一个水平分割器和一个垂直分割器。这在 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 ); 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 控件类似于在对话框中托管控件。您使用 CAxWindow
方法在运行时创建控件,然后将 CAxWindow
分配给分割器中的一个窗格。以下是如何将浏览器控件添加到水平分割器的底部窗格:
// Create the bottom pane (browser) CAxWindow wndIE; DWORD dwIEStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_HSCROLL | WS_VSCROLL; wndIE.Create ( m_wndHorzSplit, rcDefault, _T("https://codeproject.org.cn"), 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;
这是结果(为了更容易看到效果,分割条被加宽了):
窗格容器中的特殊绘制
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()
不会被调用。您可以通过将 atlctrlx.h 中第 2215 行更改为调用 pT->CalcSize()
而不是 CalcSize()
来修复此问题。
HFONT GetTitleFont()
此方法返回一个 HFONT
,该字体将用于绘制标题文本。默认值是 GetStockObject(DEFAULT_GUI_FONT)
返回的值,即 MS Sans Serif。如果您想使用更现代的 Tahoma 字体,可以重写 GetTitleFont()
并返回您创建的 Tahoma 字体句柄。
BOOL GetToolTipText(LPNMHDR lpnmh)
重写此方法以在光标悬停在“关闭”按钮上时提供工具提示文本。此方法实际上是 TTN_GETDISPINFO
的处理程序,因此您将 lpnmh
转换为 NMTTDISPINFO*
并相应地设置该结构体的成员。请记住,您必须检查通知代码——它可能是 TTN_GETDISPINFO
或 TTN_GETDISPINFOW
——并相应地访问该结构体。
void DrawPaneTitle(CDCHandle dc)
您可以重写此方法以提供您自己的标题区域绘制。您可以使用 GetClientRect()
和 m_cxyHeader
来计算标题区域的 RECT
。以下是在水平容器标题区域中绘制渐变填充的示例代码:
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 ); }
示例项目演示了重写其中一些方法,结果如下所示:
演示项目有一个“分割器”菜单,如上所示,它允许您切换分割器和窗格容器的各种特殊绘制功能,以便您可以看到差异。您还可以锁定分割器,这是通过切换 SPLIT_NONINTERACTIVE
扩展样式来完成的。
额外内容:状态栏中的进度条
正如我几篇文章前承诺的,这个新的 ClipSpy 演示了如何在状态栏中创建进度条。它的工作方式与 MFC 版本完全相同——涉及的步骤是:
- 获取第一个状态栏窗格的
RECT
- 创建一个进度条控件作为状态栏的子控件,将其
RECT
设置为窗格的RECT
- 在填充编辑控件时更新进度条位置
您可以在 CMainFrame::CreateProgressCtrlInStatusBar()
中查看代码。
接下来
在第八部分,我将探讨属性页和向导的主题。
参考文献
Ed Gadziemski 的WTL 分割器和窗格容器
版权和许可
本文受版权保护,(c)2003-2006 年由 Michael Dunn 撰写。我意识到这并不能阻止人们在网上随意复制它,但我仍然不得不声明。如果您有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译的存在,以便我可以在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码可以造福所有人。(我没有将文章本身发布到公共领域,因为仅在 CodeProject 上提供文章有助于提高我的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,我很乐意收到您的电子邮件(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在您自己的源代码中注明出处也值得赞赏,但不是必需的。
修订历史
- 2003年7月9日:文章首次发布。
- 2006年1月12日:主要编辑以修复文章中不清楚或措辞不当的部分。更新了一些屏幕截图。添加了关于
WS_EX_CLIENTEDGE
的部分。
系列导航:《第六部分(托管 ActiveX 控件) | 》第八部分(属性页和向导)