LockWindowUpdate 講解

對LockWindowUpdate可憐的誤解。
 
這是關於LockWindowUpdate系列的第一篇。LockWindowUpdate的行爲,它是用於做什麼的,以及(可能更重要的是)它不是用於做什麼。
 
LockWindowUpdate做的事情其實很簡單。當一個窗口被“鎖定”,所有向它及其子窗口的繪製都會失敗。取代繪製操作的是,窗口管理器記住了應用程序試圖在窗口的哪一部分中進行繪製。當窗口“解鎖”後,這些區域被無效化,使得應用程序得到一個WM_PAINT消息,從而重新恢復了屏幕內容與應用程序認爲應當在屏幕上顯示的內容之間的同步。
  
大家已經在CS_SAVEBITS看到了“記錄在情形X有效時,應用程序試圖做的繪製,並在情形X不再有效時做無效處理”這樣的行爲。在這種意義上,LockWindowUpdate做了同樣的簿記的工作。在你用一個CS_SAVEBITS窗口覆蓋鎖定的窗口時這個行爲就會發生,只是這裏不會保存什麼數據。
 
在文檔中明確的指出,同一時間只能有一個窗口被鎖定。同時這也可以由函數原型暗示得出。如果兩個窗口可以同時被鎖定,將無法可靠的使用LockWindowUpdate。當你做下面的事情時將會怎樣:
LockWindowUpdate(hwndA); // 鎖定窗口A
LockWindowUpdate(hwndB); // 將窗口B也鎖定
LockWindowUpdate(NULL); // ???
 
第三個對LockWindowUpdate的調用會做什麼麼?是把所有的窗口解鎖?還是隻解鎖窗口A,或是隻解鎖窗口B?不論你如何回答,都不可能讓下列的代碼可靠的使用LockWindowUpdate:
void BeginOperationA()
{
 LockWindowUpdate(hwndA);
 ...
}
 
void EndOperationA()
{
 ...
 LockWindowUpdate(NULL);
}
 
void BeginOperationB()
{
 LockWindowUpdate(hwndB);
 ...
}
 
void EndOperationB()
{
 ...
 LockWindowUpdate(NULL);
}
 
設想BeginOperation開始了由異步行爲觸發的某個操作。例如,假設操作繪製播放的反饋,因此開始於鼠標按下,並結束於鼠標釋放。
 
現在假設仍在播放過程中時,操作B結束了。EndOperationB將會清理操作B,並調用LockWindowUpdate(NULL)。如果你假設這將解鎖所有窗口,那麼就會破壞了操作A,因爲它預期hwndA仍被鎖定。類似的,如果你提出應當只解鎖hwndA,那麼不只操作A被破壞了,操作B也會被破壞(因爲儘管操作B已經完成,hwndB仍被鎖定)。另一方面,如果你建議LockWIndowUpdate(NULL)應當解鎖hwndB,那麼請考慮一下操作A先於B完成的情況。
 
如果LockWindowUpdate同一時間能夠鎖定多於一個窗口,那麼這個函數的原型就需要修改,以使得解鎖的操作可以知道那一個窗口正在被解鎖。有很多方法可以做到這一點,例如添加一個新的參數或是創建一個單獨的函數。
// 方法A – 新參數
// fLock = TRUE 鎖定, FALSE 解鎖
BOOL LockWindowUpdate(HWND hwnd, BOOL fLock);
 
// 方法B – 獨立函數
BOOL LockWindowUpdate(HWND hwnd);
BOOL UnlockWindowUpdate(HWND hwnd);
 
但是這兩個都不是實際的情況,LockWindowUpdate函數同一時間只鎖定一個窗口。這樣做的原因在瞭解了LockWindowUpdate是用來做什麼的之後會更加清晰





LockWindowUpdate系列2:LockWindowUpdate是打算如何使用的?

現在我們知道了LockWindowUpdate的行爲。現在我們來看一下它是用於做什麼的。
 
事實上,LockWindowUpdate的設計意圖可以用一個詞表達:拖拽。但我們稍後再說到這個。
 
LockWindowUpdate的目的是允許一個程序暫時的接管繪製窗口的工作。爲了做到這個,你當然需要防止窗口函數(或其它任何人)進行他們正常的繪製活動;否則,兩處的代碼(正常繪製窗口的代碼和試圖接管繪製的代碼)會互相爭奪對窗口的控制,由於互不知道對方在做什麼,最後得到的將是一團糟。
 
但是,如果你已經鎖定了窗口的更新,那麼如何才能在窗口上繪製呢?你可以在GetDCEx函數中使用DCX_LOCKWINDOWUPDATE標誌。這個標誌表示“即便是窗口已經鎖定,也讓我繪製到上面”。當然了,只有鎖定了這個窗口的代碼才能傳遞這個標誌,否則又會引發LockWindowUpdate起初試圖解決的那類衝突。
 
由於人們都很喜歡看錶格,我製作了一個表格總結了當一個窗口被鎖定更新時發生了什麼變化。
 
正常行爲 更新被鎖定
BeginPaintGetDC,等等… 繪製操作繪製到窗口上 繪製操作沒有在窗口上繪出任何東西,但影響的區域被記錄下來以供後面無效化
帶DCX_LOCKWINDOWUPDATE標誌的GetDCEx (不要使用) 繪製操作繪製到窗口上
  
換句話說,當一個窗口更新被鎖定後,普通的DC獲取函數(BeginPaint及其夥伴)向窗口上繪製的能力將被剝奪,賦於了GetDCEx(DCX_LOCKWINDOWUPDATE)。注意,如果沒有窗口被鎖定更新,不要使用DCX_LOCKWINDOWUPDATE標誌,這個標誌的目的是指出“我就是那個調用了LockWindowUpdate的傢伙,快放行!”
 
窗口管理器有幾分像喜劇中的情節。你告訴守衛說“不許任何人進入這個房間。”一小時後你回來了,守衛不讓你進去。
“對不起先生,我不能讓任何人進這個房間。”
“但我就是那個告訴你不讓任何人進去的人呀。”
“是的,先生,我是按照您的指示做的。任何人都不能進入這個房間。” 
錯誤在於最初給守衛下達的命令。你應當說:“除了我以外,任何人都不允許進入這個房間。” DCX_LOCKWINDOWUPDATE就好比你對窗口管理器說:“是我,讓我進去。”
 
如果回頭看一下LockWindowUpdate函數的工作方式,你會發現如果一個鎖定的窗口沒有試圖做任何繪製,那麼當窗口解鎖時,不會有區域被無效。儘管CS_SAVEBITS窗口類屬性會在窗口從屏幕上移除時自動保存原始的象素,並自動還原這些象素,LockWindowUpdate不會做任何類似的事。你需要自己負起責任確保在窗口被鎖定更新時你修改的象素,在調用LockWindowUpdate(NULL)後恢復到原始的值。這通常可以通過在做自己的繪圖操作前,將原始象素保存到一個屏幕外的位圖中,並在完成後將它們繪製回去。
 
好,那麼下面這就是意圖的使用方式:
 
  • 當你想接管另一個窗口的繪製,對那個窗口調用LockWindowUpdate。
  • 保存你將要覆蓋繪製的窗口的象素。
  • 繪製新的象素(這些象素往往是原始象素的修正,比如在將一個對象拖動到一個窗口上方時,你可能會添加一個表示此對象的圖片)。
  • 只要你的操作還在進行中,儘管重複。(這麼做時,如果你正修改的屏幕區域與之前修改的不同,可能需要“備份”更多的屏幕上的象素。你可以增量的做備份/還原。例如,你不必在繪製新圖片前累計需要恢復的象素集,只需要先把保存的所有象素恢復到屏幕,然後計算拖動圖片的新位置,保存新位置處的象素,接着就可以在新位置繪製新圖片。通過這種方式,你只需要處理一組“備份象素”)
  • 當操作完成,恢復原始象素,並調用LockWindowUpdate(NULL)。

 

下一篇,我們將看到“拖拽”這個詞的更多內容,以其是如何緊密的與整個LockWindowUpdate的概念綁定在一起的。
 
儘管我們纔剛剛開始討論LockWindowUpdate,你應當已經足以回管這個問題。
 
(注意:寫這個系列的目的在於描述LockWindowUpdate的意圖使用方式,不是討論這首先是不是一個好的設計)




LockWindowUpdate系列3:什麼樣的操作中應當使用LockWindowUpdate?

如我在前面所說的,LockWindowUpdate的設計意圖可以用一個詞表達:拖拽。
LockWindowUpdate最簡單的使用場景是在“拖動時顯示窗口內容”功能關閉的情況下,當你移動或是改變窗口尺寸時,被窗口管理器使用。當你開始移動/改變尺寸操作,窗口管理器鎖定整個桌面以便可以繪製細點矩形反饋,而不會因爲其它窗口偶然與細點矩形交疊而導致衝突的風險。當移動/改變尺寸的操作完成,桌面被解鎖,所有東西恢復原貌。
 
應用程序使用LockWindowUpdate的常見的場景,是希望爲拖拽提供反饋而繪製一個自定義的圖片。在這個情況下,應用程序鎖定它自己的窗口以繪製拖拽的反饋。使用DCX_LOCKWINDOWUPDATE標誌來獲取一個可以用來繪製所需的反饋的DC,這樣就不必擔心窗口函數或應用程序中任何其它的代碼偶然的繪製到反饋窗口上,並搞亂了拖拽圖片。例如,如果這個應用程序正在一個列表視圖中繪製拖拽的反饋,此時某個異步事件引發這個列表視圖的內容改變(比方說添加了一個列表項),並且拖拽的圖片正好在新添加的列表項要出現的位置,你一定不會想讓列表視圖的標準重繪行爲覆蓋(或是更糟,重合)了拖拽的圖片。
 
可能你需要鎖定其它應用程序的窗口的場景是當你要把一個物體拖過整個屏幕。如果你的程序是一個類似Spy那樣的程序,允許用戶通過拖拽一個“選擇器”到一個窗口上方來選擇它時,你可能會需要這麼做。你需要鎖定用戶當前選擇的窗口,不僅讓它自己的重繪製不會與你的“選擇器”衝突,也使其不會與你放置在窗口邊沿的高亮效果衝突。
 
現在,你可能已經注意到所有使用LockWindowUpdate場景中一個共同的思路:他們都和某種形式的拖拽有關。拖拽窗口的標題以移動它、拖拽窗口的邊框以改變它的尺寸、將一個對象拖入窗口或是拖出窗口。這不是一個巧合,LockWindowUpdate就是專門設計用於這些拖拽場景的。由於拖拽對象要用到鼠標按鍵,而只會有一個鼠標,故而同一時間不會有多個拖拽操作進行。因此,沒有必要同時鎖定多個窗口的更新。也許這個函數應當更準確的命名爲LockDragWindow。




LockWindowUpdate系列4:什麼樣的操作中不應當使用LockWindowUpdate?

那麼,現在我們已經知道了什麼樣的操作中應當使用LockWindowUpdate,現在我們來看一下人們在一些與拖拽無關的工作中錯誤使用這個函數的方式。
 
人們看到LockWindowUpdate“鎖定的窗口將不能繪製自己”的行爲,就用它來作爲WM_SETREDRAW消息的偷懶的使用方式,儘管發送一個WM_SETREDRAW消息不不比調用LockWindowUpdate更麻煩。只是多打20來個字符,而且如果使用<windows.h>中的SetWindowRedraw宏的話還少會一半。
不使用 LockWindowUpdate(hwnd)
代而使用 SendMessage(hwnd, WM_SETREDRAW, FALSE, 0) or
SetWindowRedraw(hwnd, FALSE)
 
不使用 LockWindowUpdate(NULL)
代而使用 SendMessage(hwnd, WM_SETREDRAW, TRUE, 0) or
SetWindowRedraw(hwnd, TRUE)
 
就像我們在前面所說的,同一時間系統中只能有一個窗口的更新被鎖定。如果你調用LockWindowUpdate的目的僅僅是防止窗口重繪,比如因爲你在更新這個窗口,在你的更新完成前,不希望它不停的刷新,那麼請直接禁止窗口的重繪。如果你使用了LockWindowUpdate,將引來無數下面的問題。
 
首先,如果另一個什麼程序也以同樣錯誤的方式使用LockWindowUpdate,那麼你們中會有一個人失敗。首先調用LockWindowUpdate的程序將會成功,第二個調用的程序將會失敗。現在你準備怎麼辦?你的窗口不會被鎖定。
 
其次,如果你鎖定了自己的窗口更新,這時用戶切換到另一個程序,並試圖拖拽一個對象(或甚至只是嘗試移動一下那個窗口),那一個LockWindowUpdate將會失敗,於是用戶遇到了一個由於某種神祕原因拖放失效的情形。然後,10秒鐘後,一切功能又運作作正常。“愚蠢的爛Windows”用戶嘀咕道。
 
反過來說,如果你在一個拖放或是窗口移動的過程中準備調用LockWindowUpdate,那麼你的調用就會失敗。
 
這只是更一般意義上,使用全局狀態來處理局部情況的編程錯誤中,比較具體的例子。當你想禁止自己的一個窗口的重繪時,你不會希望這會影響到系統中的其它窗口。更新自己的窗口是一個局部情況,但是你使用了全局狀態(被鎖定更新的窗口)來維持它。
 
我可以預料到有人會說:“那麼,窗口管理器應當阻止人們在一個非拖放操作中鎖定窗口更新。”問題是,窗口管理器怎麼知道這個?它只是知道發生了什麼事情,但不知道爲什麼發生。一個程序到底是由於懶於使用WM_SETREDRAW消息而使用LockWindowUpdate?還是爲了響應引發拖放操作的用戶輸入?這裏沒辦法說“用戶鼠標按鍵壓下了”,因爲用戶可能用基於鍵盤的理論上和拖放等價的操作(例如使用方向鍵來改變窗口尺寸)。基本上這個問題很難於解決,除非計算機能更多一點對讓他做的事情的猜想。
 
下一回是對LockWindowUpdate的最終評論。




LockWindowUpdate系列5:關於LockWindowUpdate的最終評論 

現在大家瞭解了LockWindowUpdate的設計意圖,我現在將要告訴大家你們爲什麼不應當使用這個函數,甚至不是因爲其設計意圖的緣因。
 
這需要回到LockWindowUpdate被創造出來的歷史環境。回到16位Windows(特別是Windows 3.1)的時代。在那時,內存還是很昂貴的,顯示驅動功能也很有限。還沒有DirectX,沒有AlphaBlend函數。你所擁有的一切就是一塊屏幕緩衝區。LockWindowUpdate函數允許你接管這個屏幕緩衝區中對應一個窗口的部分,以得以在不需窗口知道的情況下應用自己特別的效果。
 
Windows 3.1距今已經十年多了,在這期間,我們有了DirectX覆蓋、區域化窗口、分層窗口、alpha混合、桌面合成,種種我們在過去不曾擁有的絕妙圖象特效。特別是這些美妙的分層窗口和區域化窗口,允許你做幾乎所有你希望用LockWindowUpdate去做的事情。如果你希望在一個窗口邊沿繪製高亮,你可以在其邊沿放置一個區域化窗口。如果你希望在一個窗口上方繪製一個拖拽圖片,你只需要創建一個分層窗口,並把它放置到目標窗口上方即可。使用的是分層窗口、一個區域及你想要的無論哪一種奇特的alpha通道,而將冗重的alpha混合和合成推給圖象引擎來完成。更好的是,分層窗口可以伸展到拖拽經過的窗口以外,這是LockWindowUpdate無法完成的。(你可以在Windwos XP中看到這種特效,在資源管理器窗口中“全選”,並將整個選擇內容在屏幕上拖拽。你將看到拖拽圖像沒有侷限於拖拽經過的窗口邊界。)
  
更甚者,在Vista的桌面窗口管理器(desktop window manager)令人驚奇的全新合成方式中,LockWindowUpdate更是不再值得使用。鎖定一個特定窗口的更新還不算太成問題,因爲桌面窗口管理器可以只是給你這個窗口的後臺位圖。但是如果你鎖定了整個屏幕(我常見到人們這麼做),桌面窗口管理器就需要將所有窗口合成到一個實際位圖中,以便在你使用DCX_LOCKWINDOWUPDATE標誌調用GetDCEx時可以返回給你。桌面窗口管理器通常是在DirectX和顯示驅動加速的輔助下直接進行合成的,所有的合成結果通常是直接送到屏幕上,實際上根本不會存放在一個“合成後的”位圖中。但是如果你鎖定了屏幕,並請求一個屏幕的DC,桌面窗口管理器只得模擬老式的行爲,使得你可以訪問一個代表了假如根本沒有合成這回事的情況下,你“應當得到”的東西。這並不輕鬆
 
尾聲。我並不清楚這個系列是否成功。我的目標只是幫助人們更有效的使用LockWindowUpdate,並在LockWindowUpdate不適於一個工作時指導他們轉向其它的替代物。換句話說,這是一篇關於LockWindowUpdate的文章,不是函數文檔。我試圖讓我的表達顯得輕鬆一些,但我估計我的幽默並不好笑,人們只是用他們來做爲否定註解的跳板。
 
特別感謝那些把這個系列用來作爲一個抱怨文檔的機會的人們。我的意思是,咄,如果文檔是完美的,我也大可不必寫這麼一個系列。不過這些人們往往只是看了函數說明那一頁,而忽視了閱讀所有文檔。嘿,除了單純的函數說明外,還有更多的文檔哪!函數說明只是一個參考,你應當是在已經知道會發生什麼,並只是需要微調一些細節時,纔會到那裏去看一下。真正的學習應當是從概覽和文章中去進行。如果你想學習如何操作你的收音機,你不會上來就看電路圖的。
 
我認爲當Ronald D. Moore說“聽播客時你必須有足夠的忍耐力”時,他一定是在搞什麼鬼。



LockWindowUpdate系列1:LockWindowUpdate的行爲?

LockWindowUpdate系列2:LockWindowUpdate是打算如何使用的?

LockWindowUpdate系列3:什麼樣的操作中應當使用LockWindowUpdate?

LockWindowUpdate系列4:什麼樣的操作中不應當使用LockWindowUpdate?

LockWindowUpdate系列5:關於LockWindowUpdate的最終評論


原文出處:http://blogs.msdn.com/oldnewthing/archive/2007/02/19/1716211.aspx
原文出處:http://blogs.msdn.com/oldnewthing/archive/2007/02/20/1726880.aspx
原文出處:http://blogs.msdn.com/oldnewthing/archive/2007/02/21/1735472.aspx
原文出處:http://blogs.msdn.com/oldnewthing/archive/2007/02/22/1742084.aspx
原文出處:http://blogs.msdn.com/oldnewthing/archive/2007/02/23/1747713.aspx

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