繁体   English   中英

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

[英]How to write WinForms code that auto-scales to system font and dpi settings?

简介:有很多评论说“WinForms 不能很好地自动缩放到 DPI/字体设置;切换到 WPF。” 但是,我认为这是基于 .NET 1.1; 看起来他们实际上在 .NET 2.0 中实现自动缩放方面做得很好。 至少基于我们迄今为止的研究和测试。 但是,如果你们中的一些人知道得更好,我们很乐意听取您的意见。 (请不要争论我们应该切换到 WPF ......现在这不是一个选择。)

问题:

  • WinForms 中的哪些内容不能正确自动缩放,因此应该避免?

  • 程序员在编写 WinForms 代码时应该遵循哪些设计准则,以便它能够很好地自动缩放?

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

请参阅下面的社区维基答案

其中有哪些是不正确的或不充分的? 我们应该采用任何其他准则吗? 还有其他需要避免的模式吗? 对此的任何其他指导将不胜感激。

不支持正确缩放的控件:

  • AutoSize = FalseFont继承的Label 在控件上显式设置Font ,使其在“属性”窗口中以粗体显示。
  • ListView列宽不缩放。 覆盖窗体的ScaleControl来代替它。 看到这个答案
  • SplitContainerPanel1MinSizePanel2MinSizeSplitterDistance属性
  • 带有MultiLine = TrueFont继承的TextBox 在控件上显式设置Font ,使其在“属性”窗口中以粗体显示。
  • ToolStripButton的图像。 在表单的构造函数中:

    • 设置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无需更改StateImageList工作。
  • Form的大小。 创建后手动缩放固定大小的Form

设计指南:

  • 所有 ContainerControls 必须设置为相同的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,则只能在叶控件上或在你最基本的 Form 的构造函数中。 (在容器上设置字体似乎关闭了该容器的自动缩放,因为它按字母顺序出现在 AutoScaleMode 和 AutoScaleDimensions 设置的设置之后。)请注意,如果您在最基本的 Form 的构造函数中更改字体,那将导致您的 AutoScaleDimensions 以不同于 6x13 的方式计算; 特别是,如果您更改为 Segoe UI(Win 10 默认字体),那么它将是 7x15...您需要触摸设计器中的每个表单,以便它可以重新计算该 .designer 文件中的所有尺寸,包括AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); .

  • 不要使用 Anchor RightBottom锚定到 UserControl...它的定位不会自动缩放; 相反,将面板或其他容器放入您的 UserControl 并将您的其他控件锚定到该面板; 让面板使用 Dock RightBottomFill您的 UserControl。

  • 只有在调用InitializeComponent末尾的ResumeLayout时Controls 列表中的控件才会自动缩放……如果动态添加控件,则需要SuspendLayout(); AutoScaleDimensions = new SizeF(6F, 13F); AutoScaleMode = AutoScaleMode.Font; ResumeLayout(); 在添加它之前,在该控件上。如果您不使用 Dock 模式或布局管理器(如FlowLayoutPanelTableLayoutPanel则还需要调整您的定位。

  • ContainerControl派生的基类应将AutoScaleMode设置为Inherit (在ContainerControl类中设置的默认值;但不是设计器设置的默认值)。 如果您将其设置为其他任何内容,然后您的派生类尝试将其设置为 Font(应该如此),那么将其设置为Font的行为将清除设计者的AutoScaleDimensions设置,从而导致实际关闭自动缩放! (本指南与之前的指南相结合意味着您永远无法在设计器中实例化基类……所有类都需要设计为基类或叶类!)

  • 避免在设计器中静态/使用Form.MaxSize MinSize上的MinSizeMaxSize不像其他所有东西那样缩放。 因此,如果您在 96dpi 中完成所有工作,那么在更高的 DPI 下,您的MinSize不会引起问题,但可能不会像您预期的那样严格,但您的MaxSize可能会限制您的 Size 缩放,这可能会导致问题。 如果您想要MinSize == Size == MaxSize ,请不要在设计器中执行此操作...在您的构造函数或OnLoad覆盖中执行此操作...将MinSizeMaxSize设置为您正确缩放的大小。

  • 特定PanelContainer上的所有控件都应使用锚定或停靠。 如果将它们混合使用,该Panel所做的自动缩放通常会以微妙的奇怪方式出现异常行为。

  • 当它进行自动缩放时,它会尝试缩放整个表单......但是,如果在该过程中它遇到了屏幕尺寸的上限,那就是一个硬限制,然后可能会搞砸(剪辑)缩放。 因此,您应该确保设计器中 100%/96dpi 的所有窗体的大小不大于 1024x720(对应于 1080p 屏幕上的 150% 或 4K 屏幕上的 Windows 推荐值 300%)。 但是你需要减去巨大的 Win10 标题/标题栏......所以更像是 1000x680 最大尺寸......在设计器中它就像 994x642 ClientSize。 (因此,您可以在 ClientSize 上执行 FindAll References 以查找违规者。)

我的经历与当前投票最高的答案完全不同。 通过逐步完成 .NET 框架代码并仔细阅读参考源代码,我得出结论,一切都已准备就绪,可以自动缩放工作,并且只有一个微妙的问题将其搞砸了。 事实证明这是真的。

如果您创建了一个适当的可重排/自动调整大小的布局,那么几乎所有内容都会自动按照 Visual Studio 使用的默认设置(即父窗体上的 AutoSizeMode = Font 和其他所有内容的 Inherit )自动运行。

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

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


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

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

将您的应用程序定位到 .Net Framework 4.7 并在 Windows 10 v1703(Creators Update Build 15063)下运行它。 在 Windows 10 (v1703) 下使用.Net 4.7,MS 对 DPI 进行了大量改进

从 .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。

  • Win32 菜单的缩放 - 在 Per Monitor 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。

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

如果您无权访问源代码,可以转到 Windows 资源管理器中的应用程序属性,转到兼容性并选择System (Enhanced)

在此处输入图片说明

这会激活 GDI 缩放以改善 DPI 处理:

对于基于 GDI 的应用程序,Windows 现在可以在每个显示器的基础上进行 DPI 缩放。 这意味着这些应用程序将神奇地成为每个显示器的 DPI 感知。

执行所有这些步骤,您应该会为 WinForms 应用程序获得更好的 DPI 体验。 但请记住,您需要将您的应用定位为 .net 4.7,并且至少需要 Windows 10 Build 15063(Creators Update)。 在接下来的 Windows 10 Update 1709 中,我们可能会得到更多改进。

我在工作中写的指南:

WPF 在“设备独立单元”中工作,这意味着所有控件都可以完美地缩放到高 dpi 屏幕。 在 WinForms 中,它需要更加小心。

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

  1. 无论您在何处找到它(标签、按钮、面板),都将 AutoSize 属性设置为 True。
  2. 对于布局,使用 FlowLayoutPanel(a la WPF StackPanel)和 TableLayoutPanel(a la WPF Grid)进行布局,而不是 vanilla Panel。
  3. 如果您在高 dpi 机器上进行开发,Visual Studio 设计器可能会令人沮丧。 当您设置 AutoSize=True 时,它​​将根据您的屏幕调整控件的大小。 如果控件具有 AutoSizeMode=GrowOnly,它将为正常 dpi 的人保持此大小,即。 比预期的要大。 要解决此问题,请在具有正常 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

我最近遇到了这个问题,尤其是在高 dpi 系统上打开编辑器时,与 Visual Studio 重新缩放结合使用。 我发现最好保持AutoScaleMode = Font ,但将 Forms Font设置为默认字体,但以像素为单位指定大小,而不是点,即: Font = MS Sans; 11px Font = MS Sans; 11px 在代码中,我然后将字体重置为默认值: Font = SystemFonts.DefaultFont一切正常。

只有我的两分钱。 我想我会分享,因为“保持 AutoScaleMode=Font”“以像素为单位为设计器设置字体大小”是我在互联网上找不到的东西。

我的博客上有更多详细信息: http : //www.sgrottel.de/? p = 1581&lang= en

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

我不得不通过并修复一大堆 WinForms 程序的缩放,其中至少有 20 个,由不同的人以不同的风格编写。 很多用户控件、拆分器、锚点、停靠、面板、自定义控件、动态布局代码等。这需要大量的实验,但我想我已经想出了一个很好的处理方法。

这个答案让我朝着正确的方向开始: 尝试使 WinForms 在 4K 中看起来不错,但在使用 AutoScaleMode.Dpi 后表单太大?

问题是如果您有任何稍微复杂的东西, LayoutManager 往往会破坏布局。 调用 SuspendLayout() 然后做东西然后 ResumeLayout() 真的是一个问题。 (当您将用户控件与 TabControl 混合使用时,这也会对锚点造成严重破坏。但这是一个单独的问题。)

关键是将窗体上的 AutoScaleDimension 和 AutoScaleMode 属性移到 SuspendLayout()/ResumeLayout() 之外,这样一切都会在缩放之前正确布局。 由于表单设计器会根据需要对语句进行排序,因此只需从 .Designer.cs 文件中删除这两行,并将它们移到构造函数中 InitializeComponent() 方法之后的右侧。

另一个重要部分是将所有用户控件 AutoScaleMode 设置为继承,而不是字体。 这样,所有内容都可以一次性缩放,而不是在用户控件中进行缩放,然后在将内容添加到表单时重新缩放。

在更改窗体上的 AutoScaleMode 之前,我递归访问所有控件,以及任何未停靠且具有除 Top|Left 之外的锚点的任何内容,我将锚点临时设置为 Top|Left,然后将其恢复为原始值设置 AutoScaleMode 后。

做这三件事让我完成了大约 90% 的工作,并且几乎所有事情都会自动运行。 总之,这 3 件事确保所有东西都缩放一次,一起缩放,并且比例相同。 任何偏离这种模式似乎都会导致布局混乱。

在应用程序的开头调用 user32.dll SetProcessDPIAware() 也是一个好主意。 这似乎允许程序化缩放即使在 150% 时也能工作。 在设置 SetProcessDpiAwareness() 或 SetProcessDpiAwarenessContext() 时,我没有任何运气使其行为正常,无论我做什么,它们似乎都会导致布局混乱。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM