杨宁
本文首先论述可连接对象和连接点机制的原理,然后通过一个示例说明怎样用MFC编程实现可连接对象和内嵌于客户的事件接收器.
1、可连接对象和连接点机制的基本原理
为了在组件对象和客户之间提供更大的交互能力,组件对象也需要主动与客户进行通信。组件对象通过出接口(Outgoing Interface)与客户进行通信。如果一个组件对象定义了一个或者多个出接口则此组件对象叫做可连接点对象。
所谓出接口也是COM接口。每个出接口包含一组成员函数,每个成员函数代表了一个事件、一个通知或者一个请求。但是这些接口是在客户的事件接收器(sink)中实现的,所以叫出接口。事件接收器也是COM对象。
可连接对象必须实现一个IConnectionPointContainer接口用于管理所有的出接口。每个出接口对应一个连接点对象,连接点对象实现了IConnectionPoint接口。客户正是通过IConnectionPoint接口与可连接对象建立连接。每一个连接用CONNECTDATA结构描述。
CONNECTDATA包含两个成员:IUnknown* pUnk和DWORD dwCookie。pUnk对应于客户中事件接收器的IUnknown接口指针;dwCookie是由连接点对象生成的用于唯一标识此连接的32位整数。
通过一个由可连接对象实现的枚举器接口IEnumConnectionPoints,客户可以访问可连接对象的所有连接点。但是要获得IEnumConnectionPoints接口指针,要通过IConnectionPointContainer::EnumConnectionPoints(IEnumConnectionPoints**)函数,此函数返回枚举器接口指针。
通过另一个有可连接对象实现的枚举器接口IEnumConnections,无论客户还是可连接对象都可以访问一个连接点上的所有连接。通过IConnectionPoint::EnumConnections(IEnumConnections**)函数可以获得IEnumConnections接口指针。
综上所述,一个可连接对象必须实现四个接口:IConnectionPointContainer、IConnectionPoint、IEnumConnectionPoints、IEnumConnections。这四个接口的定义请阅读MSDN文档。
现在结合后面的示例简单描述一下可连接对象和客户通信的过程。在后面的示例中,可连接对象ConnObject定义了出接口IEventSink,对应此出接口,实现了一个连接点对象SampleConnPoint(此对象实现了对应于出接口的连接点接口IConnectionPoint,接口ID为IID_IEventSink)。
1.客户在获取了可连接对象的IUnknown接口指针m_pIUnknown后,调用m_pIUnknown->QueryInterface(IID_IConnectionPointContainer,(void**)&pConnPtCont);如果调用成功,pConnPtCont中将存放可连接对象的IConnectionPointContainer接口指针。如果调用不成功,则表明对象不是可连接对象。
2.调用pConnPtCont->FindConnectionPoint(IID_IEventSink,&pConnPt)。如果调用成功,pConnPt将存放对应于出接口IEventSink的连接点对象SampleConnPoint所实现的连接点接口IConnectionPoint指针;如果调用不成功,说明可连接对象不支持出接口IEventSink。
3.调用pConnPt->Advise(pIEventSink,&m_dwCookie)以建立事件接收器(EventSink)与连接点的连接。其中pIEventSink是客户事件接收器IUnknown接口的指针,此指针通过此函数传递给了可连接对象以便可连接对象发起对客户的通信;m_dwCookie是连接标识,此值由可连接对象设置由客户保存,客户还要使用此值以断开连接。
4.可连接对象可以通过连接点调用客户事件接收器中的方法。在客户与连接点成功建立连接后,连接点中已经保存了客户事件接收器接口的指针并可以调用pConnPt->GetConnections()来获取。
5.客户调用pConnPt->Unadvise(m_dwCookie)来取消连接,同时调用pConnPt->Release()释放连接点对象。
2、编程实例
现在用MFC实现一个可连接对象,然后写一个极为简单的客户和时间接收器。
需要说明的是,MFC通过CCmdTarget类实现了IConnectionPointContainer和IEnumConnectionPoints接口,此外,通过CConnectionPoint类实现了IConnectionPoint接口
1.可连接对象ConnObject
在这个对象中,实现一个一般的COM接口IEventServer,客户可以使用此接口的方法DoSomething()作一些事情,但主要的是对象将在此处触发事件。SampleConnPoint实现连接点对象。
(1)在GUIDs.h中写入:
// {EE888B01-EA9C-11d3-97B5-5254AB191930}
static const IID CLSID_ConnObject = //组件ID
{ 0xee888b01, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } };
// {EE888B02-EA9C-11d3-97B5-5254AB191930}
static const IID IID_IEventServer = //一般的COM接口,客户使用此接口的方法
//DoSomething()
{ 0xee888b02, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } };
//// {EE888B03-EA9C-11d3-97B5-5254AB191930}
static const IID IID_IEventSink = //连接点对象所实现的连接点接口ID
{ 0xee888b03, 0xea9c, 0x11d3, { 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30 } };
2. 在IConnObject.h中写入
#include "GUIDs.h"
//声明IEventServer接口
DECLARE_INTERFACE_(IEventServer,IUnknown)
{
STDMETHOD(DoSomething)()PURE;
};
//声明出接口,此出接口将由客户的事件接收器实现
DECLARE_INTERFACE_(IEventSink,IUnknown)
{
STDMETHOD(EventHandle)()PURE;
};
3.添加基类为CCmdTarget的类CConnObject.在类声明文件CConnObject1.h中加上#include “IConnObject.h”,在类声明中写入:
protected:
……
//声明实现IEventServer接口的嵌套类
BEGIN_INTERFACE_PART(EventServer,IEventServer)
STDMETHOD(DoSomething)();
END_INTERFACE_PART(EventServer)
DECLARE_INTERFACE_MAP()
//声明实现连接点的嵌套类
BEGIN_CONNECTION_PART(CConnObject,SampleConnPoint)
CONNECTION_IID(IID_IEventSink)
END_CONNECTION_PART(SampleConnPoint)
DECLARE_CONNECTION_MAP()
DECLARE_OLECREATE(CConnObject)
说明:BEGIN_CONNECTION_PART和END_CONNECTION_PART宏声明了实现连接点的嵌套类SampleConnPoint,并且是基于CConnectionPoint类的,如果需要重载CConnectionPoint类的成员函数或者添加自己的成员函数,可以在这两个宏中声明.这里,CONNECTION_IID宏重载了CConnectionPoint::GetIID()函数.使用DECLARE_CONNECTION-MAP()宏声明连接点映射表.
4.在类CConnObject的实现文件中写入
IMPLEMENT_OLECREATE(CConnObject,"ConnObject",
0xee888b01, 0xea9c, 0x11d3, 0x97, 0xb5, 0x52, 0x54, 0xab, 0x19, 0x19, 0x30);
BEGIN_INTERFACE_MAP(CConnObject,CCmdTarget)
INTERFACE_PART(CConnObject,IID_IEventServer,EventServer)
INTERFACE_PART(CConnObject,IID_IConnectionPointContainer,ConnPtContainer)
END_INTERFACE_MAP()
BEGIN_CONNECTION_MAP(CConnObject,CCmdTarget)
CONNECTION_PART(CConnObject,IID_IEventSink,SampleConnPoint)
END_CONNECTION_MAP()
说明:A.必须在接口映射中写入INTERFACE_PART(CConnObject,IID_IConnectionPointContainer,ConnPtContainer)以实现IConnectionPointContainer接口.注意,CCmdTarget类内嵌有才ConnPtContainer类以实现IConnectionPointContainer接口,并用m_xConnPtContainer加以记录.
B.用BEGIN_CONNECTION_MAP和END_CONNECTION_MAP宏实现连接点映射.CONNECTION_PART定义了实现连接点的类.
5.在CConnObject::CConnObject()中写入:
EnableConnections();
6.实现IEventServer接口
IEventServer接口是基于IUnknown接口的,实现IUnknown接口的方法这里不在赘述.在实现文件中写入:
STDMETHODIMP
CConnObject::XEventServer::DoSomething()
{
//DoSomething
METHOD_PROLOGUE(CConnObject,EventServer)
pThis->FireEvent();
return S_OK;
}
DoSomething()方法可以为客户提供需要的服务.这里着重的是可连接对象在此处触发客户事件接收器的事件,FireEvent()函数是ConnObject类实现的专门触发事件的的函数,代码如下:
void CConnObject::FireEvent()
{
//获取连接点上的连接指针队列
const CPtrArray* pConnections = m_xSampleConnPoint.GetConnections();
ASSERT(pConnections!=NULL);
int cConnections = pConnections->GetSize();
IEventSink* pIEventSink;
//对每一个连接触发事件
for(int i = 0; i < cConnections; i++)
{
//获取客户事件接收器接口指针
pIEventSink = (IEventSink*)(pConnections->GetAt(i));
ASSERT(pIEventSink!=NULL);
//调用客户事件接受器事件处理函数
//此函数是出接口定义,由客户事件接收器实现的
pIEventSink->EventHandle();
}
}
3、客户事件接收器(Sink)
事件接收器也是COM对象,也可以用嵌套类来实现,但是它只是客户的一个内部对象,所以可以没有CLSID和类厂.下面示例是一个对话框程序,对话框有三个按钮:”连接”(IDC_CONNECT),”断开”(IDC_DISCONNECT),”事件”(IDC_EVENT).
1.创建一个基于对话框的工程:ConnClient.
2.在CConnClientDlg中首先加入#include “IConnObject.h”,然后在对话框类声明中声明事件接收器嵌套类:
BEGIN_INTERFACE_PART(EventSink,IEventSink)
STDMETHOD(EventHandle)();
END_INTERFACE_PART(EventSink)
同时声明几个私有变量:
private:
LPCONNECTIONPOINTCONTAINER pConnPtCont;//记录组件对象
//IConnectionPointContainer接口指针
LPCONNECTIONPOINT pConnPt;//记录连接点接口指针
DWORD m_dwCookie;//记录连接标识
IUnknown* m_pIUnknown;//用以记录组件对象IUnknown接口指针
3.实现事件接收器:
STDMETHODIMP_(ULONG)
CConnClientDlg::XEventSink::AddRef()
{
return 1;
}
STDMETHODIMP_(ULONG)
CConnClientDlg::XEventSink::Release()
{
return 0;
}
STDMETHODIMP
CConnClientDlg::XEventSink::QueryInterface(REFIID riid,void** ppvObj)
{
METHOD_PROLOGUE(CConnClientDlg,EventSink)
if(IsEqualIID(riid,IID_IUnknown)||
IsEqualIID(riid,IID_IEventSink))
{
*ppvObj = this;
AddRef();
return S_OK;
}
else
{
return E_NOINTERFACE;
}
}
STDMETHODIMP
CConnClientDlg::XEventSink::EventHandle() //此函数将被可连接对象调用
{
::AfxMessageBox("源对象向事件接收器发出了的通知!");
return S_OK;
}
4.初始化COM库并创建组件对象实例
在CConnClientDlg::OninitDialog()中写入:
HRESULT hResult;
hResult = ::CoInitialize(NULL);
if(FAILED(hResult))
{
::AfxMessageBox("不能初始化COM库!");
return FALSE;
}
m_pIUnknown = NULL;
hResult = ::CoCreateInstance(CLSID_ConnObject,NULL,
CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&m_pIUnknown);
if(FAILED(hResult))
{
m_pIUnknown = NULL;
::AfxMessageBox("不能创建ConnObject对象!");
return FALSE;
}
m_dwCookie = 0;//预置连接标识为0
5.在按钮”连接”(IDC_CONNECT)的CLICK事件处理函数void CConnClientDlg::OnConnect()中写入:
void CConnClientDlg::OnConnect()
{
if(m_dwCookie!=0)
{
return;
}
if(m_pIUnknown!=NULL)
{
HRESULT hResult;
hResult = m_pIUnknown->QueryInterface(IID_IConnectionPointContainer,
(void**)&pConnPtCont);
if(FAILED(hResult))
{
::AfxMessageBox("不能获取对象的IConnectionPointContainer接口!");
return;
}
ASSERT(pConnPtCont!=NULL);
hResult = pConnPtCont->FindConnectionPoint(IID_IEventSink,&pConnPt);
if(FAILED(hResult))
{
pConnPtCont->Release();
::AfxMessageBox("不能获取对象的IEventSink连接点接口!");
return;
}
ASSERT(pConnPt!=NULL);
//获取事件接收器指针
IUnknown* pIEventSink;
m_xEventSink.QueryInterface(IID_IUnknown,(void**)&pIEventSink);
//通过连接点接口的Advise方法将事件接收器指针传给可连接对象
if(SUCCEEDED(pConnPt->Advise(pIEventSink,&m_dwCookie)))
{
::AfxMessageBox("与可连接对象ConnObject建立连接成功!");
}
else
{
::AfxMessageBox("不能与ConnObject建立连接!");
}
pConnPt->Release();
pConnPtCont->Release();
return;
}
}
上述代码与可连接对象的连接点建立连接.
6.编写按钮”断开”(IDC_DISCONNECT)的CLICK处理函数如下:
void CConnClientDlg::OnDisconnect()
{
if(m_dwCookie==0)
{
return;
}
pConnPt->Unadvise(m_dwCookie);
pConnPt->Release();
pConnPtCont->Release();
m_dwCookie = 0;
}
7.编写按钮”事件”(IDC_EVENT)的CLICK处理函数:
void CConnClientDlg::OnEvent()
{
if(m_pIUnknown!=NULL)
{
IEventServer* pIEventServer;
HRESULT hResult;
hResult = m_pIUnknown->QueryInterface(IID_IEventServer,(void**)&pIEventServer);
if(FAILED(hResult))
{
::AfxMessageBox("不能获取IEventServer接口!");
return;
}
pIEventServer->DoSomething();
}
}
这里,客户调用组件提供的服务DoSomething(),而正如前面所看到的,组件对象将在这个函数中触发一个由客户事件接收器处理(CConnClientDlg::XEventSink::EventHandle())的事件.
8.在退出应用时:
void CConnClientDlg::OnCancel()
{
m_pIUnknown->Release();
::CoUninitialize();
CDialog::OnCancel();
}
运行程序后,首先点击”连接”,然后点击”事件”按钮,这时将弹出MessageBox,并提示” 源对象向事件接收器发出了的通知!”.
小结
正是由于有了可连接对象这一机制,实现了客户与组件对象的双向通信,使组件对象具有了事件机制.这种类似于”服务器推送(Server push)”的技术在分布式应用系统中十分重要.
本文所举示例是用基于IUnknown接口实现的,其实,用自动化接口IDispatch作为出接口更为方便.需要说明的是,用ATL来写可连接对象更为简洁,MSDN文档中有一个示例.