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

Nat*_*ate 15 vsto ms-word office-addins visual-studio-2015

这个问题与如何从Word 2010 Addin(用C#开发)中获取"KeyPress"事件密切相关(实际上包括来自该问题答案的示例代码),但这特别是关于在Windows 10中运行的Word 2016的Visual Studio(Professional)2015中进行开发.

我正在尝试检测来自VSTO加载项的Word文档中的文本何时更改.我从中了解到

没有事件驱动的方式来做到这一点.Word在文本更改时根本不发送事件.

我看过两个解决方法:

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

我试图在如何从Word 2010 Addin(用C#开发)中获取"KeyPress"事件的答案中使用代码,它似乎观察每个keydown事件,除了那些发送到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
    }
}
Run Code Online (Sandbox Code Playgroud)

当我使用此加载项运行Word 2016时,我看到发送到Edge浏览器甚至Visual Studio的keydown事件,但不是Word本身.

是否在2016年以某种方式阻止了keydown挂钩,或者我做错了什么?

Dir*_*mar 13

如果您不在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);
}
Run Code Online (Sandbox Code Playgroud)

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 请注意,如果您想知道按下的键,我从 `wParam` 中得到它,方法是或多或少地将 `Debug.WriteLine("Key event detected.");` 替换为 `Debug.WriteLine(((System.Windows .Forms.Keys)wParam).ToString());`。我不确定这是否安全,但看起来它有效。 (2认同)
  • 使用非低级挂钩,事件已经被解释,并且您可以直接获得信息,例如关于按键事件的重复计数(请参阅 [`KeyboardProc` 回调函数](https://msdn.microsoft) 的文档.com/en-us/library/windows/desktop/ms644984(v=vs.85).aspx)),而使用低级钩子可以获得原始数据(参见[`LowLevelKeyboardProc`回调函数](https: //msdn.microsoft.com/en-us/library/windows/desktop/ms644985(v=vs.85).aspx)) (2认同)

mr.*_*eze 5

我在 Word 2013 中也遇到过同样的问题,并且必须想出一个有点“创意”的解决方案。它使用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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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
    }
}
Run Code Online (Sandbox Code Playgroud)