如何编写 WinForms 代码,自动缩放到系统字体和 dpi 设置?

简介: 有很多评论说“ WinForms 不能很好地自动调整 DPI/字体设置; 切换到 WPF。”然而,我认为这是基于。NET 1.1; 看起来他们在实现自动伸缩方面做得相当不错。NET 2.0.至少根据我们目前的研究和测试。但是,如果你们中有人更清楚的话,我们很乐意听到你们的声音。< sup > (请不要再争论我们应该切换到 WPF... ... 那不是现在的选项。)

问题:

  • 什么在 WinForms 中不能正确自动缩放,因此应该避免?

  • 编写 WinForms 代码时,程序员应该遵循什么样的设计准则,以便它能够很好地自动伸缩?

到目前为止我们已经确定的设计指南:

见下面的 社区维基解答

其中有哪些是不正确或不充分的吗?我们还应该采纳什么其他的指导方针吗?还有其他需要避免的模式吗?如果您能提供其他指导,我将不胜感激。

106831 次浏览

除了锚点不能很好地工作之外: 我会进一步说明确切的定位(也就是使用 Location 属性)对于字体缩放不能很好地工作。我不得不在两个不同的项目中解决这个问题。在这两个版本中,我们必须将所有 WinForms 控件的定位转换为使用 TableLayoutPanel 和 FlowLayoutPanel。在 TableLayoutPanel 中使用 Dock (通常设置为 Fill)属性可以很好地工作,并且可以使用系统字体 DPI 进行很好的伸缩。

我发现要让 WinForms 在高 DPI 下表现良好是非常困难的。因此,我编写了一个 VB.NET 方法来覆盖表单行为:

Public Shared Sub ScaleForm(WindowsForm As System.Windows.Forms.Form)
Using g As System.Drawing.Graphics = WindowsForm.CreateGraphics
Dim sngScaleFactor As Single = 1
Dim sngFontFactor As Single = 1
If g.DpiX > 96 Then
sngScaleFactor = g.DpiX / 96
'sngFontFactor = 96 / g.DpiY
End If
If WindowsForm.AutoScaleDimensions = WindowsForm.CurrentAutoScaleDimensions Then
'ucWindowsFormHost.ScaleControl(WindowsForm, sngFontFactor)
WindowsForm.Scale(sngScaleFactor)
End If
End Using
End Sub

我在工作中写的一本指南:

WPF 在“设备独立单元”中工作,这意味着所有的控制规模 完美的高 dpi 屏幕。在 WinForms 中,它需要更多的关注。

WinForms 以像素为单位工作。文本将根据系统 dpi 进行缩放,但通常会由未缩放的控件进行裁剪。为了避免此类问题,必须避免显式调整大小和定位。遵守以下规则:

  1. 无论您在哪里找到它(标签、按钮、面板) ,都将 AutoSize 属性设置为 True。
  2. 对于布局,使用 FlowLayoutPanel (la WPF StackPanel)和 TableLayoutPanel (a la WPF Grid)布局,而不是普通的布局 展板。
  3. 如果您是在高 dpi 计算机上开发,VisualStudio 设计器可能会让您感到沮丧。当您设置 AutoSize = True 时,它将调整控件的大小到您的屏幕。如果控件具有 AutoSizeMode = Growth Only,那么对于使用正常 dpi 的用户,它将保持这个大小,即。比预期的要大。要解决这个问题,打开计算机上的设计器与正常的 dpi 和右键单击,重置。

不支持正确伸缩的控件:

  • 遗传 AutoSize = FalseFontLabel。在控件上显式设置 Font,使其在“属性”窗口中以粗体显示。
  • ListView列的宽度不能缩放。改写表单的 ScaleControl来代替。参见 这个答案
  • SplitContainerPanel1MinSizePanel2MinSizeSplitterDistance属性
  • 遗传 MultiLine = TrueFontTextBox。在控件上显式设置 Font,使其在“属性”窗口中以粗体显示。
  • 在表单的构造函数中:

    • 设定 ToolStrip.AutoSize = False
    • 根据 CreateGraphics.DpiX.DpiY设置 ToolStrip.ImageScalingSize
    • 如果需要,设置 ToolStrip.AutoSize = True

    有时 AutoSize可以留在 True,但有时没有这些步骤它无法调整大小。对于 .NET Framework 4.5.2EnableWindowsFormsHighDpiAutoResizing,在没有这些更改的情况下工作。

  • TreeView的图像。根据 CreateGraphics.DpiX.DpiY设置 ImageList.ImageSize。对于 StateImageList,在不改变 .NET Framework 4.5.1EnableWindowsFormsHighDpiAutoResizing的情况下工作。
  • Form的大小。比例固定大小的 Form的手动创建后。

设计指引:

  • 所有 ContainerControls 必须设置为相同的 AutoScaleMode = Font。 (Font 将处理 DPI 的更改和系统字体的更改 大小设置; DPI 只处理 DPI 更改,而不处理对 系统字体大小设置。)

  • 所有 ContainerControls 也必须使用相同的 AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);设置,假设是96dpi (请参阅下一个项目符号)和 MS Sans Serif 的默认字体(请参阅下面的项目符号2)。这是由设计器自动添加的 根据 DPI 你打开设计师在... 但失踪 我们许多最古老的设计器文件 VS2005之前的版本)没有正确地添加该内容

  • 做你所有的设计工作在96dpi (我们也许可以切换到 但是互联网上的智慧告诉我们要坚持96dpi; 实验在这里是有序的; 根据设计,它应该不重要,因为它只是改变了设计者插入的 AutoScaleDimensions行)。 若要将 VisualStudio 设置为在高分辨率显示器上以虚拟96dpi 运行, 找到它的.exe 文件,右键单击编辑属性,并在兼容性下 选择“覆盖高 DPI 缩放行为。由: System 执行缩放”。

  • 确保您从来没有在容器级别设置字体... 只有在 叶控件或在您的最基本窗体的构造函数中,如果您想要一个应用程序范围的默认字体,而不是 MSSansSerif。(在容器上设置字体似乎关闭了 该容器的自动缩放,因为它是按字母顺序排在 AutoScaleMode 设置和 AutoScale維 sions 设置之后的。)注意,如果你在你最基本的 Form 的构造函数中改变了字体,这将导致你的 AutoScale 维度计算不同于6x13; 特别是,如果你改为 Segoe UI (Win 10默认字体) ,那么它将是7x15... 你将需要触摸设计器中的每一个 Form,以便它可以重新计算其中的所有维度。设计器文件,包括 AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);

  • 不要使用锚 RightBottom锚定到用户控件... 其 定位将不会自动缩放; 相反,删除一个面板或其他 容器到 UserControl 中,并将其他控件锚定到 该面板; 让面板使用码头 RightBottom,或 Fill在您的 用户控制

  • 只有控件中的控件在 ResumeLayout结束时列出 的 InitializeComponent被称为将自动缩放... 如果你 动态添加控件,然后需要 SuspendLayout(); AutoScaleDimensions = new SizeF(6F, 13F); AutoScaleMode = AutoScaleMode.Font; 在你添加它之前,在那个控件上的 ResumeLayout(); 定位也需要调整,如果你不使用码头 模式或布局管理器,如 FlowLayoutPanelTableLayoutPanel

  • ContainerControl派生的基类应该将 AutoScaleMode设置为 Inherit(类 ContainerControl中的默认值设置; 但不是设计器设置的默认值)。如果你把它设置成其他的,然后你的派生类尝试把它设置成字体(应该是这样的) ,那么把它设置成 Font的行为将清除设计器的 AutoScaleDimensions设置,实际上导致关闭自动缩放!(这个准则与前一个准则相结合,意味着您永远不能在设计器中实例化基类... ... 所有类都需要被设计为基类或叶类!)

  • 避免在设计器中静态使用 Form.MaxSize。形式上的 MinSizeMaxSize不像其他任何东西那样伸缩。因此,如果你所有的工作都是在96dpi 下完成的,那么当 DPI 更高时,你的 MinSize不会引起问题,但是可能没有你预期的那么严格,但是你的 MaxSize可能会限制你的 Size 的缩放,这可能会引起问题。如果你想要 MinSize == Size == MaxSize,不要在设计器中这样做... 在你的构造函数中这样做或者 OnLoad覆盖... 将 MinSizeMaxSize都设置为适当缩放的大小。

  • 特定 PanelContainer上的所有控件应该使用锚定或对接。如果你把它们混合在一起,由 Panel实现的自动缩放通常会以微妙而奇怪的方式表现不当。

  • 当它进行自动缩放时,它将尝试缩放整个 Form... ... 然而,如果在这个过程中,它跑到屏幕大小的上限,这是一个硬性限制,然后可以搞砸(剪辑)缩放。因此,您应该确保 Designer 中所有100%/96dpi 的表单的大小不超过1024x720(相当于1080p 屏幕上的150% 或4K 屏幕上的 Windows 推荐值300%)。但是你需要减去巨大的 Win10标题/标题栏... 所以更像1000x680最大尺寸... 在设计师将像994x642客户端大小。(因此,您可以在 ClientSize 上执行 FindAll 引用来查找违规者。)

我的经历与当前最受欢迎的答案大相径庭。通过跨越。NET 框架代码和参考源代码,我得出的结论是,一切都在适当的自动伸缩的工作,只有一个微妙的问题,它搞砸了。事实证明这是真的。

如果你创建了一个适当的可回流/自动调整大小的布局,那么几乎所有的东西都会按照 Visual Studio 使用的默认设置(即,父窗体上的 AutoSizeMode = Font,其他窗体上的 Heritage it)自动完成工作。

唯一的问题是您是否已经在设计器的窗体上设置了 Font 属性。生成的代码将按字母顺序对分配进行排序,这意味着 AutoScaleDimensions将被分配 之前 Font。不幸的是,这完全破坏了 WinForms 的自动缩放逻辑。

解决方法很简单。要么根本不在设计器中设置 Font属性(在表单构造函数中设置它) ,要么手动重新排序这些分配(但是每次在设计器中编辑表单时都必须这样做)。瞧,近乎完美的全自动伸缩,最小的麻烦。甚至表单大小也被正确缩放。


我将在这里列出遇到的已知问题:

  • 嵌套的 TableLayoutPanel 不正确地计算控制边距。没有已知的解决办法-除了避免边距和填充完全-或避免嵌套的表布局面板。

我最近遇到过这个问题,特别是在高 dpi 系统上打开编辑器时,与 VisualStudio 重新缩放相结合时。我发现最好是使用 留着 AutoScaleMode = Font,但是要将 Forms 字体设置为默认字体,而是 以像素为单位指定大小,而不是 point,即: Font = MS Sans; 11px。在代码中,我 那么将字体重置为默认值: Font = SystemFonts.DefaultFont,一切正常。

只有我的意见。我认为我分享,因为 “保持 AutoScaleMode = Font”,和 “设置设计器的像素字体大小”是我没有在互联网上找到的东西。

我有一些更多的细节在我的博客: http://www.sgrottel.de/?p=1581&lang=en

将您的应用程序作为.Net Framework 4.7的目标,并在 Windows 10 v1703(Creators Update Build 15063)下运行它。

从.NET Framework 4.7开始,Windows 窗体包括 对常见的高 DPI 和动态 DPI 场景的增强 包括:

  • 改进了许多 Windows 窗体控件的缩放和布局,例如 MonthCalendar 控件和 CheckedListBox 控件

  • 单次缩放。在.NET Framework 4.6和更早版本中,缩放是通过多次缩放执行的,这导致 有些控件的缩放比实际需要的要多

  • 支持动态 DPI 方案,在此方案中,用户在 Windows 窗体应用程序已更改后更改 DPI 或缩放因子 发射

为了支持它,添加一个应用程序清单到您的应用程序和信号,您的应用程序支持 Windows10:

<compatibility xmlns="urn:schemas-microsoft.comn:compatibility.v1">
<application>
<!-- Windows 10 compatibility -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>

接下来,添加一个 app.config并声明应用程序 Per Monitor Aware. 这现在是在 app.config 中完成的,而不是像以前一样在清单中!

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

这个 PerMonitor V2是自 Windows10创建者更新:

DPI _ AWARENESS _ CONTEXT _ PER _ MONITOR _ AWARE _ V2

也被称为每显示器 v2。一个原始的进步 每个监视器的 DPI 感知模式,使应用程序能够访问 新的 DPI 相关的缩放行为在每个顶级窗口的基础上。

  • 子窗口 DPI 更改通知 -在每个 Monitor v2上下文中,任何 DPI 更改都会通知整个窗口树 发生

  • 缩放非客户区 -所有窗口都将自动以 DPI 敏感的方式绘制其非客户区。电话 EnableNonClientDpiScaling 是不必要的

  • S调用 Win32菜单-在每个监视器 v2上下文中创建的所有 NTUSER 菜单将以每个监视器的方式扩展。

  • 对话框缩放 -在 Per Monitor v2上下文中创建的 Win32对话框将自动响应 DPI 更改。

  • 改进的 comctl32控件的缩放 -各种 comctl32控件在 Per Monitor v2中改进了 DPI 缩放行为 上下文

  • 改进的主题化行为 -在 Per Monitor v2窗口上下文中打开的 UxTheme 句柄将根据 DPI 进行操作 与该窗口相关联

现在您可以订阅3个新事件来获得 DPI 更改的通知:

  • 当控件的 DPI 设置在 DPI 之后以编程方式更改时发生 其父控件或窗体的更改事件已发生

  • 当控件的 DPI 设置在 DPI 更改之前以编程方式更改时,触发 Control.DpiChangedBeforeParent 其父控件或窗体的事件已发生

  • Form. DpiChanged ,当当前显示窗体的显示设备上的 DPI 设置发生更改时触发。

您还有3个关于 DPI 处理/缩放的助手方法:

  • Control.LogicalToDeviceUnit ,它将值从逻辑像素转换为设备像素。

  • Control.ScaleBitmapLogicalToDevice ,它将位图图像缩放到设备的逻辑 DPI。

  • Control.DeviceDpi ,它返回当前设备的 DPI。

如果你仍然看到问题,你可以 通过 app.config 条目选择退出 DPI 改进

如果你没有访问源代码的权限,你可以进入应用程序属性的文件资源管理器,进入兼容性并选择 System (Enhanced)

enter image description here

它激活了 GDI 的规模,也改善了 DPI 的处理:

对于基于 GDI 的 Windows 应用程序,现在 DPI 可以扩展这些应用程序 这意味着这些应用程序, 神奇地,变成每个显示器 DPI 感知。

完成所有这些步骤之后,您应该可以为 WinForms 应用程序获得更好的 DPI 体验。但是请记住,您需要将您的应用程序定位为。Net 4.7,至少需要 Windows 10 Build 15063(Creators Update)。在下一个 Windows10Update1709中,我们可能会得到更多的改进。

我不得不仔细检查并修复了一大堆 WinForms 程序的伸缩性,其中至少有20个是由不同的人以不同的风格编写的。许多用户控件,分离器,锚,停靠,面板,自定义控件,动态布局代码等。我做了很多实验,但我想出了一个很好的处理方法。

正是这个答案让我走上了正确的道路: 试图使 WinForms 在4K 中看起来不错,但是在使用 AutoScaleMode 之后窗体太大了?

问题是,如果你有任何稍微复杂的东西,LayoutManager 倾向于破坏布局。调用 susendLayout () ,然后执行某些操作,然后再调用 ResumeLayout () ,这确实是个问题。(当您将用户控件与 TabControl 混合使用时,这也会对锚造成严重破坏。但这是另一个问题。)

关键是将表单上的 AutoScale維度和 AutoScaleMode 属性移动到 susendLayout ()/ResumeLayout ()之外,这样在它伸缩之前所有东西都会被正确地布局。由于窗体设计器可以按照自己的意愿对语句进行排序,所以只需从。Cs 文件,并将它们移动到构造函数中 InitializeComponent ()方法的右后面。

另一个重要的部分是将所有用户控件 AutoScaleMode 设置为 Heritage,而不是 font。这样一来,所有东西都可以一次缩放,而不是在用户控件中进行缩放,然后在添加到表单中时重新缩放。

在更改表单上的 AutoScaleMode 之前,我会递归地访问所有的控件,如果没有停靠,并且除了 Top | Left 之外还有一个锚点,我会暂时将锚点设置为 Top | Left,然后在设置 AutoScaleMode 之后将其恢复到原来的值。

做这三件事情能让我完成90% 的事情,而且几乎所有的事情都能自动运行。总之,这三样东西确保了每样东西都可以一次性按照同样的比例进行缩放。任何偏离这个模式的行为似乎都会导致布局的混乱。

在应用程序的开始部分使用 PInvoke user32.dll SetProcessDPIAware ()也是一个好主意。这似乎允许程序伸缩甚至在150% 时工作。我没有任何运气使它在设置 SetProcessDpi汪汪()或 SetProcessDpiawarenessContext ()时表现正常,无论我做什么,它们似乎都会导致布局混乱。