windows窗口分析,父窗口,子窗口,所有者窗口

https://blog.csdn.net/u010983763/article/details/53636831

在Windows應用程序中,窗口是通過句柄HWND來標識的,我們要對某個窗口進行操作,首先就要獲取到這個窗口的句柄,這就是窗口和句柄的聯繫。

(本文嘗試通過一些簡單的實驗,來分析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定義如下:

  1.  
  2.  
  3.  
  4.  
  5. #define WS_OVERLAPPED       0x00000000L

  6.  
  7. #define WS_POPUP            0x80000000L

  8.  
  9. #define WS_CHILD            0x40000000L

那麼我們很容易得到這個結論: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”。此時它的創建函數爲:

  1.  
  2.  
  3.  
  4.  
  5. hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

只制定了一個WS_OVERLAPPEDWINDOW,但我們很快就找到了WS_OVERLAPPEDWINDOW的定義:

  1.  
  2.  
  3.  
  4.  
  5. #define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED     | /

  6.  
  7.                              WS_CAPTION        | /

  8.  
  9.                              WS_SYSMENU        | /

  10.  
  11.                              WS_THICKFRAME     | /

  12.  
  13.                              WS_MINIMIZEBOX    | /

  14.  
  15.                              WS_MAXIMIZEBOX)

原來overlapped窗口就是有標題,系統菜單,最小最大化按鈕和可調整大小邊框的窗口,這個定義是正確的,但只是個我們認知上的概念的問題,因爲popup和child窗口也同樣可以擁有這些(後面證明)。由於WS_OVERLAPPED爲0,那我們是不是可以把WS_OVERLAPPEDWINDOW定義中的WS_OVERLAPPED拿掉呢?那是肯定的,那也就是說WS_OVERLAPPED什麼都不是!我們只作popup和child的區分,是不是這樣?也不是,我們繼續實驗。

很簡單,接下去我們只給這個嚮導生成的代碼加一點點東西,就是把CreateWindow改成:

  1.  
  2.  
  3.  
  4.  
  5. hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

對,給窗口風格增一個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:無法創建最上層子窗口。”看來桌面是不讓我們隨便搞的。繼續,還是老地方,這回改成:

  1.  
  2.  
  3.  
  4.  
  5. hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP|WS_CHILD, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

嗯?有沒搞錯,又是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.

  1.  
  2.  
  3.  
  4.  
  5. #define WS_OVERLAPPED       0x00000000L

  6.  
  7. #define WS_POPUP            0x80000000L

  8.  
  9. #define WS_CHILD            0x40000000L

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

  1.  
  2.  
  3.  
  4.  
  5. dwStyle = GetWindowLong(hWnd, GWL_STYLE);

  6.  
  7. if (dwStyle&WS_POPUP)

  8.  
  9.  // it's a popup window

  10.  
  11. else if (dwStyle&WS_CHILD)

  12.  
  13.  // it's a child window

  14.  
  15. else

  16.  
  17.  // it's an overlapped window

這斷描述跟我的想法一致。要知道,就算你只給窗口一個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:

  1.  
  2.  
  3.  
  4.  
  5. HWND CreateWindowEx(

  6.  
  7.   DWORD dwExStyle,      // extended window style

  8.  
  9.   LPCTSTR lpClassName,  // registered class name

  10.  
  11.   LPCTSTR lpWindowName, // window name

  12.  
  13.   DWORD dwStyle,        // window style

  14.  
  15.   int x,                // horizontal position of window

  16.  
  17.   int y,                // vertical position of window

  18.  
  19.   int nWidth,           // window width

  20.  
  21.   int nHeight,          // window height

  22.  
  23.   HWND hWndParent,      // handle to parent or owner window

  24.  
  25.   HMENU hMenu,          // menu handle or child identifier

  26.  
  27.   HINSTANCE hInstance,  // handle to application instance

  28.  
  29.   LPVOID lpParam        // window-creation data

  30.  
  31. );

猜對了,我就是從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應用程序了。然後增加一個窗口處理函數,也就是我們準備創建的子窗口的處理函數了。

  1.  
  2.  
  3.  
  4.  
  5. LRESULT CALLBACK WndProcDoNothing(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

  6.  
  7. {

  8.  
  9.  return DefWindowProc(hWnd, message, wParam, lParam);

  10.  
  11. }

DoNothing?好名字。註冊之:

  1.  
  2.  
  3.  
  4.  
  5.  WNDCLASSEX wcex;

  6.  
  7.  wcex.cbSize = sizeof(WNDCLASSEX); 

  8.  
  9.  wcex.style         = CS_HREDRAW | CS_VREDRAW;

  10.  
  11.  wcex.lpfnWndProc   = (WNDPROC)WndProcDoNothing;

  12.  
  13.  wcex.cbClsExtra    = 0;

  14.  
  15.  wcex.cbWndExtra    = 0;

  16.  
  17.  wcex.hInstance     = hInstance;

  18.  
  19.  wcex.hIcon         = LoadIcon(hInstance, (LPCTSTR)IDI_ALLWINDOWTEST);

  20.  
  21.  wcex.hCursor       = LoadCursor(NULL, IDC_ARROW);

  22.  
  23.  wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);

  24.  
  25.  wcex.lpszMenuName  = NULL; //子窗口不能擁有菜單,指定了也沒有用

  26.  
  27.  wcex.lpszClassName = TEXT("child_window");

  28.  
  29.  wcex.hIconSm       = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);

  30.  
  31.  RegisterClassEx(&wcex);

最後當然是把它給創建出來了:

  1.  
  2.  
  3.  
  4.  
  5.  g_hwndChild = CreateWindowEx(NULL, TEXT("child_window"), TEXT(""), WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);

關於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看看是否有出錯。

  1.  
  2.  
  3.  
  4.  
  5. {

  6.  
  7.  DWORD rtn;

  8.  
  9.  HWND hw = GetParent(hWnd); //獲取主窗口的“Parent”

  10.  
  11.  if(hw==NULL)

  12.  
  13.   rtn = GetLastError();

  14.  
  15.  hw = GetParent(g_hwndA); //獲取A的“Parent”

  16.  
  17.  if(hw==NULL)

  18.  
  19.   rtn = GetLastError();

  20.  
  21.  hw = GetParent(g_hwndB); //獲取B的“Parent”

  22.  
  23.  if(hw==NULL)

  24.  
  25.   rtn = GetLastError();

  26.  
  27.  hw = GetParent(g_hwndC); //獲取C的“Parent”

  28.  
  29.  if(hw==NULL)

  30.  
  31.   rtn = GetLastError();

  32.  
  33. }

我的實驗結果有些令我不解,清一色返回0,包括GetLastError,也就是說沒有出錯,那GetParent返回0,根據MSDN上的描述,原因只可能是:這些窗口確實沒有Owner。不對啊?難道前面的規則和推論都是錯誤的不成?我創建它們的時候,就明明白白地指定了hWndParent參數,而且上面的實驗也表明了他們之間的Owner和Owned關係,那是不是GetParent錯了?我想是的,你先別對着我扔磚頭,想看到正確的情況麼?好,我弄給你看。

我們是如何創建A,B和C這幾個彈出窗口的?我再把創建它們的語句貼一下吧:

  1.  
  2.  
  3.  
  4.  
  5. g_hwndX = CreateWindowEx(NULL, TEXT("child_window"), TEXT("X"), WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);

現在把這個語句改爲:

  1.  
  2.  
  3.  
  4.  
  5. g_hwndX = CreateWindowEx(NULL, TEXT("child_window"), TEXT("X"), WS_POPUP|WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);

對,就是加上一個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窗口真正的主人呢?方法當然是有的,看:

  1.  
  2.  
  3.  
  4.  
  5. {

  6.  
  7.  DWORD rtn;

  8.  
  9.  HWND hw = GetWindow(hWnd, GW_OWNER); //獲取主窗口的Owner

  10.  
  11.  if(hw==NULL)

  12.  
  13.   rtn = GetLastError();

  14.  
  15.  hw = GetWindow(g_hwndA, GW_OWNER);   //獲取A的Owner

  16.  
  17.  if(hw==NULL)

  18.  
  19.   rtn = GetLastError();

  20.  
  21.  hw = GetWindow(g_hwndB, GW_OWNER);   //獲取B的Owner

  22.  
  23.  if(hw==NULL)

  24.  
  25.   rtn = GetLastError();

  26.  
  27.  hw = GetWindow(g_hwndC, GW_OWNER);   //獲取C的Owner

  28.  
  29.  if(hw==NULL)

  30.  
  31.   rtn = GetLastError();

  32.  
  33. }

這麼一來,無論是否帶有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,但我們可以寫一個簡單的函數:

  1.  
  2.  
  3.  
  4.  
  5. HWND GetTrueParent(HWND hwnd)

  6.  
  7. {

  8.  
  9.  DWORD dwStyle = GetWindowLong(hwnd, GWL_STYLE);

  10.  
  11.  if((dwStyle & WS_CHILD) == WS_CHILD)

  12.  
  13.   return GetParent(hwnd);

  14.  
  15.  else

  16.  
  17.   return GetDesktopWindow();

  18.  
  19. }

你終於憋不住了,對我大吼:“你有什麼依據說非Child窗口的Parent一定是Desktop?”我當然是有依據的,首先是這些非child window的繪製,不能超出桌面,超出桌面就什麼都看不見了,只能是桌面管理着它們的繪製,如果它們確實存在Parent的話,當然,聰明你認爲這個理由並不充分,OK,我們編程來證明,先介紹一個API:

  1.  
  2.  
  3.  
  4.  
  5. HWND FindWindowEx(

  6.  
  7.   HWND hwndParent,      // handle to parent window

  8.  
  9.   HWND hwndChildAfter,  // handle to child window

  10.  
  11.   LPCTSTR lpszClass,    // class name

  12.  
  13.   LPCTSTR lpszWindow    // window name

  14.  
  15. );

又被你猜對了,我是從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的父窗口。好,有這個函數,我們就可以知道剛纔創建的那幾個彈出窗口的老爸究竟是不是桌面。代碼十分簡單:

  1.  
  2.  
  3.  
  4.  
  5. {

  6.  
  7.  DWORD rtn;

  8.  
  9.  HWND hw = FindWindowEx(NULL, NULL, TEXT("ALLWINDOWTEST"), TEXT("AllWindowTest")); //從桌面開始查找主窗口

  10.  
  11.  if(hw==NULL)

  12.  
  13.   rtn = GetLastError();

  14.  
  15.  hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("A")); //從桌面開始查找A

  16.  
  17.  if(hw==NULL)

  18.  
  19.   rtn = GetLastError();

  20.  
  21.  hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("B")); //從桌面開始查找B

  22.  
  23.  if(hw==NULL)

  24.  
  25.   rtn = GetLastError();

  26.  
  27.  hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("C")); //從桌面開始查找C

  28.  
  29.  if(hw==NULL)

  30.  
  31.   rtn = GetLastError();

  32.  
  33. }

結果如何?(是不是偷懶乾脆不做,等着我說結果啊?)我的結果是全部找到了,和用spy++查找的結果一樣,所以我有充分的理由認爲,所有非child窗口其實是desktop的child,spy++的樹形結構組織確實也是這麼闡述的。你很厲害,你還是能夠駁斥我:“根據規則三,Parent被銷燬的時候,其Child將被銷燬,你證明給我看?”這個……有點難:

  1.  
  2.  
  3.  
  4.  
  5. HWND hwndDesktop = GetDesktopWindow();

  6.  
  7. BOOL rtn = DestroyWindow(hwndDesktop);

  8.  
  9. if(!rtn)

  10.  
  11.  DWORD dwErr = GetLastError();

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也強制幫我加上。”那……你可以強制剝離掉這個風格啊,這樣:

  1.  
  2.  
  3.  
  4.  
  5. DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE);

  6.  
  7. dwStyle &= ~(WS_CLIPSIBLINGS);

  8.  
  9. SetWindowLong(hWnd, GWL_STYLE);

執行後用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改成這樣:

  1.  
  2.  
  3.  
  4.  
  5. g_hwndA = CreateWindowEx(NULL, TEXT("child_window"), TEXT("A"), 

  6.  
  7.  WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 30, 30, 400, 300, hWnd, NULL, hInst, NULL);

  8.  
  9. g_hwndB = CreateWindowEx(NULL, TEXT("child_window"), TEXT("B"),

  10.  
  11.  WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 60, 60, 400, 300, hWnd, NULL, hInst, NULL);

  12.  
  13. g_hwndC = CreateWindowEx(NULL, TEXT("child_window"), TEXT("C"), 

  14.  
  15.  WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 90, 90, 400, 300, hWnd, NULL, hInst, NULL);

創建出來的效果如圖:

圖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的那個窗口過程改爲:

  1.  
  2.  
  3.  
  4.  
  5. LRESULT CALLBACK WndProcDoNothing(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

  6.  
  7. {

  8.  
  9.  switch(message) 

  10.  
  11.  {

  12.  
  13.  case WM_PAINT:

  14.  
  15.   {

  16.  
  17.    TCHAR szOut[20];

  18.  
  19.    TCHAR szWindowTxt[10];

  20.  
  21.    GetWindowText(hWnd, szWindowTxt, 10);

  22.  
  23.    wsprintf(szOut, TEXT("%s Paint/n"), szWindowTxt);

  24.  
  25.    OutputDebugString(szOut);

  26.  
  27.   }

  28.  
  29.   break;

  30.  
  31.  }

  32.  
  33.  return DefWindowProc(hWnd, message, wParam, lParam);

  34.  
  35. }

打印結果爲:
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風格,我們看看是不是能把東西畫到子窗口的區域去。

  1.  
  2.  
  3.  
  4.  
  5. case WM_PAINT:

  6.  
  7.  hdc = BeginPaint(hWnd, &ps);

  8.  
  9.  RECT rt;

  10.  
  11.  GetClientRect(hWnd, &rt);

  12.  
  13.  DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);

  14.  
  15.  MoveToEx(hdc, 0, 0, NULL);

  16.  
  17.  LineTo(hdc, 600, 400);  //To be simple, just a line.

  18.  
  19.  EndPaint(hWnd, &ps);

  20.  
  21.  break;

運行結果如圖:

 

圖16

嗯?沒有穿過啊?爲什麼?先動腦想想半分鐘。
那是因爲我們的實驗不夠嚴謹,現在在主窗口WM_PAINT消息的處理中加入一個Debug內容:

  1.  
  2.  
  3.  
  4.  
  5. OutputDebugString(TEXT("Main window paint/n"));

再看看debug出來的log:
Main window paint
A Paint
B Paint
C Paint
因爲是主窗口先繪製,然後纔是子窗口,所以即便這根線是穿過子窗口區域的,恐怕也看不出來了。那我們就不要在WM_PAINT裏繪製,我們增加一個菜單項,叫paint a line,點這個菜單就執行下面的代碼:

  1.  
  2.  
  3.  
  4.  
  5. //在主窗口的WM_COMMAND消息處理中

  6.  
  7. switch (wmId)

  8.  
  9. {

  10.  
  11.  //...

  12.  
  13.  case ID_PAINT_A_LINE:

  14.  
  15.  {

  16.  
  17.   HDC hdc = GetDC(hWnd);

  18.  
  19.   MoveToEx(hdc, 0, 0, NULL);

  20.  
  21.   LineTo(hdc, 600, 400);  //To be simple, just a line.

  22.  
  23.   ReleaseDC(hWnd, hdc);

  24.  
  25.  }

  26.  
  27. }

運行程序,點菜單“paint a line”,看運行效果:

 

圖17

算是“成功穿越”了,這時候你再給父窗口加上WS_CLIPCHILDREN看看,結果我就不說了,就算不嘗試其實也能想得到。相信大家到此爲止都理解了這兩個風格的作用了。

再順便說些實踐經驗,有時候我們會發覺程序在頻繁重繪的時候閃爍比較厲害,還是拿這個例子改裝一下吧,先把主窗口的WS_CLIPCHILDREN風格拿掉,然後在其窗口處理函數中加入些代碼:

  1.  
  2.  
  3.  
  4.  
  5. case WM_CREATE:

  6.  
  7.  //...

  8.  
  9.  SetTimer(hWnd, 1, 200, NULL);

  10.  
  11.  break;

  12.  
  13. case WM_TIMER:

  14.  
  15.  if (wParam==1)

  16.  
  17.   InvalidateRect(hWnd, NULL, TRUE);

  18.  
  19.  break;

意思是說每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消息的處理:

  1.  
  2.  
  3.  
  4.  
  5. case WM_MOUSEACTIVATE:

  6.  
  7. {

  8.  
  9.  HWND hwndFind=NULL;

  10.  
  11.  while(TRUE)

  12.  
  13.  {

  14.  
  15.   hwndFind = FindWindowEx(g_hwndMain, hwndFind, TEXT("child_window"), NULL);

  16.  
  17.   if (hwndFind==NULL)

  18.  
  19.    break;

  20.  
  21.   if (hwndFind==hWnd)

  22.  
  23.    PostMessage(hwndFind, WM_NCACTIVATE, TRUE, NULL);

  24.  
  25.   else

  26.  
  27.    PostMessage(hwndFind, WM_NCACTIVATE, FALSE, NULL);

  28.  
  29.  }

  30.  
  31. }

  32.  
  33. break;

現在再嘗試運行程序,點擊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的鏈表?可以這樣:

  1.  
  2.  
  3.  
  4.  
  5. void ListZOrder(HWND hParent)

  6.  
  7. {

  8.  
  9.  TCHAR szOutput[10];

  10.  
  11.  HWND hwnd = GetTopWindow(hParent);

  12.  
  13.  while(hwnd!=NULL)

  14.  
  15.  {

  16.  
  17.   wsprintf(szOutput, TEXT("%08X/n"), (UINT)hwnd);

  18.  
  19.   OutputDebugString(szOutput);

  20.  
  21.   hwnd = GetNextWindow(hwnd, GW_HWNDNEXT);

  22.  
  23.  }

  24.  
  25. }

這個函數會把某個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去做些實驗就知道了。

後記

這篇文章我想已經足夠長,我必須得結束了,這恐怕也是我寫的最長的一篇技術文章(除了我的本科畢業論文),如果你能夠從頭到尾讀到這裏,我倍感榮幸,如果它能夠給你些幫助,我想這份辛苦也是值得的。進入冬天了,天氣很冷,注意保暖。(其實現在我覺得我的腳正踩在南極大陸上……)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章