我怎么知道`ThisWorkbook`是一本’工作簿’?

我正在使用VBIDE API,并且不能假设主机应用程序是Excel或任何Office应用程序。

所以我所知道的是我正在看一个VBComponent ,它的Typevbext_ct_document

在VBE的即时窗格中,我可以获得此输出:

 ?TypeName(Application.VBE.ActiveVBProject.VBComponents("Sheet1")) VBComponent ?TypeName(Sheet1) Worksheet 

但是Sheet1对象只存在于运行时环境中,所以如果我是C#加载项,我甚至都看不到它。

唯一可以接近我需要的东西是通过组件的ParentNext属性:

 ?TypeName(Application.VBE.ActiveVBProject.VBComponents("Sheet1").Properties("Parent").Object) Workbook ?TypeName(Application.VBE.ActiveVBProject.VBComponents("Sheet1").Properties("Next").Object) Worksheet 

这让我得到了我追求的类型名称……但错误的组件! 对于ThisWorkbook ,它是顶级文档对象,我将Application对象作为Parent

 ?TypeName(Application.VBE.ActiveVBProject.VBComponents("ThisWorkbook").Properties("Parent").Object) Application 

这种方法可能很有用,但前提是硬编码特定于主机的逻辑 ,该逻辑知道当主机应用程序是Excel时,哪个组件具有“应用程序”类型的“父”属性是一个Workbook实例…并且不能保证其他主机中的其他文档模块甚至会拥有“父”属性,所以我非常难过。

我几乎可以接受任何东西 – 从p / invoke调用和低级COM“reflection”魔术( ITypeInfo类型的魔法)到…到……我不知道 – 带有时髦指针的unsafe代码要求// here be dragons评论 – 欢迎任何可能最终成为工作解决方案的潜在客户。


AFAIK VBE加载项与主机位于同一进程中,因此某处有一个指向ThisWorkbookSheet1的指针以及VBA项目中的其他任何文档类型的VBComponent

 ?ObjPtr(ThisWorkbook) 161150920 

我想我只需要以某种方式抓住指针,我将成为我需要的地方。

遗憾的是,vbComponent Properties集合的值/对象只是CoClass实例值的反映,因此它们在所有VBA主机中都不可靠。 例如,您无法知道 Parent属性将存在于Properties集合中。

当主机支持文档类型组件时,由主机决定文档支持的接口的GUID。 主机通常还负责创建/删除实际文档,就像只有Excel对象模型可以将工作表添加到工作簿一样,而VBIDE则不能。

你已经谈过工作簿和工作表,所以我将包括两个….

遗憾的是,VBIDE隐藏了有关文档类型组件的一些细节,并且在导出模块时故意省略了这些细节,甚至将导出的文档类型模块转换为类模块文本,就像这个名为Sheet1 Worksheet一样,因此它可以’ t作为文档类型模块重新导入:

 VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "Sheet1" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = True Sub Foo() End Sub 

将上述内容与Sheet1模块内实际存储的文档模块文本(压缩格式)进行比较:

 Attribute VB_Name = "Sheet1" Attribute VB_Base = "0{00020820-0000-0000-C000-000000000046}" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = True Attribute VB_TemplateDerived = False Attribute VB_Customizable = True Sub Foo() End Sub 

请注意实际模块文本中存在的3个附加属性:

 Attribute VB_Base = "0{00020820-0000-0000-C000-000000000046}" Attribute VB_TemplateDerived = False Attribute VB_Customizable = True 

根据OleViewer,GUID 0 {00020820-0000-0000-C000-000000000046}与CoClass CoClass Worksheet完全匹配:

 [ uuid(00020820-0000-0000-C000-000000000046), helpcontext(0x0002a410) ] coclass Worksheet { [default] interface _Worksheet; [default, source] dispinterface DocEvents; }; 

Workbook模块出现相同的行为。 这是VBIDE导出的文本:

 VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "ThisWorkbook" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = True 

来自VBA二进制文件中的IStream的原始文本:

 Attribute VB_Name = "ThisWorkbook" Attribute VB_Base = "0{00020819-0000-0000-C000-000000000046}" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = True Attribute VB_TemplateDerived = False Attribute VB_Customizable = True 

这次,正如预期的那样,GUID 0{00020819-0000-0000-C000-000000000046}是一个工作簿0{00020819-0000-0000-C000-000000000046}

 [ uuid(00020819-0000-0000-C000-000000000046), helpcontext(0x000305b8) ] coclass Workbook { [default] interface _Workbook; [default, source] dispinterface WorkbookEvents; }; 

以上是很好的知识,但它并没有解决你的问题,除非你可以得到组件的内存IStreams的句柄,我认为你不能。 如果您可以从上次保存的主机文档版本加载详细信息,那么您可以从基础文档加载详细信息,但我认为您不希望这样, 并且它最终可能是特定于主机的(考虑Access将VBA存储在表中的方式。)

但是,VBIDE 确实为您提供了有关CoClass的线索。 vbComponent的属性集合返回CoClass中存在的确切数量的属性,如果检查这些属性的名称,参数和类型,您会发现它们与相应CoClass的成员完全匹配,直到它们在CoClass定义中出现的顺序。

例如,Worksheet vbComponent的前10个属性是:

 Application Creator Parent CodeName _CodeName Index Name Next OnDoubleClick OnSheetActivate 

以及来自CoClass工作表中的dispinterface _Worksheet的相应propget (和propput )条目(删除了方法):

  [id(0x00000094), propget, helpcontext(0x0002a411)] Application* Application(); [id(0x00000095), propget, helpcontext(0x0002a412)] XlCreator Creator(); [id(0x00000096), propget, helpcontext(0x0002a413)] IDispatch* Parent(); [id(0x0000055d), propget, helpcontext(0x0002a7fc)] BSTR CodeName(); [id(0x80010000), propget, helpcontext(0x0002a7fd)] BSTR _CodeName(); [id(0x80010000), propput, helpcontext(0x0002a7fd)] void _CodeName([in] BSTR rhs); [id(0x000001e6), propget, helpcontext(0x0002a7fe)] long Index(); [id(0x0000006e), propget, helpcontext(0x0002a800)] BSTR Name(); [id(0x0000006e), propput, helpcontext(0x0002a800)] void Name([in] BSTR rhs); [id(0x000001f6), propget, helpcontext(0x0002a801)] IDispatch* Next(); [id(0x00000274), propget, hidden, helpcontext(0x0002a802)] BSTR OnDoubleClick(); [id(0x00000274), propput, hidden, helpcontext(0x0002a802)] void OnDoubleClick([in] BSTR rhs); [id(0x00000407), propget, hidden, helpcontext(0x0002a803)] BSTR OnSheetActivate(); 

如果您可以反映主机类型库的CoClasses并散列属性名称 (可能只是使用propget名称),那么您可以将散列与VBIDE的component.Properties集合中的名称进行比较。

这是获取类型的一种循环方式,但是如果没有访问IStream,我认为这将是您唯一的方式。