2007年1月31日星期三

MFC窗口位置管理详细分析及实例

<a href=http://www.vczx.com/article/show.php?id=832>参考文献</a>
    
在一般用MFC编写的程序的窗口客户区中,可能有好几个子窗口(具有WM_CHILD风格的窗口)。上边是工具栏,中间是视图窗口,下边是状态栏。三个窗口在框架的客户区里和平共处,互不重叠。主框架窗口的尺寸改变了,别的子窗口都能及时调整自己的尺寸以便保持相互位置关系不变,例如状态条窗口总能保持在主框架客户区底部,并且其宽度总能和主框架客户区宽度一致。工具栏窗口总能停靠在主框架的某一边不变,其宽度或高度总能和主框架客户区的宽度或高度一致,视图窗口总能填满主框架客户区的剩余空间。

    假如我们自己从CWnd类派生一个窗口类并生成一个窗口,在它的客户区里要生成若干个子窗口,我们想使这些子窗口排列得规规矩矩,互不重叠,当父窗口的尺寸变了时各个子窗口能适时调整自己的尺寸和位置,使各个子窗口之间的位置大小比例关系不变。当移动其中一个或几个子窗口时,别的子窗口能及时为这个移动了的子窗口让位。当然我们可以利用api函数里管理窗口的函数来编写自己的管理子窗口的方法。可是如果在父窗口的客户区里有了工具栏,状态条等等子窗口时,你自己加进来的子窗口还能和这些mfc提供的子窗口融洽相处吗?你如何保证你的子窗口不会覆盖了能够四处停靠的工具栏?当工具栏和状态条消失后你的子窗口如何才能知道,以便及时调整自己的大小从而覆盖工具栏和状态条腾出的空间?基于文档视图构架的窗口的客户区内还有个视图,你自己硬加上的子窗口能不和视图窗口争地盘吗?

    所以必须了解mfc的窗口管理它的客户区的方法。其实,mfc的窗口管理它的客户区的方法是非常简单的:父窗口调用一个函数,子窗口响应一个消息,就这么多。

CWnd::RepositionBars函数和WM_SIZEPARENT消息

    先简述一下mfc的窗口为子窗口分配客户区空间的过程:这一过程是父窗口与子窗口共同协调完成的。父窗口先提供它的客户区内的一块区域,叫做起始可用区域。然后调用一个函数,在这个函数里,父窗口把这片区域通过一个消息提交给它的第一个子窗口,该子窗口决定自己要占用多大一块,然后在可用区域里把它将占据的部分划出去,这样可用区域就被切去了一块。父窗口再把这块剩下的可用区域通过同样的消息提交给第二个子窗口,第二个子窗口再根据自己的需要切掉一块。如此这般,每个子窗口都切去自己所需的一块。最后剩下的可用区域就给最后的子窗口使用。可以看出,除了最后一个子窗口外,其它子窗口都得在消息响应函数里有自己的算法来决定自己将在可用区域里占据多大一块,最后一个子窗口由于别无选择,所以不需要这样的算法。

    当然,初始的可用区域是一个矩形,每次被切割后剩下的可用区域还是一个矩形,不可能是别的形状的。

    举例说来,在一个典型单文档程序中,父窗口就是从CFrameWnd派生的主框架窗口,最后一个子窗口就是视图窗口,如果用了CSplitterWnd生成分隔条的话,最后一个子窗口就是拥有分隔条的那个窗口。其它子窗口就是工具栏窗口和状态条窗口,以及可能有的别的控件窗口。

    在典型多文档界面程序中,父窗口就是主框架窗口,最后一个子窗口就是覆盖在主窗口客户区,背景为黑灰色,拥有包含文档的子框架窗口的那个窗口,这是个预定义了窗口类的窗口,它的窗口类名是"MDIClient"。如果用了CSplitterWnd生成分隔条的话,最后一个子窗口就是拥有分隔条的那个窗口。其它窗口就是工具栏窗口,状态条窗口以及可能有的别的控件窗口。

    这个函数和消息是:函数CWnd::RepositionBars()以及消息WM_SIZEPARENT。这个消息是mfc自定义的,不是windows自有的。

先简单说明一下这个函数和消息。

1。函数CWnd::RepositionBars()

    这个函数不是虚函数,所以就无法在派生类里通过覆盖来编制自己的版本了,只能搞懂它的功能,以便能灵活使用。

    简单而言,这个函数的功能是将可用的客户区区域信息放到消息WM_SIZEPARENT的消息参数里,然后枚举本窗口的所有子窗口,给每个子窗口 (除掉一个特定的子窗口,相当于上文提到的最后一个子窗口)都发送这个消息,每个响应这个消息的子窗口都会把可用客户区切去一块。最后把那个特定的子窗口的尺寸和位置调整到刚好放在最后剩下的可用区域里。

2。消息WM_SIZEPARENT

每个欲参与分配客户区的子窗口都要响应这个消息,除非这个子窗口是那个特定的子窗口。

响应这个消息的子窗口至少要做两件事:1,将可用的父窗口客户区切去自己所占据的一块。2,根据消息参数的指示,将自己的大小和位置调整到刚好容纳到自己所占据的区域里或不做调整。

下面详细介绍一下函数CWnd::RepositionBars()和消息WM_SIZEPARENT。

1。函数CWnd::RepositionBars() void RepositionBars( UINT nIDFirst, UINT nIDLast, UINT nIDLeftOver, UINT nFlag = CWnd::reposDefault, LPRECT lpRectParam = NULL, LPCRECT lpRectClient = NULL, BOOL bStretch = TRUE );

参数比较多,但还是比较好懂的。

(1)nIDFirst和nIDLast

参与分配父窗口客户区的子窗口的id范围。

每个WM_CHILD风格的窗口都有个id,这是在窗口创建过程中指定的。函数CWnd::Create()的第六个参数就是这个id。api函数 CreateWindow和 CreateWindowEx里的那个HMENU类型的参数,当窗口的风格里有WM_CHILD时,它不是指的菜单句柄,而是该窗口的id。

nIDFirst和nIDLast参数指明了:如果一个子窗口的id值大于等于nIDFirst并且小于等于nIDLast,在这个函数中才会给这个子窗口发送 WM_SIZEPARENT消息,这个子窗口才能参与父窗口客户区的分配。

(2)nIDLeftOver

前面说过,有一个特定的子窗口,它不响应WM_SIZEPARENT消息。只有当其它的子窗口都分配完了,它才来捡取父窗口客户区里剩下的那块。 nIDLeftOver正是这个子窗口的id。它也必须大于等于nIDFirst并且小于等于nIDLast。

(3)lpRectClient

这是一个指向RECT结构数据的指针。这个RECT结构里存放的正是父窗口客户区的初始可用区域。随着在该函数里依次给各个子窗口发送 WM_SIZEPARENT消息,每个响应这个消息的子窗口都会切去自己所占据的部分。最后剩下的部分,就是id为nIDLeftOver的子窗口将要占据的区域了。这个参数可以为NULL,这时初始的可用区域就是整个父窗口客户区。

(4)nFlag和lpRectParam

这两个参数放在一起讲比较好。nFlag是该函数的功能标志,它可以有三个值:reposDefault,reposQuery 和reposExtra。

当nFlag 等于reposDefault时,RepositionBars函数的功能是这样的:依次给id介于nIDFirst和nIDLast之间并且不等于 nIDLeftOver的子窗口发送WM_SIZEPARENT消息,每个响应这个消息的子窗口从lpRectClient所指的结构里切去自己所占据的部分,并且将自己的大小和位置调整到自己所占据的区域的大小,最后RepositionBars函数还将id为nIDLeftOver的子窗口的大小和位置调整到被其他子窗口切剩的可用区域内,使这个子窗口正好完全覆盖最后的可用区域。这种情况下lpRectParam不用,可以为NULL。

当nFlag 等于reposQuery 时,RepositionBars函数的功能是这样的:依次给id介于nIDFirst和nIDLast之间并且不等于nIDLeftOver的子窗口发送WM_SIZEPARENT消息,每个响应这个消息的子窗口从lpRectClient所指的结构里切去自己所占据的部分,但是他们并不调整自己的大小和位置,最后RepositionBars函数并不调整将id为nIDLeftOver的子窗口的大小和位置,而是根据bStretch的值来做动作:如果bStretch为TRUE,那么 RepositionBars函数把最后剩下的可用区域拷贝到lpRectParam指向的RECT结构里;如果bStretch为FALSE,那么 RepositionBars函数把所有其他子窗口占用掉的可用区域的高和宽(要所有的子窗口都紧排在一起,形成一个大的矩形,这个值才有意义)拷贝到 lpRectParam指向的RECT结构的bottom 和right成员里,其top和left成员被置零。使用这个nFlag值来调用RepositionBars的目的不是要重排子窗口,而是要看看,假如重排子窗口的话,这些子窗口将占去多大一块,最后剩下的可用区域在什么位置等等信息。

当nFlag等于reposExtra时,该函数的功能和nFlag等于reposDefault时差不多,有点小小的区别。此时需要用到lpRectParam。前面说过,当 nFlag等于reposDefault时,RepositionBars函数将在最后把id为nIDLeftOver的子窗口的大小和位置调整到被其他子窗口切剩的可用区域内,使这个子窗口正好完全覆盖最后的可用区域。而当nFlag等于reposExtra时,RepositionBars在调整id 为nIDLeftOver的子窗口的大小和位置前,还要用 lpRectParam来对最后剩下的可用区域做修正。假设lpRect指向的是最后的可用区域,那么这个修正是这样进行的:


lpRect->top+=lpRectParam->top;
lprect->left+=lpRectParam->left;
lpRect->right-=lpRectParam->right;
lpRect->bottom-=lpRectParam->bottom;

通过这样的修正,可以使最后剩下的可用区域不被id为nIDLeftOver的子窗口占满,而是空出一些地方来留作他用。
(5)bStretch

这个参数上面已经提到一点它的作用。它主要是提供给各个响应WM_SIZEPARENT消息的子窗口用的,子窗口例如工具栏,状态条等在决定自己将从父窗口客户区的可用空间里划走多少时,这个参数也是个判断的依据。详细可以参阅工具栏和状态条响应WM_SIZEPARENT的函数OnSizeParent ()。

2。消息WM_SIZEPARENT

这是个mfc自定义的消息。在msdn里的TN024这篇技术文章里有关于这个消息的说明。

该消息的两个参数中wParam不用,lParam是指向一个AFX_SIZEPARENTPARAMS结构变量的指针,这个结构变量是在RepositionBars函数里定义的:

AFX_SIZEPARENTPARAMS layout;

AFX_SIZEPARENTPARAMS结构定义如下:

struct AFX_SIZEPARENTPARAMS
{
HDWP hDWP;
RECT rect;
SIZE sizeTotal;
BOOL bStretch;
};
这个结构变量的成员是在RepositionBars函数里填写的:它的bStretch成员就是RepositionBars的参数bStretch,它的sizeTotal成员的两个成员cx和cy都被设置为零,它的rect成员就是从RepositionBars的参数lpRectClient里拷贝过来的,就是父窗口客户区的初始可用区域嘛。每个响应这个消息的子窗口都必须修改rect成员的值,以便切去自己所占据的部分。
成员hDWP 是什么?这得知道三个api函数:BebinDeferWindowPos(),DeferWindowPos()和EndDeferWindowPos ()。这三个api函数是用来成批设置窗口的位置和尺寸的。BebinDeferWindowPos()先通知windows分配一个将用来存贮窗口的位置和尺寸信息的结构,它不是返回这个结构的指针,而是返回代表这个结构的句柄,句柄的类型是HDWP。然后每个需要重新设置位置和尺寸的窗口都要调用 DeferWindowPos()函数(该函数需要那个HDWP 类型的句柄为参数),以便往那个结构里填写各自的窗口位置和大小信息。最后,在某个合适的时候调用EndDeferWindowPos(), windows就会根据那个结构里的信息把有关的窗口的位置和大小一次性设置好。比起针对每个窗口分别用SetWindowPos()等函数逐个设置来说,这种方法速度快。

好了,在RepositionBars函数里正是调用了BebinDeferWindowPos(),获得一个 HDWP类型的句柄,这个句柄就被填写到了上面那个结构变量 layout的成员hDWP里。然后RepositionBars函数给每个符合条件的子窗口发送WM_SIZEPARENT消息。在每个响应 WM_SIZEPARENT消息的子窗口里,要调用DeferWindowPos()来设置位置和尺寸信息。当所有的子窗口都响应完毕 WM_SIZEPARENT消息后,RepositionBars函数再调用 EndDeferWindowPos()函数,这一来,除了那个id为nIDLeftOver的子窗口外,所有的子窗口都一次性排好了位置了。

至于该结构的sizeTotal成员的意义,它累计每个子窗口所占据掉的可用区域的长宽尺寸和。每个子窗口在响应WM_SIZEPARENT消息时一般都要把自己所占据的区域的高和宽分别累加到sizeTotal结构的cy和cx成员里。这有什么意义呢?当每个子窗口所占据的区域都是挨在一起的时候,这个 sizeTotal结构就有意义了,主框架窗口可以使nFlag等于reposQuery,使bStretch等于FALSE来调用 RepositionBars函数,RepositionBars函数会把 sizeTotal结构的两个成员值拷贝到lpRectParam参数里返回给主框架类(前面也提到过),这样主框架类就知道它的客户区内的子窗口占去了客户区内多大的一块空间。如果你的主框架窗口没有利用这个信息,那么响应WM_SIZEPARENT消息的子窗口就可以不理睬sizeTotal成员。

ID的分配

可以看到,每个子窗口都有个id,同一个父窗口的子窗口的id不能重复。mfc的一些现成的控件子窗口都有预定义的id:

id名 id值 意义

AFX_IDW_TOOLBAR 0xE800 // 主窗口的工具栏的id
AFX_IDW_STATUS_BAR 0xE801 // 状态栏的id
AFX_IDW_PREVIEW_BAR 0xE802 // PrintPreview Dialog Bar
AFX_IDW_RESIZE_BAR 0xE803 // OLE in-place resize bar
AFX_IDW_REBAR 0xE804 // COMCTL32 "rebar" Bar
AFX_IDW_DIALOGBAR 0xE805 // CDialogBar

还有象单文档程序的视图窗口,多文档程序的那个MDIClient窗口,分隔条窗口,他们的id值介于下面两个id值之间:
AFX_IDW_PANE_FIRST 0xE900 //
AFX_IDW_PANE_LAST 0xE9FF

你要给你自己的子窗口分配id的话,别和上面的重复了。一般如果用IDE的菜单view/resource symbols项来加入自己的id的话,是不会重复的。有关id,还可以看看msdn里的TN020文章,那是专讲id的。

实例分析

1。CFrameWnd类是如何调用RepositionBars函数的

前面介绍了RepositionBars的各个参数和意义,现在看看CFrameWnd类是如何调用这个函数的,从中可以学习RepositionBars函数的使用方法。

CFrameWnd 类及其派生类生成的窗口的客户区内可以有工具栏,状态条和视图窗口等子窗口。当父窗口的尺寸发生变化时,这些子窗口的各自的位置和大小比例关系保持不变,这就需要父窗口一旦在它自己的尺寸发生变化时就调用RepositionBars函数。CFrameWnd类是集中在函数 RecalcLayout里调用RepositionBars函数的。该类保证了在窗口尺寸发生变化时函数RecalcLayout都被调用,从而 RepositionBars函数也能被及时调用,确保了各个子窗口都能及时调整自己的位置和大小。

RecalcLayout是个虚函数。该函数的功能就是在主框架的客户区内提供一个初始的可用区域,并把这个区域放在一个CRect类型的变量里。该函数大致是这样的:

void CFrameWnd::RecalcLayout(BOOL bNotify)
{
if (m_bInRecalcLayout)
return;//这大概是在防止该函数重入
m_bInRecalcLayout = TRUE;
....
....
....
....
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);
m_bInRecalcLayout = FALSE;
} 可以看出,mfc认为这个函数是不能重入的。在编制自己的RecalcLayout()函数时也得用同样的方法来防止重入。
后面的if语句检查框架窗口是否具有风格FWS_SNAPTOBARS,这个风格用在什么时候呢?我是这样认为的:通常都是在主框架窗口的尺寸改变
时,子窗口在响应WM_SIZEPARENT消息时调整自己的尺寸以便跟上框架窗口的尺寸变化。有这样的情况:父窗口的客户区内的子窗口的数目是动态变
化的,而且这些子窗口互相不能重叠,他们的尺寸由于某种原因不好改变。那么当子窗口的数目发生增减时,如不调整父窗口自己的尺寸,就会导
致客户区留下空白或新增加的子窗口没有多余空间安排。FWS_SNAPTOBARS风格就是用在这种情况下,使父窗口能调整自己的大小以便容纳子窗口。
看这个分支里的语句,似乎是这样的。
一般都不会有FWS_SNAPTOBARS风格的,所以一般是执行else分支。在这个分支里简单地调用RepositionBars去重排所有的子窗口,它的参数
lpRectClient 使用默认的NULL值,意思就是初始可用区域是父窗口的整个客户区。
可以在自己的派生类里编写自己的RecalcLayout函数,以便用自己的方法调用RepositionBars函数。要注意的是在CFrameWnd类的窗口刚被创建
时RecalcLayout函数也被调用,此时可能某些用户自己加的子窗口还未被创建出来,所以在这个函数内如果要引用某个用户自己加的子窗口的句柄
的话必须先用::IsWindow()函数判断一下该窗口句柄是否可用。否则的话就会出现非法操作了。


实战演练
由于精力有限,只提供一个实战例子:将视图,工具栏和状态栏赶到右边
我们要生成这样的界面:视图窗口,工具栏和状态条统统在右边,左边是个自己加的窗口。
第一步:启动AppWizard生成一个单文档程序,全部使用默认设置。
第二步:在CMainFrame类里增加一个成员 CWnd m_mywnd。
第三步:在CMainFrame::OnCreate()函数里增加这几行:
m_mywnd.CreateEx
(
WS_EX_CLIENTEDGE,
AfxRegisterWndClass
(
CS_HREDRAW|CS_VREDRAW,
::LoadCursor(NULL,IDC_ARROW),
::CreateSolidBrush(RGB(190,190,190))
),
"",
WS_VISIBLE|WS_CHILD,
CRect(0,0,0,0),
this,
IDC_MYPANE //用IDE的菜单view/resource symbols项加入的id。
);

第四步:启动ClassView,在CMainFrame里加上虚函数RecalcLayout(),函数体这样写:
void CMainFrame::RecalcLayout(BOOL bNotify)
{
if (m_bInRecalcLayout)
return;
m_bInRecalcLayout = TRUE;

//rect1是新加的窗口将占据的区域
//rect2就是提供给工具栏,状态条和视图窗口的初始可用区域。
CRect rect1,rect2;
GetClientRect(&rect1);
rect1.right=rect1.right/3;
GetClientRect(&rect2);
rect2.left=rect2.right/3;

if(::IsWindow(m_mywnd.m_hWnd)) //这句是不能少的
m_mywnd.MoveWindow(&rect1);
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposExtra, CRect(0,0,0,0),&rect2);
m_bInRecalcLayout = FALSE;
}

第五步:用IDE的菜单view/resource symbols项加入一个id:IDC_MYPANE。
第六步:编译并运行程序。

好了,在主框架窗口的左边多了一个灰色的窗口,它占主窗口客户区的三分之一。工具栏,状态条和视图都被赶到右边三分之二的地方去了。

没有评论: