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...