Is it possible to get caret position in Word to update faster?

277 views Asked by At

I'm using a low level keyboard hook and GUITHREADINFO to get caret position in Windows applications on every keyup.

Works well for almost all applications - except Word. For some reason Word only updates the position of the caret after some arbitrary delay (meaning the position will always be off one or more characters).

I can get correct position if I wait a couple hundred milliseconds (Thread.Sleep(400) for example) before fetching the position - but this usable solution for the application I'm working on.

Any way I can get the correct caret position faster? Either by forcing Word to render the caret, using a Word specific function, subscribing to a Word event or something entirely different?

Keyboard hook class:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.ComponentModel;


namespace CS_test
{
    /// <summary>
    /// A class that manages a global low level keyboard hook
    /// </summary>
    public class GlobalKeyboardHook
    {
        #region Constant, Structure and Delegate Definitions
        /// <summary>
        /// defines the callback type for the hook
        /// </summary>
        public delegate int KeyboardHookProc(int code, int wParam, ref KeyboardHookStruct lParam);

        private static KeyboardHookProc _callbackDelegate;

        public struct KeyboardHookStruct // C++ KBDLLHOOKSTRUCT 
        {
            public int vkCode;
            public int scanCode;
            public int flags;
            public int time;
            public int dwExtraInfo;
        }

        const int WH_KEYBOARD = 2;
        const int WH_KEYBOARD_LL = 13;
        const int WM_KEYDOWN = 0x100;
        const int WM_KEYUP = 0x101;
        const int WM_SYSKEYDOWN = 0x104;
        const int WM_SYSKEYUP = 0x105;
        const int LLKHF_INJECTED = 0x10;
        #endregion

        #region Instance Variables
        /// <summary>
        /// The collections of keys to watch for
        /// </summary>
        public List<Keys> HookedKeys = new List<Keys>();
        /// <summary>
        /// Handle to the hook, need this to unhook and call the next hook
        /// </summary>
        IntPtr hhook = IntPtr.Zero;

        private bool _ignoreInjected = false;
        private bool _shiftHeld = false;
        private bool _ctrlHeld = false;
        private bool _altHeld = false;

        #endregion

        #region Events
        /// <summary>
        /// Occurs when one of the hooked keys is pressed
        /// </summary>
        public event KeyEventHandler KeyDown;
        /// <summary>
        /// Occurs when one of the hooked keys is released
        /// </summary>
        public event KeyEventHandler KeyUp;
        #endregion

        #region Constructors and Destructors
        /// <summary>
        /// Initializes a new instance of the <see cref="globalKeyboardHook"/> class and installs the keyboard hook.
        /// </summary>
        public GlobalKeyboardHook(bool ignoreInjected)
        {
            _ignoreInjected = ignoreInjected;
            Hook();
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="globalKeyboardHook"/> is reclaimed by garbage collection and uninstalls the keyboard hook.
        /// </summary>
        ~GlobalKeyboardHook()
        {
            Unhook();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Installs the global hook
        /// </summary>
        /// 

        private void Hook()
        {
            _callbackDelegate = new KeyboardHookProc(HookProc);
            //IntPtr hInstance = LoadLibrary("User32");
            //hhook = SetWindowsHookEx(WH_KEYBOARD_LL, callbackDelegate, hInstance, 0);
            hhook = SetWindowsHookEx(WH_KEYBOARD_LL, _callbackDelegate, IntPtr.Zero, 0);
            if (hhook == IntPtr.Zero) throw new Win32Exception();
        }

        /// <summary>
        /// Uninstalls the global hook
        /// </summary>
        public bool Unhook()
        {
            try
            {
                bool ok = UnhookWindowsHookEx(hhook);
                if (!ok) throw new Win32Exception();
                _callbackDelegate = null;
                return true;
            }
            catch (Exception) { }

            return false;
        }

        /// <summary>
        /// The callback for the keyboard hook
        /// </summary>
        /// <param name="code">The hook code, if it isn't >= 0, the function shouldn't do anyting</param>
        /// <param name="wParam">The event type</param>
        /// <param name="lParam">The keyhook event information</param>
        /// <returns></returns>
        /// 
        public int HookProc(int code, int wParam, ref KeyboardHookStruct lParam)
        {
            if (code >= 0)
            {
                // Continue only if injected keys are allowed or key is not injected
                if (!_ignoreInjected || (lParam.flags & LLKHF_INJECTED) == 0)
                {
                    // Determine modifiers
                    if (lParam.vkCode == 160 || lParam.vkCode == 161) // LShiftKey / RShiftKey
                    {
                        _shiftHeld = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN);
                    }
                    else if (lParam.vkCode == 162 || lParam.vkCode == 163) // LControlKey / RControlKey
                    {
                        _ctrlHeld = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN);
                    }
                    else if (lParam.vkCode == 164 || lParam.vkCode == 165) // LMenu / RMenu aka LAlt / RAlt
                    {
                        // We can also determine if Alt is held by checking if bit 5 (0-indexed) of lParam.flags is set
                        _altHeld = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN);
                    }

                    Keys key = (Keys)lParam.vkCode;
                    if (HookedKeys.Contains(key))
                    {
                        Keys keyModified = key;
                        if (_shiftHeld) keyModified |= Keys.Shift;
                        if (_ctrlHeld) keyModified |= Keys.Control;
                        if (_altHeld) keyModified |= Keys.Alt;
                        KeyEventArgs kea = new KeyEventArgs(keyModified);

                        // Call keydown event handlers
                        if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) && (KeyDown != null))
                        {
                            KeyDown(this, kea);
                        }

                        // Call keyup event handlers
                        else if ((wParam == WM_KEYUP || wParam == WM_SYSKEYUP) && (KeyUp != null))
                        {
                            KeyUp(this, kea);
                        }

                        // Check if any of the handlers have handled the event
                        if (kea.Handled)
                        {
                            return 1;
                        }
                    }
                }
            }

            try
            {
                // This can crash if Windows has decided to unhook the proc during this function
                return CallNextHookEx(hhook, code, wParam, ref lParam);
            }
            catch (Exception ex)
            {
                Console.WriteLine("GlobalKeyboardHook exception: " + ex.Message);
                return 0;
            }

        }
        #endregion

        #region DLL imports
        /// <summary>
        /// Sets the windows hook, do the desired event, one of hInstance or threadId must be non-null
        /// </summary>
        /// <param name="idHook">The id of the event you want to hook</param>
        /// <param name="callback">The callback.</param>
        /// <param name="hInstance">The handle you want to attach the event to, can be null</param>
        /// <param name="threadId">The thread you want to attach the event to, can be null</param>
        /// <returns>a handle to the desired hook</returns>
        [DllImport("user32.dll")]
        static extern IntPtr SetWindowsHookEx(int idHook, KeyboardHookProc callback, IntPtr hInstance, uint threadId);

        /// <summary>
        /// Unhooks the windows hook.
        /// </summary>
        /// <param name="hInstance">The hook handle that was returned from SetWindowsHookEx</param>
        /// <returns>True if successful, false otherwise</returns>
        [DllImport("user32.dll")]
        static extern bool UnhookWindowsHookEx(IntPtr hInstance);

        /// <summary>
        /// Calls the next hook.
        /// </summary>
        /// <param name="idHook">The hook id</param>
        /// <param name="nCode">The hook code</param>
        /// <param name="wParam">The wparam.</param>
        /// <param name="lParam">The lparam.</param>
        /// <returns></returns>
        [DllImport("user32.dll")]
        static extern int CallNextHookEx(IntPtr idHook, int nCode, int wParam, ref KeyboardHookStruct lParam);

        /// <summary>
        /// Loads the library.
        /// </summary>
        /// <param name="lpFileName">Name of the library</param>
        /// <returns>A handle to the library</returns>
        [DllImport("kernel32.dll")]
        static extern IntPtr LoadLibrary(string lpFileName);
        #endregion
    }

    public class KeyboardHookLogEventArgs : EventArgs
    {
        public string Message;
        public KeyboardHookLogEventArgs(string message)
        {
            Message = message;
        }
    }
}

The caret helper class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Runtime.InteropServices;

namespace CS_test
{
    public class CaretInfo
    {
        public double Width { get; private set; }
        public double Height { get; private set; }
        public double Left { get; private set; }
        public double Top { get; private set; }

        public CaretInfo(double width, double height, double left, double top)
        {
            Width = width;
            Height = height;
            Left = left;
            Top = top;
        }
    }

    public class CaretHelper
    {

        public static CaretInfo GetCaretPosition()
        {
            // Get GUI info containing caret poisition
            var guiInfo = new GUITHREADINFO();
            guiInfo.cbSize = (uint)Marshal.SizeOf(guiInfo);
            GetGUIThreadInfo(0, out guiInfo);

            // Convert caret position to position relative to top left corner of current editor window - it seems like this is not neccessary
            /*var caretInfo = new System.Windows.Point();
            caretInfo.X = guiInfo.rcCaret.left;
            caretInfo.Y = guiInfo.rcCaret.top;
            ClientToScreen(guiInfo.hwndCaret, out caretInfo);*/

            // Get width/height
            double width = guiInfo.rcCaret.right - guiInfo.rcCaret.left;
            double height = guiInfo.rcCaret.bottom - guiInfo.rcCaret.top;

            // Get left/top relative to screen
            RECT rect;
            GetWindowRect(guiInfo.hwndFocus, out rect);

            double left = guiInfo.rcCaret.left + rect.left + 2; // Seems to always be 2 pixels off
            double top = guiInfo.rcCaret.top + rect.top + 2; // Seems to always be 2 pixels off

            return new CaretInfo(width, height, left, top);
        }

        [DllImport("user32.dll", EntryPoint = "GetGUIThreadInfo")]
        public static extern bool GetGUIThreadInfo(uint tId, out GUITHREADINFO threadInfo);

        [DllImport("user32.dll")]
        public static extern bool ClientToScreen(IntPtr hWnd, out Point position);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetWindowRect(IntPtr handle, out RECT lpRect);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetClientRect(IntPtr hWnd, ref Rect rect);

        [StructLayout(LayoutKind.Sequential)]
        public struct GUITHREADINFO
        {
            public uint cbSize;
            public uint flags;
            public IntPtr hwndActive;
            public IntPtr hwndFocus;
            public IntPtr hwndCapture;
            public IntPtr hwndMenuOwner;
            public IntPtr hwndMoveSize;
            public IntPtr hwndCaret;
            public RECT rcCaret;
        };

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }
    }
}

WPF MainWindow:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CS_test
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            GlobalKeyboardHook hook = new GlobalKeyboardHook(true);
            foreach (Keys key in Enum.GetValues(typeof(Keys))) // Add keys that KeyboardHook should listen for
            {
                if (key != Keys.Control && key != Keys.ControlKey)
                {
                    hook.HookedKeys.Add(key);
                }
            }
            hook.KeyUp += hook_KeyUp;
        }

        void hook_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e)
        {
            CaretInfo info = CaretHelper.GetCaretPosition();
            Console.WriteLine(info.Left + ", " + info.Top);
        }
    }
}

Try typing 3 letters in quick succession - should yield a log with the same coordinates for all three keyups.

0

There are 0 answers