How can I change Windows 10 Display Scaling Programmatically using C#
Asked Answered
O

8

33

I'm trying to find a way to change the Display Scaling in Windows 10 Programmatically using C#.

Let me also say that, I'm not trying to create a application that automatically forces the users screen to change resolution/scaling. Its just a tool for me to beable to toggle scales from the tray, as its something I often have to do for testing. So purposely designed for this action.

So, I was able to track down what registry entries (HKEY_CURRENT_USER\Control Panel\Desktop) are set when a User does this manually via the official dialog seen below:

Windows 10 Display Scaling Dialog

However, obviously working with the registry directly means I need to restart the machine to take affect.

I am aware that you can use the Pinvoke to change Screen Resolutions: Setting my Display Resolution

I was wondering if there is a way to change this "%" for a given Screen too? i.e.. my the screen above it says 150%, I'd like to beable to programmatically change it through the full range of 100-500%.

Ology answered 5/2, 2016 at 20:45 Comment(0)
C
45

Here is my learning from the RnD I did on system settings app (immersive control panel). (see my other answer for a simple C++ API I created from this learning - https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c. A simpler method for single monitor setups, or if you want to change DPI of just the prmary monitor is given here - https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c)

  1. System Settings app (new immersive control panel that comes with Windows 10) is able to do it. This means Certainly there is an API, only that Microsoft has not made it public.
  2. The Systems settings app is a UWP app, but can be hooked with a debugger - WinDbg.

I used WinDbg to go through calls made by this app. I found that as soon as a particular function is executed - user32!_imp_NtUserDisplayConfigSetDeviceInfo the new DPI setting takes effect on my machine.

I wasn't able to set a break-point on this function, but was able to set one on DisplayConfigSetDeviceInfo() (bp user32!DisplayConfigSetDeviceInfo).

DisplayConfigSetDeviceInfo (msdn link) is a public function, but it seems that the settings app is sending it parameters which are not documented. Here are the parameters I found during my debugging session.

((user32!DISPLAYCONFIG_DEVICE_INFO_HEADER *)0x55df8fba30)                 : 0x55df8fba30 [Type: DISPLAYCONFIG_DEVICE_INFO_HEADER *]
    [+0x000] type             : -4 [Type: DISPLAYCONFIG_DEVICE_INFO_TYPE]
    [+0x004] size             : 0x18 [Type: unsigned int]
    [+0x008] adapterId        [Type: _LUID]
    [+0x010] id               : 0x0 [Type: unsigned int]
0:003> dx -r1 (*((user32!_LUID *)0x55df8fba38))
(*((user32!_LUID *)0x55df8fba38))                 [Type: _LUID]
    [+0x000] LowPart          : 0xcbae [Type: unsigned long]
    [+0x004] HighPart         : 0 [Type: long]

Basically the values of the members of DISPLAYCONFIG_DEVICE_INFO_HEADER struct which gets passed to DisplayConfigSetDeviceInfo() are:

type : -4
size : 0x18
adapterId : LowPart : 0xcbae HighPart :0

The enum type, as defined in wingdi.h is :

typedef enum
{
      DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME                 = 1,
      DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME                 = 2,
      DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE       = 3,
      DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME                = 4,
      DISPLAYCONFIG_DEVICE_INFO_SET_TARGET_PERSISTENCE          = 5,
      DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_BASE_TYPE            = 6,
      DISPLAYCONFIG_DEVICE_INFO_GET_SUPPORT_VIRTUAL_RESOLUTION  = 7,
      DISPLAYCONFIG_DEVICE_INFO_SET_SUPPORT_VIRTUAL_RESOLUTION  = 8,
      DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO         = 9,
      DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE        = 10,
      DISPLAYCONFIG_DEVICE_INFO_FORCE_UINT32                = 0xFFFFFFFF
} DISPLAYCONFIG_DEVICE_INFO_TYPE;

While the settings app is trying to send -4 for type, we can see that the enum has no negative value.

If we are able to reverse engineer this fully, we will have a working API to set DPI of a monitor.

It seems incredibly unfair that Microsoft has some special API for its own apps, which others cannot use.

UPDATE 1 :

To verify my theory, I copied (using WinDbg), the bytes of the DISPLAYCONFIG_DEVICE_INFO_HEADER struct which are sent to DisplayConfigSetDeviceInfo() as parameter; when DPI scaling is changed from System Settings app (tried setting 150% DPI scaling).

I then wrote a simple C program to send these bytes (24 bytes - 0x18 bytes) to DisplayConfigSetDeviceInfo().
I then changed my DPI scaling back to 100%, and ran my code. Sure enough, the DPI scaling did change on running the code!!!

BYTE buf[] = { 0xFC,0xFF,0xFF,0xFF,0x18,0x00,0x00,0x00,0xAE,0xCB,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00 };
DISPLAYCONFIG_DEVICE_INFO_HEADER* packet = (DISPLAYCONFIG_DEVICE_INFO_HEADER*)buf;
    DisplayConfigSetDeviceInfo(packet);

Note that the same code may not work for you as the LUID, and id parameters, which points to a display on a system would be different (LUID generally is used for GPU, id could be source ID, target ID, or some other ID, this parameter depends on DISPLAYCONFIG_DEVICE_INFO_HEADER::type).

I now have to figure out the meaning of these 24 bytes.

UPDATE 2:

Here are the bytes I got when trying to set 175% dpi scaling.

BYTE buf[] = { 0xFC,0xFF,0xFF,0xFF,0x18,0x00,0x00,0x00,0xAE,0xCB,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00 };

If we compare the two byte buffers, we can draw the following conclusions.

  1. Byte number 21 is being used to specify DPI scaling, as all other bytes are same between 150%, and 175%.
  2. For 150% scaling, the value of Byte 21 is 1, while for 175% it is 2. The default (recommended) DPI scaling for this monitor is 125%.
  3. From the technet article mentioned by @Dodge, in Windows parlance, 0 corresponds to recommended DPI scaling value. Other integers correspond to relative dpi scaling with respect to this recommended value. 1 means one step ahead in scaling, -1 means one step down. eg. if recommended is 125%, a value of 1 would mean 150% scaling. This is indeed what we saw.

The only thing remaining is now to figure out how to get recommended DPI scaling value for a display, we will then be able to write an API of the following form - SetDPIScaling(monitor_LUID, DPIScale_percent).

UPDATE 3:

If we check the registry entries mentioned in @Dodge's answer, we come to know that these integers are stored as DWORD, and since my computer is little endian it implies that the last 4 bytes (bytes 21 to 24) are being used for them.Thus to send negative numbers we will have to use 2's complement of the DWORD, and write the bytes as little endian.

UPDATE 4:

I have also been researching on how Windows tries to generate Monitor Ids for storing DPI scaling values. For any monitor, the DPI scaling value selected by a user is stored at :

HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\
*MonitorID*

For a Dell display connected to my machine, the monitor ID was DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297. I was able to figure out the structure of monitor ID. I verified my theory with 4 different monitors.

For the Dell display (dpi scaling stored at HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\ DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297), it is as follows (Sorry for adding image, couldn't figure out a way to represent the information as succinctly).

Monitor ID image windows 10 x64 17763, 18362

Essentially, the data required from EDID to construct monitor ID is as follows.

  1. Manufacturer ID
    • Bytes 8, 9 of EDID (big endian).
    • Eg. for the Dell display, the EDID has 10AC for these bytes. Except bit 15, use rest of the 15 bits (bits 0 to 14), 5 at a time. (10AC)16 equals (0001-0000-1010-1100)2. Breaking this binary into chunks of 5 bits, starting from LSB gives us (0-00100-00101-01100)2. Converting each chunk to decimal, (0-4-5-12)10, now 'D' is 4th alphabet, 'E' is 5th, and 'L' is 12th.
    • Fallback : @@@
  2. Product ID
    • Bytes 10, 11 of EDID (little endian)
    • Eg. for the Dell display, the EDID has BCA0. Since this is little endian, simply converting it to A0BC gives us product ID.
    • Fallback : 000
  3. Serial number
    • DTD serial number is used. Base block of EDID (first 128 bytes) has 4 blocks of data called DTD. They can either be used to store timing information, or arbitrary data. The 4 DTD blocks are located at bytes 54, 72, 90, and 108. The DTD block which has serial number has first 2 bytes (byte 0, and 1) as zero, 2nd bytes also as zero, and 3rd byte as 0xFF. 4th is again zero. Byte 5 onward has serial number in ASCII. The serial number can occupy a maximum of 13 bytes (byte 5 to 17 of the DTD block). If Serial number is less than 13 characters (13 bytes), then it would be terminated by Line Feed (0x0A).
    • For the Dell display, it was 00-00-00-FF-00-39-44-52-58-56-36-38-41-30-4C-57-4C-0A. Note that the serial number has 12 bytes, and is terminated by line feed (0x0A). Converting 39-44-52-58-56-36-38-41-30-4C-57-4C to ASCII gives us 9DRXV68A0LWL.
    • Fallback : serial number at byte 12 of EDID. EDID can store Serial number at 2 places, if the DTD block EDID is not found, OS uses the serial number present at bytes 12 to 15 (32 bits little endian). For the Dell display it is (4C-57-4C-30)16, since little endian, the serial number is (304C574C)16, which is ‭(810309452‬)10. OS will use this value (in base 10 as a fallback) If even this is not present, then 0 is used.
  4. Manufacture week
    • Byte 16 of EDID (can have some variations, see Wikipedia article)
    • For the Dell display it is (21)16.
    • Fallback : 00
  5. Manufacture year
    • Byte 17 of EDID
    • Year of manufacture since 1990. Add 1990 to value at byte 17.
    • For the Dell display it is (1A)16. (1A)16 + (1990)10 = (07C6)16
    • Fallback : 0000
  6. Edid base block checksum
    • Byte 127 of EDID
    • From Wikipedia - Checksum. Sum of all 128 bytes should equal 0 (mod 256).
    • No fallback. A valid EDID has to have this value.

Note that only first 128 bytes of EDID is ever required.

A note on fallback

If some of the data required for constructing monitor ID are not present, then OS uses fallback. The fallback for each of the datum required for constructing the monitor ID, as I observed on my Windows 10 machine are given in the list above. I manually edited the EDID of my DELL display (link1 link2, link3 - beware - the method suggested in link 3 may damage your system, proceed only if sure; Link1 is most recommended) to remove all 6 items given above, the monitor ID which OS constructed for me (without MD5 suffix) was @@@0000810309452_00_0000_85, when I even removed the serial number at byte 12, the monitor ID constructed was @@@00000_00_0000_A4.

UPDATE 4:

DPI scaling is a property of source, and not of target, hence the id parameter used in DisplayConfigGetDeviceInfo(), and DisplayConfigSetDeviceInfo() is the source ID, and not the target ID.

The registry method suggested above should work fine in most cases, but has 2 drawbacks. One is that it doesn't give us parity with system settings app (in terms of the time at which settings are effected). Secondly in some rare cases (not able to repro any more) I have seen that the Monitor ID string generated by OS is slightly different - it has more components that shown in the pic above.

I have successfully created an API which we can use to get/set DPI scaling in exactly the same way, as done by system settings app. Will post in a new answer, as this is more about the approach I took for finding a solution.

UPDATE 5:

The algorithm for generating unique string (monitor IDs) storing DPI of a display is unknown to us but DisplayConfigGetDeviceInfo can be used with another undocumented parameter: -7, to construct this, as explained here - https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c

Cassilda answered 7/8, 2019 at 14:45 Comment(0)
C
17

C++ API to Get/Set DPI.

I was able to reverse engineer system settings app, and come up with an API. The code for it is present in my github repo https://github.com/lihas/windows-DPI-scaling-sample.

I have skipped explaining a lot of terminologies in this answer, since I have already done so in my previous answer for this question (https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c).

A summary of the API

  1. Class : DpiHelper
  2. Methods :
    1. GetDPIScalingInfo(), and
    2. SetDPIScaling()
Getting DPI information of a display


Call DPIScalingInfo() with adapterID, and sourceID.

DpiHelper::DPIScalingInfo DpiHelper::GetDPIScalingInfo(LUID adapterID, UINT32 sourceID)
Setting DPI of a display


Call SetDPIScaling() with adapterID, sourceID, and percentage DPI scaling to set. For eg. if you want to set DPI scaling for a source to 175%, pass 175 in the last parameter.

bool DpiHelper::SetDPIScaling(LUID adapterID, UINT32 sourceID, UINT32 dpiPercentToSet)

DpiHelper.h in the repo has thorough documentation of these 2 methods.

Also read documentation in DpiHelper.h, and README of the repo. I have released all the code in the repo in public domain, so use it in whatever way you want.

Sample app

I have also created an MFC app which uses this helper library to get/set DPI scaling. This will help you understand how to use the DpiHelper class.

Here is how it looks.

MFC app to get/set DPI

Note on DPI scaling on windows

  1. DPI scaling is property of the source and not of target (see ViPN for these terminologies).
  2. DPI scaling of a display is dependent on 3 factors - resolution, physical size of display, expected viewing distance. The exact formula which Windows uses to arrive at recommended value is unknown.
  3. In OS parlance DPI scaling values have a meaning when compared against the recommended DPI scaling of a display. Thus although we see 100%, 125%, etc. in system settings app, the OS doesn't understand scaling in percentages. Instead number of steps above, or below recommended scaling is used. For eg. a DPI scaling value of -1 would mean 1 step lower than the recommended DPI scaling. So if for a monitor the recommended value is 150%, -1 would mean 125%.

I used WinDbg Preview (MS Store), and Ghidra to do the reverse engineering. There was a point when I was about to give up for the lack of IDA Pro license, when someone suggested me Ghidra. I have been a fan ever since.

A big thanks to Ghidra!!!

Cassilda answered 23/9, 2019 at 16:38 Comment(1)
The GitHub code tested on Windows 10 1903 with VS2015, greate staffSweeping
C
8

If system wide DPI scaling is to be changed (System DPI scaling - scaling of primary monitor in case of multi monitor setup or there is only a single monitor), rather than per monitor DPI scaling, SystemParametersInfo() can be used.

This API has an undocumented parameter which achieves this : SPI_SETLOGICALDPIOVERRIDE

From Microsoft doc:

SPI_SETLOGICALDPIOVERRIDE   Do not use. 
0x009F

Usage :

SystemParametersInfo(SPI_SETLOGICALDPIOVERRIDE, relativeIndex, (LPVOID)0, 1);

To figure out what value to use for relativeIndex variable above, you have to understand how OS expects DPI scaling values to be specified (explained here).

In brief, relativeIndex tells how many steps above, or below the recommended DPI scaling value you want to go. For eg. if recommended DPI scaling value is 125%, and you want to set 150% as scaling, relativeIndex will be 1 (one step over 125%), or if you want to set 100%, relativeIndex will be -1 (one step below 125%).

All steps may not be of same size.

100,125,150,175,200,225,250,300,350, 400, 450, 500

Till 250% the steps increase in units of 25%, and after than in units of 50%.

Thus, you must first get the value of recommended DPI scaling, for which same API can be used, with SPI_GETLOGICALDPIOVERRIDE parameter.

SystemParametersInfo(SPI_GETLOGICALDPIOVERRIDE, 0, (LPVOID)&dpi, 1);

The value returned in dpi variable above is also to be understood in a special way. The value will be negative, and its magnitude will indicated the index of DPI scaling percentage in the list above.

So if this API returns -1, the recommended DPI scaling value will be 125%.

Sample code:

#include <iostream>
#include <Windows.h>

using namespace std;


static const UINT32 DpiVals[] = { 100,125,150,175,200,225,250,300,350, 400, 450, 500 };

/*Get default DPI scaling percentage.
The OS recommented value.
*/
int GetRecommendedDPIScaling()
{
    int dpi = 0;
    auto retval = SystemParametersInfo(SPI_GETLOGICALDPIOVERRIDE, 0, (LPVOID)&dpi, 1);

    if (retval != 0)
    {
        int currDPI = DpiVals[dpi * -1];
        return currDPI;
    }

    return -1;
}

void SetDpiScaling(int percentScaleToSet)
{
    int recommendedDpiScale = GetRecommendedDPIScaling();

    if (recommendedDpiScale > 0)
    {
        int index = 0, recIndex = 0, setIndex = 0 ;
        for (const auto& scale : DpiVals)
        {
            if (recommendedDpiScale == scale)
            {
                recIndex = index;
            }
            if (percentScaleToSet == scale)
            {
                setIndex = index;
            }
            index++;
        }
        
        int relativeIndex = setIndex - recIndex;
        SystemParametersInfo(SPI_SETLOGICALDPIOVERRIDE, relativeIndex, (LPVOID)0, 1);
    }
}

int main()
{
    for (;;)
    {
        int n = 0, dpiToSet = 0;
        cout << R"(
            1. Show Recommended DPI
            2. Set DPI
            Anything else to exit
)";
        cin >> n;
        switch (n)
        {
        case 1:
            cout << "recommened scaling: " << GetRecommendedDPIScaling() << "%" << endl;
            break;
        case 2:
            cout << "enter scaling to set in percentage" << endl;
            cin >> dpiToSet;
            SetDpiScaling(dpiToSet);
            break;
        default:
            exit(0);
            break;
        }
    }
    return 0;
}

Source code: https://github.com/lihas/windows-DPI-scaling-sample.

Here is a sample run. console app - SystemParametersInfo() sample run

Pros and cons
With respect to my earlier approaches (https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c, https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-c)

Pros

  1. This is a very simple API. Hence when you only have to change DPI scaling of primary monitor in a multi monitor setup, or if there is only a single monitor, prefer this approach.

Cons

  1. Cannot set DPI scaling of non primary monitors on a multi monitor setup.
  2. Doesn't return currently applied DPI scaling (though you can use other OS API for it)
  3. Doesn't give you max, min possible DPI scaling values. Though if you try to set outside this range, OS doesn't allow it, and will use nearest allowed.

References

  1. https://social.msdn.microsoft.com/Forums/vstudio/en-US/3259c521-b3ed-4121-97da-70a08fb8bb19/change-setting?forum=windowsgeneraldevelopmentissues (slightly inaccurate)
  2. How to set Windows scale and layout with python code
  3. https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa?redirectedfrom=MSDN
  4. https://github.com/lihas/windows-DPI-scaling-sample
Cassilda answered 15/7, 2020 at 13:56 Comment(2)
Do you have any idea why the documentation says Do not use for SPI_SETLOGICALDPIOVERRIDE? Presumably (at best) this can't be relied upon and may change at any future point? Or is it broken in some way?Versicolor
@Versicolor I am not sure why they have written "Do not use". In addition to points made by you another reason I can think of is that in perhaps MS doesn't want random applications messing with display settings, but then DisplayConfigSetDeviceInfo, and SetDisplayConfig don't have this written so I may be wrong.Cassilda
C
5

While searching for exactly the same, i found your question and found a possible solution.

I found that a per monitor toggle for this % value is in registry at Computer\HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\*monitorId*\DpiValue. it looks like that the meaning of the value depends on the screen (size and dpi) see this reddit post for details.

For my 24" 1080p screen 0 means 100% and 1 means 125%. This Technet Article seems to be explainig the values a bit.

Unfortunately it is not enough to change the registry value. but you can refresh the dpi by changing the resolution after writing to the registry.

the following code sets the dpi and then switches resolution low and back high to trigger the dpi update.

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using Microsoft.Win32;

namespace SetDpiScale
{
    public partial class Form1 : Form
    {
        public enum DMDO
        {
            DEFAULT = 0,
            D90 = 1,
            D180 = 2,
            D270 = 3
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        struct DEVMODE
        {
            public const int DM_PELSWIDTH = 0x80000;
            public const int DM_PELSHEIGHT = 0x100000;
            private const int CCHDEVICENAME = 32;
            private const int CCHFORMNAME = 32;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
            public string dmDeviceName;
            public short dmSpecVersion;
            public short dmDriverVersion;
            public short dmSize;
            public short dmDriverExtra;
            public int dmFields;

            public int dmPositionX;
            public int dmPositionY;
            public DMDO dmDisplayOrientation;
            public int dmDisplayFixedOutput;

            public short dmColor;
            public short dmDuplex;
            public short dmYResolution;
            public short dmTTOption;
            public short dmCollate;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)]
            public string dmFormName;
            public short dmLogPixels;
            public int dmBitsPerPel;
            public int dmPelsWidth;
            public int dmPelsHeight;
            public int dmDisplayFlags;
            public int dmDisplayFrequency;
            public int dmICMMethod;
            public int dmICMIntent;
            public int dmMediaType;
            public int dmDitherType;
            public int dmReserved1;
            public int dmReserved2;
            public int dmPanningWidth;
            public int dmPanningHeight;
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern int ChangeDisplaySettings([In] ref DEVMODE lpDevMode, int dwFlags);

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ChangeDPI(0); // 100%
        }
        private void button2_Click(object sender, EventArgs e)
        {
            ChangeDPI(1); // 125%
        }

        void ChangeDPI(int dpi)
        {
            RegistryKey key = Registry.CurrentUser.OpenSubKey("Control Panel", true);

            key = key.OpenSubKey("Desktop", true);
            key = key.OpenSubKey("PerMonitorSettings", true);
            key = key.OpenSubKey("*monitor id where to change the dpi*", true); // my second monitor here

            key.SetValue("DpiValue", dpi);

            SetResolution(1920, 1080); // this sets the resolution on primary screen
            SetResolution(2560, 1440); // returning back to my primary screens default resolution
        }

        private static void SetResolution(int w, int h)
        {
            long RetVal = 0;

            DEVMODE dm = new DEVMODE();

            dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));

            dm.dmPelsWidth = w;
            dm.dmPelsHeight = h;

            dm.dmFields = DEVMODE.DM_PELSWIDTH | DEVMODE.DM_PELSHEIGHT;


            RetVal = ChangeDisplaySettings(ref dm, 0);
        }
    }
}
Coenobite answered 21/4, 2017 at 19:5 Comment(3)
The problem with this solution is that I do not see on any win 10 computer PerMonitorSettings in the registry, or I can see only one monitor. The solution with changing resolution does not work with global LogPixels setting.Roadability
The process didn't work for me at first. It turns out, you have to first change the scaling factor manually, before the registry subkeys even show up, for you to then change programmatically. Microsoft... Why...Claudineclaudio
Not easy to get it work on Windows 10. My scale is 300%, after change 0 to 1, it becomes 350 and the UI doesn't change.Sweeping
P
1

As addition to Sahil Singhs answer. The MonitorIDs can be located as subkeys under: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors

Panorama answered 26/2, 2020 at 7:49 Comment(1)
AFAIK the registry-based mechanism work fine, but they may require signout and sign in or reboot so that registry values are read and change is effected.Cassilda
C
1

To update the DPI scaling value of a display monitor we need to set DpiValue registry value data. This registry value is located within HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors key, within a sub key which is unique to each monitor.

For example, for my Dell laptop screen it is:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors\DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297, (REG_DWORD) DpiValue

monitor unique regkey

To get the unique sub key name for each monitor eg. (DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297), we can use yet another undocumented parameter of DisplayConfigGetDeviceInfo.
When the type parameter is -7, it gives us monitorUniqueName, which forms the part before "^" in the monitor key name, the part after it is simply the MD5 hash of the former.

std::wstring DpiHelper::GetDisplayUniqueName(LUID adapterID, UINT32 targetID)
{
    _DISPLAYCONFIG_GET_MONITOR_INTERNAL_INFO mi = {};
    mi.header.adapterId = adapterID;
    mi.header.id = targetID;
    mi.header.size = sizeof(mi);
    mi.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE)-7;

    LONG res = ::DisplayConfigGetDeviceInfo(&mi.header);
    if (ERROR_SUCCESS == res)
    {
        return std::wstring(mi.monitorUniqueName);
    }

    return std::wstring();
}

The definition of the structs used can be found here - https://github.com/lihas/WindowsUndocumentedLib

Sample app generating this unique string can be found here - https://github.com/lihas/windows-DPI-scaling-sample

enter image description here

enter image description here

The value you set in DpiValue reg data is relative to recommended DPI scaling, so if recommended scaling is 250%, and you want to set new scaling as 225%, set DpiValue to 0xffffffff which is DWORD(-1), similarly for 300% it would be 1.

Changing DPI scaling via regkey, requires a logout-and-login of user for changes to take affect. One way to use this undocumented API is to first get monitorUniqueName with it, then take its MD5 hash and concat it with the monitorUniqueName with an "^" between the two. Create the regkey with this resultand string under Computer\HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\, and set the DpiValue data. Then log out the current user and log back in (or perform a reboot).

Cassilda answered 28/7, 2023 at 10:25 Comment(0)
S
0

Here is my code based on @Sahil Singh's:

Dll project to wrap the C++ API:

stdafx.h:

#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>

// reference additional headers your program requires here
#ifdef __cplusplus
extern "C" {
#endif
    extern __declspec(dllexport) void PrintDpiInfo();
    extern __declspec(dllexport) void SetDPIScaling(INT32 adapterIDHigh, UINT32 adapterIDlow, UINT32 sourceID, UINT32 dpiPercentToSet);
    extern __declspec(dllexport) void RestoreDPIScaling();
#ifdef __cplusplus
}
#endif

DpiHelper.cpp:

// DpiHelper.cpp : Defines the exported functions for the DLL application.
//

#include "stdafx.h"
#include "DpiHelper.h"
#include <memory>
#include <cassert>
#include <string>
#include <map>

bool DpiHelper::GetPathsAndModes(std::vector<DISPLAYCONFIG_PATH_INFO>& pathsV, std::vector<DISPLAYCONFIG_MODE_INFO>& modesV, int flags)
{
    UINT32 numPaths = 0, numModes = 0;
    auto status = GetDisplayConfigBufferSizes(flags, &numPaths, &numModes);
    if (ERROR_SUCCESS != status)
    {
        return false;
    }

    std::unique_ptr<DISPLAYCONFIG_PATH_INFO[]> paths(new DISPLAYCONFIG_PATH_INFO[numPaths]);
    std::unique_ptr<DISPLAYCONFIG_MODE_INFO[]> modes(new DISPLAYCONFIG_MODE_INFO[numModes]);
    status = QueryDisplayConfig(flags, &numPaths, paths.get(), &numModes, modes.get(), nullptr);
    if (ERROR_SUCCESS != status)
    {
        return false;
    }

    for (unsigned int i = 0; i < numPaths; i++)
    {
        pathsV.push_back(paths[i]);
    }

    for (unsigned int i = 0; i < numModes; i++)
    {
        modesV.push_back(modes[i]);
    }

    return true;
}


DpiHelper::DpiHelper()
{
}


DpiHelper::~DpiHelper()
{
}


DpiHelper::DPIScalingInfo DpiHelper::GetDPIScalingInfo(LUID adapterID, UINT32 sourceID)
{
    DPIScalingInfo dpiInfo = {};

    DpiHelper::DISPLAYCONFIG_SOURCE_DPI_SCALE_GET requestPacket = {};
    requestPacket.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE)DpiHelper::DISPLAYCONFIG_DEVICE_INFO_TYPE_CUSTOM::DISPLAYCONFIG_DEVICE_INFO_GET_DPI_SCALE;
    requestPacket.header.size = sizeof(requestPacket);
    assert(0x20 == sizeof(requestPacket));//if this fails => OS has changed somthing, and our reverse enginnering knowledge about the API is outdated
    requestPacket.header.adapterId = adapterID;
    requestPacket.header.id = sourceID;

    auto res = ::DisplayConfigGetDeviceInfo(&requestPacket.header);
    if (ERROR_SUCCESS == res)
    {//success
        if (requestPacket.curScaleRel < requestPacket.minScaleRel)
        {
            requestPacket.curScaleRel = requestPacket.minScaleRel;
        }
        else if (requestPacket.curScaleRel > requestPacket.maxScaleRel)
        {
            requestPacket.curScaleRel = requestPacket.maxScaleRel;
        }

        std::int32_t minAbs = abs((int)requestPacket.minScaleRel);
        if (DpiHelper::CountOf(DpiVals) >= (size_t)(minAbs + requestPacket.maxScaleRel + 1))
        {//all ok
            dpiInfo.current = DpiVals[minAbs + requestPacket.curScaleRel];
            dpiInfo.recommended = DpiVals[minAbs];
            dpiInfo.maximum = DpiVals[minAbs + requestPacket.maxScaleRel];
            dpiInfo.bInitDone = true;
        }
        else
        {
            //Error! Probably DpiVals array is outdated
            return dpiInfo;
        }
    }
    else
    {
        //DisplayConfigGetDeviceInfo() failed
        return dpiInfo;
    }

    return dpiInfo;
}

std::wstring GetTargetName(LUID adapterLUID, UINT32 sourceId)
{
    std::vector<DISPLAYCONFIG_PATH_INFO> pathsV;
    std::vector<DISPLAYCONFIG_MODE_INFO> modesV;
    int flags = QDC_ONLY_ACTIVE_PATHS;
    if (false == DpiHelper::GetPathsAndModes(pathsV, modesV, flags))
    {
        wprintf(L"DpiHelper::GetPathsAndModes() failed\r\n");
    }

    for (const auto& path : pathsV)
    {

        if (adapterLUID.LowPart == path.targetInfo.adapterId.LowPart
            && adapterLUID.HighPart == path.targetInfo.adapterId.HighPart
            && sourceId == path.sourceInfo.id)
        {
            DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName;
            deviceName.header.size = sizeof(deviceName);
            deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
            deviceName.header.adapterId = adapterLUID;
            deviceName.header.id = path.targetInfo.id;
            if (ERROR_SUCCESS != DisplayConfigGetDeviceInfo(&deviceName.header))
            {
                wprintf(L"DisplayConfigGetDeviceInfo() failed\r\n");
            }
            else
            {

                std::wstring nameString = deviceName.monitorFriendlyDeviceName;
                if (DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL == deviceName.outputTechnology)
                {
                    nameString += L"(internal display)";
                }
                return nameString;
            }
        }

    }
    return L"N/A";

}



void printOne(LUID adapterLUID, UINT32 sourceID) {
    wprintf(L"GPU=%ld.%u,Desktop_Index_In_GPU=%d,Monitor=%ls\r\n"
        ,adapterLUID.HighPart
        , adapterLUID.LowPart
        , sourceID
        , GetTargetName(adapterLUID, sourceID).data());
}



bool DpiHelper::SetDPIScaling(LUID adapterID, UINT32 sourceID, UINT32 dpiPercentToSet)
{

    wprintf(L"setting dpi scale to %d: ", dpiPercentToSet);
    printOne(adapterID, sourceID);
    DPIScalingInfo dPIScalingInfo = GetDPIScalingInfo(adapterID, sourceID);

    if (dpiPercentToSet == dPIScalingInfo.current)
    {
        return true;
    }

    if (dpiPercentToSet < dPIScalingInfo.mininum)
    {
        dpiPercentToSet = dPIScalingInfo.mininum;
    }
    else if (dpiPercentToSet > dPIScalingInfo.maximum)
    {
        dpiPercentToSet = dPIScalingInfo.maximum;
    }

    int idx1 = -1, idx2 = -1;

    int i = 0;
    for (const auto& val : DpiVals)
    {
        if (val == dpiPercentToSet)
        {
            idx1 = i;
        }

        if (val == dPIScalingInfo.recommended)
        {
            idx2 = i;
        }
        i++;
    }

    if ((idx1 == -1) || (idx2 == -1))
    {
        //Error cannot find dpi value
        return false;
    }

    int dpiRelativeVal = idx1 - idx2;

    DpiHelper::DISPLAYCONFIG_SOURCE_DPI_SCALE_SET setPacket = {};
    setPacket.header.adapterId = adapterID;
    setPacket.header.id = sourceID;
    setPacket.header.size = sizeof(setPacket);
    assert(0x18 == sizeof(setPacket));//if this fails => OS has changed somthing, and our reverse enginnering knowledge about the API is outdated
    setPacket.header.type = (DISPLAYCONFIG_DEVICE_INFO_TYPE)DpiHelper::DISPLAYCONFIG_DEVICE_INFO_TYPE_CUSTOM::DISPLAYCONFIG_DEVICE_INFO_SET_DPI_SCALE;
    setPacket.scaleRel = (UINT32)dpiRelativeVal;

    auto res = ::DisplayConfigSetDeviceInfo(&setPacket.header);
    if (ERROR_SUCCESS == res)
    {
        return true;
    }
    else
    {
        return false;
    }
    return true;
}


#define MAX_ID  10
LUID GpuId[MAX_ID];
UINT32 DesktopIndexInGpu[MAX_ID];
UINT32 oldDPI[MAX_ID];


void PrintDpiInfo() {



    std::vector<DISPLAYCONFIG_PATH_INFO> pathsV;
    std::vector<DISPLAYCONFIG_MODE_INFO> modesV;
    int flags = QDC_ONLY_ACTIVE_PATHS;
    if (false == DpiHelper::GetPathsAndModes(pathsV, modesV, flags))
    {
        wprintf(L"DpiHelper::GetPathsAndModes() failed");
    }

    int i = 0;
    for (const auto& path : pathsV)
    {
        //get display name
        auto adapterLUID = path.targetInfo.adapterId;       
        auto sourceID = path.sourceInfo.id;
        std::wstring monitor_name = GetTargetName(adapterLUID, sourceID);
        printOne(adapterLUID, sourceID);

        DpiHelper::DPIScalingInfo dpiInfo = DpiHelper::GetDPIScalingInfo(adapterLUID, sourceID);

        GpuId[i] = adapterLUID;
        DesktopIndexInGpu[i] = sourceID;
        oldDPI[i] = dpiInfo.current;


        wprintf(L"Available DPI:\r\n");
        int curdpi = 0;
        for (const auto& dpi : DpiVals)
        {
            if ((dpi >= dpiInfo.mininum) && (dpi <= dpiInfo.maximum))
                wprintf(L"    %d\r\n",dpi);
        }
        wprintf(L"    current DPI: %d\r\n",dpiInfo.current);

        i++;
        if (i >= MAX_ID) {
            wprintf(L"To many desktops\r\n");
            break;
        }
    }


}

void SetDPIScaling(INT32 adapterIDHigh, UINT32 adapterIDlow, UINT32 sourceID, UINT32 dpiPercentToSet) {
    LUID adapterId;
    adapterId.HighPart = adapterIDHigh;
    adapterId.LowPart = adapterIDlow;   
    DpiHelper::SetDPIScaling(adapterId, sourceID, dpiPercentToSet);
}

void RestoreDPIScaling() 
{
    wprintf(L"Now restore DPI settings...\r\n");
    for (int i = 0;i < MAX_ID;i++) {
        if (GpuId[i].LowPart == 0 && GpuId[i].HighPart==0) break;
        DpiHelper::SetDPIScaling(GpuId[i], DesktopIndexInGpu[i], oldDPI[i]);
    }

}

DpiHelper.h is the same as the referenced answer. Create an C++ Dll project in Visual studio and add/put in the above code and use the dll in the bellow C# application.

A C# console application that set DPI according to command line parameters and restore them when press any key:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace DispSetEx
{
    class Program
    {



        [DllImport("DpiHelper.dll")]
        static public extern void PrintDpiInfo();

        [DllImport("DpiHelper.dll")]
        static public extern int SetDPIScaling(Int32 adapterIDHigh, UInt32 adapterIDlow, UInt32 sourceID, UInt32 dpiPercentToSet);
        [DllImport("DpiHelper.dll")]
        static public extern void RestoreDPIScaling();

        static void Main(string[] args)
        {
            if ((args.Length % 3) != 0)
            {
                Console.WriteLine("wrong parameters");
                return;
            }

//print the DPI info, you need to set the command line parameters
//according to this
            PrintDpiInfo();

    //commandline parameters should be of groups of three
    //each groups's tree paramters control a desktop's setting
    //in each group:
    //GPUIdhigh.GPUIdlow DesktopIndexInGPU DPIScalingValue
    //for example:
    //    0.1234 0 100 //set the DPI scaling to 100 for desktop 0 on GPU 0.1234
    //    0.4567 0 125 //set the DPI scaling to 125 for desktop 0 on GPU 0.5678
    //    0.4567 1 150 //set the DPI scaling to 150 for desktop 1 on GPU 0.5678
    //in most cases GPUIdhigh is 0.
    //you can use the monitor name to identify which is which easily
    //you need to set the command line parameters according to the result of PrintDpiInfo
    //e.g. you should only set the DPI scaling to a value that is supported by 
    //that desktop. 


            for (int i = 0; i < args.Length / 3; i++)
            {
                string[] sa = args[i * 3].Split(new char[] { '.' });

                Int32 adapterHigh = Int32.Parse(sa[0]);
                UInt32 adapterLow = UInt32.Parse(sa[1]);
                UInt32 source = UInt32.Parse(args[i * 3 + 1]);
                UInt32 dpiscale = UInt32.Parse(args[i * 3 + 2]);

                SetDPIScaling(adapterHigh, adapterLow, source,dpiscale);
            }

            Console.WriteLine("Press any key to resotre the settings...");
            Console.ReadKey();

            RestoreDPIScaling();  
        }
    }
}
Sweeping answered 26/10, 2019 at 6:11 Comment(2)
Does this work? the method SetDPIScaling() seems to have different signatures.Abyssal
@ Steve Smith This code is self consistent, the original work has been modified (including signatures) to be wrapped in C#. Don't check the original work.Sweeping
T
0

I wanted to be able to toggle the display scaling for the built-in monitor of my notebook depending on whether I use it standalone (100%) or on my desk with two larger monitors (125%).

After a lot of frustrating powershell with inline c# switching to python and ctypes and implementing what i learned there made me happy again.

I share the script if someone comes here with the same goal. Note that setting the scaling for a monitor different from the first will not work this way

#!/usr/bin/env python
import logging
import ctypes

# Syntactic sugar
from ctypes import CFUNCTYPE as typedef
from ctypes import POINTER as Ptr
from ctypes import c_bool as BOOL
from ctypes import c_int as HRESULT
from ctypes import c_uint as UINT
from ctypes import c_void_p as HDC
from ctypes import c_void_p as HMONITOR
from ctypes import c_void_p as LPRECT
from ctypes import c_void_p as LPARAM
from ctypes import c_void_p as PVOID

user32 = ctypes.windll.user32
shcore = ctypes.windll.shcore
def dlopen(rtype, Fn, args): Fn.restype = rtype; Fn.argtypes = args; return Fn

# https://learn.microsoft.com/en-us/windows/win32/api/shtypes/ne-shtypes-device_scale_factor
from ctypes import c_int as DEVICE_SCALE_FACTOR

# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-monitorenumproc
MonitorEnumProc = typedef(BOOL, * (HMONITOR, HDC, LPRECT, LPARAM))
"""BOOL MonitorEnumProc(HMONITOR hMon, HDC hdc, LPRECT lprcClip, LPARAM dwData);
"""

# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
EnumDisplayMonitors = dlopen(BOOL, user32.EnumDisplayMonitors, (HDC, LPRECT, MonitorEnumProc, LPARAM))
"""BOOL EnumDisplayMonitors(HDC hdc, LPCRECT lprcClip, MONITORENUMPROC lpfnEnum, LPARAM dwData);
"""

# https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getscalefactorformonitor
GetScaleFactorForMonitor = dlopen(HRESULT, shcore.GetScaleFactorForMonitor, (HMONITOR, Ptr(DEVICE_SCALE_FACTOR)))
"""HRESULT GetScaleFactorForMonitor(HMONITOR hMon, DEVICE_SCALE_FACTOR *pScale);
"""

# https://learn.microsoft.com/de-de/windows/win32/api/winuser/nf-winuser-systemparametersinfow
SystemParametersInfoW = dlopen(BOOL, user32.SystemParametersInfoW, (UINT, UINT, PVOID, UINT))
"""BOOL SystemParametersInfoW(UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni);
"""


def get_display_infos():
    """Returns a list of tuples (monitor handle, DEVICE_SCALE_FACTOR)"""
    results = []
    def callback(hmon, hdc, rect, param):
        scaling = DEVICE_SCALE_FACTOR(0)
        GetScaleFactorForMonitor(hmon, ctypes.byref(scaling))
        results.append((hmon, scaling.value))
        return True
    res = EnumDisplayMonitors(0, 0, MonitorEnumProc(callback), 0)
    logging.debug('enum %s: %s', res, results)
    return results


def get_scale_factor_for_monitor(monitor):
    """Get monitor scale factor in fixed percentage: 100%, 125%, 150%, ..."""
    infos = get_display_infos()
    return infos[monitor][1]


def set_scale_factor_for_monitor(monitor, scale_factor):
    """Set monitor scale factor to fixed percentage: 100%, 125%, 150%, ...."""
    assert monitor == 0, 'Implementation only supports first monitor'
    set_logicalpioverride = int("0x009F", 16)
    param = {100: -2, 125: -1, 150: 0, 175: 1}[scale_factor]
    SystemParametersInfoW(set_logicalpioverride, param, 0, 1)


def toogle_scale(monitor, small_scale=100, big_scale=125):
    """Toggles monitor resolution of monitor i between small and big scale"""
    assert small_scale < big_scale, 'Invalid scale factor arguments'
    scale_factor = get_scale_factor_for_monitor(monitor)
    logging.debug('monitor %s: got scale factor %s%%', monitor, scale_factor)
    scale_factor = small_scale if scale_factor > small_scale else big_scale
    logging.debug('monitor %s: setting factor %s%%', monitor, scale_factor)
    set_scale_factor_for_monitor(monitor, scale_factor)


if __name__ == "__main__":
    logging.basicConfig(level=0, format='setscale %(message)s')
    toogle_scale(monitor=0, small_scale=100, big_scale=125)

Tenerife answered 8/6 at 9:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.