Skip to content

Instantly share code, notes, and snippets.

@iso2022jp
Last active May 6, 2023 06:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iso2022jp/df6a36d36568fa4ca831eb01a12c5436 to your computer and use it in GitHub Desktop.
Save iso2022jp/df6a36d36568fa4ca831eb01a12c5436 to your computer and use it in GitHub Desktop.
Mouse wheel back-flow canceller for Microsoft Pro IntelliMouse
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);
}
<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