MFC控制条窗口布局原理(上)

 MFC控制条窗口布局原理
——by Koote Bi@fudan cse
一、框架窗口
让我们先从框架窗口开始。当框架窗口改变大小时会收到WM_SIZE消息,CFrameWnd::OnSize负责处理此消息,该函数调用RecalcLayout来重新安置各子窗口,它的主体代码如下:
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);
}
这里有两个小的地方要注意,第一是FWS_SNAPTOBARS风格。一般来说,都是框架窗口主动改变大小,子窗口随之要修改自己来适应框架窗口的改变,但是这个FWS_SNAPTOBARS风格却相反,是让框架窗口改变大小来适应它的子窗口,这个风格是为CMiniDockFrameWnd准备的,这个框架窗口的大小是根据它内部的控制条来定的;第二是要注意RecalcLayout是不可重入的,MFC防止重入的方法虽然非常的简单有效,但是它的方法要不能防止多线程的重入——话说回来MFC本身就不是一个线程安全的库。
好了现在我们进入了整个重布局动作的主体函数RepositionBars(这个函数在MSDN里有文档记载,关于它的几个参数的含义就不在这里赘述了):
首先,它创建一个AFX_SIZEPARENTPARAMS结构,并填写好它的成员变量,最主要的就是两个:bStretch和rect,前一个是BOOL型变量,表明子窗口是否需要拉伸,后一个是当前客户区大小。
接着,框架窗口按照Z-Order,从最上面的子窗口开始,依次向它的所有子窗口发送一个MFC自定义的消息:WM_SIZEPARENT(ID为nIDLeftOver的子窗口除外),以通知它们,按照新的客户区,重新计算自己的大小和位置,并从AFX_SIZEPARENTPARAMS::rect中将自己所占的那一块rectangle扣除,这样所有的子窗口计算完毕后框架窗口就可以知道剩余客户区的大小(PS:到底都发送给了谁?又是按照什么次序?以MDIDemo为例,该例子创建了一个CToolBar(上)、一个CThumbnailListCtrlBar(下),在都是Dock状态下,跟踪记录下的所发送的窗口的ID,依次是0xE801->0xE81B->0xE81E->0xE81C->0xE81D,察看各子窗口ID的定义得知次序是状态栏->上方的Dock Bar->下方的Dock Bar->左边的Dock Bar->右边的Dock Bar,这里要注意的是,所找到的第一个子窗口的ID是0xE900(0xE900被定义成AFX_IDW_PANE_FIRST,在这个例子里它是View的窗口ID,与这个ID对应的还有另一个ID叫AFX_IDW_PANE_LAST,SDI的View窗口,MDI的MDIClient窗口,分隔条窗口等的ID都要介于上面两个ID值之间),等于nIDLeftOver,所以WM_SIZEPARENT消息是不发给它的。那么为什么CToolBar和CThumbnailListCtrlBar也收不到消息呢?因为CDockBar是它的父窗口,消息发给CDockBar了,CDockBar会计算它内部停靠的全部子窗口的大小,就不需要框架窗口操心了,除此之外,浮动的控制条也是不接受WM_SIZEPARENT消息的,因为浮动的控制条不会跟随主框架窗口的大小改变而改变)。
发送完毕之后(这里有个细节,框架窗口发送WM_SIZEPARENT消息用的是SendMessage,这意味着只有子窗口处理完该消息后,SendMessage才返回,框架窗口才会接着发送消息给下一个子窗口,全部发送完毕也就意味了所有的子窗口都已经从AFX_SIZEPARENTPARAMS::rect中把自己要的那一块rectangle给拿掉了),剩下的就是最后可用的客户区了,根据nFlags的值,执行不同的返回动作。其中reposQuery表示只查询,不做实际的重布局动作,把最后剩下的客户区,拷贝到lpRectParam就返回了,如果不是reposQuery那就要做重布局了,对reposDefault,我们要把ID为nIDLeftOver的子窗口的大小和位置调整到被其他子窗口切剩后剩下的可用客户区内,使这个子窗口正好完全覆盖最后的可用区域(也就是说所有的子窗口把客户区全部挤满了)。而当nFlag等于reposExtra时,在调整 nIDLeftOver子窗口的大小和位置前,用 lpRectParam来对最后剩下的可用区域做修正,具体来说就是把AFX_SIZEPARENTPARAMS::rect向里缩,缩的距离由lpRectParam指定,这样就使最后剩下的客户区不被nIDLeftOver子窗口占满,而是空出一些地方。修正完毕后最后一次性重布局所有的子窗口。
至此,框架窗口所做的动作全部完成。
 
二、控制条子窗口
接着分析控制条这边所要做的动作。根据前面的跟踪我们知道除了CStatusBar和CDockBar,从CControlBar继承下来的控制条诸如CToolBar、CDialogBar等,是收不到WM_SIZEPARENT消息的,它们的父窗口CDockBar代替它们接收这个消息。因此整个重布局过程的起点是CDockBar对WM_SIZEPARENT消息的处理函数CDockBar::OnSizeParent(对CStatusBar而言,其起点是CControlBar::OnSizeParent,这里不打算对它作进一步分析)。第一步让我们来分析这个函数所做的动作,这个函数不长,把完整代码列出:
LRESULT CDockBar::OnSizeParent(WPARAM wParam, LPARAM lParam)
{
AFX_SIZEPARENTPARAMS* lpLayout = (AFX_SIZEPARENTPARAMS*)lParam;
 
// set m_bLayoutQuery to TRUE if lpLayout->hDWP == NULL
BOOL bLayoutQuery = m_bLayoutQuery;
CRect rectLayout = m_rectLayout;
 
m_bLayoutQuery = (lpLayout->hDWP == NULL);
m_rectLayout = lpLayout->rect;
LRESULT lResult = CControlBar::OnSizeParent(wParam, lParam);
 
// restore m_bLayoutQuery
m_bLayoutQuery = bLayoutQuery;
m_rectLayout = rectLayout;
 
return lResult;
}
如前所述,WM_SIZEPARENT消息传递一个AFX_SIZEPARENTPARAMS结构体的指针作为参数,在这里我们先取出这个结构体,然后判断AFX_SIZEPARENTPARAMS::hDWP是否为空,是的话说明父窗口仅仅是想查询,并不要真的进行重布局动作(回顾一下,在RepositionBars函数中,当nFlags为reposQuery时并不调用BeginDeferWindowPos,故而AFX_SIZEPARENTPARAMS::hDWP就一定是NULL),完成必要的变量保护后,进入父类CControlBar的OnSizeParent,在此,根据控制条窗口的风格,决定如何计算控制条的尺寸,具体是这样的:
        DWORD dwMode = lpLayout->bStretch ? LM_STRETCH : 0; //拉伸?
        if((m_dwStyle & CBRS_SIZE_DYNAMIC) && (m_dwStyle & CBRS_FLOATING) //浮动,形状可变
        {
               dwMode |= LM_HORZ | LM_MRUWIDTH;//计算水平状态常用尺寸
        }
        else if(dwStyle & CBRS_ORIENT_HORZ) //水平停靠
        {
               dwMode |= LM_HORZ | LM_HORZDOCK;//计算水平停靠状态尺寸
        }
        else
        {
               dwMode |= LM_VERTDOCK; //计算垂直停靠状态尺寸
        }
        CSize size = CalcDynamicLayout(-1, dwMode);
最后调用CalcDynamicLayout,这个函数是一个虚函数,先被调用的是CControlBar:: CalcDynamicLayout,这个函数调用了CalcFixedLayout(也是一个虚函数),注意到CDockBar对此函数进行了重载,所以转了一圈我们又回到了CDockBar中。

     转载于:http://blog.csdn.net/codewarrior/archive/2006/09/04/1175211.aspx

摘要:本文通过一个程序实例描述了在VC++6.0下如何在单文档程序中通过菜单动态控制多 个窗体的切换。    一、 引言    我们在编制程序中根据需求的不同会在程序风格上选择多文档、单文档或是对话框模式 ,对于单文档模式可能是我们使用比较多的,但有时我们想采用单文档的形式显示多个不同 的窗体,如作为数据库前台应用程序就会遇到此类问题,数据库由大量的表单组成,而同常 一个窗体内只用来显示一个表单,所以要显示其他的表单时就要用到切换窗体的技术了,下 面就通过一个程序说明该技术的实现方法。    二、 实现技术    新建一个基于CFormView的单文档应用程序,再添加一个窗体和与之对应的基于 CFormView的新视类,然后通过在主框架类里添加控制代码和菜单控制实现这两个窗体的动态 切换,下面就是具体的实现过程:    (一) 用"MFC AppWizard(exe)"建立一个新项目"SwitchForm",并在第二步的创建类型上选 择为"Single documnet"单文档模式,第三、四、五、六步均取确省状态,最后一步选择 "CFormView"作为视类的基类。点按"完成"按钮,生成了初始工程"SwitchForm"。    (二) 点选菜单"Insert"、"Resource…",在弹出的"Insert Resource"对话框中"Dialog"树 里的"IDD_FORMVIEW",点击"New"按钮,生成了一个新的窗体,将其ID号改为"IDD_NEXTFORM"。 在原有的窗体上加一个静态框"这是第一个窗体";在新建的窗体上也添加一个静态框"这是第二 个窗体"。    (三) 在菜单资源的"IDR_MAINFRAME"上添加一级菜单"窗体切换",及其二级菜单"第一个窗 体"、"第二个窗体",其标识号分别为"ID_FIRSTFORM"和"ID_SECONDFORM"。修该"第一个窗体" 的属性为"Checked",表明程序初始时显示的是第一个窗体。    (四) 在"ClassView"属性页里的"SwitchForm classes"上右键,在弹出菜单上选择 "New Class…",弹出"New Class"对话框,选择"Dialog ID:"为我们刚添加的新窗体 "IDD_NEXTFORM",选择"Base class:"为"CFormView",类名取为"CNextFormView",这样就把第 二个窗体对应的视图类添加到了工程。 (五) 在框架类里添加函数SwitchToForm(): void CMainFrame::SwitchToForm(int nForm) { file://获取原来的活动窗体的视图句柄 CView* pOldActiveView = GetActiveView(); file://获取由"nForm"标识的窗体所对应的视图句柄 CView* pNewActiveView = (CView*) GetDlgItem(nForm); file://若视图句柄为空,则创建一新的。 if (pNewActiveView == NULL) { if (nForm == IDD_SW99vCHFORM_FORM) pNewActiveView = (CView*)new CSwitchFormView; if (nForm == IDD_NEXTFORM) pNewActiveView = (CView*)new CNextFormView; CCreateContext context; context.m_pCurrentDoc = pOldActiveView->GetDocument(); pNewActiveView->Create(NULL,NULL,0L, CFrameWnd::rectDefault, this,nForm,&context); pNewActiveView->OnInitialUpdate(); } file://选择pNewActiveView为活动窗体 SetActiveView(pNewActiveView); file://显示活动窗体,隐藏非活动窗体 pNewActiveView->ShowWindow(SW_SHOW); pOldActiveView->ShowWindow(SW_HIDE); int ID; if(pOldActiveView->GetRuntimeClass() == RUNTIME_CLASS(CSwitchFormView)) ID=IDD_SW99vCHFORM_FORM; if(pOldActiveView->GetRuntimeClass() == RUNTIME_CLASS(CNextFormView)) ID=IDD_NEXTFORM; file://设置窗体的ID号 pOldActiveView->SetDlgCtrlID(ID); pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST); RecalcLayout(); }    (六)添加两个菜单相对应的命令响应函数和更新函数如下: void CMainFrame::OnFirstform() { file://通过IsKindOf函数确定当前活动窗口是否是第一个窗口,如是,则无须切换, file://否则将通过SwitchToForm函数将当前活动窗口切换到"IDD_SW99vCHFORM_FORM" file://标识的第二个窗体。 if (GetActiveView()->IsKindOf(RUNTIME_CLASS(CSwitchFormView))) return; SwitchToForm(IDD_SW99vCHFORM_FORM); } void CMainFrame::OnUpdateFirstform(CCmdUI* pCmdUI) { file://通过IsKindOf函数判断当前活动窗口是否是第一个窗体,如是则将其选中。 pCmdUI->SetCheck(GetActiveView()->IsKindOf(RUNTIME_CLASS(CSwitchFormView))); } void CMainFrame::OnSecondform() { if (GetActiveView()->IsKindOf(RUNTIME_CLASS(CNextFormView))) return; SwitchToForm(IDD_NEXTFORM); } void CMainFrame::OnUpdateSecondform(CCmdUI* pCmdUI) { pCmdUI->SetCheck(GetActiveView()->IsKindOf(RUNTIME_CLASS(CNextFormView))); }    然后再在该文件开始处添加对两个视图类的引用: #include "SwitchFormDoc.h" #include "SwitchFormView.h" #include "NextFormView.h"    在此须注意:应在两个视类的引用之前添加对文档类的引用,否则会引起编译错误。另外,由于视 类的构造函数在声明时都确省的声明为保护型的,在框架类中无法引用,所以还要将两个视类的类 声明改动如下: class CNextFormView : public CFormView { public: file://将protected 改为public. CNextFormView(); …… }; class CSwitchFormView : public CFormView { public: file://将protected 改为public. CSwitchFormView(); …… };    三、 编译运行    编译运行程序,开始时的窗体上有"这是第一个窗体的字样",菜单也只有"第一个窗体"是被选中的, 当前的活动窗体是第一个窗体;点击菜单"第二个窗体",视图中的窗体上的字样变成了"这是第二 个 窗体",同时选中的菜单也由"第一个窗体"变成了"第二个窗体",实现了通过菜单将窗体进行动态切换。    总结:此程序中关键的是SwitchToView函数,在此函数中,程序搜索所有当前文档的显示窗口来查找与CruntimeClass变量匹配的视图类。如果找到,该窗口被激活。通过与之类似的方法,还可以实现在多文档模式下的单档(文档)多视(视图),通过不同的视图以不同的方式显示来自同一份文档的数据,以更好的满足程序的需要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值