将Spy++的强大功能传递到Windows窗体
借助我们的新工具,将Spy++的强大功能传递到Windows窗体
|
本文讨论了:
|
本文使用了以下技术: 。NET框架2.0 |
内容
窥探你的用户界面
内部管理间谍
使用ManagedSpyLib
访问基础控件属性
其他控制代理方法
使用窗户挂钩
使用内存映射文件
创建控制代理和句柄重新创建
单元测试的ManagedSpyLib
结论
许多开发人员使用Visual Studio提供的Spy++工具。使用Spy++,您可以了解正在运行的应用程序的窗口布局,或者识别导致bug的某个窗口消息。然而,当你创建一个微软。在基于. NET Framework的应用程序中,Spy++变得不那么有用了,因为Spy++截获的窗口消息和类与开发人员使用甚至看到的任何东西都不对应。开发人员真正想看到的是托管事件和属性值。
本文描述了如何使用一个名为ManagedSpy的新实用程序及其相关的库ManagedSpyLib,这两个库都可以从MSDN®杂志网站。与Spy++显示Win32信息(如窗口类、样式和消息)的方式类似,ManagedSpy显示托管控件、属性和事件。ManagedSpyLib允许您在另一个进程中以编程方式访问Windows窗体控件。您可以在自己的代码中获取和设置属性并同步事件。ManagedSpyLib还可以帮助您构建测试工具,并可以执行窗口、消息和事件日志记录。
窥探你的用户界面
当编写客户端应用程序时,有许多情况下传统的调试器没有用。例如,如果您的bug涉及焦点或其他UI方面,则很难调试,因为每当您遇到断点时,调试器都会修改这种状态。另一个难以调试的问题是布局。如果你的表单有一个复杂的动态布局,你的布局逻辑是否被多次调用并不总是显而易见的。为了调试这些问题,您通常必须求助于事件或消息日志来了解什么输入被提供给了您的UI。
对于复杂的UI,拥有一个窗口和相关状态的视图是很有用的。例如,很难在调试器中找到相关的控件对象。大多数时候,您必须猜测调试器变量就是您在UI中看到的变量。
图1显示带有几个嵌套控件的对话框。这个应用程序在右上角的文本框中有一个bug,尽管对于这个例子来说,bug是什么并不重要。不仅要标识哪个成员是红色文本框,还要标识相关控件的父层次结构和布局,这将非常有用。

图1**问题对话框**
ManagedSpy可以在这种情况和其他情况下提供帮助。它在您的中显示控件的树视图。基于. NET的客户端应用程序。您可以选择任何控件并获取或设置其上的任何属性。您还可以记录控件引发的一组经过筛选的事件。虽然这对于调试来说很棒,但也有助于控件的兼容性测试。您可以使用真实的应用程序和日志事件来确保为下一版本的控件保留事件顺序。
当您第一次运行ManagedSpy时,它会在窗口左侧的treeview中显示一个进程列表,并在右侧显示一个PropertyGrid。您可以展开该流程以查看该流程中的顶级窗口。
当您选择一个控件时,PropertyGrid显示该控件的属性。您可以在此检查或更改属性值。您应该注意,只要自定义类型是二进制可序列化的,就支持自定义类型(请参见基本序列化).
工具栏包含用于选择将哪些事件记录到事件窗格、在创建新窗口时刷新树视图、开始或停止将事件记录到事件窗格以及清除事件窗格的命令。
对于中所示的对话框图1,ManagedSpy显示中所示的信息图2。根据ManagedSpy,textBox1是SplitContainer (SplitContainer2)的父级,SplitContainer又是TableLayoutPanel(TableLayoutPanel 1)的父级。TableLayoutPanel的父级是TabControl,它位于另一个SplitContainer中。还要注意,ManagedSpy告诉我背景色是红色的。

图2**在ManagedSpy中调试控件**
单击Events选项卡将在treeview中的当前选定控件上显示诸如MouseMove之类的事件。要开始记录事件,请单击开始记录按钮。输出将如所示图3.

图3**记录事件**
通常会有许多鼠标事件。您可以通过单击“过滤事件”按钮来过滤这些或其他事件,这将显示一个对话框,您可以在其中指定要记录的事件。“事件过滤器”对话框列出了类型控件中的所有事件。派生类中声明的任何事件都是通过自定义事件选择来控制的。
内部管理间谍
ManagedSpy中的主要方法称为RefreshWindows。它的工作是用桌面上运行的所有进程和窗口填充树形视图。它做的第一件事是清除树形视图并重新查询系统中所有的顶层窗口:
private void RefreshWindows() {
this.treeWindow.BeginUpdate();
this.treeWindow.Nodes.Clear();
ControlProxy[] topWindows = Microsoft.ManagedSpy. ControlProxy.TopLevelWindows; ...
一旦有了顶级窗口的集合,ManagedSpy将枚举每个窗口,如果是托管窗口,则将它添加到treeview:
if (topWindows != null && topWindows.Length > 0) { foreach (ControlProxy cproxy in topWindows) { TreeNode procnode; //only showing managed windows if (cproxy.IsManaged) {
这里,ManagedSpy使用ManagedSpyLib中定义的ControlProxy类。ControlProxy表示在另一个进程中运行的窗口。如果窗户实际上是一个系统。则IsManaged将为true。因为ManagedSpy只能显示。NET框架的控件,它不显示其他窗口类型。
现在,对于每个托管的顶级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在procnode下添加另一个TreeNode来表示窗口(请参见图4).ManagedSpy使用ControlProxy。GetComponentName和ControlProxy。GetClassName作为TreeNode的标题。GetClassName是指系统。遥控器的类型—不是Spy++显示的窗口类。
图4添加一个窗口节点
string name = String.IsNullOrEmpty(cproxy.GetComponentName()) ? "<noname>" : cproxy.GetComponentName();
TreeNode node = procnode.Nodes.Add(cproxy.Handle.ToString(), name + " [" + cproxy.GetClassName() + "]");
node.Tag = cproxy; ... if (treeWindow.Nodes.Count == 0)
{
treeWindow.Nodes.Add("No managed processes running.");
treeWindow.Nodes.Add("Select View->Refresh.");
}
this.treeWindow.EndUpdate();
每当选择一个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框架2.0)。
使用ManagedSpyLib
ManagedSpy是在名为ManagedSpyLib的托管C++库的基础上编写的。ManagedSpyLib的目的是允许以编程方式访问。另一个进程中基于. NET Framework的windows。ManagedSpyLib公开了一个名为ControlProxy的类,该类表示另一个进程中的控件。虽然它不是一个实际的控件,但您可以访问它所代表的控件的所有属性和事件。
ManagedSpyLib通过使用内存映射文件在间谍和被间谍进程之间传输数据来工作。为此,所有在进程间传输的数据必须是二进制可序列化的。用于进程间通信的主要机制是自定义窗口消息和SetWindowsHookEx。这确保了目标代码运行在拥有您需要查询的窗口的线程上。这一点很重要,因为有许多操作只有在从窗口的所属线程调用时才起作用。
创建ControlProxy有两种方法。第一种方法是使用ControlProxy。FromHandle,将表示目标控件的HWND的IntPtr传递给该方法。这将向您返回目标的ControlProxy。窗口的HWND通常可以使用Win32方法(如EnumWindows)或使用应用程序(如Spy++)找到。您也可以通过访问控件的Handle属性来获取HWND。
第二种方法是使用ControlProxy.TopLevelWindows。您将为桌面上的每个顶层窗口获得一个ControlProxy。但是,并非所有这些窗口都由托管控件表示。若要确定这一点,请检查ControlProxy的属性,查看它是否确实是托管窗口。有关可以检索的内容的更多信息,请参见下面的属性部分。图5提供一个示例,列出每个进程的顶级窗口数。
图5按进程列出窗口
using System;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;
using System.Collections.Generic;
using Microsoft.ManagedSpy;
class Program { static void Main(string[] args)
{
Dictionary<int, int> topWindowCounts = new Dictionary<int, int>();
foreach (ControlProxy proxy in ControlProxy.TopLevelWindows)
{
if (!topWindowCounts.ContainsKey(proxy.OwningProcess.Id))
{
topWindowCounts.Add(proxy.OwningProcess.Id, 0);
}
topWindowCounts[proxy.OwningProcess.Id]++;
}
foreach (int pid in topWindowCounts.Keys)
{
Process p = Process.GetProcessById(pid);
Console.WriteLine("Process: " + p.ProcessName + " has " + topWindowCount[pid].ToString() + " top level windows");
}
}
访问基础控件属性
使用ControlProxy的主要原因之一是在另一个进程中访问控件的属性。(中描述了这些属性图6。)要访问这些属性,只需使用ControlProxy创建一个ControlProxy。FromHandle或ControlProxy。然后调用两个方法来访问这些值。调用GetValue从被侦测进程中的基础控件获取属性值。例如,您可以使用以下代码调用GetValue来获取Size属性:
controlproxy.GetValue("Size")
调用SetValue来更改正在监视的进程中基础控件的属性值。例如,下面将背景色设置为蓝色:
controlproxy.SetValue("BackColor", "Color.Blue")
图6控件代理属性
| 财产 | 描述 |
|---|---|
| IntPtr句柄 | 返回控件的基础HWND。这相当于访问控件的Handle属性。 |
| array<ControlProxy^>^儿童 | 返回表示控件子窗口的ControlProxy类的数组。 |
| Process^自有流程 | 返回运行控件的Process对象。 |
| 布尔被管理 | 如果ControlProxy正在检查的窗口代表托管系统。Windows.Forms.Control,则返回值为true。否则就是假的。大多数ControlProxy方法仅在窗口是System.Windows.Forms.Control时有效。 |
| Type^组件类型 | 返回控件的类型。请注意,此类型最初来自被探测的进程中加载的程序集。第一次创建ControlProxy时,程序集和类型会在探测过程中重新加载。 |
| ControlProxy^家长 | 将父窗口作为ControlProxy返回。使用父项和子项导航流程的窗口链。 |
为了演示ManagedSpyLib在跨进程编辑属性方面的有用性,我将创建一个简单的C#应用程序。我添加了一个名为textBox1的文本框和一个名为button1的按钮。然后,我双击该按钮来创建button1_Click处理程序,并添加一些包含如图7.
图7修改应用程序的其他实例
private void button1_Click(object sender, EventArgs e)
{
foreach (Process p in Process.GetProcessesByName("WindowsApplication1"))
{
if (p.Id != Process.GetCurrentProcess().Id)
{
ControlProxy proxy = ControlProxy.FromHandle(p.MainWindowHandle);
string val = (string)proxy.GetValue("MyStringValue");
MessageBox.Show("Changing " + val + " to " + MyStringValue);
proxy.SetValue("MyStringValue", (object)MyStringValue);
}
}
}
public string MyStringValue
{
get { return this.textBox1.Text; }
set { this.textBox1.Text = value; }
}
如果我运行该应用程序的两个实例,在其中一个实例的textBox1中键入一些文本,然后单击button1,它将找到该应用程序的所有其他正在运行的实例,并将它们的textBox字符串更改为匹配,如图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中,还有一些额外的方法值得一看。调用SendMessage方法向控件发送窗口消息。如果您想要创建一个测试工具,这是很有用的。例如,可以发送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 . native window的类使它变得非常简单。
- 如果我想查看窗口消息,但我与目标窗口不在同一个进程中,该怎么办?
- 如果挂钩窗口消息最终显示托管信息,它与ManagedSpyLib有什么关系?
如果您希望看到窗口消息,并且您没有在与目标窗口相同的进程中运行,则不能使用SetWindowLong。您可以使用SetWindowsHookEx,但有一点需要注意:对于大多数类型的钩子,您的回调方法必须公开为dllexport。这意味着您必须在本机DLL或混合模式C++ DLL中编写回调。正是因为这个原因,ManagedSpyLib是使用托管C++编写的。它使用Visual Studio 2005中的C++/CLI支持。
ManagedSpyLib使用窗口消息挂钩有两个原因。要在目标进程中接收请求,它必须能够在该进程中执行代码。SetWindowsHookEx允许您这样做。ManagedSpyLib还使用自定义窗口消息在进程之间发送和接收数据。这意味着它的窗口钩子在发送请求时必须被激活(比如在另一个进程中检索控件的BackColor)。
使用内存映射文件
但是ManagedSpyLib到底是如何跨进程传输数据的呢?当然,它可以发送一个自定义窗口消息,比如WM_SETMGDPROPERTY来设置属性值。但是如果属性是BackColor,比如它怎么发送BackColor。红色?窗口消息只有两个双字作为参数。
答案是它使用了内存映射文件。这实际上不是磁盘上的文件。它是一个可以在多个进程之间共享的内存区域。您将该内存映射到自己的进程地址空间。然而,这样做的结果是共享区域有不同的起始地址。因此,您必须小心地在其中存储数据——没有指针!此外,内存映射文件中不能有任何托管对象,因为公共语言运行库(CLR)无法管理该内存。这意味着您只能存储原始字节数据。
因此,ManagedSpyLib只存储二进制序列化数据。这就是为什么属性(和EventArgs)必须可序列化才能被ManagedSpyLib支持的原因。ManagedSpyLib使用CAtlFileMapping为每个事务创建一个内存映射文件。
ManagedSpyLib计算二进制流的大小,创建大小合适的内存映射文件,然后将数据复制到其中。现在您已经对ManagedSpyLib如何使用窗口挂钩安装自身和内存映射文件发送数据有了大致的了解,让我们更仔细地看看ControlProxy类是如何创建和维护的。
创建控制代理和句柄重新创建
图9阐释了如何创建ControlProxy(红色箭头)以及当其句柄更改时如何维护它(蓝色箭头)。用户最初调用任一ControlProxy。TopLevelWindows将调用EnumWindows,然后在每个枚举的窗口上调用FromHandle。所以你可以把TopLevelWindows想象成一个更复杂的FromHandle调用。

图9**创建控制代理**
ManagedSpyLib为拥有目标窗口的线程打开一个windows挂钩。然后ManagedSpyLib向目标窗口发送一个WM_GETPROXY消息(一旦这个消息被处理,windows钩子将被关闭)。在接收端,消息被接收,命令库调用控制。FromHandle获取在被探测的进程中运行的托管控件。ManagedSpyLib使用控件创建新的ControlProxy。此ControlProxy存储类型。控件和程序集的全名。当前AppDomain中所有已加载程序集的位置。
ControlProxy订阅控件的HandleCreated和HandleDestroyed事件。它稍后使用这个来维护正确的窗口句柄状态。ControlProxy存储在被监视进程的ProxyCache中,并使用二进制序列化发送回监视进程。间谍进程反序列化ControlProxy并将其添加到其本地ProxyCache中。然后,它将ControlProxy返回给用户。
当被窥探的进程为控件重新创建句柄时,ManagedSpyLib会保持正确的状态。在探测的进程中从ControlProxy接收HandleDestroyed。ControlProxy检查控制。重新创建句柄以查看控件是否只是在执行句柄重新创建。如果正在重新创建句柄,ControlProxy将等待相应的HandleCreated。它更新本地ProxyCache,然后向间谍进程的EventWindow发送一个WM_HANDLECHANGED。监视进程通过查找旧的窗口句柄从ProxyCache中定位正确的ControlProxy。然后,它更新ControlProxy和间谍进程的ProxyCache。
图10显示ControlProxy如何获取属性(红色箭头)和接收事件(蓝色箭头)。当您通过ControlProxy获取属性值时,ManagedSpyLib执行以下序列。GetValue(propertyName)。首先,间谍进程调用ControlProxy。带有属性名称的GetValue。ManagedSpyLib为拥有目标窗口的线程打开一个窗口挂钩。一旦消息被处理,这将被关闭。ManagedSpyLib存储要在内存映射文件中获取的属性的名称(调用进程的内存存储的参数部分)。它使用二进制序列化来做到这一点。

图10**获取代理和接收事件**
ManagedSpyLib向目标窗口发送WM_SETMGDPROPERTY消息。windows钩子子程(MessageHookProc)将在被窥探的进程中被调用来处理窗口消息。MessageHookProc随后将处理该命令,并使用反射来获取返回值。它将返回值存储在调用进程的内存存储中。当SendMessage完成时,间谍进程从其内存存储中反序列化返回值。它向同一个目标窗口发送一个WM_RELEASEMEM,通知它可以释放对映射文件的引用。最后,它返回值。
订阅和获取事件是相似的。监视进程调用SubscribeEvent,它在监视进程的内存存储区的参数部分存储以下内容:EventWindow句柄、要订阅的事件的名称以及该窗口中该事件的唯一事件代码(通常是事件列表中事件的索引)。
SubscribeEvent将WM_SUBSCRIBEEVENT发送到目标控件。在spied进程中接收到WM_SUBSCRIBEEVENT后,ManagedSpyLib创建一个订阅事件的EventRegister对象,并跟踪它订阅了什么事件。当事件被触发时,EventRegister向事件窗口发送一个WM_EVENTFIRED消息,其中包含源窗口、事件代码和存储在被窥探进程的内存存储中的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.
图11测试代码
private void button1_Click(object sender, EventArgs e) { Process[] procs = Process.GetProcessesByName("Multiply"); if (procs.Length != 1)
return; ControlProxy proxy = ControlProxy.FromHandle(procs[0].MainWindowHandle); if (proxy == null)
return; //find the controls we are interested in... if (cbutton1 == null)
{ foreach (ControlProxy child in proxy.Children) { if (child.GetComponentName() == "textBox1") {
textBox1 = child;
} else if (child.GetComponentName() == "textBox2") { textBox2 = child; } else if (child.GetComponentName() == "textBox3") { textBox3 = child; } else if (child.GetComponentName() == "button1") { cbutton1 = child; }
} //sync testchanged on textbox3 so we can tell if it has changed. textBox1.SetValue("Text", "5"); textBox2.SetValue("Text", "7"); textBox3.SetValue("Text", ""); textBox3.EventFired += new ControlProxyEventHandler(textBox3_EventFired); textBox3.SubscribeEvent("TextChanged");
}
else
textBox3.SetValue("Text", ""); //now click on the button to start the test... if (cbutton1 != null)
{ cbutton1.SendMessage(WM_LBUTTONDOWN, IntPtr.Zero, IntPtr.Zero); cbutton1.SendMessage(WM_LBUTTONUP, IntPtr.Zero, IntPtr.Zero);
Application.DoEvents(); } if (result == 35)
MessageBox.Show("Passed!"); else
MessageBox.Show("Fail!"); } void textBox3_EventFired(object sender, ProxyEventArgs ed) { int val; if (int.TryParse((string)textBox3.GetValue("Text"), out val) { result = val; } }
当您运行单元测试应用程序时,它会将第一个文本框更改为5,将第二个文本框更改为7。然后,它向按钮发送一次单击(通过mousedown和mouseup ),并查看最终结果(在事件回调中设置)。
结论
ManagedSpy是一个诊断工具,类似于Spy++。它显示托管属性,允许您记录事件,是使用ManagedSpyLib的一个很好的例子。ManagedSpyLib引入了一个名为ControlProxy的类。ControlProxy是系统的一种表示。另一个进程中的Windows.Forms.Control。ControlProxy允许您获取或设置属性并订阅事件,就像在目标进程内部运行一样。使用ManagedSpyLib进行自动化测试、兼容性事件记录、跨进程通信或白盒测试。
浙公网安备 33010602011771号