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.