How can I adjust only the vertical mouse sensitivity in Windows?
I've had a cheap Logitech M215 wireless mouse for years now. It still works perfectly.
For my laptop, I've purchased a couple additional mouses- both Logitech- expecting the same performance. The problem: the vertical sensitivity is awkward on both. Sensitivity along the x-axis is great, but sensitivity along the y-axis is slow to the point that I hate using them. (This problem repeats itself on my desktop PC.)
I know how to adjust the pointer speed in Windows, but I'm not looking to scale both axes- just the y-axis. There's apparently a way to do this in Ubuntu, and I've scoured lots of forums asking essentially the same question I'm asking now, to no avail. This question has even been asked here, but the comments weren't any help.
Does anyone know of a way to do this within Windows 8.1? I'm comfortable with making whatever registry changes/downloading whichever software might help.
Solution 1:
Here's an option that's not perfect, but it helps. It was originally provided by the user Nextron at https://autohotkey.com/board/topic/13531-adjusting-mouse-sensitivity-via-hotkey/.
This script gives you leeway to modify X and Y sensitivity independently on Windows, and it works with all mouses and all versions of windows. It's not perfect in that you must use integer multiples of sensitivity, and it operates by multiplying mouse movement pulses.
- Install autohotkey.
- Modify the below autohotkey script. Where it says
new MouseAccelerator(0, 1)
, change that tonew MouseAccelerator (<amount to multiply X sensitivity by> - 1, <amount to multiply Y sensitivity by> - 1)
. E.g.MouseAccelerator(0, 1)
doubles Y movement but does not affect X movement,MouseAccelerator(2, 1)
triples horizontal movement and doubles vertical movement, etc. -
Run the modified script. Hit F12 when you want to shut it down.
;The SendInput DllCall is specifically 32-bit. So check for the correct bitness of AutoHotkey and if not, try to run the right one. If (A_PtrSize=8){ SplitPath, A_AhkPath,,Dir Run %Dir%\AutoHotkeyU32.exe %A_ScriptFullPath% ExitApp } ;Call below to accelerate the mouse input. The first two parameters are the integer factors of artificial amplification added on top of the physical input. ;The first is for horizontal/x-axis movement, the second for vertical/y-axis movement. new MouseAccelerator(0, 1) F12::ExitApp ; Gets called when mouse moves or stops ; x and y are DELTA moves (Amount moved since last message), NOT coordinates. MouseAcceleratorEvent(x := 0, y := 0, accelerationx := 2, accelerationy := 2){ static MouseAcceleratorPaused If !(MouseAcceleratorPaused){ MouseAcceleratorPaused:=true VarSetCapacity( MouseInput, 28, 0 ) NumPut( x * accelerationx, MouseInput, 4, "Int" ) ; dx NumPut( y * accelerationy, MouseInput, 8, "Int" ) ; dy NumPut( 0x0001, MouseInput, 16, "UInt" ) ; MOUSEEVENTF_MOVE = 0x0001 DllCall("SendInput", "UInt", 1, "UInt", &MouseInput, "Int", 28 ) sleep,-1 MouseAcceleratorPaused:=false } } ; ================================== LIBRARY ======================================== ; Instantiate this class and pass it a func name or a Function Object ; The specified function will be called with the delta move for the X and Y axes ; Normally, there is no windows message "mouse stopped", so one is simulated. ; After 10ms of no mouse movement, the callback is called with 0 for X and Y ; https://autohotkey.com/boards/viewtopic.php?f=19&t=10159 Class MouseAccelerator { __New(accelerationx:=2, accelerationy:=2, callback:="MouseAcceleratorEvent"){ static DevSize := 8 + A_PtrSize static RIDEV_INPUTSINK := 0x00000100 this.TimeoutFn := this.TimeoutFunc.Bind(this) this.Callback := callback this.Accelerationx := accelerationx this.Accelerationy := accelerationy ; Register mouse for WM_INPUT messages. VarSetCapacity(RAWINPUTDEVICE, DevSize) NumPut(1, RAWINPUTDEVICE, 0, "UShort") NumPut(2, RAWINPUTDEVICE, 2, "UShort") NumPut(RIDEV_INPUTSINK, RAWINPUTDEVICE, 4, "Uint") ; WM_INPUT needs a hwnd to route to, so get the hwnd of the AHK Gui. ; It doesn't matter if the GUI is showing, it still exists Gui +hwndhwnd NumPut(hwnd, RAWINPUTDEVICE, 8, "Uint") this.RAWINPUTDEVICE := RAWINPUTDEVICE DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize ) fn := this.MouseMoved.Bind(this) OnMessage(0x00FF, fn) } __Delete(){ static RIDEV_REMOVE := 0x00000001 static DevSize := 8 + A_PtrSize RAWINPUTDEVICE := this.RAWINPUTDEVICE NumPut(RIDEV_REMOVE, RAWINPUTDEVICE, 4, "Uint") DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize ) } ; Called when the mouse moved. ; Messages tend to contain small (+/- 1) movements, and happen frequently (~20ms) MouseMoved(wParam, lParam){ ; RawInput statics static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, offsets := {x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput static axes := {x: 1, y: 2} ; Find size of rawinput data - only needs to be run the first time. if (!iSize){ r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", iSize, "UInt", 8 + (A_PtrSize * 2)) VarSetCapacity(uRawInput, iSize) } sz := iSize ; param gets overwritten with # of bytes output, so preserve iSize ; Get RawInput data r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", &uRawInput, "UInt*", sz, "UInt", 8 + (A_PtrSize * 2)) x := NumGet(&uRawInput, offsets.x, "Int") y := NumGet(&uRawInput, offsets.y, "Int") this.Callback.(x, y, this.Accelerationx, this.Accelerationy) ; There is no message for "Stopped", so simulate one fn := this.TimeoutFn SetTimer, % fn, -10 } TimeoutFunc(){ this.Callback.(0, 0) } }
Solution 2:
I have the M310 and have been dealing with the same issue. Some people have had success by cleaning the lens with compressed air. For me, though, it was actually my mouse pad... Turn your pad 90 degrees and see if there's any effect.
Solution 3:
I want to put here the solution of my similar case. I had quite an imbalance between the vertical and horizontal sensitivities.
Finally, I've unchecked the "Enhance pointer precision" in "Mouse Properties" and regained the balance.
That was quite unclear. As I spent much time and found the solution almost by luck, I'd like to pin it here.
Solution 4:
Here is some code that can both raise AND lower sensitivity, on a per-device basis (You could lower the sensitivity of one mouse, but not affect another).
It uses RawInput (Implemented in a C# DLL for speed) and SetWindowsHookEx (Implemented in pure AHK) for the blocking.
Note that I have only recently gotten this method to work, so there may well be issues and things that can be improved. I have a thread here on the AHK forums detailing my efforts - there may well be more up-to-date or feature rich versions there.
C# code
Start new class library project, add reference to SharpDX.RawInput via NuGet
DLL should be called MouseDelta.dll and needs to be in same folder.
When you build, it should spit out two SharpDX DLLs into the build folder - your script will also need these.
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());
}
}
Main AHK script (You should only need to edit this)
; ================= USER SCRIPT ================
#SingleInstance force
#NoEnv
#include CLR.ahk
#include MouseDelta.ahk
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
}
AHK Wrapper
Save as MouseDelta.ahk in same folder
; ================= 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)
;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
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)
}
CLR Library - allows AHK to interop with C# DLLs.
Save this as CLR.ahk in same folder, or in AHK Lib folder
; ==========================================================
; .NET Framework Interop
; http://www.autohotkey.com/forum/topic26191.html
; ==========================================================
;
; Author: Lexikos
; Version: 1.2
; Requires: AutoHotkey_L v1.0.96+
;
; Modified by evilC for compatibility with AHK_H as well as AHK_L
; "null" is a reserved word in AHK_H, so did search & Replace from "null" to "_null"
CLR_LoadLibrary(AssemblyName, AppDomain=0)
{
if !AppDomain
AppDomain := CLR_GetDefaultDomain()
e := ComObjError(0)
Loop 1 {
if assembly := AppDomain.Load_2(AssemblyName)
break
static _null := ComObject(13,0)
args := ComObjArray(0xC, 1), args[0] := AssemblyName
typeofAssembly := AppDomain.GetType().Assembly.GetType()
if assembly := typeofAssembly.InvokeMember_3("LoadWithPartialName", 0x158, _null, _null, args)
break
if assembly := typeofAssembly.InvokeMember_3("LoadFrom", 0x158, _null, _null, args)
break
}
ComObjError(e)
return assembly
}
CLR_CreateObject(Assembly, TypeName, Args*)
{
if !(argCount := Args.MaxIndex())
return Assembly.CreateInstance_2(TypeName, true)
vargs := ComObjArray(0xC, argCount)
Loop % argCount
vargs[A_Index-1] := Args[A_Index]
static Array_Empty := ComObjArray(0xC,0), _null := ComObject(13,0)
return Assembly.CreateInstance_3(TypeName, true, 0, _null, vargs, _null, Array_Empty)
}
CLR_CompileC#(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
return CLR_CompileAssembly(Code, References, "System", "Microsoft.CSharp.CSharpCodeProvider", AppDomain, FileName, CompilerOptions)
}
CLR_CompileVB(Code, References="", AppDomain=0, FileName="", CompilerOptions="")
{
return CLR_CompileAssembly(Code, References, "System", "Microsoft.VisualBasic.VBCodeProvider", AppDomain, FileName, CompilerOptions)
}
CLR_StartDomain(ByRef AppDomain, BaseDirectory="")
{
static _null := ComObject(13,0)
args := ComObjArray(0xC, 5), args[0] := "", args[2] := BaseDirectory, args[4] := ComObject(0xB,false)
AppDomain := CLR_GetDefaultDomain().GetType().InvokeMember_3("CreateDomain", 0x158, _null, _null, args)
return A_LastError >= 0
}
CLR_StopDomain(ByRef AppDomain)
{ ; ICorRuntimeHost::UnloadDomain
DllCall("SetLastError", "uint", hr := DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+20*A_PtrSize), "ptr", RtHst, "ptr", ComObjValue(AppDomain))), AppDomain := ""
return hr >= 0
}
; NOTE: IT IS NOT NECESSARY TO CALL THIS FUNCTION unless you need to load a specific version.
CLR_Start(Version="") ; returns ICorRuntimeHost*
{
static RtHst := 0
; The simple method gives no control over versioning, and seems to load .NET v2 even when v4 is present:
; return RtHst ? RtHst : (RtHst:=COM_CreateObject("CLRMetaData.CorRuntimeHost","{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}"), DllCall(NumGet(NumGet(RtHst+0)+40),"uint",RtHst))
if RtHst
return RtHst
EnvGet SystemRoot, SystemRoot
if Version =
Loop % SystemRoot "\Microsoft.NET\Framework" (A_PtrSize=8?"64":"") "\*", 2
if (FileExist(A_LoopFileFullPath "\mscorlib.dll") && A_LoopFileName > Version)
Version := A_LoopFileName
if DllCall("mscoree\CorBindToRuntimeEx", "wstr", Version, "ptr", 0, "uint", 0
, "ptr", CLR_GUID(CLSID_CorRuntimeHost, "{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}")
, "ptr", CLR_GUID(IID_ICorRuntimeHost, "{CB2F6722-AB3A-11D2-9C40-00C04FA30A3E}")
, "ptr*", RtHst) >= 0
DllCall(NumGet(NumGet(RtHst+0)+10*A_PtrSize), "ptr", RtHst) ; Start
return RtHst
}
;
; INTERNAL FUNCTIONS
;
CLR_GetDefaultDomain()
{
static defaultDomain := 0
if !defaultDomain
{ ; ICorRuntimeHost::GetDefaultDomain
if DllCall(NumGet(NumGet(0+RtHst:=CLR_Start())+13*A_PtrSize), "ptr", RtHst, "ptr*", p:=0) >= 0
defaultDomain := ComObject(p), ObjRelease(p)
}
return defaultDomain
}
CLR_CompileAssembly(Code, References, ProviderAssembly, ProviderType, AppDomain=0, FileName="", CompilerOptions="")
{
if !AppDomain
AppDomain := CLR_GetDefaultDomain()
if !(asmProvider := CLR_LoadLibrary(ProviderAssembly, AppDomain))
|| !(codeProvider := asmProvider.CreateInstance(ProviderType))
|| !(codeCompiler := codeProvider.CreateCompiler())
return 0
if !(asmSystem := (ProviderAssembly="System") ? asmProvider : CLR_LoadLibrary("System", AppDomain))
return 0
; Convert | delimited list of references into an array.
StringSplit, Refs, References, |, %A_Space%%A_Tab%
aRefs := ComObjArray(8, Refs0)
Loop % Refs0
aRefs[A_Index-1] := Refs%A_Index%
; Set parameters for compiler.
prms := CLR_CreateObject(asmSystem, "System.CodeDom.Compiler.CompilerParameters", aRefs)
, prms.OutputAssembly := FileName
, prms.GenerateInMemory := FileName=""
, prms.GenerateExecutable := SubStr(FileName,-3)=".exe"
, prms.CompilerOptions := CompilerOptions
, prms.IncludeDebugInformation := true
; Compile!
compilerRes := codeCompiler.CompileAssemblyFromSource(prms, Code)
if error_count := (errors := compilerRes.Errors).Count
{
error_text := ""
Loop % error_count
error_text .= ((e := errors.Item[A_Index-1]).IsWarning ? "Warning " : "Error ") . e.ErrorNumber " on line " e.Line ": " e.ErrorText "`n`n"
MsgBox, 16, Compilation Failed, %error_text%
return 0
}
; Success. Return Assembly object or path.
return compilerRes[FileName="" ? "CompiledAssembly" : "PathToAssembly"]
}
CLR_GUID(ByRef GUID, sGUID)
{
VarSetCapacity(GUID, 16, 0)
return DllCall("ole32\CLSIDFromString", "wstr", sGUID, "ptr", &GUID) >= 0 ? &GUID : ""
}