利用我們的新工具將 Spy++ 的功能傳送給 Windows 窗體

iDotNetSpace發表於2008-01-22

很多開發人員都使用 Visual Studio® 提供的 Spy++ 工具。使用 Spy++,您可以瞭解一個執行中的應用程式的視窗布局或識別一個導致錯誤的特定視窗訊息。然而,當您建立一個基於 Microsoft® .NET Framework 的應用程式時,Spy++ 變得不太管用了,原因就在於由 Spy++ 截獲的視窗訊息和類不能與開發人員使用甚至看到的內容相對應。開發人員真正想看到的是託管的事件和屬性值。

本文描述如何使用一個名為 ManagedSpy 的新實用工具及其關聯的 ManagedSpyLib 庫,它們均可從 MSDN®Magazine Web 站點下載。ManagedSpy 顯示託管控制元件、屬性和事件的方式與 Spy++ 顯示 Win32® 資訊(如視窗類、樣式和訊息)的方式類似。ManagedSpyLib 允許您以程式設計方式訪問另一個程式中的 Windows® 窗體控制元件。您可以獲得並設定屬性以及在您自己的程式碼中的事件上同步。ManagedSpyLib 還能幫助您建立 Test Harness,並且可以執行視窗、訊息和事件日誌記錄。

監視您的UI


編寫客戶端應用程式時,在許多情況下傳統的偵錯程式不起作用。例如,如果您的錯誤涉及焦點或其他 UI 內容,將很難進行除錯,因為偵錯程式會修改斷點處的狀態。另一個難以除錯的問題是佈局。如果窗體有一個複雜、動態的佈局,則並不總能確定佈局邏輯是否多次呼叫。要除錯這些問題,通常必須對事件和訊息日誌記錄進行重新排序,以瞭解哪些輸入能滿足 UI。

如果有複雜的 UI,對視窗及其關聯的狀態有所瞭解是很有幫助的。例如,在偵錯程式中定位相關控制元件可能很困難。多數情況下,您必須假定某個偵錯程式變數就是在 UI 中看到的那個控制元件。

圖 1 顯示一個包含一些巢狀控制元件的對話方塊。該應用程式在右上方的文字框中有一個錯誤,儘管該示例的目的並不是真的要確定這個錯誤是什麼。不但能識別出紅色的文字框是哪個成員,並且能識別出相關控制元件的父層次和佈局是很有用的。


圖 1 有問題的對話方塊


ManagedSpy 對這種情況和其他情況有所幫助。它在您的基於 .NET 的客戶端應用程式中顯示一個控制元件的樹檢視。您可以選擇任意控制元件,獲取或設定其上的任意屬性。您還可以記錄一組經過篩選的、由控制元件引發的事件。不僅對於除錯很有幫助,這對您的控制元件相容性測試也有所幫助。您可以使用真實的應用程式和日誌事件,以確保為下一版本的控制元件保留事件的排序。

您第一次執行 ManagedSpy 時,它在視窗左側以樹狀檢視顯示程式列表,同時在右側顯示 PropertyGrid。您可以展開該程式,檢視其中的頂級視窗。

您選擇一個控制元件時,PropertyGrid 顯示該控制元件的屬性。您可以在此處檢查或更改屬性值。您應該注意到,只要自定義型別是可二進位制序列化的,就受支援(參閱 Basic Serialization)。

工具欄包含的命令用於以下操作:選擇已記錄到事件窗格中的事件,在建立了新視窗後重新整理 TreeView,啟動或停止將事件記錄到事件窗格,以及清除事件窗格。

對於圖 1 所示的對話方塊,ManagedSpy 顯示如圖 2 所示的資訊。從 ManagedSpy 可以看出,textBox1 是 SplitContainer (SplitContainer2) 的父級,後者又是 TableLayoutPanel (tableLayoutPanel1) 的父級。TableLayoutPanel 的父級是 TabControl,而後者位於另一個 SplitContainer 中。還要注意,ManagedSpy 告訴我 BackColor 是紅色的。


圖 2 在 ManagedSpy 中除錯控制元件


單擊 Events 選項卡,將在樹形檢視中顯示當前所選控制元件的屬性,如 MouseMove。要開始記錄事件,單擊 Start Logging 按鈕。輸出結果,如圖 3 所示。


圖 3 記錄事件


通常有很多滑鼠事件。您可以在被記錄之前通過單擊 Filter Events 按鈕篩選這些或其他事件,這將顯示一個對話方塊以便您指定要記錄哪些事件。該事件篩選器對話方塊列出 Type 控制元件的所有事件。派生類中宣告的任何事件都通過選擇 Custom Events 進行控制。

 

ManagedSpy 內幕


ManagedSpy 中的主方法名為 RefreshWindows。它的作用是用桌面上執行的所有程式和視窗填充 TreeView。首先,它清除 TreeView 並重新查詢系統中的所有頂級視窗:

   private void RefreshWindows() {
       this.treeWindow.BeginUpdate();
       this.treeWindow.Nodes.Clear();

       ControlProxy[] topWindows =
          Microsoft.ManagedSpy.
             ControlProxy.TopLevelWindows;
    ...

一旦 ManagedSpy 獲得一個頂級視窗的集合,它就列舉每個視窗,如果它是一個託管視窗,則將其新增到樹檢視中:

if (topWindows != null && topWindows.Length > 0) {
    foreach (ControlProxy cproxy in topWindows) {
        TreeNode procnode;

        //only showing managed windows
        if (cproxy.IsManaged) {

此處,ManagedSpy 使用的是 ManagedSpyLib 中定義的 ControlProxy 類。ControlProxy 表示在另一個程式中執行的視窗。如該視窗實際上是 System.Windows.Forms.Control,則 IsManaged 將為 true。由於 ManagedSpy 只能顯示基於 .NET Framework 的控制元件的資訊,因此它不顯示其他視窗型別。

現在,對於託管的每個頂級 ControlProxy,ManagedSpy 都能找到其擁有的程式。一旦程式在 TreeView 中有一個節點,ManagedSpy 就將其用作新的 ControlProxy 項的父級 TreeNode:

Process proc = cproxy.OwningProcess;
if (proc.Id != Process.GetCurrentProcess().Id) {
    procnode = treeWindow.Nodes[proc.Id.ToString()];
    if (procnode == null) {
        procnode = treeWindow.Nodes.Add(proc.Id.ToString(),
            proc.ProcessName + "  " + 
            proc.MainWindowTitle + 
            " [" + proc.Id.ToString() + "]");
        procnode.Tag = proc;
    }
    ...

此處,procnode 是所擁有程式的 TreeNode。它的標題是用 System.Diagnostics.Process 的資訊生成的。唯一不同的有趣之處就是 ManagedSpy 避免了通過本身來顯示視窗。

最後,ManagedSpy 在 procnode 下面新增另一個 TreeNode 以表示該視窗(參見圖 4)。ManagedSpy 使用 ControlProxy.GetComponentName 和 ControlProxy.GetClassName 作為 TreeNode 的標題。GetClassName 引用遠端控制元件的 System.Type,而不是 Spy++ 顯示的視窗類。

每當您選擇一個 TreeNode 時,ManagedSpy 就將該 TreeNode 的標記放在右側顯示的 PropertyGrid 中。這就是為遠端控制元件顯示屬性的方式。下列程式碼顯示 ManagedSpy 如何顯示它的 TreeView 及其所有屬性:

private void treeWindow_AfterSelect(object sender, TreeViewEventArgs e)
{
    this.propertyGrid.SelectedObject = this.treeWindow.SelectedNode.Tag;
    this.toolStripStatusLabel1.Text = treeWindow.SelectedNode.Text;
    StopLogging();
    this.eventGrid.Rows.Clear();
    StartLogging();
}

我不會逐步說明如何記錄事件,但這並不比顯示屬性複雜。ManagedSpy 訂閱所選 ControlProxy 的 EventFired 事件。激發該事件時,向 DataGridView 控制元件新新增一行以顯示資料(DataGridView 控制元件是 .NET Framework 2.0 中新增的)。

 

使用ManagedSpyLib


ManagedSpy 寫到一個名為 ManagedSpyLib 的託管 C++ 庫的頂部。ManagedSpyLib 的目的是允許在另一個程式中通過程式設計方式訪問基於 .NET Framework 的視窗。ManagedSpyLib 公開一個名為 ControlProxy 的類,該類表示另一個程式中的一個控制元件。儘管它不是一個實際的控制元件,但您可以訪問它所表示的控制元件的所有屬性和事件。

ManagedSpyLib 通過使用記憶體對映檔案在正在偵探和已偵探的程式之間傳輸資料來起作用。為了使之正常工作,程式間傳輸的所有資料必須是可二進位制序列化的。程式間通訊使用的主要機制是自定義的視窗訊息和 SetWindowsHookEx。這確保了目的碼執行在您擁有需要查詢的視窗的執行緒上。這很重要,因為有許多操作僅在從某個視窗所擁有的執行緒進行呼叫時才起作用。

建立 ControlProxy 的途徑有兩種。第一種途徑是使用 ControlProxy.FromHandle 將代表目標控制元件的 HWND 的 IntPtr 傳入方法中。這將返回一個目標控制元件的 ControlProxy。通常可以使用 Win32 方法(如 EnumWindows)或使用諸如 Spy++ 這樣的應用程式找到某個視窗的 HWND。您還可以通過訪問某個控制元件的 Handle 屬性獲得 HWND。

第二種途徑是使用 ontrolProxy.TopLevelWindows。呼叫該靜態方法以獲得一個 ControlProxy 類的陣列。您將為桌面上每個頂級視窗獲得一個 ControlProxy。然而,不是所有這些視窗均可由託管控制元件表示。為了確定這一點,請檢查 ControlProxy 的屬性,看看它是否確實是一個託管視窗。檢視其後的 Properties 部分以瞭解有關可檢索內容的更多資訊。圖 5 提供一個列出每個程式的頂級視窗數的示例。

 

訪問底層控制元件的屬性


使用 ControlProxy 的主要原因之一是從另一個程式中的某個控制元件訪問屬性。(圖 6 中對這些屬性進行了說明。)要訪問這些屬性,只需建立一個使用 ControlProxy.FromHandle 或 ControlProxy.TopLevelWindows 的 ControlProxy,然後呼叫這兩個方法以訪問這些值。呼叫 GetValue 可從已偵探程式中的底層控制元件獲得一個屬性值。例如,您可以用以下程式碼呼叫 GetValue 以獲得 Size 屬性:

controlproxy.GetValue("Size")

呼叫 SetValue 可以更改正在觀察的程式中底層控制元件中的某個屬性值。例如,以下程式碼將背景色設為藍色:

controlproxy.SetValue("BackColor", "Color.Blue")

要證實 ManagedSpyLib 對於跨程式編輯屬性的有效性,我將建立一個簡單的 C# 應用程式。我新增一個名為 textBox1 的文字框和一個名為 button1 的按鈕。然後,我雙擊該按鈕建立 button1_Click 處理程式,然後新增一些程式碼,包括圖 7 所示的程式碼選段。

如果我執行該應用程式的兩個例項,在一個例項的 textBox1 中鍵入一些文字,然後單擊 button1,該例項將查詢該應用程式所有其他正在執行的例項,並更改它們的文字框字串以進行匹配,如圖 8 所示。


圖 8 例項


您可以訂閱另一個程式中某個控制元件的事件,如 Click 或 MouseMove。訂閱事件是一個兩步驟的過程。首先,必須用事件名稱呼叫 SubscribeEvent,讓 ControlProxy 偵聽該事件。然後,訂閱名為 EventFired 的 ControlProxy 事件:

private void SubscribeMainWindowClick(ControlProxy proxy) 
{
    proxy.SubscribeEvent("Click");
    proxy.EventFired += new ControlProxyEventHandler(
        Program.ProxyEventFired);
}

void ProxyEventFired(object sender, ProxyEventArgs args) 
{
    System.Windows.Forms.MessageBox.Show(args.eventDescriptor.Name 
        + " event fired!");
}

請注意,一個 ControlProxy 結束後,您應該取消對所有以前訂閱事件的訂閱。

ManagedSpy 本身使用 ControlProxy 類檢索屬性值。例如,FlashCurrentWindow 突出顯示所選的視窗幾秒鐘。它還為其日誌記錄功能訂閱事件。

 

其他 ControlProxy方法


ControlProxy 中有一些其他方法值得研究一下。呼叫 SendMessage 方法可以將一條視窗訊息傳送到控制元件。如果要建立一個 Test Harness,這很有用。例如,您可以傳送 WM_CLICK 或 WM_KEYDOWN 訊息來模擬輸入。如果您希望以這種方式使用 ManagedSpyLib,您可對其進行修改,以便視窗掛鉤過程始終執行並讓它篩選每條視窗訊息(除了那些已程式設計的訊息)。這建立了一個禁用其他輸入的自動驅動程式。

PointToClient 和 PointToScreen 將螢幕座標轉換為客戶端座標。SetEventWindow 和 RaiseEvent 方法不能在使用者程式碼中使用。在內部使用它們以管理跨程式的事件。ICustomTypeDescriptor 使一個物件可以動態指定屬性和事件。ControlProxy 為 PropertyGrid 支援實現該介面。您可以可直接在使用者程式碼中呼叫這些方法,但通常不需要這樣做。要訪問屬性,請使用 GetValue 和 SetValue 方法。

使用窗體掛鉤

正如前面提到的,ManagedSpyLib 通過在程式之間傳輸資料發揮作用。視窗掛鉤是一種截獲視窗訊息(如 WM_SETTEXT)的方式。建立視窗掛鉤有兩種方法。SetWindowLong 允許您截獲同一程式中特定視窗上的視窗訊息。SetWindowsHookEx 允許多種訊息掛鉤,包括在當前桌面中所有程式的所有視窗掛鉤訊息的能力。

大多數使用本機程式碼的開發人員會將 SetWindowLong 當作子類化一個視窗的 Win32 函式。子類化一個視窗後,Windows 將所有傳送到指定視窗控制程式碼的 Win32 訊息傳送給您的回撥方法。這使您能修改或只檢查該訊息。

注意,SetWindowLong 要求您現在處於與子類化視窗的同一程式中。如果您希望進行這種型別的子類化,.NET Framework 通過提供了一個名為 System.Windows.Forms.NativeWindow 的類可輕鬆實現。此處,可能會問您兩個問題。

如果我想檢視視窗訊息並且我又和目標視窗不在相同的程式中,該怎麼辦?

如果 ManagedSpyLib 不再顯示託管的訊息,那麼掛鉤的視窗訊息如何與它關聯?

如果您想檢視視窗訊息並且我又與目標視窗不在相同的程式中,則您不能使用 SetWindowLong。您可以使用 SetWindowsHookEx,但有一個注意事項:對於大多數掛鉤型別,您的回撥方法必須作為 dllexport 公開。這意味著,您必須在本機 DLL 或在一種混合模式 C++ DLL 中編寫回撥。ManagedSpyLib 正是因為這個原因而使用託管的 C++ 程式碼編寫的。它使用 Visual Studio 2005 中的 C++/CLI 支援。

ManagedSpyLib 使用視窗訊息掛鉤有兩個原因。要在目標程式中接收請求,它必須能在該程式中執行程式碼。SetWindowsHookEx 使您能這樣做。ManagedSpyLib 還使用自定義視窗訊息在程式之間傳送和接收資料。這意味著,它的視窗掛鉤必須在傳送請求時(如檢索另一個程式中某個控制元件的 BackColor)啟用。

 

使用記憶體對映檔案


但是,ManagedSpyLib 到底是怎樣跨程式傳輸資料的呢?當然,它可以傳送一個自定義的視窗訊息(如 WM_SETMGDPROPERTY)來設定一個屬性值。但例如,如果屬性是 BackColor,它如何傳送 BackColor.Red 呢?視窗訊息只用兩個 DWORD 作為引數。

答案是:它使用一個記憶體對映檔案。這不是磁碟上實際存在的檔案。它是在多個程式之間共享的記憶體的一個區域,。您將該記憶體對映到自己的程式地址空間。然而,這樣做的結果是該共享區域有一個不同的起始地址。因此,您必須謹慎地那裡儲存資料 - 沒有指標!同樣,您不能在記憶體對映檔案中有任何託管物件,因為公共語言執行庫 (CLR) 不能管理該記憶體。這意味著您只能儲存原始位元組資料。

由於這個原因,ManagedSpyLib 只儲存二進位制序列化的資料。這就是屬性(和 EventArgs)必須是可序列化的才能受到 ManagedSpyLib 支援的原因。ManagedSpyLib 使用 CAtlFileMapping 為每個事務建立一個記憶體對映檔案。

ManagedSpyLib 計算二進位制流的大小,建立適當大小的記憶體對映檔案,然後將資料複製到其中。既然您對 ManagedSpyLib 如何使用視窗掛鉤安裝其本身和記憶體對映檔案以傳送資料有所瞭解,那麼讓我們進一步看看如何建立和維護 ControlProxy 類。

 

建立 ControlProxy 和控制程式碼重建

圖 9 說明如何建立一個 ControlProxy (紅色箭頭),以及在其控制程式碼發生變化時如何維護它(藍色箭頭)。使用者最初呼叫 ControlProxy.FromHandle 或 ControlProxy.TopLevelWindows。TopLevelWindows 將呼叫 EnumWindows,然後在每個列舉視窗上呼叫 FromHandle。因此,您可將 TopLevelWindows 僅僅看作一個更復雜的 FromHandle 呼叫。


圖 9 建立一個 ControlProxy


ManagedSpyLib 為擁有目標視窗的執行緒開啟一個視窗掛鉤。然後,ManagedSpyLib 將一條 WM_GETPROXY 訊息傳送到目標視窗(該訊息處理後,視窗掛鉤將關閉)。在接收端,接收訊息,同時命令庫呼叫 Control.FromHandle 以獲得已偵探程式中正在執行的託管控制元件。使用該控制元件,ManagedSpyLib 建立一個新的 ControlProxy。該 ControlProxy 儲存控制元件的 Type.FullName 以及當前 AppDomain 中載入的所有程式集的 Assembly.Location。

ControlProxy 訂閱控制元件的 HandleCreated 和 HandleDestroyed 事件。稍後,它使用這來維持適當的視窗控制程式碼狀態。ControlProxy 儲存在已偵探程式的 ProxyCache 中,並使用二進位制序列化傳送回偵探程式。偵探程式反序列化 ControlProxy 並將其新增到它的本地 ProxyCache。然後,它將 ControlProxy 返回給使用者。

當已偵探的程式為某個控制元件重建控制程式碼時,ManagedSpyLib 保持適當的狀態。從已偵探的程式中的 ControlProxy 接收 HandleDestroyed。ControlProxy 檢查 Control.RecreatingHandle 以檢視該控制元件是否正在執行控制程式碼重建。如果該控制程式碼正在進行重建,則 ControlProxy 等待相應的 HandleCreated。它更新本地 ProxyCache,然後將 WM_HANDLECHANGED 傳送到偵探程式的 EventWindow。通過查詢舊的視窗控制程式碼,偵探程式從 ProxyCache 定位正確的 ControlProxy。然後,它更新 ControlProxy 和偵探程式的 ProxyCache。

圖 10 顯示 ControlProxy 如何獲得屬性(紅色箭頭)和接收事件(藍色箭頭)。當您通過 ControlProxy.GetValue(propertyName) 獲得一個屬性值時,ManagedSpyLib 執行以下步驟。首先,偵探程式通過屬性的名稱調入 ControlProxy.GetValue。ManagedSpyLib 為擁有目標視窗的執行緒開啟一個視窗掛鉤。該視窗掛鉤將在訊息處理後關閉。ManagedSpyLib 儲存屬性的名稱以獲得一個記憶體對映檔案(呼叫程式的記憶體儲存的 Parameters 部分)。它使用二進位制序列化完成該操作。


圖 10 獲得代理並接收事件


ManagedSpyLib 將 WM_SETMGDPROPERTY 訊息傳送到目標視窗。將在被偵探的程式內呼叫視窗掛鉤過程 (MessageHookProc) 來處理視窗訊息。然後,MessageHookProc 將處理命令並使用反射以獲得返回值。它將返回值儲存在呼叫程式的記憶體儲存中。SendMessage 完成後,偵探程式從它的記憶體儲存中反序列化返回值。它將 WM_RELEASEMEM 傳送到相同的目標視窗,以通知它可以釋放對對映檔案的引用。最後,它返回值。

訂閱和獲得事件是類似的。偵探程式調入 SubscribeEvent,它將以下內容儲存在偵探程式記憶體儲存的 Parameters 部分中:EventWindow 控制程式碼、要訂閱事件的名稱,以及該視窗中事件唯一的事件程式碼(通常是事件列表中事件的索引)。

SubscribeEvent 將 WM_SUBSCRIBEEVENT 傳送到目標控制元件。在已偵探程式中接收到 WM_SUBSCRIBEEVENT 後,ManagedSpyLib 建立一個 EventRegister 物件,該物件訂閱事件並跟蹤它訂閱的事件。一個事件被激發後,EventRegister 用源視窗將一條 WM_EVENTFIRED 訊息傳送到 Event 視窗,事件程式碼和 EventArgs 儲存在已偵探程式的記憶體儲存中。

偵探程式處理 WM_EVENTFIRED,分析源視窗、事件程式碼和 EventArgs,並利用正確的事件和 EventArg 資訊呼叫正確 ControlProxy 的 RaiseEvent。RaiseEvent 引發 ControlProxy 上的 EventFired 事件。

 

ManagedSpyLib 用於單元測試

使用 ManagedSpyLib,您無需公開應用程式的掛鉤即可進行測試。要解釋它,我建立了一個名為 Multiply 的新的基於 C# Windows 窗體的應用程式。我新增了三個文字框和一個按鈕,然後雙擊按鈕將下列程式碼新增它的 Click 事件中:

private void button1_Click(object sender, EventArgs e) 
{
    int n1 = Convert.ToInt32(this.textBox1.Text);
    int n2 = Convert.ToInt32(this.textBox2.Text);
    this.textBox3.Text = (n1 * n2).ToString();
}

該應用程式就是要計算兩個文字框的內容,並在第三個文字框中顯示結果。要點是建立一個使用這個簡單示例的單元測試應用程式。

對於下一步,我將一個新的基於 C# Windows 的應用程式新增到解決方案中並命名為 UnitTest。一個只是有著如圖 11 所示程式碼的單個按鈕的窗體。

當您執行該單元測試應用程式時,它會將第一個文字框中的值改為 5,將第二個文字框中的值改為 7。然後,它將一個 click(通過 mousedown 和 mouseup)傳送到該按鈕並檢視最終結果(在事件回撥中設定)。

 

小結


ManagedSpy 是一個診斷工具,類似於 Spy++。它顯示託管的屬性,允許您記錄事件,並且還是一個使用 ManagedSpyLib 的一個優秀示例。ManagedSpyLib 引入了一個名為 ControlProxy 的類。ControlProxy 表示另一個程式中的 System.Windows.Forms.Control。ControlProxy 允許您獲得或設定屬性和訂閱事件,就像您就在目標程式內部執行一樣。使用 ManagedSpyLib 可進行自動測試、為相容性進行事件記錄、跨程式通訊或進行白盒測試。

Benjamin Wulfe 已在 Microsoft 工作了六年多了,致力於 Visual Studio,Visual Studio 是 .NET Framework 和 .NET Compact Framework 以及一些框架類(如 ComboBox 和 NativeWindow)的 Windows 窗體設計器。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-157117/,如需轉載,請註明出處,否則將追究法律責任。

相關文章