如何使用C#编程以程序方式更改Windows 10的显示比例?

30

我正在尝试使用C#编程以编程方式更改Windows 10的显示比例。让我也说一下,我不是在创建一个自动强制用户屏幕更改分辨率/缩放比例的应用程序。它只是一个工具,让我能够从托盘中切换比例,因为这是我经常进行测试的事情。因此,特意为此操作而设计。

所以,我能够追踪到当用户通过下面看到的官方对话框手动执行此操作时设置了哪些注册表条目(HKEY_CURRENT_USER\Control Panel\Desktop):

Windows 10 Display Scaling Dialog

但是,显然直接与注册表一起工作意味着我需要重新启动机器才能生效。

我知道可以使用Pinvoke来更改屏幕分辨率:Setting my Display Resolution

我想知道是否有办法为给定屏幕更改此“%”? 例如...上面的屏幕显示150%,我想能够通过全范围的100-500%以编程方式更改它。

7个回答

44
这是我在系统设置应用程序(沉浸式控制面板)上进行的研发中的学习成果。(请参阅我为此学习创建的简单C++ API的其他答案 - https://dev59.com/gVsW5IYBdhLWcg3wO1GN#58066736。对于单显示器设置或者只想更改主要显示器的DPI,可以使用这里提供的更简单的方法 - https://dev59.com/gVsW5IYBdhLWcg3wO1GN#62916586
  1. 系统设置应用程序(Windows 10附带的新沉浸式控制面板)能够实现这一点。这意味着肯定存在一个API,只是微软没有公开它。
  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]

基本上,传递给DisplayConfigSetDeviceInfo()DISPLAYCONFIG_DEVICE_INFO_HEADER结构体成员的值如下:
type : -4
size : 0x18
adapterId : LowPart : 0xcbae HighPart :0

枚举类型,如在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;

虽然设置应用程序正在尝试发送-4作为类型,但我们可以看到枚举没有负值。
如果我们能够完全逆向工程这个问题,我们将拥有一个可以设置显示器DPI的工作API。
微软拥有一些专门为其自己的应用程序设计的特殊API,而其他人无法使用,这似乎非常不公平。 更新1: 为了验证我的理论,我使用WinDbg复制了发送给DisplayConfigSetDeviceInfo()作为参数的DISPLAYCONFIG_DEVICE_INFO_HEADER结构体的字节;当从系统设置应用程序更改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);

请注意,相同的代码可能对您无效,因为指向系统上显示的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 };

如果我们比较这两个字节缓冲区,我们可以得出以下结论。
1. 字节号21用于指定DPI缩放,因为在150%和175%之间的所有其他字节都相同。 2. 对于150%的缩放,字节21的值为1,而对于175%的缩放,它是2。该显示器的默认(推荐)DPI缩放为125%。 3. 根据@ Dodge提到的technet文章,在Windows术语中,0对应于推荐的DPI缩放值。其他整数与相对于此推荐值的dpi缩放对应。 1表示缩放的一步,-1表示缩小一步。例如,如果推荐值为125%,则值为1表示150%的缩放。这正是我们所看到的。
现在唯一剩下的问题是如何获取显示器的推荐DPI缩放值,然后我们将能够编写以下形式的API - SetDPIScaling(monitor_LUID, DPIScale_percent)

更新3:

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

更新4:

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

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

对于连接到我的机器上的戴尔显示器,监视器ID是DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297。 我能够弄清楚监视器ID的结构。我用四个不同的显示器验证了我的理论。
对于戴尔显示器(dpi缩放存储在HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\ DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297),如下所示(很抱歉添加了图片,无法找到一种简洁地表示信息的方法)。

Monitor ID image windows 10 x64 17763, 18362

从EDID中构建监视器ID所需的数据基本上如下所示。
  1. 制造商ID
    • EDID的第8、9字节(大端)。
    • 例如,对于戴尔显示器,EDID的这两个字节为10AC。除了第15位,使用剩下的15位(位0到14),每次5位。(10AC)16等于(0001-0000-1010-1100)2。将这个二进制数从最低有效位开始分成5位一组,得到(0-00100-00101-01100)2。将每个组转换为十进制,得到(0-4-5-12)10,现在第4个字母是D,第5个字母是E,第12个字母是L。
    • 备用:@@@
  2. 产品ID
    • EDID的第10、11字节(小端)
    • 例如,对于戴尔显示器,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)终止。
    • 对于戴尔显示器,它是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可以在两个位置存储序列号,如果找不到DTD块的EDID,则操作系统使用位于字节12到15(32位小端)的序列号。对于戴尔显示器,它是(4C-57-4C-30)16,由于是小端,序列号是(304C574C)16,即(810309452)10。操作系统将使用此值(以十进制形式作为备用)。如果连这个值也不存在,则使用0。
  4. 制造周
    • EDID的第16个字节(可能有一些变化,请参见维基百科文章
    • 对于戴尔显示器,它是(21)16
    • 备用:00
  5. 制造年份
    • EDID的第17个字节
    • 制造年份自1990年起。将第17个字节的值加上1990。
    • 对于戴尔显示器,它是(1A)16。 (1A)16 请注意,只有前128个字节的EDID是必需的。
      关于回退的说明

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

      更新4:

      DPI缩放是源的属性,而不是目标的属性,因此在DisplayConfigGetDeviceInfo()DisplayConfigSetDeviceInfo()中使用的id参数是源ID,而不是目标ID。
      上面建议的注册表方法在大多数情况下应该可以正常工作,但有两个缺点。一是它不能与系统设置应用程序完全匹配(在设置生效的时间方面)。其次,在一些罕见的情况下(无法再现),我发现操作系统生成的监视器ID字符串略有不同 - 它比上面图片中显示的组件更多。
      我已经成功创建了一个API,我们可以像系统设置应用程序一样使用它来获取/设置DPI缩放。将在新答案中发布,因为这更多地涉及我寻找解决方案的方法。 更新5: 生成存储显示器DPI的唯一字符串(监视器ID)的算法对我们来说是未知的,但可以使用DisplayConfigGetDeviceInfo与另一个未记录的参数:-7来构建它,如此处所述 - https://dev59.com/gVsW5IYBdhLWcg3wO1GN#76786955

16

C++ API 获取/设置 DPI。

我成功地反向工程了系统设置应用程序,并设计出了一个 API。它的代码在我的 GitHub 存储库中 https://github.com/lihas/windows-DPI-scaling-sample

由于我在之前回答此问题时已经解释了很多术语,因此在本答案中我省略了很多解释(https://dev59.com/gVsW5IYBdhLWcg3wO1GN#57397039)。

API 概述

  1. :DpiHelper
  2. 方法
    1. GetDPIScalingInfo()
    2. SetDPIScaling()
获取显示器的 DPI 信息


使用 adapterID 和 sourceID 调用 DPIScalingInfo()

DpiHelper::DPIScalingInfo DpiHelper::GetDPIScalingInfo(LUID adapterID, UINT32 sourceID)
设置显示器的DPI


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

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

这个仓库中的DpiHelper.h文件有这两种方法的详细文档。

还要阅读DpiHelper.h和仓库中的README文档。 我已经将该仓库中的所有代码发布到公共领域,因此您可以按照任何方式使用它。

示例应用程序

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

这是它的外观。

MFC app to get/set DPI

关于Windows上的DPI缩放

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

我使用了WinDbg Preview(MS Store)和Ghidra进行反向工程。当我因缺乏IDA Pro许可证而放弃时,有人建议我使用Ghidra。从那时起,我一直是它的粉丝。

非常感谢Ghidra!!!


1
GitHub的代码在Windows 10 1903上经过VS2015测试,非常棒。 - jw_

5
如果要更改系统范围内的 DPI 缩放(在多监视器设置中,主监视器的缩放或仅存在单个监视器的情况下),而不是每个监视器的 DPI 缩放,则可以使用 SystemParametersInfo()
此 API 具有一个未记录的参数,可以实现此目的: SPI_SETLOGICALDPIOVERRIDEMicrosoft doc 中了解更多:
SPI_SETLOGICALDPIOVERRIDE   Do not use. 
0x009F

使用方法:

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

要确定上面的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

直到达到250%的缩放级别,步长每次增加25%,之后每次增加50%。
因此,您必须首先获取建议的DPI缩放值,可以使用相同的API,使用SPI_GETLOGICALDPIOVERRIDE参数。
SystemParametersInfo(SPI_GETLOGICALDPIOVERRIDE, 0, (LPVOID)&dpi, 1);

上面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;
}

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

这是一个示例运行。 console app - SystemParametersInfo() sample run

优点和缺点
相对于我之前的方法(https://dev59.com/gVsW5IYBdhLWcg3wO1GN#58066736, https://dev59.com/gVsW5IYBdhLWcg3wO1GN#57397039

优点

  1. 这是一个非常简单的API。因此,当您只需要在多监视器设置中更改主监视器的DPI缩放,或者如果只有一个监视器,则应选择此方法。

缺点

在多显示器设置中无法设置非主显示器的 DPI 缩放。虽然您可以使用其他操作系统 API 来获取当前应用的 DPI 缩放,但它本身并不返回。
它也不会给出最大、最小可能的 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

5

在寻找同样问题的解决方案时,我发现了您的问题并找到了一个可能的解决方案。

我发现在注册表中通过Computer\HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\*monitorId*\DpiValue可以设置每个显示器的百分比值。该值的含义似乎取决于屏幕的大小和dpi。有关详细信息,请参见此Reddit帖子

对于我的24英寸1080p屏幕,0表示100%,1表示125%。 此Technet文章似乎解释了这些值。

不幸的是,仅更改注册表值还不足以解决问题。但是,您可以通过更改分辨率来刷新dpi。

以下代码设置dpi,然后将分辨率调低再恢复高来触发dpi更新。

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);
        }
    }
}

这个解决方案的问题在于我在任何win 10电脑的注册表中都看不到PerMonitorSettings,或者我只能看到一个监视器。通过更改全局LogPixels设置的分辨率解决方案无法解决问题。 - Vojtěch Dohnal
1
一开始这个过程对我没起作用。原来,在注册表子键出现之前,你必须先手动更改缩放因子,然后才能进行编程更改。微软...为什么... - Paul Knopf
在Windows 10上很难使其正常工作。我的比例是300%,将0更改为1后,它变为350,但UI没有改变。 - jw_

1

除了Sahil Singh的答案,我们还可以在以下位置找到MonitorIDs:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors中的subkeys


据我所知,基于注册表的机制工作正常,但可能需要注销、登录或重新启动,以便读取注册表值并实现更改。 - Sahil Singh

0
这是我的代码,基于@Sahil Singh的代码:
Dll项目用于包装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与参考答案相同。在Visual Studio中创建一个C++ Dll项目,添加/放置上述代码,并在下面的C#应用程序中使用该dll。

一个C#控制台应用程序,根据命令行参数设置DPI,并在按任意键时恢复它们:

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();  
        }
    }
}

这个能用吗?SetDPIScaling() 方法似乎有不同的签名。 - Steve Smith
@ Steve Smith 这段代码是自洽的,原始工作已经被修改(包括签名)以便在C#中使用。不要检查原始工作。 - jw_

0
要更新显示器的DPI缩放值,我们需要设置DpiValue注册表值数据。该注册表值位于HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors键中,其中包含一个对每个显示器都是唯一的子键。
例如,对于我的戴尔笔记本屏幕,它是:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers\ScaleFactors\DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297, (REG_DWORD) DpiValue

monitor unique regkey

为了获取每个监视器的唯一子键名称例如(DELA0BC9DRXV68A0LWL_21_07E0_33^7457214C9330EFC0300669BF736A5297),我们可以使用另一个未记录的参数DisplayConfigGetDeviceInfo
当类型参数为-7时,它会给出monitorUniqueName,它形成了监视器键名中"^"之前的部分,其后面的部分仅是前者的MD5哈希。
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();
}

使用的结构体定义可以在此处找到 - https://github.com/lihas/WindowsUndocumentedLib

生成此独特字符串的示例应用程序可以在此处找到 - https://github.com/lihas/windows-DPI-scaling-sample

enter image description here

enter image description here

DpiValue注册数据中设置的值是相对于推荐的DPI缩放比例的,所以如果推荐的缩放比例为250%,而您想将新的缩放比例设置为225%,则将DpiValue设置为0xffffffff,即DWORD(-1),同样地,对于300%,它将是1
通过regkey更改DPI缩放需要用户注销并重新登录才能生效。使用这个未记录的API的一种方法是首先使用它获取monitorUniqueName,然后取其MD5哈希并将其与monitorUniqueName连接起来,两者之间用"^"分隔。在Computer\HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings\下创建具有此结果和字符串的regkey,并设置DpiValue数据。然后注销当前用户并重新登录(或执行重启)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接