從0 開始 WPF MVVM 企業級框架實現與說明 ---- 第一講 WPF中 windows消息機制


談到桌面應用程序,我們第一反應就是它的消息機制是怎么處理的,那么我們就先聊聊這個windows消息機制

 

談起“消息機制”這個詞,我們都會想到Windows的消息機制,系統將鍵盤鼠標的行為包裝成一個Windows Message,然后系統主動將這些Windows Message派發給特定的窗口,實際上消息是被Post到特定窗口所在線程的消息隊列,應用程序的消息循環再不斷的從消息隊列當中獲取消息,然后再派發給特定窗口類的窗口過程來處理,在窗口過程中完成一次用戶交互。

  其實,WPF的底層也是基於Win32的消息系統,那么對於WPF應用程序來說,它是如何跟Win32的消息交互,這里到底存在一個什么樣的機制?接下來我會通過下面幾篇博文介紹這個消息機制:

讓應用程序動起來

  談到WPF的消息,首先應該知道DispactherObject以及Dispatcher在WPF系統中的作用。

  WPF大部分的對象都是從DispatcherObject派生的,從這里派生的對象具有一個明顯的特征,那就是:修改對象時所在的線程,和創建對象時所在線程必須為同一個線程,這就是微軟所謂的線程親緣性(Thread affinity)的最簡單理解。那么誰能保證線程親緣性呢?那就是Dispacher了。從DispatcherObject派生的類型繼承三個重要的成員:Dispatcher屬性,CheckAccess(), VerifyAccess()方法。其中后面兩個方法就是檢驗線程親緣性的。按照WPF的實現,如果你自己定義了個WPF的類型,並且是DispatcherObject的子類,你就必須在public的成員定義的邏輯開始處,調用base.Dispatcher.VerifyAccess(),檢驗線程親緣性。那么Dispatcher到底還做了什么事情呢?

  首先,我們看一下一個WPF的Application在啟動之后都走了哪些邏輯:

clip_image002

  通過調用堆棧可以看出,藍色的部分是啟動了一個線程,VisualStudio在Host的進程當中運行當前應用程序;紅色的部分是從Application.Main函數開始執行,經過幾個函數到達Dispatcher.Run(),最后到達Dispather.PushFrameInpl()方法。那么一個Application在Run之后,為什么要調用Dispatcher.Run()呢,他做了些什么事情你?如果通過Reflector仔細查看Application.Run(),你會發現里面實際起作用的代碼並不多,最后都是Dispatcher.Run在做事情。那么一個Application啟動之后,按照以前對Win32的消息機制的理解,當應用程序啟動后,必須進入消息循環,對於WPF,也是一樣的。那么WPF應用程序是在什么地方進入消息循環呢?其實這就是Dispatcher.Run()做的事情。查看上圖最后一步Dispacther.PushFrameImpl()的代碼,你會看到有下面的一段代碼:

clip_image004

  很明顯,橙色的部分是一個循環,看起來是不是很眼熟,跟Win32編程碰到的消息循環是否很像?對了,這就是WPF應用程序進入了消息循環。循環調用GetMessage方法從當前線程的消息隊列當中不停的獲取消息,取出一個msg之后,交給TranslateAndDispatchMessage方法Dispatch到不同的窗口過程去處理。這樣以來,任何需要應用程序處理的消息通過這個過程,被不同的窗口處理了,應用程序就動起來了。

  

 

 

下面的一篇我會介紹WPF當中的Win32窗口,正是這些窗口,處理着來自系統,或者來自應用程序內部的消息。

 

WPF內部的5個窗口

對於Windows系統來說,它是一個消息系統,消息系統的核心就是窗口。對於WPF來說也是如此。那么WPF內部為什么需要窗口,又存在哪些窗口呢?

前面,我們頻繁的提及“線程”,“Dispatcher”其實,運行WPF應用程序所在的線程就是WPF所謂的UI線程,在Application.Run之后,調用Dispatcher.Run時會檢查當前線程是否已經存在了一個Dispatcher對象,如果沒有就構造一個,在這里,一個線程對應一個Dispatcher。因此,WPF的對象在獲取this.Dispatcher屬性時,不同對象取的都是同一個Dispatcher實例。另外,前面提到的“消息循環”,“消息隊列”等都是Win32應用程序的概念,我們知道,提起這些概念,必然會跟Win32的“窗口”,“Handle”,“WndProc”之類的概念離不開,那么WPF里面究竟有沒有“窗體”,“Handle”,“WndProc”呢?

我想說的是:有,還不止一個,只不過沒有暴露出來,外面不需要關心這些。

通常情況下,一個WPF應用程序在運行起來的時候,后台會創建5個Win32的窗口,幫助WPF系統來處理操作系統以及應用程序內部的消息。在這5個窗口中,只有一個是可見的,可以處理輸入事件與用戶交互,其他4個窗口都是不可見的,幫助WPF處理來自其他方面的消息。接下來我會來介紹究竟這5個Win32的窗口如何幫助WPF處理消息,我會根據每個窗口創建的順序來介紹。

 

隱藏消息窗口 (Window 1#)

創建時機:在Application的構造函數調用基類DispatcherObject的構造函數的時候,會創建一個Dispatcher對象,在Dispatcher的私有構造函數當中。

用途:實現WPF線程模型的異步調用。

談到異步調用,相信許多人都不陌生。WinForm下,我們通常為了使一些花費較多時間的方法調用不影響UI的響應,會將這個操作分為很多步,然后使用BeginInvoke調用每一步,這樣UI響應就不會被阻塞。BeginInvoke的本質是往消息隊列當中PostMessage,而不是直接調用,與此同時,UI行為(MouseMove)導致系統也往消息隊列當中PostMessage更新UI,但由於彼此花費的時間很短,就感覺兩個消息是被同時處理似的,界面就不會覺得被阻塞了。WPF同樣面臨這樣的問題,他是如何解決的呢?在這里Window 1#起着至關重要的作用。通過下面一副圖我們來看看Window 1#在做什么事情?

clip_image002

WPF也是通過BeginInvoke來解決的,而Wpf的BeginInvoke是在Dispatcher上面暴露了,因為整個消息系統都是Dispatcher在協調。從上面圖可以看出Dispatcher在調用BeginInvoke之后所經歷的流程,最終是什么時候Foo()被真正執行的。

第一步,就是將調用的Delegate和優先級包裝成一個DispatcherOperation放入Dispatcher維護的優先級隊列當中,這個Queue是按DispatcherPriority排序的,總是高優先級的DispatcherOperation先被處理。關於優先級相關知識可以參考MSDN對WPF線程模型的解釋。

第二步,往當前線程的消息隊列當中Post一個名為MsgProcessQueue的Message。這個消息是WPF自己定義的,見Dispatcher的靜態構造函數當中的

_msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");

這個消息被Post到消息隊列之前,還要設置MSG.Handle,這個Handle就是Window 1#的Handle。指定Handle是為了在消息循環Dispatch消息的時候,指定哪個窗口的WndProc(窗口過程)處理這個消息。在這里所有BeginInvoke引起的消息都是Window1#的窗口過程來處理的。

第三步,消息循環讀取消息。

第四步,系統根據獲取消息的Handle,發現跟Window1#的Handle相同,那么這個消息派發到Window1#的窗口過程,讓其處理。

第五步,在窗口過程中,優先級隊列當中取一個DispatcherOperation。

第六步,執行DispatcherOperation.Invoke方法,Invoke方法的核心就是調用DispatcherOperation構造時傳入的Delegate,也就是Dispatcher.BeginInvoke傳入的Delegate。最終這個Foo()方法就被執行了。

通過上面的六步過程,一次Dispatcher.BeginInvoke就被處理完成。而這個過程需要消息不斷的流動,就必須加入消息隊列,最后還要特定的窗口過程處理,而核心的東西就是這個隱藏的Window1#,他在WPF當中只負責處理異步調用,其他的消息他不關心,剩余的4個窗口在處理。這個Window1#在WPF當中被包了一層殼子,如果感興趣,你可以去查看類型MessageOnlyHwndWrapper。

 

 

 

處理應用程序激活和系統關閉的窗口(Window 2#)

創建時機:在調用Application.Run之后,運行到Application.EnsureHwndSource()方法當中。

用途:派發Application的Activated,Deactivated,SessionEnding事件。

WPF為了安全起見沒有讓UI窗口來處理應用程序激活,反激活,以及操作系統關閉時對應的消息,而是內部創建了一個隱藏的窗口,專門用來接收WM_ACTIVATEAPP和WM_QUERYENDSESSION兩個Windows消息。從線程的消息隊列拿到這兩個消息后,會觸發WPF的Application.Activated,Application.Deactivated,Application.SessionEnding這三個事件。

更詳細的可參考Application類型的EnsureHwndSource(),AppFilterMessage(),這兩個方法。

上面的過程可用下圖描述:

 

Window2

 

 

系統資源更改通知窗口(Window 4#)

創建時機:Application的MainWindow的Xaml被反序列化成對象之后,需要確認Window的ThemeStyle的時候。

用途:處理當操作系統的Theme發生改變后,以及諸如SystemColors,SystemFonts,電源,顯示器等跟系統關聯的資源發生改變時,更新WPF這邊的表現。

WPF在應用出現的MainWindow在初始化完成后,會創建一個隱藏的窗口,專門處理來自系統相關資源更新后的消息,比如WM_ThemeChanged,WM_SystemColorChanged,WM_DisplayChange,WM_PowerBroadcast等等。跟Window2#的初衷類似,為了安全起見,沒有通過可見的UI窗口來處理這些消息,而是內容創建了這個隱藏的Window4#窗口來處理這些消息,確保UI窗口可以安全的更新由於系統Theme及相關資源改變后的表現。

上面的過程可用下圖描述:

Window4

 

 

 

本來想自己寫這個WPF消息機制的邏輯,畢竟資歷有限,看到網上有個大神寫得比較全面,細致,我就引用了下,做了點更改,讓你們看起來會更容易理解一點,后面好多東西我們會基於這樣一個windows消息機制來處理問題,尤其是View 層與Viewmodel層的消息通知,還有一些線程安全的問題。

 


參考:http://www.cnblogs.com/therock/articles/2140459.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM