MFC視窗位置管理詳細分析及執行個體

來源:互聯網
上載者:User

http://hi.bccn.net/space-21499-do-blog-id-15271.html

 

MFC視窗位置管理詳細分析及執行個體
        在一般用MFC編寫的程式的視窗客戶區中,可能有好幾個子視窗(具有WM_CHILD風格的視窗)。上邊是工具列,中間是視圖視窗,下邊是狀態列。三個視窗在架構的客戶區裡和平共處,互不重疊。主架構視窗的尺寸改變了,別的子視窗都能及時調整自己的尺寸以便保持相互位置關係不變,例如狀態條視窗總能保持在主架構客戶區底部,並且其寬度總能和主架構客戶區寬度一致。工具列視窗總能停靠在主架構的某一邊不變,其寬度或高度總能和主架構客戶區的寬度或高度一致,視圖視窗總能填滿主架構客戶區的剩餘空間。 
假如我們自己從CWnd類派生一個視窗類別並產生一個視窗,在它的客戶區裡要產生若干個子視窗,我們想使這些子視窗排列得規規矩矩,互不重疊,當父視窗的尺寸變了時各個子視窗能適時調整自己的尺寸和位置,使各個子視窗之間的位置大小比例關係不變。當移動其中一個或幾個子視窗時,別的子視窗能及時為這個移動了的子視窗讓位。當然我們可以利用api函數裡管理視窗的函數來編寫自己的管理子視窗的方法。可是如果在父視窗的客戶區裡有了工具列,狀態條等等子視窗時,你自己加進來的子視窗還能和這些mfc提供的子視窗融洽相處嗎?你如何保證你的子視窗不會覆蓋了能夠四處停靠的工具列?當工具列和狀態條消失後你的子視窗如何才能知道,以便及時調zhongz整自己的大小從而覆蓋工具列和狀態條騰出的空間?基於文檔視圖構架的視窗的客戶區內還有個視圖,你自己硬加上的子視窗能不和視圖視窗爭地盤嗎? 
所以必須瞭解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。
第六步:編譯並運行程式。
好了,在主架構視窗的左邊多了一個灰色的視窗,它佔主視窗客戶區的三分之一。工具列,狀態條和視圖都被趕到右邊三分之二的地方去了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.