Подтвердить что ты не робот

Обнаружение изменений текста в Word 2016 из надстройки VSTO

Этот вопрос очень тесно связан с Как получить событие "KeyPress" из Word 2010 Addin (разработанное на С#)? (и на самом деле включает образец код из ответа на этот вопрос), но это особенно касается разработки в Visual Studio (Professional) 2015 для Word 2016, работающей в Windows 10.

Я пытаюсь обнаружить, когда текст изменяется в документе Word из надстройки VSTO. Я понимаю из

что для этого не существует способа, управляемого событиями. Word просто не отправляет события при изменении текста.

Я видел два обходных решения:

  • Используйте WindowSelectionChange событие. К сожалению, это событие отправляется, когда выбор изменяется, нажимая клавиши со стрелками, используя мышь, выполняя отмену или повтор и, возможно, другие действия, но не при вводе или удалении.
  • Используйте низкоуровневый крючок событий. Это обсуждалось в нескольких из этих вопросов StackOverflow и также называлось "широко распространенной техникой" в потоке на форуме Visual Studio в Февраль 2014 года.

Я пытаюсь использовать код в ответе Как получить событие "KeyPress" из Word 2010 Addin (разработанное на С#)?, и кажется наблюдать за каждым событием keydown, кроме тех, которые были отправлены в Word 2016.

Здесь используется код Im для упрощения ссылок.

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 с этой надстройкой, я вижу события keydown, отправленные в браузер Edge и даже Visual Studio, но не в сам Word.

Являются ли блокировки клавиш как-то помешаны в Word 2016, или я делаю что-то неправильно?

4b9b3361

Ответ 1

Everthing должно работать нормально, если вы не используете крючок низкого уровня в своей надстройке 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;
        }
    }
}

Ответ 2

Я испытал эту же самую точную проблему в 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;
        }
    }
}

Использование:

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
    }
}