检测非 DPI 感知应用程序是否已被缩放/虚拟化

12

我正在尝试检测WinForms应用程序是否已在缩放/虚拟化模式下启动,因为操作系统具有高DPI。目前,在3840x2400且200%缩放的系统中,应用程序将分辨率视为1920x1200,DPI为96,比例因子为1。

我们正在使应用程序具有DPI感知能力,但在此之前,我们需要一个“快速修复”方案,以便能够检测是否进行了缩放。这是因为它会破坏应用程序中获取屏幕截图的功能。我们在Graphics.CopyFromScreen中使用缩放后的尺寸,但它期望非缩放尺寸,因此会截取错误大小的屏幕截图。

我知道有DPI感知设置,但目前我们仍希望应用程序进行缩放,但要能够检测是否进行了缩放,并获取非缩放尺寸(如果可能)。


将屏幕截图代码移入另一个支持dpiaware的应用程序中。 - Hans Passant
@HansPassant 这是一个很好的建议,但我们通过远程桌面服务作为RemoteApp提供此应用程序,这确实使这个方法更加复杂。 - Manuel
1个回答

29
没有显式标记为高 DPI 感知的应用程序将被系统欺骗,告诉它有 96 DPI,缩放因子为 100%。为了获得真正的 DPI 设置,并避免 DWM 的自动虚拟化,您需要在应用程序的清单中包含 <dpiAware>True/PM</dpiAware>。 更多信息可在此处找到。
在您的情况下,似乎您正在寻找LogicalToPhysicalPointForPerMonitorDPIPhysicalToLogicalPointForPerMonitorDPI函数对。正如链接的文档所解释的那样,默认情况下,系统将根据调用者的DPI感知返回有关其他窗口的信息。因此,如果非DPI感知应用程序尝试获取高DPI感知进程的窗口边界,则会获得已转换为其自己的非DPI感知坐标空间的边界。这将是这些功能的俗语中的“逻辑”坐标。您可以将这些转换为“物理”坐标,这些坐标实际上由操作系统(和其他高DPI感知进程)使用。
不过,回答您实际的问题:如果您绝对需要打破操作系统在一个 DPI感知的进程中的谎言,我可以想到两种方法来做到这一点:
  1. 调用GetScaleFactorForMonitor函数。如果返回的DEVICE_SCALE_FACTOR值不是SCALE_100_PERCENT,那么你就被缩放了。如果你的应用程序不支持DPI,则你正在虚拟化。

    这是一个快速而简单的解决方案,因为只需要一个简单的P/Invoke定义就可以从WinForms应用程序中调用它。然而,你不应该依赖它的结果来做任何比布尔型“我们是否被缩放/虚拟化”指示器更多的事情。换句话说,不要相信它返回的比例因子

    在一个Windows 10系统上,系统DPI为96,高DPI显示器的DPI为144(150%缩放),当预期返回SCALE_150_PERCENT时,GetScaleFactorForMonitor函数返回SCALE_140_PERCENT。我真的不明白为什么会这样。我唯一能想到的是,它是为Windows 8.1上的Metro/Modern/UWP应用程序设计的,其中150%不是有效的比例因子,但140%是。尽管缩放因子已经在Windows 10中统一,但这个函数似乎没有更新,对于桌面应用程序仍然返回不可靠的结果。

  2. 根据显示器的逻辑和物理宽度自己计算缩放因子。

    首先,你需要获取一个HMONITOR(指向特定物理显示器的句柄)。你可以通过调用MonitorFromWindow来实现这一点,传递一个WinForms窗口的句柄,并指定MONITOR_DEFAULTTONEAREST。这将为你获取正在显示你感兴趣的窗口的监视器的句柄。

    然后,你将使用这个监视器句柄来通过调用GetMonitorInfo函数获取该监视器的逻辑宽度。这会填充一个MONITORINFOEX结构体,其中包含一个RECT结构体(rcMonitor)作为其成员之一,该结构体包含该监视器的虚拟屏幕坐标。(请记住,与.NET不同,Windows API以左、上、右和下的范围表示矩形。宽度是右范围减去左范围,而高度是下范围减去上范围。)

    GetMonitorInfo填充的MONITORINFOEX结构体还会给你该监视器的名称(szDevice成员)。然后,你可以使用该名称调用EnumDisplaySettings函数,该函数将填充一个DEVMODE结构体,其中包含有关该监视器的物理显示模式的大量信息。你感兴趣的成员是dmPelsWidthdmPelsHeight,它们分别给出了每个宽度和高度的物理像素数。

    然后,你可以将逻辑宽度除以物理宽度来确定宽度的缩放因子。高度也是同样的方法(除了我所知道的所有监视器都具有方形像素,因此垂直缩放因子将等于


非常感谢您提供的详细回复!这正是我所需要的! - Manuel
1
请注意,“GetScaleFactorForMonitor”方法根本不应该被使用,即使只是用于检查是否正在应用缩放。根据我在Windows 10上(仅限非周年更新版本)的测试,使用100%或125%的比例因子,此函数仍然返回SCALE_100_PERCENT,在更高的数字中返回异常值,如答案所述。这绝对是Windows中的一个bug。枚举值本身映射到整数,即SCALE_150_PERCENT = 150,你会期望它们确实这样做,以便任何比例因子都可以准确地作为整数返回。希望他们能够修复它。 - Roger Sanders
也许你可以考虑使用GetDpiForMonitor,在我的测试中它是有效的。同时,结合使用 SetThreadDpiAwarenessContext(仅适用于Win10),将你的线程临时设置为 DPI 意识。 - Paul
当我有多个显示器并为每个设置不同的dpi时,<dpiaware> True </dpiaware>表现良好。通过将上述行添加到app.manifest文件中,GetMonitorInfo返回监视器工作区的正确矩形值。 - Hariprasad Jayprakash
2
@CodyGray 你试过你的函数了吗?对于Windows 10.0.19044.1889,非DPI感知应用程序,“GetDPIForMonitor”返回96(而不是120),“GetScaleFactorForMonitor”返回100(而不是125),“GetMonitorInfo”返回1536x864(而不是1920x1080)。这种行为在某个Windows版本中有所改变吗? - Александр Кулагин
显示剩余4条评论

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