Preventing Windows from moving the cursor on touch

A diagram shows what's this post is about:

Out of box, tapping a touchscreen display on Windows can move the cursor to touch location and leave it there, which can be an inconvenience on multi-display setups.

I made a macro that moves the cursor back to where it was before touching the screen.

If you use a touch-enabled display on Windows, you might have noticed a certain quirk: when you tap on the touchscreen, Windows might move your cursor to touch location, but will not put it back afterwards, which is an inconvenience if it's a smaller secondary display that you use for macros/etc. or even if you are using mouse/touch interchangeably.

So I wrote an AutoHotKey macro that fixes this.

A little context

During the past year I've been using SteamDeck for work when blackouts were common - usually it would sit in its little dock below a large display, like so:

A drawing shows a SteamDeck in a dock below a monitor.
Both SteamDeck and the monitor have a silly face on their screens.

And that's a neat setup because moving your eyes down is quicker than turning your head to look at a side monitor, but also because you can touch the handheld's screen to focus a chat field or swipe to scroll a to-do list.

Recently I thought that it'd be fun to have something similar going for my desktop computer, so after doing some measurements I got a 14" multi-touch monitor:

A drawing shows the same monitor, but now with a Verbatim PMT-14 mini-monitor in front of it.
Both have a silly face on their screens.

The mini-monitor in question (Verbatim PMT-14) is a curious gadget - you can connect it to laptops and mobile devices with USB-C cable, or you can connect it to regular desktop PCs with a HDMI cable for video and a USB-C ↔ USB-A cable for touch input.

This kind of dual-cable setup also means that Windows does not know which display is the touch display and you have to tell it by going in to Control Panel ➜ Tablet PC Settings ➜ Setup.

With that out of the way,

What's going on, anyway

The way touch input works on Windows is as following: if the window handles the pointer events, the cursor will stay where it was. If it doesn't, Windows will move the cursor and emit a regular mouse event so that old and/or touch-unaware apps are less likely to break.

This makes sense, but is also inconvenient to the end user because:

  1. You cannot tell on a glance whether an application handles touch input.
  2. The mouse cursor is hidden after touching the screen

And thus it can be hard to tell where your cursor will end up after touch without wiggling the mouse and checking both screens. Not the best, isn't it

The macro

This AutoHotKey v2 macro runs a timer to check that the mouse jumped onto the touchscreen display too quickly and moves the cursor to the previous position once the mouse button(s) have been released.

#SingleInstance Force

class TouchscreenCursor {
	mouseX := 0
	mouseY := 0
	mouseBuf := Buffer(8)
	lockTimer := 0
	;
	zoneLeft := 0
	zoneTop := 0
	zoneRight := 0
	zoneBottom := 0
	;
	updateFrequency := 10
	jumpThreshold := 100
	unlockAfter := 20
	;
	distance(x1, y1, x2, y2) {
		dx := x2 - x1
		dy := y2 - y1
		return Sqrt(dx * dx + dy * dy)
	}
	; creates circle tray icons
	createIcon(color) {
		size := 16
		pixels := Buffer(size * size * 4, 0)
		loop 16 {
			y := A_Index - 1
			loop 16 {
				x := A_Index - 1
				dist := this.distance(x, y, 7.5, 7.5)
				alpha := 1 - (dist - 6) / 1.5
				if (alpha > 0) {
					rgba := (Round(Min(alpha, 1) * 255) << 24) | color
					NumPut("UInt", rgba, pixels, 4 * ((y * size) + x))
				}
			}
		}
		return "HBITMAP:*" DllCall("CreateBitmap",
			"Int", 16, "Int", 16,
			"Uint", 1, "Uint", 32,
			"Ptr", pixels,
			"Ptr"
		)
	}
	resetIcon() {
		TraySetIcon(this.iconOff)
		SetTimer(this.resetIcon_t, 0)
	}
	resetIcon_t := this.resetIcon.Bind(this)
	;
	inZone(mx, my) {
		return mx >= this.zoneLeft and mx < this.zoneLeft + this.zoneRight
			and my >= this.zoneTop and my < this.zoneTop + this.zoneBottom
	}
	setZone(left, top, right, bottom) {
		this.zoneLeft := left
		this.zoneRight := right
		this.zoneTop := top
		this.zoneBottom := bottom
		A_IconTip := ("tc.setZone(" this.zoneLeft ", " this.zoneTop
			", " this.zoneRight ", " this.zoneBottom ")")
	} 
	setZoneAuto() {
		DllCall("GetCursorPos", "Ptr", this.mouseBuf)
		mx := NumGet(this.mouseBuf, 0, "Int")
		my := NumGet(this.mouseBuf, 4, "Int")
		loop MonitorGetCount() {
			MonitorGet(A_Index, &monLeft, &monTop, &monRight, &monBottom)
			if (mx >= monLeft && mx < monRight && my >= monTop && my <= monBottom) {
				this.setZone(monLeft, monTop, monRight, monBottom)
				return true
			}
		}
		return false
	}
	update() {
		if (this.lockTimer > 0) {
			if (GetKeyState("LButton") or GetKeyState("RButton") or GetKeyState("MButton")) {
				; dragging a window, etc. - please wait
			} else {
				this.lockTimer -= this.updateFrequency
				if (this.lockTimer <= 0) {
					DllCall("SetCursorPos", "int", this.mouseX, "int", this.mouseY)
					SetTimer(this.resetIcon_t, 150)
				}
			}
		} else {
			DllCall("GetCursorPos", "Ptr", this.mouseBuf)
			newX := NumGet(this.mouseBuf, 0, "Int")
			newY := NumGet(this.mouseBuf, 4, "Int")
			if (this.inZone(newX, newY)
			and not this.inZone(this.mouseX, this.mouseY)
			and this.distance(this.mouseX, this.mouseY, newX, newY) > this.jumpThreshold
			) {
				this.lockTimer := this.unlockAfter
				TraySetIcon(this.iconOn)
				SetTimer(this.resetIcon_t, 0)
			} else {
				this.mouseX := newX
				this.mouseY := newY
			}
		}
	}
	;
	__New() {
		this.iconOff := this.createIcon(0x707E98)
		this.iconOn := this.createIcon(0x43D3E0) ; 38C493 for minty color
		this.resetIcon()
	}
	listen(frequency, jumpThreshold) {
		DllCall("GetCursorPos", "Ptr", this.mouseBuf)
		this.mouseX := NumGet(this.mouseBuf, 0, "Int")
		this.mouseY := NumGet(this.mouseBuf, 4, "Int")
		this.jumpThreshold := jumpThreshold
		this.updateFrequency := frequency
		SetTimer(this.Update.Bind(this), frequency)
	}
}

tc := TouchscreenCursor()
tc.setZoneAuto()
tc.listen(10, 200)

You can save the above to an .ahk file, edit it as you please, and run it.

If you run it without changing anything, it'll assume the touchscreen monitor to be the one where the mouse cursor is at the time of launching, and you'll get this circular tray icon:

A screenshot shows the tray icon of the macro. It's a gray-blue-ish circle.

The icon will blink whenever the tool reverts mouse movement, and if you hold the mouse over it, it'll display the monitor bounds so that you can change

tc.setZoneAuto()

to something like

tc.setZone(320, 1440, 2240, 2340)

and not have position your cursor each time.

The arguments of tc.listen are how often the macro should poll for mouse position (in milliseconds) and how far the cursor should move over one such interval (in pixels) for it to be considered a touch-induced snap. The defaults should work well.

You can adjust the tray icon colors in the __New function.

As a certain caveat, this macro has no way of tell apart other software-induced mouse movements, like Mouse Jump in PowerToys.

Thanks for reading!

Related posts:

Leave a Reply

Your email address will not be published. Required fields are marked *
Note: JavaScript is currently required to post comments.

This site uses Akismet to reduce spam. Learn how your comment data is processed.