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

为MFC应用程序添加自动化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (53投票s)

2004 年 9 月 7 日

18分钟阅读

viewsIcon

234895

downloadIcon

5571

关于如何向现有应用程序添加 OLE 自动化的分步说明。此外,它还演示了如何在不使用应用程序向导提供的代码的情况下完成这些操作。

目录

引言

MFC 中的自动化支持是有些人不喜欢 MFC 的一个绝佳例子:只要你遵循预设示例并像设计的那样使用 MFC,它就能正常工作;但一旦你稍微偏离常规,你就只能靠自己了。MSDN 和在线文章中有一些教程,教你如何使用“新建项目”向导来生成一个支持自动化的应用程序,其中一些甚至解释了向导生成的代码的一些工作原理——但不幸的是,在现实世界中情况并非总是如此。有时,你会继承一个非常老的应用程序,它的初始骨架可能是用 Visual Studio 4 的向导生成的;它被几十个人扩展和修改过,每个人都有自己的理解(或缺乏理解)MFC 文档/视图架构;它已经从一个下午的快速概念验证变成了一个对公司生存至关重要的软件;它已经进行了如此多的“手术”,以至于它的内部(比喻地说)开始像一个 3 岁的孩子与一大碗意大利肉酱面搏斗后的残羹剩饭。相信我——一点也不好看。

那么,当你被要求为这样一个应用程序添加自动化支持时,你能做什么?从一个新应用程序开始,然后将旧代码的功能移回来是不可行的。即使可行,你也会发现向导生成的代码是为应用程序对象模型的中心元素是文档这一理念而构建的。在一些 MFC 应用程序中,情况已经不再是这样了。唯一的办法是找出 MFC 应用程序中自动化工作的确切原理——然后将这些部分添加到旧应用程序中。

最明显的方法是采用一个由向导生成的、支持自动化的应用程序,并将其与一个不支持自动化的应用程序进行比较。将这些差异添加到旧应用程序中,一切就绪,对吧?嗯,这部分是正确的——只要你想实现的模型是围绕文档的,就像我之前说的。如果你的模型不是这样工作的,你想知道如何让自动化工作,请继续阅读。

自动化

“等等,”你说,“你一直在说的‘自动化’到底是什么?” 啊是的,尊贵的读者,请原谅我没有早点详细说明。简单来说,自动化是一种让你的应用程序与 Visual Basic、VBScript、JavaScript 以及任何其他可以处理 COM 对象的语言进行交互的方式。这揭示了自动化的实现方式:作为一种 COM 接口。这也表明,为了让你的应用程序支持自动化,它必须支持 COM。在本篇文章的范围内,我无法提供完整的 COM 入门指南(以及完整的自动化入门指南);对于那些对 COM 的概念和工作原理不甚了解的人,我建议参考“参考文献”部分。从现在开始,我将假设你知道 COM 和自动化是什么;你知道什么是“对象模型”,并且你有一个(或者至少有一个想法)你想要自动化的应用程序的对象模型;并且你知道 MFC 的文档/视图架构。你不需要对最后一个了解太多,因为本文的大部分内容都是关于如何 *不* 使用它。

尽管我曾说过你可以从任何语言使用自动化,但对这个说法需要做一个小小的细分。脚本语言要访问 COM 对象,它们需要对象的接口描述。这种描述可以从类型库(*.tlb 文件)读取,但并非所有脚本语言都能访问。因此,有一种查询对象所提供方法的方法:实现 IDispatch 接口。但是对于 *能够* 读取 tlb 的语言,也应该有一种方法可以做到。解决方案很简单:双接口。同样,关于双接口理论的详细信息,请参阅“参考文献”部分;我在这里提到它是为了假设你希望你的对象拥有双接口。

问题

此时,作为一名感兴趣的读者,你可能已经在 MSDN 中查找了“自动化”,并且看到了大量文章,这些文章解释了 MFC 中自动化的概念以及如何使用类向导添加支持自动化的类。所以,你可能在想:“我为什么要读这个——我可以在 MSDN 上获得相同的信息?” 你看,问题在于这些文章基于以下(隐含的)假设:

  • 你是在“启用自动化”选项开启的情况下生成你的应用程序骨架的。
  • 你的对象模型围绕着一个文档对象。
  • 你想要一个 dispinterface,而不是双接口。

没有文档提供一个逐步的概述,说明你需要做什么来自动化一个现有应用程序,详细说明每个部分的作用以及需要考虑的事项。这就是本文试图解决的问题。

解决方案

本文当然!下面,我将介绍让你的应用程序可以从每种支持 COM 的语言进行脚本编写的步骤。事实证明,你必须进行的更改可以分为以下几组:

  • 在 IDL 中定义你的对象模型。
  • 将你的对象模型实现为 CCmdTarget 派生类。
  • 通用初始化。初始化 COM,将你的对象注册到系统中。

我还包含了一个关于如何从 C++ 和 VBScript 启动你的自动化应用程序的小节,用于编写小型测试客户端。

我将以教程风格呈现所有步骤,并解释这些步骤的作用以及它们的目的。包含的示例项目包含一个普通的 MFC 多文档应用程序,除了使自动化正常工作的最少更改外。这些更改在代码中已清楚标记。

为了尽量减少本文中的代码量,我将只添加一个 COM 对象。我将要自动化的应用程序名为“MyCoolApp”,外部可访问的 COM 对象将是一个通用的“Application”对象,它有一个方法:Show(),顾名思义,它会显示窗口。我认为这是一个很好的第一个要实现的方法,因为当应用程序从自动化客户端实例化时,默认情况下它不会显示。在开发应用程序时,你可以调用 Show() 方法,当你的应用程序出现时,你就知道自动化工作了。

在您的应用程序中执行的操作

IDL 文件

第一步是添加一个包含 COM 对象描述的文件。在解决方案资源管理器中,右键单击你的应用程序,然后选择“添加”->“添加新项”。选择“MIDL 文件”并输入一个文件名,例如“mycoolapp.idl”。这将把文件添加到你的项目中并打开它。如前所述,我们将创建一个名为“application”的对象,其中有一个方法:Show()。IDL 大部分是样板代码。如果你选择复制代码示例,请务必记住更改 GUID。 GUID 是你接口的全局唯一标识符;如果你复制代码示例中的 GUID,它们可能会与其他使用它们的应用程序冲突!这将来可能或可能不会造成问题,但为了安全起见,**请更改它们**!生成 GUID 很容易:在 Visual Studio 中,单击“工具”->“创建 GUID”,然后就可以生成了。单击“复制”按钮将新生成的 GUID 复制到剪贴板。

说了这么多,代码如下:

#include "olectl.h"
[ uuid(526E36B7-F346-4790-B741-75D9E5B96F4B), version(1.0) ]
// This is usually the name of your application.
library mycoolapp
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [ uuid(6263C698-9393-4377-A6CC-4CB63A6A567A),
      oleautomation,
      dual
    ]
    interface IApplication : IDispatch
    {
        [id(1), helpstring("method Show")] HRESULT Show (void);
    };

    [ uuid(9ACC7108-9C10-4A49-A506-0720E0AACE32) ]
    coclass Application
    {
        [default] interface IApplication;
    };
};

一些注意事项

  • 你需要有 3 个不同的 GUID
  • 你需要一个 coclass,它将以你建模的对象命名
  • 你需要一个接口定义,它(惯例上)命名为“I”+你建模的对象名称。

自动化类头文件

现在,我们将添加一个代表将被自动化(即可以被自动化客户端访问)的对象的类。这是客户端进入你的应用程序的入口点(如果你实现了多个接口,那么就是入口点之一)。添加它相当直接,我将引导你完成:

添加一个类,并以你的接口命名,“Application”在本例中。我将遵循 MFC 的命名约定,暂时称它为 CApplication(虽然我个人讨厌 C 前缀——参见参考文献)。你可以使用向导完成此操作,或者手动添加一个类。将其派生自 CCmdTarget。添加一个 `#include` 语句(稍后我将解释它的来源)。

#include "mycoolapp_h.h"

添加以下函数:

virtual void OnFinalRelease()
{
  CCmdTarget::OnFinalRelease();
}

HRESULT Show()
{
  AfxGetApp()->m_pMainWnd->ShowWindow(TRUE);
  return TRUE;
}

这是你放置对象任何清理代码的地方。我们有一个简单的例子,不需要清理;我们只调用父类。

在头文件中添加以下宏:

  DECLARE_DYNCREATE(CApplication)
  DECLARE_MESSAGE_MAP()
  DECLARE_OLECREATE(CApplication)
  DECLARE_DISPATCH_MAP()
  DECLARE_INTERFACE_MAP()

这些宏设置了一些成员和函数,它们是向系统注册类和将 COM 对象调用路由到你的(C++)对象所必需的。

添加一个 `enum`

enum 
{
  dispidShow = 1L
};

在这个 `enum` 中,你需要为添加到接口的每个函数都提供一个条目。在这个例子中,只有一个 `Show()`,所以 `enum` 中只有一个条目。你可以自己命名——稍后我们将看到这些名称的引用位置。

接下来,声明一个接口映射,并为你的自动化对象想要拥有的每个函数添加条目。这听起来很复杂,但它只是一个简单的宏和一些复制粘贴。

BEGIN_INTERFACE_PART(LocalClass, IApplication)
    STDMETHOD(GetTypeInfoCount)(UINT FAR* pctinfo);
    STDMETHOD(GetTypeInfo)(
        UINT itinfo,
        LCID lcid,
        ITypeInfo FAR* FAR* pptinfo);
    STDMETHOD(GetIDsOfNames)(
        REFIID riid,
        OLECHAR FAR* FAR* rgszNames,
        UINT cNames,
        LCID lcid,
        DISPID FAR* rgdispid);
    STDMETHOD(Invoke)(
        DISPID dispidMember,
        REFIID riid,
        LCID lcid,
        WORD wFlags,
        DISPPARAMS FAR* pdispparams,
        VARIANT FAR* pvarResult,
        EXCEPINFO FAR* pexcepinfo,
        UINT FAR* puArgErr);
    STDMETHOD(Show)(THIS);
END_INTERFACE_PART(LocalClass)

“但是 Roel,”你会问,“这到底是怎么回事?” 我会告诉你:GetTypeInfoCount()GetTypeInfo()GetIDsOfNames()Invoke() 构成了 IDispatch 的实现,而 Show() 是我们接口中方法的实现。如果你想确切地知道前四个函数的作用,我将参考 MSDN,但相信我,复制粘贴效果更好——我也是这么做的。对于那些想知道所有标准部分是否不能被包装在宏中的人:请参阅本文末尾。MFC ACDual 示例提供了这样的宏。

自动化类实现

现在,是时候实现我们刚刚写了头文件的类了。从 MFC 宏开始实现 `dyncreate`:

IMPLEMENT_DYNCREATE(CApplication, CCmdTarget)

接下来,实现构造函数和析构函数,并在其中添加以下语句,除了你自己的代码:

  • 构造函数中的 EnableAutomation()::AfxOleLockApp()
  • 析构函数中的 ::AfxOleUnlockApp()

然后实现消息映射:

BEGIN_MESSAGE_MAP(CApplication, CCmdTarget)
END_MESSAGE_MAP()

现在是重点部分:分派映射。分派映射看起来很像消息映射,它有一个 BEGIN_DISPATCH_MAP 部分,为你的接口的每个函数提供条目,并以 END_DISPATCH_MAP 宏结束。这个例子将展示我们只有一个方法的简单接口的分派映射:

BEGIN_DISPATCH_MAP(CApplication, CCmdTarget)
  DISP_FUNCTION_ID(CApplication, "Show", dispidShow, Show, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

BEGIN_DISPATCH_MAP 宏的参数与消息映射的参数相同:类的名称及其基类的名称。DISP_FUNCTION_ID 的参数更有趣。第一个参数(再次)是你正在实现的类的名称。第二个参数是你方法的简短字符串描述。它通常与你在类中使用的名称相同。第三个是一个我们已经在类声明中的 `enum` 中设置的唯一数字。这个数字必须是唯一的,这就是为什么 `enum` 在这里很方便(当然,也因为它允许你使用描述性名称而不是数字)。下一个参数是接口方法在类中实现的那个方法的名称。正如我所提到的,这通常与第二个参数相同(除了引号)。

第五个参数是方法的返回类型。这不像你期望的 VT_HRESULT,而是所有方法的 VT_NONE。在这里解释它为什么完全正确会太深入了,但简单来说:dispinterface 的所有方法都返回 HRESULT,正如 Show() 方法的返回类型所反映的那样。然而,那个 HRESULT 用于错误报告,而不是实际返回值。因此,当你从例如 Visual Basic 调用 Show() 时,你根本看不到那个 HRESULT——如果发生错误,VB 的标准错误处理机制就会启动。因此,你应该将所有分派映射中的函数声明为返回 VT_NONEDISP_FUNCTION_ID 宏的最后一个参数是函数参数的空格分隔列表。由于我们的函数不带任何参数,我们输入了 VTS_NONE。如果它接受一个字符串和一个整数,我们将使用“VTS_BSTR VTS_I2”,例如。请参阅 MSDN 获取此处允许的常量完整列表。

到目前为止,分派映射就到此为止。接下来是接口映射。这是 COM 对象(你的自动化应用程序)和你的 C++ 对象之间实际“连接”的地方。再次,让我们从一个例子开始:

BEGIN_INTERFACE_MAP(CApplication, CCmdTarget)
  INTERFACE_PART(CApplication, IID_IApplication, LocalClass)
END_INTERFACE_MAP()

INTERFACE_PART 宏的第一个参数是我们要实现的类的名称;第二个参数是接口名称(很可能是‘IID_’+你的接口名称),第三个参数是 BEGIN_INTERFACE_PART 宏的第一个参数(我们放在声明中的)。就这么简单。

还需要完成另一个“标准”实现,使用 IMPLEMENT_OLECREATE 宏。示例:

IMPLEMENT_OLECREATE(CApplication, "mycoolapp.Application", 
  0x9acc7108, 0x9c10, 0x4a49, 0xa5, 0x6, 0x7, 0x20, 0xe0, 0xaa, 0xce, 0x32)

第一个参数是你在 IDL 文件中指定的 coclass 名称。第二个名称是你的对象将以文本形式已知的描述,例如,对 Visual Basic;如果你不确定选择什么,将其设置为‘<库名称>’+‘.’+‘<接口名称>’。回顾一下 IDL 示例,你就会明白我的意思。第三个参数非常重要:它是你在 IDL 文件中声明的 coclass 的 GUID。

我们在这里快完成了:添加我们刚刚写了头文件的类的实现:

STDMETHODIMP_(ULONG) CApplication::XLocalClass::AddRef()
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalAddRef();
}
STDMETHODIMP_(ULONG) CApplication::XLocalClass::Release()
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalRelease();
}
STDMETHODIMP CApplication::XLocalClass::QueryInterface(
  REFIID iid, LPVOID* ppvObj)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  return pThis->ExternalQueryInterface(&iid, ppvObj);
}
STDMETHODIMP CApplication::XLocalClass::GetTypeInfoCount(
    UINT FAR* pctinfo)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetTypeInfoCount(pctinfo);
}
STDMETHODIMP CApplication::XLocalClass::GetTypeInfo(
  UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetTypeInfo(itinfo, lcid, pptinfo);
}
STDMETHODIMP CApplication::XLocalClass::GetIDsOfNames(
  REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames,
  LCID lcid, DISPID FAR* rgdispid) 
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->GetIDsOfNames(riid, rgszNames, cNames, 
    lcid, rgdispid);
}
STDMETHODIMP CApplication::XLocalClass::Invoke(
  DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags,
  DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult,
  EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr)
{
  METHOD_PROLOGUE(CApplication, LocalClass)
  LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
  ASSERT(lpDispatch != NULL);
  return lpDispatch->Invoke(dispidMember, riid, lcid,
    wFlags, pdispparams, pvarResult,
    pexcepinfo, puArgErr);
}

同样,这是可以通过几个宏大大简化的样板代码,这些宏在 ACDual MSDN 示例中提供。请参阅本篇文章末尾的关于此部分的内容。

最后,添加 Show() 函数的实现。我们只调用拥有类的 Show() 方法:

STDMETHODIMP CApplication::XLocalClass::ShowWindow()
{
    METHOD_PROLOGUE(CApplication, LocalClass)
    pThis->ShowWindow();
    return TRUE;
}

OnInitInstance 中的更改

我们还需要对 OnInitInstance() 进行一些更改,以便设置和注册 COM 对象(在注册表中)。第一个是调用:

COleTemplateServer::RegisterAll();

项目向导会在加载完 ini 文件并调用 LoadStdProfileSettings() 后立即放置此调用;我建议也放在那里。这其实无关紧要,但如果你将其与向导生成的应用程序进行比较,它看起来会更熟悉。

然后,向下滚动几行,直到看到对 ParseCommandLine() 的调用。在其正下方,添加以下代码:

if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
{
    return TRUE;
} else if (cmdInfo.m_nShellCommand == CCommandLineInfo::AppUnregister){
    AfxOleUnregisterTypeLib(LIBID_mycoolapp);
} else {
    COleObjectFactory::UpdateRegistryAll();
    AfxOleRegisterTypeLib(AfxGetInstanceHandle(), LIBID_mycoolapp);
}

当你的应用程序从自动化启动时,它将使用参数 /Embedding/Automation 运行。在这种情况下,我们不希望显示主窗口,所以我们立即返回。这意味着,如果你确实想显示主窗口,你需要在返回之前调用 pMainFrame->ShowWindow(TRUE);。第二个‘if’测试你的应用程序是否是用 /Unregserver/Unregister 开关运行的。如果是,我们将从注册表中删除我们应用程序的所有引用(实际上,我们让 MFC 为我们做这件事)。最后,如果我们没有检测到这些开关中的任何一个,我们就让 MFC 将我们 COM 对象的引用放入注册表。是的,这意味着你的应用程序每次运行时都会重新注册;这是为了确保注册表始终与可执行文件的最新位置保持一致。

关于 OnInitInstance 的更改就到这里了。如果你用向导生成了一个支持自动化的应用程序,你会看到更多的代码,特别是对 ColeTemplateServer 类型的一个 m_server 对象的几次调用。它们适用于你想让你的文档支持自动化的场景;它与 doctemplate 相关联,以便在自动化服务器启动时可以创建一个新文档。由于这段代码是向导自动生成的,我在这里就不详细介绍了。

Resource

最后要做的一件事是将类型库嵌入到你的应用程序的资源部分。转到资源视图,右键单击资源文件名,然后选择“资源包含”。在下面的框的底部,插入:

1 TYPELIB "<appname>.tlb"

其中 <appname> 当然是你的应用程序的名称(在本例中,应该是‘MyCoolApp.tlb’)。这样,资源编译器就会将类型库嵌入到可执行文件中,这样你就无需单独分发它了。

生成后步骤

此步骤并非严格必需,但会使你的生活更轻松:每次构建时注册你的应用程序。很简单:在项目的属性中,将以下行添加到你的生成后事件:

"$(TargetPath)" /RegServer

编译您的项目

为了让你的项目能够编译,你需要链接一个由 MIDL 编译器生成的文件:mycoolapp_i.c。要做到这一点,在解决方案资源管理器中右键单击“MyCoolApp”,选择“添加”->“添加现有项...”,然后选择 mycoolapp_i.c

如何使用您的自动化对象

当然,你想测试你新自动化的应用程序。请记住,有两种方法可以访问你的对象:通过“常规”COM(或“早期绑定”,仅限于支持它的环境,如 C++)和通过 IDispatch(“晚期绑定”,适用于 VBScript 等脚本语言)。我将在这里演示这两种方法。

从 C++

让我们从一个非常简单的 C++ 应用程序开始。使用类向导创建一个简单的基于对话框的应用程序,无论是 MFC 还是 ATL/WTL 应用程序。只需确保 :CoInitialize()CoUninitialize() 在某处被调用(ATL 应用程序中会自动完成)。在对话框的某个地方放一个按钮,将其连接起来,并在 BN_CLICKED 处理程序的响应中放入以下代码:

HRESULT hr;
hr = ::CoCreateInstance(CLSID_Application, NULL, 
     CLSCTX_LOCAL_SERVER, IID_IApplication, (void**)&m_IApplication);

if(SUCCEEDED(hr)) {
  if (m_IApplication) {
    m_IApplication->Show ();
  }
}

在对话框的头文件中,声明一个像这样的成员:

IApplication* m_IApplication;

现在,你所需要做的就是包含声明 IApplication 的文件。它由 midl.exe IDL 编译器自动生成,所以你必须将其复制到测试应用程序的目录中(每次更改 IDL 文件时都必须这样做),或者构建一个包含相对路径的 `#include` 语句。该文件(默认情况下)名为 <coclassname>_h.h,所以以我们的例子来说,它是 Application_h.h。当你查找这个文件时,你会注意到另一个文件:Application_i.c。该文件包含接口的实现,并且是链接器所必需的。所以,你也可以复制它,或者直接将其添加到你的项目中。

现在,构建你的应用程序,点击你创建的按钮,瞧——你的应用程序出现了!请注意,如果你删除了 m_IApplication->Show(); 这一行,你仍然可以通过查看 Windows 任务管理器中的进程列表来看到你的应用程序正在启动。

从 VBScript

从 VBScript 启动你的应用程序更容易。三行代码:

Dim obj
Set obj = CreateObject("mycoolapp.Application ")
obj.Show ()

请注意,我们传递给 CreateObject 的参数是我们传递给 IMPLEMENT_OLECREATE 宏的名称。另外,请注意,如果你在一个脚本中省略最后一行,创建一个包含所有三行的新脚本,并先运行两行脚本,然后运行三行脚本,你的应用程序只会启动一次!这意味着,如果你在应用程序的一个实例已经运行时调用 CreateObject(),将会返回该正在运行的实例的引用。

使用 ACDual 示例中的宏

本文中的许多代码都是非常标准的:它在你将来要自动化的任何应用程序中都将完全相同。为了避免这种情况,你可以使用 ACDual 示例中提供的一个头文件:mfcdual.h。示例本身可以在 MSDN 上找到;除了(本文未讨论的)一些错误处理代码外,它还包含以下宏:

  • BEGIN_DUAL_INTERFACE_PART:使用这个代替 BEGIN_INTERFACE_PART;它负责声明 IDispatch 接口的实现。
  • DELEGATE_DUAL_INTERFACE:将其添加到你的 .cpp 文件中,以实现用 BEGIN_DUAL_INTERFACE_PART 声明的函数。

我**强烈**建议你查看这些宏在 ACDual 示例中的使用方式,并查看它们的内容;当出现问题时(请注意,我说的不是“是否”,而是“何时”),它将帮助你更好地理解你的应用程序。

参考文献

关于 COM 和自动化基础知识的书籍:

在线资源

历史

2005 年 7 月 4 日 - Trevisan Andrea 更新了下载:下载代码经过少量重构,以包含一些重要部分,并确保解决方案文件类型与 Microsoft Visual Studio.NET 2002 Academic 兼容。

© . All rights reserved.