Autohotkey: Change Ultrawide 21:9 aspect ratio to 16:9

Solution 1:

Setting dmDisplayFixedOutput to DMDFO_CENTER(docs) might do the trick.
But you might also get black bars on all four sides.
It might also not make any difference. I had really inconsistent results on different displays when trying this. Your monitor settings will surely also play a part here.
But anyway, worth a try I guess.


Lets start off by looking at the _devicemodeA(docs) structure.
I added in the sizes of each member (in bytes) and their offsets:

typedef struct _devicemodeA {                   //  size    |   offset
    BYTE   dmDeviceName[CCHDEVICENAME];         //  32          0
    WORD dmSpecVersion;                         //  2           32
    WORD dmDriverVersion;                       //  2           34
    WORD dmSize;                                //  2           36
    WORD dmDriverExtra;                         //  2           38
    DWORD dmFields;                             //  4           40
    union {
      /* printer only fields */
      struct {
        short dmOrientation;                    //  2           44
        short dmPaperSize;                      //  2           46
        short dmPaperLength;                    //  2           48
        short dmPaperWidth;                     //  2           50
        short dmScale;                          //  2           52
        short dmCopies;                         //  2           54
        short dmDefaultSource;                  //  2           56
        short dmPrintQuality;                   //  2           58
      } DUMMYSTRUCTNAME;
      /* display only fields */
      struct {
        POINTL dmPosition;                      //  8           44
        DWORD  dmDisplayOrientation;            //  4           52
        DWORD  dmDisplayFixedOutput;            //  4           56
      } DUMMYSTRUCTNAME2;
    } DUMMYUNIONNAME;
    short dmColor;                              //  2           60
    short dmDuplex;                             //  2           62
    short dmYResolution;                        //  2           64
    short dmTTOption;                           //  2           66
    short dmCollate;                            //  2           68
    BYTE   dmFormName[CCHFORMNAME];             //  32          70
    WORD   dmLogPixels;                         //  2           102
    DWORD  dmBitsPerPel;                        //  4           104
    DWORD  dmPelsWidth;                         //  4           108
    DWORD  dmPelsHeight;                        //  4           112
    union {
        DWORD  dmDisplayFlags;                  //  4           116
        DWORD  dmNup;                           //  4           116
    } DUMMYUNIONNAME2;
    DWORD  dmDisplayFrequency;                  //  4           120
#if(WINVER >= 0x0400)
    DWORD  dmICMMethod;                         //  4           124
    DWORD  dmICMIntent;                         //  4           128
    DWORD  dmMediaType;                         //  4           132
    DWORD  dmDitherType;                        //  4           136
    DWORD  dmReserved1;                         //  4           140
    DWORD  dmReserved2;                         //  4           144
#if (WINVER >= 0x0500) || (_WIN32_WINNT >= _WIN32_WINNT_NT4)
    DWORD  dmPanningWidth;                      //  4           148
    DWORD  dmPanningHeight;                     //  4           152
#endif                                          //-----
#endif /* WINVER >= 0x0400 */                   //  156 (total)
} DEVMODEA, *PDEVMODEA, *NPDEVMODEA, *LPDEVMODEA;

We're interested in the dmSize, dmFields, dmDisplayFixedOutput, dmPelsWidth and dmPelsHeight members.
On other implementations e.g the dmDeviceName, dmPosition, dmDisplayOrientation and dmDisplayFrequency members could be very interesting as well.


So we start off by creating a DEVMODE structure and filling it with the our display device's current information. This way we we can only change what we want, as opposed to setting every single required piece of information in there.

So we create a variable _DEVMODE and reserve 156 bytes for it.
156 comes from the size we calculated for it above.
The reserving thing is just a thing you have to do for AHK DllCalling, nothing more special to it.

VarSetCapacity(_DEVMODE, 156)

Then we can use the EnumDisplaySettingsA(docs) function to fill the structure.
But first we notice the documentation state:

Before calling EnumDisplaySettings, set the dmSize member to sizeof(DEVMODE)

So lets do that, we know the size to be 156 bytes and we know the offset of the dmSize member to be 36 bytes.
NumPut()(docs) is used to manipulate memory at a certain memory address:

NumPut(156, _DEVMODE, 36)

Then we're ready to call EnumDisplaySettingsA:

DllCall("EnumDisplaySettingsA", Ptr, 0, UInt, ENUM_CURRENT_SETTINGS := -1, UInt, &_DEVMODE)

Parameters:

  • lpszDeviceName: Ptr 0 passed in to indicate NULL(docs), which means use the current display device. A fancier solution could choose here which monitor to use.
  • iModeNum: UInt ENUM_CURRENT_SETTINGS (its value is -1) passed in to indicate using the currently used settings, as opposed to settings stored in the registry.
  • *lpDevMode: UInt &_DEVMODE passed in, which means the pointer of our _DEVMODE structure.
    &(docs) is used to get the pointer.

And now the structure should be successfully filled.
Probably good to test it out, so lets try to get the refresh rate with NumGet()(docs) as a test:

MsgBox, % NumGet(_DEVMODE, 120) " Hz"

Now we can get onto changing the resolution and setting the DMDFO_CENTER flag to hopefully get the desired black bars.

First we set the flags to the dmFields members to indicate we are using some members.
We need to set the DM_PELSHEIGHT, DM_PELSWIDTH and DM_DISPLAYFIXEDOUTPUT flags, since we're changing the height, width, and "fixed output" status.

DM_PELSHEIGHT := 0x00100000
DM_PELSWIDTH := 0x00080000
DM_DISPLAYFIXEDOUTPUT := 0x20000000

; technically + instead of | (bitwise or) would work the same
flags := DM_PELSHEIGHT | DM_PELSWIDTH | DM_DISPLAYFIXEDOUTPUT

Then we can NumPut. We know dmFields to be at offset 40.

NumPut(flags, _DEVMODE, 40)

Then we can NumPut our desired resolution to dmPelsWidth (offset 108) and dmPelsHeight (offset 112):

NumPut(1920, _DEVMODE, 108)
NumPut(1080, _DEVMODE, 112)

And then NumPut to set the DMDFO_CENTER flag to dmDisplayFixedOutput (offset 56):

NumPut(DMDFO_CENTER := 2, _DEVMODE, 56)

Now our structure should be filled with what we want.

Note: If you're changing back to the native resolution of your monitor, don't try to use DMDFO_CENTER or DMDFO_STRETCH flags. Use DMDFO_DEFAULT, or don't use the dmDisplayFixedOutput member at all.


Now all that's left, is just using the ChangeDisplaySettingsA(docs) function to set our changes:

DllCall("ChangeDisplaySettingsA", UInt, &_DEVMODE, UInt, 0)

Again, passing in the pointer of our structure and then UInt 0 flag to indicate changing the display settings immediately.


Full example script

#NoEnv

ENUM_CURRENT_SETTINGS := -1

DM_PELSHEIGHT := 0x00100000
DM_PELSWIDTH := 0x00080000
DM_DISPLAYFIXEDOUTPUT := 0x20000000

DMDFO_DEFAULT := 0
DMDFO_STRETCH := 1
DMDFO_CENTER := 2

VarSetCapacity(_DEVMODE, 156)
NumPut(156, _DEVMODE, 36)           ;dmSize

DllCall("EnumDisplaySettingsA", Ptr, 0, UInt, ENUM_CURRENT_SETTINGS, UInt, &_DEVMODE)
MsgBox, % NumGet(_DEVMODE, 120) " Hz"

; technically + instead of | (bitwise or) would work the same
flags := DM_PELSHEIGHT | DM_PELSWIDTH | DM_DISPLAYFIXEDOUTPUT
NumPut(flags, _DEVMODE, 40)         ;dmFields

NumPut(1920, _DEVMODE, 108)         ;dmPelsWidth
NumPut(1080, _DEVMODE, 112)         ;dmPelsHeight

NumPut(DMDFO_CENTER, _DEVMODE, 56)  ;dmDisplayFixedOutput

DllCall("ChangeDisplaySettingsA", UInt, &_DEVMODE, UInt, 0)

Full example script as a function

#NoEnv

DMDFO_DEFAULT := 0
DMDFO_STRETCH := 1
DMDFO_CENTER := 2

F1::ChangeDisplaySettings(1920, 1080, DMDFO_CENTER)
F2::ChangeDisplaySettings(3840, 1600, DMDFO_DEFAULT)

ChangeDisplaySettings(width, height, DMDFO)
{
    static ENUM_CURRENT_SETTINGS := -1
        , DM_PELSHEIGHT := 0x00100000
        , DM_PELSWIDTH := 0x00080000
        , DM_DISPLAYFIXEDOUTPUT := 0x20000000
    
    VarSetCapacity(_DEVMODE, 156)
    NumPut(156, _DEVMODE, 36)       ;dmSize
    
    DllCall("EnumDisplaySettingsA", Ptr, 0, UInt, ENUM_CURRENT_SETTINGS, UInt, &_DEVMODE)
    
    ; technically + instead of | (bitwise or) would work the same
    flags := DM_PELSHEIGHT | DM_PELSWIDTH | DM_DISPLAYFIXEDOUTPUT
    NumPut(flags, _DEVMODE, 40)     ;dmFields

    NumPut(width, _DEVMODE, 108)    ;dmPelsWidth
    NumPut(height, _DEVMODE, 112)   ;dmPelsHeight

    NumPut(DMDFO, _DEVMODE, 56)     ;dmDisplayFixedOutput

    return DllCall("ChangeDisplaySettingsA", UInt, &_DEVMODE, UInt, 0)
}

Quite long and possibly overly detailed post for something you might not end up using, but meh, I'm avoiding school work.
It was fun to write and maybe at least someone can learn something from it.