原文:http://www.codeproject.com/Articles/14183/COM-in-plain-C-Part-5
添加连接对象(源、接收器)
下载例程-246Kb
内容
通常,对于一个我们调用的DLL函数来说“回调”我们自己的一个函数是比较方便的,这样我们可以在特定的时刻做些其他工作,或者接受些发生的事情的通知。例如,标准C库函数qsort,它的第四个参数是一个指向我们提供的比较两个内容项函数的指针。这样,当我们调用qsort时,它让我们来决定内容项排序的规则,而qsort本身做的实际工作是重新排序这些项。
我们不能提供随意函数给qsort。写qsort的家伙严格指定了我们的“比较”回调函数的参数、返回值,当然也严格指定了回调函数的目的。此外,qsort决定什么时候真正调用我们的回调函数(因为是qsort它调用我们的函数)。设计qsort的家伙要求我们的比较回调函数必须定义成这样:
int (__cdecl *compare)(const void *elem1,const void *elem2);
我们来创建我们自己的qsort版本,我们叫它Sort。(实际上,为了简便起见我们只做了一个冒泡排序)。假设我们把它放到一个叫ISort.dll的DLL中。
不是每次传给Sort一个指向比较函数的指针,我们这样设计,所以应用程序首先调用单独的SetCompare函数让我们的DLL把指针存在全局变量中。同时我们也在DLL中放一个名为UnsetCompare的函数来供应用程序调用来清除函数指针。因此我们DLL源代码是这样(我们把它放在文件ISort.C中):
// 存储指向应用程序Compare函数的全局变量 int (STDMETHODCALLTYPE *CompareFunc)(const void *, const void *); void STDMETHODCALLTYPE SetCompare( int (STDMETHODCALLTYPE *compare)(const void *, const void *)) { // Save the compare function ptr in a global // 把compare函数指着存储到全局变量中 CompareFunc = compare; } void STDMETHODCALLTYPE UnsetCompare(void) { CompareFunc = 0; } HRESULT STDMETHODCALLTYPE Sort(void *base, DWORD numElems, DWORD sizeElem) { void *hi; void *p; void *lo; void *tmp; // Has the app set its Compare function pointer yet? // 应用程序还没有设置Compare函数指针? if (!CompareFunc) return(E_FAIL); // Do the (bubble) sort // 排序(冒泡) if ((tmp = GlobalAlloc(GMEM_FIXED, sizeElem))) { hi = ((char *)base + ((numElems - 1) *sizeElem)); lo = base; while (hi > base) { lo = base; p = ((char*)base + sizeElem); while (p <= hi) { if ((*CompareFunc)(p, lo) >0) lo = p; (char *)p += sizeElem; } CopyMemory(tmp, lo, sizeElem); CopyMemory(lo, hi, sizeElem); CopyMemory(hi, tmp, sizeElem); (char *)hi -= sizeElem; } GlobalFree(tmp); return(S_OK); } return(E_OUTOFMEMORY); }
现在我们来写一个使用我们DLL来排序5个DWORD数组的应用程序。首先应用程序调用SetCompare来指定它的比较回调函数(我们给他命名为Compare)。然后,应用程序调用Sort。最后当我们的应用程序使用完Sort时,为了确保其他对Sort的调用不会碰巧调用到我们的Compare函数,我们调用UnSetCompare。
// 排序5个DWORD的数组 DWORD Array[5] = {2, 3, 1, 5, 4}; // 比较函数 int STDMETHODCALLTYPE Compare(const void *elem1, const void *elem2) { // 比较两个元素。我们知道我们传给Sort()的是一个DWORD数组 if (*((DWORD *)elem1) == *((DWORD *)elem2)) return 0; if (*((DWORD *)elem1) < *((DWORD *)elem2)) return -1; return 1; } int main(int argc, char **argv) { // 把我们Compare函数指针传给ISort.dll SetCompare(Compare); // 排序5个DWORD数组 Sort(&Array[0], 5, sizeof(DWORD)); UnsetCompare(); return 0; }
COM也有一种简单实现回调的方法,但正如你担心的那样,它不像我们上面的例子那样通俗易懂。在ISort目录下,你会找到实现我们上面Sort DLL的COM对象的文件。
首先,我们必须把我们的Sort函数放在某个COM对象中。最终我们是要把ISort.dll转换成一个COM组件。我们使用微软提供的DECLARE_INTERFACE_宏定义一个ISort对象。像所有的COM对象一样,它的VTable以QueryInterface、AddRef和Release函数开始。我们不用麻烦地给它添加IDispatch函数。(我们的ISort对于脚本语言,比如VBScript不可用)。然后我们添加Sort函数作为第一个额外函数。这是我们放在ISort.h中的定义:
#undef INTERFACE #define INTERFACE ISort DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS)PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD (Sort) (THIS_ void *, DWORD, DWORD) PURE; };
这样我们的应用程序可以获得ISort对象的一个实例,调用他的Sort函数。你现在应该有写获取一个COM对象和调用它的函数的C/C++应用程序的经验了。这没什么新意。
当然,我们需要运行GUIDGEN.EXE来为ISort对象和它的VTable生成GUID。我们分别给他们命名为CLSID_ISort和IID_ISort,把他们放在ISort.h中。
// ISort对象的GUID // {619321BA-4907-4596-874A-AEFF082F0014} DEFINE_GUID(CLSID_ISort, 0x619321ba, 0x4907, 0x4596, 0x87, 0x4a, 0xae, 0xff, 0x8, 0x2f, 0x0, 0x14); // ISort VTable的GUID // {4C9A7D40-D0ED-45ea-9520-1CB9095973F8} DEFINE_GUID(IID_ISort, 0x4c9a7d40, 0xd0ed, 0x45ea, 0x95, 0x20, 0x1c, 0xb9, 0x9, 0x59, 0x73, 0xf8);
像我们通常做的那样,我们需要至少添加一个额外、私有的数据程序到ISort对象中-引用计数成员。于是,我们在ISort.c中定义一个MyRealISort添加这个额外的私有数据成员到我们的ISort中:
typedef struct { ISortVtbl *lpVtbl; // ISort的VTable DWORD count; // ISort的引用计数 }MyRealISort;
至此,所做的一切对你而言再熟悉不过了。
剩下要做的是为应用程序提供某种方法,让它给我们一个指向它的Compare函数供我们Sort函数调用的指针。那我们是简单添加SetCompare和UnsetCompare函数到ISort VTable可以嘛?不。
在这它开始变得复杂。微软要求一种标准的方法来让一个应用程序提供它的回调函数给一个COM对象。微软规定应用程序应该把它的回调函数封装到一个COM对象中。特定的COM对象嘛?这是自然。
还记得qsort作者规定回调函数的参数和返回值嘛?好,因为是我们写的ISort对象,我们得构造我们自己对象,应用程序必须使用按我们需要封装的回调函数。所以我们就定义我们自己的对象,我们给它命名为ICompare。当然,我们必须以QueryInterface、AddRef和Release三个函数作为它的VTable开始部分。那么我们接着该做什么?让我们看看…我们要支持脚本语言提供给我们一个比较函数嘛(比如一个VBScript函数)?如果支持,我们接下来要放IDispatch函数。不,我们跳过这部分,现在只是允许C、C++应用程序向我们提供一个比较的回调函数。
我们向ICompare添加一个额外的函数。我们叫它Compare。这是应用程序的回调比较函数。(所以现在你明白应用程序必须封装它的回调到我们定义的COM对象中了)。这是我们的ICompare定义(我们把它添加在ISort.h中):
#undef INTERFACE #define INTERFACE ICompare DECLARE_INTERFACE_ (INTERFACE, IUnknown) { STDMETHOD (QueryInterface) (THIS_REFIID, void **) PURE; STDMETHOD_ (ULONG, AddRef) (THIS)PURE; STDMETHOD_ (ULONG, Release) (THIS) PURE; STDMETHOD_ (long, Compare) (THIS_const void *, const void *) PURE; };
我们得运行GUIDGEN.EXE来给ICompare的VTable生成一个GUID。(对象本身不需要GUID)。我们给它命名为DIID_Compare。(微软习惯在每个封装回调的VTable的GUID名字前加一个D。我们也遵从这个惯例)。
// ICompare VTable的 GUID // {4115B8E2-1823-4bbc-B10D-3D33AAA12ACF} DEFINE_GUID(DIID_ICompare, 0x4115b8e2, 0x1823, 0x4bbc, 0xb1,0xd, 0x3d, 0x33, 0xaa, 0xa1, 0x2a, 0xcf);
我们回过头来看我们的应用程序。现在我们必须在我们的应用程序代码里放一个COM对象。很明确,我们必须创建一个ICompare对象,同时把我们的Compare回调函数作为它的额外函数。因为我们只需要一个ICompare对象,所以我们简单把它声明为静态的。我们还必须#include ISort.h,因为ICompare对象、它的VTable和它的GUID定义都在它里面。因此这是我们插入到上面应用程序代码中替换Compare函数的代码:
#include "ISort.h" // 这是我们应用程序的ICompare对象。我们只需要一个, // 所以我们就把它声明为静态的。这样,我们不需要给它分配内存、释放它, // 也不需要维持引用计数。 static ICompare MyCompare; // ICompare的QueryInterface。它返回一个指向ICompare对象的指针。 HRESULT STDMETHODCALLTYPE QueryInterface(ICompare *this, REFIID vTableGuid, void **ppv) { // 由于只是一个ICompare对象。我们必须识别定义在ISort.h中的 // ICompare VTable的GUID。ICompare也伪装成一个IUnknowm。 if (!IsEqualIID(vTableGuid, &IID_IUnknown) && !IsEqualIID(vTableGuid, &DIID_ICompare)) { *ppv = 0; return(E_NOINTERFACE); } *ppv = this; // 通常情况下,我们要在这调用AddRef。但由于我们的ICompare没有分配内存, // 我们不用麻烦来做引用计数。 return(NOERROR); } ULONG STDMETHODCALLTYPE AddRef(ICompare *this) { // 我们只有一个没有分配的,而是在(MyCompare)上面申明为静态的ICompare。 // 所以我们就返回1。 return(1); } ULONG STDMETHODCALLTYPE Release(ICompare *this) { // 我们唯一的ICompare没有动态分配内存,所以我们不需要 // 为它的释放问题而担心。 return(1); } // ICompare的额外函数,它叫Compare。当我们调用ISort的 // Sort函数时ISort对象会调用它。 long STDMETHODCALLTYPE Compare(ICompare *this, const void *elem1, const void *elem2) { // 做两个元素的比较操作。我们知道我们传给Sort()的是一个DWORD数组。 if (*((DWORD *)elem1) == *((DWORD *)elem2)) return 0; if (*((DWORD *)elem1) < *((DWORD *)elem2)) return -1; return 1; } // 我们的ICompare VTable。我们只需要一个,所以把它申明为静态的。 static const ICompareVtbl ICompare_Vtbl = {QueryInterface, AddRef, Release, Compare};
注意此时在这我们的Compare函数只是简单封装到一个ICompare对象里。(此时传给它的第一个参数是一个指向我们ICompare对象的指针,在此例中它是我们静态声明的MyCompare)。
现在我们剩下的工作有点绕。我们的应用程序如何把它的ICompare对象给我们的ISort对象(在ISort.dll中)呢?我真希望我能把它说得简单易懂,但很遗憾,这要归咎于MS设计整个模式的家伙们的混乱设计的结果,这将是一段穿越恐怖房间的痛苦之旅。
为了让应用程序把它的ICompare对象传给我们的ISort对象,我们必须往ISort对象中加点东西。很明确,我们必须添加两个子对象给它。(希望你读了上一章知道如何创建一个拥有子对象的对象)。
好消息是微软已经把这两个子对象(和它们的VTable GUID)定义在和你的编译器一起的包含文件中。这两个子对象叫IConnectionPointContainer和IConnectinPoint。我们要做的是把它们加到ISort对象中。由于我们已经定义了一个MyRealISort,我们就把这两个子对象嵌入到它里面。因为我们知道应用程序会给我们一个指向ICompare对象的指针,我们也在MyRealISort里添加一个地方来保存它。
typedef struct { ISortVtbl *lpVtbl; DWORD count; IConnectionPointContainer container; IConnectionPoint point; ICompare *compare; }MyRealISort;
注意我们的基对象是ISort本身。我们可以把它符号化成这样:
typedef struct { ISort iSort; DWORD count; IConnectionPointContainer container; IConnectionPoint point; ICompare *compare; }MyRealISort;
但由于ISort只有一个lpVtbl成员,两个定义等价。
你还记得在上一章,基对象的QueryInterface必须识别它自己的VTable和所有子对象VTable的GUID。所以我们的ISort QueryInterface看起来必须是这样:
HRESULT STDMETHODCALLTYPE QueryInterface(ISort *this, REFIID vTableGuid,void **ppv) { // 因为IConnectionPointContainer是我们ISort的一个子对象, // 如果应用程序请求这个子对象时我们必须返回一个指向这个子对象的指针。 // 因为我们把IConnectionPointContainer对象嵌入到我们的MyRealISort中, // 我们可以用指针运算的方式很容易得到子对象。 if (IsEqualIID(vTableGuid, &IID_IConnectionPointContainer)) *ppv = ((unsigned char *)this + offsetof(MyRealISort, container)); else if (IsEqualIID(vTableGuid, &IID_IUnknown) || IsEqualIID(vTableGuid, &IID_ISort)) *ppv = this; else { *ppv = 0; return(E_NOINTERFACE); } this->lpVtbl->AddRef(this); return(NOERROR); }
等等。我们忘记还要检查我们的IConnectionPoint子对象的VTable GUID,返回一个指向它的指针了吧?不,我们没忘。遗憾的是,MS发明这种设计的家伙破坏了规则。IConnectionPoint子对象不能通过调用基对象的QueryInterface来获得(也不能通过IConnectionPointContainer子对象)。马上,我们会明白它是怎么被获得的。
IConnectionPointContainer子对象提供两个功能。
首先,通过把IConnectionPointContainer VTable的GUID传给我们的ISort的QueryInterface函数,应用程序可以确定我们的ISort是否接受回调函数。顺便说一下,在COM文档中,这种对象(也就是提供IConnectionPointContainer的对象)称为事件源(sourcingevents)。如果QueryInterface返回指向IConnectionPointContainer的指针,那么对象支持事件源。(如果不支持,那么返回0)。
其次,IConnectionPointContainer有一个获得IConnectionPoint子对象的函数。这个函数叫FindConnectionPoint。
那么我们来写我们IConnectionPointContainer子对象的函数。因为它是ISort的一个子对象,且ISort是基对象,我们IConnectionPointContainer的QueryInterface、AddRef和Release函数简单委托给ISort的QueryInterface、AddRef和Release。
STDMETHODIMP QueryInterface_Connect(IConnectionPointContainer *this, REFIID vTableGuid, void **ppv) { // 因为它是我们ISort(也就是MyRealISort)对象的一个子对象, // 我们委托给ISort的QueryInterface。同时因为我们把IConnectionPointContainer // 直接嵌入到MyRealISort中,我们要做的只是获得ISort的一点指针运算。 return(QueryInterface((ISort *)((char *)this - offsetof(MyRealISort,container)), vTableGuid, ppv)); } STDMETHODIMP_(ULONG) AddRef_Connect(IConnectionPointContainer *this) { // 因为是ISort的一个子对象,我们把它委托给ISort的AddRef() // 来增加ISort的引用计数。 return(AddRef((ISort *)((char *)this - offsetof(MyRealISort,container)))); } STDMETHODIMP_(ULONG) Release_Connect(IConnectionPointContainer *this) { //因为是ISort的一个子对象,我们把它委托给ISort的Release() // 来减小ISort的引用计数。 return(Release((ISort *)((char *)this - offsetof(MyRealISort, container)))); }
现在,我们给EnuumConnectionPoints函数占个位置(stub)。
STDMETHODIMP EnumConnectionPoints(IConnectionPointContainer *this, IEnumConnectionPoints **enumPoints) { *enumPoints = 0; return(E_NOTIMPL); }
真正的工作在FindConnectionPoint中。应用程序传给我们一个让我们通过它返回ISort的IConnectionPoint子对象的句柄。因为我们已经把它直接嵌入到MyRealISort中,定位起来相当容易。
应用程序还把我们定义的ICompare(回调)对象的VTable GUID传给我们。我们知道应用程序真正意义上提供给的是我们需要的正确的对象。如果应用程序事先没有给我们一个ICompare对象,我们不应该返回一个IConnectionPoint。
STDMETHODIMP FindConnectionPoint(IConnectionPointContainer *this, REFIID vTableGuid, IConnectionPoint **ppv) { // 应用程序要我们返回一个它可以使用这个对象来把它的ICompare对象 // 传给我们的IConnectionPoint对象嘛?应用程序通过传给我们 // ICompare VTable GUID(定义在ISort.h中)来请求这个对象。 if (IsEqualIID(vTableGuid, &DIID_ICompare)) { MyRealISort *iSort; // 应用程序显然要把它的ICopmare对象给我们的ISort。 // 为了做的这一点,我们需要给应用程序一个标准的IConnectionPoint。 // 这做起来很容易,因为我们把IConnectionPointContainer和IConnectionPoint // 都嵌入到我们的ISort中了。我们只需要做一点指针运算。 iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort,container)); *ppv = &iSort->point; // 因为我们给应用程序的指针指向我们的IConnectionPoint // 和IConnectionPoint是ISort的子对象,我们需要增加ISort的引用计数。 // 做起来最容易方法是调用我们的IConnectionPointContainer的AddRef, // 因为我们在这要做的只是委托给ISort的AddRef。 AddRef_Connect(this); return(S_OK); } // 我们的ISort不支持其他应用程序的回调对象。我们定义支持的是一个ICompare对象。 // 通知应用程序我们不知道他传给我们的其他GUID,也不给他IConnectionPoint对象。 *ppv = 0; return(E_NOINTERFACE); }
这就是我们IConnectionPointContainer的函数。现在我们需要写我们的IConnectionPoint的函数了。
因为IConnectionPoint也是ISort的一个子对象(就像IConnectionPointContainer一样)。它的QueryInterface、AddRef和Release会委托给ISort的QueryInterface、AddRef和Release。复制这些函数不麻烦,因为他们跟IConnectionPointContainer的几乎一样。
现在,我们给EnuumConnections函数占个位置(stub)。
GetConnectionInterface函数只是简单拷贝ICompare的VTable的GUID到一个应用程序传入的缓冲区中。稍后,我们会知道它的用途。
GetConectionPointContainer函数返回一个指向创建这个IConnectionPoint对象的IConnectionPointContainer对象。这意味着,只要应用程序占用着我们IConnectionPointContainer给它的IConnectionPoint子对象,纳闷我们的IConnectionPointContainer必须保留。这没问题,因为我们我们已经把我们的IconnnectionPointContainer和IConnectionPoint都嵌入到我们的MyRealISort中了(且我们的MyRealISort在它的所有子对象和基对象Release之前一直存在)。
上面三个函数不是很重要,所以我只提醒你看一下Sort.c中的源代码就可以了。
真正的工作是在Advise和Unadvise函数里。应用程序实际上是这样把它的ICompare给我们。Advise实质上和我们的SetCompare函数做了同样的工作。而Unadvise做我们UnsetCompare的工作。
我们来看Advise:
STDMETHODIMP Advise(IConnectionPoint *this, IUnknown *obj, DWORD*cookie) { HRESULT hr; MyRealISort *iSort; // 得到IConnectionPoint子对象所属的我们的MyRealISort。 // 因为IConnectionPoint子对象是直接嵌入在它的MyRealISort中, // 我们只需要做一点指针运算。 iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort, point)); // 我们的ISort只允许一个ICompare,所以检查应用程序是否已经调用了我们 // 的Advise()、我们已经获得了。如果是,告诉应用程序它给我们的ICompare // 超过了我们允许的数量。 if (iSort->compare) return(CONNECT_E_ADVISELIMIT); // 好,我们还没有从应用程序获得我们允许的ICompare。 // 我们通过调用应用程序传给我们的对象的QueryInterface // 来得到应用程序的ICompare对象。 // 我们传入ICompare VTable的GUID(定义在ISort.h中)。 // 把应用程序的ICompare指针保存到我们ISort的compare成员中, // 这样我们就可以在我们需要的时候得到它。 hr = obj->lpVtbl->QueryInterface(obj, &DIID_ICompare,&iSort->compare); // 我们需要返回(给应用程序)一个提供给下面的我们Unadvise() // 函数定位应用程序ICompare线索的值。 // 最简单的方式就是用应用程序的ICompare指针来做返回值。 *cookie = (DWORD)iSort->compare; return(hr); }
实际上,应用程序不传入一个指向它的ICompare对象指针。这么容易、直截了当、易懂和显而易见不是设计它的微软程序员的目标。相反,应用程序传给我们一个应用程序对象,我们可以通过请求它来让应用程序把它的ICompare给我们。我们的Advise函数应该调用这个对象的QueryInterface函数,传入ICompare VTable的GUID来请求应用程序的ICompare。我猜微软认为做双重检查来确定应用程序真的把ICompare给了我们是一个好的方法(但你不知道如果一个应用程序写的非常差劲,它不知道怎样使用Advise,而它的QueryInterface或许又会返回一个错误的对象)。当然,应用程序的QueryInterface会自动对它给我们的ICompare做一次AddRef,所以在我们使用完候应该Release应用程序的ICompare。
我们的Advise函数把应用程序的ICompare指针存储在我们的MyRealISort的compare成员中,所以我们可以在需要的时候访问和完全Release它。
Advise必须返回一个我们自己选择的DWORD值给应用程序。应用程序应该保存这个DOWRD值,以便在后面把它传给我们的Unadvise函数。我们可以选择我们希望的任何DWORD值,但这个值应该。当然,应用程序随后在它不再希望我们调用它的回调函数时调用我们的Unadvise,然后Release它的ICompare。因此让我们看看Unadvise:
STDMETHODIMP Unadvise(IConnectionPoint *this, DWORD cookie) { MyRealISort *iSort; // 得到IConnectionPoint子对象所属的我们的MyRealISort。 // 因为IConnectionPoint子对象是直接嵌入在它的MyRealISort中, // 我们只需要做一点指针运算。 iSort = (MyRealISort *)((char *)this - offsetof(MyRealISort, point)); // 用传入的值来查找我们存储的它的ICompare指针。 // 好,因为那我们的ISort只允许一个ICompare,我们已经知道我们把它存储在 // ISort->compare成员中。且Advise()把这个指针当做“cookie”值返回了。 // 因此我们马上就已经得到了ICompare。 // 我们就确信他传入的cookie是我们真正想要的指针。 if (cookie && (ICompare *)cookie == iSort->compare) { // 释放应用程序的ICompare ((ICompare *)cookie)->lpVtbl->Release((ICompare *)cookie); // 我们不再拥有应用程序的ICompare,所以清楚ISort的compare成员。 iSort->compare = 0; return(S_OK); } return(CONNECT_E_NOCONNECTION); }
Sort.c中其余的代码与我们第一章中的IExample.c的几乎一样。唯一不同之处是我们的IClassFactory的CreateInstance分配完MyRealISort后,我们必须初始化它里面的子对象和其他私有数据成员。
你可以把我们的COM对象编译成一个ISORT.DLL。用ISort替换IExample的注册工具(在RegIExample目录下)中所有IExample实例来注册ISORT.DLL。
在ISortApp目录里有一个使用我们ISort DLL的C应用程序例程。前面我们讨论了ICompare函数,所以在这新东西是应用程序如何调用我们的Advise函数来把它的ICompare对象给我们,随后调用Unadvice来请求我们释放它。这是一个相当小的例子,所以你可以仔细阅读一下ISortApp.c中的注释。
因为COM事件连接是相当复杂,我给出第二个使用回调对象的例程。在IExampleEvts目录下是另一个COM DLL的源代码。这个DLL实现了一个拥有一个DoSomething额外函数的对象(IExampleEvts)。这个函数可以调用五个不同的应用程序回调。所以我们定义了一个IFeedback对象来封装这五个回调函数。它的定义在IExampleEvts.h。(也就是一个回调对象可以包含几个额外的回调函数。且每个函数可以实现不同的目的、使用不同的参数和返回不同的值)。
花点时间细读这些例子,充分理解它。接下来会更复杂。
为了支持脚本语言,我们需要添加IDispatch函数。让我们把我们的IExampleEvts COM 对象修改成为脚本语言可以使用的。
首先,我们创建一个IExampleEvts2的新目录,并把原始源文件拷贝进去。然后我们重命名这些文件,把所有的IExampleEvts实例用IExampleEvts2替换,所有的IFeedback用IFeedback2替换。我已经替你做了。我还运行GUIDGEN.EXE给这些对象和他们的VTable生成了新的GUID。因为我们也需要一个类型库,我也给它创建了一个新的GUID。
我们需要给我们的IExamleEvts和IFeedback2添加IDispatch函数。你可以比较原始的IExampleEvts.h和新的IExampleEvts2.h看有什么改变。注意两个VTable现在添加了5个标准IDisaptch函数,且也用宏立刻指定了两个VTable基于IDispatch(而不是IUnknown)。
在IExampleEvts2.c中,我已经添加了5个IDispatch函数给我们的IExampleEvts2对象和它的静态声明的VTabl。我也添加了一个静态的MyTyeInfo变量来保存微软的GetTypeInfoOfGuid为我们创建的ITypeInfo(我们对它使用DispInvoke)。我们要做我们在第二章中给我们的IExample2对象添加支持一样的事情。至此这些改变已没有什么新的了。
我们必须修改我们的DoSomething函数。我们不能再直接调用回调函数了(指的是通过对象的lpVtbl)。这是因为脚本引擎实际上在它的VTable中没有那些额外回调函数指针。相反,我们必须间接使用回调对象(IFeedback)的Invoke函数调用它。我们必须传入我们要调用函数的相应的DISPID。
在我们的IExampleEvts2.IDL文件中有点新东西(也就是MIDL.EXE编译生成的我们的.TLB类型库文件的文件)。注意我们定义IFeedback2的VTable的方法(我们随意标注它为IFeedback2VTbl)。IFeedback2 VTable与我们定义的IExampleEvts2 VTable有点类似,除了我们对它使用dual关键字。脚本引擎不能使用一个双VTable的回调对象。我随意给第一个回调函数(Callback1)使用选择了DISPID 1,给第二个回调函数为2,等等。
其他新东西是我们这样定义IExampleEvts2对象自身:
coclass IExampleEvts2 { [default] interface IExampleEvts2VTbl; [source, default] interface IFeedback2VTbl; }
第一行的interface看起来应该很熟悉。它与我们在IDL文件中通常标记一个对象的VTable是一样的。而第二行的interface告诉使用我们类型库的人IExampleEvts2支持一个VTabel被描述为IFeedback2Vtbl的回调对象。“default”关键字意味着这个VTable是脚本引擎应该用来与我们的IExampleEvts2对象关联的回调对象。
你可以把我们的COM对象编译成IExampleEvts2.DLL。用IExampleEvts2替换IExample的注册工具(在RegIExample目录下)中所有IExample实例来注册它。
在IExampleEvts2App目录下是一个使用我们IExamlpleEvts2 DLL的C例程。与IExampleEvtsApp的源文件进行比较,会注意到我们给IFeedback2对象添加了5个标准的IDispatch函数。
尽管IExampleEvts2.idl没有把IFeedback2 VTable定义成“dual”,我们仍然把额外函数(Callback1到Callback5)包含到我们当前静态声明的VTable(IFeedback2_Vtbl)中。这样,我们欺骗DispInvoke把这个VTable当成声明为dual来使用。还要注意的是我们加载的类型库是IExampleEvts2的类型库,因为IFeedback2的VTable描述在它里面。同时我们让GetTypeInfoOfGuid来为IFeedback2 VTable创建一个ITypeInfo。我们使用这个ITypeInfo来调用我们的IFeedback2的Invoke函数。
在IExampleEvts2目录下有一个如下的IExampleEvts2.vbs的VBScript例程:
Set myobj = WScript.CreateObject("IExampleEvts2.object","label_") myobj.DoSomething() sub label_Callback1() Wscript.echo "Callback1 is called" end sub
第一行用windows Scripting Shell的CreateObject获取一个我们IExampleEvts对象的实例(应该用IExampleEvts2.objectProdID注册过了)。我们使用Script Shell的CreateObject而不是VB引擎的CreateObject的原因是因为这个模板(former)允许我们传入第二个参数-事先定义的回调函数名字符串。在这,我们指定这个字符串为label。这样比如,当我们的IExampleEvts2的DoSomething调用脚本引擎的IFeedback2的Invoke传入DISPID是1(也就是Callback1),那么VBScript调用的是label_Callback1。
实际上,我们只通过名字提供了一个显示消息框的VB函数。
一个对象可以支持几种类型的回调对象。例如,如果我们想的话,我们可以让我们的ISort对象支持接受ICompare和IFeedback2两个回调对象。在这种情况下,我们要给每个对象单独提供IConnectionPoint对象。在这张情况下,我们需要把ICompare和IFeedback2 VTabel都放到IDL文件中,在我们对象中它们是这样的:
coclass IExampleEvts2 { [default] interface IExampleEvts2VTbl; [source, default] interface IFeedback2VTbl; [source] interface ICompareVtbl; }
但注意只有一个source interface可以标记为default,同时脚本引擎只使用一个缺省对象。(也就是说脚本不能提供给上面的ICompare回调函数给我们)。
此外,我们必须给我们的IConnectionPointContainer的EnumConnectionPoints函数提供真正的代码。这个函数应该返回另一个可以枚举我们支持的每个不同类型的IConnectonPoint对象。(它返回的对象是一个像我们在我们的Collections章节中使用的标准枚举器对象)
因为回调对象通常用在交互脚本中(脚本引擎不能支持每一个单独coclass对象的多类型的回调对象),我会进一步深入研究多类型回调对象。
在我们对象例程中,我们允许应用程序(或者脚本引擎)只给我提供一个IFeedback2对象(我们把它存储在MyRealIExampleEvts2的feedback成员中)。但理论上,应用程序可以给我们提供它希望的多个IFeedback2回调对象。在我们IConnectionPoint的Advise中,我们直接拒绝接受从一个应用程序、引擎那里请求的多个IFeedback2。如果你要允许应用程序、引擎提供多个IFeedback2,那么不能用feedback成员,你不妨另外定义一个结构,结构可以连接成一个列表,结构有一个存储IFeedback2指针的成员。你在Advise中GlobalAlloc每个结构成员,把他们链接到列表头存储在MyRealIExampleEvts2 成员中的列表中。
你还需要修改DoSomething,循环整个列表,调用每个IFeedback2的Invoke。
不过,由于使用回调对象主要是脚本,而大部分只使用一个回调对象,我们跳过开发这种情况的例子。