TolikPylypchuk / SharpHook

SharpHook provides a cross-platform global keyboard and mouse hook, event simulation, and text entry simulation for .NET
https://sharphook.tolik.io
MIT License
342 stars 32 forks source link

Odd Suppress Behavior #97

Closed FaithBeam closed 6 months ago

FaithBeam commented 6 months ago

System: Windows 11 Sharphook version: 5.3.2 .NET Version: 8.0

Hello! I'd like to "convert" a real mouse button 4 click into a simulated mouse button 2 click. My set up is that I listen for mouse button 4 press and release events, suppress that event, and simulate mouse button 2 press and release events. However, I cannot get this to work when I simulate a mouse event. Both the original mouse click and simulated mouse go through. I can suppress the original mouse click when I simulate a keyboard event though.

Here is a video to show what I'm writing about: https://www.youtube.com/watch?v=TDHwdC9fakI

In the video I show two scenarios:

  1. Suppress mouse button 4 and simulate w key press (mouse button 4 suppressed)
  2. Suppress mouse button 4 and simulate mouse button 2 (mouse button 4 not suppressed)

We can see that scenario two does not work because the tabs get flipped through in notepad++ because of mouse button 4.

Here is the code in the video:

using SharpHook;
using SharpHook.Native;

namespace rmbtest;

class Program
{
    static void Main(string[] args)
    { 
        var hook = new SimpleGlobalHook();
        var simulator = new EventSimulator();
        hook.MousePressed += (sender, eventArgs) =>
        {
            switch (eventArgs.Data.Button)
            {
                case MouseButton.Button4:
                    Console.WriteLine(eventArgs.IsEventSimulated);
                    eventArgs.SuppressEvent = true;
                    // simulator.SimulateKeyPress(KeyCode.VcW);
                    simulator.SimulateMousePress(MouseButton.Button2);
                    break;
                case MouseButton.Button5:
                    hook.Dispose();
                    break;
            }
        };
        hook.MouseReleased += (sender, eventArgs) =>
        {
            switch (eventArgs.Data.Button)
            {
                case MouseButton.Button4:
                    Console.WriteLine(eventArgs.IsEventSimulated);
                    eventArgs.SuppressEvent = true;
                    // simulator.SimulateKeyRelease(KeyCode.VcW);
                    simulator.SimulateMouseRelease(MouseButton.Button2);
                    break;
            }
        };
        var t = new Thread(() =>
        {
            hook.Run();
        });
        t.Start();
        t.Join();
    }
}

Am I doing this incorrectly?

Thank you!

TolikPylypchuk commented 6 months ago

Hello! Thanks for posting this issue! I'm not sure, seems like it should work the way you assume. I will look into it.

TolikPylypchuk commented 6 months ago

Well, I can reproduce this behavior, but I don't know why that happens, and searching for that particular behavior in Windows API didn't yield any results, though I do think it's some weird quirk of the Windows API. I will try looking into it later, but for now all I can do is just shrug.

As a workaround, you can simulate events in a different thread. For example, wrapping calls to eventSimulator in Task.Run made this work for me - but you shouldn't use Task.Run as you need to guarantee the correct sequence of simulated events, and Task.Run won't give you such a guarantee.

FaithBeam commented 6 months ago

I appreciate you looking into this. I recreated the issue with direct win32 calls using the cswin32 nupkg. Comment lines 32-42 to not send rmb clicks and properly suppress xb1:

using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;

namespace newtest;

class Program
{
    static unsafe void Main(string[] args)
    {
        var handle = PInvoke.GetModuleHandle((string?)null);
        var hhook = PInvoke.SetWindowsHookEx(
            WINDOWS_HOOK_ID.WH_MOUSE_LL,
            (code, param, lParam) =>
            {
                if (code < 0)
                {
                    return PInvoke.CallNextHookEx(null, code, param, lParam);
                }
                if (param == PInvoke.WM_XBUTTONDOWN || param == PInvoke.WM_XBUTTONUP)
                {
                    var msll = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
                    // switch on high-order word
                    switch (((ushort*)&msll.mouseData)[1])
                    {
                        case PInvoke.XBUTTON1:
                            Console.WriteLine("XB1");

                            // send rmb
                            var input = new INPUT { type = INPUT_TYPE.INPUT_MOUSE };
                            input.Anonymous.mi.dx = msll.pt.X;
                            input.Anonymous.mi.dy = msll.pt.Y;
                            input.Anonymous.mi.dwFlags =
                                MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE
                                | MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTDOWN
                                | MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTUP;
                            input.Anonymous.mi.mouseData = 0;
                            input.Anonymous.mi.dwExtraInfo = 0;
                            input.Anonymous.mi.time = 0;
                            PInvoke.SendInput(1, &input, sizeof(INPUT));

                            // suppress xb1
                            return (LRESULT)(-1);
                    }
                }
                return PInvoke.CallNextHookEx(null, code, param, lParam);
            },
            handle,
            0
        );
        while (PInvoke.GetMessage(out var lpMsg, HWND.Null, 0, 0) != -1)
        {
            PInvoke.TranslateMessage(lpMsg);
            PInvoke.DispatchMessage(lpMsg);
        }
    }
}

To be able to run that code, it needs a project with cswin32 nupkg and a NativeMethods.txt designating which functions/structs to generate. I've created that here: https://github.com/FaithBeam/win32-suppress-issue

I've tried "converting" the original mouse click by editing the code, param, and lparam values and returning CallNetHook in the lowlevelmouseproc, but that didn't work. The original mouse click went through.

TolikPylypchuk commented 6 months ago

Well, in that case I don't think I can really do anything to fix this behavior as this is a quirk of the Windows API itself. I think the best way to do what you want is to simulate events on a different thread.

I'm going to close this issue as a won't-fix, but we can continue the discussion.