草庐IT

c# - 如何在没有 Application.Run 的情况下从 VBE 加载项运行宏?

coder 2023-07-12 原文

我正在为 VBE 编写 COM 加载项,其中一项核心功能涉及在单击命令栏按钮时执行现有的 VBA 代码。
代码是用户编写的单元测试代码,在一个标准 (.bas) 模块中,如下所示:

Option Explicit
Option Private Module

'@TestModule
Private Assert As New Rubberduck.AssertClass

'@TestMethod
Public Sub TestMethod1() 'TODO: Rename test
    On Error GoTo TestFail
    
    'Arrange:

    'Act:

    'Assert:
    Assert.Inconclusive

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

所以我有这个获取主机当前实例的代码 Application目的:
protected HostApplicationBase(string applicationName)
{
    Application = (TApplication)Marshal.GetActiveObject(applicationName + ".Application");
}
这是ExcelApp类(class):
public class ExcelApp : HostApplicationBase<Microsoft.Office.Interop.Excel.Application>
{
    public ExcelApp() : base("Excel") { }

    public override void Run(QualifiedMemberName qualifiedMemberName)
    {
        var call = GenerateMethodCall(qualifiedMemberName);
        Application.Run(call);
    }

    protected virtual string GenerateMethodCall(QualifiedMemberName qualifiedMemberName)
    {
        return qualifiedMemberName.ToString();
    }
}
奇迹般有效。我有类似的代码 WordApp , PowerPointAppAccessApp , 也。
问题是 Outlook 的 Application对象不公开 Run方法,所以我,好吧,卡住了。

如何在没有 Application.Run 的情况下从 VBE 的 COM 加载项执行 VBA 代码?
This answer链接到 blog post on MSDN that looks promising ,所以我试过这个:
public class OutlookApp : HostApplicationBase<Microsoft.Office.Interop.Outlook.Application>
{
    public OutlookApp() : base("Outlook") { }

    public override void Run(QualifiedMemberName qualifiedMemberName)
    {
        var app = Application.GetType();
        app.InvokeMember(qualifiedMemberName.MemberName, BindingFlags.InvokeMethod, null, Application, null);
    }
}
但是我得到的最好的是 COMException上面写着“未知名称”,并且 OUTLOOK.EXE 进程以代码 -1073741819 (0xc0000005) '访问冲突'退出 - 它也与 Excel 一样好。

更新
如果我输入 TestMethod1,此 VBA 代码有效内ThisOutlookSession :
Outlook.Application.TestMethod1
请注意 TestMethod1未被列为 Outlook.Application 的成员在 VBA IntelliSense 中 .. 但不知何故它碰巧工作。
问题是,我如何使用 Reflection 进行这项工作?

最佳答案

更新 3:

我在 MSDN forums: Call Outlook VBA sub from VSTO 上找到了这篇文章.

显然它使用 VSTO,我尝试将其转换为 VBE AddIn ,但在使用 x64 Windows 时遇到了注册类问题:

COMException (0x80040154): Retrieving the COM class factory for component with CLSID {55F88893-7708-11D1-ACEB-006008961DA5} failed due to the following error: 80040154 Class not registered



无论如何,这是那些认为他让它工作的人的回答:

MSDN 论坛帖子的开始

I found a way! What could be triggered from both VSTO and VBA? The Clipboard!!

So I used the clipboard to pass messages from one environment to the other. Here is some few codes that will explain my trick:



VSTO:
'p_Procedure is the procedure name to call in VBA within Outlook

'mObj_ou_UserProperty is to create a custom property to pass an argument to the VBA procedure

Private Sub p_Call_VBA(p_Procedure As String)
    Dim mObj_of_CommandBars As Microsoft.Office.Core.CommandBars, mObj_ou_Explorer As Outlook.Explorer, mObj_ou_MailItem As Outlook.MailItem, mObj_ou_UserProperty As Outlook.UserProperty

    mObj_ou_Explorer = Globals.Menu_AddIn.Application.ActiveExplorer
    'I want this to run only when one item is selected

    If mObj_ou_Explorer.Selection.Count = 1 Then
        mObj_ou_MailItem = mObj_ou_Explorer.Selection(1)
        mObj_ou_UserProperty = mObj_ou_MailItem.UserProperties.Add("COM AddIn-Azimuth", Outlook.OlUserPropertyType.olText)
        mObj_ou_UserProperty.Value = p_Procedure
        mObj_of_CommandBars = mObj_ou_Explorer.CommandBars

        'Call the clipboard event Copy
        mObj_of_CommandBars.ExecuteMso("Copy")
    End If
End Sub

VBA:

Create a class for Explorer events and trap this event:


Public WithEvents mpubObj_Explorer As Explorer

'Trap the clipboard event Copy
Private Sub mpubObj_Explorer_BeforeItemCopy(Cancel As Boolean)
Dim mObj_MI As MailItem, mObj_UserProperty As UserProperty

    'Make sure only one item is selected and of type Mail

    If mpubObj_Explorer.Selection.Count = 1 And mpubObj_Explorer.Selection(1).Class = olMail Then
        Set mObj_MI = mpubObj_Explorer.Selection(1)
        'Check to see if the custom property is present in the mail selected
        For Each mObj_UserProperty In mObj_MI.UserProperties
            If mObj_UserProperty.Name = "COM AddIn-Azimuth" Then
                Select Case mObj_UserProperty.Value
                    Case "Example_Add_project"
                        '...
                    Case "Example_Modify_planning"
                        '...
                End Select
                'Remove the custom property, to keep things clean
                mObj_UserProperty.Delete

                'Cancel the Copy event.  It makes the call transparent to the user
                Cancel = True
                Exit For
            End If
        Next
        Set mObj_UserProperty = Nothing
        Set mObj_MI = Nothing
    End If
End Sub

MSDN 论坛帖子结束

因此,此代码的作者将 UserProperty 添加到邮件项并以这种方式传递函数名称。同样,这将需要 Outlook 中的一些样板代码和至少 1 个邮件项目。

更新 3a:

80040154 类未注册 我得到的是因为尽管当我将代码从 VSTO VB.Net 翻译成 VBE C# 时我是针对 x86 平台的,但我正在实例化项目,例如:
Microsoft.Office.Core.CommandBars mObj_of_CommandBars = new Microsoft.Office.Core.CommandBars();

在浪费了几个小时之后,我想出了这个代码,运行了!!!



VBE C# 代码(来自我的回答 make a VBE AddIn answer here):
namespace VBEAddin
{
    [ComVisible(true), Guid("3599862B-FF92-42DF-BB55-DBD37CC13565"), ProgId("VBEAddIn.Connect")]
    public class Connect : IDTExtensibility2
    {
        private VBE _VBE;
        private AddIn _AddIn;

        #region "IDTExtensibility2 Members"

        public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
            try
            {
                _VBE = (VBE)application;
                _AddIn = (AddIn)addInInst;

                switch (connectMode)
                {
                    case Extensibility.ext_ConnectMode.ext_cm_Startup:
                        break;
                    case Extensibility.ext_ConnectMode.ext_cm_AfterStartup:
                        InitializeAddIn();

                        break;
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString());
            }
        }

        private void onReferenceItemAdded(Reference reference)
        {
            //TODO: Map types found in assembly using reference.
        }

        private void onReferenceItemRemoved(Reference reference)
        {
            //TODO: Remove types found in assembly using reference.
        }

        public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
        {
        }

        public void OnAddInsUpdate(ref Array custom)
        {
        }

        public void OnStartupComplete(ref Array custom)
        {
            InitializeAddIn();
        }

        private void InitializeAddIn()
        {
            MessageBox.Show(_AddIn.ProgId + " loaded in VBA editor version " + _VBE.Version);
            Form1 frm = new Form1();
            frm.Show();   //<-- HERE I AM INSTANTIATING A FORM WHEN THE ADDIN LOADS FROM THE VBE IDE!
        }

        public void OnBeginShutdown(ref Array custom)
        {
        }

        #endregion
    }
}

我从 VBE IDE InitializeAddIn() 方法实例化和加载的 Form1 代码:
namespace VBEAddIn
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Call_VBA("Test");
        }

        private void Call_VBA(string p_Procedure)
        {
            var olApp = new Microsoft.Office.Interop.Outlook.Application();
            Microsoft.Office.Core.CommandBars mObj_of_CommandBars;

            Microsoft.Office.Core.CommandBars mObj_of_CommandBars = new Microsoft.Office.Core.CommandBars();
            Microsoft.Office.Interop.Outlook.Explorer mObj_ou_Explorer;
            Microsoft.Office.Interop.Outlook.MailItem mObj_ou_MailItem;
            Microsoft.Office.Interop.Outlook.UserProperty mObj_ou_UserProperty;

            //mObj_ou_Explorer = Globals.Menu_AddIn.Application.ActiveExplorer
            mObj_ou_Explorer = olApp.ActiveExplorer();

            //I want this to run only when one item is selected
            if (mObj_ou_Explorer.Selection.Count == 1)
            {
                mObj_ou_MailItem = mObj_ou_Explorer.Selection[1];
                mObj_ou_UserProperty = mObj_ou_MailItem.UserProperties.Add("JT", Microsoft.Office.Interop.Outlook.OlUserPropertyType.olText);
                mObj_ou_UserProperty.Value = p_Procedure;
                mObj_of_CommandBars = mObj_ou_Explorer.CommandBars;

                //Call the clipboard event Copy
                mObj_of_CommandBars.ExecuteMso("Copy");
            }
        }
    }
}

ThisOutlookSession 代码:
Public WithEvents mpubObj_Explorer As Explorer

'Trap the clipboard event Copy
Private Sub mpubObj_Explorer_BeforeItemCopy(Cancel As Boolean)
Dim mObj_MI As MailItem, mObj_UserProperty As UserProperty

MsgBox ("The mpubObj_Explorer_BeforeItemCopy event worked!")
    'Make sure only one item is selected and of type Mail

    If mpubObj_Explorer.Selection.Count = 1 And mpubObj_Explorer.Selection(1).Class = olMail Then
        Set mObj_MI = mpubObj_Explorer.Selection(1)
        'Check to see if the custom property is present in the mail selected
        For Each mObj_UserProperty In mObj_MI.UserProperties
            If mObj_UserProperty.Name = "JT" Then

                'Will the magic happen?!
                Outlook.Application.Test

                'Remove the custom property, to keep things clean
                mObj_UserProperty.Delete

                'Cancel the Copy event.  It makes the call transparent to the user
                Cancel = True
                Exit For
            End If
        Next
        Set mObj_UserProperty = Nothing
        Set mObj_MI = Nothing
    End If
End Sub

Outlook VBA 方法:
Public Sub Test()
MsgBox ("Will this be called?")
End Sub

非常遗憾,我很遗憾地通知您,我的努力没有成功。也许它确实适用于 VSTO(我没有尝试过)但是在像狗取骨头一样尝试之后,我现在愿意放弃!

作为一个安慰,您可以在此答案的修订历史中找到一个疯狂的想法(它显示了一种模拟 Office 对象模型的方法)来运行带有参数的私有(private) Office VBA 单元测试。

我将与您线下谈谈对 RubberDuck GitHub 项目的贡献,我编写的代码与 Prodiance's Workbook Relationship Diagram 的功能相同。在微软收购他们并将他们的产品包含在 Office Audit and Version Control Server 之前。

您可能希望在完全取消之前检查此代码,我什至无法让 mpubObj_Explorer_BeforeItemCopy 事件起作用,所以如果您可以在 Outlook 中正常工作,您可能会更好。 (我在家使用 Outlook 2013,所以 2010 年可能会有所不同)。

ps 你会认为在逆时针方向上单腿跳跃后,点击我的手指,同时顺时针揉我的头like Workaround Method 2 in this KB Article我本来会钉它的……我只是掉了更多的头发!

更新 2:

在您的 Outlook.Application.TestMethod1 内你不能只使用 VB 经典的 CallByName 方法,这样你就不需要反射吗?在调用包含 CallByName 的方法之前,您需要设置一个字符串属性“Sub/FunctionNameToCall”以指定要调用的子/函数。

不幸的是,用户需要在他们的模块之一中插入一些样板代码。

更新 1:

这听起来真的很狡猾,但是由于 Outlooks 的对象模型已经完全限制了它的 Run 方法,您可以求助于... SendKeys (是的,我知道,但它会起作用)。

不幸的是oApp.GetType().InvokeMember("Run"...)下面描述的方法适用于除 Outlook 之外的所有 Office 应用程序 - 基于此知识库文章中的“属性”部分:https://support.microsoft.com/en-us/kb/306683 ,抱歉,我直到现在才知道这一点,并发现尝试和 the MSDN article misleading 非常令人沮丧。 ,最终微软锁定了它:


**
请注意 SendKeys支持和唯一的其他已知方式使用 ThisOutlookSession不是:
https://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/cQ8gF9ssN3g - 即使 Sue 不是 Microsoft PSS she would've asked and found out its unsupported .

旧...以下方法适用于除 Outlook 之外的 Office 应用程序

The problem is that Outlook's Application object doesn't expose a Run method, so I'm, well, stuck. This answer links to a blog post on MSDN that looks promising, so I tried this ... but OUTLOOK.EXE process exits with code -1073741819 (0xc0000005) 'Access violation'

The question is, how do I make this work with Reflection?



1) 这是我使用的适用于 Excel 的代码(应该同样适用于 Outlook),使用 .Net 引用: Microsoft.Office.Interop.Excel v14(不是 ActiveX COM 引用):
using System;
using Microsoft.Office.Interop.Excel;

namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
    RunVBATest();
}

public static void RunVBATest()
{
    Application oExcel = new Application();
    oExcel.Visible = true;
    Workbooks oBooks = oExcel.Workbooks;
    _Workbook oBook = null;
    oBook = oBooks.Open("C:\\temp\\Book1.xlsm");

    // Run the macro.
    RunMacro(oExcel, new Object[] { "TestMsg" });

    // Quit Excel and clean up (its better to use the VSTOContrib by Jake Ginnivan).
    oBook.Saved = true;
    oBook.Close(false);
    System.Runtime.InteropServices.Marshal.ReleaseComObject(oBook);
    System.Runtime.InteropServices.Marshal.ReleaseComObject(oBooks);
    System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcel);
}

private static void RunMacro(object oApp, object[] oRunArgs)
{
    oApp.GetType().InvokeMember("Run",
        System.Reflection.BindingFlags.Default |
        System.Reflection.BindingFlags.InvokeMethod,
        null, oApp, oRunArgs);

    //Your call looks a little bit wack in comparison, are you using an instance of the app?
    //Application.GetType().InvokeMember(qualifiedMemberName.MemberName, BindingFlags.InvokeMethod, null, Application, null);
}
}
}
}

2)确保将宏代码放入模块(全局 BAS 文件)中。
Public Sub TestMsg()

MsgBox ("Hello Stackoverflow")

End Sub

3) 确保您启用对 VBA 项目对象模型的宏安全和信任访问:

关于c# - 如何在没有 Application.Run 的情况下从 VBE 加载项运行宏?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31954364/

有关c# - 如何在没有 Application.Run 的情况下从 VBE 加载项运行宏?的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  4. ruby - 难道Lua没有和Ruby的method_missing相媲美的东西吗? - 2

    我好像记得Lua有类似Ruby的method_missing的东西。还是我记错了? 最佳答案 表的metatable的__index和__newindex可以用于与Ruby的method_missing相同的效果。 关于ruby-难道Lua没有和Ruby的method_missing相媲美的东西吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/7732154/

  5. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  6. ruby-on-rails - rails 目前在重启后没有安装 - 2

    我有一个奇怪的问题:我在rvm上安装了ruby​​onrails。一切正常,我可以创建项目。但是在我输入“railsnew”时重新启动后,我有“程序'rails'当前未安装。”。SystemUbuntu12.04ruby-v"1.9.3p194"gemlistactionmailer(3.2.5)actionpack(3.2.5)activemodel(3.2.5)activerecord(3.2.5)activeresource(3.2.5)activesupport(3.2.5)arel(3.0.2)builder(3.0.0)bundler(1.1.4)coffee-rails(

  7. ruby - 默认情况下使选项为 false - 2

    这是在Ruby中设置默认值的常用方法:classQuietByDefaultdefinitialize(opts={})@verbose=opts[:verbose]endend这是一个容易落入的陷阱:classVerboseNoMatterWhatdefinitialize(opts={})@verbose=opts[:verbose]||trueendend正确的做法是:classVerboseByDefaultdefinitialize(opts={})@verbose=opts.include?(:verbose)?opts[:verbose]:trueendend编写Verb

  8. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  9. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  10. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

随机推荐