这是我在系统设置应用程序(沉浸式控制面板)上进行的研发中的学习成果。(请参阅我为此学习创建的简单C++ API的其他答案 -
https://dev59.com/gVsW5IYBdhLWcg3wO1GN#58066736。对于单显示器设置或者只想更改主要显示器的DPI,可以使用这里提供的更简单的方法 -
https://dev59.com/gVsW5IYBdhLWcg3wO1GN#62916586)
- 系统设置应用程序(Windows 10附带的新沉浸式控制面板)能够实现这一点。这意味着肯定存在一个API,只是微软没有公开它。
- 系统设置应用程序是一个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 0x55df8fba38))
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](https://istack.dev59.com/iazO6.webp)
从EDID中构建监视器ID所需的数据基本上如下所示。
- 制造商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。
- 备用:@@@
- 产品ID
- EDID的第10、11字节(小端)
- 例如,对于戴尔显示器,EDID为BCA0。由于这是小端,将其转换为A0BC即可得到产品ID。
- 备用:000
- 序列号
- 使用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。
- 制造周
- EDID的第16个字节(可能有一些变化,请参见维基百科文章)
- 对于戴尔显示器,它是(21)16。
- 备用:00
- 制造年份