从 VSTO 加载项检测 Word 2016 中的文本更改

Detecting text changes in Word 2016 from VSTO add-in

此问题与 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 密切相关(实际上包括该问题答案中的示例代码),但这是专门针对 Visual Studio(专业版)2015 开发的Word 2016 运行宁 Windows 10.

我正在尝试通过 VSTO 加载项检测 Word 文档中的文本何时更改。我从

了解到

没有事件驱动的方法来做到这一点。当文本更改时,Word 根本不发送事件。

我看到讨论了两个解决方法:

  1. 使用WindowSelectionChange事件。不幸的是,当通过按箭头键、使用鼠标、执行撤消或重做以及可能的其他操作更改选择时,似乎会发送此事件,但在键入或删除时不会发送。
  2. 使用低级 keydown 事件挂钩。这已在其中几个 Whosebug 问题中进行了讨论,并且在 2014 年 2 月的 a thread on a Visual Studio forum 中也被称为“广泛传播的技术”。

我正在尝试使用 How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? 答案中的代码,它似乎观察到每个按键事件 除了 那些发送到 Word 2016 的事件。

这是我使用的代码,方便参考。

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace KeydownWordAddIn
{
    public partial class ThisAddIn
    {
        private const int WH_KEYBOARD_LL = 13;
        private const int WM_KEYDOWN = 0x0100;

        private static IntPtr hookId = IntPtr.Zero;
        private delegate IntPtr HookProcedure(int nCode, IntPtr wParam, IntPtr lParam);
        private static HookProcedure procedure = HookCallback;

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

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, HookProcedure lpfn, IntPtr hMod, uint dwThreadId);

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

        private static IntPtr SetHook(HookProcedure procedure)
        {
            using (Process process = Process.GetCurrentProcess())
            using (ProcessModule module = process.MainModule)
                return SetWindowsHookEx(WH_KEYBOARD_LL, procedure, GetModuleHandle(module.ModuleName), 0);
        }

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
            {
                int pointerCode = Marshal.ReadInt32(lParam);
                string pressedKey = ((Keys)pointerCode).ToString();

                // Do some sort of processing on key press.
                var thread = new Thread(() => {
                    Debug.WriteLine(pressedKey);
                });
                thread.Start();
            }
            return CallNextHookEx(hookId, nCode, wParam, lParam);
        }

        private void ThisAddIn_Startup(object sender, EventArgs e)
        {
            hookId = SetHook(procedure);
        }

        private void ThisAddIn_Shutdown(object sender, EventArgs e)
        {
            UnhookWindowsHookEx(hookId);
        }

        #region VSTO generated code
        /// <summary>
        /// Required method for Designer support.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        #endregion
    }
}

当我使用此加载项 运行 Word 2016 时,我看到按键事件发送到 Edge 浏览器甚至 Visual Studio,但没有发送到 Word 本身。

在 Word 2016 中是否以某种方式阻止了 keydown 挂钩,还是我做错了什么?

我在 Word 2013 中也遇到过同样的问题,不得不想出一个 "creative" 的解决方案。它使用 diffplex 来监视活动文档文本的变化,并在发生变化时触发事件。这不太理想,但我们会做我们必须做的事情来使事情顺利进行。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using System.ComponentModel;

namespace WordUtils {
    public class TextChangeDetector {

        public Word.Application Application;
        private BackgroundWorker bg;

        public delegate void TextChangeHandler(object sender, TextChangedEventArgs e);
        public event TextChangeHandler OnTextChanged;

        public TextChangeDetector(Word.Application app) {
            this.Application = app;
        }

        public void Start() {
            bg = new BackgroundWorker();
            bg.WorkerReportsProgress = true;
            bg.WorkerSupportsCancellation = true;
            bg.ProgressChanged += bg_ProgressChanged;
            bg.DoWork += bg_DoWork;
            bg.RunWorkerAsync(this.Application);
        }

        private void bg_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            switch (e.ProgressPercentage) {
                case 50: //change
                    if (OnTextChanged != null) {
                        OnTextChanged(this, new TextChangedEventArgs((char)e.UserState));
                    }
                    break;
            }
        }

        private void bg_DoWork(object sender, DoWorkEventArgs e) {

            Word.Application wordApp = e.Argument as Word.Application;
            BackgroundWorker bg = sender as BackgroundWorker;
            string lastPage = string.Empty;

            while (true) {
                try {
                    if (Application.Documents.Count > 0) {
                        if (Application.ActiveDocument.Words.Count > 0) {
                            var currentPage = Application.ActiveDocument.Bookmarks["\Page"].Range.Text;                         

                            if (currentPage != null && currentPage != lastPage) {
                                var differ = new DiffPlex.Differ();
                                var builder = new DiffPlex.DiffBuilder.InlineDiffBuilder(differ);                               
                                var difference = builder.BuildDiffModel(lastPage, currentPage);
                                var change = from d in difference.Lines where d.Type != DiffPlex.DiffBuilder.Model.ChangeType.Unchanged select d;
                                if (change.Any()) {                                    
                                    bg.ReportProgress(50, change.Last().Text.Last());
                                }

                                lastPage = currentPage;
                            }


                        }
                    }
                } catch (Exception) {

                }

                if (bg.CancellationPending) {
                    break;
                }
                System.Threading.Thread.Sleep(100);
            }
        }

        public void Stop() {
            if (bg != null && !bg.CancellationPending) {
                bg.CancelAsync();
            }
        }
    }

    public class TextChangedEventArgs : EventArgs {
        public char Letter;
        public TextChangedEventArgs(char letter) {
            this.Letter = letter;
        }
    }
}

用法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using WordUtils;

namespace WordAddIn1 {
    public partial class ThisAddIn {
        TextChangeDetector detector;

        private void ThisAddIn_Startup(object sender, System.EventArgs e) {
            detector = new TextChangeDetector(Application);
            detector.OnTextChanged += detector_OnTextChanged;
            detector.Start();
        }

        void detector_OnTextChanged(object sender, TextChangedEventArgs e) {
            Console.WriteLine(e.Letter);
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e) {
            detector.Stop();
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup() {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }

        #endregion
    }
}

如果您不在 VSTO 加载项中使用低级挂钩,一切都应该正常工作。

[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetCurrentThreadId();

const int WH_KEYBOARD = 2;

private static IntPtr SetHook(HookProcedure procedure)
{
    var threadId = (uint)SafeNativeMethods.GetCurrentThreadId();
    return SetWindowsHookEx(WH_KEYBOARD, procedure, IntPtr.Zero, threadId);
}

请注意,您可能还需要创建一个钩子来拦截鼠标消息,因为仅通过鼠标交互(例如通过功能区或上下文菜单复制和粘贴)就可以修改文档的文本。

VSTO 示例

这是一个完整的工作 VSTO 示例,包括键盘和鼠标挂钩:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Office = Microsoft.Office.Core;

namespace SampleAddinWithKeyboardHook
{
    public partial class ThisAddIn
    {
        // NOTE: We need a backing field to prevent the delegate being garbage collected
        private SafeNativeMethods.HookProc _mouseProc;
        private SafeNativeMethods.HookProc _keyboardProc;

        private IntPtr _hookIdMouse;
        private IntPtr _hookIdKeyboard;

        private void ThisAddIn_Startup(object sender, EventArgs e)
        {
            _mouseProc = MouseHookCallback;
            _keyboardProc = KeyboardHookCallback;

            SetWindowsHooks();
        }

        private void ThisAddIn_Shutdown(object sender, EventArgs e)
        {
            UnhookWindowsHooks();
        }

        private void SetWindowsHooks()
        {
            uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();

            _hookIdMouse =
                SafeNativeMethods.SetWindowsHookEx(
                    (int)SafeNativeMethods.HookType.WH_MOUSE,
                    _mouseProc,
                    IntPtr.Zero,
                    threadId);

            _hookIdKeyboard =
                SafeNativeMethods.SetWindowsHookEx(
                    (int)SafeNativeMethods.HookType.WH_KEYBOARD,
                    _keyboardProc,
                    IntPtr.Zero,
                    threadId);
        }

        private void UnhookWindowsHooks()
        {
            SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
            SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
        }

        private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                var mouseHookStruct =
                    (SafeNativeMethods.MouseHookStructEx)
                        Marshal.PtrToStructure(lParam, typeof(SafeNativeMethods.MouseHookStructEx));

                // handle mouse message here
                var message = (SafeNativeMethods.WindowMessages)wParam;
                Debug.WriteLine(
                    "{0} event detected at position {1} - {2}",
                    message,
                    mouseHookStruct.pt.X,
                    mouseHookStruct.pt.Y);
            }
            return SafeNativeMethods.CallNextHookEx(
                _hookIdKeyboard,
                nCode,
                wParam,
                lParam);
        }

        private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                // handle key message here
                Debug.WriteLine("Key event detected.");
            }

            return SafeNativeMethods.CallNextHookEx(
                _hookIdKeyboard,
                nCode,
                wParam,
                lParam);
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support.
        /// </summary>
        private void InternalStartup()
        {
            Startup += ThisAddIn_Startup;
            Shutdown += ThisAddIn_Shutdown;
        }

        #endregion
    }

    internal static class SafeNativeMethods
    {
        public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        public enum HookType
        {
            WH_KEYBOARD = 2,
            WH_MOUSE = 7
        }

        public enum WindowMessages : uint
        {
            WM_KEYDOWN = 0x0100,
            WM_KEYFIRST = 0x0100,
            WM_KEYLAST = 0x0108,
            WM_KEYUP = 0x0101,
            WM_LBUTTONDBLCLK = 0x0203,
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_MBUTTONDBLCLK = 0x0209,
            WM_MBUTTONDOWN = 0x0207,
            WM_MBUTTONUP = 0x0208,
            WM_MOUSEACTIVATE = 0x0021,
            WM_MOUSEFIRST = 0x0200,
            WM_MOUSEHOVER = 0x02A1,
            WM_MOUSELAST = 0x020D,
            WM_MOUSELEAVE = 0x02A3,
            WM_MOUSEMOVE = 0x0200,
            WM_MOUSEWHEEL = 0x020A,
            WM_MOUSEHWHEEL = 0x020E,
            WM_RBUTTONDBLCLK = 0x0206,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205,
            WM_SYSDEADCHAR = 0x0107,
            WM_SYSKEYDOWN = 0x0104,
            WM_SYSKEYUP = 0x0105
        }

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

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SetWindowsHookEx(
            int idHook,
            HookProc lpfn,
            IntPtr hMod,
            uint dwThreadId);

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

        [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetCurrentThreadId();

        [StructLayout(LayoutKind.Sequential)]
        public struct Point
        {
            public int X;
            public int Y;

            public Point(int x, int y)
            {
                X = x;
                Y = y;
            }

            public static implicit operator System.Drawing.Point(Point p)
            {
                return new System.Drawing.Point(p.X, p.Y);
            }

            public static implicit operator Point(System.Drawing.Point p)
            {
                return new Point(p.X, p.Y);
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MouseHookStructEx
        {
            public Point pt;
            public IntPtr hwnd;
            public uint wHitTestCode;
            public IntPtr dwExtraInfo;
            public int MouseData;
        }
    }
}

VBE 加载项示例

这里是 VBA 编辑器(VBE 加载项)的工作示例:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Extensibility;

namespace VbeAddin
{
    [ComVisible(true)]
    [ProgId("VbeAddin.Connect")]
    [Guid("95840C70-5A1A-4EDB-B436-40E8BF030469")]
    public class Connect : StandardOleMarshalObject, IDTExtensibility2
    {
        // NOTE: We need a backing field to prevent the delegate being garbage collected
        private SafeNativeMethods.HookProc _mouseProc;
        private SafeNativeMethods.HookProc _keyboardProc;

        private IntPtr _hookIdMouse;
        private IntPtr _hookIdKeyboard;

        #region IDTExtensibility2 Members

        public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
            _mouseProc = MouseHookCallback;
            _keyboardProc = KeyboardHookCallback;

            SetWindowsHooks();
        }

        public void OnDisconnection(ext_DisconnectMode removeMode, ref Array custom)
        {
            UnhookWindowsHooks();
        }

        public void OnAddInsUpdate(ref Array custom)
        {
        }

        public void OnStartupComplete(ref Array custom)
        {
        }

        public void OnBeginShutdown(ref Array custom)
        {
        }

        #endregion

        private void SetWindowsHooks()
        {
            uint threadId = (uint)SafeNativeMethods.GetCurrentThreadId();

            _hookIdMouse =
                SafeNativeMethods.SetWindowsHookEx(
                    (int)SafeNativeMethods.HookType.WH_MOUSE,
                    _mouseProc,
                    IntPtr.Zero,
                    threadId);

            _hookIdKeyboard =
                SafeNativeMethods.SetWindowsHookEx(
                    (int)SafeNativeMethods.HookType.WH_KEYBOARD,
                    _keyboardProc,
                    IntPtr.Zero,
                    threadId);
        }

        private void UnhookWindowsHooks()
        {
            SafeNativeMethods.UnhookWindowsHookEx(_hookIdKeyboard);
            SafeNativeMethods.UnhookWindowsHookEx(_hookIdMouse);
        }

        private IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                var mouseHookStruct =
                    (SafeNativeMethods.MouseHookStructEx)
                        Marshal.PtrToStructure(
                            lParam,
                            typeof(SafeNativeMethods.MouseHookStructEx));

                // handle mouse message here
                var message = (SafeNativeMethods.WindowMessages)wParam;
                Debug.WriteLine(
                    "{0} event detected at position {1} - {2}",
                    message,
                    mouseHookStruct.pt.X,
                    mouseHookStruct.pt.Y);
            }
            return SafeNativeMethods.CallNextHookEx(
                _hookIdKeyboard,
                nCode,
                wParam,
                lParam);
        }

        private IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0)
            {
                // handle key message here
                Debug.WriteLine("Key event detected.");
            }
            return SafeNativeMethods.CallNextHookEx(
                _hookIdKeyboard,
                nCode,
                wParam,
                lParam);
        }
    }

    internal static class SafeNativeMethods
    {
        public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        public enum HookType
        {
            WH_KEYBOARD = 2,
            WH_MOUSE = 7
        }

        public enum WindowMessages : uint
        {
            WM_KEYDOWN = 0x0100,
            WM_KEYFIRST = 0x0100,
            WM_KEYLAST = 0x0108,
            WM_KEYUP = 0x0101,
            WM_LBUTTONDBLCLK = 0x0203,
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_MBUTTONDBLCLK = 0x0209,
            WM_MBUTTONDOWN = 0x0207,
            WM_MBUTTONUP = 0x0208,
            WM_MOUSEACTIVATE = 0x0021,
            WM_MOUSEFIRST = 0x0200,
            WM_MOUSEHOVER = 0x02A1,
            WM_MOUSELAST = 0x020D,
            WM_MOUSELEAVE = 0x02A3,
            WM_MOUSEMOVE = 0x0200,
            WM_MOUSEWHEEL = 0x020A,
            WM_MOUSEHWHEEL = 0x020E,
            WM_RBUTTONDBLCLK = 0x0206,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205,
            WM_SYSDEADCHAR = 0x0107,
            WM_SYSKEYDOWN = 0x0104,
            WM_SYSKEYUP = 0x0105
        }

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

        [DllImport("user32.dll", SetLastError = true)]
        public static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern IntPtr SetWindowsHookEx(
            int idHook,
            HookProc lpfn,
            IntPtr hMod,
            uint dwThreadId);

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

        [DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetCurrentThreadId();

        [StructLayout(LayoutKind.Sequential)]
        public struct Point
        {
            public int X;
            public int Y;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct MouseHookStructEx
        {
            public Point pt;
            public IntPtr hwnd;
            public uint wHitTestCode;
            public IntPtr dwExtraInfo;
            public int MouseData;
        }
    }
}