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 定义的头文件,请执行以下操作:
在“生成”菜单中单击“设置”,然后从每个配置的文件列表中选择 ODL 文件。
单击“OLE 类型”选项卡并在“输出标头”文件名字段中指定文件名。 使用尚未由项目使用的文件名,因为 MkTypLib 将覆盖任何现有文件。 单击“确定”关闭“生成设置”对话框。
若要将 MkTypLib 生成的头文件中的 UUID 定义添加到项目,请执行以下操作:
在标准 include 头文件 stdafx.h 中包含 MkTypLib 生成的头文件。
创建新文件 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"
在“生成”菜单中单击“设置”,然后从每个配置的文件列表中选择 INITIIDS.CPP。
单击“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_ERRORINFO
、DUAL_ERRORINFO_PART
和 IMPLEMENT_DUAL_ERRORINFO
,这些宏都包含在 MFCDUAL.H 中。)
以下示例实现一个定义的类来支持 ISupportErrorInfo
。 CAutoClickDoc
是自动化类的名称,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;
}