Detecting text changes in Word 2016 from VSTO add-in
Asked Answered
S

2

18

This question is very closely related to How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)? (and in fact includes the sample code from the answer to that question), but this is specifically about developing in Visual Studio (Professional) 2015 for Word 2016 running in Windows 10.

I’m trying to detect when text changes in a Word document from a VSTO add-in. I understand from

that there’s no event-driven way to do this. Word simply does not send events when text changes.

I’ve seen two workarounds discussed:

  1. Use the WindowSelectionChange event. Unfortunately, this event appears to be sent when the selection is changed by pressing arrow keys, using the mouse, performing undo or redo, and probably other actions, but not when typing or deleting.
  2. Use a low-level keydown event hook. This has been discussed in several of those StackOverflow questions, and was also called a “widely spread technique” in a thread on a Visual Studio forum in February 2014.

I’m trying to use the code in the answer to How to get the “KeyPress” event from a Word 2010 Addin (developed in C#)?, and it seems to observe every keydown event except those sent to Word 2016.

Here’s the code I’m using, for ease of reference.

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

When I run Word 2016 with this add-in, I see keydown events sent to the Edge browser and even Visual Studio, but not to Word itself.

Are keydown hooks somehow prevented in Word 2016, or am I doing something wrong?

Scleroma answered 24/9, 2015 at 20:48 Comment(0)
W
17

Everthing should work fine if you don't use a low-level hook in your VSTO add-in.

[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);
}

Please note that you probably also need to create a hook to intercept mouse messages as it is possible to modify the text of a document solely by mouse interactions (e.g. copy and paste via Ribbon or context menu).

VSTO Sample

Here is a complete working VSTO sample including keyboard and mouse hooks:

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 Add-in Sample

And here is a working sample for the VBA editor (VBE add-in):

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;
        }
    }
}
Warthog answered 24/11, 2015 at 15:29 Comment(21)
In that MSDN blog post, it seems that SetWindowsHookEx is called with null hMod and a thread ID when idHook is WH_GETMESSAGE and WH_MOUSE, but not WH_KEYBOARD_LL (which is near the bottom of the post). When idHook is WH_KEYBOARD_LL, the call to SetWindowsHookEx in the post looks the same as the code I’m using. At any rate, calling SetWindowsHookEx with null hMod and a thread ID does not appear to capture keydown events in Word 2016.Scleroma
I second @Nate's observations. Also AppDomain.GetCurrentThreadId is deprecated since .net 2.0; one needs to pinvoke GetCurrentThreadId off kernel32.dll, but it's not working in Office 2010... darn, I wanted this to be the answer!Limitary
Interestingly, AppDomain.GetCurrentThreadId() returns an int and needs to be cast to a uint for the code to compile.Limitary
@Nate: I updated the sample code accordingly. Important is that I also changed to use a WH_KEYBOARD hook (instead of a low-level hook) because we are only interested in events from the current process.Warthog
Note that if you want to know the pressed key, I got it from wParam by more-or-less replacing Debug.WriteLine("Key event detected."); with Debug.WriteLine(((System.Windows.Forms.Keys)wParam).ToString());. I’m not sure if this is a safe thing to do, but it looks like it works.Scleroma
It works!! - I'm so happy I put up that bounty, you earned it well!Limitary
I am trying to pick the WINDOW MESSAGE but its not working... if (nCode >= 0) { if (wParam == (IntPtr)SafeNativeMethods.WindowMessages.WM_KEYDOWN) { // Key Down Code Here } }Belfry
@DirkVollmar As we can handle mouse message at var message = (SafeNativeMethods.WindowMessages)wParam; so How can we handle keyboard events in keyboard hook...???Belfry
Dirk, I am somewhat confused - you say to not use low-level hooks, but based on only a superficial look at the code you provide, hooks are used. Maybe there are low- and high-level hooks that I am not aware of? Thanks.Myself
@Sabuncu: Yes, the code is using hooks, a mechanism to intercept certain types of events (and it's these events we are interested in). With a low-level hook you get the events at a different, lower level.Warthog
With a non-low-level hook, events are already interpreted and you get information, e.g. about the repeat count of a key event, directly (see the documentation of the KeyboardProc callback function), whereas with a low-level hook you get the raw data (see LowLevelKeyboardProc callback function)Warthog
THANK YOU for your very helpful answers. Too bad I can only upvote once!Myself
@DirkVollmar Trying to use sample answer code. Can some one explain why theKeyboardHookCallback method would fire 6 times in a row after a single key press?Femineity
@Selwyn: That's likely because if you press and release a single key several events are generated such as 'key down' and 'key up'. Usually you would get only these events, but if you press modifier keys you would end up with additional messages. Getting six messages when typing a single letter seems indeed strange. Are you sure you only registered the hook once?Warthog
@Dirk that is the first thing I checked, placed a break point at the hook and it is only called once but i'm pretty much copy/pasting the code from the answer. Another user indicated he experienced the same symptoms in another question/answer but did not explain why: #39264817Femineity
@DirkVollmar actually I take that back it's not an exact copy/paste. I am calling SetWindowsHooks() inside ThisAddin_Startup(...)Femineity
@Dirk Vollmar Thanks for your great answer. It will be a great help if you could make it more clear how to handle the key message. For example: How can I know that the user is typing? Checking if it's a char is no good, cause CTRL-C will give us a a char as well. After all the title here is "Detecting text changes". That will be a great help - How to handle the event to know if the text changed?.Smuggle
@YeshurunKubi Are you able to detect only text changes? Or any other approach. Please share with us. I was looking something for text changes detection on paste also. Thanks!Bevon
@PriyankKotiyal I was not able to detect only text changes! And could not handle this code to work properly - for the KeyboardHookCallback fires many times as mentioned above by @Selwyn.Smuggle
@DirkVollmar How to handle copy/cut/paste via ribbon and context menu? Are there any other handlers for them? Can you share some code sample?Bevon
KeyboardHookCallback is getting multiple times even for a single key press? Around 51k+ times for doc having 150 pages.Bevon
C
6

I've experienced this same exact issue in Word 2013 and had to come up with a somewhat "creative" solution. It uses diffplex to monitor changes in the text of the active document and fires events when it changes. It's less than ideal but we do what we have to do to make things work.

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

Usage:

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
    }
}
Coed answered 22/11, 2015 at 23:17 Comment(5)
How does it handle text being inserted in the middle of a page? If I read it properly (I'm not all that familiar with the Word API), it only fires up when the last word on a page mismatches the last word from the previous iteration, correct?Limitary
That's correct. It's a naive approach but worked for my scenario. I updated it to use a diffing library (diffplex) to detect all changes and thus handle inserts.Coed
I should also point out that it only fires when the document changes so if you're trying to detect keypresses like Shift, Ctrl, Alt, etc. it won't work.Coed
Interesting high-level approach nonetheless. I hope to see a low-level hook work.. how's performance? I'd be porting this idea to work off VBComponent objects in the VBE object model - a different problem than OP, but I'm using a modified version of OP's low-level hook to capture changes in the active CodePane and, to put it simply, trigger a parser task at each keystroke.Limitary
Performance is actually good. The processor never jumped over 1% while typing for me. You can modify the sleep time in the background thread to throttle the detection rate even further. Hope it helps!Coed

© 2022 - 2024 — McMap. All rights reserved.