我如何知道 `ThisWorkbook` 是一个 `Workbook`?

8
我正在使用VBIDE API,不能假设宿主应用程序是Excel或任何Office应用程序。
因此,我只知道我正在查看一个VBComponent,并且它的Type是vbext_ct_document。
在VBE的Immediate窗格中,我可以获取此输出:
?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时,任何具有类型为“Application”的“Parent”属性的组件都是Workbook实例 ...而且不能保证其他主机中的其他文档模块甚至拥有该“Parent”属性,因此我非常困惑。

我对 literally anything 都持开放态度 - 从 p / invoke 调用和低级别COM“reflection”魔术(如ITypeInfo类型的魔术)到... 到...我不知道-需要// here be dragons注释的带有奇怪指针的unsafe代码-欢迎任何潜在成为工作解决方案的线索。


AFAIK VBE 加载项位于与宿主相同的进程中,因此 某处 存在一个指向VBA项目中的ThisWorkbookSheet1和其他文档类型VBComponent的指针。

?ObjPtr(ThisWorkbook)
 161150920

我想我只需要以某种方式抓住那个指针,就能到达我需要去的地方。

1个回答

8
很不幸,vbComponent属性集合中的值/对象只是CoClass实例值的反映,因此它们在所有VBA主机中都不可靠。例如,您无法确定Properties集合中是否存在Parent属性。
当主机支持文档类型组件时,由主机定义文档支持的接口GUID。主机通常也负责创建/删除实际文档,就像仅Excel对象模型可以向工作簿添加工作表一样,而VBIDE则不能。
您已经谈到了Workbooks和Worksheets,因此我将两者都包括在内...
不幸的是,VBIDE隐藏了有关文档类型组件的某些细节,并且在导出模块时故意省略这些细节,甚至将导出的文档类型模块转换为类模块文本,例如名为Sheet1的Worksheet,以便它无法作为文档类型模块重新导入:
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 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} 是一个工作簿CoClass:

[
  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 Worksheet 中的 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组件.Properties集合中的名称进行比较。
这是一种获取类型的迂回方法,但如果没有访问IStream的权限,我认为这将是你唯一的途径。

这非常有趣...有没有一个简单的方法获取那个隐藏的GUID?那个IStream在哪里,我为什么不能访问它?我认为这将是更健壮的解决方案;我认为我宁愿作为最后一招来匹配属性。 - Mathieu Guindon
持久化/磁盘流位于vbaProject.bin结构中,位于宿主文档内。内存(因此更为当前)的流在主机的进程空间中,但我不知道任何获取它的方法。换句话说,虽然您可以从磁盘加载流,但这仅适用于您知道如何查找VBA存储和流的文件格式,并且仅适用于上次保存的版本,因此如果文档从未保存过或存在未保存的更改,则不一定会有所有流。 - ThunderFrame
我只能访问GUIDs,因为我解压了XLSM文档内的vbaProject.bin文件中的存储和流。 - ThunderFrame
那么,这些在未保存的主机文档中是不可访问的,对吧? - Mathieu Guindon
1
即使文档之前已经保存过,也可能存在尚未保存的新文档类型组件(例如,在已保存的普通工作簿中不存在新的图表工作表)。但是,VBA 存储取决于宿主应用程序。OpenXML 格式使用复合文件格式,但 Access 使用系统表。其他宿主可能使用其他东西,因此您最终需要使用特定于宿主的解决方案。 - ThunderFrame
哦,如果那些属性还不是被禁止的禁果的话,如果您在工作表中添加了一个ActiveX控件,那么该流还包含以下内容:Attribute VB_Control = "CommandButton1, 1, 0, MSForms, CommandButton" - ThunderFrame

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接