POC: RawInput mouse via C# CLR

Talk about things C#, some related to AutoHotkey
User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

POC: RawInput mouse via C# CLR

Post by evilC » 03 Jun 2017, 11:46

Here is the fruit of much endeavor - I have been trying to work out how to receive RawInput messages (Specifically, mouse movement) in C#, from within a DLL.

In order to receive RawInput messages, a thread needs to have a message handler. The simplest way to do this is to use a form, but that then means that when the flow of execution hits Application.Run(), execution halts, so your DLL constructor never returns. Eventually I worked out how to use a primitive version of a form, running in another thread, so that the code can be asynchronous.

So here it is: RawInput in AHK with the heavy lifting done in C# via the SharpDX library

FOR ALL DEMOS IN THIS THREAD:

The SharpDX DLLs from SharpDX.zip in this post are required for the demos in all posts in this thread
You will need CLR.ahk from here
For each demo, extract the SharpDX DLLs from this post, plus the MouseDelta zip for that post into the same folder

AHK code:

Code: Select all

#Persistent
#include clr.ahk

asm := CLR_LoadLibrary("MouseDelta.dll")
md := asm.CreateInstance("MouseDelta")

md.SubscribeRelativeMove(Func("MoveEvent"))
md.SubscribeWheel(Func("WheelEvent"))
return

MoveEvent(x, y){
	Tooltip % x ", " y
}

WheelEvent(value){
	Tooltip % "Wheel: " value
}
You can also build MouseDelta.dll yourself from the included C# code
The procedure is always the same:
Create a new class library project, add a reference to SharpDX.RawInput via NuGet
Paste in the C# code
Compile

Code: Select all

using System;
using System.Windows.Forms;
using SharpDX.Multimedia;
using SharpDX.RawInput;
using System.Threading;

public class MouseDelta
{
    private readonly Thread messagePump;

    public dynamic relativeMoveCallback;
    public dynamic wheelCallback;

    private AutoResetEvent messagePumpRunning = new AutoResetEvent(false);

    public MouseDelta()
    {
        // start message pump in its own thread  
        messagePump = new Thread(RunMessagePump) { Name = "ManualMessagePump" };
        messagePump.Start();
        messagePumpRunning.WaitOne();
    }

    public void SubscribeRelativeMove(dynamic callback)
    {
        relativeMoveCallback = callback;
    }

    public void SubscribeWheel(dynamic callback)
    {
        wheelCallback = callback;
    }

    // the message pump thread  
    private void RunMessagePump()
    {
        // Create control to handle windows messages   
        MessageHandler messageHandler = new MessageHandler();

        // Register for RawInput mouse messages
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericMouse, DeviceFlags.InputSink, messageHandler.Handle);
        Device.MouseInput += ProcessMouseInput;

        messagePumpRunning.Set();
        Application.Run();
    }

    private void ProcessMouseInput(object sender, MouseInputEventArgs args)
    {
        //Console.WriteLine(string.Format("(x,y):({0},{1}) Buttons: {2} State: {3} Wheel: {4}\r\n", args.X, args.Y, args.ButtonFlags, args.Mode, args.WheelDelta));
        if (args.Mode == MouseMode.MoveRelative && relativeMoveCallback != null && (Math.Abs(args.X) + Math.Abs(args.Y) > 0))
        {
            relativeMoveCallback(args.X, args.Y);
        }
        else if (args.WheelDelta != 0 && wheelCallback != null)
        {
            wheelCallback(args.WheelDelta / 120);
        }
    }
}

// Useful SO post on handling messages - code for overriding WndProc
// https://stackoverflow.com/questions/2443867/message-pump-in-net-windows-service
// Although the above code is not quite complete. This blog post has the implementation for MessageData
// http://joe-bq-wang.iteye.com/blog/1882661

// However, by overriding WndProc, we have to process all messages, and then you do not get a SharpDX object..
// ... you just appear to get a raw WM_INPUT message

// For now, this seems to serve our purposes
internal class MessageHandler : NativeWindow
{
    public MessageHandler()
    {
        CreateHandle(new CreateParams());
    }
}
Attachments
SharpDX.zip
Required for all demos in this thread
(110.77 KiB) Downloaded 664 times
MouseDelta-Basic.zip
(3.66 KiB) Downloaded 669 times
Last edited by evilC on 17 Jun 2017, 15:07, edited 5 times in total.

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 03 Jun 2017, 12:25

Interesting :think:
I will probably return with questions and or comments :D
Thanks for sharing.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 04 Jun 2017, 06:41

[Edit: Updated to make AHK script not receive any callbacks when a non-selected mouse moves]
I made a version that allows per-mouse filtering.
At the moment, it just uses the VID/PID string, but at least this is better than the Handle string of the previous incarnation.
I think I remember someone saying they came up with a way to uniquely identify two instances of the same device - we could maybe bolt that on.
I did not bother with trying to display mouse names - I have the code, but for my main mouse it just pulled out "Logitech Receiver", and for another of my mice without a driver, it got nothing, so it seems pretty pointless.
For simplicity of use, I wrote an AHK wrapper that allows you to inject a DDL into your script which you can use to select which mouse to take input from.

Image

Code: Select all

; ================= USER SCRIPT ================
#SingleInstance force
#NoEnv
#include <CLR>

Gui, Add, Text, , Select Mouse:
mdw := new MouseDeltaWrapper("x+5 yp-3 w200")
mdw.SubscribeMove(Func("MoveEvent"))
mdw.SubscribeWheel(Func("WheelEvent"))
Gui, Show
return

GuiClose:
	ExitApp

MoveEvent(x, y, mouseId){
	ToolTip % "Move: " x ", " y ", ID: " mouseId
}

WheelEvent(value, mouseId){
	ToolTip % "Wheel: " value ", ID: " mouseId
}

; ================= WRAPPER LIBRARY ================
class MouseDeltaWrapper {
	SeenMice := {}
	SelectedMouse := 0
	MoveCallback := 0

	__New(guiOptions := "", dllPath := "MouseDelta.dll"){
		this.Callback := callback
		
		Gui, +HwndHwnd
		this.GuiHwnd := Hwnd
		
		Gui, Add, DDL, % "hwndhDDL " guiOptions, Any||
		this.hDDL := hDDL
		
		fn := this._UserSelectedMouse.Bind(this)
		GuiControl, +g, % this.hDDL, % fn
		
		asm := CLR_LoadLibrary(dllPath)
		md := asm.CreateInstance("MouseDelta")

		md.SubscribeRelativeMove(this._MoveEvent.Bind(this))
		md.SubscribeWheel(this._WheelEvent.Bind(this))
		this.md := md
		
		this._UserSelectedMouse()
	}
	
	SubscribeMove(callback){
		this.MoveCallback := callback
	}
	
	SubscribeWheel(callback){
		this.WheelCallback := callback
	}
	
	_UserSelectedMouse(){
		GuiControlGet, mouseId, , % this.hDDL
		this.SelectedMouse := mouseId == "Any" ? 0 : mouseId
		if (this.MoveCallback != 0)
			this.md.SubscribeRelativeMove(this._MoveEvent.Bind(this), this.SelectedMouse)
		if (this.WheelCallback != 0)
			this.md.SubscribeWheel(this._WheelEvent.Bind(this), this.SelectedMouse)
	}
	
	_AddMouseToDDL(mouseId){
		GuiControl, , % this.hDDL, % mouseId
	}
	
	_UpdateMice(mouseId){
		if (!this.SeenMice.HasKey(mouseId)){
			this.SeenMice[mouseId] := 1
			this._AddMouseToDDL(mouseId)
		}
	}
	
	_MoveEvent(x, y, mouseId){
		this._UpdateMice(mouseId)
		if (this.MoveCallback != 0 && (this.SelectedMouse == 0 || this.SelectedMouse == mouseId)){
			this.MoveCallback.Call(x, y, mouseId)
		}
	}
	
	_WheelEvent(value, mouseId){
		this._UpdateMice(mouseId)
		if (this.WheelCallback != 0 && (this.SelectedMouse == 0 || this.SelectedMouse == mouseId)){
			this.WheelCallback.Call(value, mouseId)
		}
	}
}
C# code, same deal as before.

Code: Select all

using System;
using System.Windows.Forms;
using SharpDX.Multimedia;
using SharpDX.RawInput;
using System.Threading;
using System.Collections.Generic;

public class MouseDelta
{
    private readonly Thread messagePump;

    public dynamic relativeMoveCallback;
    public dynamic wheelCallback;

    static private Dictionary<IntPtr, string> seenMice = new Dictionary<IntPtr, string>();
    static private string subscribedMouse = null;

    private AutoResetEvent messagePumpRunning = new AutoResetEvent(false);

    public MouseDelta()
    {
        // start message pump in its own thread  
        messagePump = new Thread(RunMessagePump) { Name = "ManualMessagePump" };
        messagePump.Start();
        messagePumpRunning.WaitOne();
    }

    public void SubscribeRelativeMove(dynamic callback, string mouseId = null)
    {
        SetSubscribedMouse(mouseId);
        relativeMoveCallback = callback;
    }

    public void SubscribeWheel(dynamic callback, string mouseId = null)
    {
        SetSubscribedMouse(mouseId);
        wheelCallback = callback;
    }

    private void SetSubscribedMouse(string mouseId)
    {
        if (mouseId != null)
        {
            subscribedMouse = mouseId == "0" ? null : mouseId;
        }
    }

    // the message pump thread  
    private void RunMessagePump()
    {
        // Create control to handle windows messages   
        MessageHandler messageHandler = new MessageHandler();

        // Register for RawInput mouse messages
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericMouse, DeviceFlags.InputSink, messageHandler.Handle);
        Device.MouseInput += ProcessMouseInput;

        messagePumpRunning.Set();
        Application.Run();
    }

    private void ProcessMouseInput(object sender, MouseInputEventArgs args)
    {
        //Console.WriteLine(string.Format("(x,y):({0},{1}) Buttons: {2} State: {3} Wheel: {4}\r\n", args.X, args.Y, args.ButtonFlags, args.Mode, args.WheelDelta));
        // Handle mouse filtering
        if (!seenMice.ContainsKey(args.Device))
        {
            DeviceInfo info = null;
            var devices = Device.GetDevices();
            foreach (var dev in devices)
            {
                if (dev.Handle == args.Device)
                {
                    info = dev;
                    break;
                }
            }
            if (info == null)
                return;
            string item = info.DeviceName;
            item = item.Substring(4);

            string[] split = item.Split('#');

            //string id_01 = split[0];    // ACPI (Class code)
            string id_02 = split[1];    // PNP0303 (SubClass code)
                                        //string id_03 = split[2];    // 3&13c0b0c5&0 (Protocol code)

            seenMice.Add(args.Device, id_02);
        }

        if (subscribedMouse != null && subscribedMouse != seenMice[args.Device])
        {
            return;
        }
        
        // Fire appropriate Callback
        if (args.Mode == MouseMode.MoveRelative && relativeMoveCallback != null && (Math.Abs(args.X) + Math.Abs(args.Y) > 0))
        {
            relativeMoveCallback(args.X, args.Y, seenMice[args.Device]);
        }
        else if (args.WheelDelta != 0 && wheelCallback != null)
        {
            wheelCallback(args.WheelDelta / 120, seenMice[args.Device]);
        }
    }
}

// Useful SO post on handling messages - code for overriding WndProc
// https://stackoverflow.com/questions/2443867/message-pump-in-net-windows-service
// Although the above code is not quite complete. This blog post has the implementation for MessageData
// http://joe-bq-wang.iteye.com/blog/1882661

// However, by overriding WndProc, we have to process all messages, and then you do not get a SharpDX object..
// ... you just appear to get a raw WM_INPUT message

// For now, this seems to serve our purposes
internal class MessageHandler : NativeWindow
{
    public MessageHandler()
    {
        CreateHandle(new CreateParams());
    }
}
Attachments
MouseDelta-MultiMouse.zip
(4.91 KiB) Downloaded 545 times
Last edited by evilC on 19 Jul 2017, 13:50, edited 4 times in total.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 04 Jun 2017, 10:11

Another version.
This one allows you to subscribe to mouse "Stop" events (Windows doesn't really have one).
When the stop event callback fires, you are passed the sum of the last 10 moves, plus the last move (x and y for both, so 4 params)

This is all the info that my RollMouse app uses, so I wrote a version of it using this lib + MicroTimer.

Move the mouse and pick it up while in motion, and it will keep moving in the direction and at roughly the same speed until you put the mouse back on the mat. Also works with trackpads.

AHK code:

Code: Select all

#SingleInstance force
#NoEnv
#Persistent
#include <CLR>

rm := new RollMouse()
return

class RollMouse {
	Rolling := 0
	MoveAmount := {x: 0, y: 0}
	
	__New(MouseDeltaDllPath := "MouseDelta.dll", MicroTimerDllPath := "MicroTimer.dll"){
		asm := CLR_LoadLibrary(MouseDeltaDllPath)
		md := asm.CreateInstance("MouseDelta")
		this.md := md
		
		asm := CLR_LoadLibrary(MicroTimerDllPath)
		mt := asm.CreateInstance("MicroTimer")
		this.mt := mt
		
		this.MoveTimer := mt.Create(this._DoMove.Bind(this), 2)
		
		md.SubscribeRelativeMove(this._MoveEvent.Bind(this))
		md.SubscribeRelativeMoveStop(this._StopEvent.Bind(this))
	}
	
	_MoveEvent(x, y, mouseId){
		if (this.Rolling){
			; Normal movement detected while we are rolling - stop the roll
			this.Rolling := 0
			this.MoveTimer.SetState(0)
		}
	}
	
	_StopEvent(tx, ty, lx, ly){
		; If we stop the mouse on the mat, inevitably the last movements will be low numbers
		; So if lastx or lasty is above a certain amount, we lifted while in motion
		if (abs(lx) > 3 || abs(ly) > 3){
			; Calculate the movement amount from the average of the total, rather than lastx or lasty...
			; ... so we get a more faithful representation of the general direction and magnitude of the gesture
			this.MoveAmount.x := (tx / 10)
			this.MoveAmount.y := (ty / 10)
			this.Rolling := 1
			this.MoveTimer.SetState(1)
		}
	}
	
	_DoMove(){
		DllCall("mouse_event", uint, 1, int, this.MoveAmount.x, int, this.MoveAmount.y, uint, 0, int, 0)
	}
}
C# (You will also need the MicroTimer DLL, included in attached ZIP)

Code: Select all

using System;
using System.Windows.Forms;
using SharpDX.Multimedia;
using SharpDX.RawInput;
using System.Threading;
using System.Collections.Generic;
using System.Diagnostics;
using System.Timers;
using System.Threading.Tasks;

public class MouseDelta
{
    private MoveReport deltaMoveTotal = new MoveReport  { x = 0, y = 0 };
    private MoveReport lastDeltaMove = new MoveReport  { x = 0, y = 0 };
    private List<MoveReport> deltaMoveHistory = new List<MoveReport>();
    private readonly Thread messagePump;
    //private readonly Thread stopTimerThread;
    private System.Timers.Timer stopTimer = null;

    public dynamic relativeMoveCallback;
    public dynamic relativeMoveStopCallback;
    public dynamic wheelCallback;

    static private Dictionary<IntPtr, string> seenMice = new Dictionary<IntPtr, string>();
    static private string subscribedMouse = null;

    private AutoResetEvent messagePumpRunning = new AutoResetEvent(false);

    class MoveReport
    {
        public int x { get; set; }
        public int y { get; set; }
    }

    public MouseDelta()
    {
        
        // start message pump in its own thread  
        messagePump = new Thread(RunMessagePump) { Name = "ManualMessagePump" };
        messagePump.Start();
        messagePumpRunning.WaitOne();
    }

    public void SubscribeRelativeMove(dynamic callback, string mouseId = null)
    {
        SetSubscribedMouse(mouseId);
        relativeMoveCallback = callback;
    }

    public void SubscribeRelativeMoveStop(dynamic callback, string mouseId = null)
    {
        SetSubscribedMouse(mouseId);
        relativeMoveStopCallback = callback;
    }

    public void SubscribeWheel(dynamic callback, string mouseId = null)
    {
        SetSubscribedMouse(mouseId);
        wheelCallback = callback;
    }

    private void SetSubscribedMouse(string mouseId)
    {
        if (mouseId != null)
        {
            subscribedMouse = mouseId == "0" ? null : mouseId;
        }
    }

    // the message pump thread  
    private void RunMessagePump()
    {
        // Create control to handle windows messages   
        MessageHandler messageHandler = new MessageHandler();

        // Register for RawInput mouse messages
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericMouse, DeviceFlags.InputSink, messageHandler.Handle);
        Device.MouseInput += ProcessMouseInput;

        messagePumpRunning.Set();
        Application.Run();
    }

    private void ProcessMouseInput(object sender, MouseInputEventArgs args)
    {
        //Console.WriteLine(string.Format("(x,y):({0},{1}) Buttons: {2} State: {3} Wheel: {4}\r\n", args.X, args.Y, args.ButtonFlags, args.Mode, args.WheelDelta));
        // Handle mouse filtering
        if (!seenMice.ContainsKey(args.Device))
        {
            DeviceInfo info = null;
            var devices = Device.GetDevices();
            foreach (var dev in devices)
            {
                if (dev.Handle == args.Device)
                {
                    info = dev;
                    break;
                }
            }
            if (info == null)
                return;
            string item = info.DeviceName;
            item = item.Substring(4);

            string[] split = item.Split('#');

            //string id_01 = split[0];    // ACPI (Class code)
            string id_02 = split[1];    // PNP0303 (SubClass code)
                                        //string id_03 = split[2];    // 3&13c0b0c5&0 (Protocol code)

            seenMice.Add(args.Device, id_02);
        }

        if (subscribedMouse != null && subscribedMouse != seenMice[args.Device])
        {
            return;
        }
        
        // Fire appropriate Callback
        if (args.Mode == MouseMode.MoveRelative && relativeMoveCallback != null && (Math.Abs(args.X) + Math.Abs(args.Y) > 0))
        {
            relativeMoveCallback(args.X, args.Y, seenMice[args.Device]);
            if (relativeMoveStopCallback != null)
            {
                lastDeltaMove = new MoveReport() { x = args.X, y = args.Y };
                deltaMoveHistory.Add(lastDeltaMove);
                while (deltaMoveHistory.Count > 10)
                {
                    deltaMoveHistory.RemoveAt(0);
                }
                deltaMoveTotal.x = 0;
                deltaMoveTotal.y = 0;
                for (var i = 0; i < deltaMoveHistory.Count; i++)
                {
                    deltaMoveTotal.x += deltaMoveHistory[i].x;
                    deltaMoveTotal.y += deltaMoveHistory[i].y;
                }
                SetStopTimer();
            }
        }
        else if (args.WheelDelta != 0 && wheelCallback != null)
        {
            wheelCallback(args.WheelDelta / 120, seenMice[args.Device]);
        }
    }

    private void SetStopTimer()
    {
        if (stopTimer == null)
        {
            stopTimer = new System.Timers.Timer(100);
            stopTimer.AutoReset = false;
            stopTimer.Elapsed += OnTimedEvent;
            stopTimer.Start();
        }
        else
        {
            stopTimer.Stop();
            stopTimer.Start();
        }

    }

    private void OnTimedEvent(Object source, ElapsedEventArgs e)
    {
        relativeMoveStopCallback(deltaMoveTotal.x, deltaMoveTotal.y, lastDeltaMove.x, lastDeltaMove.y);
        //Debug.WriteLine("evilC| Stop");
        stopTimer = null;
    }

}

// Useful SO post on handling messages - code for overriding WndProc
// https://stackoverflow.com/questions/2443867/message-pump-in-net-windows-service
// Although the above code is not quite complete. This blog post has the implementation for MessageData
// http://joe-bq-wang.iteye.com/blog/1882661

// However, by overriding WndProc, we have to process all messages, and then you do not get a SharpDX object..
// ... you just appear to get a raw WM_INPUT message

// For now, this seems to serve our purposes
internal class MessageHandler : NativeWindow
{
    public MessageHandler()
    {
        CreateHandle(new CreateParams());
    }
}
Attachments
MouseDelta-Stop.zip
(9.8 KiB) Downloaded 523 times
Last edited by evilC on 06 Jun 2017, 14:30, edited 1 time in total.

User avatar
runie
Posts: 304
Joined: 03 May 2014, 14:50
Contact:

Re: POC: RawInput mouse via C# CLR

Post by runie » 05 Jun 2017, 01:57

What's the advantage of this over a normal mouse hook?

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 05 Jun 2017, 02:18

See MouseDelta and / or Raw Input.

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 05 Jun 2017, 02:46

@ evilC
I couldn't get this to work. :cry:

Code: Select all

#Persistent
SetWorkingDir, % A_ScriptDir
; I have CLR.ahk in my user lib.
asm := CLR_LoadLibrary("MouseDelta.dll")
MsgBox, % IsObject(asm)							 	; 1
md := asm.CreateInstance("MouseDelta")				; Chrash
I tried with MouseDelta.dll from your first and last post. Tried on Ahk 1.1.25.02 64/32 Unicode. I'm on Windows 7.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 05 Jun 2017, 04:23

Run1e wrote:What's the advantage of this over a normal mouse hook?
Mouse Hooks (SetWindowsHookEx API) CAN'T distinguish between different mice but CAN block input.

RawInput (What this uses) CAN distinguish between different mice but CAN'T block input.

However, with Nefarius' HidGuardian on the horizon, we can potentially stop certain apps from being able to see certain devices, so we potentially do not need hooks to be able to block input (do a hard remap).

The main point of this code is that because mouse input happens so stupidly fast, I am trying to move as much as possible out of interpreted AHK code and into compiled C# code, allowing people to write mouse scripts in AHK that have as minimal of an impact on CPU as possible.
Last edited by evilC on 05 Jun 2017, 04:55, edited 1 time in total.


Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 06 Jun 2017, 04:09

Unfortunately, the script silently closes on md := asm.CreateInstance("MouseDelta"). I added the sharpDX... files to the script directory.


Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 06 Jun 2017, 05:33

That works :) Also, the scripts which failed before works with the MouseDelta.dll from Test.zip.


Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 06 Jun 2017, 06:42

I tried (and failed) with the dlls from this post and the first post.
My brief testing tells me this is an improvement w.r.t to accuracy and cpu usage, compared to your ahk MouseDelta. :thumbup:
Although this is POC, for your future reference, be aware that each rawinput can contain any combination of events, i.e., both movements, clicks and wheel scrolls, hence

Code: Select all

// Fire appropriate Callback
        if (args.Mode == MouseMode.MoveRelative && relativeMoveCallback != null && (Math.Abs(args.X) + Math.Abs(args.Y) > 0))
        {
            relativeMoveCallback(args.X, args.Y, seenMice[args.Device]);
        }
        else if (args.WheelDelta != 0 && wheelCallback != null)
        {
            wheelCallback(args.WheelDelta / 120, seenMice[args.Device]);
        }
might need to be something like

Code: Select all

// Fire appropriate Callback
        if (args.Mode == MouseMode.MoveRelative && relativeMoveCallback != null && (Math.Abs(args.X) + Math.Abs(args.Y) > 0))
        {
            relativeMoveCallback(args.X, args.Y, seenMice[args.Device]);
        }
      	if (args.WheelDelta != 0 && wheelCallback != null)									// removed else 
        {
            wheelCallback(args.WheelDelta / 120, seenMice[args.Device]);
        }
or you will miss the wheel event if it occurs at the same time as a movement.

Cheers.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 06 Jun 2017, 06:57

Yeah, I was wondering that...

When I get home tonight, I will try to make sure the zips in each post contain all the relevant files and test that they all work

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 06 Jun 2017, 14:32

OK, it seems like SharpDX got updated on 30th may, some of the code seems to have been targetting the old SharpDX version (4.0.0.0), so that explains why some were not working.
I updated each MouseDelta.dll to target the newest SharpDX version (4.0.1.0), and all seems fine now.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 12 Jun 2017, 09:12

This demo uses extends the "MultiMouse" example to demo selectively blocking input from one mouse, while letting input through from another mouse.
Yep, that's right - we thought it could not be done, but here it is:

Code: Select all

; ================= USER SCRIPT ================
#SingleInstance force
#NoEnv
#include <CLR>
OnExit, UnhookAndClose

GoSub, Hook

Gui, Add, Text, , Select Mouse:
mdw := new MouseDeltaWrapper("x+5 yp-3 w200")
mdw.SubscribeMove(Func("MoveEvent"))
mdw.SubscribeWheel(Func("WheelEvent"))
Gui, Show
return

^Esc::
UnhookAndClose:
GuiClose:
	GoSub, UnHook
	ExitApp

Hook:
	hHookMouse := SetWindowsHookEx(WH_MOUSE_LL	:= 14, RegisterCallback("MouseMove", "Fast"))
	return

UnHook:
	UnhookWindowsHookEx(hHookMouse)
	return

MoveEvent(x, y, mouseId){
	Global mdw
	if (mdw.SelectedMouse == 0 || mdw.SelectedMouse == mouseId){
		DllCall("mouse_event",uint,1,int, x ,int, y,uint,0,int,0)
	}
}

WheelEvent(value, mouseId){
	ToolTip % "Wheel: " value ", ID: " mouseId
}

; ================= WRAPPER LIBRARY ================
class MouseDeltaWrapper {
	SeenMice := {}
	SelectedMouse := 0
	MoveCallback := 0

	__New(guiOptions := "", dllPath := "MouseDelta.dll"){
		this.Callback := callback
		
		Gui, +HwndHwnd
		this.GuiHwnd := Hwnd
		
		Gui, Add, DDL, % "hwndhDDL " guiOptions, Any||
		this.hDDL := hDDL
		
		fn := this._UserSelectedMouse.Bind(this)
		GuiControl, +g, % this.hDDL, % fn
		
		asm := CLR_LoadLibrary(dllPath)
		md := asm.CreateInstance("MouseDelta")

		md.SubscribeRelativeMove(this._MoveEvent.Bind(this))
		md.SubscribeWheel(this._WheelEvent.Bind(this))
		this.md := md
		
		this._UserSelectedMouse()
	}
	
	SubscribeMove(callback){
		this.MoveCallback := callback
	}
	
	SubscribeWheel(callback){
		this.WheelCallback := callback
	}
	
	_UserSelectedMouse(){
		GuiControlGet, mouseId, , % this.hDDL
		this.SelectedMouse := mouseId == "Any" ? 0 : mouseId
		if (this.MoveCallback != 0)
			this.md.SubscribeRelativeMove(this._MoveEvent.Bind(this), this.SelectedMouse)
		if (this.WheelCallback != 0)
			this.md.SubscribeWheel(this._WheelEvent.Bind(this), this.SelectedMouse)
	}
	
	_AddMouseToDDL(mouseId){
		GuiControl, , % this.hDDL, % mouseId
	}
	
	_UpdateMice(mouseId){
		if (!this.SeenMice.HasKey(mouseId)){
			this.SeenMice[mouseId] := 1
			this._AddMouseToDDL(mouseId)
		}
	}
	
	_MoveEvent(x, y, mouseId){
		this._UpdateMice(mouseId)
		if (this.MoveCallback != 0 && (this.SelectedMouse == 0 || this.SelectedMouse == mouseId)){
			this.MoveCallback.Call(x, y, mouseId)
		}
	}
	
	_WheelEvent(value, mouseId){
		this._UpdateMice(mouseId)
		if (this.WheelCallback != 0 && (this.SelectedMouse == 0 || this.SelectedMouse == mouseId)){
			this.WheelCallback.Call(value, mouseId)
		}
	}
}

MouseMove(nCode, wParam, lParam)
{
	Critical
	SetFormat, Integer, D
	If !nCode && (wParam = 0x200){
		; Mouse movement - process
		if (NumGet(lParam+0, 12, "int")){
			; if the LLMHF_INJECTED flag is set, this is "injected" input (Came from mouse_event)
			; Let this input through
			Return CallNextHookEx(nCode, wParam, lParam)
		} else {
			; Block the input
			Return 1
		}
	} else {
		; Other mouse message - let through
		Return CallNextHookEx(nCode, wParam, lParam)
	}
}

SetWindowsHookEx(idHook, pfn)
{
	;Return DllCall("SetWindowsHookEx", "int", idHook, "Uint", pfn, "Uint", DllCall("GetModuleHandle", "Uint", 0), "Uint", 0)
	return DllCall("SetWindowsHookEx", "int", idHook, "Uint", pfn, "Uint", 0, "Uint", 0)
}

UnhookWindowsHookEx(hHook)
{
	Return DllCall("UnhookWindowsHookEx", "Uint", hHook)
}

CallNextHookEx(nCode, wParam, lParam, hHook = 0)
{
	Return DllCall("CallNextHookEx", "Uint", hHook, "int", nCode, "Uint", wParam, "Uint", lParam)
}
How it works:
In reality, ALL input is blocked, but MoveEvent still fires for all input and is passed the VID/PID of the mouse that moved.
In MoveEvent, if the input came from the selected mouse, it resends synthetic input using the mouse_event API call
The hook checks for the presence of the LLMHF_INJECTED flag, which is set to 1 if the input came from a mouse_event API call, and lets it through.
Last edited by evilC on 17 Jun 2017, 15:19, edited 1 time in total.

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 12 Jun 2017, 15:37

I get an error.

Code: Select all

Error:  0x8002000E - Invalid number of parameters.

Specifically: SubscribeWheel

	Line#
	078: }
	080: {
	081: GuiControlGet,mouseId,,this.hDDL
	082: this.SelectedMouse := mouseId == "Any" ? 0 : mouseId  
	083: if (this.MoveCallback != 0)  
	084: this.md.SubscribeRelativeMove(this._MoveEvent.Bind(this), this.SelectedMouse)  
	085: if (this.WheelCallback != 0)  
--->	086: this.md.SubscribeWheel(this._WheelEvent.Bind(this), this.SelectedMouse)  
	087: }
	089: {
	090: GuiControl,,this.hDDL,mouseId
	091: }
	093: {
	094: if (!this.SeenMice.HasKey(mouseId))  
	094: {
Same problem occurs if I uncomment the wheel subscriptions, but then for the SubscribeRelativeMove.
I do something similar with the cursor plugin, but I do not install my own hook, instead I use (ahk's hook) blockinput, mousemove and MouseButtons::return, then I do stuff (move and click) depending on which mouse is moved, detected via rawinput (your mousedelta).

Cheers.

Helgef
Posts: 4709
Joined: 17 Jul 2016, 01:02
Contact:

Re: POC: RawInput mouse via C# CLR

Post by Helgef » 13 Jun 2017, 02:56

I figured out what I was doing wrong, I didn't use the MouseDelta-Multi mouse version :oops:. Now, it works nice and smooth with the alt version :thumbup:.
However, the hook didn't initialise, so I changed,

Code: Select all

DllCall("SetWindowsHookEx", "int", idHook, "Uint", pfn, "Uint", DllCall("GetModuleHandle", "Uint", 0), "Uint", 0)
to

Code: Select all

DllCall("SetWindowsHookEx", "int", idHook, "Uint", pfn, "Uint", 0, "Uint", 0)
SetWindowsHookEx wrote: The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process.
source- My bold
And finally, when I close the gui, i.e., quit the script, it crashes.

User avatar
evilC
Posts: 4822
Joined: 27 Feb 2014, 12:30

Re: POC: RawInput mouse via C# CLR

Post by evilC » 18 Jun 2017, 08:24

Something still seems to be going on with the files in this thread - I am about to remote into a user's PC who is having problems to see if I can get to the bottom of it.

Post Reply

Return to “C#”