为现有的MFC应用程序添加宏脚本语言支持





5.00/5 (14投票s)
Microsoft Script Hosting介绍以及为现有MFC应用程序添加宏脚本语言支持。

引言
编写托管脚本引擎以允许用户编写脚本来自定义和扩展应用程序的项目已被证明非常成功。有成千上万的开发人员在其应用程序中使用 Windows® 脚本引擎,而且毫无疑问,如果您想为应用程序添加脚本(宏)功能,Microsoft 的实现是完全足够。一个好的选择是使用 Active Scripting 技术。首先,Active Scripting 技术使用现有的脚本语言,因此您无需学习任何新语言。如果您知道如何使用 VBScript、JavaScript 甚至 PerlScript 进行编程,这就足够了。在本文中,我将介绍一种简单的方法,允许您为应用程序(即使是现有应用程序)添加脚本支持。
描述
Active Scripting 体系结构包含一组 COM 接口,这些接口定义了将脚本引擎连接到宿主应用程序的协议。在 Active Scripting 的世界中,脚本引擎只是一个 COM 对象,它能够动态执行脚本代码,响应直接解析或脚本语句加载、对脚本引擎 IDispatch 接口的显式调用,或宿主应用程序对象的传出方法调用(事件)。宿主应用程序可以将其自动化接口暴露给脚本引擎的命名空间,从而允许从动态执行的脚本中将应用程序的对象访问为编程变量。需要添加脚本支持的客户端应用程序只需要实现该技术的宿主部分。不同的供应商可以实现他们自己的引擎实现,从而为您提供使用您已知的其他语言的替代方案。一个很好的例子是 PerlScript 引擎。公司可能会决定使用它而不是使用 JavaScript 或 VBScript,以维护现有的代码库。
图 1:Active Scripting 体系结构
图 2:Active Scripting COM 交互
图 1 显示了 Active Scripting 的基本体系结构,图 2 显示了体系结构定义的 COM 接口的详细序列图。需要使用 Active Scripting 技术的客户端应用程序会创建一个并初始化一个基于您要解析的脚本语言的脚本引擎,然后通过 SetScriptSite
方法将您的应用程序连接到引擎。然后,您可以将脚本代码提供给引擎,引擎可以根据脚本引擎的内容和状态立即(非函数)或稍后(函数调用)执行。例如,以下脚本文本仅包含全局语句,因此可以在解析时执行
ScriptHost.Display("Hello CodeProject guru around the world.");
此语句将强制应用程序显示带有提供文本的消息框(使用 MFCScriptHost.exe 应用程序),但
function HostDisplay() { ScriptHost.Display("Hello CodeProject guru around the world."); }
只有在调用 HostDisplay()
时才会强制应用程序显示此消息。但好消息是,您的应用程序也可以随时访问此新方法。要执行此函数,您的客户端应用程序(宿主对象)需要调用正在使用的脚本引擎的 IDispatch
指针的 GetIDsOfNames
和 Invoke
来强制执行此函数。Active Scripting 技术的另一个很酷的功能是,您可以将任何自动化对象添加到脚本引擎项列表中,并在脚本中访问其方法和属性。事实上,这些功能正在 Microsoft Office 应用程序、Internet Explorer 和 Visual Studio 中使用。例如,您可以有一个名为“Documents”的项,并公开您应用程序中打开的文档列表。您对 Script Site 的实现将调用脚本引擎接口指针上的 AddNamedItem("Documents")
。例如,在我们最后的示例中,脚本引擎获取“ScriptHost”命名项的 dispatch 指针并调用“Display”方法。但内部很多这个过程取决于脚本引擎的状态。也就是说,引擎的状态必须是已启动(SCRIPTSTATE_STARTED
)。此时(当引擎启动时),引擎将查询 ActiveScriptSite 对象以将命名项解析为 IDispatch
接口指针。然后,它将通过调用 GetIDsOfNames
和 Invoke 来访问该接口的属性和方法。这个新项就像一个内部变量,可以在需要时访问。另外,由于这两个 IDispatch
接口方法,脚本引擎可以轻松访问命名项的属性和方法,这也很明显。
但出于多种原因,将事件函数连接到脚本引擎会更加棘手。最明显的原因是脚本引擎必须能够对命名项进行后期绑定的事件支持。为了支持将事件绑定到宿主命名项,Active Scripting 引擎使用连接点将宿主应用程序对象的传出方法调用/事件映射到脚本函数。调用命名项事件函数的方式取决于脚本语言。VBScript 使用与 JavaScript 完全不同的方法来绑定事件调用。Andrew Clinick 的脚本事件文章提供了比我在这里能涵盖的更多的细节,所以您可能想查看一下。
我认为这足以让您入门,现在如果您想了解更多信息,请先查看本文底部的以下参考文献。本文已更新,以显示 ScriptHost 对象如何触发事件到脚本。
为您的应用程序添加脚本支持
现在,我已经构建了这个过程,您可以使用它为新的或现有的 MFC 应用程序添加脚本支持。
- 第一步是创建一个 .ODL 文件(如果您的应用程序还没有)。高级 MFC 开发人员也可以通过其他方式模拟此过程(因为我们不会注册此库),但我在这里不介绍。您需要使用 GUIDGen.exe(可在 Visual Studio 的工具文件夹中找到)生成 GUID。典型的 .ODL 文件将如下所示:
// YourAppName.odl : type library source for YourAppName.exe // This file will be processed by the MIDL compiler to produce the // type library (YourAppName.tlb). [ uuid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX), version(1.0) ] library YourAppName { importlib("stdole32.tlb"); importlib("stdole2.tlb"); //{{AFX_APPEND_ODL}} //}}AFX_APPEND_ODL}} };
- 使用 ClassWizard (Ctrl+W) 创建一个派生自
CCmdTarget
的 **Automation** 对象,该对象将是宿主对象(ActiveScriptSite 的客户端对象)。定义您希望从宿主中提供的函数。例如,您可能有诸如CreateActiveX(strProgID)
、DisplayMessage(strMessage)
等函数。通常,这些新的 dispatch 函数将从脚本调用。 - 在您的新类文件中,将所有对
CCmdTarget
的引用替换为CActiveScriptHost
。此时,您需要将 ActiveScriptHost.h/cpp 添加到您的项目中。 - *已更新* 覆盖虚拟函数
HRESULT GetClassID( LPCLSID pclsid )
。它必须成功返回您宿主对象的 CLSID。在创建步骤 2 中的自动化对象后,可以在 .ODL 文件中找到 CSLID。典型的实现将如下所示:HRESULT CHost_Proxy::GetClassID( LPCLSID pclsid ) { *pclsid = CLSID_Host_Proxy; return S_OK; }
同时请注意,此时我们的对象实际上是一个 COM 对象和 ActiveX 控件,但我们不会像通常那样注册它。 - *已更新* 您需要声明您正在使用的类型库。此步骤很棘手,但使用 MFC 宏会容易得多。只需在代理类的头文件中添加
DECLARE_OLETYPELIB(CYourHost_Proxy)
,并在 .cpp 文件中添加IMPLEMENT_OLETYPELIB(CYourHost_Proxy, _tlid, _wVerMajor, _wVerMinor)
。_tlid
是类型库的 GUID(步骤 1),而_wVerMajor
/_wVerMinor
代表您的类型库的版本号。此外,请使用资源包含编辑器添加这些指令。#ifdef _DEBUG 1 TYPELIB "Debug\\YourAppName.tlb" #else 1 TYPELIB "Release\\YourAppName.tlb" #endif
- *新增* 现在添加一个事件源对象,例如:
[ uuid(740C1C2D-692F-43F8-85FF-38DEE1742819) ] dispinterface IHostEvent { properties: methods: [id(1)] void OnRun(); [id(2)] void OnAppExit(); }; // Class information for CHost_Proxy [ uuid(F8235A29-C576-439D-A070-6E7980C9C3F6) ] coclass Host_Proxy { [default] dispinterface IHost_Proxy; [default, source] dispinterface IHostEvent; };
如本例所示,我们的宿主现在支持两个事件,我们可以使用COleControl::FireEvent
函数直接从我们的代码触发它们。这些函数非常简单。例如:void FireOnRun() {FireEvent(eventidOnRun,EVENT_PARAM(VTS_NONE));} void FireOnAppExit() {FireEvent(eventidOnAppExit,EVENT_PARAM(VTS_NONE));}
- 创建您的宿主对象实例(也可以是使用 MFC 宏动态创建的类),并调用
CYourHostProxy::CreateEngine( 'Language ProgID' )
,如果您想使用这些引擎,可以是 'JavaScript' 或 'VBScript'。 - 为您的代理函数添加实现代码,以完成您希望让高级用户在您的应用程序中执行的操作。
- 添加您希望从脚本语言访问的任何其他命名项对象
- 提供用户创建脚本或从磁盘加载脚本文本的方式。
CActiveScriptHost
类提供了您可以根据您想在应用程序中提供的功能重用的辅助函数。顺便说一句,让用户创建 Inproc-ActiveX 对象是不安全的,但 Local-server 通常是好的。不让用户创建 ActiveX 控件的一个好理由是,如果 ActiveX 内部发生崩溃,您的应用程序不应该崩溃。Local-server 免费为您提供这种安全性。
修订历史
//////////////////////////////////////////////////////// // Version history // v1.01 : Bug fix with accessing object info (ITypeInfo) // v1.10 : Add 'InvokeFuncHelper' allows to call script // function directly from c++ // v1.5 : Add support for Host event (now derive from COleControl // instead of CCmdTarget) ////////////////////////////////////////////////////////