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

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.70/5 (5投票s)

2011 年 3 月 28 日

CPOL

5分钟阅读

viewsIcon

36848

downloadIcon

867

本文介绍了如何为可连接的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, &params, NULL, NULL, NULL);
}
}
return hr;
} 

这样,我们就完成了连接点对象的实现。您可以通过事件触发函数(Fire_OnDigit)触发事件,所有连接到连接点对象的对象都将接收到事件。

Fire_OnDigit的代码中,我们知道该函数仅查找所有事件接收者对象,然后通过IDispatch接口调用它们的事件接收函数。因此,事件接收者对象必须实现IDispatch接口,并通过invoke方法调用其事件接收函数。

创建事件接收者对象

然后,我们创建事件接收者对象CalcDigit。我们像上面一样创建一个ATL简单对象,但我们不选中“连接点”选项,因为我们想要一个事件接收者对象而不是事件源对象。

理论上,创建事件接收者对象非常简单。理论上,实现一个接收单个接口事件的对象非常简单。您定义一个实现该接口的类,并将对象连接到事件源。我们有一个CalcPi类,它在_ICalcPiEvents调度接口上生成事件。让我们定义一个实现_ICalcPiEventsCCalcDigit类。

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
}

连接到事件源

我们需要使用DispEventAdviseDispEventUnadvise方法来建议和取消建议与事件源的连接。这两个方法是IDispEventImplIDispEventSimpleImpl类的成员。

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>

参考文献

  1. ATL Internals: Working with ATL 8, Second Edition http://book.douban.com/subject/2287029/
  2. ATLDuck Sample: Uses Connection Points with ATL http://msdn.microsoft.com/en-us/library/10tkk23z(v=VS.80).aspx
© . All rights reserved.