Divide and Conquer with Splitters
我们在前面介绍的 Multi-SDI 和 MDI 都是在单一的应用程序中对应用程序的数据采用多个视图显示. 另外一种流行的方式是用分割条把窗口分割为一个或多个子窗口. WTL 的 CSplitterImpl 和 CSplitterWindowImpl 类提供对分割窗口的支持. WTL 分割窗口的核心是 CSplitterImpl 类, 它实现了诸如创建分割窗口, 调整分割条两边面板大小等等所有必需的魔幻般的神奇功能. CSplitterImpl 类是作为基类来使用的, 必须从它派生自己的类. 而且 CSplitterImpl 类的派生类必须同时派生自 CWindowImpl 类(或者派生自 CMessageMap 类具有消息处理接口). 尽管 CSplitterImpl 类有自己的消息映射, 但是必须在其派生类中通过 CHAIN_MSG_MAP 或其它消息映射链链接到 CSplitterImpl 类才能捕获并进行处理.
//atlsplit.h
template <class T, bool t_bVertical = true>
class CSplitterImpl
{…
BEGIN_MSG_MAP(thisClass)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint)
if(IsInteractive())
{
MESSAGE_HANDLER(WM_SETCURSOR, OnSetCursor)
MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove)
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
MESSAGE_HANDLER(WM_LBUTTONUP, OnLButtonUp)
MESSAGE_HANDLER(WM_LBUTTONDBLCLK,
OnLButtonDoubleClick)
}
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
MESSAGE_HANDLER(WM_MOUSEACTIVATE, OnMouseActivate)
MESSAGE_HANDLER(WM_SETTINGCHANGE, OnSettingChange)
END_MSG_MAP()
…
…
};
下列代码演示了如何使用分割条将我们早期创建的 SDI 框架窗口分割为拥有两个 HTML 视图的分割窗口.
class CMainFrame :
public CFrameWindowImpl<CMainFrame>,
public CSplitterImpl<CMainFrame,true>
{
public:
DECLARE_WND_CLASS_EX(NULL, CS_DBLCLKS, COLOR_WINDOW)
typedef CFrameWindowImpl<CMainFrame> winbaseClass;
typedef CSplitterImpl< CMainFrame,true> splitbaseClass;
BEGIN_MSG_MAP(CMainFrame)
…
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_SIZE, OnSize)
CHAIN_MSG_MAP(splitbaseClass)
END_MSG_MAP()
//…
private: //the two panes within the splitter
CHtmlView m_ViewLeft;
CHtmlView m_ViewRight;
…
};
在 WM_CREATE 消息映射函数中创建好了两个视图类, 因为我们的框架窗口类 CMainFrame 是 CSplitterImpl 的派生类因此, 直接调用成员函数 CSplitterImpl::SetSplitterPanes 把两个视图类设置为分割窗口的两个面板. 最后调用 CSplitterImpl::SetSplitterPos 设置分割条位置, 分割条位置决定了两个面板的大小.
LRESULT CMainFrame::OnCreate(UINT, WPARAM,LPARAM, BOOL& bHandled)
{
…
// First the 2 views (panes)
m_ViewLeft.Create(m_hWnd, rcDefault,
_T("http://www.sellsbrothers.com/tools"), WS_CHILD |
WS_VISIBLE | WS_CLIPSIBLINGS,
WS_EX_STATICEDGE);
m_ViewRight.Create(m_hWnd, rcDefault,
_T("http://www.sellsbrothers.com/comfun"), WS_CHILD |
WS_VISIBLE | WS_CLIPSIBLINGS|WS_VSCROLL | WS_CLIPCHILDREN,
WS_EX_STATICEDGE);
// Link the panes to the splitter
SetSplitterPanes(m_ViewLeft, m_ViewRight);
// Set the initial positions
RECT rc;GetClientRect(&rc);
SetSplitterPos((rc.right - rc.left) / 4);
bHandled = FALSE;
return 0;
}
当用户调整窗口大小时, WM_SIZE 消息映射函数需要计算可用的客户区大小并减去复合条控件(Rebar)所占区域然后调用CSplitterImpl::SetSplitterRect 重置分割条位置. 注意, 窗口最小化时并不需要这样做, 实际上什么都不做. 当然, 此消息还需要进行默认处理, 因此设置 bHandled = FALSE.
LRESULT CMainFrame::OnSize(UINT, WPARAM wParam, LPARAM, BOOL& bHandled)
{
if(wParam != SIZE_MINIMIZED)
{
RECT rc; GetClientRect(&rc);
RECT rcBar; ::GetClientRect(m_hWndToolBar,&rcBar);
rc.top = rcBar.bottom;
SetSplitterRect(&rc);
}
bHandled = FALSE;
return 1;
}
最后, 为了在拖动窗口大小时避免窗口闪烁, CMainFrame 处理了 WM_ERASEBKGND 消息, 仅仅是返回一个非零值, 则此消息的处理就到此为止, 忽略掉 DefWindowProc 的默认处理, 因为 DefWindowProc 对 WM_ERASEBKGND 消息的处理是用窗口类的背景画刷刷新需要更新的背景, 因此会导致屏幕闪烁.
译者注:在消息映射宏的定义中我们看到, 如果某一消息定义了消息映射, 并且在消息映射函数中设置了 bHandled=FALSE , 那么虚拟消息处理函数 virtual BOOL ProcessWindowMessage() 将返回 FALSE, 否则返回 TRUE;如果某一消息没定义消息映射, 则虚拟消息处理函数必然返回 FALSE, 窗口处理过程将会在消息映射链中寻找其它处理途径. 因此 bHandled 变量决定了虚拟消息处理函数 virtual BOOL ProcessWindowMessage() 的返回值, 而此函数的返回值又决定了在消息映射链中是否对此消息做进一步处理, 而消息映射函数的返回值则是作为虚拟消息处理函数的一个参数被返回的.
LRESULT CMainFrame::OnEraseBackground(UINT, WPARAM, LPARAM, BOOL&)
{
return 1;
}
Figure11a 展示了上述使用 CSplitterImpl 的情况, 也就是需要把客户区分割成两个面板的窗口类只需要从 CSplitterImpl 类派生即可.
Figure 11a: CSplitterImpl in the Inheritance Scenario
如果从 CSplitterImpl 类派生的话必须处理 WM_ERASEBKGND 和 WM_SIZE 消息, 每次都这样, 而且做同样地事, 这是很痛苦的. 幸好, WTL 添加了另一个抽象层, 在 CSplitterImpl 和派生的窗口类之间提供了 CSplitterWindowImpl 类专门用来处理这两个消息. 同时 CSplitterWindowImpl 类把通知消息反射给父窗口处理, 把未处理的普通消息链接到 CSplitterImpl 处理.
//atlsplit.h
template <class T, bool t_bVertical = true, class TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CSplitterWindowImpl :
public CWindowImpl< T, TBase, TWinTraits >,
public CSplitterImpl< CSplitterWindowImpl<T ,
t_bVertical, TBase, TWinTraits >,
t_bVertical>
{
public:
DECLARE_WND_CLASS_EX(NULL, CS_DBLCLKS, COLOR_WINDOW)
typedef CSplitterWindowImpl< T , t_bVertical, TBase, TWinTraits > thisClass;
typedef CSplitterImpl<
CSplitterWindowImpl<T,t_bVertical,TBase, TWinTraits >,
t_bVertical>baseClass;
BEGIN_MSG_MAP(thisClass)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_SIZE, OnSize)
CHAIN_MSG_MAP(baseClass)
FORWARD_NOTIFICATIONS()
END_MSG_MAP()
LRESULT OnEraseBackground(UINT,WPARAM, LPARAM, BOOL&){return 1;}
LRESULT OnSize(UINT, WPARAM wParam, LPARAM, BOOL& bHandled)
{
if(wParam != SIZE_MINIMIZED)
SetSplitterRect();
bHandled = FALSE;
return 1;
}
};
你可以在垂直方向和水平方向搭配使用 WTL 的分割窗口分割客户区, 形成嵌套的网格状的多视图窗口. 本书附带的示例程序中, NestedSplitters 展示了嵌套的分割窗口的创建. (见 Figure 11b)
Figure 11b: NestedSplitters Sample Application
如果你想要以组合的方式使用分割窗口控件而不是从分割窗口类继承的话, WTL 提供的 CSplitterWindow 和 CHorSplitterWindow 可以满足你的要求. 下面的示例展示了在窗口类的 COM 控件中使用分割窗口类 CSplitterWindow, 这个示例是用旧版本的 ATL COM AppWizard 生成的( 译者注:相关知识可查阅 ATL 以及组件对象模型的书籍 )
class ATL_NO_VTABLE CFooControl :
public CComObjectRootEx<CComSingleThreadModel>,
public CComControl<CFooControl>,…
{
CFooControl()
{
m_bWindowOnly = TRUE;
}
BEGIN_MSG_MAP(CFooControl)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
CHAIN_MSG_MAP(CComControl<CFooControl>)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
// Message handlers and other usual suspects
private:
CSplitterWindow m_splitter;
CHtmlView m_LeftView;
CHtmlView m_RightView;
};
LRESULT CFooControl::OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
AtlAxWinInit();
//Create the Left and right views
m_LeftView.Create(m_hWnd, rcDefault, _T("http://www.microsoft.com"),WS_CHILD | WS_VISIBLE |
WS_CLIPSIBLINGS | WS_CLIPCHILDREN,WS_EX_STATICEDGE);
m_RightView.Create(m_hWnd, rcDefault, _T("http://www.microsoft.com"),WS_CHILD | WS_VISIBLE |
WS_CLIPSIBLINGS | WS_CLIPCHILDREN,WS_EX_STATICEDGE);
RECT rect; GetClientRect(&rect);
//Create the splitter object
m_splitter.Create(m_hWnd, rect, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
m_splitter.SetSplitterPanes(m_LeftView, m_RightView);
m_splitter.SetSplitterPos((rect.right - rect.left) / 4);
return 0L;
}
最后, WTL 分割窗口类可以在一个面板中动态切换不同的视图, 只需要调用 CSplitterImpl::SetSplitterPane()成员函数 并传递一个不同的视图窗口句柄. 你可以 设置 数据成员变量 m_cxyMin 并调用 CSplitterImpl::SetSplitterPos() 来限制分割面板的最小尺寸以及固定分割条的位置.
GDI Wrappers
To be continued...