利用我们的新工具将 Spy++ 的功能传送给 Windows 窗体

很多开发人员都使用 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/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/12639172/viewspace-157117/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值