摘要:本文概述用 Microsoft Visual Basic 编制 SAX2 接口的方法。
简介
May 2000 MSXML Technology Preview 的关键功能之一是实现了 SAX2 (Simple API for XML, version 2)。MSDN XML 开发人员中心提供的题为 XML 开发人员的 SAX2 快速入门一文和可下载的 Microsoft® Visual C++® 应用程序,可作为 SAX2 的简介。在本文中,我将概述用 Visual Basic® 编制 SAX2 接口的方式。请注意,不对本示例提供技术支持,本示例的目的仅是帮助您建立 SAX/Visual Basic 解决方案的原型。此外,应该清楚的是本例中的接口不会对 Microsoft Visual Basic 将来对 SAX 的支持产生影响。
世界,你好!
在有了 Visual Basic (VB) 后,使用组件对象模型 (COM) 组件(例如 MSXML3.dll)的典型方式是创建新的标准 EXE 工程,然后进入工程/引用菜单,添加对 MSXML3.dll 类型库(Microsoft XML,版本 3.0)的引用。此时可以用 Visual Basic 对象浏览器来查看所选接口的属性、方法和事件。
我是用 MSXML3.dll 完成这些任务的,然后徒劳地查找 SAX2 接口,但是在该类型库中没有找到任何远程 SAX-y。然后我查看了注册表以确定是否安装了正确的 MSXML3.dll 版本。当然是这样的,在 HKEY_CLASSES_ROOT 目录下还有两个很有可能的 ProgIDs(特别是 Msxml2.SAXXMLReader 和 Msxml2.SAXXMLReader.3.0),所以我连续在新闻组中询问了类似于“我丢失了什么东西吗?”的问题。很快就有答复指出我实际上丢失了一些东西。
使用来源,Luke
SAX2 接口是在称为 Xmlsax.idl 的文件中定义的。如果将 MSXML3.dll 安装到默认文件夹中,则可以在称为 C:\Program Files\Microsoft XML Parser SDK\inc 的文件夹中找到 Xmlsax.idl。在找到该文件后,我将 Xmlsax.idl 编译到称为(非常合适的)Xmlsax.tlb 的类型库中。MIDL IDL 是我用来进行编译的工具。
回到 Visual Basic 集成开发环境 (IDE) 中,我返回到工程/引用菜单,这次我从引用对话框中选择了浏览以便找到新的 Xmlsax.tlb 文件。用这种方法选择类型库的结果,VB IDE 首先注册了类型库文件。这就振奋多了。现在 SAX2 接口可以显示在“对象浏览器”中了。
字符构造材料
对某些方法参数的检查证实了新闻组的答复对我的建议:接口对 Visual Basic 有一点不友好。请记住,Microsoft XML SDK 3.0 中的文档将 SAX2 接口描述为“Microsoft 的 SAX2 的 COM/C++ 实现”。总而言之,在 Visual Basic 开发人员期望看到一个字符串参数的地方实际上是两个参数。这些参数的第一个有“pwch”匈牙利前缀(“到通配符数组的指针”?),第二个有“cch”前缀(“字符计数”?)。
下面是 StartElement 方法的 ISAXContentHandler IDL 的样子:
HRESULT StartElement( [in] const wchar_t * pwchNamespaceUri, [in] int cchNamespaceUri, [in] const wchar_t * pwchLocalName, [in] int cchLocalName, [in] const wchar_t * pwchQName, [in] int cchQName, [in] ISAXAttributes * pAttributes);
好在新闻组的答复提供了一段处理这些参数的 Visual Basic 代码。该函数的简化版本如下。
Public Function UnicodeArrayToString(pwchArray As Integer, ByVal lCharCount As Long) As StringDim sText As String' 调整字符串大小为正确的字符数sText = String$(lCharCount, 0)' 复制这些值。请注意大小是字符数的两倍,’因为字符串是 Unicode(双字节)Call CopyMemory(ByVal StrPtr(sText), iUnicodeCharArrayFirstElt, lCharCount * 2)UnicodeArrayToString = sTextEnd Function
XMLSAX 接口返回 Visual Basic 整型数组,加上数组中的项目数。UnicodeArrayToString 函数使用未公开的 Visual Basic StrPtr 函数,加上对 CopyMemory Windows API 的调用,以便将这些值转换为 Visual Basic 字符串。CopyMemory 是 Windows API RtlMoveMemory 的别名。
用于 Visual Basic 的 SAX2“快速入门”
我们现在可以开始在 Visual Basic 中编写“SAX2 快速入门”的版本了。为了获得类似于 C++“快速入门”应用程序的结果,请使用下面的指令。注意不需要对 MSXML 的引用。
Option ExplicitPrivate Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSource As Any, ByVal ByteLen As Long)Implements XMLSAX.ISAXContentHandlerPrivate Sub Command1_Click()Call ParseEnd SubPrivate Function Parse() As BooleanDim i() As IntegerDim str As StringDim sax As SAXXMLReader30Set sax = New SAXXMLReader30Call sax.PutContentHandler(Me)str = App.Path & "\" & "test.xml"' 调整数组大小为字符的数目ReDim i(Len(str))' 复制内存,注意其大小是字符数的两倍,' 因为它是 unicode(双字节)字符串 CopyMemory i(0), ByVal StrPtr(str), Len(str) * 2' 忽略下面中的第一个矩阵项sax.ParseURL i(0), Len(str)End FunctionPrivate Sub ISAXContentHandler_Characters(pwchChars As Integer, ByVal cchChars As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_EndDocument()'什么也不做End SubPrivate Sub ISAXContentHandler_EndElement(pwchNamespaceUri As Integer, ByVal cchNamespaceUri As Long, pwchLocalName As Integer, ByVal cchLocalName As Long, pwchQName As Integer, ByVal cchQName As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_EndPrefixMapping(pwchPrefix As Integer, ByVal cchPrefix As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_IgnorableWhitespace(pwchChars As Integer, ByVal cchChars As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_ProcessingInstruction(pwchTarget As Integer, ByVal cchTarget As Long, pwchData As Integer, ByVal cchData As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_PutDocumentLocator(ByVal pLocator As XMLSAX.ISAXLocator)'什么也不做End SubPrivate Sub ISAXContentHandler_SkippedEntity(pwchName As Integer, ByVal cchName As Long)'什么也不做End SubPrivate Sub ISAXContentHandler_StartDocument()End SubPrivate Sub ISAXContentHandler_StartElement(pwchNamespaceUri As Integer, ByVal cchNamespaceUri As Long, pwchLocalName As Integer, ByVal cchLocalName As Long, pwchQName As Integer, ByVal cchQName As Long, ByVal pAttributes As XMLSAX.ISAXAttributes)Dim str As String' 调整字符串大小为正确的字符数str = String$(cchLocalName, 0)' 复制这些值。请注意大小是字符数的两倍,' 因为它是 unicode(双字节)字符串CopyMemory ByVal StrPtr(str), pwchLocalName, cchLocalName * 2Debug.Print strEnd SubPrivate Sub ISAXContentHandler_StartPrefixMapping(pwchPrefix As Integer, ByVal cchPrefix As Long, pwchUri As Integer, ByVal cchUri As Long)'什么也不做End Sub
将它包装起来
在克服了用 Visual Basic 编写 SAX2 程序的最初困难后,我决定,最好长期封装所有 Visual Basic 內部 SAX2 介面包装类別中棘手的要素。
工作结果可以在本文的下载地点看到。它虽不完整,但是演示了所有要点。该示例代码可以编译为 ActiveX® 组件 (VBXMLSAX),然后重复使用(在 Visual Basic、VBScript、JScript® 或者甚至 Visual C++ 中),并不需知道实现的细节。对那些希望了解某些实现问题的人来说,请阅读它们。
停止分析器,我想离开!
基于事件的分析器(如 SAX2)的强大功能之一,是能在与用户定义条件匹配时停止分析。例如,您可能希望在 XML 文档中遇到称为“UNINTERESTING”的元素时停止分析。SAX2 处理程序接口允许用户停止分析。我们尝试将处理程序接口上的方法表示为自己的包装程序类中的 Visual Basic 事件,使得事件接收方(例如 VB 窗体对象)可以控制分析的终止。
对接口的处理程序家族文档的观察告诉我们,处理程序方法通过将返回值 (HRESULT) 设置为 ERR_FAIL 符号值,来表示需要停止分析。只有一个问题:VB 隐藏了 HRESULT,因此无法在自定义处理程序代码中设置该值。
实际上是可以设置该值的,但是必须跳过某些环节。该技术称为“vtable 修改”,并被描述在 Bruce McKinney 的书 Hardcore Visual Basic 中。在运行时,有可能将调用重新定向,从 Visual Basic 所提供事件处理程序定向为用户自己的函数。该技术的关键是使用 AddressOf 操作符来获得重载的函数地址,然后使用 CopyMemory 来覆盖相应接口的 vtable 项。由于重载函数是由用户(不是 Visual Basic)定义的,因此返回值是可控制的。
让我们来看一个示例。ISAXContentHandler 接口提供称为 StartDocument 的方法。如果我们创建实现 ISAXContentHandler (CVBSAXContentHandler) 的 Visual Basic 类包装程序,那么 Visual Basic 将提供与下面类似的处理程序原型:
Private Sub ISAXContentHandler_StartDocument()
在后面的函数原型实际上是:
Public Function ISAXContentHandler_StartDocument(ByVal This As ISAXContentHandler) As Long
实际上返回类型是 HRESULT,但是可以使用 Visual Basic Long 数据类型来存储 HRESULT 值。因此,为了重载 StartDocument 方法,我们用 BAS 文件中的后一种原型创建了函数。在运行时调用的是这个重载函数而不是 Visual Basic 类包装程序中的程序。请注意增加的参数 This。它是所调用的 ISAXContentHandler 处理程序的实例。下面是 StartDocument 的重载函数:
Public Function ISAXContentHandler_StartDocument(ByVal This As ISAXContentHandler) As Long #If iDebug = -1 Then Debug.Print "VTable Replacement for ISAXContentHandler_StartDocument"#End If Dim booAbort As Boolean Dim objVBSAXContentHandler As CVBSAXContentHandler Set objVBSAXContentHandler = This Call objVBSAXContentHandler.StartDocument(booAbort) If booAbort Then ISAXContentHandler_StartDocument = E_FAIL Else ISAXContentHandler_StartDocument = S_OK End IfEnd Function
使用该技术有一个问题。AddressOf 操作符只能用于在标准模块(BAS 文件)中定义的函数。因此,同样的函数将在特定处理程序接口的所有实例上调用。这样就提出一个问题,如何确定被调用的处理程序包装程序的实例(因为它是触发该事件的包装程序)。
好在 vtable 重载函数需要的格式包含 This 参数(参见前面的代码示例),它的类型就是被调用的接口。通过该参数,我们可以声明类型为 CVBSAXContentHandler 的变量,来执行与 QueryInterface 等价的功能,然后进行下面的赋值。
Dim objCVBSAXContentHandler As CVBSAXContentHandlerSet objCVBSAXContentHandler = This
调用有效地获得了 Visual Basic 包装程序接口。然后我们可以调用包装程序,然后就可以按预期的方式触发事件了。
为了达到这种效果,我们在 CVBSAXContentHandler 上按“Friend”范围定义了的方法。用这种方法,VBSAXXML 组件的内部代码可以访问函数,但是外部客户机甚至无法看到它。该 Friend 方法的实现仅仅触发事件、传送所有原始参数,加上 Abort 标志(用 ByRef 传送),因此处理该事件的代码可以设置标志。下面是代码:
Friend Sub StartDocument(Abort As Boolean) RaiseEvent StartDocument(Abort)End Sub
在从客户机应用程序(例如窗体)的 StartDocument 事件处理程序返回后,CVBSAXContentHandler 中的 Friend StartDocument 方法简单地将 Abort 参数的值传回 vtable 重载函数。根据 Abort 标志的值,vtable 重载可以正确地设置底层 ISAXContentHandler.StartElement 方法的 HRESULT 返回值。
其他处理程序方法也可以按类似的方式重载。下面是重载 StartElement 方法的函数原型,它有自己的参数。
Public Function ISAXContentHandler_StartElement(ByVal This As ISAXContentHandler, pwchNamespaceUri As Integer, ByVal cchNamespaceUri As Long, pwchLocalName As Integer, ByVal cchLocalName As Long, pwchQName As Integer, ByVal cchQName As Long, ByVal pAttributes As XMLSAX.ISAXAttributes) As Long
Friend 等价物类似于:
Friend Sub StartElement(ByVal NamespaceUri As String, ByVal LocalName As String, ByVal QName As String, ByVal Attributes As CVBSAXAttributes, Abort As Boolean) RaiseEvent StartElement(NamespaceUri, LocalName, QName, Attributes, Abort)End Sub
在此函数有自己的函数,它是将 C++ 风格参数映射为 Visual Basic 友好字符串的 vtable 重载函数。
类型库 Hacking
在某些情况下可以发现,编译提供的 Xmlsax.idl 文件产生的 Xmlsax.tlb 类型库,不仅仅是对 Visual Basic 不友好,而且实际上在 Visual Basic 中不能用。特别是在 IDL 中定义为 [out] 的方法的参数,对 Visual Basic 来说是不可接受的。如果参数定义为 [in, out](ByRef 参数)或者 [out, retval](返回值),那么 Visual Basic 不允许使用它。这是 SAX2 接口中某些方法的状况。
此外,我还在 ISAXAttributes 接口上调用大多数方法时遇到了麻烦。例如,GetLocalName 方法在 Visual Basic 中产生了一个子程序。调用者将索引传送给 Attributes 集合。程序返回整型的 by-now-familiar 数组和数组长度参数。IDL 类似于:
HRESULT GetLocalName( [in] int nIndex, [out] const wchar_t ** ppwchLocalName, [out] int * pcchLocalName);
我知道 [out] 参数在 Visual Basic 中是不可接受的。因此,我决定将 IDL 中的 [out] 参数更改为 [in, out]。但是我无法确定如何将 const wchar_t ** 数据类型解码。我注意到,Hardcore Visual Basic 一书提供了称为 UPointerToString 的函数,好像很有帮助。但是 UPointerToString 需要指针(在 VB 中是 Long)。因此我决定将数据类型从 const wchar_t ** 改变为 int *。修改后的 IDL 类似于:
HRESULT GetLocalName( [in] int nIndex, [in, out] int * ppwchLocalName, [in, out] int * pcchLocalName);
结果该格式在 Visual Basic 中是可接受的。字符串值可以用 UPointerToStringEx(与 UPointerToString 相同,但是用字符计数作为参数)成功解码。
ISAXAttributes 上的方法现在可以作为子程序调用:
Call pAttributes.GetValue(0, pwchValue, cchValue)
上面的调用获得索引位置为 0 的属性值,将指针分配给 pwchValue 中的值和 cchValue 中的值长度。然后从下面的代码中返回 VB 字符串(类似于上面的描述)。
sAttValue = String$(cchValue, 0) CopyMemory ByVal StrPtr(sAttValue), ByVal pwchValue, cchValue * 2
请注意 pwchValue 前面必须加 ByVal。
我复制了 Xmlsax.idl 文件,称之为 XmlsaxVB.idl,然后在将修改后的副本最终编译为 XmlsaxVB.tlb 之前修改了副本。下载中包括的 ActiveX 组件工程将引用这个修改后的类型库。
当然,不能建议修改 IDL 文件为一般规则。但需要建议的是,不要让修改后的类型库离开您开发所用的计算机。如果用“软件包和部署向导”创建安装程序,那么这样的类型库不应该包括在安装软件包中。但是,由于本文中讨论的 MSXML 只是技术预览,因此任何人尝试在生产环境中引入该示例都是没有危险的。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=5674