1. 程式人生 > >windows視窗分析,父視窗,子視窗,所有者視窗

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的