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%.
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);
}
}
}
This is partial answer. Posting it as an answer to take community help in arriving at the final answer.
- 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.
- 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.
- Byte number 21 is being used to specify DPI scaling, as all other bytes are same between 150%, and 175%.
- 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%.
- 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).
Essentially, the data required from EDID to construct monitor ID is as follows.
- 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 :
@@@
- 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
- 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). Converting39-44-52-58-56-36-38-41-30-4C-57-4Cto ASCII gives us9DRXV68A0LWL. - 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
0is used.
- 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 (
- Manufacture week
- Byte 16 of EDID (can have some variations, see Wikipedia article)
- For the Dell display it is (21)16.
- Fallback :
00
- 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
- 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.
来源:https://stackoverflow.com/questions/35233182/how-can-i-change-windows-10-display-scaling-programmatically-using-c-sharp

