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:
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%.
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://stackoverflow.com/a/58066736/981766. A simpler method for single monitor setups, or if you want to change DPI of just the prmary monitor is given here - https://stackoverflow.com/a/62916586/981766)
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.
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.
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.
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)
.
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.
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).
Essentially, the data required from EDID to construct monitor ID is as follows.
@@@
000
0x0A
).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
.0
is used.00
0000
Note that only first 128 bytes of EDID is ever required.
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
.
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.