接上篇, MFC技术系列(四)--Frame窗口(2)
MFC中主要涉及到下面的文件和类:
barcore.cpp (CControlBar)
dockcont.cpp (CDockContext)
bardock.cpp (CDockBar)
winfrm2.cpp (CFrameWnd)
bartool.cpp (CToolBar)
MFC的Frame窗口除了支持Doc-View框架外,还提供了一个重要特性,就是Dock,即:我们可以从CControlBar派生各种Bar,通过CFrameWnd所提供的Dock方法,可以将该Bar Dock到四个方向,或者让其Floating(通过另一个特殊的Frame窗口来支持,后面将会解释)。在MFC提供的默认SDI或者MDI框架代码中,我们能看到类似如下的代码:
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
其中,分别表示起如下作用:
1. 让该tool bar可以dock到任何边
2. 初始化Frame窗口的四个dock bar
3. 将tool bar和dock bar关联起来(做为dock bar的子窗口)
实际上,CFrameWnd提供的Dock功能是由其内部的四个Dock bar所提供的(分别对应四个边)。
先说说CControlBar类,它的定义位于afxext.h中,实现位于barcore.cpp中。在MFC中说到Bar,一般都是指继承该类而来。既然CControlBar是依赖于CFrameWnd,它的m_pDockSite方法即用来保存其所在的Frame窗口对象的指针,这种关联在CControlBar的OnCreate方法中完成,而且Frame窗口也借由它的AddControlBar方法完成对ControlBar的关联(可见这里是双向关联)。当然,在CContolBar销毁(OnDestroy)时,将调用Frame窗口的RemoveControlBar,解除关联。
它的主要功能反映在下面的方法中:
EnableDocking:该方法并不在barcore.cpp中,而是在bardock.cpp中。
下面是两个针对Control Bar尺寸风格进行具体计算的方法,虚方法。这两个方法和对应的风格主要是在CToolBar中应用得较多。该方法的调用入口请参见下面的CFrameWnd的描述:
CalcFixedLayout:对应CBRS_SIZE_FIXED。该风格表现为:无论是停靠,还是浮动状态,其形状都不可改变。
CalcDynamicLayout:对应CBRS_SIZE_DYNAMIC。该风格表现为:如果是浮动状态,那么当用户拖动工具栏的边缘时,工具栏在需要的地方“换行”以改变形状。如果是停靠状态,则不可以改变形状。
消息响应方法:
1.OnPaint
此时,主要由DoPaint虚方法完成绘制。默认包含两个步骤,即:绘制Bar的边缘DrawBorders和绘制Bar的手柄DrawGripper。值得注意的是,CToolBar具有自己的OnPaint方法,它只是处理了延迟layout的情形,然后调用缺省的消息处理(DefWindowProc)。我们知道系统在处理WM_PAINT消息时,将会发送WM_NCPAINT和WM_ERASEBKGND两个消息(在当前场景下,background erase消息是在EraseNonClient方法中发出的)。因此,实际上,CToolBar是在OnNcPaint方法中完成绘制,不过它并没有自己的实现,该方法中仅仅是调用CControlBar的EraseNonClient方法。在EraseNonClient方法中,由于是要处理非客户区,所以使用了CWindowDC,而且,剪切掉了客户区域,只留下非客户区域供绘制(使用CDC的ExcludeClipRect方法,具体请参见后续文章《DC和绘图》),具体绘制的内容仍然是Border和Gripper(也是调用上述两个方法)。
2. OnWindowPosChanging
对消息WM_WINDOWPOSCHANGING的响应,此时,根据CBRS_BORDER系列风格,将right,或bottom边缘处理为无效区域。
该消息的产生应当区别停靠和浮动状态:
a. 停靠状态:此时,CControlBar为子窗口,当移动Bar时,其position发生了变化,故会产生该消息;
b. 浮动状态:此时,CControlBar同样为子窗口。但你所移动的为其外面的Frame窗口,所以如果仅仅是移动,将不会产生该消息。只有改边Frame窗口尺寸时,Bar尺寸和position也被更改,才会产生该消息。
这里默认并没有调用基类的方法,而是直接调用了DefWindowProc API。原因是:基类将会调用具体窗口的消息处理函数,即:CWnd中的m_pfnSuper成员,但是某些控件并不能正确处理WM_WINDOWPOSCHANGING消息,比如:带有TBSTYLE_FLAT 风格的CToolBar。所以,直接采用了上述API,它使用系统缺省的消息处理函数,处理所有未处理的消息。
说到该消息,不能不提到下面三个方法:
BeginDeferWindowPos、DeferWindowPos、EndDeferWindowPos。它们常用来处理多个窗口协同的尺寸变化。它实际上是同时记录了多个窗口的位置信息(使用HDWP 结构),在最后一起来改变它们。
3. OnSizeParent
响应父窗口尺寸变化。该消息为MFC私有消息,由父窗口发送到子窗口。在Dock体系中,父窗口就是该Control Bar所在的Frame窗口。
4. OnLButtonDown
鼠标按下,调用CWnd的OnToolHitTest方法判断是否点击为空白区域(没有点击到任何子窗口)时,准备拖动Bar。此时,将委托给其内部的CDockContext实例的StartDrag方法。那么什么时候结束拖动呢? CDockContext帮我们处理了拖动到结束的所有绘画细节。详细内容请看“CDockContext类”一节。
5. OnLButtonDblClk
鼠标双击,同样也是处于空白区域时,在浮动和停靠状态间执行切换。此时,调用CDockContext的ToggleDocking方法。
6. OnMouseActivate
如果处于浮动状态,那么将激活顶层top-level窗口,通常就是应用的主窗口。这里使用了CWnd提供的ActivateTopParent方法。
该类提供了如下方法用来封装ControlBar的Dock功能:DockControlBar和FloatControlBar。其中,
1. DockControlBar:初始化Frame窗口的四个dock bar
2. FloatControlBar要实现浮动,那么包围在ControlBar之外的一定是一个不同于当前Frame窗口的另一个窗口。MFC提供了CMiniDockFrameWnd类服务于此。因此,在该方法内部,实际上创建一个该类的对象,作为该Bar新的Frame。(前面提到CControlBar中使用m_pDockSite存放Frame窗口对象)
Frame窗口在哪里做Layout?
当Frame窗口尺寸改变时,其RecalcLayout将会被调用。在该方法中,FrameWnd的所有子窗口都将被layout,主要工作的方法就是RepositionBars,其声明如下:
void RepositionBars(
UINT nIDFirst,
UINT nIDLast,
UINT nIDLeftOver,
UINT nFlag = reposDefault,
LPRECT lpRectParam = NULL,
LPCRECT lpRectClient = NULL,
BOOL bStretch = TRUE
);
其中,
1. nIDFirst和nIDLast指明了子窗口的ID范围。在RecalcLayout中,使用了0 ~ 0xffff。nIDLeftOver则表明该子窗口将充满除了control bars外剩余的客户区域,在RecalcLayout中,使用了AFX_IDW_PANE_FIRST,这也是Frame窗口中的View的ID。
2. 该方法中,将遍历所有子窗口,如果在first ~ last范围中时,将发送WM_SIZEPARENT消息到每个子窗口。此时,就是Frame窗口的四个DockBar。在它们响应方法OnSizeParent中,将会根据设置的CBRS风格,调用前面提到的CControlBar的CalcDynamicLayout动态的layout方法,该方法的默认实现将会调用CalcFixedLayout,而CDockBar重载了该方法,其实现逻辑比较复杂(bardock.cpp中)
3. 然后,该方法将会调用View的CalcWindowRect方法,以便能充满剩余客户区域。该方法为CWnd提供,CView重载了它。
4. 当ControlBar处于浮动状态时,CFrameWnd的FWS_SNAPTOBARS风格表明该Frame窗口将调整为刚好包围ControlBar。此时,调用稍微复杂些。
具体代码片段如下:
if (GetStyle() & FWS_SNAPTOBARS)
{
CRect rect(0, 0, 32767, 32767);
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposQuery,
&rect, &rect, FALSE);
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposExtra,
&m_rectBorder, &rect, TRUE);
CalcWindowRect(&rect);
SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(),
SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOZORDER);
}
else
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposExtra, &m_rectBorder);
MFC 提供了如下 CControlBar 的派生类: CDockBar, CToolBar, CStatusBar, CDialogBar, CReBar 。这些类的详细情况,将在后续的章节中描述。