COM 连接点






4.84/5 (71投票s)
2003年1月22日
5分钟阅读

285265

4433
本文旨在通过一个清晰的实际示例来解释连接点的概念,该示例将演示一个进程内 COM 服务器和一个使用该服务器的 MFC 客户端。
引言
本文旨在通过一个清晰的实际示例来解释连接点的概念,该示例将演示一个进程内 COM 服务器和一个使用该服务器的 MFC 客户端。
它到底是什么?
这是 COM 对象用来回调客户端的一种方法。换句话说,客户端会收到来自 COM 对象的事件通知。
也许您听说过回调函数。好吧,它是这样的。假设您有一个公开了 IArithematic
接口的 COM 对象,并且有一个资源密集型的方法,比如 Add(int a,int b)
- (“为了简单起见,任何东西都可以,”隐士说,他赤身裸体地住在喜马拉雅山洞里。Sam Weller —《匹克威克 papers》)。试想一下,这个方法会花费大量时间,而您不想一直等到任务完成。您可以使用这段时间做些其他事情。所以,这就是连接点发挥作用的地方。您在客户端代码中分配一个名为 ExecutionOver(int Result)
的函数,COM 对象在完成执行 Add 方法后可以调用它。
因此,当 COM 对象完成任务后,它会调用客户端函数 ExecutionOver
(传递加法的结果)。客户端很高兴地在消息框中弹出结果。这就是全部内容。现在我们将深入探讨细节。
COM 对象如何知道如何调用 ExecutionOver??
想象一下,客户端公开了一个名为 ISink
的接口,该接口有一个名为 ExecutionOver(int result)
的方法。现在,如果客户端可以将此接口传递给 COM 对象,COM 对象就可以愉快地调用 ExecutionOver
。例如,在 COM 代码片段中可能如下所示:
//=================================================== ISink *pClientSink; //(Client somehow passes the ISink interface pointer //we shall see how later -- so pClientSink is loaded now HRESULT Add(int a , int b) { pClientSink->ExecutionOver(a+b); } //=====================================================
这才是真正发生的事情。其余部分是为了使这一切足够通用。微软通过定义可连接对象来实现这一点。让我们从检查连接所涉及的 COM 接口开始——IConnectionPoint
和 IConnectionPointContainer
。对象(而不是客户端)实现了这两个接口。
这两个接口如下所示。
interface IConnectionPointContainer : IUnknown { HRESULT EnumConnectionPoints( IEnumConnectionPoints **ppEnum) = 0; HRESULT FindConnectionPoint(REFIID riid, IConnectionPoint **ppCP) = 0; }; interface IConnectionPoint : IUnknown { HRESULT GetConnectionInterface(IID *pIID) = 0; HRESULT GetConnectionPointContainer( IConnectionPointContainer **ppCPC) = 0; HRESULT Advise(IUnknown *pUnk, DWORD *pdwCookie) = 0; HRESULT Unadvise(DWORD dwCookie) = 0; HRESULT EnumConnections(IEnumConnections **ppEnum) = 0; };
现在,我们一步一步地看看这一切是如何工作的。
COM 客户端调用 CoCreateInstance
来创建 COM 对象。一旦客户端获得了初始接口,它就可以通过调用 QueryInterface
来查询对象是否支持任何出站接口(使用 IConnectionPointContainer
)。如果对象回答“是”,并返回一个有效的指针,客户端就知道它可以尝试建立连接。
一旦客户端知道对象支持出站接口(换句话说,能够回调客户端),客户端就可以通过调用 IConnectionPointContainer::FindConnectionPoint
来请求一个特定的出站接口,使用代表所需接口的 GUID。如果对象实现了该出站接口,对象就会返回一个指向该连接点的指针。此时,客户端使用该 IConnectionPoint
接口指针并调用 IConnectionPoint::Advise( [in] IUnknown *pUnk, [out] DWORD *pdwCookie)
来传递其回调接口的实现,以便对象可以回调客户端。为了再次明确,传递给 advise 方法的 IUnknown
的指针是一个在客户端 EXE 中定义和实现的接口的指针。
好的,现在让我们通过一个实际的例子来说明这一切。
- 创建一个新的 ATL-COM AppWizard 项目,并将其命名为 ConnectionCOM。
- 右键单击类视图并创建一个新的 ATL 对象。
将其命名为 Add(接口 IAdd)。
在单击“确定”按钮之前,请务必选中“支持连接点”复选框。
点击“确定”。
注意类视图中生成的类。您会找到一个 IAdd
和一个 _IAddEvents
。后者只是一个代理类,需要在客户端实现。它是因为我们勾选了 Connection_Points
复选框而生成的。
向 IAdd
接口添加一个方法 'Add(int a,int b)
',向 _IAddEventsInterface
添加一个方法 'ExecutionOver(int Result)
'。类视图将如下所示:
但是,由于我们选择了一个双重接口,并且不需要那么多麻烦,所以让我们通过编辑 IDL 文件来移除对 IDispatch
的支持。下面是原始文件。
//=========================================================== // ConnectionCOM.idl : IDL source for ConnectionCOM.dll // : : library CONNECTIONCOMLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(AFE854B0-246F-4B66-B26F-A1060225C71C), helpstring("_IAddEvents Interface") ] // Old block - take this out // dispinterface _IAddEvents // { // properties: // methods: // [id(1), helpstring("method ExecutionOver")] // HRESULT ExecutionOver(intResult); // }; //To this one -put this in interface _IAddEvents : IUnknown { [id(1), helpstring("method ExecutionOver")] HRESULT ExecutionOver(intResult); }; [ uuid(630B3CD3-DDB1-43CE-AD2F-4F57DC54D5D0), helpstring("Add Class") ] coclass Add { [default] interface IAdd; //[default, source] dispinterface _IAddEvents; take this line //out and put the line below in [default, source] interface _IAddEvents ; }; }; //================================================================
呼!客户端方面几乎完成了。现在,进行一次构建,因为我们需要类型库来用 ATL 做一些巧妙的事情。现在,右键单击 CoClass 并单击“实现连接点”。
在出现的对话框中选中 _IAddEvents。
将生成一个 CProxy_IAddEvets
类,其中包含 Fire_ExecutionOver(int result)
方法。这将负责 COM 对象如何调用客户端接口(并处理多个客户端调用同一 COM DLL 以及其他此类问题)。现在,让我们实现我们旧的 IAdd
接口的 Add 方法。
//===================================================== STDMETHODIMP CAdd::Add(int a, int b) { // TODO: Add your implementation code here Sleep(2000); // to simulate a long process //OK, process over now; let's notify the client Fire_ExecutionOver(a+b); return S_OK; } //======================================================
进行一次构建,COM 就准备好了。确保 COM 已注册。
现在是客户端方面
创建一个新的 MFC AppWizard(exe)基于对话框的项目 - ConnectionClient
。它看起来像这样:
现在到了主要部分。
我们创建一个 CSink
类,它派生自 _IAddEvents
。您可以使用类向导来完成此操作。您必须提供 _IAddEvents
接口定义的头文件。为此,请将 ConnectionCOM.h 和 ConnectionCOM.tlb 文件复制到您的客户端 EXE 的项目文件夹中,并将以下行添加到 Sink.h 文件中:
#include "ConnectionCOM.h" #import "ConnectionCOM.tlb" named_guids raw_interfaces_only
现在,我们还有一项额外任务,即实现 _IAddEvents
接口中定义的每个方法。(永远不要忘记,COM 接口只是一个纯抽象基类,派生类必须实现其所有方法。)
所以,让我们实现第一个 ExecutionOver
。
STDMETHODIMP ExecutionOver(int Result) { CString strTemp; strTemp.Format("The result is %d", Result); AfxMessageBox(strTemp); return S_OK;; };
现在是 QueryInterface、AddRef 和 Release。
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **ppvObject) { if (iid == IID__IAddEvents) { m_dwRefCount++; *ppvObject = (void *)this; return S_OK; } if (iid == IID_IUnknown) { m_dwRefCount++; *ppvObject = (void *)this; return S_OK; } return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() { m_dwRefCount++; return m_dwRefCount; } ULONG STDMETHODCALLTYPE Release() { ULONG l; l = m_dwRefCount--; if ( 0 == m_dwRefCount) delete this; return l; }
我们现在几乎完成了。
现在,在对话框类的 SendToServer
按钮点击事件处理程序中,我们将完成最后一点代码。
#include "Sink.h" // for our CSink class #include <atlbase.h> // for ATL smart pointers void CConnectionClientDlg::OnSendToServer() //SendToServer button click event { UpdateData(1); HRESULT hr; //call CoInitialize for COM initialisation hr =CoInitialize(NULL); if(hr != S_OK) return -1; // create an instance of the COM object CComPtr<IAdd> pAdd; hr =pAdd.CoCreateInstance(CLSID_Add); if(hr != S_OK) return -1; IConnectionPointContainer * pCPC; //IConnectionPoint * pCP; //these are declared as a dialog's member //DWORD dwAdvise; //variables,shown here for completeness //check if this interface supports connectable objects hr = pAdd->QueryInterface(IID_IConnectionPointContainer, (void **)&pCPC); if ( !SUCCEEDED(hr) ) { return hr; } // //OK, it does; now get the correct connection point interface //in our case IID_IAddEvents hr = pCPC->FindConnectionPoint(IID__IAddEvents,&pCP); if ( !SUCCEEDED(hr) ) { return hr; } //we are done with the connection point container interface pCPC->Release(); IUnknown *pSinkUnk; // create a notification object from our CSink class // CSink *pSink; pSink = new CSink; if ( NULL == pSink ) { return E_FAIL; } //Get the pointer to CSink's IUnknown pointer (note we have //implemented all this QueryInterface stuff earlier in our //CSinkclass hr = pSink->QueryInterface (IID_IUnknown,(void **)&pSinkUnk); //Pass it to the COM through the COM's _IAddEvents //interface (pCP) Advise method; Note that (pCP) was retrieved //through the earlier FindConnectoinPoint call //This is how the com gets our interface, so that it just needs //to call the interface method when it has to notify us hr = pCP->Advise(pSinkUnk,&dwAdvise); //dwAdvise is the number returned, through which //IConnectionPoint:UnAdvise is called to break the connection //now call the COM's add method, passing in 2 numbers pAdd->Add(m_number1 ,m_number2); //do whatever u want here; once addition is here a message box //will pop up showing the result //pCP->Unadvise(dwAdvise); call this when you need to //disconnect from server pCP->Release(); return hr; }
现在,构建对话框 EXE。然后运行它。连接点的内容就到此为止。