如何处理来自嵌入式Excel.OleObjects或Excel.Shapes的事件

18

我正在为一款旧的VBA程序制作C#和VB.NET版本,它有很多像CommandButton或图像这样的MSForms/OleObjects嵌入其中。

一开始我的想法是将所有按钮声明为Microsoft.Vbe.Interop.Forms.CommandButton,但这导致了一个COM异常,即System._COM类型无法转换为...Forms.CommandButton。如果我尝试更通用版本的此解决方案,我找不到任何项,而如果我尝试遍历所有VBComponent,我会发现它们都是workbook中的sheets,但没有控件:

foreach (VBComponent xxx in Globals.ThisWorkbook.VBProject.VBComponents) {
    Interaction.MsgBox(xxx.Name);
    Interaction.MsgBox(xxx.ToString);
}
因此,所有这些控件都不在.VBComponents中,但我可以在thisworkbook.worksheets(n).OLEobjects中找到它们作为OLE对象(这对我来说很反直觉,但我可能从一开始就不理解这个系统)。 如何处理这种对象的单击操作? 我假设我需要使用Excel.OLEObjectEvents_Event接口,但我似乎无法弄清楚如何使用。如果我尝试使用delegate创建自定义事件,我似乎无法将它们分配给OleObjects。如果我使用ActionClickEventHandler.CreateDelegate,我会遇到各种错误,使我认为这是一个死路。
来自微软的官方文档似乎并没有太大帮助,尽管它让我了解了Verb概念,我正在研究这一点。到目前为止,这只产生了类似于“应用程序启动失败”的COM错误。
甚至只是尝试使用两个标准事件之一,.GotFocus,我总是会遇到0x80040200错误。
Excel.OLEObject ButtonCatcher = Globals.ThisWorkbook.Worksheets(1).OLEObjects("CommandButton1");
ButtonCatcher.GotFocus += CommandButton1_Click;

第二行抛出了COMException异常 Exception from HRESULT: 0x80040200。在查看了Office开发网站上的代码编号后,我检查了按钮是否启用。

尝试在包含控件的工作表中的代码中采用更加通用的方法

object CommandButtonStart = this.GetType().InvokeMember("CommandButton1", System.Reflection.BindingFlags.GetProperty, null, this, null);
抛出一个“缺少方法”错误。 非常感谢任何帮助,这似乎应该很明显,但我却没想到。
编辑:我还发现我可以将这些控件转换为Excel.Shape,但这并不能让我更接近从VSTO运行函数或子程序。 我正在尝试使用Excel.Shape.OnAction,但这需要调用一个VBA子程序。 假设VSTO是可见的,则可以调用从VSTO调用子程序的VBA子程序。 这似乎非常绕,只有在最后没有办法时才想这样做。

2
您是否已确定工作表上的控件是否为ActiveX控件?常规表单控件(非ActiveX)不会表现出事件,并且只能通过onAction方法调用子程序。这可能就是您遇到问题的地方吗? - SWa
请帮助我理解。我发现VBA嵌入在Office软件程序中(或其他采用它的程序中)。VBA不是独立的。那么,“旧的VBA程序”是什么? - Smandoli
只是顺便提一下,根据我的旅行代理说,所有好的“最后手段”都是“非常绕路”的。这几乎是一个规律。 - Smandoli
@Kyle 这些是ActiveX控件,可以通过TructCenter选项禁用ActiveX来使其消失。 - Atl LED
@Smandoli 当然,这不是一个完整的程序,但编程分类不是我的强项(在Bio.SE上找我帮忙实际分类)。我们应该借鉴VSTO并将项目称为插件吗?我不介意任何人编辑那个短语以使其更正确。我没有编写原始代码,也从来没有太喜欢VBA。一个VBA解决方案?有组织的宏集合?我认为重点已经表达出来了,对吧? - Atl LED
显示剩余8条评论
5个回答

3

解决方案类型: VSTO文档级

场景:
1.) 运行时创建Excel.Worksheet。(不是Worksheet主机项)
2.) 在工作表上运行时添加一个按钮,当点击该按钮时触发C#代码。

程序集引用:
Microsoft.Vbe.Interop (Microsoft.Vbe.Interop.dll)
Microsoft.Vbe.Interop.Forms (Microsoft.Vbe.Interop.Forms.dll)
Microsoft.VisualBasic (Microsoft.VisualBasic.dll)

已测试/可用代码:

using MSForms = Microsoft.Vbe.Interop.Forms;
using System.Windows.Forms;

...

Microsoft.Vbe.Interop.Forms.CommandButton CmdBtn;

private void CreateOLEButton()
{
   Excel.Worksheet ws = Globals.ThisWorkbook.Application.Sheets["MyWorksheet"];

   // insert button shape
   Excel.Shape cmdButton = ws.Shapes.AddOLEObject("Forms.CommandButton.1", Type.Missing, false, false, Type.Missing, Type.Missing, Type.Missing, 500, 5, 100, 60);
   cmdButton.Name = "btnButton";

   // bind it and wire it up
   CmdBtn = (Microsoft.Vbe.Interop.Forms.CommandButton)Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(ws, null, "btnButton", new object[0], null, null, null);
   CmdBtn.Caption = "Click me!";
   CmdBtn.Click += new MSForms.CommandButtonEvents_ClickEventHandler(ExecuteCmd_Click);
}

private void ExecuteCmd_Click()
{
   MessageBox.Show("Click");
}

这个可行,但点击事件过一会儿就消失了。你可以点击几次,然后按钮就停止工作了。有什么想法发生了什么?我尝试的所有方法都是一样的。 - Daniel Möller
@DanielMöller 我没有遇到过这种情况。 不过听起来你已经进行了一些侦查。 COM+ 可以非常棘手,我不是 COM 专家,无法在此发表意见。 如果你找到解决方案,请发帖分享。谢谢。 - Leo Gurdian
2
我发现垃圾回收器正在清除我的对象。我不得不将它们声明为静态成员,以便在方法运行后它们仍然保留在内存中。现在,不幸的是,我的事件只在源应用程序运行时保持活动状态,它们不会在表格中持久存在。 - Daniel Möller
垃圾回收器!哼,它与COM+对象不兼容。 - Leo Gurdian

1
你尝试过使用NewLateBinding.LateGet吗?
using MSForms = Microsoft.Vbe.Interop.Forms;
using Microsoft.VisualBasic.CompilerServices;

...

MSForms.CommandButton CommandButton1 = (MSForms.CommandButton)NewLateBinding.LateGet(Globals.ThisWorkbook.Worksheets(1), null, "CommandButton1", new object[0], null, null, null);                
CommandButton1.Click += new Microsoft.Vbe.Interop.Forms.CommandButtonEvents_ClickEventHandler(CommandButton1_Click);

这个问题在MSDN的VSTO论坛和一个旧博客文章中有提到。


不,我会尝试一下的。 - Atl LED
仍然收到0x80040200错误代码,结合Ben的评论,也许我们可以让它正常工作。 - Atl LED
即使使用了提升的注册库,仍然出现了“0x80040200”错误。虽然这似乎是个好主意,但实际上并不难测试。只需创建一个ActiveX按钮,尝试使用NewLateBinding.LateGet在该行引发错误即可。 - Atl LED

0
使用Interop Forms Toolkit。它是由Microsoft免费提供的。它提供了COM包装器和事件信使类,用于在.NET和VBA之间通信事件数据。我已经使用它来处理从VBA到.NET的控件事件以及从.NET到VBA的事件。您可以使用Interop。
从工具包文档中:
Interop UserControls在Visual Basic 6.0中提供了一组基本的内置事件(单击、GotFocus、验证等)。您可以在Visual Studio .NET中定义自己的自定义事件,并使用RaiseEvent引发它们。为了在Visual Basic 6.0中处理事件,您需要添加项目引用并在代码中添加一个WithEvents变量声明,然后使用WithEvents变量而不是控件本身来处理事件。
如何处理Interop UserControl自定义事件
1. 在Visual Basic 6.0中,单击“项目”菜单上的“引用”。请注意,已经有一个对ControlNameCtl的引用,其中ControlName是您的Interop UserControl的名称。
2. 在可用引用列表中,找到一个关于ControlName的引用并选中它,然后点击确定。
3. 在代码编辑器中,添加一个WithEvents变量的声明:
Dim WithEvents ControlNameEvents As ControlLibrary.ControlName

在Form_Load事件处理程序中,添加以下代码以初始化WithEvents变量:
Private Sub Form_Load()
    Set ControlNameEvents = Me.ControlNameOnVB6Form
End Sub

在 WithEvents 变量的事件处理程序中添加代码以处理您的自定义事件:

Private Sub ControlNameEvents_MyCustomEvent()
    MsgBox("My custom event fired")
End Sub

0

你能否以编程的方式向工作簿中的CodeModule添加代码,像这样

Private Sub CommonButton_Click(ByVal buttonName As String)
  MsgBox "You clicked button [" & buttonName & "]"
End Sub

Private Sub CreateEventHandler(ByVal buttonName As String)
    Dim VBComp As VBIDE.VBComponent
    Dim CodeMod As VBIDE.CodeModule
    Dim codeText As String
    Dim LineNum As Long

    Set VBComp = ThisWorkbook.VBProject.VBComponents(Me.CodeName)
    Set CodeMod = VBComp.CodeModule
    LineNum = CodeMod.CountOfLines + 1

    codeText = codeText & "Private Sub " & buttonName & "_Click()" & vbCrLf
    codeText = codeText & "  Dim buttonName As String" & vbCrLf
    codeText = codeText & "  buttonName = """ & buttonName & "" & vbCrLf
    codeText = codeText & "  CommonButton_Click buttonName" & vbCrLf
    codeText = codeText & "End Sub"
    CodeMod.InsertLines LineNum, codeText
End Sub

刚试了一下,抛出了 HRESULT: 0x80040200 异常,让我稍微调试一下。 - Atl LED
试图稍微整理一下,还有一些无效的转换。例如,Me.Codename 是 Sheet1,但需要是 int 类型;因此将其更改为 this.index 等。已经编译成功,但按钮没有添加任何功能。 - Atl LED
如果应该将VBA宏放在其中一个模块或工作表代码的末尾,似乎并没有把它们放在那里(我检查了所有的代码)。按钮没有任何反应,我尝试调整索引大小写为VBComp,但仍然没有反应。 - Atl LED
啊,我刚意识到你是想要一个VBA解决方案。是的,我可以调用它来处理VBA处理的操作,但这与我的目标毫不相关。如果我们能避免的话,我想让它们由VSTO处理,而不是在VBA中处理。 - Atl LED

0

谢谢Leo Gurdian
在VB.Net中看起来像这样:

程序集引用:
Microsoft.Vbe.Interop (Microsoft.Vbe.Interop.dll)
Microsoft.Vbe.Interop.Forms (Microsoft.Vbe.Interop.Forms.dll)
Microsoft.VisualBasic (Microsoft.VisualBasic.dll)

Imports Microsoft.Office.Interop.Excel
Imports Microsoft.VisualBasic
Imports MSForms = Microsoft.Vbe.Interop.Forms

Private Sub CreateOLEButton()
    Dim ws As Excel.Worksheet = (CType(Application.ActiveSheet, Excel.Worksheet))
    Dim cmdButton As Excel.Shape = ws.Shapes.AddOLEObject("Forms.CommandButton.1", Type.Missing, False, False, Type.Missing, Type.Missing, Type.Missing, 500, 5, 100, 60)
    cmdButton.Name = "btnButton"
    Dim CmdBtn As Microsoft.Vbe.Interop.Forms.CommandButton = CType(Microsoft.VisualBasic.CompilerServices.NewLateBinding.LateGet(ws, Nothing, "btnButton", New Object(-1) {}, Nothing, Nothing, Nothing), Microsoft.Vbe.Interop.Forms.CommandButton)
    CmdBtn.Caption = "Click me!"
    AddHandler CmdBtn.Click, AddressOf ExecuteCmd_Click
End Sub
Private Sub ExecuteCmd_Click()
    'do something 
End Sub

这个 AddHandler 是属于哪个命名空间的? - Daniel Möller
https://learn.microsoft.com/pl-pl/dotnet/visual-basic/language-reference/statements/addhandler-statement - Jacek Augustyn

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