程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WPF自定義控件 - 頂級控件

WPF自定義控件 - 頂級控件

編輯:關於.NET

作為一個WPF程序員,我最希望看到的是WPF的應用,或者更確切的說是絢麗 的應用,雖然限於自身的實力還不能拿出成績來,但看到別人的作品時,心裡還 是有很大的寬慰——WPF是可以做出更加動人地產品的,只要你堅定 的走下去,帶著不滿現狀的追求走下去。

下圖是Telerik的WPF控件,我相信很多人也下過他的DEMO,研究過他的代碼,並由此激起對WPF的信心。今天 我們就來仿造他的DragAndDrop做一個自己的控件。

一.Windows的窗體

一般來說我們只要打開Visual Studio 2008依次打 開文件(File)->新建(New)->項目(Project),在彈出的新項目(New Project)窗口中選中Windows中的WPF Application,單擊確定(OK)便可以開始我 們的WPF之旅。

之後我們按F5或綠色箭頭 來 運行程序會看到一個空白的窗口。

這太普通,如果你有過桌面程序開發經歷,這個或許都不能帶起你的 任何激情。況且他上面沒有任何東西,也就意味著沒有向MM們炫耀的資本。當然 還有一點最重要的,這個窗體的出現也是在你預期之內的——你理所 當然的認為該有這麼個窗體,因為裡面有一個繼承於Window的類,而App.xaml中 的 StartupUri屬性是指向這個類的XAML的,所以他就應該出現?

那麼如 果我再追問App.xaml是什麼,作用是什麼,為什麼把XAML放到StartupUri屬性中 便會出現?你可能會聳聳肩坦然:WPF自動做的,我們應該注重的是WPF使用本身 。當然這話也沒錯,每個人的興趣愛好不同,你如果想知道為什麼可以跟著我走 ,如果你認為我說的不好,非常歡迎提出批評指正。

實際上我對解析 XAML也沒有任何興致,對他自動生成的App.g.cs以及XX.g.cs也不怎麼關心,我 所希望知道的是WPF是如何把圖像給繪制出來顯示給我們看的,空窗體也是繪制 了,要不然你怎麼看的到那個窗體。

可能已經有人不耐煩了,操作系統 提供了CreateWindowEx來創建窗體,他肯定是調用了這個。

那口說無憑,我們端出利器Windbg來看真相是否如此,如果沒有可以到 http://www.microsoft.com/whdc/devtools/debugging/default.mspx下載。這 裡我用的是6.8版本,為什麼要說版本,這個是因為我之前用了某低版本的有很 多問題,又是新手還抱怨是操作系統的問題。

使用之前我們先設置 Symbol的路徑,為什麼要設置Symbol呢?其實我們的程序執行的時候只是二進字 代碼,debug的時候只是讓我們能得到這些代碼段,如果我們看到的內存信息是 二進制類似無疑是痛苦的起碼也是難於馬上理解的,所以微軟用了pdb文件來記 錄,這樣就可以使內存地址和函數名相關聯,用 debug的時候可以先從pdb上看 看這段地址是否有對應內容,有的話就顯示,當然pdb還記錄了一些源碼信息, 數據類型,數據結構,不過微軟提供的 Symbol大部分都給去掉了。

在我 的路徑中 srv*

d:\symcache

*http://msdl.microsoft.com/download/symbols;
d:\symcache

大家看到有兩個d:\symcache,這個說明我把要下 載的Symbol放到了D盤symcache文件夾下,下次需要讀的話先從這個文件夾下取 沒有的話再到http://msdl.microsoft.com/download/symbols中下載,如果你有 需要也可以把d:\symcache換成其他的,比如;E:\Program\Symbol。下載文件的 時候旁邊會有*BUSY*出現。

附加我的進程TopControl.exe:

然後便是在CreateWindowEx函數這裡下斷點

0:000>  bc * 
0:000> BU USER32! NtUserCreateWindowEx

其中bc * 是清除全部斷點,為什麼是 USER32!NtUserCreateWindowEx而不是USER32!CreateWindowEx,這個似乎就要看 版本了,我的不支持CreateWindowEx。再者如果你對Windows內核熟悉的話就會 知道其實CreateWindowEx內部是調用了 NtUserCreateWindowEx,ShowWindow是 NtUserShowWindow,怎麼被我的才學嚇倒了?哈哈,其實這個是和操作系統有關 的,我的機器是Windows 7,列表是從這個網頁看的http://hi.baidu.com/%B7% FA%CD%DE/blog/item/66658d10fb447af4c2ce7929.html。

在Debug菜單中Restart後g

0:000> ~*Kl
.  0   Id: b50.82c Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP  RetAddr
001ef1cc 76f80cfd USER32!NtUserCreateWindowEx 
001ef470 76f80e29 USER32!VerNtUserCreateWindowEx+0x1a3
…節約空間省略
   1  Id: b50.ca8 Suspend: 1 Teb:  7ffde000 Unfrozen
ChildEBP RetAddr
0084f994 77875e4c  ntdll!KiFastSystemCallRet
0084f998 75bb6872 ntdll! NtWaitForMultipleObjects+0xc
…節約空間省略

~*KL命令是查看全部線程堆棧信息, 如果你不是在當前線程(當 前線程看左邊,0:00代表0線程,0:01代表1線程,以此類推)請用命令 ~[線程 號]s,如~0s 就是轉到第0個線程,如果~1s就是到第1個線程。我們看到001ef1cc 76f80cfd USER32!NtUserCreateWindowEx在第0號線程那麼我只要~0s轉到0號線 程。

0:000> .load  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
0:000>  !clrstack 
OS Thread Id: 0x1350 (0)
ESP        EIP
0041ec8c 76f80d5d [NDirectMethodFrameGeneric: 0041ec8c]  MS.Win32.UnsafeNativeMethods.IntCreateWindowEx(Int32, System.String,  System.String, Int32, Int32, Int32, Int32, Int32,  System.Runtime.InteropServices.HandleRef,  System.Runtime.InteropServices.HandleRef,  System.Runtime.InteropServices.HandleRef, System.Object)
0041ecd0 69c25bec MS.Win32.UnsafeNativeMethods.CreateWindowEx (Int32, System.String, System.String, Int32, Int32, Int32,  Int32, Int32, System.Runtime.InteropServices.HandleRef,  System.Runtime.InteropServices.HandleRef,  System.Runtime.InteropServices.HandleRef, System.Object)
0041ed18 69c257fc MS.Win32.HwndWrapper..ctor(Int32, Int32,  Int32, Int32, Int32, Int32, Int32, System.String, IntPtr,  MS.Win32.HwndWrapperHook[])
0041edd8 5aecde40  System.Windows.Application.EnsureHwndSource()
0041edec 5aecd646  System.Windows.Application.RunInternal(System.Windows.Window)
0041ee10 5aeb49d6 System.Windows.Application.Run (System.Windows.Window)
0041ee20 5aeb4999  System.Windows.Application.Run()
0041ee2c 002d0096  TopControl.App.Main()
0041f04c 6a9c1b6c [GCFrame: 0041f04c] 

除了

0:000> .load  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
0:000>  !clrstack

這兩個我打的命令,一個是加載sos以便看到托管信息 ,一個是看托管信息棧,其他信息請從下往上看,也就是說先執行的在下,後執 行的在上。

注意這裡有時候你會得到類似信息

OS Thread  Id: 0x82c (0)
ESP       EIP
001ef66c 76f80d5d  [DebuggerClassInitMarkFrame: 001ef66c] 

沒有關系,多g幾 次就可以看到想要的了。

很明顯的我們看出TopControl就是選中的控件 ,在App的Main函數中調用Application的Run函數,然後Run函數中又調用了一個 有參的Run函數,從參數類型上我們可以看出是一個窗體類,之後又調了 RunInternal 和EnsureHwndSource 並在MS.Win32.HwndWrapper的

構造函 數中調用了 MS.Win32.UnsafeNativeMethods的靜態方法CreateWindowEx,後又 調用IntCreateWindowEx利用 p/invoke引發了user32.dll中的CreateWindowEx, 這裡不說p/invoke,具體的可以看黃際洲、崔曉源的《精通.NET 互操作》,多 說一句,我們的托管代碼也是要編譯為機器碼才能運行的,Win32 API也可以說 是段機器碼,這樣當然能調用的。如果你想得到譬如CreateWindowEx結構體類的 詳細寫法,掛接的消息處理函數等具體信息,就可以用Reflector查看

HwndWrapper

來獲得,假如你是個一步步仔細看的人你會發現在 RunInternal的這句:

if (window.Visibility !=  Visibility.Visible)
{
  base.Dispatcher.BeginInvoke (DispatcherPriority.Send, delegate(object obj)
  {
     (obj as Window).Show();
    return null;
  },  window);
}

在Show中你會看到夢寐的ShowWindow函數。

可問題又來了,即便我知道了WPF是調用了CreateWindowEx來創建的窗體 ,並用了個ShowWindow函數可為什麼要這麼做呢?這個和顯示以及畫圖有什麼聯 系?

這裡我們就要了解下調用CreateWindowEx時需要的參數

IntPtr windowHandle = User32Dll.CreateWindowEx (ExtendedWndStyle.WS_EX_LAYOUTRTL//擴展樣式 
, m_WndClsName // 剛才注冊完的名稱 
, null     //窗體名稱 
,  WndStyle.WS_VISIBLE | WndStyle.WS_CHILD //子窗體 
,  this.Left //X坐標 
, this.Top //Y 坐標 
, this.Width  //寬度
, this.Height //高度
, this.Parent.Handle //父 對象句柄
, IntPtr.Zero //上下文菜單句柄
,  Kernal32Dll.GetModuleHandle(null)//實例句柄
, IntPtr.Zero//指向 一個值的指針,該值傳遞給窗口 WM_CREATE消息 
);

可 以看到它需要傳入的具體的左上角坐標,大小(寬度,高度),這些信息很明顯的 可以得到了一個矩形區域,鼠標移動的時候有一個坐標,看看坐標是不是在這個 矩形區域就可以了(操作系統還要判斷層疊或父子關系),Windows操作系統把它 (代碼上的參數)存成一個結構體放到系統區域,他所產生的Handle 和進程中 的Handle表不一樣,window handle是全局唯一的,也是說整個操作系統不會有 相同的兩個window handle,Handle是什麼?其實就是一個標示(代號),為什麼 常會翻譯為句柄,這個我不知道,不過看蔡學鏞的.NET 的自動記憶體管理台灣 那邊似乎翻譯為視窗代號,我個人認為更容易理解些。一個線程上可以創建多個 window Handle,譬如WPF和WinForm默認都是在同一窗體線程上創建UI控件。

當我們移動鼠標時,鼠標的位移會產生中斷,CPU在開中斷的前提下把鼠標中 斷放到IDT中斷表中獲取對應處理函數,而Windows操作系統檢查所有的結構體來 判斷應該把這個消息交給哪個窗體處理,畢竟鼠標移動的消息只要求被移動到上 面的窗口得知就可以了,這樣我們可以根據鼠標的位置重繪圖形讓人感覺到效果 ,比如鼠標移上按鈕發光。雖然循環判斷這麼多窗體結構我們按常理就知道很費 時間,可更棘手的問題是我們給的結構體畢竟是一個規則的矩形,萬一窗口是一 個不規則的圖形怎麼辦?像我在WPF自定義控件——使用Win32控件 中產生的不規則圖形,或是如淘寶旺旺的這種窗體上凸出一塊的做法。那麼其實 就要給操作系統一些不規則圖形的信息,當畫面超出不規則的區域時直接裁減掉 就可以了,當然不規則信息是我們給的,Windows給了我們兩個API進行傳遞一個 叫作SetWindowRgn,如果要實踐不規則圖形請參看 Windows 中不規則窗體的編 程實現;在XP之後操作系統為方便我們只要使用WS_EX_LAYERED樣式放入一張帶 ALPHA通道的PNG圖片,不過直到調用 SetLayeredWindowAttributes 或者 UpdateLayeredWindow 函數才會有效,就可以混合桌面的顏色達到效果然後自動 計算不規則圖形,當然這個做法有個很大的副作用那就是和它同一個線程上的控 件都顯示不了,所以有些人就搞了兩層,如同我文章裡的那樣,一層圖片,一層 控件。當然你可以在圖片上自己畫控件,或是把控件放在另外一個線程上接受響 應。如果你對這種做法很感興趣那麼請看Layered Window,或想對png格式了解 請看 了解PNG文件存儲格式。

操作系統知道了怎麼判別我們的窗體,那麼怎麼把鼠標消息傳遞給我們呢? 畢竟線程是在內核被創建的,對應的線程結構體也就在內核區而我們的處理函數 在用戶區,那怎麼辦獲得?估計說到這裡已經沒有懸念了,大家已經知道了使用 win32 API GetMessage或PeekMessage來監聽,放個MSG結構讓內核填充,如果你 不清楚可以詳細的看這篇深入GetMessage和PeekMessage,很遺憾它的原文似乎 沒有了,我沒有搜索到,如果哪位大俠還有私人珍藏記得分享哦。

從用戶區到內核區?什麼叫作內核?什麼叫作用戶區域,其實IA32架 構CPU在保護模式下允許我們的內存區域劃分成4塊,分別以4種級別來表示 (Windows和Liunx均只用了0,和3兩層),級別越高數字越小,數字最小的在最內 層,

在非一致性代碼段下

,級別低的不能

直接訪問級別 高的,級別高的可以訪問級別低的;這很好理解,有些系統區域的重要信息不是 誰都能動的,而且CPU規定有些指令只能在0層使用,比如LLDT,SLDT,MOVE CRN,SMSW等等;不過在一致代碼段情況下情況正好相反,就是說級別低的可以訪 問級別高的,可級別高的不能訪問級別低的,Level3的代碼可以訪問Level0的代 碼,Level0的代碼不能訪問Level3的,為什麼這樣呢?這是怕有些用戶訪問了 Level0層的代碼然後借以來控制Level3上的代碼。這裡GetMessage或 PeekMessage都可以填寫我們用戶區的MSG結構體,很明顯是屬於非一致代碼段了 。

說到這可能又有人迷糊了:啥是什麼非一致性代碼或是一致性代碼, 其實這只是種說法,簡單說他只是內存段描述符上的一個標示C,值為1就代表這 個區域內的代碼都是一致性代碼,0就說明這個區域內的代碼是非一致性代碼; 一致代碼段不太常用,一般放些共享數據。

用戶態的代碼不能直接訪問到是Level0的代碼,有時候就是需要用到 Level0的代碼來操縱CPU的特殊指令怎麼辦?雖然你不能隨便訪問特權級的代碼 ,但CPU給了你一些“曲線救國”的方案,比如調用門,中斷。在 Windows和Liunx系統中均沒有采用調用門、任務門,原因是使用那些指令很麻煩 ,況且速度也不快要200多個CPU周期,Windows采用了中斷直接來操作,這就好 比,你沒有權利進入軍區,可如果你買了門票找個向導還是可以進入,不過需要 從游客通道規規矩矩的走,當然游客通道就表明了禁區你是不可以去的,能強制 進入禁區的人,我們稱為hacker。所以我們只好找“導游”繞一圈進 入特定參觀“景點”,下中斷如int 152進入中斷表,選擇代碼地址 到GDT再到LDT讀取,最後才能進入內核段,在實際操作中的這個導游就是win32 API,在他的引導下你就完成了參觀,當然這個參觀過程是黑箱,只能得到結果 。從這一過程中也可以知道 從用戶態到內核是很耗時間的,繞路了嘛。

什麼是IDT,GDT,LDT?這個都是CPU保護模式的概念,建議你看下楊季文的 《80x86匯編語言程序設計教程》還有Intel自己的說明書《IA-32 Intel Architecture Software Developer’s Manual》卷3

這裡還要再啰 嗦幾句,我們知道線程是操作系統調度的最小單位,也就是說在單CPU情況下其 實一個時間只能使用一個線程,但是如果切換的快,就會給我們造成同時進行的 假象。這就好比幾個人演出,可舞台在同一時刻只能一個人演出,一個人演出一 段時間然後換另外一個人出場。

在下圖中我們把身上帶旗頭上長觸角( 我不知道專業術語叫啥)的稱為A,另外一個女的稱為B,男的稱為C。

圖一.剛開始是C已下台,A正在舞台演出,B等待演出

圖二.緊接著A演出結束,換B演出,C等待

圖三.C演出,B退場,A等待

圖四.和圖一樣這樣進行了一個循環

圖五.減少每個人出場時間,提高循環速度

圖六.當循環超過到我們眼睛的延遲就會感覺他們一起演出

圖七.如果演出A演出時候劇目說需要C怎麼辦?A還要繼續演出

圖八.只需要把當前劇目記錄到C的筆記中,等他上台讓他演

圖九.但下次演出時還是需要按照順序來執行,依舊是B演出

圖十.輪到C上場的時候先檢查自己的筆記,如果筆記有內容則當劇目 演出

圖畫的人物A、B、C就是線程的縮影,線程是一個個輪流執行的,如 果操作系統讓線程每個線程執行的時間少些,比如10毫秒,這樣循環很快就讓我 們感覺這麼多線程其實是一同執行的,可一個線程執行過程中需要另外一個線程 做事,如果傳遞消息就要建立一個窗口如上的CreateWindowEx或 CreateWindow ,為什麼一定要建立窗體?這個是操作系統規定的,整個報文系統是建立在窗體 結構之下的;如若傳遞的消息是異步的(PostMessage),異步的意思是我這邊 先運行沒關系到時候你那邊運行一遍就行的情況,那麼就把信息記錄到另一個線 程的某隊列中,當運行到那個線程,操作系統會判斷這個隊列是否為空,不為空 的話就用那個線程的GetMessage方法去調用(GetMessage方法有點復雜,這裡只 是簡單描述,實際情況復雜的多,查驗的隊列也有7個)。如果是同步的 (SendMessage),同步的意思是一定要你做完了我才能做,可這裡並不是說用到A 線程了就馬上運行A線程(A線程權限突然提高除外),還是要把信息放到A線程 的消息隊列中, 然後自己休眠(告訴操作系統不需要再運它,可以轉向運行其 他線程),等操作系統安排A運行的時候,等A讀完了這條消息,才告訴發送這條 信息的線程,A已經OK了,你可以醒了起來干活了,告訴這件事其實就是設置一 個標志位。可能有些人馬上會問,他的數據結構是怎麼樣的,如果他等了多個線 程怎麼辦?可實際情況是一個時間他只能執行一次SendMessage然後就開始睡覺 了 (設了一個標志位告訴操作系統不需要再運行它),所以他只能等待一個線程 ,假如他等待的線程也在等待他的話,那麼這個就是死鎖。死鎖不怎麼占用CPU ,都不運行怎麼會占用,不過一直不執行,那麼你會發現畫面不動(無法得到處 理隊列中的各種重繪,移動消息),感覺“卡”了,然後我們就會說 這個程序“死掉”了,說到這裡還是有人不信,明明程序“當 了”操作系統會出現提示說什麼“沒有響應”,實際上操作系 統會向程序的隊列發送一個消息,而這個消息上有計數器,當主線程長時間休眠 ,自然不會去執行那個消息,消息上的計數器遞減到0時,操作系統便會告訴我 們這個程序沒有響應了,回想下當你主線程中有個長循環的時候是不是也會出現 這種提示。

這裡又出現了一個點,操作系統怎麼會知道哪個是主線程? 操作系統會在創建進程的時候創建一個線程,那個線程一般作為主線程,到這裡 有人終於長長的呼了口氣:我就是不在那個所謂的主線程創建窗體,他怎麼通知 ?你一直不創建窗體一路走到黑確實沒有必要通知,如果在其他線程創建窗口, 操作系統會判斷是否已經有 UI線程(就是建立過窗體的),如果之前沒有,那 麼會把第一個創建窗口的線程作為以後傳遞消息的線程,這一過程具體是怎麼樣 的?這我還真不知道了,不過我知道你問的這個東西絕對有價值,有意義,支持 你繼續探究,個人認為只有不斷刨根問底的程序員才能看到真正的全貌掌握真正 的實質。

說到GetMessage,順便也來說說常用的這種結構:

while (true)
{
  if (!this.GetMessage(ref  msg, IntPtr.Zero, 0, 0))
  {
    break;
  }

  TranslateMessage(ref msg);
  DispatchMessage (ref msg);
}

為什麼要加個While循環?GetMessage只能 得到一次信息,信息在線程關閉前自然是要不停獲取處理的,這類“死循 環”不是會很耗麼?實際當 GetMessage沒有東西的時候,他會自動休眠, 得到消息然後翻譯(Translate)消息,轉為unicode或Ansi編碼, DispatchMessage是把消息轉發到對應的窗口處理函數中去,這個處理函數什麼 時候定的呢?就是在注冊窗口類的時候 (RegisterClassEx),其中的lpfnWndProc 指針就是傳這個,說到這裡可能要問了,為什麼操作系統不在GetMessage裡直接 轉發,而需要通過DispatchMessage轉發一次,這裡因為有些消息你可以率選是 否要處理,要處理的才給DispatchMessage分配,GetMessage是告訴操作系統這 個消息已經發送給這個線程了,如果另一個線程在等待(SendMessage)那麼就可 以醒了,DispatchMessage是通知自己的處理函數。

上面說的都是概念上 的,為什麼說是概念上,因為Windows是不開源的,剛才的說法從很大一部分來 說是一種臆斷,不過研究Windows的從來不乏人才,比如開源的仿照Windows的 ReactOS,如果你對window內核處理感興趣,建議你看下毛德操的《Windows 內核 情景分析》或David A. Solomon 的《Microsoft Windows Internals》,最近在 看Raymond Chen 的《The Old New Thing》感覺也可以對Windows的設計有一定 了解。

二.WPF的頂級窗口

頂級窗口的概念是相對於二級窗口的, 頂級窗口是坐標相對於桌面或者說是屏幕的左上角坐標,二級窗口的坐標是相對 於父容器的左上角坐標。頂級窗口是以桌面為參照物是針對全局而言的,全局就 要遵循Win32 的規則,剛才我們看到了WPF其實也是一個Window handle,如果他 是一個不規則窗體,必然要保存一個描述不規則圖形的結構,而結構體是在內核 ,一定要通過相應Win32 API才能放的進去,剛才說了系統給我們提供了兩個方 案一個是設置區域(SetWindowRgn),一個是設置層 (WS_EX_LAYERED),WPF選擇的 是哪一種呢?

我們在剛才創建的窗口中寫如下代碼:

<Window x:Class="TopControl.Window2"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="300" Width="300" AllowsTransparency="True"  WindowStyle="None">
  <Window.Background>
     <VisualBrush>
      <VisualBrush.Visual>
        <TextBlock>Curry</TextBlock>
       </VisualBrush.Visual>
    </VisualBrush>
  </Window.Background>
</Window>

運 行之後當然很容易看到這樣的不規者窗體,如果自己實在懶的動手,你也可以下 載MSDN的示例Non-Rectangular Windows Sample

依照剛才的做法用Debug在NtUserUpdateLayeredWindow中下斷點。

BU USER32!NtUserUpdateLayeredWindow

得到信息如下:

#  3  Id: 1c48.270c Suspend: 1 Teb: 7ffdc000  Unfrozen
ChildEBP RetAddr
03d7f820 76f7b636 USER32! NtUserUpdateLayeredWindow
03d7f850 5f4abcca USER32! UpdateLayeredWindowIndirect+0x3f
WARNING: Stack unwind  information not available. Following frames may be wrong.
03d7f8b0 5f45ad21 wpfgfx_v0300! MilUtility_PathGeometryCombine+0x46616 
03d7f918 5f45adb3  wpfgfx_v0300! MilCompositionEngine_DeinitializePartitionManager+0x4f4c
03d7f958  5f478d5d wpfgfx_v0300! MilCompositionEngine_DeinitializePartitionManager+0x4fde
03d7f97c  5f478dfe wpfgfx_v0300!MilUtility_PathGeometryCombine+0x136a9
03d7f9c4 5f478e42 wpfgfx_v0300! MilUtility_PathGeometryCombine+0x1374a
03d7f9dc 5f433726  wpfgfx_v0300!MilUtility_PathGeometryCombine+0x1378e
03d7fa34  5f4337b5 wpfgfx_v0300!MilChannel_AppendCommandData+0x61a9
03d7fa4c 5f42ddb8 wpfgfx_v0300!MilChannel_AppendCommandData+0x6238  
03d7fa74 5f42dbe9 wpfgfx_v0300! MilChannel_AppendCommandData+0x83b 
03d7fac0 5f42dc61  wpfgfx_v0300!MilChannel_AppendCommandData+0x66c
03d7fae8  5f42b6a2 wpfgfx_v0300!MilChannel_AppendCommandData+0x6e4
03d7fb00 779d1174 wpfgfx_v0300! MilChannel_SetNotificationWindow+0x26f3
03d7fb0c 7788b3f5  KERNEL32!BaseThreadInitThunk+0xe
03d7fb4c 7788b3c8 ntdll! __RtlUserThreadStart+0x70
03d7fb64 00000000 ntdll! _RtlUserThreadStart+0x1b

其中wpfgfx_v0300模塊就是WPF的核心模塊,之前叫做Milcore,我們常 常看到的圖說的就是它。

這很明顯的是Render線程,看看我們的主線程 中有些什麼,也就是常說的UI線程。

0  Id: 1c48.16a4  Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr
0025df78 77875e4c ntdll!KiFastSystemCallRet
0025df7c  75bb6872 ntdll!NtWaitForMultipleObjects+0xc
0025e018 779cf12a  KERNELBASE!WaitForMultipleObjectsEx+0x100
0025e060 779cf29e  KERNEL32!WaitForMultipleObjectsExImplementation+0xe0
0025e07c  5f4635ef KERNEL32!WaitForMultipleObjects+0x18 
WARNING: Stack  unwind information not available. Following frames may be  wrong.
0025e1a4 5f46364a wpfgfx_v0300! MilUtility_PolygonHitTest+0x1e63
0025e1f4 5bd98571  wpfgfx_v0300!MilComposition_WaitForNextMessage+0x43
0025e214  5bd98571 PresentationCore_ni+0x1d8571 
0025e238 5bdbad90  PresentationCore_ni+0x1d8571 
0025e288 5bd9ffba  PresentationCore_ni+0x1fad90
0025e2b0 5bd9d0ba  PresentationCore_ni+0x1dffba
0025e2cc 69c2668e  PresentationCore_ni+0x1dd0ba
0025e318 69c265ba  WindowsBase_ni+0x9668e
0025e338 69c264aa  WindowsBase_ni+0x965ba
0025e354 69c2639a  WindowsBase_ni+0x964aa
0025e394 69c24504  WindowsBase_ni+0x9639a
0025e3b8 69c23661  WindowsBase_ni+0x94504
0025e3f4 69c235b0 WindowsBase_ni+0x93661  
0025e424 69c25cfc WindowsBase_ni+0x935b0
0025e474  76f886ef WindowsBase_ni+0x95cfc

注意紅色標示的說明他竟然 等待了,休眠了。

轉到主線程 ~0s,看CLR堆棧信息

0:000> !clrstack 
OS Thread Id: 0x16a4 (0)
ESP       EIP
0025e20c 778764f4  [NDirectMethodFrameStandalone: 0025e20c]  System.Windows.Media.Composition.DUCE+UnsafeNativeMethods.MilCompositi on_WaitForNextMessage(IntPtr, Int32, IntPtr[], Int32, UInt32,  Int32 ByRef)
0025e22c 5bd98571  System.Windows.Media.MediaContext.CompleteRender()
0025e240  5bdbad90 System.Windows.Interop.HwndTarget.OnResize()

可 以看出UI線程是通過System.Windows.Media.Composition.DUCE模塊和Render線 程進行的交互,而進行交互傳輸的是MediaContext。

說到這裡可能引來 眾多崇拜的目光,居然一下就debug出來,完全應該去買體育彩票,要不然3.5個 億就是你的了,可實際上我剛開始用的是NtUserSetWindowRgn來下斷點,為什麼 用那個呢?這個還要從 Nick的Transparent and non-rectangular windows博文 說起,他裡面說了因為技術原因所以使用了SetWindowRgn,我也癡癡的,堅定不 移的相信了很長一段時間,直到我寫WPF Win32控件的時候,嘗試著去驗證下, 居然不能斷不到,才慌了神,Nick如在身邊的話我應該會思量著用盡各種酷刑逼 其招供而後繼續摧殘^-^。當時剛用上Win7也不久,下了UpdateLayeredWindow( 當然沒有加上NtUser)也沒斷到,還在想是不是邪惡的微軟又搞了什麼內部API ,於是查了各種Vista的資料,特別是Windows Vista Developer Story都快被我 翻到爛,也沒看出個所以來。

直到不久前閱讀了由Dflying Chen翻譯的 Windows Vista for Developers——第三部分:桌面窗口管理器(首先感謝翻譯,這裡提 個小意見翻譯的時候能不能把圖片Down下來重貼,如今原文沒了,圖片也沒了) ,回想起Lester 博文 Part III: Non-Rectangular Window in WPF (use of Thumb)中的sParams.UsesPerPixelOpacity = true;一句讓我堅定了 UpdateLayeredWindow的信心,抱著試試看的心理(感覺在打啥廣告)用了 NtUserUpdateLayeredWindow來下,居然成功了,那個高興啊有種喜極而泣的沖 動,要知道這問題可困擾了我半年多,真是好奇心害死貓。回頭想想Nick也在後 面說了以後會在操作系統裡加支持的,畢竟對每個象素點作變化需求還是有的, 說到這裡還要再強調下,我的這個是在Win7下試驗的,因為有Win7的DWM (Desktop Window Manager)的支持,不知道XP會不會不同,所以你的操作系統是 XP下不到的話換個NtUserSetWindowRgn試試。

為什麼要說這些呢?,因 為它讓讓我知道了一直努力下去上帝也是會開眼的,挫折總會有的,彎路也會有 的,怎麼越過?有時候只需要再堅持下,這樣如果我碰到 WPF Window的 AllowsTransparency=True導致WebBrowser不可視臨時解決方法 我想我用 SetWindowRgn也會臉不紅,心不跳。

窗體的結構信息得到落實,窗體的 結構信息告訴操作系統是為了啥?為了讓操作系統正確的把各種消息發送給我們 ,很明顯要使用GetMessage這個API來指定消息接收入口,那WPF的窗體又是怎樣 使用GetMessage的呢?

我們把運行中的程序斷下,並檢查CLR堆棧很容易 看到如下信息:

0:000> !clrstack 
OS Thread Id:  0x24f0 (0)
ESP       EIP
002fec98 778764f4  [NDirectMethodFrameStandalone: 002fec98]  MS.Win32.UnsafeNativeMethods.IntGetMessageW(System.Windows.Interop.MSG  ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
002fecb4 69c28e95 MS.Win32.UnsafeNativeMethods.GetMessageW (System.Windows.Interop.MSG ByRef,  System.Runtime.InteropServices.HandleRef, Int32, Int32)
002feccc 69c23d88 System.Windows.Threading.Dispatcher.GetMessage (System.Windows.Interop.MSG ByRef, IntPtr, Int32, Int32)
002fed08 69c23c6b  System.Windows.Threading.Dispatcher.PushFrameImpl (System.Windows.Threading.DispatcherFrame)
002fed58 69c23379  System.Windows.Threading.Dispatcher.PushFrame (System.Windows.Threading.DispatcherFrame)
002fed64 69c2331c  System.Windows.Threading.Dispatcher.Run()
002fed70 5aece37e  System.Windows.Application.RunDispatcher(System.Object)
002fed7c  5aecd67f System.Windows.Application.RunInternal (System.Windows.Window)
002feda0 5aeb49d6  System.Windows.Application.Run(System.Windows.Window)
002fedb0  5aeb4999 System.Windows.Application.Run()
002fedbc 005e0096  TopControl.App.Main()
002fefe4 6a9c1b6c [GCFrame: 002fefe4]

其實這裡的很多信息我們在CreateWindow中已經看到過,說明他 們是在同一個線程中,我們注意從 System.Windows.Application.RunDispatcher(System.Object)這句開始起了變 化,打開 Reflector一直看,會發現PushFrameImpl最有料,GetMessage方法也 在其中,他壓入的frame用 frame.Continue來執行,如何退出消息循環?執行 Dispatcher.ExitAllFrames();這裡GetMessage是一個內部函數,說來慚愧之前 都看到這層就忽略了直到寫這篇文章的時候才打開看了下,發現他居然不是直接 調用的win32 API而是先判斷有沒有Com服務,這讓我傻了眼,不知道咋回事了, 不過幸好周大哥在,問了下才恍然,是為了調用Text Services Framework, 其 實認真看自己也能明白的,不過確實說明了心態,遇事要鎮靜坦然處之,這是我 還要向各位高手學習的。Text Services Framework這塊我也不懂,要了解的自 己google吧 ^-^

//<SecurityNote>
// Critical -  as this calls critical methods (GetMessage, TranslateMessage,  DispatchMessage).
// TreatAsSafe - as the critical method  is not leaked out, and not controlled by external  inputs.
//</SecurityNote>
[SecurityCritical,  SecurityTreatAsSafe ]
private void PushFrameImpl (DispatcherFrame frame)
{
  SynchronizationContext  oldSyncContext = null;
  SynchronizationContext  newSyncContext = null;
  MSG msg = new MSG();

  _frameDepth++;
  try
  {
    // Change  the CLR SynchronizationContext to be compatable with our  Dispatcher.
    oldSyncContext =  SynchronizationContext.Current;
    newSyncContext = new  DispatcherSynchronizationContext(this);
     SynchronizationContext.SetSynchronizationContext(newSyncContext);

    try
    {
      while (frame.Continue)
      {
        if (! GetMessage(ref msg, IntPtr.Zero, 0, 0))
           break;

        TranslateAndDispatchMessage(ref  msg);
      }

      // If this was the  last frame to exit after a quit, we
      // can  now dispose the dispatcher.
      if(_frameDepth ==  1)
      {
        if(_hasShutdownStarted)
        {
          ShutdownImpl();
         }
      }
    }
     finally
    {
      // Restore the old  SynchronizationContext.
       SynchronizationContext.SetSynchronizationContext(oldSyncContext);
    }
  }
  finally
  {
     _frameDepth--;
    if(_frameDepth == 0)
    {
      // We have exited all frames.
       _exitAllFrames = false;
    }
  }
}

這份代碼是Reference Source中拿過來的,其他都沒什麼好說的 了,主要是他把CLR中的SynchronizationContext 換成了 DispatcherSynchronizationContext,沒了?沒了。

!!!本來MS想把 事情簡單化的,這要從Winform說起,SynchronizationContext 是抽象類,所以 具體的實現是WindowsFormsSynchronizationContext,他做了什麼?或者說他有 什麼用,其實這都是分層的罪,分層講究前後端分開,後端的類不知道前端的具 體的實現,那麼自然就不能知道前端的具體控件,如果在後端創建了一個線程, 在線程中要把數據給UI線程,怎麼辦?Control.Invoke?NO! 分層是不允許知道 具體控件的,那怎麼辦?涼拌?好吧,分的清楚其實也是有個人知道聯系的,這 個聯系人就是SynchronizationContext ,當Application創建的時候,微軟把當 前的控件(Form)放入,然後就可以了,我們後端就就可以使用 SynchronizationContext.Current來得到UI線程,作 SynchronizationContext.Post就相當於做了 Control.BeginInvoke。Winform和 WPF的傳遞不同,為了兼容所以做了這個動作,當然WPF的後端類也可以用這個來 操作,統一嘛,比Application.Current.Dispatcher總歸要好看些,用 Dispather.CurrentDispatcher只能拿到當前線程的。

換了 DispatcherSynchronizationContext自然是有原因的,那什麼原因呢?用的話主 要是Post方法不同,WPF他是為了實現自己的消息隊列用了BeginInvoke (DispatcherPriority.Normal)來實現,BeginInvoke使用對我們一般來說,無非 就是以下這個樣子

this.Dispatcher.BeginInvoke(((Action) delegate()
{
  //Do Method
}),
System.Windows.Threading.DispatcherPriority.Normal);

在 這點上,我感覺微軟做的不太好,可又限於自身功力說不上來,從我的角度和用 法來看BeginInvoke(Invoke內部其實也是 BeginInvoke)的作用就是用於主線程 ,就是UI線程用的,可他或許為了統一還是為了別的什麼其他的目的,為每個線 程都分配了 Dispatcher,這裡的配分是指你用過類似 Dispather.CurrentDispatcher的用法後創建的,為什麼要調用這個方法後才創 建?我認為他區分不了UI線程了普通後台線程,用了寧可錯殺1000也不放過1個 的做法;並在Dispatcher初始化的時候新建了一個 Message-Only Window,這個 純消息窗口顯然只是為了接受和發送消息消息的,怎麼發送?BeginInvoke負責 發送信息;怎麼接受?他Hook了一個函數(WndProcHook)進行接受;它怎麼知 道消息的類別?他自己定義了一個消息類別進行判斷:

private  static int _msgProcessQueue =  UnsafeNativeMethods.RegisterWindowMessage ("DispatcherProcessQueue");

等等,BeginInvoke為什麼要發信 息給自己接受?這是為了維護DispatcherPriority,他先把數據存成 DispatcherOperation放到一個PriorityQueue<T>結構體中,再根據一定 的算法取出相對權限最大的那個,具體算法?還是自己看看吧,其實我也沒細看 ,然後判斷取出的這個DispatcherPriority屬於哪個區域,區域?他把 DispatcherPriority分為4個區,無效區(這個概念是我自己加的)、空閒區,後 台區,前台區:

public enum DispatcherPriority
{
  Invalid = -1,
  Inactive = 0,

   //IdlePriorityRange
  SystemIdle = 1,
   ApplicationIdle = 2,
  ContextIdle = 3,

   //BackgroundPriorityRange
  Background = 4,
  Input  = 5,

  // ForegroundPriorityRange
  Loaded =  6,
  Render = 7,
  DataBind = 8,
  Normal  = 9,
  Send = 10
}

然後根據每個區的標准看 看,設置標志位(剛開始為0或1),判斷是否到了用的時候了(標志為是否加到1 或2),到了則返回執行,如果沒有用到,先改變標志位(加1),之後設置一個 Timer(SetTimer),下次線程切換時再執行,Timer時間到的時候操作系統會發送 (PostMessage)WM_TIMER消息給Message-only window,然後在它的Hook函數 (WndProcHook)中執行——繼續判斷取出隊列判斷直到運行我們傳入 的函數。

這裡要注意發送和接收消息都在同一個線程只是時機不同, 這 個過程對於UI線程肯定是有意義的,可對於非UI線程,似乎意義不大吧?還請各 位高手指點;而且區域裡的每項目的作用是什麼?為什麼設置個Timer就會有如 此效果也還望高人指點。

這一塊更詳細的內容請參閱周大哥的一站式 WPF--線程模型和Dispatcher ,消息循環處理請看 Win32 和 WPF 之間共享消息 循環。

我Windbg也是新手,學習Windbg主要通過以下幾個博客

http://blog.csdn.net/eparg

http://www.cnblogs.com/juqiang

http://www.titilima.cn/

三.圖像的顯示

暫留,圖像縮 放,API,矢量圖形,不規則圖形。

四.控件拖動

說了這麼多關於 窗體的,我們的拖動當然是要跨窗體的,跨窗體還需有個箭頭跟著,那這個箭頭 該如何畫呢?能跨窗體的必然也是窗體,只需要把窗體大小設置為屏幕大小,屏 幕大小?那不是會把東西給全部遮住?其實這裡就用到了 NtUserUpdateLayeredWindow來計算不規則圖形,畢竟箭頭只是一小塊。鼠標可 以跨窗體那麼消息就要監聽全局的,就是要 用SetWindowHookEX做個全局鉤子。

首先我們來回顧下Windows下的拖放操作:

對於拖拽方(Drag), 托拽源

1.用戶用鼠標拖拽了一個對象後,告知操作系統是在拖拽而不是 在簡單的移動鼠標,這需要調用ole32.dll的DoDragDrop(dataObject, dropSource, okEffect, effects)函數來實現,dropSource要注冊接口。

2.拖動過程中需要改變鼠標?其實操作系統在回調拖拽方,怎麼回調? 其實需要拖動的對象都需要繼承一些COM接口.GiveFeedback便是為了實現拖動事 件源能夠修改鼠標指針的外觀。

3.有時在拖動過程中發現鼠標或鍵盤狀 態改變了,比如用戶按了ESC,或是特殊快捷鍵按鈕需要取消,怎麼來判斷和中 斷這個操作,操作系統通過QueryContinueDrag接口操作。

[ComImport, Guid("00000121-0000-0000-C000-000000000046"),  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public  interface IOleDropSource
{
  [PreserveSig]
  int  OleQueryContinueDrag(int fEscapePressed, [In, MarshalAs (UnmanagedType.U4)] int grfKeyState);
  [PreserveSig]
   int OleGiveFeedback([In, MarshalAs(UnmanagedType.U4)] int  dwEffect);
}

對於目的地(Drop)方

1.告訴操作系統 該窗體是可以拖放的,調用ole32.dll的RegisterDragDrop(hwnd, dropTarget) ,其中dropTarget要注冊接口。

2、當有物體拖拽進了 hwnd 所在的區域 時,操作系統回調DragEnter接口。

3、當物體在 hwnd 所在區域內滑動 時,操作系統回調DragOver接口。

4、當物體拖拽出 hwnd 所在區域時, 操作系統回調DragLeave接口。

5、當拖拽的物體放下時(Mouse Down), 操作系統回調Drop接口。

[ComImport, InterfaceType (ComInterfaceType.InterfaceIsIUnknown), Guid("00000122-0000-0000- C000-000000000046")]
public interface IOleDropTarget
{
  [PreserveSig]
  int OleDragEnter([In, MarshalAs (UnmanagedType.Interface)] object pDataObj, [In, MarshalAs (UnmanagedType.U4)] int grfKeyState, [In, MarshalAs (UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
  [PreserveSig]
  int OleDragOver([In, MarshalAs (UnmanagedType.U4)] int grfKeyState, [In, MarshalAs (UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
  [PreserveSig]
  int OleDragLeave();
   [PreserveSig]
  int OleDrop([In, MarshalAs (UnmanagedType.Interface)] object pDataObj, [In, MarshalAs (UnmanagedType.U4)] int grfKeyState, [In, MarshalAs (UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
}

對WPF而言,很多控件我們已經知道了其實是在同一個window Handle中的,所以在Hwndsource初始化的時候便使用了RegisterDragDrop來進行 注冊,當鼠標進入區域時根據HitTest的結果來判斷是否選中控件,然後再根據 控件的AllowDrop來判斷是否可以拖動,AllowDrop為True則引發相關事件。當然 WPF還定義了 DragDrop類更加簡單的來調用操作。可以參看Drag and Drop Overview。

同一個WPF程序內的拖拉,請參看我的上篇文章“WPF自 定義控件 —— 裝飾器”中的各種拖拽示例。

先做一個類似透明窗體的控件MouseTip,對於拖拽跟隨的物體,在拖拽 的時候只要計算拖拽源的位置和拖拽目標的位置便可以計算出他的距離和角度, 其實就是計算出他的向量。然後使用MouseTip,MouseTip實際上也是在window handle的封裝基礎上,那麼就可以用win32 API SetWindowPos來移動。設計一個 結構,使得MouseTip的長度為兩者的距離,運用Binding這樣XAML的設計者就可 以搭配出任意的圖形。比如示例的就是用一個Rectangle來幫定計算出的長度, 然後對整個UI運用RotateTransform來綁定角度,在這個當中您或許會看到我沒 有把圖標的長度減掉而是用了Converter來處理,我這麼做的目的是因為我認為 沒有必要拘泥於這種圖標箭頭的形式,做個瞄准儀鼠標換個降落傘的圖標,選定 確定後用導彈形式發射禮包過去也是可以的。

都說好,未免有失偏頗, 在我實際做的過程中確實碰到了很多“意外”,最後的實現和我預期 的做法差別還是挺大的,當然這就不能避免BUG的存在。下面是我做的過程中發 現的一些BUG和設計方面處理不好的地方:

1.鼠標狀態不定,這個是我計 算偏離量的時候沒有計算好,導致操作系統看到的是拖拉的窗體。

2.有 時候比較卡,甚至會拖不動,畢竟操作系統計算不規則圖形也是花時間的。

3.箭頭和下面的方塊沒有完全處理好,仔細看會發現。

4.拖拉的 圖片我或是說東西得到我用的是MouseDown事件參數中的e.OriginalSource,然 後做VisualBrush,可以說是通過點擊測試拿到的,這個壞處是如果圖片下面還有 文字說明,那點擊的時候可能只會拖動文字或圖片,因為點擊測試只能拿到一個 控件,如果你采用 itemsControl.ItemContainerGenerator.ContainerFromItem ,你會發現當選中的藍色背景也會一起拿過來,最安全的方法應該是把拖動的東 西模板傳進去,這樣還能控制在拖動的時候樣子,當然這樣就要多做些屬性支持 。

5.拖動過程中可能會崩潰,因為我沒有做try,沒有做類型檢測。

6.傳遞的類一定要加序列化標簽Serializable,而且最好重載你的 Equals方法以免不幸,本來想拖動的過程中用 FindWindow來看如果是同一窗體 或線程則直接拖拉,不需要序列化,不過這個沒有做,有需要的朋友可以自行添 加。

五.小結

其實這篇東西寫的比較久,一個是自己懶散,一個是有些內 容開始也只是了解大概,真正去了解才知道博大精深,而且拉的面太廣,比如 WPF到底如何來呈現RootVisual的具體的步驟,這些都要落在周大哥身上了^-^。 如果上文說的太水的地方還請見諒,有什麼不對的地方歡迎批評,我一直認為只 有認識到自己的不足和狹隘錯誤才能進步。在這裡還要感謝智者千慮,如果不是 他的題目和才學,我想我是不會認真去做這個東西,也不會從中發現樂趣。原以 為小結才是最重要,在做的時候感概很多,現在居然啥都說不上來。哎~~~

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved