在 COM & ATL 中使用用户定义类型
关于 COM 中的 SAFEARRAY 和 UDT 的分步教程
前言
我之所以开始做这件事,是因为我很少从新闻组或类似社区获得帮助。另一方面,由于我使用了CodeProject和CodeGuru上其他开发者/程序员提供的代码,因此加入其中一两个并随便看看似乎是合理的。
2000年5月上旬,我注意到有几篇关于UDT及其与VB和ATL交互的帖子。这时,我可以说我对这个主题没有任何实际经验。事实上,我从未用C++或ATL专业开发过COM。此外,我花了很长时间才意识到,不能将C或C++的编码技术应用于VB。尽管如此,我仍然认为自己在COM环境中是新手。
确实,在COM中实现UDT的帮助非常少,实现UDT数组的帮助更少。过去,在COM中使用UDT甚至不可想象。如今,COM对UDT提供了支持,但没有真正的示例项目展示如何使用此功能。因此,一位同行开发者的私人邮件启发了我继续深入研究。
我将逐步介绍创建一个ATL项目的方法,该项目使用UDT与VB客户端进行通信。使用C++客户端也会很容易。
本文档将逐步进行。我假设您熟悉ATL、COM和VB。沿途我可能会介绍我自己的实践,这些实践可能与本示例无关,但另一方面,您可能也使用过这些实践,或者初学者可能会从中受益。
创建ATL项目。
作为起点,请使用向导创建一个ATL DLL项目。将项目名称设置为UDTDemo,然后接受默认设置。现在让我们看一下生成的“IDL”文件。
//UDTDemo.IDL import "oaidl.idl"; import "ocidl.idl"; [ uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F), version(1.0), helpstring("UDTDemo 1.0 Type Library") ] library UDTDEMOLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); };
修改类型库名称
正如您所料,到目前为止,此文件中没有任何未知的内容。嗯,事实是我并不真正喜欢我在项目中创建的项目名称中添加的“Lib”部分,并且在将任何对象插入项目之前,我总是会更改它。这非常容易。
作为第一步,编辑“IDL”文件并将库名称设置为您喜欢的名称。您只需记住,在使用MIDL生成的代码时,此名称区分大小写。修改后的文件如下所示。
//UDTDemo.IDL import "oaidl.idl"; import "ocidl.idl"; [ uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F), version(1.0), helpstring("UDTDemo 1.0 Type Library") ] library UDTDemo //UDTDEMOLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); };
第二步是将先前库名称的每个出现替换为新名称。除了“IDL”文件之外,找到库名称的唯一文件是主项目实现文件“UDTDemo.cpp”,其中调用了DllMain并初始化了_module。您也可以使用工具栏上的“在文件中查找”命令搜索“UDTDEMOLib”字符串。
无论我们使用哪种方式,我们都必须将“LIBID_UDTDEMOLib”字符串替换为“LIBID_UDTDemo”。注意字符串的大小写。它区分大小写。
现在您可以将类型库的名称更改为您真正喜欢的任何名称。再次请记住,除非在将任何对象添加到项目中或在项目进行任何早期编译之前完成,否则这并不是一件简单的事。
下面是我们项目的修改后的DllMain函数。
//UDTDemo.cpp extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/) { if (dwReason == DLL_PROCESS_ATTACH) { //_Module.Init(ObjectMap, hInstance, &LIBID_UDTDEMOLib); _Module.Init(ObjectMap, hInstance, &LIBID_UDTDemo); DisableThreadLibraryCalls(hInstance); } else if (dwReason == DLL_PROCESS_DETACH) _Module.Term(); return TRUE; // ok }
现在您可以编译项目了。确保一切都做得正确。如果出现问题,您应确保将“UDTDEMOLib”的所有出现都替换为“UDTDemo”。
定义结构。
空项目毫无用处。我们的目的是定义一个UDT,或者相应地定义一个struct,我现在就这么做。
演示项目将处理命名变量。这意味着我们需要一个结构来保存变量的名称和值。虽然我还没有测试过,但我们可以添加一个VARIANT来保存其他一些特殊数据。
选择上述类型是为了让您能够看到整个过程,而无需任何家庭作业。:)
因此,请打开UDTDemo.idl文件,并在library块之前添加以下行。
//UDTDemo.idl typedef [ uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F), version(1.0), helpstring("A Demo UDT variable for VB projects") ] struct UDTVariable { [helpstring("Special case variant")] VARIANT Special; [helpstring("Name of the variable")] BSTR Name; [helpstring("Value of the variable")] long Value; } UDTVariable;
再次保存并生成。一切都应该可以毫无问题地编译。嗯,您需要按照这个进度进行这个演示项目。:)
用户定义数据类型。理论。
每当在IDL中创建结构时,都需要为其指定一个UUID,以便类型库管理器接口可以获取有关UDT的信息并访问它。(我这次也明白了为什么:)。
UUID
如何获得结构的UUID?不,我们不执行guidgen实用程序。我的下一个免费技巧就是这个。它可能不被批准,但它有效。转到库部分,复制库的UUID,然后将其粘贴到结构内部的`typedef`关键字之后,在尖括号内。然后转到第8位数字并减去(1)。
The library uuid(C21871A1-33EB-11D4-A13A-BE2573A1120F) | \./ The UDTVariable uuid(C21871A0-33EB-11D4-A13A-BE2573A1120F)
正如文档所述,UUID是使用当前日期、当前时间和计算机网卡的唯一编号创建的。嗯,日期和时间部分位于UUID的前八位数字部分。接下来的四位数字部分对我来说尚不清楚。其余部分是唯一的,我们可以说,它标识了您的PC。因此,从时间部分减去一(1)将我们带回过去。最后,这个UUID仍然是唯一的!
经验法则,在创建库UUID之后,对于我插入项目中的每个接口和coclass,我都会在UUID的时间部分加一(1)。减去一(1)表示我使用的结构或枚举。基本上,接口UUID将被替换,稍后将对此进行演示。
我陷入这种麻烦的唯一原因是,据说Windows在注册表中处理连续的UUID速度更快!
更多类型属性。
在定义了结构的UUID之后,我们定义其版本号。这是一个在检查VB创建对象的类型库后发现的技巧。VB为它添加到类型库中的每个对象都添加了一个版本号。这个版本号在此项目中永远不会被使用,但为什么不使用它呢?
然后添加一个帮助字符串。这个简短的描述对使用我们库的任何人来说都非常有用。我建议一直使用它。
我们也可以添加public关键字以使结构对库客户端可见。这不是必需的,因为它最终会隐式地对客户端可见。客户端不应该能够创建任何可能未在其对象接口中使用的结构。
UDT数据成员。
让我们继续处理数据成员。首先,我们UDT的每个数据成员都必须是自动化兼容类型。在我看来,在UDT中,我们只能使用VARIANT联合中定义的类型,如果您检查了代码,或者VB允许我们在variant类型中使用的任何内容。
这只是为了我们避免为我们的结构创建封送代码。否则,您可以自由地传递甚至带有结构的位数组:)。
我们UDT成员的数据类型选择得当,以便我们可以预期一些困难,并使演示尽可能完整。
-
long Value : 选择一个long类型的成员是因为它表现得像任何其他内置类型。内置类型(long、byte、double、float、DATE、VT_BOOL)不需要额外的考虑。
-
BSTR Name : 虽然VB中的字符串很容易处理,但在这里我们有一些需要考虑的地方。字符串的创建、初始化和销毁是使用字符串进行演示的好理由。
-
VARIANT Special : 这个刚刚出现。既然我们要这样做,那么变体比BSTR更难使用,不仅在初始化和终止方面,而且在检查实际变体的实际类型方面也是如此。这不算太糟!
结构成员中的数组。
此时,您应该知道如何在IDL中声明简单类型的结构。最后,既然您知道如何声明一个将在VB中使用的UDT结构,我们必须进行练习1并创建一个包含UDT数组的UDT。原因是数组也是特殊情况,而且由于我们一开始就没有在结构中放置数组,所以让我们以数组为例。使用long或其他类型的数组在此演示的这一点上是相同的。
//UDTDemo.idl typedef [ uuid(C218719F-33EB-11D4-A13A-BE2573A1120F), version(1.0), helpstring("A Demo UDT Holding an Array of Named Variables") ] struct UDTArray { [helpstring("array of named variables")] SAFEARRAY(UDTVariable) NamedVars; } UDTArray;
正如您所注意到的,唯一的区别是我们首先使用了SAFEARRAY声明,但也包含了我们新声明的UDT的名称。这是让COM机制了解数组将包含什么内容的正确方法。此时,我们声明了一个包含类型化数组的UDT。
声明一个long数组将很简单,如下声明。
SAFEARRAY(long)
进行测试。
我们可以再次编译我们的项目。此时,创建一个测试VB项目,并通过项目菜单中的引用将我们的库添加到此客户端项目中,这将很有用。现在按F2检查VB在我们的库中可以看到什么。嗯,窗口中只显示了globals。
这是因为我们在IDL文件中将UDT声明在library块之外。嗯,如果任何声明的项目(enum、UDT、Interface)未显式或隐式地导入到library块内,那么该项目对类型库的客户端来说是未知的(未知)。
让我们做一个简单的测试。保存VB项目,然后关闭它。否则,UDTDemo项目将无法通过链接步骤。在“UDTDemo.idl”文件内部,转到library块内并添加以下行。
//UDTDemo.IDL library UDTDemo //UDTDEMOLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); struct UDTVariable; struct UDTArray; };
再次生成UDTDemo项目,并打开VB演示项目。打开引用对话框,取消选中UDTDemo行,关闭它,然后通过引用再次使用UDTDemo注册VB项目。
现在打开对象浏览器,将显示我们在库中定义的两个UDT。关闭VB项目,并注释掉“UDTDemo.idl”文件中的前几行。这些结构将通过我们将要定义的接口隐式添加到库中。
测试结束。
我们UDT的大秘密是MIDL编译器将足够的信息附加到库中,以便它可以由IRecordInfo接口描述。因此,Ole Automation封送知道如何使用我们的UDT类型作为VT_RECORD类型。标识为记录的项目可以连接。因此,记录数组也可以。
还有一件事。SAFEARRAY(UDTVariable)声明是LPSAFEARRAY的typedef。这意味着该结构实际上声明为
struct UDTArray
{
LPSAFEARRAY NamedVars;
}
这导致我们得出结论,我们的代码中没有提供有关数组所持数据类型的信息。只有与类型库兼容的客户端才知道类型信息。
演示UDT对象
到目前为止,我们有一些真正无用的结构。我们可以将它们用于任何地方,除非在VB内部,如果我们更改“UDTDemo.idl”文件的话。
为了使我们的演示项目稍微有用一些,让我们向项目中添加一个对象。使用希望为人熟知的插入“新ATL对象”菜单项。在Atl对象向导中,选择“简单对象”,然后按“下一步”。
然后,在“ATL对象向导属性”中,键入“UDTDemoOb”作为短名称。我们可以使用任何喜欢的名称,但必须避免使用“UDTDemo”,因为它与库名称冲突。
然后,正如我总是建议的那样,在属性选项卡中,选中“支持ISupportErrorInfo”选项,将其保持为公寓线程,但正如我刚刚意识到的,也要选中对话框中的“支持连接点”。
现在按“确定”,向导将在IDL文件中为我们创建一个两个接口和一个coclass对象,以及一个实现UDTDemoOb接口的类。
我们检查了连接点的支持,因为当我们使用连接点接口的代理代码生成器时,如果任何参数是数组类型,代码一开始就远非正确。它会给出关于布尔值的轻微警告,并编译错误的代码。所以我们也要看一下。
此时,正如本文档开头所述,我将替换向导生成的UUID。你们其他人可以编译项目或与我一起检查。
如果您愿意,可以跳过此项暂时不要编译项目。
首先,复制库UUID并将其粘贴到为a) IUDTDemoOb接口、b) _IUDTDemoObEvents事件接口和c) UDTDemo coclass定义的每个UUID上方。复制UUID时,可以注释掉向导生成的UUID。然后,从上述顺序开始,为每个新出现的内容,将第一个部分增加一。代码部分将如下所示。
//UDTDemo.idl [ object, //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), //previous one uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), //library one, modified dual, helpstring("IUDTDemoOb Interface"), pointer_default(unique) ] interface IUDTDemoOb : IDispatch { }; [ //uuid(9117A523-34C3-11D4-A13A-AAA07458B90F), //previous one uuid(C21871A3-33EB-11D4-A13A-BE2573A1120F), //library one, modified helpstring("_IUDTDemoObEvents Interface") ] dispinterface _IUDTDemoObEvents { properties: methods: }; [ //uuid(9117A522-34C3-11D4-A13A-AAA07458B90F), //previous one uuid(C21871A4-33EB-11D4-A13A-BE2573A1120F), //library one, modified helpstring("UDTDemoOb Class") ] coclass UDTDemoOb { [default] interface IUDTDemoOb; [default, source] dispinterface _IUDTDemoObEvents; };
在上述项目中,您可能会注意到新创建的UUID在第一部分有所不同,并且是连续的。但是,这些与库的UUID在第一部分和第二部分都有所不同。事实是,这些UUID的创建时间比为库创建的UUID晚一天。
由于新创建的UUID是连续的,所以我们知道将它们替换为其他连续的UUID是可以的,这些UUID应该在过去已经被创建。
此时,还有三个与coclass UDTDemo对象的UUID出现。这些在“UDTDemo.rgs”文件中。因此,复制对象的新UUID,在编辑器中打开“.rgs”文件,并将旧UUID替换为新UUID。
以上操作适用于向导创建的所有对象。
// UDTDemoOb.rgs HKCR { UDTDemo.UDTDemoOb.1 = s 'UDTDemoOb Class' { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}' } UDTDemo.UDTDemoOb = s 'UDTDemoOb Class' { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F}' CurVer = s 'UDTDemo.UDTDemoOb.1' } NoRemove CLSID { ForceRemove { CLSID = s '{C21871A3-33EB-11D4-A13A-BE2573A1120F } = s 'UDTDemoOb Class' { ProgID = s 'UDTDemo.UDTDemoOb.1' VersionIndependentProgID = s 'UDTDemo.UDTDemoOb' ForceRemove 'Programmable' InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Apartment' } 'TypeLib' = s '{C21871A1-33EB-11D4-A13A-BE2573A1120F}' } } }
跳过区域结束
编译项目。确保一切正常。如果我们用VB客户端检查项目,此时,我们只会看到UDTDemo对象出现在对象浏览器中。这是正确的。
所以,让我们继续为我们的对象添加一个属性。使用属性向导添加一个名为UdtVar的属性,接受一个指向UDTVariable的指针。稍后我们将讨论指针。UDTVariable不在对话框的类型列表中,所以我们必须手动添加它。查看下面的图片。
按下[确定]按钮后,我们的接口看起来是这样的。
[ object, //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), dual, helpstring("IUDTDemoOb Interface"), pointer_default(unique) ] interface IUDTDemoOb : IDispatch { [propget, id(1), helpstring("Returns a UDT Variable in the UDTObject")] HRESULT UdtVar([out, retval] UDTVariable * *pVal); [propput, id(1), helpstring("Sets a UDT Variable in the UDTObject")] HRESULT UdtVar([in] UDTVariable * newVal); };
让我们先来看put属性。我们中的大多数人都知道,在put属性中,我们必须“按值”传递变量。这里我们定义了一个[in]变量,它是指向UDTVariable的指针。所以我们“按引用”传递变量。在C和C++领域,我们知道这样做更快。VB和COM也一样。在VB中处理类型和结构时,无论数据流向哪个方向,我们都必须使用byref声明。由被调用者负责强制执行传入数据的完整性,以便在方法返回时传入参数不变。
另一方面,get属性接受一个指向指针的参数。起初看起来是正确的,因为“指向指针”是对“指针”的引用,而get属性的参数类型始终声明为指向put属性参数类型的指针。
一如既往,当参数是out参数时,被调用者负责分配内存。这意味着我们必须在我们的get_方法中调用“new UDTVariable”。但VB无法理解指针。是吗?
上面的VB错误表明,VB无法在返回UDT的get方法中接受指向指针的参数。所以我们必须修改我们对象的get属性,只接受指向UDTVariable的指针。我们的方法仍然处理UDTVariable的内存分配。让我们看看。
VB dimension a UDTVariable Allocate memory for the UDT. The memory is sizeof( UDTVariable ). Pass the variables address to the object. Object allocates memory for UDT.Name Object initializes the string If object.special is not an integral type allocates memory for the type set the value of Object.Special.
因此,我们的get方法仍然负责为UDTVariable分配内存。它只是不分配UDTVariable主体内存。
所以在此之后,我们可以转到我们接口的get方法,并删除一个“*”字符。同时,将参数名称从pVal和newVal更改为“pUDT”。对于VB、C++客户端应用程序开发者来说,这稍微更清晰一些,因为这是在IDE环境中自动完成的。
我们还希望此属性成为默认属性。转到并用id(0)替换两个方法中的id(1)。我们的接口现在看起来像这样。
[ object, //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), dual, helpstring("IUDTDemoOb Interface"), pointer_default(unique) ] interface IUDTDemoOb : IDispatch { [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")] HRESULT UdtVar([out, retval] UDTVariable *pUDT); [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")] HRESULT UdtVar([in] UDTVariable *pUDT); };
但这还不够。我们必须告知CUDTDemoOb类接口中的更改。所以,转到头文件,从get_UdtVar方法中删除“*”,既然我们已经在这里了,就把参数的名称改为“pUDT”。对.cpp文件也做同样的处理。
这是CUDTDemoOb类中的修改。
//CUDTDemoOb.h STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT); STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT); //CUDTDemoOb.cpp STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT) STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT)
我们现在可以编译项目了。
那么这些关于不兼容的自动化接口的警告是什么?(“警告MIDL2039:接口不符合[oleautomation]属性”)您可以安全地忽略此警告。在几篇文章中都有提到。当MIDL编译器升级后,警告就会消失。(嗯,如果接口定义在Library块内部,情况可能并非如此)。
我们可以再次打开VB项目,并在对象浏览器中进行检查。该属性已存在,并为我们的对象声明。还有一个指向UDTVariable的引用。这是正确的,因为现在UDT通过IUDTDemoOb接口隐式插入到UDTDemo库中。
使用UDTVariable
所以,让我们回到UDTDemo库,让它做一些有用的事情。首先,我们需要CUDTDemoOb类中有一个UDTVariable成员。所以打开头文件,并为变量添加一个声明。
//CUDTDemoOb.h protected: UDTVariable m_pUDT;
我们还必须修改类的构造函数来初始化m_pUDT结构。我们还需要为类添加一个析构函数。
//CUDTDemoOb.h CUDTDemoOb() { CComBSTR str = _T("Unnamed"); m_pUDT.Value = 0; //default value zero (0) m_pUDT.Name = ::SysAllocString( str ); //default name "Unnamed" ::VariantInit( &m_pUDT.Special ); //default special value "Empty" } virtual ~CUDTDemoOb() { m_pUDT.Value = 0; //just in case ::SysFreeString( m_pUDT.Name ); //free the string memory ::VariantClear( &m_pUDT.Special ); // free the variant memory }
现在是时候在我们的类的属性中添加一些功能了。
当涉及到指针时,始终检查传入的NULL指针。所以,进入get_和put_属性的实现,并添加以下内容。
//CUDTDemoOb.cpp If( !pUDT ) return( E_POINTER );
现在进入put_UdtVar属性方法。我们必须做的是将传入变量的成员分配给我们的对象所持有的成员。对于Value成员来说这很容易,但对于其他两个,我们必须在分配新值之前释放它们分配的内存。这就是为什么我们选择了字符串和Variant。所以代码现在看起来像下面这样。
//CUDTDemoOb.cpp STDMETHODIMP CUDTDemoOb::put_UdtVar(UDTVariable *pUDT) { if( !pUDT ) return( E_POINTER ); if( !pUDT->Name ) return( E_POINTER ); m_pUDT.Value = pUDT->Value; //easy assignment ::SysFreeString( m_pUDT.Name ); //free the previous string first m_pUDT.Name = ::SysAllocString( pUDT->Name ); //make a copy of the incoming ::VariantClear( &m_pUDT.Special ); //free the previous variant first ::VariantCopy( &m_pUDT.Special, &pUDT->Special ); //make a copy return S_OK; }
正如所有伟大的作家所说,为了清晰起见,我们删除了错误检查:)。
您可能已经注意到,我们还检查了字符串Name是否为null。我们必须这样做。BSTRs声明为指针,所以这个字段可能是NULL。关键是,NULL指针不是一个空的COM字符串。一个空的COM字符串是长度为零的字符串。
方法返回后,我们的对象就拥有了传入结构的副本,这正是我们想要做的。
现在转到get_UdtVar方法。这与前面的方法相反。我们必须用对象内部UDT结构的值来填充传入的结构。
我们可以检查代码。
//CUDTDemoOb.cpp STDMETHODIMP CUDTDemoOb::get_UdtVar(UDTVariable *pUDT) { if( !pUDT ) return( E_POINTER ); pUDT->Value = m_pUDT.Value; //return value ::SysFreeString( pUDT->Name ); //free old (previous) name pUDT->Name = ::SysAllocString( m_pUDT.Name ); //copy new name ::VariantClear( &pUDT->Special ); //free old special value ::VariantCopy( &pUDT->Special, &m_pUDT.Special ); //copy new special value return S_OK; }
主要区别在于,现在传入UDT的Name和Special成员可能是NULL和Empty。这是允许的,因为我们的对象有义务填充结构。被调用者只负责分配UDT本身的内存,而不是其成员的内存。
为什么我们要释放传入的字符串?嗯,因为被调用者可能传递一个已经初始化的UDT。SysFreeString和VariantClear系统方法可以分别处理NULL字符串指针和空Variant。释放字符串可能会给我们带来错误。如果方法不是从VB调用的,则*Name* BSTR指针可能包含一个非NULL但无效的指针(垃圾)。所以这将会是
HRESULT hr = ::SysFreeString( pUDT->Name ); //free old (previous) name if( FAILED( hr ) ) return( hr ); //if for any reason there is error FAIL
编译项目,打开VB客户端项目,在窗体上添加一个按钮,并在那里进行一些赋值检查。
Private Sub cmdFirstTest_Click() Dim a_udt As UDTVariable ''define a couple UDTVariables Dim b_udt As UDTVariable Dim ob_udt As New UDTDemoOb ''declare and create a UDEDemoOb object a_udt.Name = "Ioannis" ''initialize one of the UDTS a_udt.Value = 10 a_udt.Special = 15.5 ob_udt.UdtVar = a_udt ''assign the initialized UDT to the object b_udt = ob_udt.UdtVar ''assign the UDT of the object to the second UDT ''put a breakpoint here and check the result in the immediate window End Sub现在试试这个。
b_udt = ob_udt.UdtVar ''assign the UDT of the object to the second UDT ''put a breakpoint here and check the result in the immediate window b_udt.Special = b_udt ''it actually makes a full copy of the b_udt b_udt.Special.Special.Name = "kostas" ''vb does not use references
UDT数组
所以,您可能会说,到目前为止我们还没有看到任何数组。这是我们的下一步。我们将向接口添加一个方法,该方法将返回UDT数组。它将接受两个数字作为输入,start和length,并返回一个具有length个项目的UDTVariables数组,其中包含连续值。
所以,转到UDTDemo项目,右键单击IUDTDemoOb接口,然后选择“添加方法”。
在对话框中,键入“UDTSequence”作为方法名称,并添加以下参数。“[in] long start, [in] long length, [out, retval] SAFEARRAY(UDTVariable) *SequenceArr”。按[Ok]键,让我们看看向导为我们添加了什么。
现在不要编译!
嗯,新方法的定义已经插入到IUDTDemoOb接口中。
//udtdemo.idl [ object, //uuid(9117A521-34C3-11D4-A13A-AAA07458B90F), uuid(C21871A2-33EB-11D4-A13A-BE2573A1120F), dual, helpstring("IUDTDemoOb Interface"), pointer_default(unique) ] interface IUDTDemoOb : IDispatch { [propget, id(0), helpstring("Returns a UDT Variable in the UDTObject")] HRESULT UdtVar([out, retval] UDTVariable *pUDT); [propput, id(0), helpstring("Sets a UDT Variable in the UDTObject")] HRESULT UdtVar([in] UDTVariable *pUDT); [id(1), helpstring("Return successive named values")] HRESULT UDTSequence([in] long start [in] long length, [out, retval] SAFEARRAY(UDTVariable) *SequenceArr); };
上面经过了一点编辑,以便在这里立即显示。我们应该已经知道那里有什么了。我们之前看到了SAFEARRAY(UDTVariable)是什么。这是指向包含UDTVariables的SAFEARRAY结构的指针的声明。所以SequenceArr实际上是指向SAFEARRAY指针的引用。到目前为止一切都很好。
现在让我们检查CUDTDemoOb类的头文件。
//udtdemoob.h public: STDMETHOD(UDTSequence)(/*[in]*/ long start, /*[in]*/ long length, /*[out, retval]*/ SAFEARRAY(UDTVariable) *SequenceArr); STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT); STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);
起初看起来是对的。不对。编译器没有宏或任何可见的东西来理解SAFEARRAY(UDTVariable)声明。正如我们在本文档开头所说,我们的代码永远不会有关于SAFEARRAY结构的足够类型信息。数组的类型信息应该在运行时检查。所以我们必须修改代码。将SAFEARRAY(UDTVariable)替换为SAFEARRAY *。
代码应该看起来像这样。
//udtdemoob.h public: STDMETHOD(UDTSequence)(/*[in]*/ long start, /*[in]*/ long length, /*[out, retval]*/ SAFEARRAY **SequenceArr); STDMETHOD(get_UdtVar)(/*[out, retval]*/ UDTVariable *pUDT); STDMETHOD(put_UdtVar)(/*[in]*/ UDTVariable *pUDT);
您可能已经意识到,我们必须修改CUDTDemoOb类的实现文件来纠正这个问题。嗯,令我惊讶的是,向导甚至没有添加SequenceArr的声明。
//udtdemoob.cpp STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length ) //Where is the SafeArray ? { return S_OK; }
正如您所看到的,我们必须添加SAFEARRAY **SequenceArr声明。另一方面,如果SequenceArr被声明了,只需像我们在头文件中一样替换它。
//udtdemoob.cpp STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length, SAFAARRAY **SequenceArr ) { return S_OK; }
现在我们可以编译项目了。再次检查VB客户端项目,在对象浏览器中查看新方法,并确认它返回UDTVariables数组。
所以回到UDTSequence的实现来开始添加检查代码。首先,我们必须测试传出的数组变量不为null。第二个检查是length变量。它不能小于或等于零。
//udtdemoob.cpp STDMETHODIMP CUDTDemoOb::UDTSequence(long start, longlength, SAFEARRAY **SequenceArr) { if( !SequenceArr ) return( E_POINTER ); if( length <= 0 ) { HRESULT hr=Error(_T("Length must be greater than zero") ); return( hr ); } return S_OK; }
您可能会注意到Error方法的用法。这是ATL提供的,非常容易通知客户端错误,而不会遇到太多麻烦。
下一步是检查实际的数组指针。解引用的那个。此时有两种可能性。它可能为NULL,这是可以的,因为我们返回数组,或者它包含非零值,假设它是一个数组,我们就清除它并创建一个新的。
所以方法继续。
//udtdemoob.cpp STDMETHODIMP CUDTDemoOb::UDTSequence(long start, long length, SAFEARRAY **SequenceArr) { if( !SequenceArr ) return( E_POINTER ); if( length <= 0 ) { HRESULT hr = Error( _T("Length must be greater than zero") ); return( hr ); } if( *SequenceArr != NULL ) { ::SafeArrayDestroy( *SequenceArr ); *SequenceArr = NULL; } return S_OK; }
创建数组
现在我们可以创建一个新数组来保存命名变量的序列。我们的第一个想法是使用::SafeArrayCreate方法,因为我们不知道我们到底需要什么。搜索MSDN库,在文档中我们找不到关于UDT的内容。另一方面,::SafeArrayCreateEx方法暗示它可以创建一个Record(UDT)数组。
与普通版本一样,此方法需要访问SAFEARRAYBOUND结构、维度数、数据类型和指向IRecordInfo接口的指针。所以,按部就班。a)我们需要一个“记录”数组,使用VT_RECORD,b)我们只需要一个(1)维度,c)我们需要一个零基数组(lbound)带长度(cbElements)。好的。这是我们目前所拥有的。
SAFEARRAYBOUND rgsabound[1]; rgsabound[0].lLbound = 0; rgsabound[0].cElements = length; *SequenceArr = ::SafeArrayCreateEx(VT_RECORD, 1, rgsabound, /*what next ?*/ );
再次搜索MSDN,发现了“::GetRecordInfoFromGuids”方法。实际上有两个,但这个方法似乎更容易用于本教程。此方法的参数是:
-
rGuidTypeLib:包含UDT的类型库的GUID。在我们的例子中是UDTDemo库,LIBIID_UDTDemo
-
uVerMajor:UDT类型库的主版本号。此库的版本是(1.0)。所以主版本是1。
-
uVerMinor:UDT类型库的次版本号。在本例中为零(0)。
-
lcid:调用者的区域设置ID。通常零是默认值。使用零。
-
rGuidTypeInfo:描述UDT的typeinfo的GUID。这是UDTVariable的GUID,但它在任何地方都找不到。
-
ppRecInfo:指向RecordInfo对象上IRecordInfo接口的指针。我们将此指针传递给“::SafeArrayCreateEx”方法。
所以,进入IDL文件,复制UDTVariable结构的uuid,并将其粘贴到实现文件开头。然后将其声明为一个正式的UUID结构。
所以这个“C21871A0-33EB-11D4-A13A-BE2573A1120F”变成
//udtdemoob.cpp const IID UDTVariable_IID = { 0xC21871A0, 0x33EB, 0x11D4, { 0xA1, 0x3A, 0xBE, 0x25, 0x73, 0xA1, 0x12, 0x0F } };
现在,我们准备在*UDTSequence*函数内部创建一个未初始化的UDTVariable结构数组。
////////////////////////////////////////////////// //here starts the actual creation of the array ////////////////////////////////////////////////// IRecordInfo *pUdtRecordInfo = NULL; HRESULT hr = GetRecordInfoFromGuids( LIBID_UDTDemo, 1, 0, 0, UDTVariable_IID, &pUdtRecordInfo ); if( FAILED( hr ) ) { HRESULT hr2 = Error( _T("Can not create RecordInfo interface for" "UDTVariable") ); return( hr ); //Return original HRESULT hr2 is for debug only } SAFEARRAYBOUND rgsabound[1]; rgsabound[0].lLbound = 0; rgsabound[0].cElements =length; *SequenceArr = ::SafeArrayCreateEx( VT_RECORD, 1, rgsabound, pUdtRecordInfo ); pUdtRecordInfo->Release(); //do not forget to release the interface if( *SequenceArr == NULL ) { HRESULT hr = Error( _T("Can not create array of UDTVariable " "structures") ); return( hr ); } ////////////////////////////////////////////////// //the array has been created //////////////////////////////////////////////////
现在我们创建了一个未初始化的数组,并且必须向其中放入数据。此时您也可以在VB中进行测试,以检查该方法是否返回具有预期大小的数组。即使没有数据。
如果您收到HRESULT错误代码“找不到元素”,请确保您已正确键入UDTVariable_IID。
此时,您还应该知道,系统为数组分配的内存是零初始化的。这意味着Value和Name成员被初始化为零(0),而Special成员被初始化为VT_EMPTY。这在希望区分数组中已初始化或未初始化的槽时非常有用。
向数组添加数据
有两种方法可以用数据填充数组。一种是使用::SafeArrayPutElement方法逐个添加,另一种是使用*::SafeArrayAccessData*来更快地操作数据。根据我的经验,当我们需要访问单个元素时,我们将使用前者;当我们需要对数组所持有的整个数据范围进行计算时,我们将使用后者。
结构的安全数组在内存中显示为*普通结构数组*。起初可能会产生误解,认为SAFEARRAY中的每个数组项都包含记录信息。事实并非如此。对于整个数组,只有一个IRecordInfo或ITypeInfo指针。SAFEARRAY使用了一个简单的旧技巧。它们分配内存来保存SAFEARRAY结构,但在开头还会分配更多内存来保存必要的指针。MSDN库中有此说明。
所以现在我们将创建两个内部方法来演示将数据输入数组的两种方式。
首先,我们将使用::SafeArrayPutElement方法。在CUDTDemoOb类的声明中,插入此方法的声明。此方法应声明为受保护的,因为它将仅由类本身内部调用。
//udtdemoob.h protected: HRESULT SequenceByElement(long start, long length, SAFEARRAY *SequenceArr);
与UDTSequence方法唯一的区别是,这个方法只接受SAFEARRAY的指针。而不是SAFEARRAY(UDTSequence)中使用的指针到指针。
填充数组的算法非常简单。对于数组中的每个UDTVariable,我们将从*start*开始的连续值设置到我们结构体的Value成员中,将此数字转换为BSTR并分配给结构的Name成员。最后,将Special成员的值设置为long或double类型,并为其分配相同的数值,只是在使用double版本时添加*“0.5”*以获得不同的数据。
在我们类的实现文件中添加方法定义。
//udtdemoob.cpp HRESULT CUDTDemoOb::SequenceByElement(long start, long length, SAFEARRAY *SequenceArr) { return( S_OK ); }
我们可能会跳过检查此方法中的传入变量,因为这些方法应该只在类内部调用,并且在调用这些方法之前已经进行了初步检查。
//udtdemoob.cpp HRESULT CUDTDemoOb::SequenceByElement(long start, long length, SAFEARRAY *SequenceArr) { HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound ); if( FAILED( hr ) ) return( hr ); return( S_OK ); }
要执行的第一个检查是数组的下界。虽然我们声明我们处理零基数组,但有人可能会传递一个特殊边界的数组。在VB中很容易获得一个一维数组。这也是一种知道我们拥有有效SAFEARRAY指针的方法。
以下代码将数字转换为字符串,并将字符串值分配给*a_udt*结构体的*Name*成员。
//udtdemoob.cpp HRESULT CUDTDemoOb::SequenceByElement(long start, long length, SAFEARRAY *SequenceArr) { HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound ); if( FAILED( hr ) ) return( hr ); hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR ); hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name ); return( S_OK ); }
您可以在随附的项目中看到代码,因此我们将解释大局。在循环内部执行此行。
//udtdemoob.cpp HRESULT CUDTDemoOb::SequenceByElement(long start, long length, SAFEARRAY *SequenceArr) { HRESULT hr = SafeArrayGetLBound( SequenceArr, 1, &lBound ); if( FAILED( hr ) ) return( hr ); hr = ::VariantChangeType( &a_variant, &a_udt.Special, 0, VT_BSTR ); hr = ::VarBstrCat( strDefPart, a_variant.bstrVal, &a_udt.Name ); hr = ::SafeArrayPutElement( SequenceArr, &i, (void*)&a_udt ); return( S_OK ); }
在这行代码中,系统将在数组的第i个位置添加*a_udt*。我们必须知道的是,在此调用中,系统会完整复制我们传递的结构。系统之所以能够执行*完整复制*,是因为使用了我们在创建数组时使用的IRecordInfo接口。因此,我们必须释放BSTR或VARIANT持有的任何内存。在我们目前的情况下,我们只释放*a_variant*变量,因为它持有唯一分配的字符串的引用。
让我们转到::SafeArrayAccessData方法,看看有什么不同。第一个变化是,现在我们使用UDTVariable的指针*p_udt*。第二个大的区别是,在循环内部只有设置结构成员的代码,通过指针。唯一实际访问数组的代码是在循环之外,使用访问和释放数据实际驻留的内存的方法。循环内部还有一个检查。
//udtdemoob.cpp //.... if( p_udt->Name ) ::SysFreeString( p_udt->Name ); //....
这表明,由于我们不干扰地访问数据,我们在将其分配给数据之前必须释放为BSTR字符串、VARIANT甚至接口指针分配的任何内存。正如前面所指出的,检查NULL值可能足以进行此简单演示。
我希望很明显,当需要访问数组中的所有或大部分数据时,调用第二个方法——*::SafeArrayAccessData*——更好,但如果您想一次修改一两个元素,使用*::SafeArrayGetElement*和*::SafeArrayPutElement*对方法也可能更合适。
作为最后一步,在*UDTSequence*方法体末尾插入以下行,并使用VB客户端项目进行测试。您可以注释掉您喜欢的任何内容,以查看其工作原理,并发现它们都给出相同的结果。
// hr = SequenceByElement( start, length, *SequenceArr );
hr = SequenceByData( start, length, *SequenceArr );
静态数组
我们的方法存在设计上的缺陷。它只能返回一个*动态*创建的数组。这意味着数组是在堆上创建的。尝试在VB中添加以下行并进行检查。
dim a_udt_arr(5) as UDTVariable dim a_udt_ob as UDTDemoOb a_udt_arr = UDTDemoOb.UDTSequence(15, 5) ''Error here
嗯,一致数组,我认为这就是它们所谓的,在本演示中只能作为* [in]*参数提供。所以,暂时向我们的*UDTSequence*方法添加一个额外的检查。另一个问题是,数组总是作为*双重引用*指针传递。
所以,让我们尝试修改数组的方法。
在接口中再添加一个属性
像集合一样称它为*Item*。签名将是
//udtdemoob.idl [propput, .....] Item( [in] long index, [in] SAFEARRAY(UDTVariable) *pUDTArr, [in] UDTVariable *pUDT ); [propget, .....] Item( [in] long index, [in] SAFEARRAY(UDTVariable) *pUDTArr, [out, retval] UDTVariable *pUDT );
我们添加此项的原因是演示对传入数组的一些检查。正如您可能从方法定义中猜到的那样,即使数组声明为* [in]*,它们仍然可以以任何方式修改。我们的第一个检查是查看它是否是*UDTVariable*结构的数组。由于此检查至少在两个方法中执行,因此我们可以将其放入对象实现类中的受保护函数中。
正如您所注意到的,我们的对象仍然不保留关于传入数组的任何状态。
HRESULT IsUDTVariableArray( SAFEARRAY *pUDTArr, bool &isDynamic )
您可能期望的唯一区别是声明末尾的bool引用。嗯,这个检查函数将能够告知我们*a)*我们是否真的可以修改数组(通过重新分配内存来追加或删除项,甚至销毁并重新创建数组),*b)*我们是否只能修改插入到数组中的单个UDTVariable结构。前者功能将不会在此演示项目中实现。
我们的第一个检查是传入数组的维度数。我们希望它是单维的。阅读本教程后,您可以将其扩展到多维数组,尽管存在一个小问题。
long dims = SafeArrayGetDim( pUDTArr ); if( dims != 1 ) { hr = Error( _T("Not Implemented for multidimentional arrays") ); return( hr ); }
下一步是检查数组是否已创建为保存结构。这可以通过检查传入数组的特征标志是否指示记录支持来轻松完成。
unsigned short feats = pUDTArr->fFeatures; //== 0x0020; if( (feats & FADF_RECORD) != FADF_RECORD ) { hr = Error( _T("Array is expected to hold structures") ); return( hr ); }
最后一步是比较数组所持结构体的名称与我们的名称。为此,我们必须访问数组所持的IRecordInfo接口指针。
IRecordInfo *pUDTRecInfo = NULL; hr = ::SafeArrayGetRecordInfo( pUDTArr, &pUDTRecInfo ); if( FAILED( hr ) && !pUDTRecInfo ) return( hr );
现在进行比较。
BSTR udtName = ::SysAllocString( L"UDTVariable" ); BSTR bstrUDTName = NULL; //if not null. we are going to have problem hr = pUDTRecInfo->GetName( &bstrUDTName); if( VarBstrCmp( udtName, bstrUDTName, 0, GetUserDefaultLCID()) != VARCMP_EQ ) { ::SysFreeString( bstrUDTName ); ::SysFreeString( udtName ); hr = Error(_T("Object Does Only support [UDTVariable] Structures") ); return( hr ); }
在随附的项目中,还有一些额外的检查作为演示,但只能通过调试器进行。实现Item属性在此之后很简单。
使用VARIANT
我认为到目前为止还不够,因为我们还没有讨论过使用我们的结构体与variant。所以,让我们再给我们的对象添加一个属性。向我们的接口添加以下定义。
HRESULT VarItem([in] long items, [out, retval]
LPVARIANT pUdtData );
现在,进入*CUDTDemoOb*类实现文件中新属性的定义,让我们做一些事情。
首先是一些检查。普通的null指针检查,然后检查VARIANT是否包含任何数据。如果不为空,我们应该清空它。
if( !pUdtData ) return( E_POINTER ); if( pUdtData->vt != VT_EMPTY ) ::VariantClear( pUdtData );
下一步是实现算法,该算法返回*a)*单个UDTVariable结构(如果项变量等于或小于一(1))。*b)*结构数组(如果项大于一(1))。
在这两种情况下,我们都必须将传出VARIANT的类型设置为VT_RECORD,这是访问VARIANT pUdtData变量的唯一相似之处。对于单个UDTVariable结构,我们必须将VARIANT的pRecInfo成员设置为有效的IRecordInfo接口指针。这之前已经演示过了。然后将新结构分配给variant的pvRecord成员。另一方面,返回数组,我们必须将传出VARIANT的类型更新为VT_ARRAY类型。然后,我们将一个已构建的数组分配给variant的parray成员。两种分配都很容易完成,因为我们已经在对象中实现了相应的属性和方法。
if( items <= 1 ) { IRecordInfo *pUdtRecordInfo = NULL; hr = ::GetRecordInfoFromGuids( LIBID_UDTDemo, 1, 0, 0, UDTVariable_IID, &pUdtRecordInfo ); if( FAILED( hr ) ) { HRESULT hr2= Error( _T("Can not create RecordInfo" "interface for UDTVariable") ); return( hr ); } //assign record information on the variant pUdtData->pRecInfo = pUdtRecordInfo; pUdtRecordInfo = NULL; //MIND. we do not release the interface. //VariantClear should pUdtData->vt = VT_RECORD; pUdtData->pvRecord= NULL; hr= get_UdtVar( (UDTVariable*) &(pUdtData->pvRecord) ); } else { //here the valid pointer of the union is the array. //so the array holds the record info. pUdtData->vt = VT_RECORD | VT_ARRAY; hr = UDTSequence(1, items, &(pUdtData->parray) ); }
我认为这对于UDT与COM的基本教程来说已经足够了。没有定义接口来访问类型库中定义的第二种类型*UDTArray*,但此时这应该很简单(我骗了你:)。在演示项目中,我已经显式地将结构添加到了库体中,所以您可以在VB中进行尝试。
事件中的“安全数组”
我还说过,向导为创建的接口创建的代码存在缺陷,用于返回任何类型的数组。这在实现*VarItem*方法时已部分解决。项目中演示了一个事件方法。以下是对此生成的方法所做的更改。
假设我们中的许多人都没有在控件中使用过事件,我将对此做更具体的说明。
让我们开始连接点的旅程。首先,我们必须向*IUDTDemoObEvents*接口添加一个方法。这是此方法的签名。到目前为止,您已经掌握了理解此方法签名的知识。此外,到目前为止只有*UDTDemo.idl*发生了变化。
[id(1), helpstring
("Informs about changes in an array of named vars")]
HRESULT ChangedVars(SAFEARRAY(UDTVariable) *pVars);
再次编译项目,并在VB客户端中检查*Object Browser*。您可能会看到对象中声明的事件。
现在项目已编译,并且*UDTDemo*类型库已更新,我们可以更新*CUDTDemoOb*类以使用*IUDTDemoObEvents*接口。在项目窗口中,右键单击*CUDTDemoOb*类,然后从弹出菜单中选择*实现连接点*。
在接下来的对话框中,选择(选中)*_IUDTDemoObEvents*接口,然后按[ok]。
向导现在已将一个文件添加到项目中。“UDTDemoCP.h”,其中实现了CProxy_IUDTDemoEvents< class T >模板类,并处理UDTDemoOb coclass对象的事件接口。CUDTDemoOb类现在继承自新生成的代理类。
代理类包含*Fire_ChangedVars*方法,该方法已实现,我们可以从类的任何位置调用它来触发事件。
所以,让我们进入*UDTSequence*方法的实现,仅用于演示和触发事件。
//UDTDemoOb.cpp - UDTSequence method //hr = SequenceByElement( start, length, *SequenceArr ); hr = SequenceByData( start, length, *SequenceArr); return Fire_ChangedVars( SequenceArr ); //<<---- changed here //return S_OK;
现在编译项目,并观察输出。
warning C4800: 'struct tagSAFEARRAY ** ' : forcing value to bool 'true' or 'false' (performance warning)
这实际上不是一个警告。这是一个实现错误,会导致运行时问题。让我们仅为演示目的看一下。再次打开VB客户端,并在演示窗体的声明中添加以下内容。我希望您知道*WithEvents*关键字的含义。
Dim WithEvents main_UDT_ob As UDTDemoOb
也要更新以下内容。
Private Sub Form_Load() Set main_UDT_ob = New UDTDemoOb End Sub Private Sub Form_Unload(Cancel As Integer) Set main_UDT_ob = Nothing End Sub Private Sub main_UDT_ob_ChangedVars(pVars() As UDTDemo.UDTVariable) Debug.Print pVars(1).Name, pVars(1).Special, pVars(1).Value End Sub
在事件处理程序中的调试语句中设置断点,并运行客户端。看看我们得到了什么。
在独立执行中,我们得到
嗯,实际错误是以下内容,并且应该是预期的错误,因为我们知道警告。这是在VC++调试器中发现的,作为Invoke方法的返回HRESULT。
0x80020005 == Type Mismatch
是时候检查向导为我们生成的代码了。
HRESULT Fire_ChangedVars(SAFEARRAY * * pVars) { CComVariant varResult; T* pT = static_cast<T*>(this); int nConnectionIndex; CComVariant* pvars = new CComVariant[1]; int nConnections = m_vec.GetSize(); for( nConnectionIndex = 0; nConnectionIndex < nconnections; nConnectionIndex++) { pT->Lock(); CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex); pT->Unlock(); IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p); if (pDispatch != NULL) { VariantClear(&varResult); pvars[0] = pVars; DISPPARAMS disp = { pvars, NULL, 1, 0 }; pDispatch->Invoke( 0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, &varResult, NULL, NULL); } } delete[] pvars; return varResult.scode; }
让我们检查出问题的代码行。
CComVariant* pvars = new CComVariant[1]; int nConnections = m_vec.GetSize();
这逻辑上假定可能有一个以上的客户端连接到我们的对象实例。但是没有错误检查意味着至少期望一个客户端连接。这是向导代码,所以它应该执行一些检查。我们不应该知道IConnectionPointImpl ATL类的每一个细节。
int nConnections = m_vec.GetSize(); if( !nConnections ) return S_OK; CComVariant* pvars = new CComVariant[1];
当然,我在夸大其词,但这是我做这类事情的方式。
这最后一行错误地假定我们的对象只连接了一个客户端。每次在循环中调用Invoke时,varResult变量都被设置为被调用方法的返回值。既没有检查varResult是否返回任何错误代码,也没有检查Invoke方法本身的返回值,后者在我们的项目中给出了正确的错误。因此,在这种情况下,调用事件方法将成功或失败,具体取决于是否通知了与我们的UDTDemoOb对象连接的最后一个对象。考虑使用具有连接客户端的Single Instance Exe Server!
pDispatch->Invoke( 0x1, .. . return varResult.scode;
这并不是要责怪任何人,因为如果我们想要每个连接的错误处理,我们应该自己去做。只需记住,您需要根据项目来处理它。
实际问题
pvars[0] = pVars;
CComVariant不处理任何种类的数组。但是,由于它直接派生自VARIANT结构,因此很容易修改代码以正确处理。
//pvars[0] = pVars; pvars[0].vt = VT_ARRAY | VT_BYREF | VT_RECORD; pvars[0].pparray = pVars;
要通过VARIANT传递任何种类的数组,您只需定义数组的*VT_Type*,或与*VT_ARRAY*类型进行或运算。与我们之前的例子唯一的区别是,这里我们也使用了*VT_BYREF*参数。这很有必要,因为我们有一个*指向指针的指针*参数。当然,*VB中的byref*意味着我们使用variant联合的*pparray*成员。对于包含字符串的数组,它将是
pvars[0].vt = VT_ARRAY | VT_BSTR; //array to strings pvars[0].parray = ... pvars[0].vt = VT_ARRAY | VT_BYREF | VT_BSTR; //pointer to array to strings pvars[0].pparray = ...
同样,虽然我们处理的是包含UDT结构的数组,但我们不需要在variant中设置IRecordInfo接口。
编译项目并尝试一下。不要害怕,除非您更改了项目的idl文件,否则代码不会改变。这就是为什么我们首先在事件(sink)接口中定义所有方法,然后在对象中实现连接点接口。
最后说明
正如你们中的许多人可能注意到的,这已经写了很长时间了。现在发布它的原因是,我需要为我正在进行的一个演示项目使用用户定义结构(UDT),并且这篇文章在其实施过程中确实很有帮助。所以,我希望它对开发者社区来说也值得一读且有帮助。
参考文献
MSDN Library
Platform SDK \ Component Services \ COM \ Automation \ User Defined Data Types. Extending Visual Basic with C++ DLLs, by Bruce McKinney. April 1996
MSJ杂志
Q&A ActiveX / COM, by Don Box. MSJ November 1996 Understanding Interface Definition Language: A Developer's survival guide, by Bill Hludzinski MSJ August 1998.
书籍
Beginning ATL COM Programming, by Richard Grimes, George Reilly, Alex Stockton, Julian Templeman, Wrox Press, ISBN 1861000111 Professional ATL COM Programming, by Richard Grimes. Wrox Press. ISBN 1861001401