MFC窗口位置管理详细分析及实例 在一般用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()。
ID的分配
可以看到,每个子窗口都有个id,同一个父窗口的子窗口的id不能重复。mf
c的一些现成的控件子窗口都有预定义的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类及其派生类生成的窗口的客户区内可以有工具栏,状态条和视图
窗口等子窗口。当父窗口的尺寸发生变化时,这些子窗口的各自的位置和大小比
例关系保持不变,这就需要父窗口一旦在它自己的尺寸发生变化时就调用Reposi
tionBars函数。CFrameWnd类是集中在函数RecalcLayout里调用RepositionBars函
数的。该类保证了在窗口尺寸发生变化时函数RecalcLayout都被调用,从而Repo
sitionBars函数也能被及时调用,确保了各个子窗口都能及时调整自己的位置和
大小。
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_r
ectBorder);
m_bInRecalcLayout = FALSE;
}
可以看出,mfc认为这个函数是不能重入的。在编制自己的RecalcLayout()函
数时也得用同样的方法来防止重入。
后面的if语句检查框架窗口是否具有风格FWS_SNAPTOBARS,这个风格用在什
么时候呢?我是这样认为的:通常都是在主框架窗口的尺寸改变时,子窗口在响
应WM_SIZEPARENT消息时调整自己的尺寸以便跟上框架窗口的尺寸变化。有这样的
情况:父窗口的客户区内的子窗口的数目是动态变化的,而且这些子窗口互相不
能重叠,他们的尺寸由于某种原因不好改变。那么当子窗口的数目发生增减时,
如不调整父窗口自己的尺寸,就会导致客户区留下空白或新增加的子窗口没有多
余空间安排。FWS_SNAPTOBARS风格就是用在这种情况下,使父窗口能调整自己的
大小以便容纳子窗口。看这个分支里的语句,似乎是这样的。
一般都不会有FWS_SNAPTOBARS风格的,所以一般是执行else分支。在这个分
支里简单地调用RepositionBars去重排所有的子窗口,它的参数lpRectClient 使
用默认的NULL值,意思就是初始可用区域是父窗口的整个客户区。
可以在自己的派生类里编写自己的RecalcLayout函数,以便用自己的方法调
用RepositionBars函数。要注意的是在CFrameWnd类的窗口刚被创建时RecalcLay
out函数也被调用,此时可能某些用户自己加的子窗口还未被创建出来,所以在这
个函数内如果要引用某个用户自己加的子窗口的句柄的话必须先用::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。
第六步:编译并运行程序。
好了,在主框架窗口的左边多了一个灰色的窗口,它占主窗口客户区的三分
之一。工具栏,状态条和视图都被赶到右边三分之二的地方去了。