禁用WPF应用程序的DPI感知功能

30

你好!

我已经花了一些时间在开发一个 WPF 应用程序上(作为学习经验,不过这确实是一次学习经验),现在终于准备好发布了。发布意味着在我的 HTPC 上安装它,并用于浏览我的电影收藏。

我在运行 1920*1080 的 PC 上进行了设计,但 DPI 设置为正常值,而 HTPC/TV 则以相同的分辨率运行,但 DPI 设置更高,这是显而易见的原因。

问题是我的应用程序在 HTPC 上出现问题,几乎混乱了所有视觉效果。我知道这是由于糟糕的设计造成的(我的错),但由于这是仅供我使用的应用程序,所以我正在寻找快速解决方法,而不是完全重新设计。我读到可以通过将以下内容添加到 AssemblyInfo.cs 文件中来停止应用程序的 DPI 感知:

[assembly: System.Windows.Media.DisableDpiAwareness]

然而,它似乎没有任何作用,应用程序的行为仍然相同。

有人能指点我正确的方向吗?

谢谢, Johan

9个回答

37

DPIAwareness

以下是一些想法(未经尝试):

你正在使用XP吗?在该平台上,该选项可能无法正常工作。

以下可能只是设置相同DpiAwareness选项的不同方法:

  • 查看EXE的“兼容性模式”设置...右键单击它的属性...并启用“禁用显示缩放”

  • 创建一个清单,并声明你没有意识到这一点

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
 ...
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>false</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
 ...
</assembly>
  • 调用 SetProcessDPIAware 函数(需要在窗口创建前调用)

    http://msdn.microsoft.com/zh-cn/library/windows/desktop/ms633543(v=vs.85).aspx

  • 你可以调用 IsProcessDPIAware 函数来检查是否已经应用了 DIP 设置。

    获取 DPI 信息

    以下是如何获取当前使用的 DPI 的方法:

    通过窗口的 PresentationSource 访问 CompositionTarget 获取 DPI 缩放信息。然后,你可以使用这些信息对元素进行缩放调整,比如把 "stuff" 的尺寸长度等以设备独立单位为基准指定,这样当高 DPI 生效时就不会导致实际像素使用增加(可以使用各种技术实现,如 ViewBox 或者对元素宽度等进行计算)。

    double dpiX, double dpiY;
    PresentationSource presentationsource = PresentationSource.FromVisual(mywindow);
    
    if (presentationsource != null) // make sure it's connected
    {
        dpiX = 96.0 * presentationsource.CompositionTarget.TransformToDevice.M11;
        dpiY = 96.0 * presentationsource.CompositionTarget.TransformToDevice.M22;
    }
    

    进行缩放调整

    • 使用ViewBox技巧

      查看我之前提供的答案,允许Canvas使用被解释为“本地”像素的位置,无论DPI缩放如何。

      WPF针对LCD屏幕全高清

    • 访问CompositionTarget上的TransFormToDevice缩放矩阵,然后从中计算一个新矩阵,只需撤消该缩放并将其用于LayoutTransformRenderTransform

    • 使用一种方法(甚至可能将其放入转换器中),以显式基础修改DIP(设备无关位置)位置/长度....如果您希望窗口大小与特定像素大小匹配,则可以这样做。


    1
    两台机器都运行在Windows 7上。我尝试了应用程序清单选项,但似乎没有起作用。我会尽快调查其他选项,感谢您的建议。 - Johan Verbelen

    11

    这篇博客文章解释了如何使用Decorator子类来实现这一点。

    简而言之,创建一个类如下:

    namespace MyNamespace
    {
        public class DpiDecorator : Decorator
        {
            public DpiDecorator()
            {
                this.Loaded += (s, e) =>
                {
                    Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
                    ScaleTransform dpiTransform = new ScaleTransform(1 / m.M11, 1 / m.M22);
                    if (dpiTransform.CanFreeze)
                        dpiTransform.Freeze();
                    this.LayoutTransform = dpiTransform;
                };
            }
        };
    };
    

    然后在您的顶级窗口定义中添加类似xmlns:custom="clr-namespace:MyNamespace"的内容,然后使用<custom:DpiDecorator>将您的DPI独立用户界面包围起来...</custom:DpiDecorator>


    4

    以上方法均无法真正禁用WPF的dpi缩放。

    .Net 4.6中WPF计算dpi缩放的方法如下: 1)HwndTarget是所有可视元素使用的CompositionTarget。 2)UIElement是可视元素的基类,也是dpi缩放计算结果存储的地方。

    WPF确保全局缩放在准备第一个可视元素时计算一次。这由HwndTarget上的布尔值控制{private static bool _setDpi = true}。计算dpi后,将_setDpi字段设置为false,并使用此代码快捷方式使用dpi感知方法{if(!HwndTarget._setDpi) return;}。

    每个UIElement都有相似的东西,它具有使用{private static bool _setDpi = true}的相同模式,以允许首次计算。

    接下来的检查来自ProcessDpiAwareness,它可以是None、Process或Monitor。为了阻止WPF考虑单个监视器,您需要将ProcessDpiAwareness设置为Process(int 1)。

    最后,当计算dpi时,结果存储在名为DpiScaleXValues和DpiScaleYValues的2个列表中。因此,您需要初始化这些正确的值。

    以下是我自己使用的示例代码(仅适用于.net 4.6):

            var setDpiHwnd = typeof(HwndTarget).GetField("_setDpi", BindingFlags.Static | BindingFlags.NonPublic);
            setDpiHwnd?.SetValue(null, false);
    
            var setProcessDpiAwareness = typeof(HwndTarget).GetProperty("ProcessDpiAwareness", BindingFlags.Static | BindingFlags.NonPublic);
            setProcessDpiAwareness?.SetValue(null, 1, null);
    
            var setDpi = typeof(UIElement).GetField("_setDpi", BindingFlags.Static | BindingFlags.NonPublic);
    
            setDpi?.SetValue(null, false);
    
            var setDpiXValues = (List<double>)typeof(UIElement).GetField("DpiScaleXValues", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null);
    
            setDpiXValues?.Insert(0, 1);
    
            var setDpiYValues = (List<double>)typeof(UIElement).GetField("DpiScaleYValues", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null);
    
            setDpiYValues?.Insert(0, 1);
    

    这非常吸引人,可以让您获得真正的1:1像素比。然而,想到所有已经初始化但未被修复的其他事情,这有点可怕。我在SystemParameters.VirtualScreenWidth遇到了一个案例。这就是我搜索这个问题时想要的东西,但我觉得我不能使用它。我希望微软提供一种支持的方法来设置基本缩放因子。 - Rick Velde
    这个程序之前一直运行得很完美,但是在 Windows 10 更新到 1903 版本后,它现在总是使用每个显示器的缩放比例。有什么好的解决方法吗? - Josef Nemec

    3

    一个快速解决方案是将你的窗口内容包裹在Viewbox中。如果Viewbox的直接子元素有显式的宽度和高度,并且纵横比在两台计算机上都相同,则这应该可以让你立即开始运行。


    1
    感谢您的输入!我尝试了这个方法,但由于某些原因我的电脑内存不足!一开始,当我在XAML中添加ViewBox标签时,它会长时间挂起,并且我看到VS使用的内存飙升到4 Gb以上。当我尝试编译时,即使我的机器有12 Gb内存,也会出现内存不足的情况,所以很可能是我在ViewBox内部执行的操作出了问题。 - Johan Verbelen

    2

    DpiAwareness只影响DPI虚拟化。在Windows中,它显示为“在Windows 7和8自定义分辨率框中使用Windows XP样式缩放”,选中表示禁用,未选中表示启用。(如果更改了主字体大小屏幕,请记得应用!仅仅点击“确定”不足以完成设置。)

    如果关闭了DPI虚拟化,则无论应用程序告诉操作系统它支持什么,它都将始终使用标准缩放。当开启DPI虚拟化时,就会产生真实缩放(有意识)和只是简单地将双线性调整大小应用于整个应用程序(不知道)之间的差异。

    我刚想起来还有一个注意事项:如果没有启用Aero Glass,则DPI虚拟化根本无法工作。


    1

    Calin Pirtea的回答真正禁用了DPI缩放。

    对于那些想要将应用程序拉伸到大于100%的显示比例并接受模糊的人,Carlos Borau的回答有效。以下是.NET 4.6(C#)的方法:

    1 将清单文件添加到您的应用程序项目中

    1. 右键单击项目,选择添加->新项目
    2. 选择常规->应用程序清单文件

    2 取消下面的注释并将dpiAware = false设置为:

      <application xmlns="urn:schemas-microsoft-com:asm.v3">
          <windowsSettings>
              <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">false</dpiAware>
          </windowsSettings>
      </application>
    

    0
    在我的情况下,更改我的exe的高DPI设置有所帮助。以下是示例。我在组合框中选择了“应用程序”。

    enter image description here

    在这里阅读更多相关内容


    0

    对于使用VB.NET的开发者,访问app.manifest的方法是:

    enter image description here

    然后取消注释此部分并将其设置为“false”

    enter image description here


    0
    在 Windows 10 上更新到 .NET 4.8 运行时后,我在使用 Calin 的设置 ProcessDpiAwareness 方法时遇到了以下异常:
    System.ArgumentException: '无法将类型为 'System.Int32' 的对象转换为类型 'System.Nullable`1[MS.Win32.NativeMethods+PROCESS_DPI_AWARENESS]'。'
    将代码更改为以下内容可以解决该异常,但我不确定是否能达到相同的结果。
    var processDpiAwareness = typeof(HwndTarget).GetProperty("ProcessDpiAwareness", BindingFlags.Static | BindingFlags.NonPublic);
    
    if (processDpiAwareness != null)
    {
          var underlyingType = Nullable.GetUnderlyingType(processDpiAwareness.PropertyType);
    
          if (underlyingType != null)
          {
               var val = Enum.Parse(underlyingType, "PROCESS_SYSTEM_DPI_AWARE");
               processDpiAwareness.SetValue(null, val, null);
          }
     }
    

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