65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (83投票s)

2003年7月11日

20分钟阅读

viewsIcon

407055

downloadIcon

5407

一篇关于使用 WTL 分割窗口和窗格容器的教程。

目录

引言

自从 Explorer 在 Windows 95 中首次亮相,以其双窗格的文件系统视图以来,分割窗口一直是一种流行的 UI 元素。MFC 有一个复杂而强大的分割窗口类,然而它学习起来有些困难,并且与文档/视图框架耦合。在第七部分中,我将讨论 WTL 分割窗口,它比 MFC 的简单得多。虽然 WTL 的分割器实现比 MFC 的功能少,但它更容易使用和扩展。

本部分的示例项目将是 ClipSpy 的重写,当然是使用 WTL 而不是 MFC。如果您不熟悉该程序,请现在查看文章,因为我将在这里复制 ClipSpy 的功能,而不再深入解释它的工作原理。本文的重点是分割窗口,而不是剪贴板。

WTL 分割窗口

头文件 atlsplit.h 包含所有 WTL 分割窗口类。有三个类:CSplitterImplCSplitterWindowImplCSplitterWindowT。下面解释了这些类及其基本方法。

CSplitterImpl 是一个模板类,它接受两个模板参数:一个窗口接口类名和一个布尔值,指示分割器方向:true 表示垂直,false 表示水平。CSplitterImpl 几乎包含了分割器的所有实现,并且许多方法都可以被重写,以便您可以提供分割条的自定义绘制或其他效果。CSplitterWindowImpl 派生自 CWindowImplCSplitterImpl,但代码不多。它有一个空的 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_LEFTSPLIT_PANE_RIGHTSPLIT_PANE_TOPSPLIT_PANE_BOTTOMSPLIT_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_PROPORTIONALSPLIT_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,如下所示:

 [AppWizard pg 2 - 22K]

 [VC7 AppWizard - 23K]

我们不需要视图窗口,因为分割器及其窗格将成为“视图”。在 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())都将正常工作。

如果现在运行应用程序,您将看到分割器正在运行。即使没有为窗格创建任何内容,基本行为也存在。您可以拖动条,双击它会将其移动到中心。

 [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
  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;
}

这是结果的样子,在列表控件添加了一些列之后:

 [Splitter w/panes - 4K]

请注意,分割器对窗格中可以放置的窗口没有任何限制,不像 MFC 中您必须使用 CView。窗格窗口应该至少具有 WS_CHILD 样式,但除此之外,您基本上可以自由使用任何东西。

WS_EX_CLIENTEDGE 的影响

此处需要对 WS_EX_CLIENTEDGE 样式对分割器和窗格中窗口的影响进行一点旁注。我们可以在三个地方应用此样式:主框架、分割窗口或分割窗格中的窗口。WS_EX_CLIENTEDGE 在每种情况下都会创建不同的外观,因此我将在此处进行说明。

框架窗口上的 WS_EX_CLIENTEDGE
这是最不吸引人的选择,因为分割器边框有边缘,但分割条没有边缘。

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

分割窗口上的 WS_EX_CLIENTEDGE
CSplitterWindow 具有 WS_EX_CLIENTEDGE 样式时,绘图代码会额外地沿着分割条的两侧绘制边框,以便每个窗格以及整个分割窗口都有一个边缘。

 [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 消息映射中找到:

  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 左侧窗格中的小部件,称为“窗格容器”。此控件提供一个带有文本的标题区域,并可选择提供一个关闭按钮。

 [Explorer pane container - 3K]

窗格容器管理一个子窗口,就像分割器管理两个窗格窗口一样。当容器调整大小时,子窗口会自动调整大小以匹配容器内的空间。

窗格容器的实现中有两个类,都在 atlctrlx.h 中:CPaneContainerImplCPaneContainerCPaneContainerImpl 是一个派生自 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 被设置为分割器的左窗格。这是修改后的左窗格的样子。

 [Pane container - 5K]

关闭按钮和消息处理

当用户单击“关闭”按钮时,窗格容器会向其父级发送一个 WM_COMMAND 消息,命令 ID 为 ID_PANE_CLOSE(在 atlres.h 中定义的常量)。当您在分割器中使用窗格容器时,通常的做法是调用 SetSinglePaneMode() 来隐藏包含窗格容器的分割器窗格。(但请记住提供一种方法让用户再次显示该窗格!)。

CPaneContainer 消息映射也具有 FORWARD_NOTIFICATIONS() 宏,就像 CSplitterWindow 一样,因此容器将其客户端窗口的通知消息传递给其父级。在 ClipSpy 的情况下,列表控件和主框架之间有两个窗口(窗格容器和分割器),但 FORWARD_NOTIFICATIONS() 宏确保列表的所有通知都到达主框架。

高级分割器功能

本节中,我将描述如何使用 WTL 分割器实现一些常见的用户界面高级技巧。

嵌套分割器

如果您计划编写一个应用程序,例如电子邮件客户端或 RSS 阅读器,您可能会使用嵌套分割器 - 一个水平分割器和一个垂直分割器。这在 WTL 分割器中很容易实现 - 您只需将一个分割器创建为另一个分割器的子窗口。

为了实际演示这一点,我们将在 ClipSpy 中添加一个水平分割器。水平分割器将是顶层的,垂直分割器将嵌套在其中。在添加一个名为 m_wndHorzSplitterCHorSplitterWindow 成员后,我们以与创建 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 );
//...
}

结果如下所示:

 [Horz splitter w/empty pane - 5K]

在窗格中使用 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;

这是结果(为了更容易看到效果,分割条被加宽了):

 [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() 不会被调用。您可以通过将 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_GETDISPINFOTTN_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 );
}

示例项目演示了重写其中一些方法,结果如下所示:

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

演示项目有一个“分割器”菜单,如上所示,它允许您切换分割器和窗格容器的各种特殊绘制功能,以便您可以看到差异。您还可以锁定分割器,这是通过切换 SPLIT_NONINTERACTIVE 扩展样式来完成的。

额外内容:状态栏中的进度条

正如我几篇文章前承诺的,这个新的 ClipSpy 演示了如何在状态栏中创建进度条。它的工作方式与 MFC 版本完全相同——涉及的步骤是:

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

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

接下来

在第八部分,我将探讨属性页和向导的主题。

参考文献

Ed Gadziemski 的WTL 分割器和窗格容器

版权和许可

本文受版权保护,(c)2003-2006 年由 Michael Dunn 撰写。我意识到这并不能阻止人们在网上随意复制它,但我仍然不得不声明。如果您有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译的存在,以便我可以在此处发布链接。

本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码可以造福所有人。(我没有将文章本身发布到公共领域,因为仅在 CodeProject 上提供文章有助于提高我的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,我很乐意收到您的电子邮件(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),但这不是必需的。在您自己的源代码中注明出处也值得赞赏,但不是必需的。

修订历史

  • 2003年7月9日:文章首次发布。
  • 2006年1月12日:主要编辑以修复文章中不清楚或措辞不当的部分。更新了一些屏幕截图。添加了关于 WS_EX_CLIENTEDGE 的部分。

系列导航:《第六部分(托管 ActiveX 控件) | 》第八部分(属性页和向导)

© . All rights reserved.