创建一个DPI感知的应用程序

81
我有一个 C# 表单应用程序,当我更改显示器 DPI 时,所有控件都会移动。 我使用了代码 this.AutoScaleMode = AutoScaleMode.Dpi,但它没有解决这个问题。 有人有什么想法吗?

你也可以查看这篇博客,我认为它提供了关于这个主题的很好的信息:http://www.telerik.com/blogs/winforms-scaling-at-large-dpi-settings-is-it-even-possible- - checho
就我个人而言,在最新的Windows 10上,我更成功地采取了相反的方法:确保我的传统Windows Forms应用程序不知道DPI。这迫使Windows 10使用其winforms的默认缩放,这很完美。要从代码中进行测试,请SetProcessDpiAwareness(0)。 0 =在某些枚举中不知道 - 谷歌详细信息,需要从“shcore.dll”中导入DllImport。建议在app.manifest中执行此操作;我只是提到代码作为测试,以确保。 - ToolmakerSteve
我在一个C# Winforms应用程序中遇到了一个问题,应用窗口使用的DPI从96 DPI(缩放因子125%)的屏幕设置变为120 DPI(缩放因子100%),但只在运行可执行文件时发生-在VS2013 IDE中运行时没有出现该问题。附加调试器以查找更改发生的位置给出了无法重现的结果。通过调用SetProcessDpiAwareness(0)进行修复。 - SimonKravis
11个回答

125

编辑:自 .NET 4.7 起,Windows Forms 在高 DPI 上的支持得到了改进。 在 learn.microsoft.com 上阅读更多信息。但它仅适用于 Win 10 创作者更新版本及更高版本,因此根据您的用户群体,现在可能还不可行。


虽然有些困难,但并非不可能实现。当然,您最好选择迁移到 WPF,但这可能不切实际。

我花费了很多时间来解决这个问题。以下是一些规则和指南,可以在没有FlowLayoutPanel或TableLayoutPanel的情况下正确地解决它:

  • 始终在默认的 96 DPI (100%) 下编辑/设计应用程序。如果您在 120 DPI (125% 等) 下设计,则在以后返回 96 DPI 进行工作时将会出现问题。
  • 我已经尝试过 AutoScaleMode.Font 并取得了成功,但我没有尝试过 AutoScaleMode.DPI。
  • 确保所有容器(表单、面板、选项卡页、用户控件等)使用默认字体大小。8.25 像素。最好根本不要在所有容器的.Designer.cs 文件中设置字体大小,这样它将使用容器类中的默认字体。
  • 所有容器必须使用相同的AutoScaleMode
  • 确保所有容器在 Designer.cs 文件中都设置了以下行:

this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); // for design in 96 DPI

  • 如果您需要在标签/文本框等上设置不同的字体大小,请为每个控件单独设置字体,而不是在容器类上设置字体,因为Winforms 使用容器的字体设置来缩放其内容,并且具有与其包含的表单不同字体大小的面板一定会出现问题。如果表单和所有容器都使用相同的字体大小,则可能有效,但我没有尝试过。
  • 使用另一台机器或具有更高DPI设置的虚拟Windows安装(VMware,Virtual PC,VirtualBox)立即测试设计。只需在DEV机器上从/bin/Debug文件夹运行已编译的.exe文件即可。
  • 我保证,如果您按照这些指南操作,即使您放置了具有特定锚定点并且不使用流面板的控件,也会没问题。我们构建的一个应用程序已在数百台具有不同DPI设置的机器上部署,我们不再收到任何投诉。所有表单/容器/网格/按钮/文本字段等大小都正确缩放,字体也是如此。图片也可以正常工作,但在高DPI下可能会稍微失真。

    编辑:此链接包含许多有用信息,尤其是如果选择使用AutoScaleMode.DPI:相关stackoverflow问题链接


    2
    我没有尝试过在FlowLayoutPanel中使用这个方法,但是在每个窗体或用户控件的.Designer.cs文件(由Visual Studio设计器生成的部分类文件)中,您需要设置AutoScaleDimensions和AutoScaleMode。这适用于“普通”窗体,在这种窗体中,您可以使用锚点放置控件,例如,您可以为其位置指定特定的x和y坐标。 - Trygve
    3
    非常感谢这篇非常有用的文章。在我参与的一个项目中,遇到了这个问题,需要修复别人留下的代码。我只是从所有的*.Designer.cs文件中删除了所有AutoScale*行,现在一切都可以正常工作了。 - Alexey Yakovenko
    3
    我们根据您上面给出的建议进行了实验,结果表明这是好建议。自从您发布那条建议三年前,您有学到其他需要遵循的指南吗?我们发现了另外一些...在这里发布:https://dev59.com/ymEh5IYBdhLWcg3wKgqg。 - Brian Kennedy
    @BrianKennedy,我们转向了WPF,并在过去的1.5年中使用SVG进行Web开发,以实现完美的缩放和质量。尽管如此,我们仍然维护着一个WinForms应用程序,并且使用上述指南已经足够满足我们的需求,我们从未收到客户的任何投诉。在从.NET 1.1移动到2.0(然后是4.0)之后停止工作的一件事是使用GDI+绘制的组件的缩放。我们绘制大型表单,看起来像文本框和标签的背景。 "纸张形式"的组件可以缩放,但与x和y像素锚定的标签和文本框不同。 - Trygve
    2
    @MuratfromDaminionSoftware:是的,它有其缺点。在发布这篇文章时(5年前),高DPI屏幕还不是很普遍,但现在情况迅速改变了。在高DPI上开发的问题在于所有由.Designer生成的代码都变得特定于高DPI,而在构建后返回较低的DPI会导致一切缩放不正确。但是,在构建后移动到更高的DPI就没有问题。 - Trygve
    显示剩余3条评论

    47

    注意: 这不会修复控件在 DPI 改变时的移动,它只会修复模糊的文本!!。


    如何在高dpi设置下修复模糊的Windows Forms:

    1. 进入Forms设计器,然后选择你的Form(通过点击标题栏)
    2. 按F4打开属性窗口
    3. 然后找到AutoScaleMode属性
    4. 将其从Font(默认)更改为Dpi

    现在,进入Program.cs(或者你的Main方法所在的文件),并将其更改为:

    namespace myApplication
    {
        static class Program
        {
            [STAThread]
            static void Main()
            {
                // ***this line is added***
                if (Environment.OSVersion.Version.Major >= 6)
                    SetProcessDPIAware();
    
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainForm());
            }
    
            // ***also dllimport of that function***
            [System.Runtime.InteropServices.DllImport("user32.dll")]
            private static extern bool SetProcessDPIAware();
        }
    }
    

    保存并编译。现在你的表单应该再次变得清晰易懂。


    来源: http://crsouza.com/2015/04/13/how-to-fix-blurry-windows-forms-windows-in-high-dpi-settings/

    如何在高DPI设置下修复模糊的Windows窗体?
    如果您在Windows操作系统中使用高分辨率显示器,则可能会遇到一个问题,即应用程序界面看起来模糊或缩小。 这是由于Windows无法正确处理高DPI显示器的缩放,从而导致了这个问题。 不过,有一种方法可以解决这个问题,即通过修改应用程序的配置文件来启用高DPI感知。 本文将介绍如何执行此操作。


    这在win10上与VS2017非常好用!谢谢 - Andrea
    如果您查看源链接中的屏幕图像,它们是非常不同的。虽然使用DPI模式缩放的方法可以解决模糊问题,但控件的大小会发生变化,影响布局。因此,这是一个脆弱的解决方案,必须小心应用。 - Jazimov
    在 VS 2019 上运行非常良好(分辨率为1920 x 1080),但控件的大小有所不同,但我可以接受。 - Ebrahim ElSayed
    非常感谢!在Windows 11 / VS 2022中运行得非常好。 - Umar Abbas

    18
    我终于找到了解决屏幕方向和 DPI 处理问题的方法。
    微软已经提供了一份说明文档,但有一个小缺陷会完全破坏 DPI 处理。只需按照下面文档中“为每个方向创建单独的布局代码”部分提供的解决方案即可。 http://msdn.microsoft.com/en-us/library/ms838174.aspx 然后是重要的部分!在 Landscape() 和 Portrait() 方法的代码内,在每个方法的最后添加以下行:
    this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
    

    所以,这两个方法的代码会像这样:
    protected void Portrait()
    {
       this.SuspendLayout();
       this.crawlTime.Location = new System.Drawing.Point(88, 216);
       this.crawlTime.Size = new System.Drawing.Size(136, 16);
       this.crawlTimeLabel.Location = new System.Drawing.Point(10, 216);
       this.crawlTimeLabel.Size = new System.Drawing.Size(64, 16);
       this.crawlStartTime.Location = new System.Drawing.Point(88, 200);
       this.crawlStartTime.Size = new System.Drawing.Size(136, 16);
       this.crawlStartedLabel.Location = new System.Drawing.Point(10, 200);
       this.crawlStartedLabel.Size = new System.Drawing.Size(64, 16);
       this.light1.Location = new System.Drawing.Point(208, 66);
       this.light1.Size = new System.Drawing.Size(16, 16);
       this.light0.Location = new System.Drawing.Point(192, 66);
       this.light0.Size = new System.Drawing.Size(16, 16);
       this.linkCount.Location = new System.Drawing.Point(88, 182);
       this.linkCount.Size = new System.Drawing.Size(136, 16);
       this.linkCountLabel.Location = new System.Drawing.Point(10, 182);
       this.linkCountLabel.Size = new System.Drawing.Size(64, 16);
       this.currentPageBox.Location = new System.Drawing.Point(10, 84);
       this.currentPageBox.Size = new System.Drawing.Size(214, 90);
       this.currentPageLabel.Location = new System.Drawing.Point(10, 68);
       this.currentPageLabel.Size = new System.Drawing.Size(100, 16);
       this.addressLabel.Location = new System.Drawing.Point(10, 4);
       this.addressLabel.Size = new System.Drawing.Size(214, 16);
       this.noProxyCheck.Location = new System.Drawing.Point(10, 48);
       this.noProxyCheck.Size = new System.Drawing.Size(214, 20);
       this.startButton.Location = new System.Drawing.Point(8, 240);
       this.startButton.Size = new System.Drawing.Size(216, 20);
       this.addressBox.Location = new System.Drawing.Point(10, 24);
       this.addressBox.Size = new System.Drawing.Size(214, 22);
    
       //note! USING JUST AUTOSCALEMODE WILL NOT SOLVE ISSUE. MUST USE BOTH!
       this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); //IMPORTANT
       this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;   //IMPORTANT
       this.ResumeLayout(false);
    }
    
    protected void Landscape()
    {
       this.SuspendLayout();
       this.crawlTime.Location = new System.Drawing.Point(216, 136);
       this.crawlTime.Size = new System.Drawing.Size(96, 16);
       this.crawlTimeLabel.Location = new System.Drawing.Point(160, 136);
       this.crawlTimeLabel.Size = new System.Drawing.Size(48, 16);
       this.crawlStartTime.Location = new System.Drawing.Point(64, 120);
       this.crawlStartTime.Size = new System.Drawing.Size(248, 16);
       this.crawlStartedLabel.Location = new System.Drawing.Point(8, 120);
       this.crawlStartedLabel.Size = new System.Drawing.Size(48, 16);
       this.light1.Location = new System.Drawing.Point(296, 48);
       this.light1.Size = new System.Drawing.Size(16, 16);
       this.light0.Location = new System.Drawing.Point(280, 48);
       this.light0.Size = new System.Drawing.Size(16, 16);
       this.linkCount.Location = new System.Drawing.Point(80, 136);
       this.linkCount.Size = new System.Drawing.Size(72, 16);
       this.linkCountLabel.Location = new System.Drawing.Point(8, 136);
       this.linkCountLabel.Size = new System.Drawing.Size(64, 16);
       this.currentPageBox.Location = new System.Drawing.Point(10, 64);
       this.currentPageBox.Size = new System.Drawing.Size(302, 48);
       this.currentPageLabel.Location = new System.Drawing.Point(10, 48);
       this.currentPageLabel.Size = new System.Drawing.Size(100, 16);
       this.addressLabel.Location = new System.Drawing.Point(10, 4);
       this.addressLabel.Size = new System.Drawing.Size(50, 16);
       this.noProxyCheck.Location = new System.Drawing.Point(168, 16);
       this.noProxyCheck.Size = new System.Drawing.Size(152, 24);
       this.startButton.Location = new System.Drawing.Point(8, 160);
       this.startButton.Size = new System.Drawing.Size(304, 20);
       this.addressBox.Location = new System.Drawing.Point(10, 20);
       this.addressBox.Size = new System.Drawing.Size(150, 22);
    
       //note! USING JUST AUTOSCALEMODE WILL NOT SOLVE ISSUE. MUST USE BOTH!
       this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); //IMPORTANT
       this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;   //IMPORTANT
       this.ResumeLayout(false);
    }
    

    对我来说像魅力一样运作良好。


    “Creating Separate Layout Code for Each Orientation” 部分与原问题无关,该问题是关于一个 Winform 应用程序的。 - Victor Zakharov

    6

    4

    在Windows Forms中设计DPI感知应用程序真的很困难。您需要使用布局容器,在DPI更改时正确地调整大小(例如TableLayoutPanel或FlowLayoutPanel)。所有控件也需要调整大小。这些容器的配置可能是一个挑战。

    对于简单的应用程序,可以在合理的时间内完成,但对于大型应用程序来说,确实是很多工作。


    3

    从经验角度看:

    • 除非必要,否则不要在Windows表单中使用DPI感知
    • 为此,在应用程序中的所有表单和用户控件上始终将 AutoScaleMode 属性设置为 None
    • 结果:当 DPI 设置更改时,获得所见即所得类型的界面

    我遇到了这样一种情况,将AutoScaleMode设置为字体模式会导致应用程序在我尝试以中等大小的Windows运行(125%或120ppi)而不是通常的“较小”(96ppi)时立即崩溃。我按照您的建议将AutoScaleMode设置为无,到目前为止一切都看起来很好... - Dan W

    2

    我曾经为此苦苦挣扎,但最终在Windows 10和其他系统上找到了一个超级简单的解决方案。

    在你的WinForms App.config文件中粘贴以下内容:

    <System.Windows.Forms.ApplicationConfigurationSection>
        <add key="DpiAwareness" value="PerMonitorV2" />
    </System.Windows.Forms.ApplicationConfigurationSection>
    

    然后创建一个app.manifest文件,并将以下行粘贴或注释掉:
    <!-- Windows 10 -->
    <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    

    在完成上述操作后,我成功地在我的4k屏幕上获得了很好的DPI效果。欲了解更多信息,请参阅此链接此链接

    FYI,这种方法在.NET 5.0或更高版本中不起作用,因为标签"System.Windows.Forms.ApplicationConfigurationSection"会导致编译错误。 参考:https://github.com/microsoft/dotnet/issues/374 - Mario
    @Mario 我觉得从.NET 5.0开始,DPI已经很好了,不需要我进行任何更改...至少在我的系统上,新建一个应用程序的DPI已经很好了。 - Jonas

    1
    1. 如果您希望您的WinForms应用程序是 DPI-Aware 应用程序,除了 Trygve 的好答案外,如果您有一个大项目,您可能希望自动缩放您的窗体及其内容。您可以通过创建 ScaleByDPI 函数来实现此功能:

    ScaleByDPI 函数将接收一个 Control 参数,通常是一个窗体,然后递归迭代所有子控件(若 (control.HasChildren == true)),并将您的应用程序控件和字体大小以及图像、图标和图形的尺寸缩放到操作系统配置的 DPI。请尝试为它们实现此功能。

    ScaleByDPI 函数的特殊注意事项:

    a. 对于所有具有默认字体大小的控件,您需要将它们的 Font.Size 设置为 8.25。

    b. 您可以通过 (control.CreateGraphics().DpiX / 96) 和 (control.CreateGraphics().DpiY / 96) 来获取 devicePixelRatioX 和 devicePixelRatioY 值。

    c. 您需要根据 control.Dock 和 control.Anchor 值定义的算法来缩放 Control.Size 和 Control.Location。请注意,control.Dock 可能有 6 种可能的值,而 control.Anchor 可能有 16 种可能的值。

    d. 这个算法需要设置下一个布尔变量的值:isDoSizeWidth、isDoSizeHeight、isDoLocationX、isDoLocationY、isDoRefactorSizeWidth、isDoRefactorSizeHeight、isDoRefactorLocationX、isDoRefactorLocationY、isDoClacLocationXBasedOnRight、isDoClacLocationYBasedOnBottom。请保留html标签。

    e. 如果您的项目使用的控件库不是Microsoft控件,这些控件可能需要特殊处理。

    关于上述(d.)布尔变量的更多信息:

    *有时一组控件(可能是按钮)需要在同一垂直线上依次放置,并且它们的Anchor值包括右侧但不包括左侧,或者它们需要在同一水平线上依次放置,并且它们的Anchor值包括底部但不包括顶部,在这种情况下,您需要重新计算控件的位置值。

    *对于Anchor包含Top和Bottom和/或Left和Right的控件,您需要重新调整控件的大小和位置值。

    ScaleByDPI函数的用途:

    a. 将以下命令添加到任何Form构造函数的末尾:ScaleByDPI(this);

    b. 在动态向Form添加任何控件时,也要调用ScaleByDPI([ControlName])。

    1. 当您在构造函数结束后动态设置任何控件的大小或位置时,请创建并使用以下函数之一,以获取缩放后的 Size 或 Location 值:ScaleByDPI_X、ScaleByDPI_Y、ScaleByDPI_Size、ScaleByDPI_Point。

    2. 为了将您的应用程序标记为 DPI 感知应用程序,请将 dpiAware 元素添加到您的应用程序程序集清单中。

    3. 将所有 Control.Font 的 GraphicsUnit 设置为 System.Drawing.GraphicsUnit.Point。

    4. 在所有容器的 *.Designer.cs 文件中,将 AutoScaleMode 值设置为 System.Windows.Forms.AutoScaleMode.None。

    5. 对于 ComboBox 和 TextBox 等控件,更改 Control.Size.Height 不起作用。在这种情况下,更改 Control.Font.Size 将修复控件的高度。

    6. 如果窗体的 StartPosition 值为 FormStartPosition.CenterScreen,则需要重新计算窗口的位置。


    大多数应用程序不是更喜欢使用AutoScaleMode.Font吗?只有那些试图占据屏幕特定百分比的应用程序才会使用AutoScaleMode.DPI,这样做才有用。 - Jon Coombs
    我们必须编辑 Designer.cs 文件吗?Winforms 抱怨用户编辑该文件,因为当您使用 VS 设计器自身创建 GUI 时,该文件应该是自动生成的。将其添加到 Form1_Load 事件中是否足够? - Dan W

    0
    项目文件添加(yourproject.csproj): true 如果存在Program.cs文件:
    ApplicationConfiguration.Initialize();
    使其免费。删除此行,因为它设置了引导程序的dpi偏好,将其自定义为项目的偏好。
    Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(MainForm);
    在MainForm.cs的构造函数中添加:
    this.AutoScaleMode = AutoScaleMode.Dpi;
    并从MainForm.designer.cs中删除:
    this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    这些适用于 .Net 6 Winform 应用程序,非常明显。

    0
    作为一名拥有4K显示器的开发人员,我一直在尝试各种解决方案。所有WPF窗口都很好,但所有WinForms缩放都不正确。
    这发生在WPF中的WinForms应用程序中,也发生在独立的WinForms应用程序中。两个应用程序都是.NET 4.7.2。因此,它应该像所有其他答案中解释的那样工作,但实际上并没有。
    这是我的设置 enter image description here 最终我采用了“unaware”而不是“PerMonitorV2”的解决方案。
    在app.manifest文件中。
      <application xmlns="urn:schemas-microsoft-com:asm.v3">
        <windowsSettings>
          <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">unaware</dpiAwareness>
        </windowsSettings>
      </application>
    

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