理解 CDockablePane






4.95/5 (79投票s)
CDockablePane 的良好参考
目录
简介
可对接窗格是一个通用的窗口容器,类似于视图,它具有两种相对于对接性的状态:已对接或浮动在迷你框架中。与视图的主要区别在于,视图用于显示应用程序的主内容,而窗格提供与视图相关的上下文内容。例如,Visual Studio 中的工具箱窗格在您将新对话框插入项目时始终处于活动状态并填充有控件,否则它将显示为空窗格。
可对接窗格是一个至关重要的窗口,需要支持复杂的应用程序布局,以便随时显示或隐藏,为您的应用程序桌面提供额外的空间。
惯用语澄清
在本文中,我将使用 CMainFrame
一词来指向您从 CFrameWndEx
(或 CMDIFrameWndEx
)派生的类,并隐式地使用“窗格”一词来指代 CDockablePane
。当我使用 CTreePane
时,表示一个从 CDockablePane
派生的类,它包含一个树形控件作为主子窗口,以此类推 CListPane
等。
基本用法
派生您自己的类
要将可对接窗格添加到您的项目中,第一步是派生一个新类自 CDockablePane
,并且您必须为 OnCreate
和 OnSize
添加消息处理程序,并添加一个成员子窗口作为主内容。您的简单 CTreePane
类应如下所示:
class CTreePane : public CDockablePane
{
DECLARE_MESSAGE_MAP()
DECLARE_DYNAMIC(CTreePane)
protected:
afx_msg int OnCreate(LPCREATESTRUCT lp);
afx_msg void OnSize(UINT nType,int cx,int cy);
private:
CTreeCtrl m_wndTree ;
};
并且您的 OnCreate
事件处理程序应调用基类实现并创建您的子树,如下所示:
int CTreePane::OnCreate(LPCREATESTRUCT lp)
{
if(CDockablePane::OnCreate(lp)==-1)
return -1;
DWORD style = TVS_HASLINES|TVS_HASBUTTONS|TVS_LINESATROOT|
WS_CHILD|WS_VISIBLE|TVS_SHOWSELALWAYS | TVS_FULLROWSELECT;
CRect dump(0,0,0,0) ;
if(!m_wndTree.Create(style,dump,this,IDC_TREECTRL))
return -1;
return 0;
}
在 OnSize
处理程序中,您应该将控件的大小调整为填充整个可对接窗格的客户端区域。否则,在显示之前,您会看到窗格下方的内容,因为可对接窗格使用浅色(Null
)画笔注册其窗口类,该画笔会擦除背景,同样的原因,如果您决定不填充整个客户端区域,您应该处理 OnPaint
来绘制剩余的客户端区域。
void CTreePane::OnSize(UINT nType,int cx,int cy)
{
CDockablePane::OnSize(nType,cx,cy);
m_wndTree.SetWindowPos(NULL,0,0,cx,cy, SWP_NOACTIVATE|SWP_NOZORDER);
}
在 CFrameWnd 中准备窗格
要在您的框架中支持可对接窗格,您必须首先从 Ex 系列框架(CFrameWndEx
、CMDIFrameWndEx
等)派生,并在 OnCreate
处理程序中,您应该通过设置允许的对接区域、通用属性、智能对接模式等来初始化对接管理器。
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
CDockingManager::SetDockingMode(DT_SMART);
EnableAutoHidePanes(CBRS_ALIGN_ANY);
...
}
下一步是为主框架添加一个窗格成员变量,并在构造函数中将其默认值设置为 null
,并为主框架添加一个命令处理程序来显示窗格。
void CMainFrame::OnTreePane()
{
if(m_treePane && m_treePane->GetSafeHwnd())
{
m_treePane->ShowPane(TRUE,FALSE,TRUE);
return ;
}
m_treePane = new CTreePane;
UINT style = WS_CHILD | CBRS_RIGHT |CBRS_FLOAT_MULTI;
CString strTitle = _T("Tree Pane");
if (!m_treePane->Create(strTitle, this,
CRect(0, 0, 200, 400),TRUE,IDC_TREE_PANE, style))
{
delete m_treePane;
m_treePane = NULL ;
return ;
}
m_treePane->EnableDocking(CBRS_ALIGN_ANY);
DockPane((CBasePane*)m_treePane,AFX_IDW_DOCKBAR_LEFT);
m_treePane->ShowPane(TRUE,FALSE,TRUE);
RecalcLayout();
}
由于可对接窗格继承自控件条,因此您可以将所有控件条的样式应用于可对接窗格。两种有趣的样式很重要:
CBRS_FLOAT_MULTI
使可对接窗格在附加到选项卡时作为整体浮动。- 像
CBRS_LEFT
这样的对齐样式为窗格提供初始对齐。
DockPane
函数然后将您的窗格对接到底部框架的选定侧,要使您的窗格彼此相对对接,请使用:
m_treePane ->DockToWindow(m_listPane,CBRS_ALIGN_BOTTOM)
要使您的窗格最初处于 float
状态,请使用 FloatPane
函数和屏幕坐标矩形:
m_treePane->FloatPane(rect);
要使其最初自动隐藏,请使用 ToggleAutoHide
函数。
标签页窗格是将窗格对接在一起形成常规选项卡控件的概念,其中包含各个窗格。应用某些命令将影响所有窗格,例如自动隐藏,而其他命令将仅影响活动窗格,例如关闭。要将您的 CListPane
添加到先前创建的 CTreePane
,您只需添加另一行:
// make list pane attach to treepane if it's already created
CDockablePane* pTabbedBar = NULL;
if(m_listPane && m_listPane->GetSafeHwnd())
m_treePane->AttachToTabWnd(m_listPane, DM_SHOW, TRUE,&pTabbedBar);
要使您的选项卡具有 Outlook 样式,请在创建窗格时将 AFX_CBRS_OUTLOOK_TABS
作为第七个参数传递给 Create
函数。
关闭时销毁窗格
从其标题栏关闭可对接窗格只会隐藏窗格,而不会销毁它。要在关闭窗格时销毁它,您必须添加一个处理程序到预注册的 MFC 消息 AFX_WM_ON_PRESS_CLOSE_BUTTON
,该消息从可对接窗格发送到其父框架,在其中 OnLButtonDown
消息的中间。
ON_REGISTERED_MESSAGE(AFX_WM_ON_PRESS_CLOSE_BUTTON,OnClosePane)
LRESULT CMainFrame::OnClosePane(WPARAM,LPARAM lp)
{
CBasePane* pane = (CBasePane*)lp;
int id = pane->GetDlgCtrlID();
pane->ShowPane(FALSE, FALSE, FALSE);
RemovePaneFromDockManager(pane,TRUE,TRUE,TRUE,NULL);
AdjustDockingLayout();
pane->PostMessage(WM_CLOSE);
PostMessage(WM_RESETMEMBER,id,0);
return (LRESULT)TRUE;//prevent close , we already close it
}
首先,我们隐藏窗格,然后将其从对接管理器中移除,然后调整框架的对接布局。之后,我们向窗格发送一个 WM_CLOSE
消息,而不是直接发送,因为此消息是在 OnLButtonDown
的中间生成的,并且处理程序必须有效才能完成消息处理程序。
WM_CLOSE
将生成 WM_DESTROY
消息来销毁窗格。之后,我发送我自己的注册消息 WM_RESETMEMBER
来删除我的成员变量并将其值重置为 NULL
。并且您应该始终返回 true
来阻止关闭,因为我们已经关闭了它,并且关闭它会使 CDockablePane
在尝试隐藏具有无效句柄的窗格时感到意外,并可能导致异常和崩溃。
LRESULT CMainFrame::OnResetMember(WPARAM wp,LPARAM)
{
int id = (int)wp;
switch(id)
{
case IDC_TREE_PANE:
delete m_treePane;
m_treePane = NULL ;
break;
要阻止窗格完全关闭,只需在创建它时删除 AFX_CBRS_CLOSE
,它将在父框架销毁时被销毁。
CFrameWnd 和 CDockablePane 之间的命令路由
命令路由是将不同类的消息映射链接在一起,以便非窗口对象能够接收和处理消息(例如 CWinApp
和 CDocument
)的概念。该机制由 CCmdTarget
类中定义的虚拟函数 OnCmdMsg
控制。该函数扫描消息映射,如果找到命令的处理程序则返回 true
,否则返回 false
。例如,要禁用所有文档命令,只需在 Document
派生类中重写它,并简单地返回 false
而不调用父实现。另一个用途是多重继承,将您的消息映射链接到多个父级,并优先处理一个父级处理程序而不是另一个。
SDI 框架的默认命令路由:
- 活动视图,然后是附加的文档
- 此框架对象
- 应用程序对象
默认情况下,可对接窗格不接收来自主框架的命令。要添加此功能,请遵循以下步骤:
CList<CBasePane*> m_regCmdMsg;
//register pane as command target
void CMainFrame::RegCmdMsg(CBasePane* pane)
//remove pane from command target list
void CMainFrame::UnregCmdMsg(CBasePane* pane)
调用 RegCmdMsg
的一个好地方是在它首次显示之前,而对于 UnregCmdMsg
,则是在之前的 OnClosePane
处理程序中,在发送关闭消息之前。
BOOL CMainFrame::OnCmdMsg(UINT id,int code , void *pExtra,AFX_CMDHANDLERINFO* pHandler)
{
//route cmd first to registered dockable pane
POSITION pos = m_regCmdMsg.GetHeadPosition();
while (pos)
{
CBasePane* pane = m_regCmdMsg.GetAt(pos);
if(pane->IsVisible() &&
pane->OnCmdMsg(id,code,pExtra,pHandler))
return TRUE;
m_regCmdMsg.GetNext(pos);
}
return CFrameWndEx::OnCmdMsg(id,code,pExtra,pHandler);
}
- 将
CDockablePane
的列表添加为框架类中的成员变量。 - 添加两个成员函数来注册和取消注册可对接窗格作为命令目标。
- 重写
OnCmdMsg
以首先将命令路由到已注册的可对接窗格。 - 在框架的
OnDestroy
处理程序中,移除列表中的所有项。
高级用法
CDockablePane 中的 ActiveX
如果可对接窗格可以包含控件,那么它可以包含任何子对话框或视图,甚至 ActiveX。在我的示例中,我实现了 PdfPane
,它托管 Acrobat ActiveX 并接收主框架的 ID_FILE_OPEN
命令来打开和加载 PDF 文件到控件(请注意,窗格必须处于活动状态才能接收命令事件,并且您的机器上必须安装 Acrobat Reader,加载文件后,您必须双击其客户端区域才能显示文件;此缺陷来自 ActiveX 开发人员,而非我)。
CDockablePane 内的 Splitter 和 Toolbar
添加 Toolbar
Toolbar 被设计为父框架的子窗口,因为 Toolbar 的默认命令路由总是将命令路由到它能找到的第一个父框架。要覆盖默认行为,您必须派生一个新类并重写两个函数:
class CPaneToolBar : public CMFCToolBar
{
virtual void OnUpdateCmdUI(CFrameWnd*, BOOL bDisableIfNoHndler)
{
CMFCToolBar::OnUpdateCmdUI((CFrameWnd*)
GetOwner(),bDisableIfNoHndler);
}
virtual BOOL AllowShowOnList() const { return FALSE; }
};
AllowShowOnList
函数可防止您的 Toolbar 出现在 Toolbar 自定义对话框中,而 OnUpdateCmdUI
将使 Toolbar 在窗格消息映射中而不是在框架消息映射中搜索其命令更新例程。
在窗格类中添加 Toolbar 的成员变量后,您可以在 OnCreate
中创建它,如下所示:
if(!m_toolbar.Create(this, AFX_DEFAULT_TOOLBAR_STYLE, IDR_TREETOOLBAR))
return -1;
m_toolbar.LoadToolbar(IDR_TREETOOLBAR);
m_toolbar.SetOwner(this);
// All commands will be routed via this control ,
// not via the parent frame:
m_toolbar.SetRouteCommandsViaFrame(FALSE);
调整 Toolbar 的大小是一个直接的过程:
void TreePane::OnSize(UINT type,int cx,int cy)
{
CDockablePane::OnSize(type, cx, cy);
int cyTlb = m_toolbar.CalcFixedLayout(FALSE, TRUE).cy;
CRect rectClient;
GetClientRect(rectClient);
m_toolbar.SetWindowPos(NULL, rectClient.left, rectClient.top,
rectClient.Width(), cyTlb,SWP_NOACTIVATE | SWP_NOZORDER);
m_tree.SetWindowPos(NULL,rectClient.left, rectClient.top + cyTlb,
rectClient.Width() , rectClient.Height() - cyTlb , SWP_NOZORDER | SWP_NOACTIVATE);
}
向窗格添加 SpliteWnd
Splitter 是一个窗口,用于将特定窗口划分为多个可调整大小的区域(行和列);就像 Toolbar 是为与主框架一起工作而设计的,Splitter 是为与视图一起工作而设计的。为了使其与常规控件一起工作,我们必须对其进行扩展并添加一个成员函数来创建并添加要拆分的窗口:
class CPaneSplitter : public CSplitterWndEx
{
public :
BOOL AddWindow(int row, int col, CWnd* pWin,CString clsName,
DWORD dwStyle,DWORD dwStyleEx, SIZE sizeInit);
};
BOOL CPaneSplitter::AddWindow(int row, int col, CWnd* pWnd ,
CString clsName , DWORD dwStyle,DWORD dwStyleEx, SIZE sizeInit)
{
m_pColInfo[col].nIdealSize = sizeInit.cx;
m_pRowInfo[row].nIdealSize = sizeInit.cy;
CRect rect(CPoint(0,0), sizeInit);
if(!pWnd->CreateEx(dwStyleEx,clsName,NULL,dwStyle,rect,this,IdFromRowCol(row, col)))
return FALSE;
return TRUE;
}
在我随附的示例中,我创建了一个 CSplitePane
,其中包含一个 shell 列表和一个 shell 树,由 splitter 窗口分隔。
常见问题
上下文菜单问题
当您创建可对接窗格时,即使右键单击客户端区域,也会出现一个烦人的上下文菜单。要使此菜单消失,您必须重写 OnShowControlBarMenu
并返回 TRUE
。要仅在单击标题栏时显示它,请使用以下代码:
BOOL TreePane::OnShowControlBarMenu(CPoint pt)
{
CRect rc;
GetClientRect(&rc);
ClientToScreen(&rc);
if(rc.PtInRect(pt))
return TRUE;//hide a pane contextmenu on client rea
//show on caption bar
return CDockablePane::OnShowControlBarMenu(pt);
}
智能对接模式
您可以在框架的 OnCreate
处理程序中调用 CDockingManager::SetDockingMode(DT_SMART)
来支持 VS2005 对接样式。当您的应用程序支持不同的外观(如 Office2007 蓝色和黑色主题)时,问题就会出现。当应用程序主题更改时,对接模式会自动恢复到其默认值(DM_STANDARD
)。这发生在调用 SetDefaultManager
时,因此在设置新外观后,您必须再次将对接模式设置为 Smart:
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerOffice2007));
CDockingManager::SetDockingMode(DT_SMART);
两个类包含 SetDockMode
函数:您的窗格和对接管理器。将对接管理器设置为标准,将您的窗格设置为智能,将限制您的窗格只能对接到底部框架的四个侧面,并将阻止对接至另一个窗格,并将阻止通过拖动进行浮动。
在对话框内对接
将可对接窗格创建为对话框的子窗口不被直接允许,但有两种解决方法:
- 使用商业产品,如 Codejock 库,其中包含用于对话框的特殊对接管理器。
- 创建一个框架窗口(无边框、菜单栏或工具栏)作为对话框的子窗口,并将可对接窗格添加到该框架,并将窗体视图作为主内容。用户会感觉可对接窗格已对接到底部对话框(请参阅示例代码中的框架对话框)。
如果建议重要,这两种方法都强烈不推荐。在对话框中使用可对接窗格时,使对话框的完整提示会分散用户对主要问题的注意力,而对话框旨在回答用户直接而简单的问题。您应该考虑重新设计您的任务为多任务处理(记住,分而治之)。
有时,复杂的对话框是不可避免的。我在示例中添加了一个非常复杂的对话框(电话簿),其中包含五个子对话框,以启发您。
在视图内对接
类似于在对话框内对接的解决方案可能有效,但不推荐,并且可以被两种方法替代:
- 添加一个父视图并将其分割成行和列的视图,当主视图关闭时,所有子视图也会关闭。
- 使用普通视图并在
OnInitialUpdate
中,创建一个新的可对接窗格,以框架为父窗口,并通过存储引用成员变量在两个对象之间建立相互依赖关系。当关闭其中任何一个时,让另一个知道并相应地更新自身。
窗格和 WM_GETMINMAXINFO
您无法使用此消息限制窗格的最大和最小尺寸,就像在常规窗口中一样。当我尝试时,消息处理程序从未被调用。窗格允许您通过调用 SetMinSize(CSize(100,100))
来设置最小尺寸。只有当窗格未附加到选项卡式窗格时,您的最小尺寸才会生效。
对接至桌面窗口
您无法创建基于可对接窗格的应用程序,就像基于对话框的应用程序一样,因为可对接窗格期望一个框架作为父窗口(Ex 系列),该框架包含对接管理器作为成员,这对可对接窗格正确运行至关重要。
为什么需要对接至桌面?
假设您需要向用户显示天气或 RSS 提要、新闻标题、体育赛事等。
我遵循了以下步骤但失败了,我在此列出它们,因为您可能比我更了解,并可以在下面的仪表板中帮助我修复它们。
- 创建一个不可见的窗口作为我框架的父窗口(
WS_POPUP | WS_EX_TOOLWINDOW
)以防止任务栏按钮。 - 创建我的框架并使其初始以全屏模式运行并隐藏它。
- 尝试将我的窗口对接到底部框架的右上角,但什么也没出现。
另一种可能有效的解决方案,我没有尝试过:
- 创建无边框、无菜单、无工具栏的平面框架。
- 将其大小调整为占用桌面屏幕宽度的 1/4,并将其位置设置在桌面的右侧。
- 创建并显示对接窗格,并将其大小调整为占用整个框架区域;当可对接窗格调整大小时,相应地调整您的框架大小,反之亦然。
- 当您自动隐藏时,将您的框架缩小到只占用控件条的宽度和整个屏幕的高度。
- 关闭您的窗格时,同时关闭您的框架。
提示
GetDockingManager()->DisableRestoreDockState(TRUE);
- 要禁用从注册表中加载对接布局,请在框架构造函数中进行以下调用:
- 始终将您的可对接窗格析构函数定义为
virtual
,这将节省大量调试时间和可能的内存泄漏。 - 要阻止用户对接您的窗格,请将
CBRS_NOALIGN
传递给窗格的EnableDocking
函数。
结论
我写这篇文章的主要原因是 Feature Pack 类文档的不足,以及 MSDN 中这句话让我非常生气:
“此主题包含是为了完整性。有关更多详细信息,请参阅位于 Visual Studio 安装的 VC\atlmfc\src\mfc 文件夹中的源代码。”
我希望本文能成为 CDockablePane
的一个很好的参考,希望您喜欢它。
结语
示例的设计考虑了扩展性,请随时询问示例或声明,我会尽快更新源代码示例。
参考文献
- Command Routing 和 TN021: Command and Message Routing
- Hans Dietrich 的 XHtmlTree
CSysImageList
和CFileRegister
类由 dan-g 作为 ToDoList 项目的一部分。- Chris Maunder 的 文章:Creating an application with no taskbar icon