00 Win 窗口分析

Post date: 2012/3/1 上午 01:18:44

(本文嘗試通過一些簡單的實驗,來分析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. #定義WS_OVERLAPPED 0x00000000L
  2. #定義WS_POPUP 0x80000000L
  3. #定義WS_CHILD 0x40000000L

那麼我們很容易得到這個結論:style的最高位是1的,是一個popup窗口,style的次高位是1的,代表是一個child窗口,如果最高位次高位都是0,那這個窗口就是一個overlapped窗口,如果兩位都是1,厄……MSDN告訴我們不能這麼幹,事實呢?我後面再講。其實這個結論是有點過時的,甚至很能誤導人,不是我們的原因,很可能是Windows的歷史原因,為什麼?具體也是後面講。嘿嘿。

OK,我們現在開始來嘗試,看看這些風格究竟影響窗口幾何,對了,準備spy++,這是必備工具。

用VC + + + +的嚮導建立一個的Hello World的Windows的程序,注意是Windows的程序,不是MFC中的你好

圖3

然后用spy++查看这个窗口的风格,发现其风格显示为“WS_OVERLAPPEDWINDOW|WS_VISIBLE|WS_CLIPSIBLING|WS_OVERLAPPED”。此时它的创建函数为:

  1. HWND = CreateWindow的(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,0,CW_USEDEFAULT,0,空,空,HINSTANCE,為NULL);

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

  1. #定義WS_OVERLAPPEDWINDOW(WS_OVERLAPPED | /
  2. WS_CAPTION | /
  3. WS_SYSMENU | /
  4. WS_THICKFRAME | /
  5. WS_MINIMIZEBOX同時| /
  6. WS_MAXIMIZEBOX)

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

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

  1. 的hWnd = CreateWindow的(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW | WS_POPUP,CW_USEDEFAULT,0,CW_USEDEFAULT,0,空,空,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. HWND = CreateWindow的(szWindowClass,szTitle,WS_OVERLAPPEDWINDOW | WS_POPUP | WS_CHILD,CW_USEDEFAULT,0,CW_USEDEFAULT,0,空,空,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,未果,有人提起這個問題,但沒有令我滿意的答复,下面這段文字是我找到的可能有點線索的答复:

事實上,微軟Spy + +的是錯誤的。

控制其類型有兩種窗口風格位。如果風格的DWORD的高序位被設置,窗口是一個彈出窗口。如果下位設置,窗口是一個子窗口。如果沒有設置,窗口重疊。(如果兩者都設 ​​置了,結果是無證的。)

從這些定義看WINUSER.H。

  1. #定義WS_OVERLAPPED 0x00000000L
  2. #定義WS_POPUP 0x80000000L
  3. #定義WS_CHILD 0x40000000L

你的窗口樣式(0x94c00880)有高序位集和下位明確,因此它是一個彈出窗口,而不是重疊的窗口。

正確的方法來確定所有三種類型的窗口(這是什麼Spy + +的應該做的)

  1. dwStyle = GetWindowLong(HWND,GWL_STYLE);
  2. (dwStyle&WS_POPUP)
  3. / /這是一個彈出窗口
  4. 否則, 如果 (dwStyle&WS_CHILD)
  5. / /這是一個子窗口
  6. 其他
  7. / /這是一個重疊窗口

這斷描述跟我的想法一致。要知道,就算你只給窗口一個WS_POPUP的風格,WS_OVERLAPPED也會顯示在spy++上的,我認為這十分有問題,究竟spy++如何判,估計得請教比爾蓋茨了。還有一段有趣的描述,估計也有所幫助:

只要...

WS_POPUP | WS_OVERLAPPED

...是absolutelly ...相當於

WS_POPUP

... 你為什麼關心Spy + +的名單WS_OVERLAPPED或不呢?

請與我們玩“托馬斯異教徒”停止

使用“上行走的水”的設備在這裡再次變得過於昂貴,並再次。;)

雖然這麼說,我還是認為,spy++給了我們不少誤導,那麼對WS_OVERLAPPED的討論就暫時告一段落吧,作為一個技術人,很難容忍自己無法理解的邏輯,我就是這麼種人……不過如果再扯下去的話這篇文章就不能結束了,所以姑且認為,這是spy++的錯,而我們還是認為窗口分3種——popup,child和Overlapped。(Undocumented不在此列,也不在本文講述之列)

二,家長與業主

這是內容最多的一節,做好心理準備。

微軟和我們開了個玩笑,告訴我們,窗口和人一樣,可以有父母,有主人……我們先來看一個最著名的Windows API:

  1. HWND的 CreateWindowEx的(
  2. DWORD dwExStyle, / /擴展窗口樣式
  3. LPCTSTR lpClassName, / /註冊類名
  4. LPCTSTR lpWindowName, / /窗口的名稱
  5. DWORD dwStyle, / /窗口樣式
  6. INT X, / /水平的窗口位置
  7. Y, / /窗口的垂直位置
  8. INT nWidth, / /窗口的寬度
  9. nHeight, / /窗口的高度
  10. 的HWND hWndParent, / /處理父母或所有者窗口
  11. HMENU HMENU, / /菜單句柄或子標識符
  12. 的HINSTANCE hInstance的, / /處理的應用實例
  13. LPVOID lpParam / ​​/窗口創建數據
  14. );

猜對了,我就是從MSDN上copy下來的,看第九個參數的名字叫hWndParent,顧名思義哦,這就是Parent窗口了,不過我們中國人不喜歡稱之“父母窗口”,我們喜歡叫它“父窗口”,簡單一點。其實這個名字對我們造成了不少的誤導,我只能說,可能也是由於歷史原因,比如在Windows 1.0(1985年出的,當時沒什麼影響力)的時候,只有Parent這個概念,沒有Owner的概念。

回頭看看文章開始我提起的,我企圖將Tooltip的父窗口設置為一個圖形窗口,不能成功,Tooltip的父窗口會自動變成應用程序主窗口,這是為什麼?好,現在開始講概念了,都是我花了很多時間在互聯網上搜索,篩選,確認,得出來的結論:

規則一所有者窗口控制了國有窗口的生存,當所有者窗口被銷毀的時候,其所屬的國有窗口就會被銷毀。

規則二:父窗口控制了子窗口的繪製,子窗口不可能顯示在其母公司窗口的客戶區之外。

規則三:父窗口的同時控制了子窗口的生存,當父窗口被銷毀的時候,其所屬的子窗口就會被銷毀。

規則四 ​​:所有者窗口不能是子窗口

規則五子窗口一定 ​​有家長(否則怎麼叫孩子嗎?),一定沒有所有者。

規則六:非子窗口的家長一定什麼桌面,它們不一定有業主。

這是比較重要的幾點,如果你認為這跟你以前學到的,或者認知的有所不同,先別急著抗議,先看看我是怎麼理解的。除了這幾條規則,下面我還會逐步給出一些規則。

先說比較好理解的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. LRESULT CALLBACK WndProcDoNothing(HWND HWND, UINT的 消息, wParam參數 WPARAM, LPARAM中 的lParam)
  2. {
  3. 返回 DefWindowProc函數(HWND,消息,WPARAM,LPARAM);
  4. }

DoNothing好名字註冊之。

  1. 的WNDCLASSEX wcex;
  2. wcex.cbSize, 我zeof (WNDCLASSEX);
  3. wcex.style = CS_HREDRAW | CS_VREDRAW;
  4. wcex.lpfnWndProc =(WNDPROC)WndProcDoNothing“;
  5. wcex.cbClsExtra = 0;
  6. wcex.cbWndExtra = 0;
  7. wcex.hInstance = hInstance的;
  8. wcex.hIcon = LoadIcon(HINSTANCE,(LPCTSTR )IDI_ALLWINDOWTEST);
  9. wcex.hCursor = LoadCursor(NULL IDC_ARROW);
  10. wcex.hbrBackground =(HBRUSH )(COLOR_WINDOW +1);
  11. wcex.lpszMenuName = NULL; //子窗口不能擁有菜單,指定了也沒有用
  12. wcex.lpszClassName = TEXT(“child_window” );
  13. wcex.hIconSm = LoadIcon(wcex.hInstance(LPCTSTR )IDI_SMALL“);
  14. RegisterClassEx(wcex);

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

  1. g_hwndChild = CreateWindowEx的(為NULL,TEXT(“child_window” ),TEXT(“” ),WS_CHILD | WS_VISIBLE,WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS,30,30,400,300,HWND,空,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,再仔細點:

如果窗口是一個子窗口,返回值是父窗口的句柄。如果窗口是頂層窗口,返回值是一個所有者窗口的句柄。

什麼是top-level window?就是非Child window,這個後面再詳細談這個,現在註意看了,GetParent返回的有可能不是parent,對於非child窗口來說,返回的就不是parent,為什麼?因為非child窗口的parent恆定是Desktop啊(規則6),這還需要獲取嗎?我們接下去的實驗是用來測試GetParent這個函數是否工作正常的,什麼?測試M$提供的API,沒錯,呵呵,當一把微軟的測試員吧。接上面那個實驗:

//在窗口創建完成後,調用下面的代碼,在第一個GetParent處設置個斷點,查看返回值,如果返回NULL,按照MSDN所說的,用GetLastError看看是否有出錯。

  1. {
  2. DWORD RTN;
  3. HWND HW = GetParent(HWND); / /獲取主窗口的“家長”
  4. 如果(HW == NULL)
  5. RTN = GetLastError函數();
  6. HW = GetParent(g_hwndA)的; / /獲取一個的“父
  7. 如果(HW == NULL)
  8. RTN = GetLastError函數();
  9. HW = GetParent(g_hwndB)的; / /獲取乙的“家長”
  10. 如果(HW == NULL)
  11. RTN = GetLastError函數();
  12. HW = GetParent(g_hwndC)的; / /獲取ç的“父
  13. 如果(HW == NULL)
  14. RTN = GetLastError函數();
  15. }

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

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

  1. g_hwndX = CreateWindowEx的(為NULL,TEXT(“child_window” ),TEXT(“X”號),WS_VISIBLE,WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS,30,30,400,300,HWND,空,HINSTANCE,NULL);

現在把這個語句改為:

  1. g_hwndX = CreateWindowEx的(為NULL,TEXT(“child_window” ),文字(“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. DWORD RTN;
  3. HWND HW = GetWindow(HWND,GW_OWNER); / /獲取主窗口的所有者
  4. 如果(HW == NULL)
  5. RTN = GetLastError函數();
  6. HW = GetWindow(g_hwndA,GW_OWNER); / /獲取一個的業主
  7. 如果(HW == NULL)
  8. RTN = GetLastError函數();
  9. HW = GetWindow(g_hwndB,GW_OWNER); / /獲取乙的業主
  10. 如果(HW == NULL)
  11. RTN = GetLastError函數();
  12. HW = GetWindow(g_hwndC,GW_OWNER); / /獲取ç的所有者
  13. 如果(HW == NULL)
  14. RTN = GetLastError函數();
  15. }

這麼一來,無論是否帶有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. HWND的 GetTrueParent(HWND HWND)
  2. {
  3. DWORD dwStyle = GetWindowLong(HWND,GWL_STYLE);
  4. 如果((dwStyle&WS_CHILD)== WS_CHILD)
  5. 返回 GetParent(HWND);
  6. 其他
  7. 返回 GetDesktopWindow();
  8. }

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

  1. 的HWND FindWindowEx的(
  2. 的HWND hwndParent, / /父窗口的句柄
  3. HWND的 hwndChildAfter, / /子窗口的句柄
  4. LPCTSTR類型 lpszClass
  5. LPCTSTR類型 lpszWindow / /窗口的名稱
  6. );

又被你猜對了,我是從MSDN上copy下來的(^_^),看MSDN對這個函數的說明:

hwndParent

[]要搜查。

如果hwndParent是NULL,函數使用桌面窗口作為父窗口的子窗口的父窗口句柄。功能是桌面的子窗口的窗口之間的搜索。

hwndChildAfter

[中]一個子窗口處理。開始搜索下一個子窗口,在Z順序。子窗口必須是子窗口的的hwndParent直接的,不只是一個子孫窗口。

如果hwndChildAfter是NULL,hwndParent的第一個子窗口開始搜索。

lpszClass

窗口類名(我來翻譯,簡單點)

lpszWindow

窗口標題

關鍵是看第一個參數,如果hwndParent為NULL,函數就查找desktop的“子窗口”,但這個“子窗口”是加引號的,因為這裡的“子窗口”和本文前面一直提到的子窗口確實不太一樣,那就是這裡的“子窗口”沒有WS_CHILD風格,算是一個特殊吧,也難怪GetParent不願意告訴我們desktop就是這些非Child的父窗口。好,有這個函數,我們就可以知道剛才創建的那幾個彈出窗口的老爸究竟是不是桌面。代碼十分簡單:

  1. {
  2. DWORD RTN;
  3. HWND HW = FindWindowEx的(NULL,NULL,TEXT(“ALLWINDOWTEST” ),TEXT(“AllWindowTest” )) / /從桌面開始查找主窗口 ;
  4. 如果(HW == NULL)
  5. RTN = GetLastError函數();
  6. HW = FindWindowEx的(NULL,NULL,文字(“child_window ),TEXT(“A”的)) / /從桌面開始查找一個
  7. 如果(HW == NULL)
  8. RTN = GetLastError函數();
  9. HW = FindWindowEx的(NULL,NULL,TEXT(“child_window” ),TEXT(“” )) / /從桌面開始查找乙;
  10. 如果(HW == NULL)
  11. RTN = GetLastError函數();
  12. HW = FindWindowEx的(NULL,NULL,TEXT(“child_window” ),TEXT(“” )) / /從桌面開始查找ç;
  13. 如果(HW == NULL)
  14. RTN = GetLastError函數();
  15. }

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

  1. HWND的 hwndDesktop GetDesktopWindow =();
  2. 布爾 RTN = DestroyWindow而(hwndDesktop)的;
  3. 如果(!RTN)
  4. DWORD dwErr = GetLastError函數();

My god,Desktop沒了,你說我們還能看到什麼呢?當然微軟不會沒想到這點,DestroyWindow當然不能成功​​,錯誤代碼為5,“拒絕訪問”。好,我有些累了,不能再糾纏了,轉入下一節!留個作業如何?嘗試使用SetParent這個API,改變窗口的Parent,觀察運行情況,並思考這樣做有什麼不好之處。

三,如何體現WS_CLIPSIBLING和WS_CLIPCHILD?

看了這個標題,應該怎麼做?我想你十有八九是打開MSDN,輸入這兩個關鍵字去搜索吧?OK,不用了,我把MSDN對這兩個窗口風格的說明貼出來:

WS_CLIPCHILDREN排除當你繪製在父窗口子窗口佔用的面積。創建父窗口時使用。

WS_CLIPSIBLINGS剪輯子窗口彼此相對的是,當一個特定的子窗口接收油漆消息,WS_CLIPSIBLINGS樣式剪輯出子窗口地區所有其他子窗口重疊更新。(如果WS_CLIPSIBLINGS沒有給出和子窗口重疊,當你在一個子窗口的客戶區畫,它有可能在鄰近的子窗口的客戶區繪製。)使用

僅WS_CHILD樣式。

找到是不難,但如果光看這個就明白的話我也不必要寫這種文章了,沒有適當的代碼去實踐,估計很多人是不懂這兩個風格甚麼含義的。OK,現在我來帶你實踐。spy++開著不?哈,別關啊,後面還要用到。用spy++觀察各個top-level window(非Child窗口)的屬性,是不是都有個WS_CLIPSIBLINGS?想找個沒有的都不行,如果你不服氣,你要自己創建一個沒有WS_CLIPSIBLINGS風格的頂層窗口,好吧,我在這裡等你一會兒(……一會兒過去了……),你垂頭喪氣地回來了: “不行,即便我不指定這個風格,Windows也強制幫我加上。”那……你可以強制剝離掉這個風格啊,這樣:

  1. DWORD dwStyle = GetWindowLong(HWND,GWL_STYLE);
  2. dwStyle&=〜(WS_CLIPSIBLINGS);
  3. 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. g_hwndA = CreateWindowEx的(NULL,TEXT(“child_window” ),TEXT(“一” ),
  2. WS_CHILD | WS_VISIBLE,| WS_OVERLAPPEDWINDOW,30,30,400,300,HWND,NULL,hInst,NULL);
  3. g_hwndB = CreateWindowEx的(NULL,TEXT(“child_window” ),TEXT(“B”類),
  4. WS_CHILD | WS_VISIBLE,| WS_OVERLAPPEDWINDOW,60,60,400,300,HWND,NULL,hInst,NULL);
  5. g_hwndC = CreateWindowEx的(NULL,TEXT(“child_window” ),TEXT(“C”類),
  6. 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. LRESULT CALLBACK WndProcDoNothing(HWND HWND, UINT的 消息, wParam參數 WPARAM, LPARAM中 的lParam)
  2. {
  3. 開關(消息)
  4. {
  5. 案件 的WM_PAINT:
  6. {
  7. TCHAR szOut [20];
  8. TCHAR szWindowTxt [10];
  9. GetWindowText函數(HWND,szWindowTxt 10);
  10. wsprintf(szOut,TEXT( “%s的油漆/ N” ),szWindowTxt);
  11. OutputDebugString的(szOut);
  12. }
  13. 打破;
  14. }
  15. 返回 DefWindowProc函數(HWND,消息,WPARAM,LPARAM);
  16. }

打印結果為

:A 油漆

塗料B,

Ç塗料

那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. 案件 的WM_PAINT:
  2. HDC =調用BeginPaint(HWND,&PS);
  3. RECT的RT;
  4. GetClientRect(HWND,&RT);
  5. DrawText的(HDC,szHello,函數strlen(szHello),&RT,DT_CENTER);
  6. MoveToEx(HDC,0,0,NULL);
  7. 的LineTo(HDC,600,400); / /很簡單,只是一條線。
  8. 調用EndPaint(HWND,&PS);
  9. 打破;

運行結果如圖:

圖16

嗯?沒有穿過啊?為什麼?先動腦想想半分鐘。

那是因為我們的實驗不夠嚴謹,現在在主窗口WM_PAINT消息的處理中加入一個Debug內容:

  1. OutputDebugString的(TEXT( “ 主窗口漆/ N” ));

再看看出來的調試日誌:

主窗口油漆

的塗料

乙油漆

ç

一條線,點這個菜單就執行下面的程式碼:

  1. //在主窗口的WM_COMMAND消息處理中
  2. 開關 (wmId)
  3. {
  4. / / ...
  5. 案件 ID_PAINT_A_LINE:
  6. {
  7. 的HDC 的HDC = GetDC的(HWND);
  8. MoveToEx(HDC,0,0,NULL);
  9. 的LineTo(HDC,600,400); / /很簡單,只是一條線。
  10. ReleaseDC(HWND,HDC);
  11. }
  12. }

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

圖17

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

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

  1. 案件 的WM_CREATE:
  2. / / ...
  3. SetTimer的(HWND,1,200,空);
  4. 打破;
  5. 案件 的WM_TIMER:
  6. (wParam參數== 1)
  7. InvalidateRect函數(HWND為NULL,TRUE);
  8. 打破;

意思是說每0.2秒重繪一次主窗口,大家看看,是不是閃爍得厲害,閃爍過程中,我們依稀看到了這根線穿過了子窗口的區域……然後把WS_CLIPCHILDREN風格賦予主窗口,其餘不變,再看看,是不是閃爍現像大為減少?通過這個例子告訴大家甚麼叫“把現有的技術用得最好”(參考我上一篇博文),有時候就差那麼一點點。

四,前景,主動,焦點及對Z順序的理解

看前面的這個“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. 案件 WM_MOUSEACTIVATE:
  2. {
  3. HWND hwndFind = NULL;
  4. 而(真)
  5. {
  6. hwndFind = FindWindowEx的(g_hwndMain,hwndFind,TEXT(“child_window” ),為NULL);
  7. 如果 (空== hwndFind)
  8. 打破;
  9. 如果 (HWND == hwndFind)
  10. PostMessage的(hwndFind,WM_NCACTIVATE TRUE時,為NULL);
  11. 其他
  12. PostMessage的(hwndFind,WM_NCACTIVATE,假,空);
  13. }
  14. }
  15. 打破;

現在再嘗試運行程序,點擊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. ,無效 ListZOrder(HWND的 hParent為)
  2. {
  3. TCHAR szOutput [10];
  4. HWND的 HWND = GetTopWindow(hParent為);
  5. 而(hwnd! = NULL)
  6. {
  7. wsprintf(szOutput,TEXT( “%08X / N” ),(UINT )HWND);
  8. OutputDebugString的(szOutput);
  9. HWND = GetNextWindow(HWND GW_HWNDNEXT)的;
  10. }
  11. }

這個函數會把某個Parent的子窗口句柄值,按照z order次序,從最頂打印到最底。如果hParent為NULL,那麼就從桌面的最頂窗口開始,列出所有桌面的窗口,這樣意義不大,為什麼?因為你會找出來很多很多窗口,可見的,不可見的,奇奇怪怪的,變來變去的,所以這種列窗口的方法通常是用於列子窗口的。

最後我想提提Fore​​ground、Active和Focus這三者,非常容易讓人搞混的三個概念,我給出一些提示和方法,讀者自己去編程序體​​驗。

窗口,而不能是孩子 窗口我就給那麼多提示吧。

我不想直接告诉你它们究竟还有什么不同,我现在给出三个API:GetFocus、GetActiveWindow和GetForegroundWindow,大家用这三个API去做些实验就知道了。

後記

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