Windows窗口分析
(本文嘗試通過一些簡單的實驗,來分析Windows的窗口機制,并對微軟的設計理由進行一定的猜測,需要讀者具備C++、Windows編程及MFC經驗,還得有一定動手能力。文中可能出現一些術語不統一的現象,比如“子窗口”,有時候我寫作“child window”,有時候寫作“child”,我想應該不會有太大影響,文章太長,不一一更正了)
問題開始于我的最近的一次開發經歷,我打算把程序的一部分界面放在DLL中,而這部分界面又需要使用到Tooltip,但DLL中的虛函數PreTranslateMessage無法被調用到,原因大家可以在網上搜索一下,這并不是我這篇文章要講的。PreTranslateMessage不能被調,那Tooltip也就不能起作用,因為Tooltip需要在PreTranslateMessage中加入tooltip.RelayEvent(&msg)來觸發事件,方可正常顯示。解決方法有好幾個,我用的是比較麻煩的一個——完全自己手動編寫Tooltip,然后用WM_MOUSEMOVE等事件來觸發Tooltip顯示,寫好之后發現些小問題,那就是調試運行時候IDE給了個warning,說我在析構函數中調用了DestroyWindow,這樣會導致窗口OnDestry和OnNcDestroy不被正常調用,這個問題我以前遇到過,當然解決方法也是顯而易見的,只需要在窗口對象(C++概念,非Windows內核對象,下文同)銷毀前,調用DestroyWindow即可。對于要銷毀的這個窗口的子窗口,是不需要顯式調用DestroyWindow的,因為父窗口在銷毀的時候也會銷毀掉它們,OK,我把這個過程用個示意圖說明一下:
圖1
上圖表示了App Window及其子窗口的關系,現在假設我們要銷毀Parent Window 1(對應的對象指針是m_pWndParent1),我們可以m_pWndParent1->DestroyWindow(),這樣Child Window 1,Parent Window 2,Child Window 2都被銷毀了,銷毀的時候這些窗口的OnDestry和OnNcDestroy都被調用了,最后delete m_pWndParent1,此時m_pWndParent1->m_hWnd已經是NULL,不會再去調用Destroy,在析構的時候也就不會出現Warning。但如果不先執行m_pWndParent1->DestroyWindow()而直接delete m_pWndParent1,那么在CWnd::~CWnd中就會調用DestroyWindow(m_hWnd),這樣會產生WM_DESTROY和WM_NCDESTROY,會嘗試去調用OnDestry和OnNcDestroy,但由于是在CWnd的函數~CWnd()的內部調用這兩個成員,此時的虛函數表指針并不指向派生類的虛函數表,因此調用的其實是CWnd::OnDestroy和CWnd::OnNcDestroy,派生類的OnDestry和OnNcDestroy不被調用,但我們很多時候把釋放內存等操作寫在派生類的OnDestroy和OnNcDestroy中,這樣,就容易導致內存泄露和邏輯混亂了。
上面這些道理我當然是知道的,但Warning還是出現了,而且我用排除法確定了是跟我寫的那個Tooltip有關,下面是關于我的Tooltip的截圖:
圖2
大家看到,Tooltip顯示在我的圖形窗口上,它是個彈出式(popup)窗口,其內容為當前鼠標光標的坐標值,圖形窗口之外,我是不想讓它顯示的,那么按照我的思路,Tooltip就應該設計是圖形窗口的子窗口,它的窗口對象就應該作為圖形窗口對象的成員,在圖形窗口OnCreate的時候創建,在圖形窗口被DestroyWindow的時候自動銷毀,前面提到過,父窗口被銷毀的時候,其子窗口會被自動銷毀,沒錯吧,所以不需要顯式去對Tooltip調用DestroyWindow。可事實證明了這樣是有問題的,因為Tooltip的父窗口根本不是,也不能是圖形窗口。大家可以看到我的圖形窗口是作為一個子窗口嵌入到別的窗口中去的,它的屬性包含了WS_CHILD,通過實驗,我發現Tooltip的父窗口只能指定為程序主窗口,如果企圖指定為那個圖形窗口的話,它就自動變為程序主窗口,再進一步研究發現,彈出式窗口的父窗口都不能是帶WS_CHILD風格的窗口,然后打開spy++查看,彈出式窗口的上一級都是桌面,可是,通過GetParent函數,得到的彈出式窗口的父窗口卻是程序主窗口而不是桌面,為什么?……問題越來越多,我糊涂了,上面說的都是在我深入理解前,所看到的現象,包括了我的一些概念認識方面的錯誤。
好吧,我們現在開始,一點點地通過實驗去攻破這些難題!
一、神秘的WS_OVERLAPPED
我們從WinUser.h頭文件中可以看出,窗口可分三種,其Window Styles定義如下:
那么我們很容易得到這個結論:style的最高位是1的,是一個popup窗口,style的次高位是1的,代表是一個child窗口,如果最高位次高位都是0,那這個窗口就是一個overlapped窗口,如果兩位都是1,厄……MSDN告訴我們不能這么干,事實呢?我后面再講。其實這個結論是有點過時的,甚至很能誤導人,不是我們的原因,很可能是Windows的歷史原因,為什么?具體也是后面講。嘿嘿。
OK,我們現在開始來嘗試,看看這些風格究竟影響窗口幾何,對了,準備spy++,這是必備工具。
用VC++的向導創建一個Hello World的Windows程序,注意是Windows程序,不是MFC的Hello World,這樣我們可以繞開MFC,專注于查看一些Windows的技術細節,編譯,運行。
圖3
然后用spy++查看這個窗口的風格,發現其風格顯示為“WS_OVERLAPPEDWINDOW|WS_VISIBLE|WS_CLIPSIBLING|WS_OVERLAPPED”。此時它的創建函數為:
只制定了一個WS_OVERLAPPEDWINDOW,但我們很快就找到了WS_OVERLAPPEDWINDOW的定義:
原來overlapped窗口就是有標題,系統菜單,最小最大化按鈕和可調整大小邊框的窗口,這個定義是正確的,但只是個我們認知上的概念的問題,因為popup和child窗口也同樣可以擁有這些(后面證明)。由于WS_OVERLAPPED為0,那我們是不是可以把WS_OVERLAPPEDWINDOW定義中的WS_OVERLAPPED拿掉呢?那是肯定的,那也就是說WS_OVERLAPPED什么都不是!我們只作popup和child的區分,是不是這樣?也不是,我們繼續實驗。
很簡單,接下去我們只給這個向導生成的代碼加一點點東西,就是把CreateWindow改成:
對,給窗口風格增一個popup風格,看看會怎么樣?運行!這回可不得了,窗口縮到了屏幕的左上角,并且寬度高度都變為了最小,當然,你還是可以用鼠標拖動窗口邊緣來調整它的大小的。如圖:
圖4
這是為什么呢?觀察CreateWindow的,第四、第五、第六和第七參數,分別為窗口的x坐標,y坐標,寬度,和高度,CW_USEDEFAULT被define成0,所以窗口被縮到左上角去也就不奇怪了,可沒有popup,光是overlapped風格的窗口,為什么不會縮呢?看MSDN的說明,對第四個參數的說明:“If this parameter is set to CW_USEDEFAULT, the system selects the default position for the window's upper-left corner and ignores the y parameter. CW_USEDEFAULT is valid only for overlapped windows; if it is specified for a pop-up or child window, the x and y parameters are set to zero. ”其余幾個參數也有類似的描述,這說明了什么?說明Windows對overlapped和popup還是作區分的,而這點,算是我們發現的第一個不同。哦,還有件事情,就是用spy++觀察其風格,發現其確實多了一個WS_POPUP,其余沒什么變化。
繼續,這回還是老地方,把WS_POPUP改為WS_CHILD,試試看,這回創建窗口失敗了,返回0,用GetLastError查看具體錯誤信息,得到的是:“1406:無法創建最上層子窗口。”看來桌面是不讓我們隨便搞的。繼續,還是老地方,這回改成:
嗯?有沒搞錯,又是popup又是child,肯定不能成功吧,不試不知道,居然成功了,這個創建出來的窗口乍一看,跟popup風格的很像,但用起來有些怪異,比如:當它被別的窗口擋住的時候,不能通過點擊它的客戶區來讓它顯示在前面,即使點擊它的標題欄,也是要松開鼠標左鍵,它才能顯示在前面,還有就是用spy++的“瞄準器”沒法準確捕捉到這個窗口,瞄準器對準它的時候,就顯示Caption為“Program Manager”,class為“Program”,“Program Manager”是什么?其實就是我們所看到的這個桌面(注意,不是桌面,我說的是我們說“看到的桌面”,就是顯示桌面圖標的這個所能看到的桌面窗口,和前面提到的桌面窗口是有區別的)的父窗口的父窗口,這個窗口一般情況下是不能直接“瞄準”到的,這點可以通過spy++證實,如圖:
圖5
圖6
spy++不能直接“瞄準”這個popup和child并存的怪窗口,但我們有別的辦法捕捉到它,<Alt>+<F3>,輸入窗口的標題來查找(記得運行程序后刷新一下才能找到),結果見下圖:
圖7
我們從上圖中清楚地看到,popup和child并存!用spy++逐個查看桌面窗口的下屬,這種情況還是無獨有偶的,但這樣的窗口代表了什么意義,我就不清楚了,總之用起來怪怪的,對Microsoft來說,這可能就是Undocumented,OK,我們了解到這里就行了,但一般情況下,我們不要去創建這種奇怪的窗口。這幾輪實驗給我們什么啟示?設計上的啟示:一個應用程序的主窗口通常是一個Overlapped類型的窗口,當然有時可以是一個popup窗口,比如基于對話框的程序,但不應該是一個child窗口,盡管上面演示了如何給應用程序主窗口加入child風格。
那還有一個問題,我為什么認為WS_OVERLAPPED神秘呢?這還算是拜spy++所賜,按照我們一般的想法,如果一個窗口的風格的最高兩位都是0,它既不是popup也不是child的時候,那它就是Overlapped。事實上spy++的判定不是這樣的,就以剛才的實驗為例,當使用WS_OVERLAPPEDWINDOW|WS_POPUP風格創建窗口的時候,WS_OVERLAPPED和WS_POPUP屬性同時出現了,我做了很多很多的嘗試,企圖找出其中規律,看看spy++是怎么判定WS_OVERLAPPED的,但至今沒結論,我到MSDN上search,未果,有人提起這個問題,但沒有令我滿意的答復,下面這段文字是我找到的可能有點線索的答復:
Actually, Microsoft Spy++ is wrong.
There are two bits in the window style that control its type. If the high-order bit of the style DWORD is set, the window is a popup window. If the next bit is set, the window is a child window. If neither is set, the window is overlapped. (If both are set, the result is undocumented.)
Look at these definitions from WinUser.h.
Your window style (0x94c00880) has the high-order bit set and the next bit clear so it is a popup window, not an overlapped window.
The correct way to identify all three types of windows (this is what Spy++ should do) is
這斷描述跟我的想法一致。要知道,就算你只給窗口一個WS_POPUP的風格,WS_OVERLAPPED也會顯示在spy++上的,我認為這十分有問題,究竟spy++如何判,估計得請教比爾蓋茨了。還有一段有趣的描述,估計也有所幫助:
As long as...
WS_POPUP | WS_OVERLAPPED
...is absolutelly equivalent with...
WS_POPUP
... why do you care if Spy++ lists WS_OVERLAPPED or not?
Please stop playing "Thomas Unbeliever" with us.
Becomes too expensive to use "walking on the water" device here again, and again. ;)
雖然這么說,我還是認為,spy++給了我們不少誤導,那么對WS_OVERLAPPED的討論就暫時告一段落吧,作為一個技術人,很難容忍自己無法理解的邏輯,我就是這么種人……不過如果再扯下去的話這篇文章就不能結束了,所以姑且認為,這是spy++的錯,而我們還是認為窗口分3種——popup,child和Overlapped。(Undocumented不在此列,也不在本文講述之列)
二、Parent與Owner
這是內容最多的一節,做好心理準備。
微軟和我們開了個玩笑,告訴我們,窗口和人一樣,可以有父母,有主人……我們先來看一個最著名的Windows API:
猜對了,我就是從MSDN上copy下來的,看第九個參數的名字叫hWndParent,顧名思義哦,這就是Parent窗口了,不過我們中國人不喜歡稱之“父母窗口”,我們喜歡叫它“父窗口”,簡單一點。其實這個名字對我們造成了不少的誤導,我只能說,可能也是由于歷史原因,比如在Windows 1.0(1985年出的,當時沒什么影響力)的時候,只有Parent這個概念,沒有Owner的概念。
回頭看看文章開始我提起的,我企圖將Tooltip的父窗口設置為一個圖形窗口,不能成功,Tooltip的父窗口會自動變成應用程序主窗口,這是為什么?好,現在開始講概念了,都是我花了很多時間在互聯網上搜索,篩選,確認,得出來的結論:
規則一:Owner window控制了Owned window的生存,當Owner window被銷毀的時候,其所屬的Owned window就會被銷毀。
規則二:Parent window控制了Child window的繪制,Child window不可能顯示在其Parent window的客戶區之外。
規則三:Parent window同時控制了Child window的生存,當Parent window被銷毀的時候,其所屬的Child window就會被銷毀。
規則四:Owner window不能是Child window。
規則五:Child window一定有Parent(否則怎么叫Child?),一定沒有Owner。
規則六:非Child window的Parent一定是桌面,它們不一定有Owner。
這是比較重要的幾點,如果你認為這跟你以前學到的,或者認知的有所不同,先別急著抗議,先看看我是怎么理解的。除了這幾條規則,下面我還會逐步給出一些規則。
先說比較好理解的Child window,上文提到了,包含了WS_CHILD風格的窗口就叫Child window,我們中文叫“子窗口”。那么我前面提到的我寫的那個Tooltip,是不是“子窗口”呢?——當然不是了,它沒有WS_CHILD風格啊,它是popup風格的,我想當然地認為在創建它的時候給它指定了那個Parent參數,那它的Parent就是那個參數,其實是錯的。這個實驗最簡單了,隨便找些應用程序,比如“附件”里的計算器,用spy++的“瞄準器”觀察上面的按鈕等“子窗口”,在Styles標簽中,我們可以看到WS_CHILD(或者WS_CHILDWINDOW,一樣的)屬性,然后在Windows標簽中,我們可以清楚地看到,凡是包含了WS_CHILD屬性的窗口(子窗口),都沒有Owner window,不信還可以繼續觀察其它應用程序,省去自己編程了。再看它們的Parent window,是不是一定有的?——當然一定有。
前面說了,子窗口不能顯示在父窗口客戶區之外,我們最常見的子窗口就是那些擺在對話框上的控件,什么button啊,listbox啊,combobox啊……都有個共同特點,不能拖動的,除非你重寫它們的window procedure,然后響應WM_MOUSEMOVE等消息,實現所謂“拖動”。那么有沒有能夠像應用程序主窗口那樣有標題欄,能夠被自由拖動的子窗口呢?——當然有!要創建是嗎?簡單,直接用MFC向導創建一個MDI程序即可,MDI的那些View其實就是可以自由拖動的子窗口,可以用spy++查看一下它們的屬性,當然,你是不能把它們拖出主窗口的客戶區的。也許你跟我一樣,覺得MFC封裝了過多的技術細節,想完全自己手動創建一個能拖動的子窗口,而且看起來就像個MDI的界面,OK,follow me。
首先當然是用應用程序向導生成最普通的Window應用程序了。然后增加一個窗口處理函數,也就是我們準備創建的子窗口的處理函數了。
DoNothing?好名字。注冊之:
最后當然是把它給創建出來了:
關于WS_CLIPSIBLINGS屬性,下文將提到。好,就這樣,大家看看運行效果:
圖8
是不是很少遇到這種窗口組織結構?確實很少人這樣用,而且哦,你會發現子窗口的標題欄沒辦法變為彩色,它一直是灰的,就表示它一直處于未激活狀態,你怎么點它,拖它,調它,都沒用的,而這個時候程序主窗口一直顯示為激活狀態,如何激活這個子窗口?我曾經對此苦思冥想,最后才知道,子窗口是無法被激活的,你立即反駁:“那MFC如何做到的?”哈哈,好,你反應夠快,我下文會給你演示如何“激活”子窗口。(注意是加引號的)現在嘗試移動主窗口,你會發現所有它的子窗口都會跟著主窗口移動的,這就好像我們看蘋果落地一樣,不會覺得奇怪,但你有沒有想過,主窗口移動的時候,其子窗口對屏幕的位置也發生了變化,不變的是相對主窗口的客戶區坐標。這就是子窗口的特性。再試試看啟用/禁用主窗口,顯示/隱藏主窗口看看,就不難得出結論:
規則七:子窗口會隨著其父窗口移動,啟用/禁用,顯示/隱藏。
子窗口我們就暫時講那么多,接著講所有者窗口,就是Owner window,由于子窗口一定沒有Owner,因此Owner window是對popup和Overlapped而言的,而popup和Overlapped前面也提到了,不一定有Owner,不像Child那樣一定有Parent。現在進入我們下一個實驗:
還是用向導生成最普通的Windows hello world程序,步驟和上一個實驗很相似,僅僅改了一點點東西,改了哪點?就是把CreateWindowEx函數的第四個參數的WS_CHILD拿掉,其余不變,代碼我就不貼了,大家編譯并運行看看。大家會看到類似這個效果:
圖9
彈出窗口的caption是藍色的,說明它處于激活狀態,如果你現在點擊程序主窗口,那彈出窗口的標題欄就變灰,而程序主窗口的標題欄變藍,兩個窗口看起來就像并列的關系,但你很快發現它們其實不并列,因為如果它們有重疊部分的話,彈出窗口總是遮擋程序主窗口。用spy++觀察之,發現程序主窗口就是彈出窗口的Owner。
規則八:非Child window總是顯示在它們的Owner之前。
看到了沒?這個時候CreateWindowEx的第九個參數的意義就不是Parent window,而是Owner,那把這個參數改為NULL,會有什么效果呢?馬上試試看,反正這么容易。
圖10
初一看沒什么變化,其實變化大了,一是主窗口這回可以顯示在彈出窗口之前了,二是任務欄上出現了兩個button。
圖11
用spy++觀察到這兩個窗口的Owner都是NULL。
規則九:Owner為NULL的非Child窗口能夠(不是一定哦)在任務欄上出現它們的按鈕。
這個時候,你應該清楚為什么給一個MessageBox正確指定一個Owner這么重要了吧?我以前有個同事,非常“厲害”,他創建了一個程序,一旦出現點什么問題,就能把MessageBox彈得滿屏都是,而且把任務欄霸占得渣都不剩,他大概是沒明白這個道理。MessageBox是一個非child窗口,如果不指定一個正確的Owner,那彈出MessageBox之后,Owner還是處于可操作的狀態,兩個窗口看起來是并列的,都在任務欄上有顯示,如果再彈出MessageBox,先關閉那個MessageBox?我看先關哪個都沒問題,因為界面操作上沒有限制,但這樣很容易導致邏輯混亂,如果不幸走入了個死循環,連續彈MessageBox,那就像這位同事寫的那個程序那樣,滿屏皆是消息框了。
我們現在來進行一些稍微復雜點點的實驗,就是創建A彈出窗口,其Owner為主窗口,創建B彈出窗口,其Owner為A窗口,創建C彈出窗口,其Owner為B窗口。步驟模仿上面的窗口創建步驟即可,好,編譯,運行,效果大致如此:
圖12
現在,把主窗口最小化,看看發生了什么事情。你會發現A窗口不見了,而B,C窗口尚在,A窗口究竟是跟隨主窗口一起最小化了呢,或者被銷毀了呢?還是被隱藏了呢?答案是被隱藏了,我們可以通過spy++找到它,發現它的屬性里邊沒有WS_VISIBLE。那現在將主窗口還原,A這時候出現了,那現在我們最小化A,Oh?What happen?B不見了,主窗口和C都還在,我們還是老辦法,用spy++看B,發現它沒了WS_VISIBLE屬性,現在還原A窗口,方法如下圖所示:
圖12_x
注意,最小化的A并不顯示在任務欄上。還原A后B也出現了。
規則十:Owner窗口最小化后,被它擁有的窗口會被隱藏。
前面測試的是最小化,那我們現在不妨來測試一下,讓A隱藏,會怎么樣?在主窗口里創建一個button,點這個button,就執行ShowWindow(g_hwndA, SW_HIDE),如圖:
圖13
你會發現,被隱藏的只有A,A隱藏后主窗口,B和C都是可見的,你可以繼續嘗試,隱藏B和C,或者主窗口,不過,你隱藏了主窗口的話恐怕就沒法通過主窗口的菜單來關閉程序了,只能打開任務管理器結束掉程序。
規則十一:Owner隱藏,不會影響其擁有的窗口。
現在不是最小化,也不是隱藏,而是測試“關閉”,即銷毀窗口,嘗試關閉A,發現B,C被關閉;嘗試關閉B,發現C被關閉。這個規則也就是規則一了,不必再列。
好,我不可能把所有的規則都列出來,但我相信前面所寫的這些東西,對大家起到了拋磚引玉的作用了,其它規則,也可以通過類似的實驗得出,或者用已有的規則去推導。那在轉入下一節前,我提點問題:
為什么子窗口沒有Owner?(就是我們來猜猜微軟為什么這樣設計)試想一個Child既有Parent,又有Owner,Parent控制其繪制,Owner控制其存在,在Owner銷毀的時候,子窗口就要被銷毀,而其Parent有可能還繼續存在,那這個子窗口的消失可能有點不明不白,這是其中一個原因,另一個原因也類似,如果Parent不控制子窗口的存在,只管其繪制,那么在Parent銷毀的時候,Owner可以繼續存在,這個時候的子窗口是存在,而又不能顯示和訪問的,這可能會導致別的怪異問題,既然起了Child這個名字,就應該把它全權交給Parent,由Parent來決定它的一切,我想這就是微軟的道理。
那我們如何獲取一個窗口的Parent和Owner?大家都知道API函數,GetParent,這是用來獲取Parent窗口句柄的API——慢!這并不完全正確!大家再仔細點看看MSDN,再仔細點:
If the window is a child window, the return value is a handle to the parent window. If the window is a top-level window, the return value is a handle to the owner window.
什么是top-level window?就是非Child window,這個后面再詳細談這個,現在注意看了,GetParent返回的有可能不是parent,對于非child窗口來說,返回的就不是parent,為什么?因為非child窗口的parent恒定是Desktop啊(規則6),這還需要獲取嗎?我們接下去的實驗是用來測試GetParent這個函數是否工作正常的,什么?測試M$提供的API,沒錯,呵呵,當一把微軟的測試員吧。接上面那個實驗:
//在窗口創建完成后,調用下面的代碼,在第一個GetParent處設置個斷點,查看返回值,如果返回NULL,按照MSDN所說的,用GetLastError看看是否有出錯。
我的實驗結果有些令我不解,清一色返回0,包括GetLastError,也就是說沒有出錯,那GetParent返回0,根據MSDN上的描述,原因只可能是:這些窗口確實沒有Owner。不對啊?難道前面的規則和推論都是錯誤的不成?我創建它們的時候,就明明白白地指定了hWndParent參數,而且上面的實驗也表明了他們之間的Owner和Owned關系,那是不是GetParent錯了?我想是的,你先別對著我扔磚頭,想看到正確的情況么?好,我弄給你看。
我們是如何創建A,B和C這幾個彈出窗口的?我再把創建它們的語句貼一下吧:
現在把這個語句改為:
對,就是加上一個WS_POPUP,看看情況變得怎么樣?
很驚訝,對不?GetParent這回全部都正確地按照MSDN的描述工作了,這是我發現的popup和Overlapped的第二個差別,第一個差別?在文章開頭附近,自己回去找。而spy++顯示出來的那個Parent,其實就是GetParent返回的結果。記住,對于非child窗口來說,GetParent返回的并不是Parent,MSDN也是這么說的,你看看這個函數的名字是不是很有誤導性?還有spy++也真是的,將錯就錯。好吧,就讓它錯去吧,但我們得記住:對非Child窗口來說,Parent一定是桌面。好,再有個問題,看剛剛這個實驗,對于有WS_POPUP風格的非Child窗口來說,GetParent能夠取回它的Owner,可對于沒有WS_POPUP風格的非Child窗口來說,GetParent恒定返回0,那我們如何有效地取得非Child窗口真正的主人呢?方法當然是有的,看:
這么一來,無論是否帶有WS_POPUP風格,都能夠正常取得其所有者了,這個跟spy++的結果一致,用GetWindow取得的Owner總是正確的,那有沒有一種方法,使得取得的Parent總是正確的?很遺憾,沒有直接的API,包括使用GetWindowLong(hwnd, GWL_HWNDPARENT)都不能一直正確返回Parent,BTW,有位高人說,GetWindowLong(hwnd, GWL_HWNDPARENT)和GetParent(hwnd)有時候會得到不同的結果,不過這個我嘗試不出來,我觀察的,它們總是返回一樣的結果,無論對什么窗口,真懷疑GetParent(hwnd)就是return (HWND)GetWindowLong(hwnd, GWL_HWNDPARENT),雖然我們不能直接一步獲取正確的Parent,但我們可以寫一個簡單的函數:
你終于憋不住了,對我大吼:“你有什么依據說非Child窗口的Parent一定是Desktop?”我當然是有依據的,首先是這些非child window的繪制,不能超出桌面,超出桌面就什么都看不見了,只能是桌面管理著它們的繪制,如果它們確實存在Parent的話,當然,聰明你認為這個理由并不充分,OK,我們編程來證明,先介紹一個API:
又被你猜對了,我是從MSDN上copy下來的(^_^),看MSDN對這個函數的說明:
hwndParent
[in] Handle to the parent window whose child windows are to be searched.
If hwndParent is NULL, the function uses the desktop window as the parent window. The function searches among windows that are child windows of the desktop.
hwndChildAfter
[in] Handle to a child window. The search begins with the next child window in the Z order. The child window must be a direct child window of hwndParent, not just a descendant window.
If hwndChildAfter is NULL, the search begins with the first child window of hwndParent.
lpszClass
窗口類名(我來翻譯,簡單點)
lpszWindow
窗口標題
關鍵是看第一個參數,如果hwndParent為NULL,函數就查找desktop的“子窗口”,但這個“子窗口”是加引號的,因為這里的“子窗口”和本文前面一直提到的子窗口確實不太一樣,那就是這里的“子窗口”沒有WS_CHILD風格,算是一個特殊吧,也難怪GetParent不愿意告訴我們desktop就是這些非Child的父窗口。好,有這個函數,我們就可以知道剛才創建的那幾個彈出窗口的老爸究竟是不是桌面。代碼十分簡單:
結果如何?(是不是偷懶干脆不做,等著我說結果啊?)我的結果是全部找到了,和用spy++查找的結果一樣,所以我有充分的理由認為,所有非child窗口其實是desktop的child,spy++的樹形結構組織確實也是這么闡述的。你很厲害,你還是能夠駁斥我:“根據規則三,Parent被銷毀的時候,其Child將被銷毀,你證明給我看?”這個……有點難:
My god,Desktop沒了,你說我們還能看到什么呢?當然微軟不會沒想到這點,DestroyWindow當然不能成功,錯誤代碼為5,“拒絕訪問”。好,我有些累了,不能再糾纏了,轉入下一節!留個作業如何?嘗試使用SetParent這個API,改變窗口的Parent,觀察運行情況,并思考這樣做有什么不好之處。
三、如何體現WS_CLIPSIBLING和WS_CLIPCHILD?
看了這個標題,應該怎么做?我想你十有八九是打開MSDN,輸入這兩個關鍵字去搜索吧?OK,不用了,我把MSDN對這兩個窗口風格的說明貼出來:
WS_CLIPCHILDREN?? Excludes the area occupied by child windows when you draw within the parent window. Used when you create the parent window.
WS_CLIPSIBLINGS?? Clips child windows relative to each other; that is, when a particular child window receives a paint message, the WS_CLIPSIBLINGS style clips all other overlapped child windows out of the region of the child window to be updated. (If WS_CLIPSIBLINGS is not given and child windows overlap, when you draw within the client area of a child window, it is possible to draw within the client area of a neighboring child window.) For use with the
WS_CHILD style only.
找到是不難,但如果光看這個就明白的話我也不必要寫這種文章了,沒有適當的代碼去實踐,估計很多人是不懂這兩個風格什么含義的。OK,現在我來帶你實踐。spy++開著不?哈,別關啊,后面還要用到。用spy++觀察各個top-level window(非Child窗口)的屬性,是不是都有個WS_CLIPSIBLINGS?想找個沒有的都不行,如果你不服氣,你要自己創建一個沒有WS_CLIPSIBLINGS風格的頂層窗口,好吧,我在這里等你一會兒(……一會兒過去了……),你垂頭喪氣地回來了:“不行,即便我不指定這個風格,Windows也強制幫我加上。”那……你可以強制剝離掉這個風格啊,這樣:
執行后用spy++一看,還是沒有把WS_CLIPSIBLINGS風格去掉,看來Windows是吃定你的了。嗯,前面說的都是top-level window,那對于child window呢?創建一個MFC對話框,在上面加幾個button,然后增加/刪除這幾個button的WS_CLIPSIBLINGS風格?你除了發現child window對與WS_CLIPSIBLING風格不再是強制的之外,恐怕仍然一無所獲吧。還是得Follow me,我還是不用MFC,用最簡單的Windows API。模仿第二節的創建幾個popup窗口A、B、C的那個例子,只不過現在的CreateWindowEx改成這樣:
創建出來的效果如圖:
圖14
一眼看沒什么奇怪的,但嘗試拖動里邊的窗口就出現些問題了,首先是顯示在最前端的C窗口不能拖動(其實是被擋住了),然后你發現B也不能拖動,A可以,A一拖,就出現這種情況:
圖15
如果你嘗試拖動B,C,情況可能更奇怪,總之就是窗口似乎不能正常繪制。那如何才能正常呢?我不說你都知道了,就是這節的主題,給這幾個child window加上WS_CLIPSIBLINGS風格,就OK了,那如何解釋?現在看圖14,表面上看是C疊在B上面,而B疊在A上面,事實上正好相反不是,(關于窗口Z order的問題看下一節)事實是B疊在C之上,A疊在B上面,所以企圖拖C,其實點到的是A的客戶區,C當然“拖不動”,那為什么看起來是C疊B,B疊A?這跟繪制順序有關系,A先繪,然后B,最后C,也許你又要我驗證了,好,我改一下代碼,打個log出來給你看。把Do nothing的那個窗口過程改為:
打印結果為:
A Paint
B Paint
C Paint
那B為什么繪在A的上面?那就是因為沒有指定WS_CLIPSIBLINGS,WS_CLIPSIBLINGS這個風格會在窗口繪制的時候裁掉“它被它的兄弟姐妹擋住的區域”,被裁掉的區域當然不會被繪制。對子窗口來說,這個風格不是一定有的,因為微軟考慮到大多數子窗口,比如dialog上的控件,基本上都是固定不會移動的,不會產生互相疊起來的現象。那對于top-level窗口,如果可以沒有這個風格,那我們的界面可能很容易混亂,所以這個風格是強制的。也許你要問:“那為什么我移動A的時候,A自己不會重繪?”當然不會了,因為我移動A,A本來就是在最頂層,完全可見的,沒有什么區域變得無效需要重新繪制,所以它不會被重繪,這個可以通過log看出來。
現在分析下一個風格WS_CLIPCHILDREN,前一個是裁兄弟姐妹,這個是裁孩子,微軟也夠狠的。不多說了,直接改代碼來體會這個風格的作用,按照這個意思,有這個風格的父窗口在繪制的時候,不會把東西繪到子窗口的區域上去,這個嘛,簡單,我們只要在父窗口的WM_PAINT里畫點東西試試看就好了。代碼還是前面的代碼,把A,B,C都加上WS_CLIPSIBLINGS,主窗口不要WS_CLIPCHILDREN風格,我們看看是不是能把東西畫到子窗口的區域去。
運行結果如圖:
?
圖16
嗯?沒有穿過啊?為什么?先動腦想想半分鐘。
那是因為我們的實驗不夠嚴謹,現在在主窗口WM_PAINT消息的處理中加入一個Debug內容:
再看看debug出來的log:
Main window paint
A Paint
B Paint
C Paint
因為是主窗口先繪制,然后才是子窗口,所以即便這根線是穿過子窗口區域的,恐怕也看不出來了。那我們就不要在WM_PAINT里繪制,我們增加一個菜單項,叫paint a line,點這個菜單就執行下面的代碼:
運行程序,點菜單“paint a line”,看運行效果:
?
圖17
算是“成功穿越”了,這時候你再給父窗口加上WS_CLIPCHILDREN看看,結果我就不說了,就算不嘗試其實也能想得到。相信大家到此為止都理解了這兩個風格的作用了。
再順便說些實踐經驗,有時候我們會發覺程序在頻繁重繪的時候閃爍比較厲害,還是拿這個例子改裝一下吧,先把主窗口的WS_CLIPCHILDREN風格拿掉,然后在其窗口處理函數中加入些代碼:
意思是說每0.2秒重繪一次主窗口,大家看看,是不是閃爍得厲害,閃爍過程中,我們依稀看到了這根線穿過了子窗口的區域……然后把WS_CLIPCHILDREN風格賦予主窗口,其余不變,再看看,是不是閃爍現象大為減少?通過這個例子告訴大家什么叫“把現有的技術用得最好”(參考我上一篇博文),有時候就差那么一點點。
四、Foreground、Active、Focus及對Z order的理解
看前面的這個“MDI”例子,也許你發現它跟MFC向導創建出來的MDI界面的最大不同就是子窗口無法“激活”,你怎么點,怎么拖都不行,它們的caption恒定是灰色的,我曾經為此苦思冥想……spy++是個好東西,前面主要是用它來查看窗口的屬性,現在我們用它來查看窗口消息,(不知道怎么做的看看spy++的幫助)在消息過濾中,我們只選擇一個消息,就是WM_NCACTIVATE,MSDN對這個消息的說明是:The WM_NCACTIVATE message is sent to a window when its nonclient area needs to be changed to indicate an active or inactive state. 那就是窗口激活狀態改變的時候,會收到這個消息啰?而我觀察下來的結果是,The WM_NCACTIVATE never came.
辦法總該是有的,比如利用SetActiveWindow這個API,在主界面上做個按鈕,點一下這個按鈕,就SetActiveWindow(g_hwndA),這樣來激活A窗口,而事實上這樣做是徒勞,A既沒有被激活,也沒有收到WM_NCACTIVATE。但我還是有辦法的,大家看下面的代碼,在那個叫WndProcDoNothing的窗口里加入對WM_MOUSEACTIVATE消息的處理:
現在再嘗試運行程序,點擊A,B,C窗口,是不是就可以把它們的caption變為彩色(我的是默認的淺藍色)了?什么道理?雖然這幾個子窗口不能真正地被激活(Windows機制決定的,只有top-level window才能被激活),但可以通過發WM_NCACTIVATE消息來欺騙它們,讓它們以為自己被激活了,于是把自己的caption繪制為淺藍色。如圖:
圖18
也許你還發現,點擊子窗口的客戶區不能讓子窗口調整到其它子窗口的前面,窗口那個前,那個后的這種次序叫“Z order”,又譯作“Z軸”,order是“序”的意思,這其實是窗口管理器維護的一個鏈表,沒錯,是鏈表,不是數組,不是隊列,不是堆棧,為什么是鏈表?因為窗口的次序經常發生變化,鏈表是最方便修改次序的了,只需要改變節點的指針,這點性能考慮,微軟是肯定做過的。下面是窗口的Z order的描述(我的描述,從MSDN改編):
桌面是最底層的窗口,不能改變的;對于top-level window,如果存在owner,一定會顯示在owner之上(owner一定不會擋住它),不存在擁有關系的top-level窗口,互相之間都有可能會阻擋,用戶的操作,窗口顯示隱藏最大最小化還原,或者顯式調用API設定等都有可能影響它們的次序,但微軟為了使得有些窗口總是能夠顯示在最頂或最底,還設立了一套特殊的規則,那就是top most window,SetWindowPos這個API就有調整次序的功能,或者把某窗口設置為top most,top most總是顯示在其它非top most窗口的上面,如果兩個窗口同時是top most,那么誰更上面呢?——都有可能,top most之間又是“公平競爭”的關系了,雖然他們對非top most總是保持著優勢,那把一個owner設置為top most,會怎么樣呢?由于被擁有的窗口必須在其owner的上面,所以那些被擁有的窗口也都全部變成了top most,盡管你沒有給他們指定top most,用spy++觀察top most窗口的屬性,在Extended Style欄目中,能看到一個“WS_EX_TOPMOST”屬性,這就是top most窗口的標志了。OK,top-level window的情況看來都沒什么問題了,那child window的情況呢?大家都知道,child是繪制在其parent的客戶區中的,不可能超出其parent的界限,相當于是其parent的一部分,那我們可不能以認為其child的z order跟其parent的是一致的呢?對于其它top-level窗口來說,這樣看是沒問題的,因為一個top-level窗口被移到了前面,它的child也會跟著它顯示在前面,反之亦然,但一個在Parent窗口內部,哪個child在前,哪個在后,又是有自己的一套private z order的,所謂國有國法,家有家規嘛,這樣看,我想就沒什么問題了。哦,不對,還有一點沒說,對于child來說,不能是top most窗口,用SetWindowPos設置也是沒用的。
那我們如何來知道整個Z order的鏈表?可以這樣:
這個函數會把某個Parent的子窗口句柄值,按照z order次序,從最頂打印到最底。如果hParent為NULL,那么就從桌面的最頂窗口開始,列出所有桌面的窗口,這樣意義不大,為什么?因為你會找出來很多很多窗口,可見的,不可見的,奇奇怪怪的,變來變去的,所以這種列窗口的方法通常是用于列子窗口的。
最后我想提提Foreground、Active和Focus這三者,非常容易讓人搞混的三個概念,我給出一些提示和方法,讀者自己去編程序體驗。
首先是Foreground窗口,說起Foreground就不能不說Foreground線程,Windows同時管理著很多線程,但為了給用戶操作起來“爽”一些,需要更快地響應用戶的操作,就弄了這么個Foreground線程的概念。比如用戶在玩掃雷,那掃雷這個程序的某個線程(據我所知掃雷只有一個線程)就被提升為Foreground線程,這個線程擁有比別的線程略高的優先級,能獲取更多的cpu時間片,以此更快一些地響應用戶,用戶正在使用的這個掃雷程序的主界面,就是Foreground窗口。那Active窗口是什么呢?Active窗口就是目前用戶正在使用的那個窗口……厄,這種解釋也未免太敷衍人了,那它跟Foreground窗口有什么異同啊?首先說“同”,那就是它們都必須是top-level window,而不能是child window,不同嘛……還是等等再說,那現在輪到Focus窗口了,Focus窗口就是目前直接接收到用戶鍵盤輸入消息的那個窗口,可以是child window。我就給那么多提示吧。
我不想直接告訴你它們究竟還有什么不同,我現在給出三個API:GetFocus、GetActiveWindow和GetForegroundWindow,大家用這三個API去做些實驗就知道了。
總結
以上是生活随笔為你收集整理的Windows窗口分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 矩阵 计算机应用,《计算机视觉算法:基于
- 下一篇: ant java 返回_使用Ant自动化