如何获取不受Windows显示缩放影响的显示器真实像素分辨率
问题根源
你观察到的API返回值和实际GDI坐标不匹配的现象,是Windows DPI虚拟化机制的默认行为:当进程未声明高DPI感知时,系统会自动向进程上报缩放后的虚拟分辨率,同时对进程发起的所有GDI绘制、屏幕捕获操作做自动坐标缩放,才会出现125%缩放下GetSystemMetrics(SM_CXSCREEN)返回1536、传入该值调用BitBlt只能截取部分屏幕、SetPixel坐标偏移的问题。
可用实现方案
方案1:开启进程高DPI感知后调用常规API(推荐,无坐标换算成本)
这是最稳定、兼容性最好的方案,开启高DPI感知后系统会关闭对进程的DPI虚拟化,所有系统API返回的坐标、尺寸都和物理像素一一对应,不需要额外做换算。
你需要在程序启动阶段、创建任何窗口/调用任何GDI API之前,根据程序兼容的Windows版本选择对应API设置DPI感知等级:
- Windows 10 1607及以上版本:调用
SetProcessDpiAwarenessContext,传入参数DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 - Windows 8.1及以上版本:调用
SetProcessDpiAwareness,传入参数PROCESS_PER_MONITOR_DPI_AWARE - Windows Vista及以上版本:调用
SetProcessDPIAware
设置完成后,即可通过常规API拿到真实物理分辨率:
- 单屏场景:直接调用
GetSystemMetrics(SM_CXSCREEN)获取水平真实像素,GetSystemMetrics(SM_CYSCREEN)获取垂直真实像素 - 多屏场景:通过
EnumDisplayMonitors枚举目标显示器,调用GetMonitorInfo拿到的rcMonitor矩形就是该显示器的物理像素范围;也可以针对目标显示器的设备上下文调用GetDeviceCaps(hdc, HORZRES)、GetDeviceCaps(hdc, VERTRES)获取对应宽高。
方案2:不修改进程DPI感知状态直接读取物理分辨率(适配特殊场景)
如果你的程序不需要全局开启高DPI感知,仅需要单独获取真实分辨率,可以直接通过显示配置枚举API读取,返回值不受进程DPI感知状态影响:
调用EnumDisplaySettings传入目标显示器的设备名和ENUM_CURRENT_SETTINGS参数,填充DEVMODE结构后,结构内的dmPelsWidth、dmPelsHeight字段就是当前显示器的真实水平、垂直像素值。
核心代码示例:
DEVMODE devMode = {}; devMode.dmSize = sizeof(DEVMODE); // 传入NULL获取主屏幕配置,多屏场景替换为对应显示器的设备名即可 EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &devMode); // devMode.dmPelsWidth = 真实水平像素 // devMode.dmPelsHeight = 真实垂直像素
注意:使用该方案时,进程仍处于DPI虚拟化状态,后续调用
BitBlt、SetPixel等GDI API时,系统仍会做自动坐标缩放。你需要先通过GetDeviceCaps(hdc, LOGPIXELSX)拿到当前系统的DPI值,计算出缩放系数(缩放系数=当前DPI/96),将物理像素坐标除以缩放系数得到虚拟坐标后再传入GDI API,否则还是会出现坐标错位、截取不全的问题。
内容的提问来源于stack exchange,提问作者Steven Brown

