实用 ATL:连接点和接收事件的两种方法






3.70/5 (5投票s)
本文介绍了如何为可连接的ATL对象实现连接点,并提供了接收事件的两种方法。
引言
本文介绍了如何为可连接的ATL对象实现连接点,并提供了接收事件的两种方法。
实现连接点
1. 创建新的ATL项目
在此项目中,我们保留ATL项目向导的默认选项,即仅选择“动态链接库”选项,其他选项保持未选中状态。
2. 创建新的ATL简单对象
右键单击刚刚创建的项目,选择“添加类”。在“添加类”对话框中,选择“ATL简单对象”,然后单击“添加”按钮。在向导中,我们将新ATL简单对象的短名称输入为CalcPi
。在“选项”面板中,我们选中“连接点”选项,其他保持默认。单击“完成”以结束向导。
指定选项后,简单对象向导会为您生成用于开始添加实现的骨架文件。对于类,有一个新生成的头文件包含类定义,一个用于实现的*.cpp文件,以及一个包含注册信息的*.RGS文件。此外,IDL文件会更新以包含新的接口定义。在此项目中,它将生成事件接口_ICalcEvents
和事件代理类CProxy_ICalcPiEvents
。
3. 添加事件函数定义
在自动生成的idl文件中,为_ICalcEvents
接口添加事件函数定义。
[
uuid(C341057F-62AA-44C7-B865-26EC97515D29),
helpstring("_ICalcPiCtrlEvents interface")
]
dispinterface _ICalcPiCtrlEvents
{
properties:
methods:
[id(1), helpstring("method OnDigit")] void OnDigit([in] SHORT nIndex, [in] SHORT nDigit);
};
代码添加了事件函数“OnDigit
”的定义,您也可以通过向导添加函数定义。
4. 实现事件触发函数
在事件代理类CProxy_ICalcPiEvents
中,我们添加事件触发函数。函数名称总是以Fire_
开头,例如Fire_OnDigit
,您可以将其更改为其他名称,但为了代码的可读性,最好使用此形式。下面是事件触发函数:
HRESULT Fire_OnDigit( SHORT nIndex, SHORT nDigit)
{
HRESULT hr = S_OK;
T * pThis = static_cast<t>(this);
int cConnections = m_vec.GetSize();
// For every object that connects to the connection points object,
// call its event receiving function
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<iunknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
if(punkConnection==NULL) continue;
CComQIPtr<idispatch> pConnection=punkConnection.p;
if (pConnection)
{
// Parameters of event receiving function, same as the definition of event function
CComVariant avarParams[2];
avarParams[1] = nIndex;
avarParams[1].vt = VT_I2;
avarParams[0] = nDigit;
avarParams[0].vt = VT_I2;
DISPPARAMS params = { avarParams, NULL, 2, 0 };
// Dispid is the id of event function in idl file
hr = pConnection->Invoke(/*dispid*/1, IID_NULL, LOCALE_USER_DEFAULT,
DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
}
}
return hr;
}
这样,我们就完成了连接点对象的实现。您可以通过事件触发函数(Fire_OnDigit
)触发事件,所有连接到连接点对象的对象都将接收到事件。
从Fire_OnDigit
的代码中,我们知道该函数仅查找所有事件接收者对象,然后通过IDispatch
接口调用它们的事件接收函数。因此,事件接收者对象必须实现IDispatch
接口,并通过invoke方法调用其事件接收函数。
创建事件接收者对象
然后,我们创建事件接收者对象CalcDigit
。我们像上面一样创建一个ATL简单对象,但我们不选中“连接点”选项,因为我们想要一个事件接收者对象而不是事件源对象。
理论上,创建事件接收者对象非常简单。理论上,实现一个接收单个接口事件的对象非常简单。您定义一个实现该接口的类,并将对象连接到事件源。我们有一个CalcPi
类,它在_ICalcPiEvents
调度接口上生成事件。让我们定义一个实现_ICalcPiEvents
的CCalcDigit
类。
coclass CalcDigit {
[default] dispinterface _ICalcPiEvents;
};
现在,使用ATL实现CCalcDigit
类。
class ATL_NO_VTABLE CCalcDigit :
...
public _ICalcPiEvents,
... { }
不幸的是,事件接口通常是dispinterface,因此常规的接口实现技术不起作用。当您运行MIDL编译器时,生成的_ICalcPiEvents
接口只会得到这个:
MIDL_INTERFACE("A924C9DE-797F-430d-913D-93158AD2D801")
_ICalcEvents : public IDispatch
{
};
从上面的代码中,我们可以看出_ICalcEvents
不包含我们在idl文件中定义的事件函数OnDigit
。尽管我们实现了这个接口,但我们并没有真正实现事件接收函数。
使用虚拟接口接收事件
有两种方法可以解决这个问题。一种方法是您需要定义一个具有与事件dispinterface相同的调度方法、调度标识符和函数签名的虚拟双接口。例如,我们可以定义一个名为ICalcDigit
的虚拟接口。
interface ICalcDigit : IDispatch{
[id(1), helpstring("method OnDigit")]
void OnDigit([in] SHORT nIndex, [in] SHORT nDigit);
};
事件接收者类将是:
class ATL_NO_VTABLE CCalcDigit :
public CComObjectRootEx<ccomsinglethreadmodel>,
public CComCoClass<ccalcdigit,>,
public IDispatchImpl<icalcdigit, *wmajor="*/" *wminor="*/">,
public IDispatchImpl<_ICalcPiEvents, &__uuidof(_ICalcPiEvents),
&LIBID_atl_callLib, /* wMajor = */ 1>
我们通过派生我们的类自IDispatchImpl
类来实现IDispatch
接口。然后,我们实现事件接收函数。
STDMETHODIMP_(void,OnDigit) (short nIndex, short nDigit)
{
// event process code
}
CCalcDigit
必须继承自_ICalcPiEvents
接口,因为我们需要暴露_ICalcPiEvents
接口来告诉连接点对象(事件源)我们实现了事件接口。
由于我们实现了ICalcDigit
和_ICalcPiEvents
,并且它们都继承自IDispatch
接口,因此我们必须指定我们要暴露哪个IDispatch
接口。我们可以使用以下代码来执行此操作:
BEGIN_COM_MAP(CCalcDigit)
COM_INTERFACE_ENTRY(ICalcDigit)
COM_INTERFACE_ENTRY2(IDispatch, ICalcDigit)
COM_INTERFACE_ENTRY(_ICalcPiEvents)
END_COM_MAP()
代码将暴露的IDispatch
接口分配给ICalcDigit
,因为ICalcDigit
包含我们事件接收函数的定义。
连接到事件源
我们可以通过以下代码将事件接收对象连接到事件源:
IUnknown* pUnk = NULL;
IConnectionPointContainer* pConnPtContainer = NULL;
IConnectionPoint* m_pICalcPiConnectionPoint;
// Create an instance of connection points ATL object
hr = CoCreateInstance(CLSID_CalcPi, NULL, CLSCTX_ALL, IID_IUnknown, (void**)&pUnk);
//Get IconnectionPointContainer interface
hr = pUnk->QueryInterface(IID_IConnectionPointContainer, (void**)&pConnPtContainer);
ATLENSURE(SUCCEEDED(hr) && pConnPtContainer != NULL);
// Find ICalcPiEvents connection point
hr = pConnPtContainer->FindConnectionPoint
(DIID__ICalcPiEvents, &m_pICalcPiConnectionPoint);
// Connect to event source
CCalcDigit* m_pCalcDigit = new CComObject<ccalcdigit>;
IUnknown* pUnk;
hr=m_pCalcDigit->QueryInterface(IID_IUnknown,(void**)&pUnk);
ATLENSURE(SUCCEEDED(hr));
hr = m_pICalcPiConnectionPoint->Advise(pUnk, &m_dwCookie);
通过IDispEventImpl和IDispEventSimpleImpl类接收事件
派生自IDispEventImpl和IDispEventSimpleImpl类
我们定义以下宏:
#define DEFENDANT_SOURCE_ID 0
#define PLAINTIFF_SOURCE_ID 1
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0
typedef IDispEventImpl<defendant_source_id,> DefendantEventImpl;
typedef IDispEventSimpleImpl<plaintiff_source_id,>
PlaintiffEventImpl;</plaintiff_source_id,></defendant_source_id,>
IDispEventImpl
类需要一个描述调度接口的类型库。然后,我们像下面一样继承这两个类中的一个或两个:
class ATL_NO_VTABLE CCalcDigit :
public CComObjectRootEx<ccomsinglethreadmodel>,
public CComCoClass<ccalcdigit,>,
public DefendantEventImpl,
public PlaintiffEventImpl</ccalcdigit,></ccomsinglethreadmodel>
事件接收器映射
然后,我们需要定义事件接收器映射:
BEGIN_SINK_MAP(CCalcDigit)
SINK_ENTRY_EX(DEFENDANT_SOURCE_ID,__uuidof(_ICalcPiEvents),1,DefendantOnDigit)
SINK_ENTRY_INFO(PLAINTIFF_SOURCE_ID,__uuidof(_ICalcPiEvents),
1,PlaintiffOnDigit,&PlaintiffOnDigitInfo)
END_SINK_MAP()
如果类继承自IDispEventImpl
,我们可以使用SINK_ENTRY_EX
宏映射事件接收函数。此宏不需要事件函数信息,因为IDispEventImpl
类将通过您提供的类型库自动确定事件函数定义。如果类继承自IDispEventSimpleImpl
,那么我们需要使用_ATL_FUNC_INFO
结构来提供函数信息,同时使用SINK_ENTRY_INFO
宏映射事件接收函数,如下所示:
static _ATL_FUNC_INFO PlaintiffOnDigitInfo = {
CC_STDCALL, VT_EMPTY, 2, { VT_I2,VT_I2 }};
实现事件接收函数
然后我们需要实现事件接收函数:
void __stdcall DefendantOnDigit(SHORT nIndex,SHORT nDigit)
{
// event process code
}
连接到事件源
我们需要使用DispEventAdvise
和DispEventUnadvise
方法来建议和取消建议与事件源的连接。这两个方法是IDispEventImpl
和IDispEventSimpleImpl
类的成员。
DefendantEventImpl* defendant_imp=(DefendantEventImpl*)m_pCalcDigit;
hr=defendant_imp->DispEventAdvise(m_pCalcPi,&DIID__ICalcPiEvents);
至此,我们已成功将事件接收对象连接到连接点对象。如果通过Fire_OnDigit
方法触发事件,则事件接收对象将接收到事件。
在脚本中接收事件
我们可以在HTML脚本(如JavaScript或VbScript)中接收基于ATL的连接点对象的事件。如果您希望脚本能够成功接收事件源的事件,最好创建一个ATL控件对象而不是ALT简单对象。VbScript代码如下:
<object id="CalcPiCtrl"
classid="CLSID:268E67E1-56FA-409C-8404-4D617FAA87ED" height="50%">
<script language="vbscript">
sub cmdCalcPi_onClick
CalcPiCtrl.Fire__Event
end Sub
sub CalcPiCtrl_onDigit(index,digit)
alert(index)
end sub
</script>
<input type="button" name="cmdCalcPi" value="Fire Event" /></object>
参考文献
- ATL Internals: Working with ATL 8, Second Edition http://book.douban.com/subject/2287029/
- ATLDuck Sample: Uses Connection Points with ATL http://msdn.microsoft.com/en-us/library/10tkk23z(v=VS.80).aspx