It’s easy to use Windows Hooks, even from C#


Microsoft Windows is based on showing windows of data to the user (duh). An application can create windows with which to interact with the user. Every window has a Window Handle, which is used just like an ID number. The Windows OS communicates with the application windows via Windows Messages. For example, when you type a key, a WM_KEYDOWN message is sent to the window. If you uncover a part of a window, a WM_PAINT message is sent to the window telling it to paint itself. If you click on a button, a WM_MOUSEDOWN is sent. If the mouse handler does a lot of work (like query a database) on the same thread as the Click, other windows Messages may not get processed, and the user might try to interact with an unresponsive window.

You can see how the plumbing works by starting Visual Studio, File->New->Project->C++ ->Win32->Win 32 Project. Then choose all the defaults in the wizard. A simple program that creates a window and handles the messages is created for you. This sample program has a main “message loop” which just gets and dispatches the messages, which can get processed by the provided Window Procedure.

There are several programming libraries or frameworks available to ease working with Windows windows. Below is a Windows Presentation Foundation application written in C#. Others include Microsoft Foundation Classes (MFC), GDI, GDI+, DirectX, Windows Forms. Each of these technologies has their strengths, weaknesses and idiosyncrasies.

It’s easy to use Windows Hooks to monitor messages being sent to a window. Try the code below. Try it on a Touch device, such as a Surface Pro.

Check out the WH_JOURNALRECORD and WH_JOURNALPLAYBACK hooks, which allow you to record and playback a sequence of windows events. Be careful, because you can hang the entire system.

See also:

Spy on your programs to learn about SPY++, a tool that can show messages being sent to various windows. (“C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\spyxx.exe” )

<code>

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;

// Start Visual Studio
// File->New->Project->C#->WPF Application
// Replace MainWindow.Xaml.cs with this code

namespace WpfApp1
{
  /// <summary>
  /// Interaction logic for MainWindow.xaml
  /// </summary>
  public partial class MainWindow : Window
  {
    IntPtr _hook;
    IntPtr _hwnd;
    NativeMethods.CBTProc _callback;
    TextBox _txtStatus;
    CheckBox _chkBox;
    public MainWindow()
    {
      InitializeComponent();
      this.WindowState = WindowState.Maximized;
      // create an instance of the delegate that
      // won't be garbage collected to avoid:
      //   Managed Debugging Assistant 'CallbackOnCollectedDelegate' :** 
      //   'A callback was made on a garbage collected delegate of type 
      //   'WpfApp1!WpfApp1.MainWindow+NativeMethods+CBTProc::Invoke'. 
      //   This may cause application crashes, corruption and data loss. 
      //   When passing delegates to unmanaged code, they must be 
      //   kept alive by the managed application until it is guaranteed 
      //   that they will never be called.'
      _callback = this.CallBack;
      _hook = NativeMethods.SetWindowsHookEx(
          NativeMethods.HookType.WH_MOUSE,
          _callback,
          instancePtr: IntPtr.Zero,
          threadID: NativeMethods.GetCurrentThreadId());
      this.Closed += MainWindow_Closed;
      this.Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
      try
      {
        _txtStatus = new TextBox()
        {
          Margin = new Thickness(10, 0, 0, 0),
          MaxLines = 60,
          IsReadOnly = true,
          VerticalScrollBarVisibility = ScrollBarVisibility.Auto
        };
        _chkBox = new CheckBox()
        {
          Content = "Divert mouse messages",
          ToolTip = "try to move the mouse around and click on the window controls\r" +
            "You can reenable the mouse by using the keyboard\r" + 
            "(spacebar when the chkbox has focus) to uncheck this option\r"+
            "Notice that tooltips don't show when checkbox is checked"
        };
        var btn = new Button()
        {
          Content = "Click Me",
          ToolTip = "Now you see me",
          Width = 150,
          HorizontalAlignment = HorizontalAlignment.Left
        };
        btn.Click += (ob, eb) =>
        {
          AddStatusMsg("Thanks, I needed that");
        };
        var sp = new StackPanel()
        {
          Orientation = Orientation.Vertical
        };
        sp.Children.Add(
          new Label()
          {
            Content = "Move the mouse around"
          });
        sp.Children.Add(_chkBox);
        sp.Children.Add(btn);
        sp.Children.Add(_txtStatus);
        this.Content = sp;
        AddStatusMsg("Starting");
      }
      catch (Exception ex)
      {
        this.Content = ex.ToString();
      }
    }
    public void AddStatusMsg(string msg)
    {
      Dispatcher.BeginInvoke(new Action(
          () =>
          {
            var txt = DateTime.Now.ToString("MM/dd/yy HH:mm:ss:fff ") + msg;
            _txtStatus.AppendText(txt + "\r\n");
            _txtStatus.ScrollToEnd();
          }
          ));
    }

    private void MainWindow_Closed(object sender, EventArgs e)
    {
      if (_hook != IntPtr.Zero)
      {
        NativeMethods.UnhookWindowsHookEx(this._hook);
      }
    }

    private IntPtr CallBack(int code, IntPtr wParam, IntPtr lParam)
    {
      var wind = Window.GetWindow(this);
      if (_hwnd == null)
      {
        _hwnd = (new WindowInteropHelper(wind)).EnsureHandle();
      }
      var mouseInfo = Marshal.PtrToStructure<NativeMethods.MOUSEHOOKSTRUCT>(lParam);
      AddStatusMsg($"{code} wParam {wParam} lParam {lParam} {mouseInfo}");
      if (_chkBox.IsChecked == true)
      {
        return new IntPtr(1); // non-zero indicates don't pass to target
      }
      return NativeMethods.CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
    }

    private static class NativeMethods
    {
      [StructLayout(LayoutKind.Sequential)]
      public struct MOUSEHOOKSTRUCT
      {
        public POINT pt; // Can't use System.Windows.Point because that has X,Y as doubles, not integer
        public IntPtr hwnd;
        public uint wHitTestCode;
        public IntPtr dwExtraInfo;
        public override string ToString()
        {
          return $"({pt.X,4},{pt.Y,4})";
        }
      }

#pragma warning disable 649 // CS0649: Field 'MainWindow.NativeMethods.POINT.Y' is never assigned to, and will always have its default value 0
      public struct POINT
      {
        public int X;
        public int Y;
      }
#pragma warning restore 649

      // from WinUser.h
      public enum HookType
      {
        WH_MIN = (-1),
        WH_MSGFILTER = (-1),
        WH_JOURNALRECORD = 0,
        WH_JOURNALPLAYBACK = 1,
        WH_KEYBOARD = 2,
        WH_GETMESSAGE = 3,
        WH_CALLWNDPROC = 4,
        WH_CBT = 5,
        WH_SYSMSGFILTER = 6,
        WH_MOUSE = 7,
        WH_HARDWARE = 8,
        WH_DEBUG = 9,
        WH_SHELL = 10,
        WH_FOREGROUNDIDLE = 11,
        WH_CALLWNDPROCRET = 12,
        WH_KEYBOARD_LL = 13,
        WH_MOUSE_LL = 14
      }
      public enum HookCodes
      {
        HC_ACTION = 0,
        HC_GETNEXT = 1,
        HC_SKIP = 2,
        HC_NOREMOVE = 3,
        HC_NOREM = HC_NOREMOVE,
        HC_SYSMODALON = 4,
        HC_SYSMODALOFF = 5
      }
      public enum CBTHookCodes
      {
        HCBT_MOVESIZE = 0,
        HCBT_MINMAX = 1,
        HCBT_QS = 2,
        HCBT_CREATEWND = 3,
        HCBT_DESTROYWND = 4,
        HCBT_ACTIVATE = 5,
        HCBT_CLICKSKIPPED = 6,
        HCBT_KEYSKIPPED = 7,
        HCBT_SYSCOMMAND = 8,
        HCBT_SETFOCUS = 9
      }

      public delegate IntPtr CBTProc(int code, IntPtr wParam, IntPtr lParam);

      [DllImport("user32.dll")]
      [return: MarshalAs(UnmanagedType.Bool)]
      public static extern bool UnhookWindowsHookEx(IntPtr hookPtr);

      [DllImport("user32.dll")]
      public static extern IntPtr CallNextHookEx(IntPtr hookPtr, int nCode, IntPtr wordParam, IntPtr longParam);

      [DllImport("user32.dll")]
      public static extern IntPtr SetWindowsHookEx(HookType hookType, CBTProc hookProc, IntPtr instancePtr, uint threadID);

      [DllImport("kernel32.dll")]
      public static extern uint GetCurrentThreadId();
    }
  }

}

</code>

Comments (0)

Skip to main content