If you read my article on using Autohotkey for radiology, I describe that I use a click-lock script to simulate holding down the left mouse button. This allows me to power-scroll by using a single keystroke (in my case, backslash) to toggle scrolling on/off instead of needing to hold the mouse in a death grip for hours a day (which is a great way to destroy your wrist):
;toggle holding down the left mouse button
\::
alt := not alt
if (alt)
{
Click Down
}
else
{
Click Up
}
Return
If you also happened to read my post on radiology equipment or the follow-up deeper dive on how I use the Contour Shuttle for radiology, you may also know that I really enjoy autoscrolling with the Shuttle’s outer dial: When I twist the dial, each “hour” on the clockface repeats the mouse scroll wheel multiple times a second to allow me to scroll to varying speeds without needing to move at all. It takes some getting used to, but it’s awesome.
Not everyone has the Shuttle or the ability to install software on hospital computers, so I was thinking about how to recreate that without the wheel.
The following script was made—with a surprising amount of back and forth—using ChatGPT (I just kept telling it what errors it was getting and it eventually figured it out). I include it here as a potentially helpful tool but mostly to inspire you to play with making your own things to solve your own needs. The LLMs available for free online now make this sort of thing comically easy related to even just a couple of years ago.
The way this example works is by combining Alt + any number key (1-9) to scroll up and Ctrl + 1-9 to scroll down. The higher the number you press, the faster you scroll. As in, Alt+1 scrolls slowly and Alt+9 scrolls quickly. The reality is that anyone using some variant of this would almost certainly want to change the hotkeys used on an actual keyboard (perhaps using ZXC and ASD for slow, medium, and fast scrolling respectively instead of the numbers), but it would probably be best used with a small keypad where you could pick a handful of your favorite speeds and assign them to some obscure key combination that you would map to one of those keypad buttons.
Regardless, the point is that with a small amount of work, we can set up an off-hand alternative to jerking the mouse wheel back and forth incessantly. The more joints we spread these repetitive motions to, the better.
Enjoy:
#Persistent
#SingleInstance Force
SetBatchLines, -1
; Define scroll speeds (in milliseconds per scroll)
scrollSpeeds := [1000, 500, 200, 100, 67, 50, 40, 33, 25]
; Variables to track active scrolling
scrollUpActive := false
scrollDownActive := false;
Function to start scrolling up
StartScrollUp(speed) {
global scrollUpActive
scrollUpActive := true
while (scrollUpActive) {
Send {WheelUp}
Sleep speed
}
}
; Function to start scrolling down
StartScrollDown(speed) {
global scrollDownActive
scrollDownActive := true
while (scrollDownActive) {
Send {WheelDown}
Sleep speed
}
}
; Function to stop scrolling
StopScrolling() {
global scrollUpActive, scrollDownActive
scrollUpActive := false
scrollDownActive := false
}
; Manually Define Hotkeys for Alt + 1-9 (Scroll Up)
~Alt & 1::StartScrollUp(scrollSpeeds[1])
~Alt & 2::StartScrollUp(scrollSpeeds[2])
~Alt & 3::StartScrollUp(scrollSpeeds[3])
~Alt & 4::StartScrollUp(scrollSpeeds[4])
~Alt & 5::StartScrollUp(scrollSpeeds[5])
~Alt & 6::StartScrollUp(scrollSpeeds[6])
~Alt & 7::StartScrollUp(scrollSpeeds[7])
~Alt & 8::StartScrollUp(scrollSpeeds[8])
~Alt & 9::StartScrollUp(scrollSpeeds[9])
; Manually Define Hotkeys for Ctrl + 1-9 (Scroll Down)
~Ctrl & 1::StartScrollDown(scrollSpeeds[1])
~Ctrl & 2::StartScrollDown(scrollSpeeds[2])
~Ctrl & 3::StartScrollDown(scrollSpeeds[3])
~Ctrl & 4::StartScrollDown(scrollSpeeds[4])
~Ctrl & 5::StartScrollDown(scrollSpeeds[5])
~Ctrl & 6::StartScrollDown(scrollSpeeds[6])
~Ctrl & 7::StartScrollDown(scrollSpeeds[7])
~Ctrl & 8::StartScrollDown(scrollSpeeds[8])
~Ctrl & 9::StartScrollDown(scrollSpeeds[9])
; Ensure scrolling stops when releasing Alt or Ctrl
~Alt Up::
~Ctrl Up::
StopScrolling()
return
Note that this script as copy/pasted doesn’t play nicely with my scripts in the other post because I personally use the ctrl key in my macros to control Powerscribe, but changing things up is as easy as just changing a letter or two.
I am not an expert here, and I guarantee there are better ways to achieve this functionality, but stuff like this is a great example of what’s possible for a novice with a little vibe coding enabled by current LLMs.
4 Comments
This is the scrolling script I use.
#Requires AutoHotkey v2.0
scroll := MouseMovement(“MButton”, scrollfunc)
scrollfunc(xDelta, yDelta) {
static accumulator := 0
static threshold := 20 ; Adjust this value to control sensitivity (higher = less sensitive)
; Add the current movement to our accumulator
accumulator += yDelta
; Check if we’ve accumulated enough movement to trigger an arrow key
while (accumulator >= threshold) {
Send “{Up}”
accumulator -= threshold
}
while (accumulator <= -threshold) {
Send "{Down}"
accumulator += threshold ; Add because accumulator is negative
}
}
class MouseMovement {
__New(hk, callback) {
this.MD := MouseDelta(callback)
this.Toggle := 0
; Set up hotkeys for button down and up
Hotkey hk, hotkeyDownFunc, "On"
Hotkey hk " Up", hotkeyUpFunc, "On"
hotkeyDownFunc(*) {
if !this.Toggle {
this.Toggle := 1
; Start monitoring of mouse delta when button is pressed
this.MD.SetState(this.Toggle)
; Enable hook to block mouse movement
this.hook := MouseMovement.Hook(LowLevelMouseProc)
}
}
hotkeyUpFunc(*) {
if this.Toggle {
this.Toggle := 0
; Stop monitoring of mouse delta when button is released
this.MD.SetState(this.Toggle)
; Remove hook
this.hook := ""
}
}
LowLevelMouseProc(nCode, wParam, lParam) {
static WM_MOUSEMOVE := 0x200
if (wParam = WM_MOUSEMOVE) {
return 1 ; block mouse move
}
Return DllCall("CallNextHookEx", "Ptr", 0, "Int", nCode, "UInt", wParam, "Ptr", lParam)
}
}
; converted from https://www.autohotkey.com/boards/viewtopic.php?style=19&p=519818#p519818
class Hook {
__New(callback, isGlobal := true) {
static WH_MOUSE_LL := 14
this.pCallback := CallbackCreate(callback, 'Fast', 3)
this.hHook := DllCall('SetWindowsHookEx', 'Int', WH_MOUSE_LL, 'Ptr', this.pCallback,
'Ptr', !isGlobal ? 0 : DllCall('GetModuleHandle', 'UInt', 0, 'Ptr'),
'UInt', isGlobal ? 0 : DllCall('GetCurrentThreadId'), 'Ptr')
}
__Delete() {
DllCall('UnhookWindowsHookEx', 'Ptr', this.hHook)
CallbackFree(this.pCallback)
}
}
}
; converted from: https://www.autohotkey.com/boards/viewtopic.php?t=10159
Class MouseDelta {
State := 0
__New(callback) {
this.MouseMovedFn := this.MouseMoved.Bind(this)
this.Callback := callback
}
Start() {
static DevSize := 8 + A_PtrSize, RIDEV_INPUTSINK := 0x00000100
; Register mouse for WM_INPUT messages.
RAWINPUTDEVICE := Buffer(DevSize)
NumPut("UShort", 1, RAWINPUTDEVICE, 0)
NumPut("UShort", 2, RAWINPUTDEVICE, 2)
NumPut("Uint", RIDEV_INPUTSINK, RAWINPUTDEVICE, 4)
; WM_INPUT needs a hwnd to route to.
NumPut("Ptr", A_ScriptHwnd, RAWINPUTDEVICE, 8)
this.RAWINPUTDEVICE := RAWINPUTDEVICE
DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize)
OnMessage(0x00FF, this.MouseMovedFn)
this.State := 1
return this ; allow chaining
}
Stop() {
static RIDEV_REMOVE := 0x00000001
static DevSize := 8 + A_PtrSize
OnMessage(0x00FF, this.MouseMovedFn, 0)
RAWINPUTDEVICE := this.RAWINPUTDEVICE
NumPut("Uint", RIDEV_REMOVE, RAWINPUTDEVICE, 4)
DllCall("RegisterRawInputDevices", "Ptr", RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize)
this.State := 0
return this ; allow chaining
}
SetState(state) {
if (state && !this.State)
this.Start()
else if (!state && this.State)
this.Stop()
return this ; allow chaining
}
Delete() {
this.Stop()
this.MouseMovedFn := ""
}
; Called when the mouse moved.
; Messages tend to contain small (+/- 1) movements, and happen frequently (~20ms)
MouseMoved(wParam, lParam, *) {
Critical
; RawInput statics
static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, pcbSize:=8+2*A_PtrSize, offsets := {x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput
; 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))
uRawInput := Buffer(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 := 0, y := 0 ; Ensure we always report a number for an axis. Needed?
x := NumGet(uRawInput, offsets.x, "Int")
y := NumGet(uRawInput, offsets.y, "Int")
Critical false
this.Callback.Call(x, y)
}
}
Very interested in this. Getting errors when i compile unfortunately
I think the real answer to every AHK question is basically to run it through an LLM over and over until it figures it out.