一、添加连接点
1. 新建ATL项目
设置项目属性。如果需要属性化,可以选中“属性化”复选框,如果要生成可执行文件而不是DLL,选择“可执行文件”,如果需要MFC支持,选中“支持MFC”,这里保持默认选项。
2. 添加ATL简单对象
在新建的工程上,鼠标右键菜单命令“添加类”,在弹出的对话框中选择“ATL简单对象”
点击“添加”,在弹出的对话框中输入对象的“简称”
点击下一步,选中“连接点“,这样向导将会自动生成事件接口_IcalcEvents及事件代理类CProxy_ICalcPiEvents
3. 为连接点添加事件定义
在项目生成的IDL文件中,找到连接点事件接口_ICalcEvents,为其添加事件函数定义
[
uuid(C341057F-62AA-44C7-B865-26EC97515D29),
helpstring("_ICalcPiCtrlEvents 接口")
]
dispinterface _ICalcPiCtrlEvents
{
properties:
methods:
[id(1), helpstring("方法OnDigit")] void OnDigit([in] SHORT nIndex, [in] SHORT nDigit);
};
上面的代码添加了OnDigit事件函数的定义,也可以通过向导来添加事件函数的定义。
4. 实现事件激发函数
在事件代理类CProxy_IcalcPiEvents,添加事件激发函数的定义,事件激发函数的定义通常以Fire_EventName表示,如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 (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)
{
// 调用函数的参数,与事件函数OnDigit相同
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为idl中事件函数的id
hr = pConnection->Invoke(/*dispid*/1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms, NULL, NULL, NULL);
}
}
return hr;
}
这样就完成了ATL对象连接点的实现。可以在ATL对象类中通过Fire_OnDigit来激发事件,只要是连接到此连接点的外部对象,都可以收到由Fire_OnDigit激发的事件。因为Fire_OnDigit函数只是用于激发事件的接收函数,因此Fire_OnDigit可以定义为任意名称。
从上面的Fire_OnDigit代码可以看出,其只是查找接收事件的对象,并且将事件通过事件接收对象的IDispatch接口来调用事件接收函数,因此接收对象需要实现IDispatch接口,并且通过Invoke方法可以调用事件接收函数。
二、定义事件接收对象
接下来定义接收事件的对象CalcDigit,按上面的方法新建ATL简单对象,因为对象用于接收事件,因此取消选择“连接点”。
理论上定义事件接收对象应该是件很简单的事,只需要实现事件接口并连接到事件源(连接点)对象上就行了。但是事件接口通常被定义为dispinterface,即继承Idispatch接口。MIDL编译器为事件接口生成的代码如下,
MIDL_INTERFACE("E3130D9E-BE45-4448-B37D-8814AF673FAC")
_ICalcPiEvents : public IDispatch
{
};
从上面的代码可以看出,里面并没有我们刚才定义的事件函数OnDigit的定义。因此我们如果直接实现此事件接口,并没有真正实现实际接收函数。
1. 通过定义假接口来接收事件
(1)定义假接口
有两种解决办法,第一个是通过定义一个和_IcalcPiEvents具有相同事件函数的接口,然后通过实现此接口来定义事件接收函数,如定义一个IcalcDigit接口
interface ICalcDigit : IDispatch{
[id(1), helpstring("方法OnDigit")] void OnDigit([in] SHORT nIndex, [in] SHORT nDigit);
};
可以看到,此接口和事件接口的函数定义完全相同,然后我们实现此接口,我们的CalcDigit类现在的定义为
class ATL_NO_VTABLE CCalcDigit :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CCalcDigit, &CLSID_CalcDigit>,
public IDispatchImpl<ICalcDigit, &IID_ICalcDigit, /*&LIBID_PiSvrLib*/&LIBID_atl_callLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
public IDispatchImpl<_ICalcPiEvents, &__uuidof(_ICalcPiEvents), /*&LIBID_PiSvrLib*/&LIBID_atl_callLib, /* wMajor = */ 1>
这里因为事件接口继承自Idispatch,所有我们通过继承IdisptchImpl类来实现此接口。
接下来就是实现接收事件的函数
STDMETHODIMP_(void,OnDigit) (short nIndex, short nDigit)
{
// 事件处理代码
}
因为Fire_OnDigit函数通过Idispatch接口来调用我们的事件接收函数,然而我们既实现了IcalcDigit,又实现了_IcalcPiEvents接口,这两个接口继承至Idispatch,因此需要指明哪个Idispatch默认来自哪个接口,因为我们的事件接收函数来自IcalcDigit接口,因此我们暴露此接口的Idispatch
BEGIN_COM_MAP(CCalcDigit)
COM_INTERFACE_ENTRY(ICalcDigit)
COM_INTERFACE_ENTRY2(IDispatch, ICalcDigit)
COM_INTERFACE_ENTRY(_ICalcPiEvents)
END_COM_MAP()
上面代码定义了ATL类暴露的接口,
COM_INTERFACE_ENTRY2(IDispatch, ICalcDigit)
表示Idispatch接口默认来自IcalcDigit。
通过定义假接口的方法来实现事件接收函数,好处是代码简单,但是需要另外定义与事件接口具有相同的属性和方法签名的假接口。
(2)连接到事件源
IUnknown* pUnk = NULL;
IConnectionPointContainer* pConnPtContainer = NULL;
IConnectionPoint* m_pICalcPiConnectionPoint;
// 创建ATL对象实例
hr = CoCreateInstance(CLSID_CalcPi, NULL, CLSCTX_ALL, IID_IUnknown, (void**)&pUnk);
// 获得IconnectionPointContainer接口
hr = pUnk->QueryInterface(IID_IConnectionPointContainer, (void**)&pConnPtContainer);
ATLENSURE(SUCCEEDED(hr) && pConnPtContainer != NULL);
// 查找ICalcPiEvents的连接点
hr = pConnPtContainer->FindConnectionPoint(DIID__ICalcPiEvents, &m_pICalcPiConnectionPoint);
// 连接到连接点
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);
2. 通过IDispEventImpl 和 IDispEventSimpleImpl 类
ATL提供IDispEventImpl和IDispEventSimpleImpl类来实现IDispatch接口。
(1)继承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,
CCalcDigit,
&__uuidof(_ICalcPiEvents),
&LIBID_hello_atlLib,
LIBRARY_MAJOR, LIBRARY_MINOR> DefendantEventImpl;
typedef IDispEventSimpleImpl<PLAINTIFF_SOURCE_ID,
CCalcDigit, &__uuidof(_ICalcPiEvents)> PlaintiffEventImpl;
然后使事件接收对象继承其中任意一个类或者同时继承两个类。
class ATL_NO_VTABLE CCalcDigit :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CCalcDigit, &CLSID_CalcDigit>,
public DefendantEventImpl,
public PlaintiffEventImpl
(2)事件接收映射
通过ATL宏来定义事件接收函数映射,
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类可以通过传给其的LIBID_hello_atlLib类型库来确定事件函数的原型,因此不需要为其提供_ATL_FUNC_INFO,如果继承的是IDispEventSimpleImpl类,则需要通过_ATL_FUNC_INFO宏来声明事件函数的原型结构,并使用SINK_ENTRY_INFO宏来映射事件接收函数。函数原型声明,
static _ATL_FUNC_INFO PlaintiffOnDigitInfo = {
CC_STDCALL, VT_EMPTY, 2, { VT_I2,VT_I2 }};
(3) 实现事件接收函数
接下来就是实现事件接收函数
void __stdcall DefendantOnDigit(SHORT nIndex,SHORT nDigit)
{
// 事件处理代码
}
(4) 连接到事件源
通过IDispEventImpl 和 IDispEventSimpleImpl 类实现事件接收的类,需要使用这两个类的DispEventAdvise和DispEventUnadvise方法来添加和删除事件连接。
DefendantEventImpl* defendant_imp=(DefendantEventImpl*)m_pCalcDigit;
hr=defendant_imp->DispEventAdvise(m_pCalcPi,&DIID__ICalcPiEvents);
这样就成功的将事件接收对象连接到事件源了。如果事件源通过Fire_OnDigit方法激发事件,那么事件接收对象的接收函数OnDigit就会接收到事件。
三、在脚本中调用
可以通过脚本,在HTML中使用我们创建的ATL并接收ATL对象中的事件。为了成功的接收事件,最好是使用ATL控件,而不是ATL简单对象。脚本代码如下,
<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" />
四、参考
1. ATL Internals: Working with ATL 8, Second Edition
2. ATLDuck Sample: Uses Connection Points with ATL