Last active
May 6, 2023 06:18
-
-
Save iso2022jp/df6a36d36568fa4ca831eb01a12c5436 to your computer and use it in GitHub Desktop.
Mouse wheel back-flow canceller for Microsoft Pro IntelliMouse
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Drawing; | |
using System.Runtime.InteropServices; | |
using System.Threading; | |
using System.Windows.Forms; | |
// [main: STAThread] | |
Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); | |
Thread.CurrentThread.SetApartmentState(ApartmentState.STA); | |
// To customize application configuration such as set high DPI settings or default font, | |
// see https://aka.ms/applicationconfiguration. | |
ApplicationConfiguration.Initialize(); | |
using var canceller = new WheelBackflowCanceller(); | |
using var context = new TaskTrayApplicationContext(); | |
Application.Run(context); | |
sealed class TaskTrayApplicationContext : ApplicationContext | |
{ | |
private readonly NotifyIcon trayIcon; | |
public TaskTrayApplicationContext() | |
{ | |
trayIcon = new NotifyIcon() | |
{ | |
Icon = SystemIcons.Application, | |
ContextMenuStrip = new ContextMenuStrip(), | |
Text = Application.ProductName, | |
Visible = true, | |
}; | |
_ = trayIcon.ContextMenuStrip.Items.Add("Exit", null, Exit); | |
trayIcon.DoubleClick += Exit; | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
if (disposing) | |
{ | |
trayIcon.Dispose(); | |
} | |
base.Dispose(disposing); | |
} | |
private void Exit(object? sender, EventArgs e) | |
{ | |
ExitThread(); | |
} | |
} | |
sealed partial class WheelBackflowCanceller : IDisposable | |
{ | |
private readonly SafeHookHandle hookHandle; | |
private readonly TimeSpan timeout; | |
private readonly int maxDelta; | |
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromMilliseconds(400); | |
public static readonly int DefaultMaxDelta = 360; | |
public WheelBackflowCanceller() : this(DefaultTimeout, DefaultMaxDelta) | |
{ | |
} | |
public WheelBackflowCanceller(TimeSpan timeout, int maxDelta) | |
{ | |
this.timeout = timeout; | |
this.maxDelta = maxDelta; | |
Debug.WriteLine("SetWindowsHookEx"); | |
hookHandle = NativeMethods.SetWindowsHookEx(WH_MOUSE_LL, HandleLowLevelMouseMessage, IntPtr.Zero, 0); | |
} | |
public void Dispose() | |
{ | |
hookHandle.Dispose(); | |
} | |
private const int WH_MOUSE_LL = 14; | |
private const int WM_MOUSEWHEEL = 0x020A; | |
private const int HC_ACTION = 0; | |
private int detectedDirection; | |
private uint startTime; | |
private int totalDelta; | |
private DateTime monitorUntil = DateTime.MinValue; | |
private enum FilterAction { | |
Pass = 0, | |
Suppress = 1, | |
} | |
private FilterAction Rectify(ref NativeTypes.MSLLHOOKSTRUCT hook) | |
{ | |
var now = DateTime.Now; | |
var willActivate = monitorUntil <= now; | |
var delta = hook.mouseData.high; | |
// 連続操作はタイムアウトを伸ばす | |
monitorUntil = now + timeout; | |
// 生成されたイベントはそのまま通す | |
if (hook.flags.HasFlag(NativeTypes.LLMHF.Injected)) | |
{ | |
Debug.WriteLine($"I: {delta,5:+0;-0; 0}Δ: Injected message detected, pass through."); | |
return FilterAction.Pass; | |
} | |
var direction = Math.Sign(delta); | |
var message = ""; | |
// 大きすぎる移動を抑制 | |
if (Math.Abs(delta) > maxDelta) | |
{ | |
message = $" Large delta {delta,5:+0;-0; 0}Δ reduced."; | |
delta = (short)(maxDelta * direction); | |
hook.mouseData.high = delta; | |
} | |
// 監視開始 | |
if (willActivate) | |
{ | |
detectedDirection = direction; | |
startTime = hook.time; | |
totalDelta = hook.mouseData.high; | |
Debug.WriteLine(""); | |
Debug.WriteLine($"A: {0,4:+0} ms {delta,5:+0;-0; 0}Δ: Wheel activated.{message}"); | |
return FilterAction.Pass; | |
} | |
var time = hook.time - startTime; | |
totalDelta += hook.mouseData.high; | |
var totalDirection = Math.Sign(totalDelta); | |
if (totalDirection != 0 && detectedDirection != totalDirection) | |
{ | |
// 最初とは逆方向の移動が多いならそちらに切り替える | |
detectedDirection = totalDirection; | |
message += " Change direction."; | |
} | |
if (direction != detectedDirection) | |
{ | |
// 逆方向は抑制する | |
Debug.WriteLine($"S: {time,4:+0} ms {delta,5:+0;-0; 0}Δ: Back-flow detected, suppress this message.{message}"); | |
return FilterAction.Suppress; | |
} | |
else | |
{ | |
// 順方向は通す | |
Debug.WriteLine($"P: {time,4:+0} ms {delta,5:+0;-0; 0}Δ: Normal-flow detected, pass through.{message}"); | |
return FilterAction.Pass; | |
} | |
} | |
private IntPtr HandleLowLevelMouseMessage(int nCode, IntPtr wParam, IntPtr lParam) | |
{ | |
if (nCode < HC_ACTION || wParam.ToInt32() != WM_MOUSEWHEEL) | |
{ | |
return NativeMethods.CallNextHookEx(hookHandle, nCode, wParam, lParam); | |
} | |
var hook = Marshal.PtrToStructure<NativeTypes.MSLLHOOKSTRUCT>(lParam); | |
return Rectify(ref hook) switch | |
{ | |
FilterAction.Suppress => 1, | |
_ => NativeMethods.CallNextHookEx(hookHandle, nCode, wParam, lParam), | |
}; | |
} | |
} | |
sealed partial class SafeHookHandle : SafeHandle | |
{ | |
public SafeHookHandle() : base(IntPtr.Zero, true) | |
{ | |
} | |
public override bool IsInvalid => handle == IntPtr.Zero; | |
protected override bool ReleaseHandle() | |
{ | |
Debug.WriteLine("UnhookWindowsHookEx"); | |
return NativeMethods.UnhookWindowsHookEx(handle); | |
} | |
} | |
static class NativeTypes | |
{ | |
[Flags] | |
internal enum LLMHF : uint | |
{ | |
Injected = 1, | |
// LowerILInjected = 2, | |
} | |
#pragma warning disable CS0649 | |
internal struct MOUSEDATA | |
{ | |
public ushort low; | |
public short high; | |
} | |
internal struct MSLLHOOKSTRUCT | |
{ | |
public POINT pt; | |
public MOUSEDATA mouseData; // uint | |
public LLMHF flags; | |
public uint time; | |
public IntPtr dwExtraInfo; | |
} | |
internal struct POINT | |
{ | |
public int x; | |
public int y; | |
} | |
#pragma warning restore CS0649 | |
} | |
static partial class NativeMethods | |
{ | |
internal delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam); | |
[LibraryImport("user32", EntryPoint = "SetWindowsHookExW", SetLastError = true)] | |
internal static partial SafeHookHandle SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId); | |
[LibraryImport("user32", SetLastError = true)] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
internal static partial bool UnhookWindowsHookEx(IntPtr hhk); | |
[LibraryImport("user32", SetLastError = true)] | |
internal static partial IntPtr CallNextHookEx(SafeHookHandle hhk, int nCode, IntPtr wParam, IntPtr lParam); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>WinExe</OutputType> | |
<TargetFramework>net7.0-windows</TargetFramework> | |
<Nullable>enable</Nullable> | |
<UseWindowsForms>true</UseWindowsForms> | |
<ApplicationHighDpiMode>DpiUnaware</ApplicationHighDpiMode> | |
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> | |
</PropertyGroup> | |
</Project> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment