如何使用C#以编程方式更改Windows 10 Display Scaling

Fut*_*e32 7 c# dpi screen-resolution

我正在尝试找到一种方法来使用C#以编程方式更改Windows 10中的显示比例。

我也要说的是,我并不是要创建一个自动强制用户屏幕更改分辨率/缩放比例的应用程序。它只是我可以从托盘切换秤的工具,这是我经常需要做的测试。因此专门为此动作而设计。

因此,当用户通过如下所示的官方对话框手动执行此操作时,我能够跟踪设置了哪些注册表项(HKEY_CURRENT_USER \ Control Panel \ Desktop):

Windows 10显示缩放对话框

但是,显然直接使用注册表意味着我需要重新启动计算机才能生效。

我知道您可以使用Pinvoke更改屏幕分辨率: 设置我的显示分辨率

我想知道是否也可以为给定的屏幕更改此“%”?即。我上面的屏幕显示150%,我希望能够以编程方式在100-500%的整个范围内进行更改。

Sah*_*ngh 12

C++ 获取/设置 DPI 的 API。

我能够逆向工程系统设置应用程序,并提出一个 API。它的代码存在于我的 github 存储库https://github.com/lihas/windows-DPI-scaling-sample 中

我在这个答案中跳过了解释很多术语,因为我在我之前对这个问题的回答中已经这样做了(/sf/answers/4017792761/)。

API 摘要

  1. :DpiHelper
  2. 方法
    1. GetDPIScalingInfo() 和
    2. 设置DPIScaling()
获取显示器的 DPI 信息


DPIScalingInfo()使用adapterID 和sourceID 调用。

DpiHelper::DPIScalingInfo DpiHelper::GetDPIScalingInfo(LUID adapterID, UINT32 sourceID)
Run Code Online (Sandbox Code Playgroud) 设置显示器的 DPI


使用要设置的适配器 ID、源 ID 和百分比 DPI 缩放调用 SetDPIScaling()。例如。如果要将源的 DPI 缩放设置为 175%,请在最后一个参数中传递 175。

bool DpiHelper::SetDPIScaling(LUID adapterID, UINT32 sourceID, UINT32 dpiPercentToSet)
Run Code Online (Sandbox Code Playgroud)

repo 中的 DpiHelper.h 有这两种方法的完整文档。

另请阅读DpiHelper.h 中的文档和repo 的README。我已经在公共领域的 repo 中发布了所有代码,所以你可以随意使用它。

示例应用程序

我还创建了一个 MFC 应用程序,它使用这个帮助程序库来获取/设置 DPI 缩放。这将帮助您了解如何使用 DpiHelper 类。

这是它的外观。

MFC 应用程序来获取/设置 DPI

Windows 上 DPI 缩放的注意事项

  1. DPI 缩放是源的属性,而不是目标的属性(有关这些术语,请参阅ViPN)。
  2. 显示器的 DPI 缩放取决于 3 个因素 - 分辨率、显示器的物理尺寸、预期观看距离。Windows 用于达到推荐值的确切公式是未知的。
  3. 在操作系统的说法中,DPI 缩放值在与推荐的显示器 DPI 缩放比例进行比较时具有意义。因此,虽然我们在系统设置应用程序中看到 100%、125% 等,但操作系统不理解百分比缩放。而是使用高于或低于推荐比例的步骤数。例如。DPI 缩放值为 -1 意味着比推荐的 DPI 缩放低 1 步。因此,如果监视器的推荐值为 150%,则 -1 表示 125%。

我使用 WinDbg Preview (MS Store) 和Ghidra进行逆向工程。有一段时间,当我因为缺乏 IDA Pro 许可证而准备放弃时,有人建议我使用 Ghidra。从那以后我一直是粉丝。

非常感谢Ghidra!!!


Sah*_*ngh 7

如果要更改系统范围的 DPI 缩放(系统 DPI 缩放 - 在多显示器设置或只有单个显示器的情况下主显示器的缩放),而不是按显示器 DPI 缩放, 可以使用SystemParametersInfo() 。

该 API 有一个未记录的参数可以实现此目的:SPI_SETLOGICALDPIOVERRIDE

来自微软文档

SPI_SETLOGICALDPIOVERRIDE   Do not use. 
0x009F
Run Code Online (Sandbox Code Playgroud)

用法 :

SystemParametersInfo(SPI_SETLOGICALDPIOVERRIDE, relativeIndex, (LPVOID)0, 1);
Run Code Online (Sandbox Code Playgroud)

要弄清楚上面的变量使用什么值relativeIndex,您必须了解操作系统如何期望指定 DPI 缩放值(此处解释)。

简而言之,relativeIndex告诉您想要高于或低于建议的 DPI 缩放值多少步。例如。如果建议的 DPI 缩放值为 125%,并且您希望将缩放设置为 150%,则relativeIndex 将为 1(比 125% 一级),或者如果您想设置 100%,relativeIndex 将为 -1(比 125 低一级) %)。

所有台阶的尺寸可能不同。

100,125,150,175,200,225,250,300,350, 400, 450, 500
Run Code Online (Sandbox Code Playgroud)

直到 250%,步数以 25% 为单位增加,之后以 50% 为单位增加。

因此,您必须首先获取推荐的 DPI 缩放值,可以使用相同的 API 和SPI_GETLOGICALDPIOVERRIDE参数。

SystemParametersInfo(SPI_GETLOGICALDPIOVERRIDE, 0, (LPVOID)&dpi, 1);
Run Code Online (Sandbox Code Playgroud)

上面dpi变量返回的值也要以特殊的方式来理解。该值为负数,其大小表示上面列表中 DPI 缩放百分比的索引。

因此,如果此 API 返回 -1,则建议的 DPI 缩放值为 125%。

示例代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

源代码: https: //github.com/lihas/windows-DPI-scaling-sample

这是一个示例运行。 控制台应用程序 - SystemParametersInfo() 示例运行

优点和缺点 关于
我之前的方法(/sf/answers/4064671551/,/sf/answers/4017792761/

优点

  1. 这是一个非常简单的API。因此,当您只需在多显示器设置中更改主显示器的 DPI 缩放比例时,或者只有一个显示器时,请首选此方法。

缺点

  1. 无法在多显示器设置上设置非主显示器的 DPI 缩放。
  2. 不返回当前应用的 DPI 缩放(尽管您可以使用其他操作系统 API)
  3. 不提供最大、最小可能的 DPI 缩放值。但是,如果您尝试设置在此范围之外,操作系统将不允许这样做,并且将使用最接近的允许值。

参考

  1. https://social.msdn.microsoft.com/Forums/vstudio/en-US/3259c521-b3ed-4121-97da-70a08fb8bb19/change-setting?forum=windowsgeneraldevelopmentissues(稍微不准确)
  2. 如何使用Python代码设置Windows的比例和布局
  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


Sah*_*ngh 6

这是我在系统设置应用程序(沉浸式控制面板)上所做的RnD的学习。(有关我从此学习中创建的简单C ++ API的其他信息,请参阅我的其他答案-https: //stackoverflow.com/a/58066736/981766

  1. System Settings应用程序(Windows 10随附的新的沉浸式控制面板)能够做到这一点。这意味着肯定有一个API,只是Microsoft没有公开它。
  2. 系统设置应用程序是UWP应用程序,但可以与调试器-WinDbg挂钩。

我使用WinDbg来处理此应用程序发出的呼叫。我发现,一旦执行特定功能user32!_imp_NtUserDisplayConfigSetDeviceInfo,新的DPI设置将在我的机器上生效。

我无法在此函数上设置断点,但能够在上设置一个断点DisplayConfigSetDeviceInfo() (bp user32!DisplayConfigSetDeviceInfo)

DisplayConfigSetDeviceInfo(msdn链接)是一个公共功能,但似乎设置应用程序正在向其发送未记录的参数。这是我在调试会话期间发现的参数。

((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]
Run Code Online (Sandbox Code Playgroud)

基本上,DISPLAYCONFIG_DEVICE_INFO_HEADER传递给struct 的成员的值DisplayConfigSetDeviceInfo()是:

type : -4
size : 0x18
adapterId : LowPart : 0xcbae HighPart :0
Run Code Online (Sandbox Code Playgroud)

wingdi.h中定义的枚举类型为:

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;
Run Code Online (Sandbox Code Playgroud)

在设置应用尝试发送-4的类型时,我们可以看到枚举没有负值。

如果我们能够完全对它进行反向工程,我们将有一个有效的API来设置监视器的DPI。

微软为自己的应用程序提供了一些其他人无法使用的特殊API,这似乎非常不公平。

更新1

为了验证我的理论,我复制(使用WinDbg)DISPLAYCONFIG_DEVICE_INFO_HEADER结构的字节并DisplayConfigSetDeviceInfo()作为参数发送到该字节。当从“系统设置”应用中更改了DPI缩放比例时(尝试设置为150%DPI缩放比例)。

然后,我编写了一个简单的C程序,将这些字节(24字节-0x18字节)发送给DisplayConfigSetDeviceInfo()
然后,我将DPI缩放比例更改回100%,并运行了我的代码。果然,在运行代码时DPI缩放比例确实发生了变化!!!

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);
Run Code Online (Sandbox Code Playgroud)

请注意,与LUID相同的代码可能对您不起作用,并且id参数指向系统上的显示会有所不同(LUID通常用于GPU,id可以是源ID,目标ID或其他一些ID ,此参数取决于DISPLAYCONFIG_DEVICE_INFO_HEADER :: type)。

我现在必须弄清楚这24个字节的含义。

更新2

这是尝试设置175%dpi缩放比例时获得的字节数。

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 };
Run Code Online (Sandbox Code Playgroud)

如果我们比较两个字节缓冲区,我们可以得出以下结论。

  1. 字节号21用于指定DPI缩放,因为所有其他字节在150%和175%之间相同。
  2. 对于150%缩放,字节21的值为1,而对于175%则为2。此监视器的默认DPI缩放(推荐)为125%。
  3. TechNet文章由@Dodge提到,在Windows中的说法,0对应于推荐DPI缩放比例值。其他整数对应于相对于此推荐值的dpi缩放。1表示缩放比例领先一步,-1表示缩小比例。例如。如果推荐值为125%,则值为1表示150%缩放。这确实是我们所看到的。

现在剩下的唯一事情就是弄清楚如何获得建议的显示器DPI缩放比例值,然后我们将能够编写以下形式的API- SetDPIScaling(monitor_LUID, DPIScale_percent)

更新3

如果我们检查@Dodge的答案中提到的注册表项,就会知道这些整数存储为DWORD,并且由于我的计算机是低端字节序的,因此这意味着将使用最后4个字节(21至24字节)。因此,要发送负数,我们将必须使用DWORD的2的补码,并将字节写为little endian。

更新4

我还一直在研究Windows如何尝试生成用于存储DPI缩放值的Monitor ID。对于任何监视器,用户选择的DPI缩放值存储在:

HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\
*MonitorID*
Run Code Online (Sandbox Code Playgroud)

对于连接到本机的Dell显示器,显示器ID为DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297。我能够弄清监视器ID的结构。我用4种不同的显示器验证了我的理论。

对于Dell显示器(DPI缩放比例存储在HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\ DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297),如下所示(很抱歉,无法添加图像,无法找到一种简洁地表示信息的方式)。

显示器ID图像窗口10 x64 17763、18362

从本质上讲,从EDID到构造监视器ID所需的数据如下。

  1. 制造商编号
    • EDID的字节8、9(大端)。
    • 例如。对于Dell显示器,EDID为这些字节提供10AC。除位15外,请一次使用其余15位(位0至14)中的5位。(10AC)16等于(0001-0000-1010-1100)2。从LSB开始,将该二进制文件分成5位的块,得到(0-00100-00101-01100)2。将每个块转换为十进制(0-4-5-12)10,现在'D'是 4 字母,'E'是 5 字母,'L'是 12 字母。
    • 倒退 : @@@
  2. 产品编号
    • EDID的字节10、11(小尾数)
    • 例如。对于Dell显示器,EDID具有BCA0。由于这是小端,因此只需将其转换为A0BC即可获得产品ID。
    • 倒退 : 000
  3. 序列号
    • 使用DTD序列号。EDID的基本块(前128个字节)具有4个称为DTD的数据块。它们可以用于存储时序信息或任意数据。4 DTD块位于字节54,72,90,和108,其具有序列号的DTD块具有前2个字节(字节0和1)为零,2 也字节为零,和3 字节作为0xFF。4 再次为零。字节5及以上具有ASCII序列号。序列号最多可占用13个字节(DTD块的字节5至17)。如果序列号少于13个字符(13个字节),则它将由换行符(0x0A)终止。
    • 对于Dell显示器,它是00-00-00-FF-00-39-44-52-58-56-36-38-41-30-4C-57-4C-0A。请注意,序列号有12个字节,并以换行(0x0A)结尾。转换39-44-52-58-56-36-38-41-30-4C-57-4C为ASCII给我们9DRXV68A0LWL
    • 后备:EDID字节12处的序列号。EDID可以在2个位置存储序列号,如果未找到DTD块EDID,则OS使用字节12到15(32位小端)的序列号。对于Dell显示器,由于是低位字节序,所以它是(4C-57-4C-30)16,序列号是(304C574C)16,即?(810309452?)10。操作系统将使用该值(以10为底)作为后备值。即使不存在,也将使用该值0
  4. 制造周
    • EDID的字节16(可以有一些变化,请参阅Wikipedia文章
    • 对于Dell显示器,它是(21)16
    • 倒退 : 00
  5. 制造年份
    • EDID的字节17
    • 自1990年以来的制造年份。在字节17的值中加上1990。
    • 对于Dell显示器,它是(1A)16。(1A)16 +(1990)10 =(07C6)16
    • 倒退 : 0000
  6. 依基块校验和
    • EDID的字节127
    • 维基百科 -校验和。所有128个字节的总和应等于0(mod 256)。
    • 没有后备。有效的EDID必须具有此值。

请注意,仅需要EDID的前128个字节。

关于备用的注意事项

如果不存在构造监视器ID所需的某些数据,则OS使用回退。上面的列表中列出了我在Windows 10计算机上观察到的构造监视器ID所需的每个数据的备用。我手动编辑了DELL显示器的EDID(link1 link2link3-注意-链接3中建议的方法可能会损坏您的系统,只有在确定的情况下才可以继续操作;建议使用Link1)删除上面给出的所有6项,该显示器ID为我构建的操作系统(不带MD5后缀)是@@@0000810309452_00_0000_85,甚至在删除字节12的序列号时,构建的监视器ID也为@@@00000_00_0000_A4

更新4

DPI缩放是源的特性,而不是目标的,因此,使用在id参数DisplayConfigGetDeviceInfo(),并且DisplayConfigSetDeviceInfo()是源ID,而不是目标的ID。

上面建议的注册表方法在大多数情况下应该可以正常工作,但有两个缺点。一是它不能使我们与系统设置应用程序保持同等水平(就设置生效时间而言)。其次,在极少数情况下(无法再复制),我已经看到OS生成的Monitor ID字符串略有不同-它具有更多组件,如上图所示。

我已经成功创建了一个API,我们可以使用它以与系统设置应用程序完全相同的方式来获取/设置DPI缩放比例。将发布一个新答案,因为这更多地是我寻找解决方案所采用的方法。


Dod*_*dge 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)