Editor's Choice: This article has been selected by our editors as an exceptional contribution.

Keyboard shortcuts (hotkeys) to move mouse in multi-monitor configuration - AutoHotkey Script

Joe WinogradDeveloper
CERTIFIED EXPERT
50+ years in computers
Development•Sales
CIO•Document Imaging
EE — FELLOW 2017
MVE 2015,2016,2018
RENOWNED 2018,2019
CERTIFIED GOLD 2020
Published:
Updated:
Edited by: Andrew Leniart
In configurations with multiple monitors, it can be difficult and time-consuming to move the mouse pointer to a particular monitor. This article presents a script written in AutoHotkey that defines hotkeys — such as Alt+Ctrl+1, Alt+Ctrl+2, etc. — to move the mouse easily and quickly to any monitor.
Introduction

In an interesting question here at Experts Exchange, a member requested this:
Code for using Windows AutoHotkey to immediately move mouse pointer to particular monitor
The member goes on to say this:
...several users who have 8 or more monitors connected to their Windows 10 computers
The member indicates that the requirement is to write an AutoHotkey script that defines a series of keyboard shortcuts (aka "hotkeys") to move the mouse pointer to each monitor. In particular:
 
Alt+Ctrl+1 ==> move mouse to monitor 1
Alt+Ctrl+2 ==> move mouse to monitor 2
Alt+Ctrl+3 ==> move mouse to monitor 3
Alt+Ctrl+4 ==> move mouse to monitor 4
Alt+Ctrl+5 ==> move mouse to monitor 5
Alt+Ctrl+6 ==> move mouse to monitor 6
Alt+Ctrl+7 ==> move mouse to monitor 7
Alt+Ctrl+8 ==> move mouse to monitor 8
Alt+Ctrl+9 ==> move mouse to monitor 9
 
I wrote a no-frills script (Version 1) and posted it at the EE thread. After that, however, I decided to write this EE article with an enhanced script. The article began with Version 2, then went to Version 3, and is now on Version 4.
 
Version 4 Features

• Defines the nine hotkeys shown above (Alt+Ctrl+1 through Alt+Ctrl+9). The monitor numbers (1-9) are established by the script. They may or may not be the same numbers as reported by Windows in Settings>Display. For example, let's say that Settings>Displaylooks like this (with monitor #1 disconnected):


The script may report them as 2, 1, 3 from left to right (and showing only three monitors because one is disconnected). You can figure out how the script is referring to the monitors via the Identify Monitors feature (discussed below).

• Defines Alt+Ctrl+0 (zero) as a hotkey to move the mouse to the Primary monitor, regardless of its monitor number.

• Allows other "modifier" keys to be used for the hotkeys rather than hard-coding Alt+Ctrl ("!^"), as follows:

! (exclamation mark) ==> Alt
^ (caret, circumflex, hat) ==> Ctrl
+ (plus sign) ==> Shift
# (hash mark, pound sign, octothorpe) ==> Win (Windows logo key)

You may use any number of those modifiers, and they may be in any order, that is, !^ (Alt+Ctrl) behaves the same as ^! (Ctrl+Alt).

• Allows horizontal (X) and vertical (Y) offsets to be specified. That is, the mouse pointer will be moved to the specified number of pixels (OffsetX) from the left edge of the monitor and the specified number of pixels (OffsetY) from the top edge of the monitor. If OffsetX is set to -1, it moves the mouse to the horizontal center of the monitor. If OffsetY is set to -1, it moves the mouse to the vertical center of the monitor.

• Displays an icon in the system tray. The default is a monitor icon, but it can be changed to a mouse icon. The icons look like this (may vary based on the version of Windows, as it gets the icons from a built-in Windows file; also, may render a little differently in the tray):


• Displays a tooltip when hovering on the system tray icon. The tooltip shows the file name of the script (without its path and file extension) and brief usage instructions. It looks like this:


• Displays this context menu when right-clicking on the system tray icon:


• Selecting the Show Monitor and Virtual Screen Information menu item displays a dialog that looks like this:


The monitor numbers (1, 2, and 3 in the example above, followed by a colon) are established by the script, as noted earlier (they are not the same numbers as in Settings>Display). The monitor names (such as \\.\DISPLAY8 in the example above) are the names reported by Windows. Those names overall, and the ending numbers specifically (such as 8, 9, and 10 in the example above) are assigned by Windows (and irrelevant to the operation of the script).

The L, R, T, and B values next to each monitor name are the bounding monitor coordinates for that monitor — Left, Right, Top, and Bottom.

The VirtualX and VirtualY values are the coordinates for the left edge and top edge of the virtual screen, respectively. The VirtualW and VirtualH values are the width and height of the virtual screen, respectively. The virtual screen is the bounding rectangle of all the monitors.

• Selecting the Identify Monitors menu item displays a monitor number on each monitor for however many seconds you prefer (set at 5 seconds in the attached script — change it to whatever you want). The monitor number will be, of course, 1 through 9 and will look like this, for example:


Those numbers are the ones established by the script and used by the hotkeys, such as  Alt+Ctrl+3. They are also the numbers that are reported in the Show Monitor and Virtual Screen Information dialog. The top image in this article shows all nine identifying numbers in that nine-monitor configuration.

The image files (PNG) for all nine numbers are included in the attached ZIP file.

• Selecting the Change Tray Icon menu item changes the icon to a mouse if it is currently a monitor or to a monitor if it is currently a mouse. The default icon in the attached script is the monitor. Changing it via this menu pick does not change it permanently — to do that, change the TrayIconNum assignment statement in the script.

• Selecting the Start with Windows (On/Off toggle) menu item toggles that setting, i.e., if it is Off (the default), it turns On; if it is On, it turns Off. When it is On, a checkmark appears next to it in the context menu:


The method used to start the script with Windows is to create a shortcut (a .LNK file) and put it in the Startup folder (the one for the logged in user, not the Public one). The icon for it in the Startup folder looks like this (may vary based on the version of Windows, as it gets the icon from a built-in Windows file; also, may render a little differently in the tray):


Turning this off deletes the shortcut in the Startup folder; turning it on creates the shortcut. This setting is "sticky" (survives exiting the script).

• Selecting the Reload Script menu item exits the script and immediately re-runs it.

• Selecting the About menu item displays a dialog box with a Title bar that contains the file name of the script without its path and file extension. The body of the dialog contains three lines:

(1) The full path/name/extension of the script.

(2) The Version of the script as specified in the assignment statement on line 2 of the source code. I recommend this to be in two-segment format (but it can be anything you want), where the first segment is the "major" version and the second segment is the "minor" version (separated by a period). For example, 4.1, which was the first version of the script to have this feature.
 
(3) The Modified date/time stamp of the script file.
 
Here's an example of it:


• Selecting the Exit menu item exits (terminates/quits) the script, thereby removing the icon from the system tray and un-defining all the hotkeys. To make sure that this wasn't selected by accident, it displays this dialog, with No as the default button (so that an accidental Enter key won't cause an unwanted exit):


The Script — Source Code
 
As noted at the top of this article, the Experts Exchange member specifically asked for an AutoHotkey solution. If you are not familiar with the language, my EE article will get you going on it:

AutoHotkey - Getting Started

Here is the script in a code block:

; Joe Winograd 20-Oct-2020
Version:="4.1"
#Warn,UseUnsetLocal ; warning on uninitialized variables
#NoEnv ; avoid checking empty variables to see if they are environment variables
#SingleInstance Force ; replace old instance immediately
SetBatchLines,-1 ; run at maximum speed

Gosub,InitializeVars ; initialize all variables
Gosub,ConfigureInitialTray ; configure initial system tray (notification area)
Return

InitializeVars:
SplitPath,A_ScriptName,,,,ProgramName ; ProgramName==>script name without path or extension
FileGetTime,ScriptDateTime,%A_ScriptName%,M ; modified time of script
FormatTime,ScriptDateTime,%ScriptDateTime%,yyyyMMdd-HHmmss
TrayIconFile:=A_WinDir . "\System32\mmcndmgr.dll" ; get icons from this built-in file (need DLL that exists in XP-W10 and has both monitor and mouse icons)
TrayIconNumMon:="-30557" ; monitor icon
TrayIconNumMou:="-30547" ; mouse icon
StartupIconFile:=A_WinDir . "\System32\netshell.dll" ; get icon from this built-in file (need DLL that exists in XP-W10 and first icon is monitor)

; *** begin variables to change ***
IdentifySeconds:=5 ; length of time in seconds for monitor-identifying numbers to stay on screen
TrayIconNum:=TrayIconNumMon ; default icon - TrayIconNumMon is monitor, TrayIconNumMou is mouse
OffsetX:=-1 ; move mouse to this number of pixels from left edge of monitor (-1 means center)
OffsetY:=-1 ; move mouse to this number of pixels from top edge of monitor (-1 means center)
;OffsetX:=0 ; example - left edge
;OffsetY:=0 ; example - top edge
;OffsetX:=400 ; example - 400 pixels from left edge
;OffsetY:=300 ; example - 300 pixels from top edge
; modifier keys:  ! is Alt   ^ is Ctrl   + is Shift   # is Win (the Windows logo key)
HotkeyModifiers:="!^" ; Alt+Ctrl - these are the modifier keys for the number keys (0-9)
; *** end variables to change ***

IdentifyMilliseconds:=IdentifySeconds*1000 ; Sleep time is in milliseconds
NumModifiers:=StrLen(HotkeyModifiers) ; get number of modifier keys
Hotkey,%HotkeyModifiers%0,MoveMousePrimary,On ; define Modifiers+0 hotkey to move mouse to Primary monitor regardless of its monitor number
SysGet,OrigNumMons,MonitorCount ; original number of monitors - when script was run
Loop,%OrigNumMons% ; process all monitors
  Hotkey,%HotkeyModifiers%%A_Index%,MoveMouseMon,On ; define Modifiers+N hotkey
Return

ConfigureInitialTray:
Menu,Tray,NoStandard ; do not use standard AutoHotkey context menu
Menu,Tray,Add,Show &Monitor and Virtual Screen Information,ContextMenu
Menu,Tray,Add,&Identify Monitors,ContextMenu
Menu,Tray,Add,Change &Tray Icon,ContextMenu
Menu,Tray,Add,Start with &Windows (On/Off toggle),ContextMenu
StartupLink:=A_Startup . "\" . ProgramName . ".lnk"
If (FileExist(StartupLink))
  Menu,Tray,Check,Start with &Windows (On/Off toggle)
Else
  Menu,Tray,Uncheck,Start with &Windows (On/Off toggle)
Menu,Tray,Add,&Reload Script,ContextMenu
Menu,Tray,Add,&About,ContextMenu
Menu,Tray,Add,E&xit,ContextMenu
Menu,Tray,Default,Show &Monitor and Virtual Screen Information
HotkeyModifiersTip:=StrReplace(HotkeyModifiers,"+","Shift+") ; do this first so we don't catch the other plus signs
HotkeyModifiersTip:=StrReplace(HotkeyModifiersTip,"!","Alt+")
HotkeyModifiersTip:=StrReplace(HotkeyModifiersTip,"^","Ctrl+")
HotkeyModifiersTip:=StrReplace(HotkeyModifiersTip,"#","Win+")

TrayTip:=ProgramName . "`n" . HotkeyModifiersTip . "N move to monitor 0-9 (0 always primary)`nRight-click for context menu"
Menu,Tray,Tip,%TrayTip%
Menu,Tray,Icon,%TrayIconFile%,%TrayIconNum%
Return

MoveMouseMon:
MonNum:=SubStr(A_ThisHotkey,NumModifiers+1,1) ; monitor number is after modifiers
PerformMove(MonNum,OffsetX,OffsetY) ; move it
Return

MoveMousePrimary:
SysGet,PrimaryMonNum,MonitorPrimary ; get number of Primary monitor
PerformMove(PrimaryMonNum,OffsetX,OffsetY) ; move it
Return

PerformMove(MoveMonNum,OffX,OffY)
{
  global MoveX,MoveY
  Gosub,CheckNumMonsChanged ; before performing move, check if the number of monitors has changed
  RestoreDPI:=DllCall("SetThreadDpiAwarenessContext","ptr",-3,"ptr") ; enable per-monitor DPI awareness and save current value to restore it when done - thanks to lexikos for this
  SysGet,Coordinates%MoveMonNum%,Monitor,%MoveMonNum% ; get coordinates for this monitor
  Left:=Coordinates%MoveMonNum%Left
  Right:=Coordinates%MoveMonNum%Right
  Top:=Coordinates%MoveMonNum%Top
  Bottom:=Coordinates%MoveMonNum%Bottom
  If (OffX=-1)
    MoveX:=Left+(Floor(0.5*(Right-Left))) ; center
  Else
    MoveX:=Left+OffX
  If (OffY=-1)
    MoveY:=Top+(Floor(0.5*(Bottom-Top))) ; center
  Else
    MoveY:=Top+OffY
  DllCall("SetCursorPos","int",MoveX,"int",MoveY) ; first call to move it - usually works but not always
  Sleep,10 ; wait a few milliseconds for first call to settle
  DllCall("SetCursorPos","int",MoveX,"int",MoveY) ; second call sometimes needed
  DllCall("SetThreadDpiAwarenessContext","ptr",RestoreDPI,"ptr") ; restore previous DPI awareness - thanks to lexikos for this
  Return
}

CheckNumMonsChanged:
SysGet,CurrNumMons,MonitorCount ; current number of monitors
If (OrigNumMons!=CurrNumMons)
{
  MsgBox,4144,Warning,Number of monitors changed since script was run`nOriginal=%OrigNumMons%`nCurrent=%CurrNumMons%`n`nWill reload script when you click OK button
  Reload
  Sleep,2000 ; give Reload two seconds to work during this Sleep - if Reload successful, will never get to code below
  MsgBox,4112,Error,Unable to reload script`nWill exit when you click OK button`nYou will have to re-run the script manually
  ExitApp
}
Return ; number of monitors has not changed

ContextMenu:
If (A_ThisMenuItem="Show &Monitor and Virtual Screen Information")
{
  Gosub,CheckNumMonsChanged ; before showing info, check if the number of monitors has changed
  RestoreDPI:=DllCall("SetThreadDpiAwarenessContext","ptr",-3,"ptr") ; enable per-monitor DPI awareness and save current value to restore it when done - thanks to lexikos for this
  AllMons:=""
  Loop,%OrigNumMons% ; process all monitors
  {
    MonNum:=A_Index
    SysGet,MonName,MonitorName,%MonNum% ; get name of this monitor
    SysGet,Coordinates%MonNum%,Monitor,%MonNum% ; get coordinates for this monitor
    Left:=Coordinates%MonNum%Left
    Right:=Coordinates%MonNum%Right
    Top:=Coordinates%MonNum%Top
    Bottom:=Coordinates%MonNum%Bottom
    AllMons:=AllMons . A_Index . ": " . MonName . " L=" . Left . " R=" . Right . " T=" . Top . " B=" . Bottom . "`n"
  }
  SysGet,VirtualX,76 ; coordinate for left side of virtual screen
  SysGet,VirtualY,77 ; coordinate for top of virtual screen
  SysGet,VirtualW,78 ; width of virtual screen
  SysGet,VirtualH,79 ; height of virtual screen
  SysGet,PrimaryMonNum,MonitorPrimary ; number of Primary monitor
  MsgBox,4160,%ProgramName%,Monitor numbers`, names`, coordinates:`n%AllMons%Primary monitor number: %PrimaryMonNum%`n`nVirtual screen information:`nVirtualX=%VirtualX%`nVirtualY=%VirtualY%`nVirtualW=%VirtualW%`nVirtualH=%VirtualH%
  DllCall("SetThreadDpiAwarenessContext","ptr",RestoreDPI,"ptr") ; restore previous DPI awareness - thanks to lexikos for this
  Return
}

If (A_ThisMenuItem="&Identify Monitors")
{
  Gosub,CheckNumMonsChanged ; before identifying, check if the number of monitors has changed
  RestoreDPI:=DllCall("SetThreadDpiAwarenessContext","ptr",-3,"ptr") ; enable per-monitor DPI awareness and save current value to restore it when done - thanks to lexikos for this
  VarSetCapacity(PosPtr,4,0) ; get current position so it can be restored after identify
  DllCall("GetCursorPos","ptr",&PosPtr)
  SaveX:=NumGet(PosPtr,0,"int")
  SaveY:=NumGet(PosPtr,4,"int")
  SysGet,NumMons,MonitorCount
  Loop,%NumMons% ; process all monitors
  {
    PerformMove(A_Index,-1,-1) ;  put identifying number in center of screen
    IdentifyImage:=A_ScriptDir . "\Monitor" . A_Index . ".png" ; credit to www.iconarchive.com for the images
    SplashImage,%A_Index%:%IdentifyImage%,B X%MoveX% Y%MoveY%
  }
  Sleep,%IdentifyMilliseconds%
  Loop,%NumMons% ; process all monitors
    SplashImage,%A_Index%:Off
  DllCall("SetCursorPos","int",SaveX,"int",SaveY)
  DllCall("SetThreadDpiAwarenessContext","ptr",RestoreDPI,"ptr") ; restore previous DPI awareness - thanks to lexikos for this
  Return
}

If (A_ThisMenuItem="Change &Tray Icon")
{
  If (TrayIconNum=TrayIconNumMou)
    TrayIconNum:=TrayIconNumMon
  Else
    TrayIconNum:=TrayIconNumMou
  Menu,Tray,Icon,%TrayIconFile%,%TrayIconNum%
  Return
}

If (A_ThisMenuItem="Start with &Windows (On/Off toggle)")
{
  If (FileExist(StartupLink))
  {
    ; it's on, so this click turns it off
    Menu,Tray,Uncheck,Start with &Windows (On/Off toggle)
    FileDelete,%StartupLink%
    Return
  }
  Else
  {
    ; it's off, so this click turns it on
    Menu,Tray,Check,Start with &Windows (On/Off toggle)
    FileCreateShortcut,%A_ScriptFullPath%,%StartupLink%,%A_ScriptDir%,,%ProgramName%,%StartupIconFile% ; in a DLL, only first icon is valid (in this case, it is dual monitors)
    {
      If (ErrorLevel!=0)
      {
        MsgBox,4112,Fatal Error,Error Level=%ErrorLevel% trying to create Startup shortcut:`n%StartupLink%
        ExitApp
      }
    }
    Return
  }
}

If (A_ThisMenuItem="&Reload Script")
{
  Reload
  Sleep,2000 ; give Reload two seconds to work during this Sleep - if Reload successful, will never get to code below
  MsgBox,4112,Error,Unable to reload script`nWill exit when you click OK button`nYou will have to reload the script manually
  ExitApp
}

If (A_ThisMenuItem="&About")
{
  MsgBox,4160,About %ProgramName%,%A_ScriptFullPath%`n`nVersion %Version%`n`nModified: %ScriptDateTime%
  Return
}

If (A_ThisMenuItem="E&xit")
{
  MsgBox,4388,%ProgramName% - Terminate?,Are you sure you want to quit and deactivate all hotkeys?
  IfMsgBox,No
    Return
  ExitApp
}
My hope is that the descriptive variable names along with the extensive comments in the script provide enough documentation for readers to modify it, but if you have any questions, post them here, and I'll try to assist.

The script should work in all versions of Windows from XP through W10, both 32-bit and 64-bit, with up to nine monitors. I tested it on XP/32-bit with two monitors, W7/64-bit with three monitors, W10/64-bit with two monitors, and W10/64-bit with three monitors — all worked perfectly. If you use it on other configurations — a different version of Windows and/or a different number of monitors — I'll really appreciate it if you post your results as a comment under the article so that I can add it to the section below.

Results from Experts Exchange Members

• After I fixed the DPI scaling bug, Richard Chu reported success in W10 with a four-monitor configuration. You may see all his feedback in the comments under this article beginning on 2-Oct-2020.

Uday Sawhney reported success in W10 with a five-monitor configuration. I was very gratified to see him say, "Great program. I have PAID for programs that do not simply work. Your [program] does." All his feedback is at this EE Q&A thread.

Acknowledgements

My thanks to Andrew Leniart, Senior Editor here at Experts Exchange, for his deep knowledge of the article editor and his ability to solve formatting problems. He has spent extensive time and effort on improving the appearance of this article (and many others!), for which I'm extremely grateful.

My thanks to Richard Chu for reporting the DPI scaling bug and running many tests to help me troubleshoot the problem and solve it.

Conclusion

The attached ZIP file contains the AutoHotkey script (an AHK file containing the source code shown above) and all the supporting files required for the script to work. Simply unpack all the files in the ZIP file into the same folder, then run the AHK file.

If you find this article to be helpful, please click the thumbs-up icon below. This lets me know what is valuable for EE members and provides direction for future articles. Thanks very much! Regards, Joe

MoveMouseToMonitorV4.1.zip

5
2,538 Views
Joe WinogradDeveloper
CERTIFIED EXPERT
50+ years in computers
Development•Sales
CIO•Document Imaging
EE — FELLOW 2017
MVE 2015,2016,2018
RENOWNED 2018,2019
CERTIFIED GOLD 2020

Comments (38)

Joe WinogradDeveloper
CERTIFIED EXPERT
Fellow
Most Valuable Expert 2018

Author

Commented:
You're welcome, Richard, and thanks to you for doing some digging, too. Regards, Joe
Joe WinogradDeveloper
CERTIFIED EXPERT
Fellow
Most Valuable Expert 2018

Author

Commented:
Hi Richard,
A quick note to let you know that I haven't found a work-around with AutoHotkey. If I happen to come across one, I'll let you know, of course, but at this point I'm not going to be actively researching it. Regards, Joe
Hi Joe,
Much appreciated for it! I have also been looking for it, but seems most forum said its not possible at the moment. I have also added this feature request to the PowerToys, hopefully someday they can implement it.

https://github.com/microsoft/PowerToys/preleases/

Thank you once again Joe! Much appreciated!
Cheers
Richard
Joe WinogradDeveloper
CERTIFIED EXPERT
Fellow
Most Valuable Expert 2018

Author

Commented:
Hi Richard,
Great idea to make it a feature request for PowerToys! Please let me know if you get any feedback on it. Thanks, Joe
Absolutely will do! Thank you Joe!

View More

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.

Get access with a 7-day free trial.
You Belong in the World's Smartest IT Community