Automating Windows Display Settings
Juanjo sada
Posted on June 21, 2024
Intro
Recently I found out about the wonders of the PyAutoGUI library through the book 'Automate the Boring Stuff with Python' by Al Sweigart, which you can read online for free here. However, to actually learn something you have to apply it yourself, so I could think of no better thing to automate than one of the most tedious problems I'm sure everyone can relate to. Yes, you guessed it, it's disabling one of your multiple monitors on your computer so you can use it for another input device (in my case, an xbox).
All code can be found in this repo
Context
So in case it wasn't clear due to my poor writing, this is probably a very niche solution to very niche problem, but what's fun about being an engineer if you can't use your skills to build tools that help you in your daily workflow? Even if you have to use them for years to make up for the time you spent developing them.
I have 3 monitors in my computer setup.
Usually all 3 monitors are used by my PC at the same time, however, I also use monitor 1 for my Xbox. My PC is connected to the monitor via a Display Port cable, while the Xbox uses an HDMI. The issue becomes that if I simply switch the input from DP to HDMI when playing xbox, the PC display remains active, but it's running in the 'background' for the PC. This means that when I move windows around, or open new windows, the 1st display is still fair game, which can make it impossible to keep track of things, as all I see in the main screen is the Xbox interface. So generally I disable the first monitor, making the display settings layout look like this:
The Idea
Every time I wanted to play xbox, I would open display settings and disable the first monitor, allowing the other two monitors act as the only ones available, therefore allowing me to use the first monitor for the Xbox freely, without fear of 'losing' any windows in a display that isn't actually being shown by the monitor.
Now, this is a pretty straightforward process, which only takes about 20 seconds to disable the monitor, and then another 20 to reenable it. That's 40 wasted seconds every time I play and stop playing, which can add up specially if you play often. So why not automate it?
Now, the automations are simple, one to disable the first monitor on my PC when I'm going to start playing Xbox, and another to reenable it after I'm done playing Xbox.
Note: Due to the nature of the tutorial, it's hard to include a gif, since most recording software gets weird when changing around display settings, hope this doesn't cause too much trouble.
The Solution
To implement these steps I first tried using PyAutoGUI's click() function, which can take a picture file as a parameter, look for it on the screen, click on it if found. However, it became apparent quite quickly that this could present an issue because PyAutoGUI cannot locate an image on screen if that screen is not the 'main display', and after running some tests, I realized that when working with 3 monitors, specially if changing the main display around, display settings could be opened in ANY of the 3 monitors. Considering that using the pictures wasn't the best option now, I changed the approach to instead get the coordinates of every button we need to click.
Buttons needed
Automation to disable the first monitor
The buttons we need to press, in order, are:
- Symbol for second monitor
- 'Make this my main display' checkbox
- Symbol for first monitor
- Options dropdown menu
- 'Disconnect this display' option
- 'Accept Changes' button (this will show up after you disconnect the display)
Automation to reenable the first monitor
The buttons we need to press, in order, are:
- Symbol for first monitor
- Options dropdown menu
- 'Extend desktop to this display' button
- 'Make this my main display' checkbox
- 'Accept Changes' button (this will show up after you extend desktop to the display)
Monitor considerations
Since 'Display Settings' can be opened in any of the monitors, we actually need to be able to determine in which screen 'Display Settings' was opened, and keep 3 sets of coordinates, one for each monitor. Then, we can determine which screen is being used, and get the coordinates for that given screen.
For this I first maximize the screen and check the size, since all 3 of my monitors are different size, checking this actually gives the size of which exactly which screen 'Display Settings' opened in, and therefore which set of coordinates to use. To do this we get the active window, maximize it, and compare the size to a tuple containing the size of each of the screens.
# maximize display settings to ensure the coordinates are the same every time
logging.debug('Maximizing display settings window')
display_settings_window = pyautogui.getActiveWindow()
display_settings_window.maximize()
time.sleep(1)
# determine in which monitor Display Settings was opened
logging.debug('Determining in which monitor display settings was opened')
if display_settings_window.size == (2576, 1456):
screen = 'first_screen'
elif display_settings_window.size == (1936, 1096):
screen = 'second_screen'
elif display_settings_window.size == (1295, 735):
screen = 'third_screen'
else:
logging.debug(f'Could not determine screen of size:{str(display_settings_window.size)}')
raise Exception('Could not determine screen')
logging.debug(f'Display settings opened on {screen}')
Now we can access the coordinate values for the given 'screen' key in the dictionary.
pyautogui.click(button_coordinates[screen]['second_screen_symbol'], interval=0.5)
Main display will affect coordinates
On top of determining which monitor 'Display Settings' opened to, I encountered another issue. PyAutoGUI's coordinates are (0,0) at the top left of the main display
Meaning that once the main display is changed during the program run, the coordinate starting point will change, meaning all button coordinates will change as well, as they are relative to this (0,0) coordinate.
This means that the coordinates depend not only on which monitor 'Display Settings' actually opened in, but also which monitor is the main display. So we must keep track of which monitor is the main display, and note the new location of the buttons to be pressed after the change (which luckily isn't all of them).
In the automation to disable the monitor, changing the main display is the second step, meaning that the buttons whose coordinates need to be relative to the top left of the second monitor are:
- Symbol for first monitor
- Options dropdown menu
- 'Disconnect this display' option
- 'Accept Changes' button
In the automation to reenable the monitor, only the 'Accept Changes' button needs to be pressed after changing the main display.
Getting button coordinates
To get these the coordinates for all the buttons we will use PyAutoGUI's mouseInfo(), a tool that will show the coordinates of your current mouse location, and allow you to copy them as a tuple. To access this tool, you need to run the pyautogui.mouseInfo() command in the python interpreter in a command line.
So we open a command line in the project directory, make sure we have pyautogui installed (you can also use a venv), and then open the python interpreter, import pyautogui, and call the function to open the tool.
.\.venv\Scripts\activate
python
And in the python interpreter
import pyautogui
pyauogui.mouseInfo()
This should open the following tool.
Using this tool, you can now begin to register the coordinates of each button you need to press, making sure to take into account which monitor will be the main display at the time of clicking the button.
The tool has a '3 Sec. Button Delay' option, allowing you to click on 'Copy XY (F2)', and having 3 seconds to move your cursor to the desired location before it actually copies the coordinates.
Do this for all the buttons, in all 3 monitors, and you are set. In my case, the resulting dictionary was as follows:
button_coordinates = {
'first_screen':{
'second_screen_symbol':(1525, 269),
'extend_desktop_symbol':(-776, 292),
'make_main_display_symbol':(999, 583),
'first_screen_after_change_symbol':(-1203, 104),
'disconnect_this_display_symbol':(-788, 330)
# note there's no need for 'keep_changes' here because the button
# doesn't appear on screen until after this display is disconnected
},
'second_screen':{
'second_screen_symbol':(3766, 434),
'extend_desktop_symbol':(1460, 460),
'make_main_display_symbol':(3240, 747),
'first_screen_after_change_symbol':(1036, 270),
'disconnect_this_display_symbol':(1450, 497),
'keep_changes':(885, 594),
},
'third_screen':{
'second_screen_symbol':(1175, 1707),
'extend_desktop_symbol':(-1176, 1732),
'make_main_display_symbol':(681, 2025),
'first_screen_after_change_symbol':(-1558, 1543),
'disconnect_this_display_symbol':(-1175, 1769),
'keep_changes':(-1371, 411),
}
}
However, when running the second automation, where you only start with two monitors, the coordinates are different, you also have to find these.
button_coordinates = {
'first_screen':{
'first_screen_symbol':(1345, 269),
'extend_desktop_symbol':(-776, 292),
'make_main_display_symbol':(999, 583),
'disconnect_this_display_symbol':(-788, 330)
},
'second_screen':{
'first_screen_symbol':(1404,285),
'extend_desktop_symbol':(1486, 423),
'make_main_display_symbol':(681,585),
'disconnect_this_display_symbol':(1492, 460),
'keep_changes':(877, 591)
},
'third_screen':{
'first_screen_symbol':(-851, 287),
'extend_desktop_symbol':(-788, 419),
'make_main_display_symbol':(-1877,1864),
'disconnect_this_display_symbol':(-788, 455),
'keep_changes':(-1723, 1689)
}
}
Once you actually have these coordinates, most of the hard work is done and the rest is just orchestrating the flow.
Unfortunately, this would be different for any computer monitor setup, meaning that if I changed one of my monitors, or you wanted to clone this repo and apply it to your setup, this step would have to be redone.
Orchestrating the flow
To actually orchestrate the steps we need to know a few different pyautogui functions
pyautogui.press(key) # for pressing a specific key on the keyboard
pyautogui.write(string) # for writing full strings with the keyboard
pyautogui.click(coordinate_tuple) # for clicking the mouse on a specific coordinate
We also need to consider the some of these steps take time, so we need to add certain pauses along the program to ensure things aren't clicked before they're even on screen.
This can be done with the time python library
import time
time.sleep(1) # this will hold for 1 second
We will also want to setup logging so the console will show the steps as they're being executed, and if we encounter any problems, it'll be easier to debug.
import logging
logging.basicConfig(format='%(asctime)s-%(levelname)s-%(message)s', level=logging.DEBUG)
Now to actually start the automations
Automation to disable monitor
- We minimize all the windows to avoid any potential overlaps, issues, etc.
pyautogui.hotkey('win', 'm')
- We open the display settings by pressing the windows key, typing 'Display Settings' and pressing the enter key
pyautogui.press('win') # press windows key on keyboard
time.sleep(0.3)
pyautogui.write('Display Settings')
time.sleep(0.3)
pyautogui.press('enter') # press enter key on keyboard
time.sleep(0.3)
- We get the active window, maximize it, and check the size to determine which screen it opened to. Maximizing the screen also allows the coordinates to be the same every time
# maximize display settings to ensure the coordinates are the same every time
logging.debug('Maximizing display settings window')
display_settings_window = pyautogui.getActiveWindow()
display_settings_window.maximize()
time.sleep(1)
# Determine the monitor based on the window size
logging.debug('Determining in which monitor display settings was opened')
if display_settings_window.size == (2576, 1456):
screen = 'first_screen'
elif display_settings_window.size == (1936, 1096):
screen = 'second_screen'
elif display_settings_window.size == (1295, 735):
screen = 'third_screen'
else:
logging.debug(f'Could not determine screen of size: {str(display_settings_window.size)}')
raise Exception('Could not determine screen')
logging.debug(f'Display settings opened on {screen}')
- We click on the second screen symbol and then click to set it as the main display
# click on the second screen symbol inside of display settings
logging.debug('Finding second screen')
pyautogui.click(button_coordinates[screen]['second_screen_symbol'], interval=0.5)
time.sleep(0.5)
# set it as the main display
logging.debug('Setting second screen as main display')
pyautogui.click(button_coordinates[screen]['make_main_display_symbol'], interval=0.5)
time.sleep(2.5)
- We click back on the first screen symbol, then the dropdown menu to show the options, and then click on 'disconnect this display'. Then we wait a bit longer than usual because disconnecting the display might take a few seconds
# click back on the first screen symbol
logging.debug('Finding first screen')
pyautogui.click(button_coordinates[screen]['first_screen_after_change_symbol'], interval=0.5)
# open the 'extend desktop to this display' menu and disconnect the display
logging.debug('Disconnecting first screen')
pyautogui.click(button_coordinates[screen]['extend_desktop_symbol'], interval=0.5)
pyautogui.click(button_coordinates[screen]['disconnect_this_display_symbol'], interval=0.5)
# wait to make sure the monitor has been disconnected
time.sleep(2.5)
- Check again for the size of the active window, just in case it moved to another monitor after disconnecting the main display, then click on 'keep changes'.
display_settings_window = pyautogui.getActiveWindow()
if display_settings_window.size == (1936, 1096):
screen = 'second_screen'
elif display_settings_window.size == (1295, 735):
screen = 'third_screen'
pyautogui.click(button_coordinates[screen]['keep_changes'])
Automation to reenable monitor
- Steps are repeated to open display settings, maximize window, and check window size. (except we don't need to check if it's opened in the first monitor, since only two are active)
- Click on the first monitor symbol, open the dropdown menu, and click on 'Extend desktop to this display', this will also take a few seconds, so note the time.sleep() timer is longer.
# select the first screen
logging.debug('Selecting first screen')
pyautogui.click(button_coordinates[screen]['first_screen_symbol'])
time.sleep(0.3)
# click on the options menu and select 'extend desktop to this display'
logging.debug('Extending desktop to first display')
pyautogui.click(button_coordinates[screen]['disconnect_this_display_symbol'])
time.sleep(0.3)
pyautogui.click(button_coordinates[screen]['extend_desktop_symbol'])
time.sleep(3)
- Now we do check all 3 monitors to see where the display settings ended up after extending the desktop, and click on 'keep changes'
# determine in which monitor display settings are after extending to first display
display_settings_window = pyautogui.getActiveWindow()
logging.debug('Determining in which monitor display settings was opened')
if display_settings_window.size == (2576, 1456):
screen = 'first_screen'
elif display_settings_window.size == (1936, 1096):
screen = 'second_screen'
elif display_settings_window.size == (1295, 735):
screen = 'third_screen'
else:
logging.debug(f'Could not determine screen of size: {str(display_settings_window.size)}')
raise Exception('Could not determine screen')
logging.debug(f'Display settings opened on {screen}')
# click on keep changes
logging.debug('Clicking on keep changes')
pyautogui.click(button_coordinates[screen]['keep_changes'])
time.sleep(0.3)
- Finally, we make the first monitor the main display, leaving us back where the automation to disable the monitor started.
# make first monitor the main display
logging.debug('Making first monitor the main display')
pyautogui.click(button_coordinates[screen]['make_main_display_symbol'])
time.sleep(0.3)
Now we're actually done with the scripts, but this doesn't end here, of course.
The Implementation
We still need a way to be able to quickly run it for most comfort.
To do this we will create batch scripts in windows to change into the directory of the scripts, use the python executable from the virtual environment to ensure all needed dependencies are included, and run the python script.
Creating batch scripts
Create a directory to hold these scripts, and take note of the path.
I created a file called 'to_xbox.bat'. Meaning I can use it to disable the monitor, and switch TO use my xbox.
@echo off
cd /d "E:\CompSci\automation\PyAutoGUI\xbox_display_settings"
".\.venv\Scripts\python.exe" to_xbox.py
Then I created another file called 'from_xbox.bat'. Meaning that I can use it to reenable the monitor and switch FROM using my xbox.
@echo off
cd /d "E:\CompSci\automation\PyAutoGUI\xbox_display_settings"
".\.venv\Scripts\python.exe" from_xbox.py
Add batch scripts folder to PATH
- Search environment variables in the windows search bar
- Click on environment variables
- Under 'User variables for {username}', double click on 'Path', and on the right side menu click on 'New'. Paste the path of the folder where you created the batch files.
Now you should be able to run your batch files as commands from the command line using the name of the file (excluding the .bat extension)
So now, I can simply press Win + r to open the run utility, type the name of the batch file I need, and it will start running the automation.
That is all, I hope this helps the one person who is also interested in this super niche kind of automation.
P.D. This is the first blog post I write, if you have any feedback on my writing, if I explained too much or too little, I'm always open to constructive criticism.
Also if you have any questions, I'm always more than happy to help where I can.
Posted on June 21, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.