Search code examples
python-3.xwinapipywin32

(WinApi) ChangeDisplaySettingsEx does not work


I'm trying to write a python script to switch the primary monitor. I have 3 Monitors (one is plugged into my i5's graphics chip, and 2 are plugged into a ATI HD7870)

I wrote the following script:

import win32api as w
import win32con as c

i = 0
workingDevices = []

def setPrimary(id):
    global workingDevices
    return w.ChangeDisplaySettingsEx(
        workingDevices[id].DeviceName,
        w.EnumDisplaySettings(
            workingDevices[id].DeviceName,
            c.ENUM_CURRENT_SETTINGS
            ),
        c.CDS_SET_PRIMARY | c.CDS_UPDATEREGISTRY | c.CDS_RESET) \
        == c.DISP_CHANGE_SUCCESSFUL

while True:
    try:
        Device = w.EnumDisplayDevices(None, i, 1)
        if Device.StateFlags & c.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP: #Attached to desktop
            workingDevices.append(Device)

        i += 1
    except:
        break

print("Num Devices: ", len(workingDevices))

for dev in workingDevices:
    print("Name: ", dev.DeviceName)

Invoking it leads to:

In [192]: %run test.py
Num Devices:  3
Name:  \\.\DISPLAY1
Name:  \\.\DISPLAY2
Name:  \\.\DISPLAY7

In [193]: setPrimary(0)
Out[193]: True

In [194]: setPrimary(1)
Out[194]: True

In [195]: setPrimary(2)
Out[195]: True

So far it looks great, but the problem is: nothing changes. My monitors flicker shortly because of the CDS_RESET but the primary screen does not change, although ChangeDisplaySettingsEx returns DISP_CHANGE_SUCCESSFUL

Does anyone have an Idea why? (I use Python 3.5.1 and PyWin32 build 220)

PS I use 1 as the third arg for EnumDisplayDevices because the msdn states it should be set to one, although the PyWin help says it should be set to 0. But the behaviour of the script does not change independent of this value beeing one or zero


Solution

  • Ok, I found the solution. Apperantly the primary monitor must always be at position (0, 0). So when I tried to set another monitor to primary its position was set to (0, 0) which caused it to intersect with the old primary one. It seems the way to go is to update the positions of all Monitors, and write those changes to the registry, and then once this is done apply the changes by calling ChangeDisplaySettingsEx() with default parameters. This is my new (now working) code:

    import win32api as w
    import win32con as c
    
    def load_device_list():
        """loads all Monitor which are plugged into the pc
        The list is needed to use setPrimary
        """
        workingDevices = []
        i = 0
        while True:
            try:
                Device = w.EnumDisplayDevices(None, i, 0)
                if Device.StateFlags & c.DISPLAY_DEVICE_ATTACHED_TO_DESKTOP: #Attached to desktop
                    workingDevices.append(Device)
    
                i += 1
            except:
                return workingDevices
    
    
    def setPrimary(id, workingDevices, MonitorPositions):
        """
        param id: index in the workingDevices list.
                  Designates which display should be the new primary one
    
        param workingDevices: List of Monitors returned by load_device_list()
    
        param MonitorPositions: dictionary of form {id: (x_position, y_position)}
                                specifies the monitor positions
    
        """
    
        FlagForPrimary = c.CDS_SET_PRIMARY | c.CDS_UPDATEREGISTRY | c.CDS_NORESET
        FlagForSec = c.CDS_UPDATEREGISTRY | c.CDS_NORESET
        offset_X = - MonitorPositions[id][0]
        offset_Y = - MonitorPositions[id][1]
        numDevs = len(workingDevices)
    
        #get devmodes, correct positions, and update registry
        for i in range(numDevs):
            devmode = w.EnumDisplaySettings(workingDevices[i].DeviceName, c.ENUM_CURRENT_SETTINGS)
            devmode.Position_x = MonitorPositions[i][0] + offset_X
            devmode.Position_y = MonitorPositions[i][1] + offset_Y
            if(w.ChangeDisplaySettingsEx(workingDevices[i].DeviceName, devmode, 
                FlagForSec if i != id else FlagForPrimary) \
                != c.DISP_CHANGE_SUCCESSFUL): return False
    
        #apply Registry updates once all settings are complete
        return w.ChangeDisplaySettingsEx() == c.DISP_CHANGE_SUCCESSFUL;
    
    if(__name__ == "__main__"):
        devices = load_device_list()
        for dev in devices:
            print("Name: ", dev.DeviceName)
    
        MonitorPositions = {
            0: (0, -1080),
            1: (0, 0),
            2: (1920, 0)
        }
    
        setPrimary(0, devices, MonitorPositions)