一、概念和區別
在windows系統中,每個視窗對象都對應有一個資料結構,形成一個list鏈表。系統的視窗管理器通過這個list來擷取視窗資訊和管理每個視窗。這個資料結構中有四個資料用來構建list,即child、sibling、parent、owner四個域。
所以我們可以看到,視窗之間的關係有兩種:owner-owned 關係和 parent-child關係。前者稱之為擁有/被擁有關係,後者稱之為父/子關係。在這篇文字中,我把owner視窗稱之所有者視窗。換句話說,一個視窗在有一個父視窗(parent)的同時,還可能被不同的視窗擁有(owner),也可以有自己的子視窗(child)。在MFC 的CWnd類中,所有者視窗儲存在m_hWndOwner成員變數中,父視窗則儲存在m_hParent中,但是這兩個值並不一定和視窗對象資料結構中的值相對應。
視窗之間的關係,決定了視窗的外在表現。比如顯示、銷毀等。
如果一個視窗資料的owner域非NULL,則它和該視窗建立了owner-owned 關係,擁有關係決定了:
(1)被擁有的視窗永遠顯示在擁有它的那個視窗的前面;
(2)當所有者視窗最小化的時候,它所擁有的視窗都會被隱藏;
(3)當所有者視窗被銷毀的時候,它所擁有的視窗都會被銷毀。
需要注意的是,隱藏所有者視窗並不會影響它所擁有的視窗的可見狀態。比如:如果視窗 A 擁有視窗B,視窗B擁有視窗C,則當視窗A最小化的時候,視窗B被隱藏,但是視窗 C還是可見。
如果一個視窗的parent域非NULL,則它和該視窗之間就建立了parent-child關係。父子決定了:
(1)視窗在螢幕上面的顯示位置。父視窗提供了用來定位子視窗的座標系統,一個子視窗只能顯示在它的父視窗的客戶區中,之外的部分將被裁減。這個裁減法則決定了如果父視窗不可見,則子視窗肯定不可見。如果父視窗移動到了螢幕之外,子視窗也一樣。
(2)當父視窗被隱藏時,它的所有子視窗也被隱藏。
(3)父視窗被銷毀的時候,它所擁有的子視窗都會被銷毀。
注意!最小化父視窗不會影響子視窗的可見狀態,子視窗會隨著父視窗被最小化,但是它的WS_VISIBLE屬性不會變。
Windows系統為什麼要使用兩種關係呢?這是為了更加靈活的管理視窗。舉個例子:組合框(combobox)的下拉式清單方塊(list box)可以超出組合框的父視窗的客戶區,這樣有利於顯示,因此系統建立該list box的時候,是作為控制台視窗(desktop window)的子視窗,它的父視窗hWndParent是NULL,這樣,list box的顯示地區是限制在整個螢幕內,但是該list box的所有者卻是組合框的第一個非子視窗祖先(比如對話方塊),當它的所有者視窗銷毀後,該 list box自動銷毀。
另外,視窗之間訊息的傳遞也和視窗關係有關,通常,一個視窗會把自己的通知訊息發送給它的父視窗,但不全是這樣,比如,CToolBar發送通知訊息給它的所有者視窗而不是父視窗。這樣以來,就可以允許工具條作為一個視窗(比如一個 OLE 容器程式視窗)的子視窗的同時,能夠給另一個視窗(比如in-place架構視窗)發送訊息。至於某類視窗到底是把訊息發送給誰,是父視窗還是所有者視窗,microsoft並沒有明示。還有,在現場(in-place)編輯的情況下,當一個 server 視窗啟用或者失效的時候,架構視窗所擁有的子視窗自動隱藏或者顯示,這也是通過直接調用SetOwner函數實現的。
二、視窗類別型的說明和限制
(1)控制台視窗(desktop window)。這是系統最早建立的視窗。可以認為它是所有 WS_OVERLAPPED 類型視窗的所有者和父視窗。Kyle Marsh在他的文章“Win32 Window Hierarchy and Styles”中指出,當系統初始化的時候,它首先建立控制台視窗,大小覆蓋整個螢幕。所有其它視窗都在這個控制台視窗上面顯示。視窗管理器所用的視窗list中第一個就是這個控制台。它的下一層視窗叫做最上層視窗(top-level),最上層視窗是指所有非child、沒有父視窗,或者父視窗是desktop的視窗,它們沒有WS_CHILD屬性。
(2)WS_OVERLAPPED類型的視窗可以顯示在螢幕的任何地方。它們的所有者視窗是控制台。
Overlapped 類型的視窗屬於最上層視窗,一般作為應用程式的主視窗。不論是否給出了WS_CAPTION、WS_BORDER屬性,這類視窗建立後都有標題列和邊框。Overlapped視窗可以擁有其它最上層視窗或者被其它最上層視窗所擁有。所有overlapped視窗都有WS_CLIPSIBLINGS屬性。系統可以自動化佈建 overlapped視窗的大小和初始位置。
當系統 shuts down的時候,它將銷毀所有overlapped類型的視窗。
(3)WS_POPUP類型的視窗可以顯示在螢幕任何地方,它們一般沒有父視窗,但是如果明確調用SetParent,這類視窗也可以有父視窗。
WS_POPUP類型的視窗的所有者是在CreateWindow函數中通過設定hWndParent參數給定的,如果hWndParent不是子視窗,則該視窗就成為這個新的彈出式視窗的owner,否則,系統從hWndParent的父視窗向上找,直到找到第一個非子視窗,把它作為該快顯視窗的owner。當owner視窗銷毀的時候,系統自動銷毀這個快顯視窗。
Pop-up類型的視窗也屬於最上層視窗,它和 overlapped 視窗的主要區別是彈出式視窗不需要有標題列,也不必有邊框。彈出式可以擁有其它最上層視窗或者被擁有。所有彈出式視窗也都有 WS_CLIPSIBLINGS屬性。
(4)所有者視窗(owner)只能是 overlapped 或者 pop-up 類型的視窗,子視窗不能是所有者視窗,也就是說子視窗不能擁有其它視窗。
overlapped 或者 pop-up 類型的視窗在擁有其它視窗的同時,也可以被擁有。
在使用CreateWindowEx建立 WS_OVERLAPPED 或者 WS_POPUP類型的視窗時,可以在 hwndParent 參數中給出它的所有者視窗的控制代碼。如果 hwndParent 給出的是一個child 類型的視窗控制代碼,則系統自動將新建立視窗的所有權交給該子視窗的頂級父視窗。在這種情況下,參數hwndParent被儲存在建立視窗的parent域中,而它的所有者視窗控制代碼則儲存在owner域中。
(5)預設情況下,對話方塊和訊息框屬於 owned 視窗,除非在建立它們的時候明確給出了WS_CHILD屬性,(比如對話方塊中嵌入對話方塊的情形)
否則由系統負責給它們指定owner視窗。需要注意的是,一旦建立了owned類型的視窗,就無法再改變其所有關係,因為WIN32沒有沒有提供改變視窗所有者的方法。
而且在Win32中,由於有多線程的存在,所以要注意保證父子視窗或者owner/owned 視窗要同屬於一個線程。
(6)對於 WS_CHILD類型的視窗,它的父視窗就是它的所有者視窗。一個子視窗的父視窗也是在CreateWindow函數中用hWndParent參數指定的。子視窗只能在父視窗的客戶區中顯示,並隨父視窗一起銷毀。
子視窗必須有一個父視窗,這是它和overlapped 以及 pop-up 視窗之間的主要區別。父視窗可以是最上層視窗,也可以是其它子視窗。
三、幾個相關函數的說明
(1)擷取/設定所有者視窗
win32 API提供了函數GetWindow函數(GW_OWNER 標誌)來擷取一個視窗的所有者視窗控制代碼。
GetWindow(hWnd, GW_OWNER)永遠返回視窗的所有者(owner)。對於子視窗,函數返回 NULL,因為它們的父視窗就相當於所有者(注意,是“相當於”)。因為Windows系統沒有維護子視窗的所有者資訊。
MFC中則是通過如下函數得到所有者視窗指標:
_AFXWIN_INLINE CWnd* CWnd::GetOwner() const
{ return m_hWndOwner != NULL ? CWnd::FromHandle(m_hWndOwner) : GetParent(); }
從上述代碼我們可以看出,它返回的值和GetWindow返回的有所區別,如果當前視窗沒有owner,那麼將返回它的父視窗指標。
但是Windows沒有提供改變視窗所有者的方法。MFC中則提供了改變所有者的方法:
_AFXWIN_INLINE void CWnd::SetOwner(CWnd* pOwnerWnd)
{ m_hWndOwner = pOwnerWnd != NULL ? pOwnerWnd->m_hWnd : NULL; }
另外,mfc還提供了CWnd::GetSafeOwner( CWnd* pParent, HWND* pWndTop );函數,可以用來得到參數pParent的第一個非child屬性的父視窗指標。如果這個參數是NULL,則返回當前線程的主視窗(通過AfxGetMainWnd得到)。架構經常使用這個函數尋找對話方塊或者屬性頁面的所有者視窗。
(2)擷取/設定父視窗
WIN32 API給出了函數GetParent和SetParent。而mfc也是完全封裝了這兩個函數:
_AFXWIN_INLINE CWnd* CWnd::SetParent(CWnd* pWndNewParent)
{ ASSERT(::IsWindow(m_hWnd)); return CWnd::FromHandle(::SetParent(m_hWnd,
pWndNewParent->GetSafeHwnd())); }
_AFXWIN_INLINE CWnd* CWnd::GetParent() const
{ ASSERT(::IsWindow(m_hWnd)); return CWnd::FromHandle(::GetParent(m_hWnd)); }
對於SetParent,msdn裡面說明了父子視窗必須是同一個進程的。但是由於視窗控制代碼是系統全域唯一的,不屬於同一個進程的情況下,也可以成功調用,但是後果未知。
GetParent的傳回值比較複雜,對於overlapped類型的視窗,它返回0,對於WS_CHILD類型,它返回其父視窗,對於WS_POPUP類型,它返回其所有者視窗,如果想得到建立它時所傳遞進去的那個hwndParent參數,應該用GetWindowWord(GWW_HWNDPARENT)函數。
(3)GetWindowWord(hWnd, GWW_HWNDPARENT)返回一個視窗的父視窗,如果沒有,則返回其所有者。
(4)上面談到,當一個owner視窗被最小化後,系統自動隱藏它所擁有的視窗。當owner視窗被恢複的時候,系統自動顯示它所擁有的視窗。在這兩種情況下,系統都會發送(send)WM_SHOWWINDOW訊息給被擁有的視窗。某些時候,我們可能需要隱藏 owned視窗,但並不想最小化其所有者視窗,這時候,可以通過ShowOwnedPopups函數來實現,該函數設定或者刪除當前視窗所擁有的視窗的WS_VISIBLE屬性,然後發送WM_SHOWWINDOW訊息更新視窗顯示。