DPI感知 - 一个版本中不可感知,另一个版本中系统感知

19
我们遇到了一个非常奇怪的问题。我们的应用程序是一个C#/WinForms应用程序。在我们的6.0版本中,我们的应用程序不支持DPI。但在我们的6.1版本中,它突然变得支持DPI。
在6.0版本中,如果您将其在高DPI下运行,它会使用Windows位图缩放,这很好,因为它不会影响屏幕布局。在6.1版本中,由于某种原因它变成了DPI感知的,用户界面被搞乱了。
我们现在没有能力去修复它。我们有数百个屏幕,让它们都在DPI感知模式下正常工作需要大量时间。
我们使用SysInternals Process Explorer进行了确认。在我们的6.0版本中,它显示“未感知”,但在我们的6.1版本中,它最初显示“未感知”,然后变成“系统感知”。
当代码从EXE进入包含所有用户界面代码的程序集DLL时(我们的EXE基本上是一个非常薄的外壳;它真正做的就是在我们的表示层程序集上调用Controller类),后者的情况出现了。
我们已经确认以下内容:
- 两个版本都是使用VSS 2017在发布模式下构建的。 - 两个版本的目标是相同的.NET Framework(4.5)。 - 两个版本使用相同的DevExpress版本。 - 两个版本都有相同的应用程序清单,其中未启用DPI感知设置。
  • 两个版本均未调用与DPI相关的Windows API。
  • 通过使用Sys Internals和一些消息框,我们确定了6.1版在何时意识到(进入Presentation程序集的入口点)以及在该点加载了哪些DLL(我们的、DevExpress和其他依赖项),然后构建了一个引用相同DLL的小型虚拟应用程序,并确认这些DLL已被加载。但该虚拟应用程序并未变为DPI感知模式。
  • 我们已比较了两个版本的主要csproj文件,未发现有任何实质性差异。
    • 两个版本均未引用WPF中的任何内容。
  • 我们不明白为什么我们的6.1版本突然变得DPI感知。我们不知道还可以查看什么,需要修复此版本以使其回到DPI未感知模式。它正在阻碍我们的发布。非常感谢任何指针。我们愿意尝试任何东西。


    你是否在引用来自WPF的东西(“Presentation”)?从6.0到6.1有些变化。你提到了什么没有改变。此外,请参阅SetProcessDpiAwareness以强制执行它。 - Jimi
    1
    清单文件没有 DPI 意识。但是它是否明确禁用了它:<dpiAware>False</dpiAware>?请参考Hans的答案。DPI 意识是基于线程的。如果需要,阅读我链接的 MSDN 文档,转到SetThreadDpiAwarenessContext。这可能是由依赖项引起的(引用 WPF 组件)。使用 DPL 的 Telerik 控件也会出现此问题。 - Jimi
    1
    如果是这种情况(而且您不知道),请尝试在您的 assemblyinfo.cs 文件中设置 [assembly: System.Windows.Media.DisableDpiAwareness] - Jimi
    @Jimi:我们已经这样做了,问题得到了解决。不知道为什么会出现这种情况,因为我们在两个版本中使用了完全相同的依赖项。我们费尽心思进行了验证。如果你想把你的评论作为答案发布,我会为你点赞。谢谢! - Roel Vlemmings
    1
    又有一个多管闲事的人把一个并非重复的问题标记为重复。我永远不会搜索所谓重复问题的主题。这个问题是相关的,并且被很好地呈现出来了。 - PhysicalEd
    显示剩余2条评论
    1个回答

    19

    关于这个问题的报告:
    一个应用程序通过 Windows 虚拟化来缩放其 UI 内容,它是通过设计不支持 DPI 感知的。突然之间(尽管经过一些修改,导致了次要版本更新),似乎没有明显的原因,它变成了 DPI 感知的(系统感知的)。

    • 该应用程序还依赖于app.manifest <windowsSettings> 的解释,其中缺少 DPI 感知定义会默认为 DPI 无感知(向后兼容)。

    • 没有直接引用 WPF 组件和与 DPI 相关的 API 调用。

    • 该应用程序包括第三方组件(以及可能的外部依赖项)。


    由于 DPI 感知已成为 UI 呈现的一个重要方面,考虑到可用的屏幕分辨率的多样性(以及相关的 DPI 缩放设置),大多数组件生产商已经适应了高 DPI,并且他们的产品是 DPI 感知的(在检测到 DPI 更改时进行缩放)并使用 DPI 感知程序集(通常引用 WPF 程序集,根据定义是 DPI 感知的)。

    当一个 DPI 感知组件在项目中被引用(直接或间接地),一个 DPI 无感知的应用程序将变成 DPI 感知的,除非 DPI 感知已被明确禁用。

    声明程序集 DPI 感知的更直接(也更推荐)的方法是在应用程序清单中明确声明它。

    关于 Visual Studio 2017 之前的应用程序清单设置,请参考 Hans Passant 的答案:
    如何在高 DPI 设置的机器上配置应用程序

    自 Visual Studio 2015-Upd.1 起,此设置已经存在于 app.manifest 中,只需取消注释即可。将该节设置为:<dpiAware>false</dpiAware>

    <?xml version="1.0" encoding="utf-8"?>
    <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
      <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
    
      //(...)
       
      <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
           DPI. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need 
           to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should 
           also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. -->
    
      <application xmlns="urn:schemas-microsoft-com:asm.v3">
        <windowsSettings>
          <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">false</dpiAware>
        </windowsSettings>
      </application>
    
    //(...)
    
    </assembly>
    

    请参考以下 MSDN 文章以获取更多信息:
    在 Windows 上开发高 DPI 桌面应用程序
    为进程设置默认 DPI 意识

    另一种方法是使用这些 Windows API 函数设置进程上下文 DPI 意识:

    Windows 7
    SetProcessDPIAware

    [DllImport("user32.dll", SetLastError=true)]
    static extern bool SetProcessDPIAware();
    

    Windows 8.1
    SetProcessDpiAwareness

    [DllImport("shcore.dll")]
    static extern int SetProcessDpiAwareness(ProcessDPIAwareness value);
    
    enum ProcessDPIAwareness
    {
        DPI_Unaware = 0,
        System_DPI_Aware = 1,
        Per_Monitor_DPI_Aware = 2
    }
    

    Windows 10, version 1703
    SetProcessDpiAwarenessContext()
    (选择 Per-Monitor DPI-Awareness 时,请使用 Context_PerMonitorAwareV2)

    另请参见:混合模式 DPI 缩放和 DPI-aware API - MSDN

    Windows 10, version 1809(2018 年 10 月)
    新增了一个DPI_AWARENESS_CONTEXTDPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED

    DPI 未感知,但 GDI 内容质量得到提高。此模式的行为类似于 DPI_AWARENESS_CONTEXT_UNAWARE,但在窗口显示在高 DPI 显示器上时还可以自动提高文本和其他基于 GDI 的原语渲染质量。

    使用 GetWindowDpiAwarenessContext() 函数检索窗口的 DPI_AWARENESS_CONTEXT 句柄,使用GetThreadDpiAwarenessContext() 检索当前线程的 DPI_AWARENESS_CONTEXT 句柄。然后使用GetAwarenessFromDpiAwarenessContext()DPI_AWARENESS_CONTEXT 结构中检索 DPI_AWARENESS 值。

    [DllImport("user32.dll", SetLastError=true)]
    static extern IntPtr GetWindowDpiAwarenessContext(IntPtr hWnd);
    
    [DllImport("user32.dll", SetLastError=true)]
    static extern IntPtr GetThreadDpiAwarenessContext();
    
    [DllImport("user32.dll", SetLastError=true)]
    static extern int GetAwarenessFromDpiAwarenessContext(IntPtr DPI_AWARENESS_CONTEXT);
    
    
    [DllImport("user32.dll", SetLastError=true)]
    static extern int SetProcessDpiAwarenessContext(DpiAwarenessContext value);
    
    // Virtual enumeration: DPI_AWARENESS_CONTEXT is *contextual*. 
    // This value is returned by GetWindowDpiAwarenessContext() or GetThreadDpiAwarenessContext()
    // and finalized by GetAwarenessFromDpiAwarenessContext(). See the Docs.
    
    enum DpiAwarenessContext
    {
        Context_Undefined = 0,
        Context_Unaware = (DPI_AWARENESS_CONTEXT) -1,
        Context_SystemAware = (DPI_AWARENESS_CONTEXT) -2,
        Context_PerMonitorAware = (DPI_AWARENESS_CONTEXT) -3,
        Context_PerMonitorAwareV2 = (DPI_AWARENESS_CONTEXT) -4,
        Context_UnawareGdiScaled = (DPI_AWARENESS_CONTEXT) -5
    }
    

    由于DPI感知是基于线程的,因此这些设置可以应用于特定线程。当重新设计用户界面以实现DPI感知时,让系统在关注更重要的功能的同时缩放较不重要的组件可以很有用。

    SetThreadDpiAwarenessContext
    (与SetProcessDpiAwarenessContext()相同的参数)

    Assemblyinfo.cs
    如果一个第三方/外部组件引用了WPF程序集并重新定义了应用程序的DPI感知状态,可以通过在项目的Assemblyinfo.cs中插入参数来禁用此自动行为:

    [assembly: System.Windows.Media.DisableDpiAwareness]
    

    @ElektroStudios DPI_AWARENESS_CONTEXT。我需要更新这个,因为Windows 10的2018年10月(1809)更新添加了一个新值:DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED - Jimi
    1
    @ElektroStudios 更新了更多信息,现在应该更加清晰明了。 - Jimi
    1
    @ElektroStudios DPI_AWARENESS_CONTEXT(请参阅文档和我所写的内容)不是一个常量。它是一个对不透明结构的句柄,由两个描述函数GetWindowDpiAwarenessContextGetThreadDpiAwarenessContext返回。然后使用GetAwarenessFromDpiAwarenessContext检索实际值,该函数将返回一个转换后的DPI_AWARENESS_CONTEXT,表示该值。您需要减去ContextDPIAwareness枚举器值,并将该值传递给SetProcessDpiAwarenessContextSetThreadDpiAwarenessContext以设置DPIAwareness上下文。 - Jimi
    1
    @ElektroStudios 在编程中硬编码一些任意值可能会起作用,也可能不起作用,这取决于Windows版本和更新。实际值可能会发生变化,这就是为什么以这种扭曲的方式提供它的原因。顺便说一句,你的问题对我来说一点也不麻烦。从我过去的经验来看,你的问题非常到位,而且相当有趣。 - Jimi
    1
    @ElektroStudios 好的,是的,“DPI_AWARENESS_CONTEXT”不能在设计时设置,因为此时它不存在。您需要派生它(它可能从进程感知变为线程感知)。该枚举器与“GetAwarenessFromDpiAwarenessContext”返回的转换值结合使用。当我手头有Visual Studio时,我会发布一个示例代码。由于您是对的,这并不是很清楚;我假设阅读此内容的某个人会参考文档并自动正确解决它。但实际上,在PInvoke.net中也缺少C#实现。 - Jimi
    显示剩余3条评论

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