Auto-adjusting screen area on Wacom tablets

(mouseover/click to play GIF) Script in action. See: Full-sized version; that wallpaper

If you have a display-less Wacom tablet (Wacom One/Intuos/etc.) and a large enough monitor (or monitors), mapping screen space to tablet can be a bit bothersome. But that can be fixed.

A bit of history

At some point during the past few years I got a new laptop, and a tiny Wacom tablet (One S):

Then I added a monitor for a bit more screen space:

Moving windows to the "tablet's" screen is a bit awkward, but Win+Shift+Left is a thing.

Later I added another monitor:

A triangular screen layout is pretty efficient in terms of how much you have to move the mouse around (other option: macros), but screen switch hotkeys get a bit awkward.

Later I added ano-- ah, hold on, apparently the laptop only has two display ports, and one of them isn't even HDMI 2.0. After considering the options, I settled on a specific 4K TV that's basically just a big monitor (2160p @ 43" has same density as 1080p @ 21.5"):

This was mostly a good idea, except you can't put something in center of the bigger screen with hotkeys alone, and most applications don't position their windows consistently enough for per-application options in Tablet Properties to be of good use.

So I thought - could I, by a chance, trick Tablet Properties into auto-updating the Screen Area to match the currently focused window? Then I wouldn't even have to switch between mouse/tablet most of the time.

As you might suspect, the answer was "yes".

The idea

The tricks outlined here are made possible thanks to this specific window:

It is what you use to define screen area yourself, spotting a screenshot control for quickly defining an area through click & drag, a button to point at exact TL/BR corners (showing a full-screen overlay to do so), and, a recent addition - a set of fields for entering coordinates by hand.

The later are exactly what we need, and we can have the script operate them, with a few gotchas:

  • Coordinates may not exceed desktop bounds
  • Right cannot be less than Left, ditto Bottom vs Top
  • Value changes take effect instantly, but require simulating focus correctly
    (activate field - change value - deactivate field)
  • Do not change the region while the mouse/stylus is being held down
    (else dragging objects between windows can get weird)

Fortunately, AutoIt can help us with most of these, and we can use WinAPI functions for the rest.

The code

As per above, the implementation is relatively straightforward, although the boilerplate code does add up a bit.

#include <WinAPI.au3>
#include <WindowsConstants.au3>
#include <GUIConstantsEx.au3>
#include <SendMessage.au3>
#include <Math.au3>
Const $margin = 120 ; in px, how far to let stylus move outside the window bounds
Const $minWidth = 600 ; in px, minimum window width (not to require large movements for tiny windows)
Const $minHeight = 300 ; in px, minimum window height (ditto)
Const $pollRate = 5 ; in ms, lower is more likely to notice short taps
;HotKeySet("{pause}", "DoExit") ; keyboard shortcut to exit script, if you need it
; if these ever change, you would find them via AutoIt Window Info:
Const $idY1 = 53
Const $idY2 = 54
Const $idX1 = 55
Const $idX2 = 56
Func Clamp($x, $a, $b)
	If ($x < $a) Then Return $a
	If ($x > $b) Then Return $b
	Return $x
Func DoExit()
	Exit 0
Global $desktopX1 = _WinAPI_GetSystemMetrics($SM_XVIRTUALSCREEN)
Global $desktopY1 = _WinAPI_GetSystemMetrics($SM_YVIRTUALSCREEN)
Global $desktopW  = _WinAPI_GetSystemMetrics($SM_CXVIRTUALSCREEN)
Global $desktopH  = _WinAPI_GetSystemMetrics($SM_CYVIRTUALSCREEN)
Global $desktopX2 = $desktopX1 + $desktopW
Global $desktopY2 = $desktopY1 + $desktopH
Global $desktopRect[4] = [$desktopX1, $desktopY1, $desktopW, $desktopH]
Global $hwnd = 0x0
Global $hfX1, $hfX2, $hfY1, $hfY2
Func IsMouseDown() ; LMB/RMB/MMB
	Return BitAND(_WinAPI_GetAsyncKeyState(0x1), 0x8000) <> 0 _
		Or BitAND(_WinAPI_GetAsyncKeyState(0x2), 0x8000) <> 0 _
		Or BitAND(_WinAPI_GetAsyncKeyState(0x4), 0x8000) <> 0
Func SetRegion_1($h0, $h1, $h2, $v)
	_SendMessage($h1, $WM_SETFOCUS, $h0, 0)
	ControlSetText($hwnd, "", $h1, String($v))
	_SendMessage($h1, $WM_KILLFOCUS, $h2, 0)
Func SetRegion($x1, $y1, $x2, $y2)
	Local $h0 = $hwnd
	; so, we can't enter x1 that is >x2 and what a trouble
	If ($y1 < Int(ControlGetText($hwnd, "", $idY2))) Then
		Local $h1 = $hfY1, $v1 = $y1, $h2 = $hfY2, $v2 = $y2
		Local $h2 = $hfY1, $v2 = $y1, $h1 = $hfY2, $v1 = $y2
	If ($x1 < Int(ControlGetText($hwnd, "", $idX2))) Then
		Local $h3 = $hfX1, $v3 = $x1, $h4 = $hfX2, $v4 = $x2
		Local $h4 = $hfX1, $v4 = $x1, $h3 = $hfX2, $v3 = $x2
	SetRegion_1($h0, $h1, $h2, $v1)
	SetRegion_1($h1, $h2, $h3, $v2)
	SetRegion_1($h2, $h3, $h4, $v3)
	SetRegion_1($h3, $h4, $h2, $v4)
	; for some reason we have to do a dance after the last input 
	_SendMessage($h2, $WM_SETFOCUS,  $h4, 0)
	_SendMessage($h2, $WM_KILLFOCUS, $h0, 0)
	_SendMessage($h0, $WM_SETFOCUS,  $h2, 0)
Local $curr = 0, $prev = 0
Local $noRect[4] = [0,0,0,0]
Local $cr[4] = [0,0,0,0]
Local $isFullScreen = False
Local $delayUpdate = False
Local $nullPtr = Ptr(0)
Local $noWindowNote = True
While 1
	; Config window [re-]opened?
	Local $hwnd_ = WinGetHandle("Portion of Screen")
	If ($hwnd_ == $nullPtr) Then 
		If ($noWindowNote) Then
			ConsoleWrite("Please open 'Mapping > Screen Area > Portion of Screen' from Wacom Tablet Preferences" & @CRLF)
			$noWindowNote = False
	If ($hwnd <> $hwnd_) Then
		$hwnd = $hwnd_
		ConsoleWrite("Config window changed to " & $hwnd & @CRLF)
		For $attempt = 1 To 50 ; window appears before controls for some reason
			$hfY1 = ControlGetHandle($hwnd, "", $idY1)
			$hfY2 = ControlGetHandle($hwnd, "", $idY2)
			$hfX1 = ControlGetHandle($hwnd, "", $idX1)
			$hfX2 = ControlGetHandle($hwnd, "", $idX2)
			If ($hfY1 <> $nullPtr And $hfY2 <> $nullPtr And _
				$hfX1 <> $nullPtr And $hfX2 <> $nullPtr) Then ExitLoop
			If ($attempt == 50) Then ConsoleWrite("Couldn't discover controls" & @CRLF)
		ConsoleWrite("Controls: "&$hfY1&", "&$hfY2&", "&$hfX1&", "&$hfX2&@CRLF)
	$curr = WinGetHandle("[ACTIVE]")
	; Try to walk up the window's parents-chain so that your area doesn't readjust
	; simply because you mouseovered a floating panel in Paint.NET:
	While ($curr <> 0)
		Local $par = _WinAPI_GetWindow($curr, 4) ; GW_OWNER
		If ($par <> 0) Then
			$curr = $par
	; figure out whether we need to update
	$update = False
	If ($curr <> $prev) Then ; active window changed?
		$prev = $curr
		;$update = True
		Local $currClass = _WinAPI_GetClassName($curr)
		Local $currTitle = WinGetTitle($curr)
		If ($currClass == "Windows.UI.Core.CoreWindow") Then
			$isFullScreen = ($currTitle == "Start" Or $currTitle == "Search")
			$isFullScreen = False
		$cr = $noRect
		;ConsoleWrite("Class:" & $currClass & @CRLF)
		;ConsoleWrite("Title:" & $currTitle & @CRLF)
		;ConsoleWrite("Full:" & $isFullScreen & @CRLF)
		; same as below:
		If (IsMouseDown()) Then $delayUpdate = True
		Local $nr = $isFullScreen ? $desktopRect : WinGetPos($curr)
		If (Not IsArray($nr)) Then
			; we have no permit to even poll the size of this window
		ElseIf (IsMouseDown()) Then
			; allowing to change target area while mouse is held down causes a variety of side effects
			; (such as causing you to drag objects not where you wanted)
			$delayUpdate = True
		ElseIf IsArray($cr) And ($cr[0]<>$nr[0] Or $cr[1]<>$nr[1] Or $cr[2]<>$nr[2] Or $cr[3]<>$nr[3]) Then
			$update = True
			$cr = $nr
	If ($update And IsArray($cr)) Then
		If ($delayUpdate) Then
			$delayUpdate = False
		;ConsoleWrite("Update!" & @CRLF)
		Local $cx1 = $cr[0]
		Local $cy1 = $cr[1]
		Local $cw = $cr[2]
		Local $ch = $cr[3]
		Local $cx2 = $cx1 + $cw
		Local $cy2 = $cy1 + $ch
		; apply min size:
		If ($cw < $minWidth) Then
			$cx1 = $cx1 + Floor(($cw - $minWidth) / 2)
			$cw = $minWidth
			$cx2 = $cx1 + $cw
		If ($ch < $minHeight) Then
			$cy1 = $cy1 + Floor(($ch - $minHeight) / 2)
			$ch = $minHeight
			$cy2 = $cy1 + $ch
		; apply margin:
		$cx1 = Clamp($cx1 - $margin, $desktopX1, $desktopX2)
		$cx2 = Clamp($cx2 + $margin, $desktopX1, $desktopX2)
		$cy1 = Clamp($cy1 - $margin, $desktopY1, $desktopY2)
		$cy2 = Clamp($cy2 + $margin, $desktopY1, $desktopY2)
		; and update:
		;ConsoleWrite("SetRegion T"&$cy1&" B"&$cy2&" L"&$cx1&" R"&$cx2&@CRLF)
		SetRegion($cx1, $cy1, $cx2, $cy2)

How to use this

  1. Firstly, you'll want to install AutoIt if you don't have it installed yet.
  2. Then, create a new au3 script in it, paste the code there, and save it somewhere.
  3. Open Wacom Tablet Properties, switch to Mapping, tick the "Force Proportions" checkbox
  4. Pick "Portion" in "Screen area" to open the "Portion of screen" window.
  5. Run the script (F5)

For subsequent uses, re-open the script and repeat steps 4-5;

You may compile your script into an executable for convenience and/or edit the few constants at the beginning for padding or minimum window size;

Having Start/Search menu open or clicking the desktop will map to the whole desktop for ease of switching active windows;

If you know yourself some AutoIt/BASIC, you can tweak the script to use custom logic with SetRegion (for instance, instead of following the active window, have keyboard shortcuts to snap to different regions of screen and/or different monitors).

Known issues

  • A few applications (most notably, Steam client) misuse windows in novel ways while not indicating their relationships to main window correctly, causing the script to readjust region to a popup window.
  • Using plenty of padding on windows close to screen edges can cause the area to be off-center on the tablet as Tablet Properties does not allow screen area to exceed desktop bounds.
  • Doesn't play well with per-application profiles because you cannot open multiple "portion of screen" windows at once (so you'd have to keep per-app profiles mapped to specific areas)

Have fun!

Related posts:

Leave a Reply

Your email address will not be published. Required fields are marked *

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