译:MFC 程序员的 WTL 教程(七)(第二版)

特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。

链接:上一部分下一部分

第七部分 – 分割条窗口

内容

  • 简介
  • WTL 分割条窗口
    • 创建一个分割条
    • 基本方法
    • 数据成员
  • 开始示例工程
  • 在窗格中创建窗口
    • WS_EX_CLIENTEDGE 的影响
  • 消息路由
  • 窗格容器
    • 基本方法
    • 在分割条窗口中使用窗格容器
    • 关闭按钮以及消息处理
  • 高级分割条特性
    • 嵌套分割条
    • 在窗格中使用 ActiveX 控件
    • 特殊绘制
  • 窗格容器中的特殊绘制
  • 奖励:状态栏中的进度条
  • 下一步
  • 参考资料
  • 修订历史

简介

自从 Windows 95 的资源管理器以其文件系统的双窗格视图粉墨登场以来,分割条窗口就成了一个流行的 UI 元素。MFC 中有一个复杂而强大的分割条窗口类,但是学会如何使用它却有点困难,而且它关联于文档/视图框架。在本部分里,我将探讨 WTL 的分隔条窗口,与 MFC 的分割条窗口相比没那么复杂。尽管 WTL 分隔条的实现没有 MFC 的特性丰富,但它却极其易于使用和扩展。

本章的示例工程是对 ClipSpy 的重新实现,当然,是使用了 WTL 而不是 MFC。如果你对该程序不熟悉,现在可以先浏览一下相关的文章,因为在这儿我会复制 ClipSpy 中的功能,但不会提供其工作原理的深入解释。本文主要聚焦于分割条窗口而不是剪贴板。

WTL 分割条窗口

在头文件 atlsplit.h 中包括了所有的 WTL 分割条窗口类。共有三个类: CSplitterImplCSplitterWindowImplCSplitterWindowT。下列会解释这些类及其基本的方法。

CSplitterImpl 是一个接收两个模板参数的模板类,一个是窗口接口类的名字,一个是表示分隔条方向的布尔值: true 为垂直方向, false 为水平方向。 CSplitterImpl 中含有分割条的几乎所有的实现,许多方法都是可覆盖的,因此你可以为分割条提供定制绘制或者其他的效果。 CSplitterWindowImplCWindowImpl 以及 CSplitterImpl 派生而来,不过并没有太多代码,它有一个空的 WM_ERASEBKGND 处理器以及一个改变分割条窗口的大小的 WM_SIZE 处理器。

最后, CSplitterWindowT 派生于 CSplitterImpl 并且提供了窗口类名。如果你不需要任何的定制,则有另外的两个 typedef 可供平常使用而不必用上面的三个类: CSplitterWindow 用于垂直分割条, CHorSplitterWindow 用于水平分割条。

创建一个分割条

因为 CSplitterWindow 派生于 CWindowImpl,所以你可以像创建其它子窗口一样创建分割条。如果分割条在主框架的生命期内一直存在,好比在 ClipSpy 中的那样,那你就可以在 CMainFrame 中添加一个 CSplitterWindow 类型的成员变量。在 CMainFrame::OnCreate() 中,先把分割条作为框架的子窗口创建出来,再把它设置为主框架的客户窗口:

创建分割条之后,你可以把窗口关联到它的窗格上,并进行其它必要的初始化工作。

基本方法

调用 SetSplitterPos() 可以设置分隔条的位置。位置使用像素单位表示,对于垂直分割条是相对于分割条窗口的顶部边缘,对于水平分割条是相对于分割条窗口的左部边缘。使用缺省值 -1 可以将分割条置于正中,使得两个窗格具有相同的大小。对于 bUpdate 通常应该传入 true 以便在分割条移动之后立刻相应地改变两个窗格的大小。 GetSplitterPos() 返回分割条相对于分割条窗口的顶部边缘或者左部边缘的当前位置。(如果是单窗格模式的分割条, GetSplitterPos() 会返回两个窗格同时显示出来后分割条回到的位置)。

调用 SetSinglePaneMode() 可以在单窗格和双窗格之间改变分割条的模式。在单窗格模式下,仅有一个窗格是可见的,并且分割条也隐藏了起来,这和 MFC 动态分割条的工作方式是一样的(但是没有用于重新调整分割条的小握柄(little gripper handle))。 nPane 允许的值有: SPLIT_PANE_LEFTSPLIT_PANE_RIGHTSPLIT_PANE_TOPSPLIT_PANE_BOTTOM 以及 SPLIT_PANE_NONE。前四个表示要显示哪个窗格,例如,传入 SPLIT_PANE_LEFT 会显示左边的窗格并隐藏右边的窗格。传入 SPLIT_PANE_NONE 会把两个窗格全显示出来。 GetSinglePaneMode() 返回表示了当前模式的这五个 SPLIT_PANE_* 值之一。

分割条窗口还有扩展的风格位,用来在整个分割条窗口改变大小时控制分割条的移动方式。可用的风格有:

  • SPLIT_PROPORTIONAL:分隔条中的两个窗格一起改变大小
  • SPLIT_RIGHTALIGNED:当整个分割条改变大小时右窗格保持大小不变,左窗格改变大小
  • SPLIT_BOTTOMALIGNED:当整个分割条改变大小时下窗格保持大小不变,上窗格改变大小

如果没有指定上述的三个风格之一,那分割条缺省就是左或者上对齐的。如果你将 SPLIT_PROPORTIONALSPLIT_RIGHTALIGNED/ SPLIT_BOTTOMALIGNED 一起传入,则 SPLIT_PROPORTIONAL 优先。

还有一个另外的风格可以控制用户是否可以移动分割条:

  • SPLIT_NONINTERACTIVE:分隔条既不可移动也不响应鼠标

缺省的扩展风格是 SPLIT_PROPORTIONAL

调用 SetSplitterPane() 可以为分割条的某个窗格关联一个子窗口。 nPaneSPLIT_PANE_* 值之一,表示要设置哪一个窗格。 hWnd 是子窗口的窗口句柄。你可以通过调用 SetSplitterPanes() 为两个窗格一次性同时设置好子窗口。你通常会采用 bUpdate 的缺省值,告诉分隔条立即改变子窗口的大小以适应窗格的大小。 SetSplitterPane() 会返回一个 bool 值,但它仅在你为 nPane 参数传递了非法值的时候才会返回 false

你可以用 GetSplitterPane() 获取窗格中窗口的 HWND 值。如果没有为窗格关联窗口的话, GetSplitterPane() 会返回 NULL。

SetActivePane() 会把焦点设置到分割条中的窗口之一上。 nPaneSPLIT_PANE_* 值之一,表示你要把哪一个窗格设置为活动的。它还设置了缺省的活动窗格(后文会解释)。 GetActivePane() 检查拥有焦点的窗口,如果该窗口是一个窗格窗口或者是窗格窗口的子窗口,则会返回一个具体代表了是哪一个窗格的 SPLIT_PANE_* 值。如果拥有焦点的窗口不是窗格的子, GetActivePane() 会返回 SPLIT_PANE_NONE

如果分隔条处于单窗格模式下,焦点会被设置到可见窗格上。否则, ActivateNextPane() 使用 GetActivePane() 检查拥有焦点的窗口,如果某个窗格(或者窗格的子)拥有焦点,那么分割条会把焦点设置到另外一个窗格上。否则, ActivateNextPane()bNext 为 true 时会激活左/上窗格,bNext 为 false 时会激活右/下窗格。

使用 SPLIT_PANE_* 值之一或者一个窗口句柄调用 SetDefaultActivePane() 会将该窗格设置为缺省的活动窗格。如果分隔条自身得到了焦点,它接着就会调用 SetFocus() 把焦点设置到缺省的活动窗格上。 GetDefaultActivePane() 会返回一个指明了缺省活动窗格的 SPLIT_PANE_* 值。

GetSystemSettings() 会读取若干的系统设置并相应地设置数据成员。为 bUpdate 参数传入会使分隔条立刻使用新的设置重绘自身。

分割条在创建时会自动调用此函数,所以你不必亲自调用它。不过,你的主框架应该处理 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:对于垂直分隔条:此值控制绘制于分隔条周边的三维边框的宽度,如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 2*GetSystemMetrics(SM_CXEDGE),否则的话,缺省值为 0;对于水平分隔条:此值控制绘制于分隔条周边的三维边框的高度,如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 2*GetSystemMetrics(SM_CYEDGE),否则的话,缺省值为 0。

m_bFullDrag:如果此成员设置为 true,当拖动分隔条时窗格会立即改变大小,如果为 false,则拖动时仅绘制分隔条的一个影像(ghost image),只要用户释放分隔条后才会改变窗格的大小。缺省值为 SystemParametersInfo(SPI_GETDRAGFULLWINDOWS) 的返回值。

开始示例工程

我们已经有了非常坚实的基础了,现在来看看怎样才能搞出一个包含分隔条的框架窗口来。用 WTL AppWizard 开始一个新的工程,在第一页里,选中 SDI Application 并点击 Next,在第二页,不要选择 Toolbar,也不要选择 Use a view window,就象这样:

 [AppWizard pg 2 - 22K]

 [VC7 AppWizard - 23K]

我们不需要视图窗口,因为分割条以及其窗格会成为“视图”。在 CMainFrame 里添加一个 CSplitterWindow 成员:

然后在 OnCreate() 里,创建该分隔条并将其设置为视图窗口:

请注意,在设置分隔条的位置之前你需要设置 m_hWndClient 并调用 CFrameWindowImpl::UpdateLayout()UpdateLayout() 会把分隔条窗口的大小调整为自己的初始尺寸。如果你忽略了此步骤,分割条的大小就失去了控制,其宽度可能会比 200 个像素还小,最终导致 SetSplitterPos() 起不到你想要的效果。

取代调用 UpdateLayout() 的另一个方法是先获取框架窗口的 RECT,在创建分隔条时使用此取得的 RECT 而不是 rcDefault。这样,你就在正确的初始位置上创建了分隔条,而后续需要处理位置的方法(例如 SetSplitterPos())也都可以正常工作。

如果你现在就运行此应用,你就可以看到分割条在工作。即使不在窗格里创建任何东西,基本的功能也仍然可用。你可以拖动分割条,或者通过双击把分割条移到正中。

 [Empty splitter - 4K]

为了演示管理窗格窗口的不同方法,我将使用一个 CListViewCtrl 的派生类以及一个普通的 CRichEditCtrl。下面是从我们用作左窗格的 CClipSpyListCtrl 类中取得的一个片断:

如果你从之前的系列文章一路读来,那你阅读此类就不应该有任何问题。它处理 WM_CHANGECBCHAIN 以得知其他剪贴板浏览器的来去,处理 WM_DRAWCLIPBOARD 以得知何时剪贴板的内容发生了变化。

由于窗格窗口在应用的生命期内一直存在,所以我们也可以为它们在 CMainFrame 中使用成员变量:

在窗格中创建窗口

现在我们已经有了用于分隔条和窗格的成员变量,填充分隔条就是一件简单的事情了。分隔条窗口创建之后,我们就可以把分隔条作为父窗口来创建两个子窗口了:

可以看到,两个 Create() 调用都把 m_wndVertSplit 作为了父窗口。 RECT 参数不重要,因为分隔条在需要时会重新改变两个窗格窗口的大小,所以我们可以使用 CWindow::rcDefault

最后一步是把窗格的 HWND 传递给分隔条。这也需要放在 UpdateLayout() 之前,以使所有的窗口都可以得到正确的大小。

下面就是结果,列表控件里已经添加了几列:

 [Splitter w/panes - 4K]

注意,分隔条并不限制什么窗口才能放到窗格里,不像 MFC 总是假定你使用的是 CView。窗格窗口至少要有 WS_CHILD 风格,但除此之外再使用什么东西,你是有相当大的自由空间的。

WS_EX_CLIENTEDGE 的影响

接下来的少许不太重要的话题是关于把 WS_EX_CLIENTEDGE 风格应用到分割条和窗格窗口上时的影响的。我们可以把此风格应用到三个地方:主框架、分割条窗口或者分割条窗格中的窗口。每种情况下 WS_EX_CLIENTEDGE 都会创建出一个不同的样子来,因此我在下面用图示来说明。

WS_EX_CLIENTEDGE 应用于框架窗口:
这是最不起眼的一种选择,因为分割条窗口(splitter)的边框(border)具有了沿起(edge),而分割条(bar)却没有。

 [Client edge style on main frame - 4K]  [Client edge style on main frame (XP) - 7K]

WS_EX_CLIENTEDGE 应用于分割条窗口:
当一个 CSplitterWindow拥有 WS_EX_CLIENTEDGE 风格时,其绘制代码执行了额外的步骤,沿着分割条(bar)的每条边绘制一个边框。因此,就像整个分割条窗口的周围一样,每个窗格附近都有一个沿起(edge)。

 [Client edge style on splitter - 4K]  [Client edge style on splitter (XP) - 7K]

WS_EX_CLIENTEDGE 应用于窗格窗口:
每个窗格窗口都有边框,分割条并入到了框架窗口的菜单和边框中,没有任何的破绽。这在 XP 之前的 Windows 上(或者关闭了主题的 XP)更为显眼,在开着主题的 XP 上,很难说那儿有一个分割条,除非你用鼠标去试。

 [Client edge style on pane - 4K]  [Client edge style on pane (XP) - 7K]

消息路由

现在,因为我们在主框架和窗格窗口之间又有了另外的一个窗口,你可能会惊讶通知消息是怎么工作的。尤其是,主框架怎样才能接收到 NM_CUSTOMDRAW 通知而又反射回列表?答案在 CSplitterWindowImpl 的消息映射中可以找到:

最后面的 FORWARD_NOTIFICATIONS() 宏很重要。回忆一下第四部分, 有好几个通知消息,总是会发送到子窗口的父窗口。 FORWARD_NOTIFICATIONS() 所做的事情就是把消息重新发送给分隔条的父窗口。所以,在列表把 WM_NOTIFY 消息发送给分隔条(也即列表的父窗口)的时候,分隔条则依次把 WM_NOTIFY 消息发送给主框架(分隔条的父窗口)。在主框架窗口反射消息的时候,消息也会被发送回最初产生 WM_NOTIFY 消息的窗口里,也就是列表自己,所以分隔条并不干涉反射。

所有这些的结果就是发送于主框架和列表间的通知消息并不会因为有分隔条窗口的存在而受到影响。这就使得添加或者去处分隔条都相当的容易,因为子窗口类根本不需要为了能使其消息处理可以继续工作而作任何改动。

窗格容器

WTL 还支持一种窗口部件(widget),就好比在资源管理器的左窗格中的那个一样,称为窗格容器。此控件提供了一个带有文字的题头区域,以及一个可选的关闭按钮:

 [Explorer pane container - 3K]

如同分隔条窗口管理着两个窗格窗口一样,窗个容器管理了一个子窗口。在容器被改变大小时,其子窗口也自动改变大小以匹配容器内部的空间。

窗格容器的实现有两个类,都在 atlctrlx.h 里: CPaneContainerImplCPaneContainerCPaneContainerImpl 是一个 CWindowImpl 的派生类,其中包含了全部的实现, CPaneContainer 仅仅提供了一个窗口的类名。除非你要覆盖某些方法以改变容器的绘制,通常只要用 CPaneContainer 即可。

基本方法

创建 CPaneContainer 与创建其它子窗口一样。有两个 Create() 方法,不过仅有第二个参数不同。在第一个版本里,你传递的是一个字符串,用于题头中显示的标题文字,在第二个版本里,要传递的是一个字符串表项的 ID。至于其余的参数,缺省值通常就足够了。

CPaneContainer 还有其他的扩展风格,可以控制关闭按钮以及容器的布局:

  • PANECNT_NOCLOSEBUTTON:设置此风格可以从题头上把关闭按钮去掉
  • PANECNT_VERTICAL:设置此风格会使得题头区域为沿着容器窗口的左边缘垂直放置

此扩展风格的缺省值为 0,也即意味着是一个带有关闭按钮的水平容器。

调用 SetClient() 可以把一个子窗口关联到窗格容器。这与 CSplitterWindowSetSplitterPane() 方法很类似。 SetClient() 的返回值是旧的客户窗口的 HWND。调用 GetClient() 可以得到当前客户窗口的 HWND

调用 SetTitle() 可以改变显示在容器的题头区域内的文字。调用 GetTitle() 可以得到当前的题头文字,而调用 GetTitleLength() 则可以得到当前题头文字以字符为单位的长度(不包括 null 结束符)。

如果窗格容器具有关闭按钮,你可以使用 EnableCloseButton() 来启用或者禁用它。

在分割条窗口中使用窗格容器

为了演示如何向一个现存的分割条中添加窗格容器,我们来向 ClipSpy 的分割条的左窗格里添加一个容器。我们不把列表控件关联到左窗格上,而代之以窗格容器。然后再把列表关联到窗格容器上。下面是在 CMainFrame::OnCreate() 中改变了的代码,用以设置窗格容器。

注意,列表控件的父是 m_wndPaneContainer,而且, m_wndPaneContainer 被设置为了分割条的左窗格。下面就是改动后的左窗格的样子。

 [Pane container - 5K]

关闭按钮以及消息处理

当用户点击了关闭按钮时,窗格容器会向其父窗口发送 WM_COMMAND 消息,命令 ID 为 ID_PANE_CLOSE(为定义在 atlres.h 中的一个常量)。如果你在分割条中使用了窗格容器,则通常的动作是调用 SetSinglePaneMode() 来隐藏窗格容器所在的分割条窗格。不过要记住,还应该给用户提供再把它显示出来的方法。

CPaneContainer 的消息映射中也有 FORWARD_NOTIFICATIONS() 宏,就像 CSplitterWindow 一样,所以容器会把通知消息从其客户窗口传递到其父窗口。在 ClipSpy 中,尽管在列表控件和主框架之间有两个窗口,即窗格容器和分割条,但是 FORWARD_NOTIFICATIONS() 宏却使得从列表中发出的所有通知都可以到达主框架。

高级分割条特性

在这一节里,我会介绍如何使用 WTL 的分割条来做一些常见的高级 UI 技巧。

嵌套分割条

如果你计划写一个类似于邮件客户端或者 RSS 阅读器的应用,最终你就可能会使用到嵌套分割条,一个水平的和一个垂直的。用 WTL 的分割条来做这件事相当容易 – 只需把一个分割条创建为另一个的子窗口。

出于演示目的,我们要为 ClipSpy 添加一个水平分割条。水平分割条处于最顶级,垂直分割条将嵌套于其中。在添加完名为 m_wndHorzSplitterCHorSplitterWindow 类型成员之后,我们要像创建 m_wndVertSplitter 一样把它创建出来。因为要使 m_wndHorzSplitter 作为顶级分隔条, m_wndVertSplitter 只好作为 m_wndHorzSplitter 的子窗口创建。最后, m_hWndClient 设置为 m_wndHorzSplitter,从而使得此窗口现在占据了主框架的客户区。

下面就是成果:

 [Horz splitter w/empty pane - 5K]

在窗格中使用 ActiveX 控件

在分割条窗格中掌控 ActiveX 控件与在对话框中掌控控件类似。你可以在运行时使用 CAxWindow 的方法创建控件,然后再将此 CAxWindow 关联到分割条的一个窗格上。下面显示了如何把浏览器控件添加到水平分割条的下窗格中:

特殊绘制

如果你要为分割条提供不同的外观,比如说要在其上绘制纹理,你可以从 CSplitterWindowImpl 派生一个类并覆盖 DrawSplitterBar()。如果你只是想改变一下外观,你可以将 CSplitterWindowImpl 中现成的函数复制过来,再做一些所希望的小小的改动。下面是一个例子,在分割条上绘制了斜纹图案。

下面就是结果,分隔条被拓宽了,以便能更容易地看到效果:

 [custom drawn bars - 14K]

窗格容器中的特殊绘制

CPaneContainer 有几个可以覆盖的方法,用以改变窗格容器的外观。你可以从 CPaneContainerImpl 派生新类并覆盖你想要覆盖的方法,例如:

更有意思的几个方法是:

CalcSize() 的目的只是用来设置 m_cxyHeader,它控制着容器的题头区域的宽度(或者是高度)。不过,在 SetPaneContainerExtendedStyle() 中有一个小错误,该错误会导致当窗格在水平和垂直模式间切换时派生类的 CalcSize() 不会被调用到。你可以通过把 CalcSize() 调用改为 pT->CalcSize() 来修正此错误,位置在 atlctrlx.h 的 2215 行。

此方法返回一个用于绘制题头文字的 HFONT。缺省值由 GetStockObject(DEFAULT_GUI_FONT) 返回,其实就是 MS Sans Serif 字体。如果你希望使用看起来更现代一些的字体 Tahoma,你可以覆盖 GetTitleFont() 并返回一个你所创建的 Tahoma 字体的句柄。

覆盖此方法可以提供鼠标悬停于关闭按钮时所需的工具提示文字。此方法其实是 TTN_GETDISPINFO 的处理器,因而你可以将 lpnmh 转型为 NMTTDISPINFO* 并相应设置该结构中的成员。需要记住的是你应该检查通知代码 – 它既可能是 TTN_GETDISPINFO 也可能是 TTN_GETDISPINFOW – 然后再据此访问该结构。

你可以覆盖此方法以提供题头区域的自绘制。可以使用 GetClientRect()m_cxyHeader 来计算题头区域的 RECT。下面是一段示例代码,在一个水平容器的题头区域里作了渐变填充绘制:

示例工程演示了覆盖这些方法中的几个,其结果显示在这儿:

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

如上所示,示例工程有一个 Splitters 菜单,允许你切换分隔条以及窗格容器的几种特殊绘制,以便你可以看到其间的区别。你还可以锁定分割条,这是通过切换 SPLIT_NONINTERACTIVE 扩展风格来完成的。

奖励:状态栏中的进度条

正如我早在上几篇文章之前所允诺过的,这一新的 ClipSpy 会演示如何在状态栏上创建进度条。此工作与 MFC 版本一样 – 所需的步骤有:

  1. 得到第一个状态栏窗格的 RECT
  2. 创建一个进度条控件,作为状态栏的子窗口,将其 RECT 设置为上述窗格的 RECT
  3. 在填充编辑器控件的同时更新进度条的位置

可以到 CMainFrame::CreateProgressCtrlInStatusBar() 中查看代码。

下一步

在第八部分里,我将介绍有关属性表和向导的话题。

参考资料

WTL Splitters and Pane Containers – Ed Gadziemski

修订历史

2003 年 7 月 9 日:首次发布
2006 年 1 月 12 日:主要修正了文中不清楚或者用词不当的地方。更新了一些屏幕截图。添加了有关 WS_EX_CLIENTEDGE 的一节。

链接:上一部分下一部分

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注