TN065:针对 OLE 自动化服务器的双重接口支持

注意

以下技术说明在首次包括在联机文档中后未更新。 因此,某些过程和主题可能已过时或不正确。 要获得最新信息,建议你在联机文档索引中搜索热点话题。

本说明介绍如何为基于 MFC 的 OLE 自动化服务器应用程序添加双重接口支持。 ACDUAL 示例演示了双重接口支持,本说明中的示例代码摘自 ACDUAL。 本说明中所述的宏(例如 DECLARE_DUAL_ERRORINFO、DUAL_ERRORINFO_PART 和 IMPLEMENT_DUAL_ERRORINFO)是 ACDUAL 示例的一部分,可以在 MFCDUAL.H 中找到。

双重接口

尽管 OLE 自动化允许实现 IDispatch 接口、VTBL 接口或双重接口(即涵盖前面两种接口),但 Microsoft 强烈建议为所有已公开的 OLE 自动化对象实现双重接口。 与仅限 IDispatch 或仅限 VTBL 的接口相比,双重接口具有以下重要优势:

  • 可以在编译时通过 VTBL 接口进行绑定,或者在运行时通过 IDispatch 进行绑定。

  • 可以使用 VTBL 接口的 OLE 自动化控制器能够受益于改进的性能。

  • 使用 IDispatch 接口的现有 OLE 自动化控制器仍可正常工作。

  • VTBL 接口更容易从 C++ 调用。

  • 需要使用双重接口才能与 Visual Basic 对象支持功能兼容。

将双重接口支持添加到基于 CCmdTarget 的类

双重接口实际上只是派生自 IDispatch 的自定义接口。 在基于 CCmdTarget 的类中实现双重接口支持的最直接方式是首先使用 MFC 和 ClassWizard 在类上实现常规调度接口,然后再添加自定义接口。 在大多数情况下,自定义接口实现直接委托回到 MFC IDispatch 实现。

首先,修改服务器的 ODL 文件以定义对象的双重接口。 若要定义双重接口,必须使用一个 interface 语句,而不是 Visual C++ 向导生成的 DISPINTERFACE 语句。 不要删除现有的 DISPINTERFACE 语句,而应添加新的 interface 语句。 通过保留 DISPINTERFACE 形式,可以继续使用 ClassWizard 为对象添加属性和方法,但必须将等效的属性和方法添加到 interface 语句。

双重接口的 interface 语句必须包含 OLEAUTOMATION 和 DUAL 属性,并且接口必须派生自 IDispatch。 可以使用 GUIDGEN 示例为双重接口创建 IID

[ uuid(0BDD0E81-0DD7-11cf-BBA8-444553540000), // IID_IDualAClick
    oleautomation,
    dual
]
interface IDualAClick : IDispatch
    {
    };

准备好 interface 语句后,开始为方法和属性添加条目。 对于双重接口,需要重新排列参数列表,以便双重接口中的方法和属性访问器函数返回 HRESULT 并将其返回值作为包含特性 [retval,out] 的参数传递。 请记住,对于属性,需要添加具有相同 ID 的读取 (propget) 和写入 (propput) 访问函数。例如:

[propput, id(1)] HRESULT text([in] BSTR newText);
[propget, id(1)] HRESULT text([out, retval] BSTR* retval);

定义方法和属性后,需要在 coclass 语句中添加对 interface 语句的引用。 例如:

[ uuid(4B115281-32F0-11cf-AC85-444553540000) ]
coclass Document
{
    dispinterface IAClick;
    [default] interface IDualAClick;
};

更新 ODL 文件后,使用 MFC 的接口映射机制在对象类中为双重接口定义一个实现类,并在 MFC 的 QueryInterface 机制中创建相应的条目。 需要在 INTERFACE_PART 块中为 ODL 的 interface 语句中的每个条目添加一个条目,并为调度接口添加条目。 每个包含 propput 特性的 ODL 条目都需要一个名为 put_propertyname 的函数。 每个包含 propget 特性的条目都需要一个名为 get_propertyname 的函数。

若要为双重接口定义实现类,请将 DUAL_INTERFACE_PART 块添加到对象类定义中。 例如:

BEGIN_DUAL_INTERFACE_PART(DualAClick, IDualAClick)
    STDMETHOD(put_text)(THIS_ BSTR newText);
    STDMETHOD(get_text)(THIS_ BSTR FAR* retval);
    STDMETHOD(put_x)(THIS_ short newX);
    STDMETHOD(get_x)(THIS_ short FAR* retval);
    STDMETHOD(put_y)(THIS_ short newY);
    STDMETHOD(get_y)(THIS_ short FAR* retval);
    STDMETHOD(put_Position)(THIS_ IDualAutoClickPoint FAR* newPosition);
    STDMETHOD(get_Position)(THIS_ IDualAutoClickPoint FAR* FAR* retval);
    STDMETHOD(RefreshWindow)(THIS);
    STDMETHOD(SetAllProps)(THIS_ short x, short y, BSTR text);
    STDMETHOD(ShowWindow)(THIS);
END_DUAL_INTERFACE_PART(DualAClick)

若要将双重接口连接到 MFC 的 QueryInterface 机制,请将 INTERFACE_PART 条目添加到接口映射:

BEGIN_INTERFACE_MAP(CAutoClickDoc, CDocument)
    INTERFACE_PART(CAutoClickDoc, DIID_IAClick, Dispatch)
    INTERFACE_PART(CAutoClickDoc, IID_IDualAClick, DualAClick)
END_INTERFACE_MAP()

接下来,需要填入接口的实现。 在大多数情况下,都可以委托给现有的 MFC IDispatch 实现。 例如:

STDMETHODIMP_(ULONG) CAutoClickDoc::XDualAClick::AddRef()
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CAutoClickDoc::XDualAClick::Release()
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    return pThis->ExternalRelease();
}

STDMETHODIMP CAutoClickDoc::XDualAClick::QueryInterface(
    REFIID iid,
    LPVOID* ppvObj)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    return pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP CAutoClickDoc::XDualAClick::GetTypeInfoCount(
    UINT FAR* pctinfo)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
    ASSERT(lpDispatch != NULL);
    return lpDispatch->GetTypeInfoCount(pctinfo);
}

STDMETHODIMP CAutoClickDoc::XDualAClick::GetTypeInfo(
    UINT itinfo,
    LCID lcid,
    ITypeInfo FAR* FAR* pptinfo)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
    ASSERT(lpDispatch != NULL);

    return lpDispatch->GetTypeInfo(itinfo, lcid, pptinfo);
}

STDMETHODIMP CAutoClickDoc::XDualAClick::GetIDsOfNames(
    REFIID riid,
    OLECHAR FAR* FAR* rgszNames,
    UINT cNames,
    LCID lcid,
    DISPID FAR* rgdispid)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
    ASSERT(lpDispatch != NULL);

    return lpDispatch->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgdispid);
}

STDMETHODIMP CAutoClickDoc::XDualAClick::Invoke(
    DISPID dispidMember,
    REFIID riid,
    LCID lcid,
    WORD wFlags,
    DISPPARAMS FAR* pdispparams,
    VARIANT FAR* pvarResult,
    EXCEPINFO FAR* pexcepinfo,
    UINT FAR* puArgErr)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE);
    ASSERT(lpDispatch != NULL);

    return lpDispatch->Invoke(dispidMember, riid, lcid,
        wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
}

对于对象的方法和属性访问器函数,需要填入实现。 方法和属性函数通常可以委托回到使用 ClassWizard 生成的方法。 但是,如果设置属性来直接访问变量,则需要编写代码以获取值/将值放入变量。 例如:

STDMETHODIMP CAutoClickDoc::XDualAClick::put_text(BSTR newText)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    // MFC automatically converts from Unicode BSTR to
    // Ansi CString, if necessary...
    pThis->m_str = newText;
    return NOERROR;
}

STDMETHODIMP CAutoClickDoc::XDualAClick::get_text(BSTR* retval)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    // MFC automatically converts from Ansi CString to
    // Unicode BSTR, if necessary...
    pThis->m_str.SetSysString(retval);
    return NOERROR;
}

传递双重接口指针

传递双重接口指针的操作并不是简单直接的,尤其是需要调用 CCmdTarget::FromIDispatch 时。 FromIDispatch 仅适用于 MFC 的 IDispatch 指针。 此问题的解决方法之一是查询 MFC 设置的原始 IDispatch 指针并将该指针传递给需要它的函数。 例如:

STDMETHODIMP CAutoClickDoc::XDualAClick::put_Position(
    IDualAutoClickPoint FAR* newPosition)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDisp = NULL;
    newPosition->QueryInterface(IID_IDispatch, (LPVOID*)&lpDisp);
    pThis->SetPosition(lpDisp);
    lpDisp->Release();
    return NOERROR;
}

在通过双重接口方法传回指针之前,可能需要将其从 MFC IDispatch 指针转换为双重接口指针。 例如:

STDMETHODIMP CAutoClickDoc::XDualAClick::get_Position(
    IDualAutoClickPoint FAR* FAR* retval)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    LPDISPATCH lpDisp;
    lpDisp = pThis->GetPosition();
    lpDisp->QueryInterface(IID_IDualAutoClickPoint, (LPVOID*)retval);
    return NOERROR;
}

注册应用程序的类型库

AppWizard 不会生成代码来向系统注册 OLE 自动化服务器应用程序的类型库。 虽然还可以通过其他方式注册类型库,但让应用程序在更新其 OLE 类型信息时(即,每当应用程序独立运行时)注册类型库会更方便。

若要在每次应用程序独立运行时注册应用程序的类型库,请执行以下操作:

  • 在标准 include 头文件 STDAFX.H 中包含 AFXCTL.H,以访问 AfxOleRegisterTypeLib 函数的定义。

  • 在应用程序的 InitInstance 函数中,找到 COleObjectFactory::UpdateRegistryAll 调用。 在此调用的后面添加 AfxOleRegisterTypeLib 调用,指定对应于类型库的 LIBID,以及类型库的名称

    // When a server application is launched stand-alone, it is a good idea
    // to update the system registry in case it has been damaged.
    m_server.UpdateRegistry(OAT_DISPATCH_OBJECT);
    
    COleObjectFactory::UpdateRegistryAll();
    
    // DUAL_SUPPORT_START
        // Make sure the type library is registered or dual interface won't work.
        AfxOleRegisterTypeLib(AfxGetInstanceHandle(),
            LIBID_ACDual,
            _T("AutoClik.TLB"));
    // DUAL_SUPPORT_END
    

修改项目生成设置以适应类型库更改

要修改项目的生成设置,以便 MkTypLib 在每次重新生成类型库时生成包含 UUID 定义的头文件,请执行以下操作

  1. 在“生成”菜单中单击“设置”,然后从每个配置的文件列表中选择 ODL 文件

  2. 单击“OLE 类型”选项卡并在“输出标头”文件名字段中指定文件名。 使用尚未由项目使用的文件名,因为 MkTypLib 将覆盖任何现有文件。 单击“确定”关闭“生成设置”对话框

若要将 MkTypLib 生成的头文件中的 UUID 定义添加到项目,请执行以下操作

  1. 在标准 include 头文件 stdafx.h 中包含 MkTypLib 生成的头文件

  2. 创建新文件 INITIIDS.CPP 并将其添加到项目。 在此文件中,在包含 OLE2.H 和 INITGUID.H 之后包含 MkTypLib 生成的头文件:

    // initIIDs.c: defines IIDs for dual interfaces
    // This must not be built with precompiled header.
    #include <ole2.h>
    #include <initguid.h>
    #include "acdual.h"
    
  3. 在“生成”菜单中单击“设置”,然后从每个配置的文件列表中选择 INITIIDS.CPP

  4. 单击“C++”选项卡,单击类别“预编译标头”,然后选中“不使用预编译标头”单选按钮。 单击“确定”关闭“生成设置”对话框

在类型库中指定正确的对象类名

Visual C++ 随附的向导错误地使用实现类名在服务器的 ODL 文件中为 OLE 可创建类指定 coclass。 虽然这样做不会影响项目,但实现类名不一定是你希望对象用户使用的类名。 若要指定正确的名称,请打开 ODL 文件,找到每个 coclass 语句,然后将实现类名替换为正确的外部名称。

请注意,当 coclass 语句更改时,MkTypLib 生成的头文件中 CLSID 的变量名称也会相应地更改。 需要更新代码以使用新的变量名称。

处理异常和自动化错误接口

自动化对象的方法和属性访问器函数可能会引发异常。 如果是这样,应在双重接口实现中处理这些异常,并通过 OLE 自动化错误处理接口 IErrorInfo 将有关异常的信息传回给控制器。 此接口通过 IDispatch 和 VTBL 接口提供详细的上下文错误信息。 若要指示错误处理程序可用,应实现 ISupportErrorInfo 接口。

为了演示错误处理机制,我们假设 ClassWizard 生成的用于实现标准调度支持的函数引发了异常。 MFC 的 IDispatch::Invoke 实现通常会捕获这些异常,并将其转换为通过 Invoke 调用返回的 EXCEPTINFO 结构。 但是,在使用 VTBL 接口时,你需要自行负责捕获异常。 保护双重接口方法的示例:

STDMETHODIMP CAutoClickDoc::XDualAClick::put_text(BSTR newText)
{
    METHOD_PROLOGUE(CAutoClickDoc, DualAClick)
    TRY_DUAL(IID_IDualAClick)
    {
        // MFC automatically converts from Unicode BSTR to
        // Ansi CString, if necessary...
        pThis->m_str = newText;
        return NOERROR;
    }
    CATCH_ALL_DUAL
}

CATCH_ALL_DUAL 负责在发生异常时返回正确的错误代码。 CATCH_ALL_DUAL 使用 ICreateErrorInfo 接口将 MFC 异常转换为 OLE 自动化错误处理信息。 (MFCDUAL.H 文件中的 ACDUAL 示例中提供了一个示例 CATCH_ALL_DUAL 宏。MFCDUAL.CPP 文件中提供了该宏调用的用于处理异常的函数 DualHandleException。)CATCH_ALL_DUAL 确定根据发生的异常类型返回的错误代码:

  • COleDispatchException - 在本例中,HRESULT 是使用以下代码构造的:

    hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, (e->m_wCode + 0x200));
    

    这会创建一个特定于导致异常的接口的 HRESULT。 错误代码偏移 0x200,以避免与系统为标准 OLE 接口定义的 HRESULT 发生任何冲突。

  • CMemoryException - 在本例中返回了 E_OUTOFMEMORY

  • 任何其他异常 - 在本例中返回了 E_UNEXPECTED

若要指示使用 OLE 自动化错误处理程序,还应实现 ISupportErrorInfo 接口。

首先,将代码添加到自动化类定义中,以指明它支持 ISupportErrorInfo

然后,将代码添加到自动化类的接口映射,以将 ISupportErrorInfo 实现类与 MFC 的 QueryInterface 机制相关联。 INTERFACE_PART 语句与为 ISupportErrorInfo 定义的类匹配。

最后,实现定义的类来支持 ISupportErrorInfo

ACDUAL 示例包含了三个宏来帮助完成这三个步骤:DECLARE_DUAL_ERRORINFODUAL_ERRORINFO_PARTIMPLEMENT_DUAL_ERRORINFO,这些宏都包含在 MFCDUAL.H 中。)

以下示例实现一个定义的类来支持 ISupportErrorInfoCAutoClickDoc 是自动化类的名称,IID_IDualAClick 是接口的 IID,该接口是通过 OLE 自动化错误对象报告的错误的源

STDMETHODIMP_(ULONG) CAutoClickDoc::XSupportErrorInfo::AddRef()
{
    METHOD_PROLOGUE(CAutoClickDoc, SupportErrorInfo)
    return pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CAutoClickDoc::XSupportErrorInfo::Release()
{
    METHOD_PROLOGUE(CAutoClickDoc, SupportErrorInfo)
    return pThis->ExternalRelease();
}

STDMETHODIMP CAutoClickDoc::XSupportErrorInfo::QueryInterface(
    REFIID iid,
    LPVOID* ppvObj)
{
    METHOD_PROLOGUE(CAutoClickDoc, SupportErrorInfo)
    return pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP CAutoClickDoc::XSupportErrorInfo::InterfaceSupportsErrorInfo(
    REFIID iid)
{
    METHOD_PROLOGUE(CAutoClickDoc, SupportErrorInfo)
    return (iid == IID_IDualAClick) S_OK : S_FALSE;
}

另请参阅

按编号列出的技术说明
按类别列出的技术说明