如何编写自动缩放到系统字体和 dpi 设置的 WinForms 代码?
Posted
技术标签:
【中文标题】如何编写自动缩放到系统字体和 dpi 设置的 WinForms 代码?【英文标题】:How to write WinForms code that auto-scales to system font and dpi settings? 【发布时间】:2014-05-09 05:36:35 【问题描述】:简介:有很多 cmets 说“WinForms 不能很好地自动缩放到 DPI/字体设置;切换到 WPF。”但是,我认为这是基于 .NET 1.1 的;看来他们实际上在 .NET 2.0 中实现自动缩放方面做得很好。至少基于我们迄今为止的研究和测试。但是,如果你们中的一些人知道得更好,我们很乐意听取您的意见。 (请不要争论我们应该切换到 WPF ......这不是现在的选择。)
问题:
WinForms 中的哪些内容不能正确自动缩放,因此应避免使用?
程序员在编写 WinForms 代码时应遵循哪些设计准则以使其能够很好地自动缩放?
到目前为止我们已经确定的设计指南:
请参阅下面的community wiki answer。
其中有任何不正确或不充分的吗?我们应该采用任何其他指导方针吗?还有其他需要避免的模式吗?任何其他有关这方面的指导将不胜感激。
【问题讨论】:
【参考方案1】:不支持正确缩放的控件:
Label
与 AutoSize = False
和 Font
继承。在控件上显式设置Font
,使其在“属性”窗口中以粗体显示。
ListView
列宽不缩放。覆盖表单的ScaleControl
来代替它。见this answer
SplitContainer
的 Panel1MinSize
、Panel2MinSize
和 SplitterDistance
属性
TextBox
与 MultiLine = True
和 Font
继承。在控件上显式设置Font
,使其在“属性”窗口中以粗体显示。
ToolStripButton
的图片。在表单的构造函数中:
ToolStrip.AutoSize = False
根据CreateGraphics.DpiX
和.DpiY
设置ToolStrip.ImageScalingSize
如果需要,设置ToolStrip.AutoSize = True
。
有时AutoSize
可以保留在True
,但有时如果没有这些步骤,它无法调整大小。无需更改即可使用 .NET Framework 4.5.2 和 EnableWindowsFormsHighDpiAutoResizing
。
TreeView
的图片。根据CreateGraphics.DpiX
和.DpiY
设置ImageList.ImageSize
。对于StateImageList
,在不改变.NET Framework 4.5.1 和EnableWindowsFormsHighDpiAutoResizing
的情况下工作。
Form
的大小。创建后手动缩放固定大小的Form
。
设计指南:
所有 ContainerControl 必须设置为相同的 AutoScaleMode = Font
。
(字体将处理 DPI 更改和对系统字体的更改
尺寸设置; DPI 将仅处理 DPI 更改,不处理
系统字体大小设置。)
所有 ContainerControls 也必须设置为相同的AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
,假设为 96dpi(参见下一个项目符号)和 MS Sans Serif 的默认字体(参见项目符号两个下方)。这是设计师自动添加的
根据您在其中打开设计器的 DPI... 但从
许多我们最古老的设计师文件。也许 Visual Studio .NET(
VS 2005 之前的版本)没有正确添加。
所有设计师的工作都以 96dpi(我们也许可以切换到
120dpi;但是互联网上的智慧说要坚持96dpi;
那里的实验是有序的;按照设计,这无关紧要,因为它只是更改了设计师插入的AutoScaleDimensions
行)。
要将 Visual Studio 设置为在高分辨率显示器上以虚拟 96dpi 运行,
找到它的 .exe 文件,右键单击以编辑属性,然后在兼容性下
选择“覆盖高 DPI 缩放行为。缩放执行者:系统”。
确保您从未在容器级别设置字体...仅在
如果您想要除 MS Sans Serif 之外的应用程序范围的默认字体,请在最基础表单的构造函数中使用叶控件或。 (在容器上设置字体似乎关闭
该容器的自动缩放,因为它按字母顺序出现在 AutoScaleMode 和 AutoScaleDimensions 设置的设置之后。)请注意,如果您确实更改了最基础表单的构造函数中的字体,这将导致您的 AutoScaleDimensions 计算不同于 6x13;特别是,如果您更改为 Segoe UI(Win 10 默认字体),那么它将是 7x15 ......您将需要触摸 Designer 中的每个表单,以便它可以重新计算该 .designer 文件中的所有尺寸,包括AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
。
不要使用锚定到用户控件的锚Right
或Bottom
...
定位不会自动缩放;相反,放下一个面板或其他
将容器放入您的 UserControl 并将您的其他控件锚定到
该小组;让面板使用 Dock Right
、Bottom
或
用户控件。
仅当 ResumeLayout
结尾时,控件列表中的控件
InitializeComponent
的调用将被自动缩放......如果你
动态添加控件,则需要SuspendLayout();
AutoScaleDimensions = new SizeF(6F, 13F);
AutoScaleMode = AutoScaleMode.Font;
ResumeLayout();
在添加之前在该控件上。还有你的
如果您不使用 Dock,也需要调整定位
模式或布局管理器,例如 FlowLayoutPanel
或 TableLayoutPanel
。
从ContainerControl
派生的基类应将AutoScaleMode
设置为Inherit
(类ContainerControl
中设置的默认值;但不是设计者设置的默认值)。如果您将其设置为其他任何内容,然后您的派生类尝试将其设置为 Font(应该如此),那么将其设置为 Font
的行为将清除设计者对 AutoScaleDimensions
的设置,从而导致实际切换关闭自动缩放! (本指南与之前的指南相结合意味着您永远无法在设计器中实例化基类......所有类都需要设计为基类或叶类!)
避免在设计器中静态/使用Form.MaxSize
。表单上的MinSize
和MaxSize
的缩放比例不如其他所有内容。因此,如果您以 96dpi 完成所有工作,那么在较高 DPI 下,您的 MinSize
不会引起问题,但可能不像您预期的那样严格,但您的 MaxSize
可能会限制您的尺寸缩放,这可能导致问题。如果您想要MinSize == Size == MaxSize
,请不要在设计器中执行此操作...在您的构造函数中执行此操作或OnLoad
覆盖...将MinSize
和MaxSize
设置为您适当缩放的大小。
特定Panel
或Container
上的所有控件都应使用锚定或对接。如果将它们混合使用,Panel
所做的自动缩放通常会以微妙而奇怪的方式出现异常。
当它进行自动缩放时,它会尝试缩放整个窗体...但是,如果在该过程中它遇到屏幕尺寸的上限,这是一个硬限制,可以然后拧紧(剪辑)缩放。因此,您应确保设计器中 100%/96dpi 的所有表单的大小不大于 1024x720(对应于 1080p 屏幕上的 150% 或 4K 屏幕上的 Windows 推荐值 300%)。但是你需要减去巨大的 Win10 标题/标题栏......所以更像是 1000x680 最大尺寸......在设计器中它将像 994x642 ClientSize。 (因此,您可以在 ClientSize 上执行 FindAll References 来查找违规者。)
【讨论】:
NumericUpDown
也无法正确缩放其Margin
。似乎边距被缩放了两次。如果我把它缩小一次,它看起来不错。
AutoScaleMode = Font
不适用于在 Ubuntu 上使用非常大字体的用户。我们更喜欢AutoScaleMode = DPI
> TextBox with MultiLine = True 和 Font 继承。一整天都在发疯——这就是解决办法!非常感谢!顺便说一句,同样的修复也是 ListBox 控件的修复。 :D
对我来说,继承字体的列表框不能很好地缩放。他们在明确设置后执行。 (.NET 4.7)
在this reddit thread dealing with winform scaling problem 我找到了这个链接to a Telerik Demo Monitor DPI Sample 免责声明我自己没有使用过。这个Telerik article is about scaling dpi settings【参考方案2】:
我的经历与当前投票最多的答案完全不同。通过单步调试 .NET 框架代码并仔细阅读参考源代码,我得出的结论是,自动缩放工作一切就绪,只是在某个地方出现了一个微妙的问题。事实证明这是真的。
如果你创建了一个正确的可重排/自动调整大小的布局,那么几乎所有的东西都会自动按照 Visual Studio 使用的默认设置(即 AutoSizeMode = Font 在父窗体上,而在其他所有东西上继承) )。
唯一的问题是,如果您在设计器中的表单上设置了 Font 属性。生成的代码将按字母顺序对分配进行排序,这意味着AutoScaleDimensions
将被分配之前 Font
。不幸的是,这完全破坏了 WinForms 的自动缩放逻辑。
虽然修复很简单。要么根本不在设计器中设置Font
属性(在表单构造函数中设置它),要么手动重新排序这些分配(但是每次在设计器中编辑表单时都必须继续这样做)。瞧,几乎完美的全自动缩放,麻烦最少。甚至表单大小也正确缩放。
我会在这里列出我遇到的已知问题:
嵌套TableLayoutPanel
calculates control margins incorrectly。除了完全避免边距和填充 - 或避免嵌套表格布局面板之外,没有已知的解决方法。
【讨论】:
不要在设计器中设置Font
: 想到一个想法:继续在设计器中设置字体,这样您就可以使用所需的字体进行设计。那么在构造函数中,布局后,读取该字体属性并再次设置相同的值?或者也许只是要求重新进行布局? [警告:我没有理由测试这种方法。] 或者根据 Knowleech's answer,在设计器中指定 像素(因此 Visual Studio 设计器不会在高 DPI 显示器上重新缩放),并且在代码读取该值,从像素转换为点(以获得正确的缩放)。
我们代码的每一位都在自动缩放模式之前设置了自动缩放尺寸,并且它们都可以完美缩放。似乎在大多数情况下顺序并不重要。
我在我的代码中搜索了 AutoScaleDimensions
未按照最佳答案中的建议设置为 new SizeF(6F, 13F)
的实例。事实证明,在每种情况下,表单的 Font 属性都已设置(不是默认设置)。看来AutoScaleMode = Font
,然后AutoScaleDimensions
是根据表单的字体属性计算的。此外,Windows 控制面板 中的 Scaling 设置似乎对AutoScaleDimensions
有影响。【参考方案3】:
将您的应用程序定位到 .Net Framework 4.7 并在 Windows 10 v1703(Creators Update Build 15063)下运行它。与.Net 4.7 under Windows 10 (v1703), MS made a lot of DPI improvements。
从 .NET Framework 4.7 开始,Windows 窗体包括 针对常见的高 DPI 和动态 DPI 场景的增强功能。这些 包括:
改进了许多 Windows 窗体控件的缩放和布局,例如 MonthCalendar 控件和 CheckedListBox 控件。
单程缩放。在 .NET Framework 4.6 及更早的版本中,缩放是通过多次传递执行的,这会导致 一些控件的扩展超出了必要的范围。
支持动态 DPI 方案,在这种方案中,用户在 Windows 窗体应用程序运行后更改 DPI 或比例因子 启动。
要支持它,请将应用程序清单添加到您的应用程序并表明您的应用程序支持 Windows 10:
<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>
此PerMonitorV2 是自 Windows 10 创意者更新以来的新内容:
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
也称为 Per Monitor v2。比原来的进步 每显示器 DPI 感知模式,使应用程序能够访问 基于每个***窗口的新 DPI 相关缩放行为。
子窗口 DPI 更改通知 - 在 Per Monitor v2 上下文中,通知整个窗口树任何 DPI 更改 发生。
非客户区缩放 - 所有窗口将自动以 DPI 敏感方式绘制其非客户区。来电 EnableNonClientDpiScaling 是不必要的。
SWin32 菜单的缩放 - 在每监视器 v2 上下文中创建的所有 NTUSER 菜单都将以每监视器的方式缩放。
对话框缩放 - 在 Per Monitor v2 上下文中创建的 Win32 对话框将自动响应 DPI 更改。
改进了 comctl32 控件的缩放 - 各种 comctl32 控件改进了 Per Monitor v2 中的 DPI 缩放行为 上下文。
改进的主题行为 - 在 Per Monitor v2 窗口的上下文中打开的 UxTheme 句柄将根据 DPI 运行 与该窗口相关联。
现在您可以订阅 3 个新事件以获得有关 DPI 更改的通知:
Control.DpiChangedAfterParent,在 DPI 之后以编程方式更改控件的 DPI 设置时触发 已发生其父控件或表单的更改事件。
Control.DpiChangedBeforeParent,当控件的 DPI 设置在 DPI 更改之前以编程方式更改时触发 其父控件或窗体的事件已发生。
Form.DpiChanged,当 DPI 设置在当前显示表单的显示设备上发生更改时触发。
您还有 3 个关于 DPI 处理/缩放的辅助方法:
Control.LogicalToDeviceUnits,将值从逻辑像素转换为设备像素。
Control.ScaleBitmapLogicalToDevice,将位图图像缩放为设备的逻辑 DPI。
Control.DeviceDpi,返回当前设备的 DPI。
如果您仍然发现问题,您可以opt-out of the DPI improvements via app.config entries。
如果您无权访问源代码,您可以在 Windows 资源管理器中转到应用程序属性,转到兼容性并选择 System (Enhanced)
激活 GDI 缩放以改善 DPI 处理:
对于基于 GDI 的应用程序,Windows 现在可以在 DPI 上进行缩放 每个监视器的基础。这意味着这些应用程序将 神奇地,成为每个显示器的 DPI 感知。
执行所有这些步骤,您应该会为 WinForms 应用程序获得更好的 DPI 体验。但请记住,您需要将您的应用程序定位为 .net 4.7,并且至少需要 Windows 10 Build 15063(Creators Update)。在下一个 Windows 10 更新 1709 中,我们可能会得到更多改进。
【讨论】:
【参考方案4】:我在工作中写的指南:
WPF 在“设备独立单元”中工作,这意味着所有控件都可以缩放 非常适合高 dpi 屏幕。在 WinForms 中,它需要更多的关注。
WinForms 以像素为单位。文本将根据系统 dpi 进行缩放,但通常会被未缩放的控件裁剪。为避免此类问题,您必须避免显式调整大小和定位。请遵守以下规则:
无论您在哪里找到它(标签、按钮、面板),都将 AutoSize 属性设置为 True。 对于布局,使用 FlowLayoutPanel(a la WPF StackPanel)和 TableLayoutPanel(a la WPF Grid)进行布局,而不是 vanilla 面板。 如果您在高 dpi 计算机上进行开发,Visual Studio 设计器可能会令人沮丧。当您设置 AutoSize=True 时,它将根据您的屏幕调整控件的大小。如果控件具有 AutoSizeMode=GrowOnly,则对于正常 dpi 的人来说,它将保持这个大小,即。比预期的要大。要解决此问题,请在具有正常 dpi 的计算机上打开设计器,然后右键单击、重置。
【讨论】:
对于可以在所有内容上调整 AutoSize 大小的对话框将是一场噩梦,我不希望我的按钮变得越来越小,因为我在运行程序时手动增加了我的对话框大小。【参考方案5】:我发现让 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
【讨论】:
【参考方案6】:我最近遇到了这个问题,尤其是在高 dpi 系统上打开编辑器时结合 Visual Studio 重新缩放。我发现最好保留 AutoScaleMode = Font
,但是将Forms Font设置为默认字体,但以像素为单位指定大小,而不是点,即:Font = MS Sans; 11px
。在代码中,我然后将字体重置为默认值:Font = SystemFonts.DefaultFont
,一切都很好。
只有我的两分钱。我想我分享一下,因为 “保持 AutoScaleMode=Font” 和 “为 Designer 设置字体大小(以像素为单位)” 是我在互联网上没有找到的。 p>
我的博客上有更多详细信息:http://www.sgrottel.de/?p=1581&lang=en
【讨论】:
【参考方案7】:除了锚点不能很好地工作之外:我会更进一步说精确定位(也就是使用 Location 属性)在字体缩放方面效果不佳。我不得不在两个不同的项目中解决这个问题。在这两者中,我们必须将所有 WinForms 控件的定位转换为使用 TableLayoutPanel 和 FlowLayoutPanel。在 TableLayoutPanel 中使用 Dock(通常设置为 Fill)属性效果很好,并且可以很好地与系统字体 DPI 一起缩放。
【讨论】:
【参考方案8】:我不得不对一大堆 WinForms 程序进行缩放并修复,其中至少有 20 个,由不同的人以不同的风格编写。大量的用户控件、拆分器、锚点、停靠、面板、自定义控件、动态布局代码等。这需要大量的实验,但我想我已经想出了一个很好的处理方法。
这个答案让我朝着正确的方向开始:Trying to make WinForms look good in 4K but forms too large after using AutoScaleMode.Dpi?
问题是如果您有任何稍微复杂的事情,LayoutManager 往往会破坏布局。调用 SuspendLayout() 然后做一些事情然后 ResumeLayout() 确实是一个问题。 (当您将用户控件与 TabControl 混合使用时,这也会对锚点造成严重破坏。但这是一个单独的问题。)
关键是将表单上的 AutoScaleDimension 和 AutoScaleMode 属性移到 SuspendLayout()/ResumeLayout() 之外,以便在缩放之前正确布局所有内容。由于表单设计器可以根据需要对语句进行排序,因此只需从 .Designer.cs 文件中删除这两行,并将它们移动到构造函数中 InitializeComponent() 方法的右侧。
另一个重要部分是将所有用户控件 AutoScaleMode 设置为 Inherit,而不是字体。这样一来,所有内容都会立即缩放,而不是在用户控件中进行缩放,然后在将内容添加到表单时重新缩放。
在更改表单上的 AutoScaleMode 之前,我递归地访问所有控件,以及任何未停靠且具有除 Top|Left 以外的锚点的任何内容,我暂时将锚点设置为 Top|Left,然后将其恢复为设置 AutoScaleMode 后的原始值。
做这三件事可以让我完成大约 90% 的工作,而且几乎所有事情都会自动进行。这三件事一起确保所有内容都按比例缩放一次,全部一起并达到相同的比例。任何与此模式的偏差似乎都会导致布局混乱。
在应用程序开始时 PInvoke user32.dll SetProcessDPIAware() 也是一个好主意。这似乎允许程序化扩展即使在 150% 时也能正常工作。在设置 SetProcessDpiAwareness() 或 SetProcessDpiAwarenessContext() 时,我没有任何运气让它正常运行,无论我做什么,它们似乎都会导致布局混乱。
【讨论】:
以上是关于如何编写自动缩放到系统字体和 dpi 设置的 WinForms 代码?的主要内容,如果未能解决你的问题,请参考以下文章
Win10 字体模糊解决(DPI缩放禁用),设置默认输入法英文