如何在使用Visual Studio 2010创建的Excel Add-In中执行.Onkey事件?

11

我正在使用Visual Studio 2010创建Excel插件。我希望能够在用户点击组合键时运行一些代码。

以下是我编写的代码:

Public Class CC

Private Sub ThisAddIn_Startup() Handles Me.Startup
    EnableShortCut()
End Sub

Sub A1()
    MsgBox "A1"
End Sub

Sub A2()
    MsgBox "A2"
End Sub

Sub A3()
    MsgBox "A3"
End Sub

Public Sub EnableShortCut()
    With Application
        .OnKey "+^{U}", "A1"  'action A1 should be performed when user clicks  Ctrl + Shift + U
        .OnKey "+^{L}", "A2"  'action A2 should be performed when user clicks  Ctrl + Shift + L
        .OnKey "+^{P}", "A3"  'action A3 should be performed when user clicks  Ctrl + Shift + P
    End With
End Sub

End Class

安装后,单击快捷方式时,插件显示错误。它说找不到特定的宏。 当Sub EnableShortCut()代码位于Excel VBA模块中时,它可以正常工作。但是,当它添加到使用Visual Studio创建的Excel插件中时,它就无法正常工作。 请有人帮我解决这个问题。


抱歉,这个只有在 Word 中可用,Excel 中没有。 - Kiru
我会尝试并告诉你。- Ian 4月18日13:56 - Siddharth Rout
不要在意我的赏金,只是在测试一些东西。 - Martin Devillers
@JeremyThompson 它应该就在评论区下面。这是我其中一个获得赏金的答案的截图:http://i47.tinypic.com/w7dhy1.png - Martin Devillers
@JeremyThompson 而对于更加极客的方法:前往我的问题并查看页面源代码。搜索名为“canOpenBounty”的变量并检查其值:http://i47.tinypic.com/5ci169.png - Martin Devillers
显示剩余9条评论
4个回答

10

"当用户按下一组键时,我希望能运行一些代码。"

这有点棘手,如果不使用任何外部依赖,则可以通过键盘钩子来实现它,使用 VSTO Excel Add-in:

Imports System
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

Friend Class KeyboardHooking
    ' Methods
    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Private Shared Function CallNextHookEx(ByVal hhk As IntPtr, ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As IntPtr
    End Function

    <DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Private Shared Function GetModuleHandle(ByVal lpModuleName As String) As IntPtr
    End Function

    Private Shared Function HookCallback(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
        If ((nCode >= 0) AndAlso (nCode = 0)) Then
            Dim keyData As Keys = DirectCast(CInt(wParam), Keys)
            If (((BindingFunctions.IsKeyDown(Keys.ControlKey) AndAlso BindingFunctions.IsKeyDown(Keys.ShiftKey)) AndAlso BindingFunctions.IsKeyDown(keyData)) AndAlso (keyData = Keys.D7)) Then
'DO SOMETHING HERE
            End If
            If ((BindingFunctions.IsKeyDown(Keys.ControlKey) AndAlso BindingFunctions.IsKeyDown(keyData)) AndAlso (keyData = Keys.D7)) Then
'DO SOMETHING HERE
            End If
        End If
        Return CInt(KeyboardHooking.CallNextHookEx(KeyboardHooking._hookID, nCode, wParam, lParam))
    End Function

    Public Shared Sub ReleaseHook()
        KeyboardHooking.UnhookWindowsHookEx(KeyboardHooking._hookID)
    End Sub

    Public Shared Sub SetHook()
        KeyboardHooking._hookID = KeyboardHooking.SetWindowsHookEx(2, KeyboardHooking._proc, IntPtr.Zero, Convert.ToUInt32(AppDomain.GetCurrentThreadId))
    End Sub

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Private Shared Function SetWindowsHookEx(ByVal idHook As Integer, ByVal lpfn As LowLevelKeyboardProc, ByVal hMod As IntPtr, ByVal dwThreadId As UInt32) As IntPtr
    End Function

    <DllImport("user32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Private Shared Function UnhookWindowsHookEx(ByVal hhk As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
    End Function


    ' Fields
    Private Shared _hookID As IntPtr = IntPtr.Zero
    Private Shared _proc As LowLevelKeyboardProc = New LowLevelKeyboardProc(AddressOf KeyboardHooking.HookCallback)
    Private Const WH_KEYBOARD As Integer = 2
    Private Const WH_KEYBOARD_LL As Integer = 13
    Private Const WM_KEYDOWN As Integer = &H100

    ' Nested Types
    Public Delegate Function LowLevelKeyboardProc(ByVal nCode As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
End Class

Public Class BindingFunctions
    ' Methods
    <DllImport("user32.dll")> _
    Private Shared Function GetKeyState(ByVal nVirtKey As Integer) As Short
    End Function

    Public Shared Function IsKeyDown(ByVal keys As Keys) As Boolean
        Return ((BindingFunctions.GetKeyState(CInt(keys)) And &H8000) = &H8000)
    End Function

End Class

C#版本 - 上述VB.net代码被转换的原始版本,但我不得不使用Reflector,因为CodeConverter和devfusion没有正确转换。
class KeyboardHooking
{
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod,
    uint dwThreadId);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);

public delegate int LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
private static LowLevelKeyboardProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;

//declare the mouse hook constant.
//For other hook types, you can obtain these values from Winuser.h in the Microsoft SDK.

private const int WH_KEYBOARD = 2; // mouse
private const int HC_ACTION = 0;

private const int WH_KEYBOARD_LL = 13; // keyboard
private const int WM_KEYDOWN = 0x0100;

public static void SetHook()
{
    // Ignore this compiler warning, as SetWindowsHookEx doesn't work with ManagedThreadId
    #pragma warning disable 618
    _hookID = SetWindowsHookEx(WH_KEYBOARD, _proc, IntPtr.Zero, (uint)AppDomain.GetCurrentThreadId());
    #pragma warning restore 618

}

public static void ReleaseHook()
{
    UnhookWindowsHookEx(_hookID);
}

//Note that the custom code goes in this method the rest of the class stays the same.
//It will trap if BOTH keys are pressed down.
private static int HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    if (nCode < 0)
    {
        return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
    else
    {

        if (nCode == HC_ACTION)
        {
            Keys keyData = (Keys)wParam;

            // CTRL + SHIFT + 7
            if ((BindingFunctions.IsKeyDown(Keys.ControlKey) == true)
                && (BindingFunctions.IsKeyDown(Keys.ShiftKey) == true)
                && (BindingFunctions.IsKeyDown(keyData) == true) && (keyData == Keys.D7))
            {
                // DO SOMETHING HERE
            }

            // CTRL + 7
            if ((BindingFunctions.IsKeyDown(Keys.ControlKey) == true)
                && (BindingFunctions.IsKeyDown(keyData) == true) && (keyData == Keys.D7))
            {
                // DO SOMETHING HERE
            }



        }
        return (int)CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
}
}

public class BindingFunctions
{
[DllImport("user32.dll")]
static extern short GetKeyState(int nVirtKey);

public static bool IsKeyDown(Keys keys)
{
    return (GetKeyState((int)keys) & 0x8000) == 0x8000;
}

}

在上述代码的HookCallback()方法中,您需要放置代码以捕获按下组合键时的事件,我给您提供了两个示例Ctrl + Shift + 7和Ctrl + 7。

然后在您的Excel AddIn中将其连接起来:

Private Sub ThisAddIn_Startup() Handles Me.Startup

'enable keyboard intercepts
KeyboardHooking.SetHook()

不要忘记在完成后禁用它:
Private Sub ThisAddIn_Shutdown() Handles Me.Shutdown

'disable keyboard intercepts
KeyboardHooking.ReleaseHook()

@Ian请尝试更正后的代码。我已经测试过了 - 之前无法工作的原因是因为我将它从C#转换为VB.Net,但代码转换器出现了问题,谢谢。 - Jeremy Thompson
2
这也帮了我,Jeremy,谢谢。有一个问题 - 你知道如何覆盖正常的按键吗?例如,Ctrl+Shift+C将执行插件命令,但仍然在打开的Excel单元格/Word文档中输入“c”。 - Steven DAmico
3
在这里回答我自己的一个傻问题:为了覆盖之前的按键,你需要返回一个值,这样函数就不会循环到下一个钩子。在你的命令后面加上类似于"return -1"的语句就足以实现覆盖。 - Steven DAmico
@JeremyThompson 您的解决方案类似于 https://dev59.com/Lmsz5IYBdhLWcg3wFUC_#8800908 - 但是那里的处理在线程内部。我发现,如果我不将处理放入线程中,则我按下的键会替换所选文本;如果像其他示例一样进行线程处理,则可以正常工作。但由于我想要转储到剪贴板,因此线程不是一个选项。您是否对线程内/外的键盘处理有任何见解? - Michael Paulukonis
@JeremyThompson:AppDomain.GetCurrentThreadId现在已经被弃用了。有什么解决办法吗? - Kannan Suresh
显示剩余2条评论

3
使用 Excel-DNA(我开发的一个开源的.NET/Excel集成库),您在.NET代码中定义的方法和自定义函数将通过C API与Excel注册。因此,行为更接近于VBA,您的代码与Application.OnKey“…”字符串也可以正常工作。
Excel-DNA允许您的代码位于编译后的.NET .dll程序集或直接以'.dna'文件的形式出现,当您加载插件时将进行处理。以下是这样一个文本文件的示例(如果它是在编译的项目中,则代码看起来相同)。如其他答案中所提到的,我已重命名了宏,以避免它们的名称与单元格名称A1等冲突。
制作插件:
  1. 将代码保存为名为OnKeyTest.dna的文本文件,
  2. 将其与Excel-DNA宿主库ExcelDna.xll的副本发布在CodePlex上进行匹配,然后将其复制并重命名为OnKeyTest.xll(匹配名称是加载.xll时找到.dna文件的方式)。

这两个文件将形成您完整的插件,只需要在计算机上安装.NET即可运行。

<DnaLibrary Language="VB" RuntimeVersion="v2.0" >
<![CDATA[
Imports ExcelDna.Integration

Public Class MyAddIn
    Implements IExcelAddIn

    Private Sub AutoOpen() Implements IExcelAddIn.AutoOpen
        EnableShortCut()
    End Sub

    Private Sub AutoClose() Implements IExcelAddIn.AutoClose
    End Sub

    Sub EnableShortCut()
        With ExcelDnaUtil.Application
            .OnKey("+^{U}", "MacroA1")  'action A1 should be performed when user clicks  Ctrl + Shift + U
            .OnKey("+^{L}", "MacroA2")  'action A2 should be performed when user clicks  Ctrl + Shift + L
            .OnKey("+^{P}", "MacroA3")  'action A3 should be performed when user clicks  Ctrl + Shift + P
        End With
    End Sub
End Class

Public Module MyMacros
    Sub MacroA1()
        MsgBox("A1")
    End Sub

    Sub MacroA2()
        MsgBox("A2")
    End Sub

    Sub MacroA3()
        MsgBox("A3")
    End Sub
End Module
]]>
</DnaLibrary>

2

这很难做到,Application.OnKey()方法非常受限制。它只能调用一个宏,并且不能传递任何参数。这意味着您必须提供一组宏。您不希望特定于工作簿的宏,而是需要在任何文档中都能工作的宏。让我们先解决这个问题。

  • 启动Excel并使用文件+关闭关闭默认工作簿
  • 点击记录宏。将“存储宏”设置更改为个人宏工作簿
  • 单击确定以关闭对话框,然后单击停止录制
  • 点击Visual Basic。请注意,现在有一个名为PERSONAL.XLSB的VBA项目和一个Module1

删除Macro1并复制/粘贴此VBA代码:

Sub MyAddinCommand1()
  Application.COMAddIns("ExcelAddin1").Object.Command 1
End Sub

Sub MyAddinCommand2()
  Application.COMAddIns("ExcelAddin1").Object.Command 2
End Sub

Sub MyAddinCommand3()
  Application.COMAddIns("ExcelAddin1").Object.Command 3
End Sub

尽可能地重复,你需要为每个想要定义的快捷键创建一个宏。单击保存。现在,你已经在c:\ users \ yourname \ appdata \ roaming \ microsoft \ excel \ xlstart \ personal.xlsb中创建了一个文件,其中包含宏。任何使用你的扩展的人也需要拥有这个文件,这是部署细节。

下一步你需要做的是公开你的命令。你编写的addin中的Sub A1()不能胜任,方法需要作为COM可见类的方法公开。添加一个新类到你的项目中,并让代码看起来像这样:

Imports System.Runtime.InteropServices

<InterfaceType(Runtime.InteropServices.ComInterfaceType.InterfaceIsIDispatch)> _
<ComVisible(True)> _
Public Interface IMyAddinCommand
    Sub Command(ByVal index As Integer)
End Interface

<ClassInterface(Runtime.InteropServices.ClassInterfaceType.None)> _
<ComVisible(True)> _
Public Class MyAddinCommand
    Implements IMyAddinCommand

    Public Sub Command(index As Integer) Implements IMyAddinCommand.Command
        MsgBox("Command #" + CStr(index))
    End Sub
End Class

这是一个简单的类,公开了一个名为Command()的方法,该方法接受一个整数参数。我只使用了MsgBox,您需要编写一个Select语句来根据index的值实现命令。还要注意与全局宏中的代码匹配。

还有一件事情,您必须明确地公开此类。在插件中重写RequestComAddInAutomationService函数。我用于测试的函数如下:

Public Class ThisAddIn

    Private Sub ThisAddIn_Startup() Handles Me.Startup
        Application.OnKey("+^{U}", "Personal.xlsb!MyAddinCommand1")  '' Ctrl + Shift + U
    End Sub

    Protected Overrides Function RequestComAddInAutomationService() As Object
        If commands Is Nothing Then commands = New MyAddinCommand
        Return commands
    End Function

    Private commands As MyAddinCommand

End Class

按 F5 键编译并启动 Excel。当我按下 Ctrl+Shift+U 时,会出现以下内容: enter image description here

再次强调,我不想使用除VSTO插件之外的任何其他文件。 - Kannan Suresh
这不是一个现实的选择,你确实需要一个宏。你可以在启动事件中从资源中复制文件到指定位置,但这会让IT人员对你产生严重的问题。他们无法区分宏病毒注入攻击的差异。你可以使用安装程序轻松地将其放置在正确的位置。 - Hans Passant
@HansPassant,您能否快速给我一些关于我发布的解决方案在安全方面的意见,谢谢。 - Jeremy Thompson
@Jeremy - 同样的问题,没有人能够确定它不是一个键盘记录器。 - Hans Passant

0

首先,A1、A2、A3被视为单元格地址。

  1. 创建.xlsm文件并添加以下VBA代码

    Sub AOne()
    MsgBox "来自AOne的消息"
    End Sub
    
    Sub ATwo()
    MsgBox "来自ATwo的消息"
    End Sub
    
    Sub AThree()
    MsgBox "来自AThree的消息"
    End Sub
    
  2. 现在在Visual Studio中创建一个Excel工作簿项目,添加现有文件并选择上面创建的.xlsm文件

    private void ThisWorkbook_Startup(object sender, System.EventArgs e)
    {
        EnableShortCut();
    }
    
    private void ThisWorkbook_Shutdown(object sender, System.EventArgs e)
    {
    }
    
    public void EnableShortCut()
    {
        Excel.Application app = Globals.ThisWorkbook.Application;
        app.OnKey("+^{U}", "AOne"); //当用户单击Ctrl + Shift + U时执行A1操作
        app.OnKey("+^{L}", "ATwo");//当用户单击Ctrl + Shift + L时执行A2操作
        app.OnKey("+^{P}", "AThree"); //当用户单击Ctrl + Shift + P时执行A3操作
    }
    

运行你的项目,这应该可以工作。在Excel中使用Application.OnKey或在Word中使用Application.Keybindings将宏名称作为参数。

检查我关于在Word中使用快捷键的答案这里


1
请仔细阅读问题。我不是在Excel工作簿上进行操作,而是使用Visual Studio创建插件。您所描述的方法是创建工作簿,这种差异是巨大的。我曾经遇到过类似的问题,并在此处发布了https://dev59.com/l2LVa4cB1Zd3GeqPyq-Q#10192858。在那种情况下,我能够在提供的帮助下得出解决方案。 - Kannan Suresh
我认为我仔细阅读了问题,您忘了说明它是应用程序级别的插件还是文档级别的插件。我回答时假设您正在使用文档级别的插件。不要紧,您可以通过将VBA代码复制到Book.xltx中并将其放入XLStart文件夹中或创建一个新的文件夹来遵循相同的步骤。 - Kiru

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