MANAGED SPY:用我们的新工具继续Spy++对Windows Forms的神话
原著:Benjamin Wulfe
翻译:小刀人
原代码下载:ManagedSpy.rar
本文使用.NET Framework 2.0
本文讨论:
新工具ManagedSpy
理解ManagedSpy在调试中如何工作和起作用的
看看ManagedSpyLib的内部工作机制
在单元测试中使用ManagedSpyLib
有不少开发者使用Visual
Studio®提供的工具Spy++。使用Spy++,你可以了解一个运行中的应用程序的窗体布局或确定一个导致bug的特定窗体消息。然而,当你创建一个基于Microsoft®
.NET框架的应用程序,Spy++变得不太管用了,因为由Spy++截取窗体消息和类不能与开发者使用或甚至看到的任何事情通信。开发者真正所想看到的是被托管的事件和属性值。
本文描述如何使用一个名为ManagedSpy的新工具并且它的关联库ManagedSpyLib,它们均可从MSDN®Magazine网址下载(译注:已包含在本文源代码下载链接ManagedSpy.rar中,其中的TestManagedSpy解决方案是译者为方便大家所做)。与Spy++显示Win32®
信息比如窗体类、样式和消息相似,ManagedSpy显示托管控件、属性和事件。ManagedSpyLib允许你以编程方式访问另一个进程中的Windows®窗体控件。你可以获得并设置属性以及在你自己的代码中事件上同步。ManagedSpyLib也可以帮助你创建测试带(test
harnesses)并且可以执行窗体、消息和事件记录(event logging)。
监视你的UI(用户界面)
当编写客户应用程序,这里有些是传统调试器不能发挥作用的案例。举个例子,如果你的bug涉及焦点或其他UI方面(的问题),它是很难来调试的,因为无论何时你碰到了一个断点(breakpoint)调试器修改了状态。另一个问题很难调试就是布局。如果你的窗体有一个复杂和动态布局,不论你的布局逻辑是否被调用多次它不一定总是明显的。为了调试这些问题,你一般必须诉诸于事件或消息记录以对你的UI获得的输入获得了解。
有着复杂的UI,对于窗体和关联状态的有所了解是有用的。举个例子,在调试器中定位相关控件对象可能会是很难的。大多数时候你必须猜测一个调试器变量就是一个你在UI中看到的。
图 1显示了一个有着一些嵌套控件的对话框。这个应用程序在右上方文本框有一个bug,虽然这个例子目的的真正所在并不是这个bug是什么。它将用来确定:不但它的成员是红色文本框而且也是父层次和相关控件的布局
图1 问题对话框
ManagedSpy可以对这个状况和其他(状况)有所帮助。它在你的基于.Net的客户应用程序中展示一个控件的树视图。你可以选择任何控件以及在上面获得或设置任何属性。你也可以记录控件生成的过滤后的一系列事件(译注:这是该工具的功能之一)。在调试时很有用,在你的控件的兼容性测试中也可以有所帮助。你可以使用真实应用(real
applications)和记录事件(log events)以确保你的控件的下一个版本的事件顺序被保存。
当你首次运行ManagedSpy,它在窗体左边的一个treeview(树视图)中显示一个进程列表同时在右边显示一个PropertyGrid(属性格)。你可以扩展这个进程来查看这个进程中的顶级窗体。
当你选择了一个控件,PropertyGrid显示了该控件上的属性。你可以在这里查看或改变属性。你应该注意到自定义类型是受到支持的,只要他们被二进制序列化(请看Basic Serialization)。
这个工具条包含选择哪些记录事件到事件面板(event pane),当新的窗体已经被创建时以刷新树视图,开始或停止记录事件到事件面板,和清空事件面板等等命令。
对于图1中显示的对话框,ManagedSpy显示出图2中的信息。对于ManagedSpy,SplitContainer
(SplitContainer2)是textBox1的父项,TableLayoutPanel(tableLayoutPanel1)又是的SplitContainer父项。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的中文意思为“控件代理”。)一个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添加其它TreeNode在procnode下以描述该窗体(看图 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用一个内存映射(memory-mapped)文件在窥探和被窥探进程之间传递数据而起作用。
为了正常工作,所有在进程中传递的数据必须被二进制序列化。进程之间通信用的主要机制是自定义窗体消息和SetWindowsHookEx函数。这确保了目标代码运行在拥有你需要窥探的窗体的线程上。这是重要的,因为这里有一些操作只能在从拥有窗体的线程调用时才能起作用。
这里有两个方法来创建ControlProxy。首先是通过使用ControlProxy.FromHandle,传递到IntPtr代表目标控件的HWND的方法中。为了这个目标(控件)它给你返回一个ControlProxy。窗体的HWND常可用EnumWindows
或用如Spy++的应用程序的Win32方法找到。你也可以通过访问一个控件的Handle属性获得HWND。
第二个方法是用ControlProxy.TopLevelWindows。你可以调用静态方法以获得一个ControlProxy类的数组。你将为桌面上的每个顶级窗体获得一个ControlProxy。然而不是所有这些窗体都是由托管控件代表的。为了确定这个,检查ControlProxy的属性就看它是否确实是一个托管窗口。查看属性部分取决于你可以获得的更多信息。图 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窥探那个事件。你然后预定在ControlProxy事件上调用EventFired:
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方法来发送一个窗体消息到控件。如果你想要创建一个测试带这将是很有用的。举个例子,你可以发送WM_CLICK
或 WM_KEYDOWN信息来模拟输入。如果你希望以这个方法使用ManagedSpyLib,你可以修改它以使得窗体钩子进程总是打开的并使它过滤每个窗体消息除了那些你已经编入的(消息)。这创建了一个自动控制驱动程序使得其它输入失效。
PointToClient 和PointToScreen用于转换屏幕坐标到客户坐标。SetEventWindow
和 RaiseEvent方法不是给用户代码使用的。在内部它们被用来管理事件交叉进程。ICustomTypeDescriptor允许一个对象动态确定属性和事件。ControlProxy由PropertyGrid的支持实现这个接口(译注:指ICustomTypeDescriptor)。你可以从用户代码中直接调用这些方法,但是它们并不是必需的。为了访问属性,用GetValue
和SetValue方法。
使用窗体钩子 (Window
Hooks)
正如前面提到的,ManagedSpyLib通过在进程之间传递数据发挥作用。一个窗体钩子是截取如WM_SETTEXT消息的方法。这里有两个创建窗体钩子的方法。SetWindowLong函数允许你在相同的进程中特定窗体上截取窗体消息。
SetWindowsHookEx函数允许一个宽范围的消息挂钩,包括在当前桌面中为所有进程的所有窗体挂钩消息的能力。
使用本机代码的大多开发者会将SetWindowLong当做子类化一个窗体的Win32函数。在你已经子类化一个窗体,Windows将为你指定的窗体句柄发送你的回调方法所有预定的Win32消息这允许你来修改或只是检查消息。
注意当你正在子类化的窗体时SetWindowLong需要你在相同的进程中。如果你希望做这个类型的子类化,.NET
Framework通过提供一个名为System.Windows.Forms.NativeWindow使得它非常简单。你可能会在这一点上问两个问题:
1、如果我想查看窗体消息同时我又和目标窗体不再相同的进程中怎么办?
2、如果在它结束时显示托管信息到底挂钩窗体消息如何联系ManagedSpyLib?
如果你想要看看窗体消息同时你没有运行在和目标窗体相同进程中,你不能使用SetWindowLong函数。你可以在一个警示下使用SetWindowsHookEx:对于大多类型的钩子,你的回调方法必需作为一个dllexport而暴露。这意味着你必需在一个本机DLL或混合模式C++DLL中写这个回调方法。ManagedSpyLib就是因这个原因而用托管C++写得。它使用在Visual
Studio 2005中的C++/CLI支持。
ManagedSpyLib使用窗体消息挂钩有两个原因。在目标进程中接收请求,它必需可以在该进程中执行代码。SetWindowsHookEx允许你这样作。
ManagedSpyLib也使用自定义窗体消息在进程之间来发送和接收数据。这意味着其窗体钩子必需在它发送一个请求时被激活(如重获在另一个进程中的一个控件的背景色)。
使用内存映射(Memory-Mapped)文件
但是ManagedSpyLib如何准确地在进程之间传输数据呢?当然,它可以发送一个如WM_SETMGDPROPERTY的自定义窗体消息来设置一个属性值。但是如果属性是背景色,举个例子,它如何发送BackColor.Red?窗体消息只有两个DWORDs作为参数。
回答是它使用了一个内存映射文件。它不是在硬盘上实际存在的文件。它是在多进程之间可共享的内存的一个区域。你映射内存到你自己进程的地址空间。然而,这样的结果就是共享区域有一个不同的开始地址。因此,在其中你不得不小心存储数据-没有指针!同样,你不能在内存映射文件中有任何托管对象,因为公共语言运行时(CLR)不能管理内存。这意味着你只可以存储原始字节数据。
由于这个原因,ManagedSpyLib只能存储二进制序列化数据。这就是为什么属性(和EventArgs)必须是可序列化的以受到ManagedSpyLib的支持。ManagedSpyLib用CAtlFileMapping来为每个处理创建一个内存映射文件。
ManagedSpyLib计算二进制流的尺寸,创建一个有着正确尺寸的内存映射文件,同时拷贝数据到其中。现在你已对ManagedSpyLib如何使用窗体钩子来安装它自己和内存映射文件来发送数据有一个大致的概念,让我们更进一步看看ControlProxy类是如何被创建和保持的。
创建一个ControlProxy类和句柄再建(Handle
Recreation)
图 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如何获得属性(红色箭头)并且接收事件(蓝色箭头)。ManagedSpyLib在你通过ControlProxy.GetValue(propertyName)获得一个属性值时执行以下步骤。首先,窥探进程用属性的名称调入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保存在被窥探进程的内存存储。
窥探进程用正确的事件和EventArg信息在正确的ControlProxy上处理WM_EVENTFIRED,解析源窗体,事件代码和EventArgs,和调用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。然后它发送一个单击(通过一个mousedown和mouseup)到这个按钮并查看最后的结果(它在事件回调时被设置)。
小结
ManagedSpy是一个诊断工具,和Spy++类似。它显示受托管的属性,允许你记录事件,并且是一个使用ManagedSpyLib的很好例子。ManagedSpyLib引入了一个称为ControlProxy的类。一个ControlProxy是一个System.Windows.Forms.Control的代理,允许你获得或设置属性并预定事件好似你正在目标进程中运行着一样。使用ManagedSpyLib来做自动化测试,为兼容性做事件记录、进程交互通讯,或白盒测试。
作者简介
Benjamin Wulfe 已在Visual Studio项目中为微软工作了六年了,是.NET Framework和.NET Compact
Framework同时也是几个Framework类如ComboBox 和NativeWindow的Windows Forms设计师。
本文由 VCKBASE MTT 翻译