65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化Windows应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (135投票s)

2002年12月2日

CPOL

11分钟阅读

viewsIcon

402245

downloadIcon

12341

不导出任何程序接口的 Windows 应用程序,可以通过将 COM 对象注入应用程序进程来转换为自动化服务器。

Sample Image

引言

不导出任何程序接口的 Windows 应用程序,尽管如此,也可以转换为自动化服务器。在此上下文中,“自动化”一词表示在独立进程中运行的客户端能够

  • 控制转换为服务器的应用程序,

  • 以同步和/或异步(通过接收事件通知)模式使用其某些服务。

这在旧的遗留应用程序和第三方应用程序中尤其有用。它们的功能和用户界面可以升级,并且它们的服务可以供其他应用程序使用。这可以通过注入到进程中的 COM 对象来实现。Jeffrey Richter [1] 描述了 Win32 NT 中的 DLL 注入技术,该技术已经几乎成为常规。现在我们尝试更进一步:将 COM 对象嵌入到目标应用程序(现在“晋升”为服务器)中,并使用此组件从其进程外部与应用程序进行通信。

技术简述

为简单起见,假设我们的目标应用程序只运行在一个线程中。该应用程序有一个主框架窗口,该窗口带有一个客户端(视图)窗口。为了自动化这样的应用程序,应该采取以下步骤。

  • 一个特殊的 Loader 应用程序使用远程线程将 Plugin DLL 注入到目标进程中。这是一个工作线程,其函数是 Plugin DLL 的 DllMain()。在 DllMain() 返回后,该线程将被终止。
  • Plugin DLL 还包含回调窗口过程,用于子类化目标应用程序的框架窗口和视图窗口。这些窗口过程包括具有目标应用程序新功能的消息处理程序。Plugin DLL 的 DllMain() 实际上执行子类化。
  • 例如,新框架窗口的窗口过程包含一个用于附加 Plugin 特定的 Windows 消息 WM_CREATE_OBJECT 的处理程序。DllMain() 执行目标应用程序窗口的子类化,然后将 WM_CREATE_OBJECT 消息发布到框架窗口。然后 DllMain() 返回。
  • 在收到 WM_CREATE_OBJECT 消息后,新的框架窗口过程将创建一个 COM 对象,并调用其方法以向运行对象表 (ROT) 注册。如果需要向客户端进行异步通知,则嵌入的 COM 对象应支持连接点机制,并在其 IDL 文件中定义传出接收器接口。最好将传出接口定义为基于 IDispatch(或双重)的接口,以允许脚本客户端实现它。
  • 客户端应用程序通过 ROT 获取注入的 COM 对象的代理,从而获得对目标应用程序的控制。要订阅服务器的事件通知,客户端应实现接收器接口并将其指针封送(“建议”)到嵌入式对象。

为了完成自动化任务,需要有关目标应用程序的窗口类名信息。可以使用 Visual Studio 安装中的 Spy 实用程序来显示它。另一个有用的工具是 ROT Viewer。它也是一个 Visual Studio 实用程序。ROT Viewer 允许开发人员检查 ROT,从而检查嵌入到目标进程中的 COM 对象的正确注册。

测试示例

众所周知的记事本文本编辑器被用作自动化目标应用程序。

Loader.exe 应用程序负责将 NotepadPlugin.dll 注入到 Notepad.exe 中。Loader 查找记事本的运行实例(如果尚无运行实例,则启动一个新实例),获取其进程句柄,并使用远程线程实际执行 NotepadPlugin.dll 的注入(详情请参阅 [1, 2] 和代码示例)。

NotepadPlugin.dll 将被注入到 Notepad.exe 中。它的 DllMain() 方法首先查找 Notepad.exe 的框架窗口和视图窗口,然后使用适当的自定义窗口过程对它们进行子类化。如上所述,DllMain() 不是在 Notepad.exe 主线程中运行,而是在 Loader 应用程序远程创建的附加线程中运行。当 NotepadPlugin.dll 完全加载后,此附加线程将消失。自定义的 WM_CREATE_OBJECT Windows 消息被发布到框架窗口,以启动用于自动化的 COM 对象的创建及其向 ROT 的注册。新框架窗口过程的 WM_CREATE_OBJECT 消息处理程序初始化 COM,创建 NotepadHandler COM 对象,并调用其适当的方法进行 ROT 注册。

COM 进程内服务器组件 NotepadHandler.dll 实现了双接口 IHandler,该接口专门为我们的自定义记事本自动化量身定制。IHandler 具有用于向 ROT 注册/注销对象的方法。传出源双接口 IHandlerEvents 已添加到 NotepadHandler 项目中(在 NotepadHandler.idl 文件中)。该接口包含方法 HRESULT SentenceCompleted([in] BSTR bsText)NotepadHandler 组件实现了连接点机制(可以通过在 Visual Studio 工作区的 ClassView 选项卡中右键单击 CHandler 并打开相应对话框来激活“实现连接点...”项以添加适当的代码)。

根据我的场景(仅用于说明),服务器(嵌入式 COM 对象)获取所有记事本编辑器的键盘输入符号(包括非字母符号)。一旦出现句末符号(“.”、“?”和“!”字符),服务器就会触发 Fire_SentenceCompleted() 事件,从而向所有事件订阅者提供已完成的句子(相应缓冲区的内容)。

下面讨论了 Win32 和 .NET 平台上的两种客户端变体。它们都支持两种 ROT 注册方法:ActiveObject 和 Moniker 注册。注册详情可以通过 ROT Viewer 工具查看。

Win32 客户端

AutomClient.exe 应用程序是 Win32 自动化客户端的示例。它通过使用组件的 ROT 注册为 NotepadHandler COM 对象创建代理,并实际上通过 NotepadHandler 实现的 IHandler 接口控制自动化的记事本实例。

客户端应用程序进程应实现 IHandlerEvents 接口。这可以通过多种方式完成。我选择创建一个特殊的 COM 组件(进程内服务器),即 Notification。为此,通过 ATL COM AppWizard 向工作区添加了额外的项目 NotepadHandlerNotification。然后,使用右键菜单将 ATL 对象插入到组件中。此对象实现了一个我称为 INotification 的附加接口。此接口不是强制性的,但可能有助于通过适当的方法向组件提供来自客户端应用程序的数据(我通过这种方式向组件提供客户端应用程序主窗口的句柄)。CNotification 类实现 IHandlerEvents 接口(通过激活“实现接口...”右键菜单项,然后打开相应的对话框)。客户端应用程序类 CAdvise 包含建议机制。方法 CAutomationClientDlg::OnButtonAutomate() 包含负责建议的代码。

在 Notification 组件中实现的 IHandlerEvents 接口的 SentenceCompleted() 方法由服务器调用。应用程序主窗口句柄(在此情况下)通过 INotification 接口的 SetMainWnd() 方法提供给 Notification 组件。拥有此句柄后,SentenceCompleted() 方法向客户端应用程序的主窗口发布通知消息,传递一个指向包含从服务器接收到的数据的缓冲区的指针。

默认情况下使用 ActiveObject 注册。要切换到 Moniker 注册,应在编译前取消注释文件 .\Include\DefineMONIKER.h 中 _MONIKER 的定义。

.NET 客户端

AutomClientNET.exe 应用程序是 .NET 自动化客户端的示例。从用户角度来看,它的行为与 AutomClient.exe 应用程序类似。AutomClientNET.exe 应用程序使用 RotHelper 程序集类为 NotepadHandler COM 对象创建 .NET Interop 代理。RotHelperNotepadHandler Interop(作为 NOTEPADHANDLERLib)必须添加到 AutomClientNET 项目的引用中。这可以通过标准 VS.NET 的“添加引用...”对话框完成:通过“项目”选项卡添加 RotHelper,并通过在“COM”选项卡中选择 NotepadHandler 类型库添加 NOTEPADHANDLERLib

默认情况下使用 ActiveObject 注册。要切换到 Moniker 注册,应在编译前取消注释(在文件 .\AutomationClientNET\NotepadClientForm.cs 的开头)_MONIKER 的定义。

运行测试

测试示例已提供编译好的演示。请注意,在运行之前,NotepadHandler.dllNotepadHandlerNotification.dll COM 组件必须使用 regsvr32.exe 实用程序进行注册。要进行注册,您需要运行演示目录中的 Register Components.bat 文件。然后可以启动一个或多个 AutomationClient.exeAutomationClientNET.exe 副本。

通过按下 NOTEPAD AUTOMATE 按钮,客户端内部启动 Loader.exe 应用程序。后者启动记事本并对其进行自动化。记事本主窗口标题中会出现单词 [Automated]。或者,您可以在 AutomationClient.exe 之前手动运行记事本。在这种情况下,Loader 会自动化已经运行的记事本实例。一旦记事本被自动化,客户端就会订阅其 Sentence Completed 事件。

现在您可以在自动化的记事本实例中输入一些文本,然后按下客户端应用程序的“复制文本”按钮。您输入的最后一段文本将出现在客户端应用程序的编辑框中。为了简化,只复制实际输入的字符和符号,不复制粘贴的文本。按下客户端的“查找”和“追加菜单”按钮会导致相应的记事本响应。

如果用户输入一些字符,后跟一个句子结束符号(“.”、“?”或“!”),则字符序列将复制到所有订阅该事件的客户端应用程序的编辑框中。一旦输入了 .?! 集合中的任何字符,自动化的记事本就会向其订阅者(客户端)发送(“触发”)Sentence Completed 事件。在此事件发生时,相应的缓冲区内容将发送给订阅者并由其显示。

退出记事本时,NotepadPlugin.dll 会代表记事本生成一个“再见...”消息框,以娱乐用户:)

编译测试

测试示例包括用于 Visual C++ 6.0 的 NotepadAuto 工作区和用于 Visual Studio .NET 的 AutomationClientNET 解决方案(文件 NotepadAuto.dswAutomationClientNET.sln 位于源文件的主目录中)。VC++ 6.0 的内容完全独立于 .NET 解决方案,可以单独测试。

首先,应该将文件 NotepadAuto.dsw 加载到 VC++ 6.0 Studio 并构建 _Build_All_Projects 项目。然后,如果您对 .NET 客户端感兴趣,必须将文件 AutomationClientNET.sln 加载到 VS.NET 中并构建其项目(请确保 RotHelperNOTEPADHANDLERLib 互操作的引用已添加到 AutomationClientNET 项目的引用中)。

psapi 库包含 EnumWindows()EnumProcesses() 函数,这些函数是系统范围搜索特定窗口和进程所必需的。因此,如果 psapi.dll 文件未安装在您的系统目录中,则应将其从 .\Psapi_library 目录复制到系统目录,或者以其他方式使其可加载(例如,通过复制到通过环境变量路径、工作目录或通过 Visual Studio 的“工具”->“选项”对话框->“目录”可用的目录)。还假定 Notepad.exe 位于您的 [Windows]\System32 目录中。完成上述安排后,您可以构建 _Build_All_Projects 项目(其余是其依赖项)并运行示例。

应运行 AutomationClient.exe 应用程序以测试 Win32 示例。请注意,示例应在 Visual Studio 外部进行测试。应运行 AutomationClientNET.exe 应用程序以测试 .NET 示例。此测试也可以从 VS.NET 执行。

进一步开发的一些方法

设计修改 描述
用户界面 (UI) 升级 目标应用程序的框架和视图窗口可以通过 MFC 感知的插件 DLL 进行子类化。这允许进行相当大的 UI 升级,例如,为旧式应用程序添加工具栏。
操作序列 客户端可以实现提供给目标应用程序的命令序列。此序列可以通过一些脚本来实现。
配置器 嵌入到目标进程中的对象可以作为“对象工厂”。借助它,可以在目标进程中创建其他 COM 对象以完成各种特定任务。对象工厂组件通过其方法和/或注册表获取新对象的构建数据。
分布式系统的一部分 自动化应用程序可以包含在分布式系统中(例如,参见 [3])。

结论

本文介绍了一种自动化不导出程序接口的 Windows 应用程序的技术。在注入到目标进程的 DLL 中使用 COM 对象,开发人员可以自动化此类应用程序。这种方法对于升级目标应用程序(特别是遗留应用程序)的功能和 GUI,并将其服务暴露给进程外客户端非常有用。源示例演示了 Win32 (MFC) 和 .NET (C#) 客户端的这种自动化。

参考文献

[1] Jeffrey Richter。Advanced Windows。第三版。Microsoft Press,1997。
[2] John Peloquin。远程库加载
[3] Dino Esposito。 将对象模型添加到非 COM 应用程序

更新

增加了 .NET 客户端。注入的 COM 对象的 ROT 注册可以通过 ActiveObject 或 Moniker 进行。文章文本已更改以反映代码中的更改。

谢谢

感谢所有阅读本文并提出意见和建议的人。特别感谢 Alex Furman 对 RotHelper.Moniker 类的改进。

© . All rights reserved.