Browse Source

Move global mouse hook to a separate thread in a separate window

Equbuxu 2 years ago
parent
commit
1ca158c31f

+ 60 - 53
src/PixiEditor/Helpers/GlobalMouseHook.cs

@@ -5,7 +5,9 @@ using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows;
 using System.Windows.Input;
 using System.Windows.Input;
+using System.Windows.Interop;
 using System.Windows.Threading;
 using System.Windows.Threading;
+using PixiEditor.Views;
 
 
 namespace PixiEditor.Helpers;
 namespace PixiEditor.Helpers;
 
 
@@ -13,86 +15,91 @@ public delegate void MouseUpEventHandler(object sender, Point p, MouseButton but
 
 
 // see https://stackoverflow.com/questions/22659925/how-to-capture-mouseup-event-outside-the-wpf-window
 // see https://stackoverflow.com/questions/22659925/how-to-capture-mouseup-event-outside-the-wpf-window
 [ExcludeFromCodeCoverage]
 [ExcludeFromCodeCoverage]
-internal static class GlobalMouseHook
+internal class GlobalMouseHook
 {
 {
-    private static int mouseHookHandle;
-    private static Win32.HookProc mouseDelegate;
+    private static readonly Lazy<GlobalMouseHook> lazy = new Lazy<GlobalMouseHook>(() => new GlobalMouseHook());
+    public static GlobalMouseHook Instance => lazy.Value;
 
 
+    public event MouseUpEventHandler OnMouseUp;
 
 
-    public static event MouseUpEventHandler OnMouseUp
-    {
-        add
-        {
-            // disable low-level hook in debug to prevent mouse lag when pausing in debugger
-#if !DEBUG
-                Subscribe();
-#endif
-            MouseUp += value;
-        }
+    private int mouseHookHandle;
+    private Win32.HookProc mouseDelegate;
 
 
-        remove
-        {
-            MouseUp -= value;
-#if !DEBUG
-                Unsubscribe();
-#endif
-        }
-    }
+    private Thread mouseHookWindowThread;
+    private IntPtr mainWindowHandle;
+    private IntPtr childWindowHandle;
 
 
-    private static event MouseUpEventHandler MouseUp;
+    private GlobalMouseHook() { }
 
 
-    public static void RaiseMouseUp()
+    public void Initilize(MainWindow window)
     {
     {
-        MouseUp?.Invoke(default, default, default);
-    }
+        // disable low-level hook in debug to prevent mouse lag when pausing in debugger
+#if DEBUG
+        return;
+#endif
+        mainWindowHandle = new WindowInteropHelper(window).Handle;
+        if (mainWindowHandle == IntPtr.Zero)
+            throw new InvalidOperationException();
 
 
-    private static void Unsubscribe()
-    {
-        if (mouseHookHandle != 0)
+        window.Closed += (_, _) =>
         {
         {
-            int result = Win32.UnhookWindowsHookEx(mouseHookHandle);
-            mouseHookHandle = 0;
-            mouseDelegate = null;
-            if (result == 0)
-            {
-                int errorCode = Marshal.GetLastWin32Error();
-                throw new Win32Exception(errorCode);
-            }
-        }
+            if (childWindowHandle != IntPtr.Zero)
+                Win32.PostMessage(childWindowHandle, Win32.WM_CLOSE, 0, 0);
+        };
+
+        mouseHookWindowThread = new Thread(StartMouseHook)
+        {
+            Name = $"{nameof(GlobalMouseHook)} Thread"
+        };
+        mouseHookWindowThread.Start();
     }
     }
 
 
-    private static void Subscribe()
+    private void StartMouseHook()
     {
     {
+        LowLevelWindow window = new LowLevelWindow(nameof(GlobalMouseHook), mainWindowHandle);
+        childWindowHandle = window.WindowHandle;
+
+        mouseDelegate = MouseHookProc;
+        mouseHookHandle = Win32.SetWindowsHookEx(
+            Win32.WH_MOUSE_LL,
+            mouseDelegate,
+            Win32.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
+            0);
         if (mouseHookHandle == 0)
         if (mouseHookHandle == 0)
         {
         {
-            mouseDelegate = MouseHookProc;
-            mouseHookHandle = Win32.SetWindowsHookEx(
-                Win32.WH_MOUSE_LL,
-                mouseDelegate,
-                Win32.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
-                0);
-            if (mouseHookHandle == 0)
-            {
-                int errorCode = Marshal.GetLastWin32Error();
-                throw new Win32Exception(errorCode);
-            }
+            int errorCode = Marshal.GetLastWin32Error();
+            throw new Win32Exception(errorCode);
         }
         }
+
+        window.RunEventLoop();
     }
     }
 
 
-    private static int MouseHookProc(int nCode, int wParam, IntPtr lParam)
+    //private void Unsubscribe()
+    //{
+    //    int result = Win32.UnhookWindowsHookEx(mouseHookHandle);
+    //    mouseHookHandle = 0;
+    //    mouseDelegate = null;
+    //    if (result == 0)
+    //    {
+    //        int errorCode = Marshal.GetLastWin32Error();
+    //        throw new Win32Exception(errorCode);
+    //    }
+    //}
+
+    private int MouseHookProc(int nCode, int wParam, IntPtr lParam)
     {
     {
         if (nCode >= 0)
         if (nCode >= 0)
         {
         {
             Win32.MSLLHOOKSTRUCT mouseHookStruct = (Win32.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32.MSLLHOOKSTRUCT));
             Win32.MSLLHOOKSTRUCT mouseHookStruct = (Win32.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32.MSLLHOOKSTRUCT));
             if (wParam == Win32.WM_LBUTTONUP || wParam == Win32.WM_MBUTTONUP || wParam == Win32.WM_RBUTTONUP)
             if (wParam == Win32.WM_LBUTTONUP || wParam == Win32.WM_MBUTTONUP || wParam == Win32.WM_RBUTTONUP)
             {
             {
-                if (MouseUp != null)
+                if (OnMouseUp is not null)
                 {
                 {
 
 
                     MouseButton button = wParam == Win32.WM_LBUTTONUP ? MouseButton.Left
                     MouseButton button = wParam == Win32.WM_LBUTTONUP ? MouseButton.Left
                         : wParam == Win32.WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
                         : wParam == Win32.WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
-                    Dispatcher.CurrentDispatcher.BeginInvoke(() =>
-                        MouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
+                    Application.Current?.Dispatcher.BeginInvoke(() =>
+                        OnMouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
                 }
                 }
             }
             }
         }
         }

+ 105 - 0
src/PixiEditor/Helpers/LowLevelWindow.cs

@@ -0,0 +1,105 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace PixiEditor.Helpers;
+internal class LowLevelWindow
+{
+    private bool disposed;
+    private Win32.WndProc wndProcDelegate;
+
+    public IntPtr WindowHandle { get; private set; }
+
+    public LowLevelWindow(string uniqueWindowName, IntPtr parentWindow)
+    {
+        if (string.IsNullOrEmpty(uniqueWindowName))
+            throw new ArgumentException(nameof(uniqueWindowName));
+
+        wndProcDelegate = CustomWndProc;
+
+        // Create WNDCLASS
+        Win32.WNDCLASS windowParams = new Win32.WNDCLASS();
+        windowParams.lpszClassName = uniqueWindowName;
+        windowParams.lpfnWndProc = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
+
+        ushort classAtom = Win32.RegisterClassW(ref windowParams);
+
+        int lastError = Marshal.GetLastWin32Error();
+        if (classAtom == 0 && lastError != Win32.ERROR_CLASS_ALREADY_EXISTS)
+            throw new Win32Exception("Could not register window class");
+
+        // Create window
+        WindowHandle = Win32.CreateWindowExW(
+            0,
+            uniqueWindowName,
+            String.Empty,
+            Win32.WS_CHILD, //| Win32.WS_OVERLAPPEDWINDOW
+            0,
+            0,
+            0,
+            0,
+            parentWindow,
+            IntPtr.Zero,
+            IntPtr.Zero,
+            IntPtr.Zero
+        );
+
+        if (WindowHandle == 0)
+            throw new Win32Exception("Could not create window");
+
+        //Win32.ShowWindow(WindowHandle, 1);
+        //Win32.UpdateWindow(WindowHandle);
+    }
+
+    public void RunEventLoop()
+    {
+        while (true)
+        {
+            var bRet = Win32.GetMessage(out Win32.MSG msg, WindowHandle, 0, 0);
+            if (bRet == 0 || msg.message == Win32.WM_CLOSE)
+                return;
+
+            if (bRet == -1)
+            {
+                // handle the error and possibly exit
+            }
+            else
+            {
+                Win32.TranslateMessage(ref msg);
+                Win32.DispatchMessage(ref msg);
+            }
+        }
+    }
+
+    private static IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
+    {
+        return Win32.DefWindowProcW(hWnd, msg, wParam, lParam);
+    }
+
+    public void Dispose()
+    {
+        Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    private void Dispose(bool disposing)
+    {
+        if (!disposed)
+        {
+            if (disposing)
+            {
+                // Dispose managed resources
+            }
+
+            // Dispose unmanaged resources
+            if (WindowHandle != IntPtr.Zero)
+            {
+                Win32.DestroyWindow(WindowHandle);
+                WindowHandle = IntPtr.Zero;
+            }
+            disposed = true;
+        }
+    }
+
+    ~LowLevelWindow() => Dispose(false);
+}

+ 113 - 2
src/PixiEditor/Helpers/Win32.cs

@@ -4,15 +4,59 @@ using System.Text;
 namespace PixiEditor.Helpers;
 namespace PixiEditor.Helpers;
 internal class Win32
 internal class Win32
 {
 {
-    public const int WM_GETMINMAXINFO = 0x0024;
     public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
     public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
     public const int WH_MOUSE_LL = 14;
     public const int WH_MOUSE_LL = 14;
+
+    public const int WM_GETMINMAXINFO = 0x0024;
     public const int WM_LBUTTONUP = 0x0202;
     public const int WM_LBUTTONUP = 0x0202;
     public const int WM_MBUTTONUP = 0x0208;
     public const int WM_MBUTTONUP = 0x0208;
     public const int WM_RBUTTONUP = 0x0205;
     public const int WM_RBUTTONUP = 0x0205;
+    public const int WM_CLOSE = 0x0010;
+    public const int WM_DESTROY = 0x0002;
+
+    public const int ERROR_CLASS_ALREADY_EXISTS = 1410;
+    public const int CW_USEDEFAULT = unchecked((int)0x80000000);
+
+    public const uint WS_CHILD = 0x40000000;
+    public const uint WS_CAPTION = 0x00C00000;
+    public const uint WS_OVERLAPPED = 0x00000000;
+    public const uint WS_SYSMENU = 0x00080000;
+    public const uint WS_THICKFRAME = 0x00040000;
+    public const uint WS_MINIMIZEBOX = 0x00020000;
+    public const uint WS_MAXIMIZEBOX = 0x00010000;
+    public const uint WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
 
 
     public delegate int HookProc(int nCode, int wParam, IntPtr lParam);
     public delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+    public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    internal struct WNDCLASS
+    {
+        public uint style;
+        public IntPtr lpfnWndProc;
+        public int cbClsExtra;
+        public int cbWndExtra;
+        public IntPtr hInstance;
+        public IntPtr hIcon;
+        public IntPtr hCursor;
+        public IntPtr hbrBackground;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszMenuName;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszClassName;
+    }
 
 
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct MSG
+    {
+        public IntPtr hwnd;
+        public uint message;
+        public IntPtr wParam;
+        public IntPtr lParam;
+        public uint time;
+        public POINT pt;
+        public uint lPrivate;
+    }
 
 
     [StructLayout(LayoutKind.Sequential)]
     [StructLayout(LayoutKind.Sequential)]
     public struct MSLLHOOKSTRUCT
     public struct MSLLHOOKSTRUCT
@@ -143,5 +187,72 @@ internal class Win32
 
 
     [DllImport("kernel32.dll")]
     [DllImport("kernel32.dll")]
     public static extern IntPtr GetModuleHandle(string name);
     public static extern IntPtr GetModuleHandle(string name);
-}
 
 
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr CreateWindowExW(
+       UInt32 dwExStyle,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpClassName,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpWindowName,
+       UInt32 dwStyle,
+       Int32 x,
+       Int32 y,
+       Int32 nWidth,
+       Int32 nHeight,
+       IntPtr hWndParent,
+       IntPtr hMenu,
+       IntPtr hInstance,
+       IntPtr lpParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr DefWindowProcW(
+        IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DestroyWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool ShowWindow(
+        IntPtr hWnd,
+        int nCmdShow
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool UpdateWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern int GetMessage(
+        out MSG lpMsg,
+        IntPtr hWnd,
+        uint wMsgFilterMin,
+        uint wMsgFilterMax
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DispatchMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool TranslateMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern System.UInt16 RegisterClassW(
+        [In] ref WNDCLASS lpWndClass
+    );
+
+    [DllImport("Kernel32.dll", SetLastError = true)]
+    public static extern int GetCurrentThreadId();
+
+    [DllImport("user32.dll")]
+    public static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
+}

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/IoViewModel.cs

@@ -32,7 +32,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         MouseMoveCommand = new RelayCommand(mouseFilter.MouseMoveInlet);
         MouseMoveCommand = new RelayCommand(mouseFilter.MouseMoveInlet);
         MouseUpCommand = new RelayCommand(mouseFilter.MouseUpInlet);
         MouseUpCommand = new RelayCommand(mouseFilter.MouseUpInlet);
         PreviewMouseMiddleButtonCommand = new RelayCommand(OnPreviewMiddleMouseButton);
         PreviewMouseMiddleButtonCommand = new RelayCommand(OnPreviewMiddleMouseButton);
-        GlobalMouseHook.OnMouseUp += mouseFilter.MouseUpInlet;
+        GlobalMouseHook.Instance.OnMouseUp += mouseFilter.MouseUpInlet;
 
 
         InputManager.Current.PreProcessInput += Current_PreProcessInput;
         InputManager.Current.PreProcessInput += Current_PreProcessInput;
 
 

+ 6 - 0
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -53,6 +53,7 @@ internal partial class MainWindow : Window
 
 
         DataContext.CloseAction = Close;
         DataContext.CloseAction = Close;
         Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
         Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
+        ContentRendered += MainWindow_ContentRendered;
 
 
         preferences.AddCallback<bool>("ImagePreviewInTaskbar", x =>
         preferences.AddCallback<bool>("ImagePreviewInTaskbar", x =>
         {
         {
@@ -62,6 +63,11 @@ internal partial class MainWindow : Window
         DataContext.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
         DataContext.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
     }
     }
 
 
+    private void MainWindow_ContentRendered(object sender, EventArgs e)
+    {
+        GlobalMouseHook.Instance.Initilize(this);
+    }
+
     public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
     public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
     {
     {
         MainWindow window = new();
         MainWindow window = new();