如何编写WinForms代码以自动适应系统字体和dpi设置?

170

介绍:有很多评论称“WinForms无法良好自适应DPI/字体设置,应转向WPF”。然而,我认为这种说法基于.NET 1.1版本;实际上,在.NET 2.0中,他们似乎已经非常好地实现了自适应。至少根据我们目前的研究和测试结果来看是这样的。但是,如果你们中有更好的方法,请与我们分享。(请不要争论我们应该转向WPF……目前不可行)

问题:

  • 在WinForms中,什么不能良好自适应,因此应该避免使用?

  • 程序员在编写WinForms代码时应遵循哪些设计准则,以确保其能够良好自适应?

迄今为止我们已确定的设计准则:

请参阅下面的社区公共答案

上述准则是否有误或不足?我们应采用其他哪些准则?还有哪些模式需要避免?对此的任何其他指导将非常感激。

8个回答

163
###不支持正确缩放的控件:
  • Label带有AutoSize = False和继承的Font。在控件上明确设置Font,以便在属性窗口中以粗体显示。
  • ListView列宽不会自动调整。覆盖表单的ScaleControl方法来实现。参见this answer
  • SplitContainerPanel1MinSizePanel2MinSizeSplitterDistance属性
  • TextBox带有MultiLine = True和继承的Font。在控件上明确设置Font,以便在属性窗口中以粗体显示。
  • ToolStripButton的图像。在表单的构造函数中:
  • 设置ToolStrip.AutoSize = False
  • 根据CreateGraphics.DpiX.DpiY设置ToolStrip.ImageScalingSize
  • 如果需要,设置ToolStrip.AutoSize = True
  • PictureBox.SizeMode必须设置为Zoom或StretchImage。
有时候,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(请参见下面的项目符号)。这是由设计师根据您在设计师中打开的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文件中的所有维度,包括AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);

  • 不要在UserControl上使用锚定RightBottom...它的定位将不会自动缩放;相反,在您的UserControl中放置一个Panel或其他容器,并将其他控件锚定到该面板;让面板在您的UserControl中使用Dock RightBottomFill

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

  • 派生自ContainerControl的基类应保持AutoScaleMode设置为Inherit(类ContainerControl中设置的默认值;但设计师设置的默认值不是)。如果将它设置为其他任何值,然后您的派生类尝试将其设置为Font(应该这样做),


NumericUpDownMargin 也不能很好地进行缩放,似乎它被缩放了两次。如果我将其缩放一次,它看起来就好了。 - ygoe
具有MultiLine = True和继承字体的文本框。整天都在发疯 - 这就是解决方法!非常感谢!顺便说一句,相同的解决方法也适用于ListBox控件。 :D - neminem
在这个 Reddit 帖子中,讨论了 Winform 缩放问题,我找到了这个链接,指向 Telerik Demo Monitor DPI 示例。免责声明:我自己没有使用过它。这篇 Telerik 文章是关于缩放 DPI 设置的。 - surfmuggle
我希望我可以多次点赞。非常有用的信息! - rory.ap
非常感谢您的出色回答!只有一个小修正:在1920x1080上,150%不是更像1280x720(而不是您提供的1024x720)吗?因此,最终客户端大小将为1252x642,对吗? - Dani Neuss
显示剩余4条评论

30

我的经验与当前得票最高的答案有所不同。通过逐步分析.NET框架代码和查看参考源代码,我得出结论:自动缩放的一切都已就绪,只是某个微小的问题搞乱了它。这证明是正确的。

如果创建一个适当的可重新布局/自动调整大小的布局,则几乎所有东西都会按照应该的方式自动处理,使用默认设置(即Visual Studio使用的AutoSizeMode = Font和Inherit)。除非您在设计器中设置了表单的Font属性,否则唯一的问题是。生成的代码将按字母顺序排序赋值,这意味着AutoScaleDimensions将在Font之前赋值。不幸的是,这完全破坏了WinForms自动缩放逻辑。

但解决方法很简单。要么根本不在设计器中设置Font属性(在表单构造函数中设置),要么手动重新排序这些赋值(但这样你每次在设计器中编辑表单时都必须这样做)。嗨,近乎完美和完全自动的缩放,几乎零麻烦。甚至窗体大小也可以正确缩放。


我将在遇到问题时列出已知问题:

  • 嵌套TableLayoutPanel在高DPI监视器上会不正确计算控件边距,目前没有已知的解决方法,除非完全避免使用边距和填充,或避免嵌套表格布局面板。

1
在设计器中未设置“Font”的情况下:一个想法浮现在脑海中:在设计器中设置字体,以便您可以使用所需的字体进行设计。然后在构造函数中,在布局之后,读取该字体属性并再次设置相同的值?或者只是要求重新执行布局?[注意:我没有理由测试这种方法。] 或者根据Knowleech的答案,在设计器中指定像素(因此Visual Studio设计器不会在高DPI监视器上重新缩放),并在代码中读取该值,将其从像素转换为点(以获得正确的缩放)。 - ToolmakerSteve
1
我们的每一行代码都在自动缩放模式之前设置了正确的自动缩放尺寸,因此所有内容都可以完美地缩放。似乎在大多数情况下顺序并不重要。 - Josh
我搜索了我的代码,查找了所有未将AutoScaleDimensions设置为建议的new SizeF(6F, 13F)的实例。结果发现,在每个实例中,窗体的字体属性都已被设置(而不是默认值)。看起来当AutoScaleMode = Font时,AutoScaleDimensions是基于窗体的字体属性计算的。此外,Windows控制面板中的缩放设置似乎会影响AutoScaleDimensions - Walter Stabosz

27

针对 .Net Framework 4.7 的应用程序,并在 Windows 10 v1703(创作者更新版本号15063)下运行。使用Windows 10下的.Net 4.7(v1703),微软进行了很多DPI改进

从 .NET Framework 4.7 开始,Windows Forms 包括了许多高 DPI 和动态 DPI 场景的增强,其中包括:

  • 提高了一些 Windows Forms 控件的缩放和布局,例如 MonthCalendar 控件和 CheckedListBox 控件。

  • 单次缩放,.NET Framework 4.6 及更早版本的缩放是通过多次传递来完成的,这导致某些控件被缩放得过多。

  • 支持动态 DPI 场景,即用户在启动 Windows Forms 应用程序后更改 DPI 或比例因子。

为了支持它,请向您的应用程序添加一个应用程序清单(manifest),并表明您的应用程序支持 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 Creator 更新后的新功能:

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 控件的改进缩放行为 - 在 Per Monitor v2 上下文中,各种 comctl32 控件具有改进的 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。

如果仍然存在问题,您可以通过应用程序配置条目退出 DPI 改进

如果您无法访问源代码,则可以在 Windows Explorer 中的应用程序属性中选择兼容性,然后选择 System (Enhanced)

enter image description here

这将激活 GDI


13

我在工作中编写的一份指南:

WPF 使用“独立于设备的单位”,这意味着所有控件都可以完美地适应高 DPI 屏幕。在 WinForms 中需要更加小心。

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

  1. 在所有地方(标签、按钮、面板)将 AutoSize 属性设置为 True。
  2. 对于布局,请使用 FlowLayoutPanel(类似于 WPF StackPanel)和 TableLayoutPanel(类似于 WPF Grid)进行布局,而不是普通 Panel。
  3. 如果您在高 DPI 计算机上开发,则 Visual Studio 设计器可能会引起沮丧。当您设置 AutoSize=True 时,它将调整控件大小以适应您的屏幕。如果控件具有 AutoSizeMode=GrowOnly,则它将保持此大小,适用于正常 DPI 的人,即比预期的要大。要解决此问题,请在具有正常 DPI 的计算机上打开设计器,然后右键单击重置。

3
对于可调整大小的对话框,自动调整大小可能会带来麻烦,我不希望在手动调整对话框大小时运行程序时我的按钮变得越来越大或越来越小。 - Josh

10

我发现让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

7
我最近遇到了这个问题,特别是在高dpi系统上打开编辑器时与Visual Studio重新缩放相结合。我发现最好保持AutoScaleMode = Font,但将表单的Font设置为默认字体,但以像素而不是点指定大小,即:Font = MS Sans; 11px。在代码中,我然后将字体重置为默认值:Font = SystemFonts.DefaultFont,一切都很好。
这只是我的个人见解。我想分享一下,因为“保持AutoScaleMode = Font”和“为设计师设置像素字体大小”是我在互联网上找不到的东西。
我在我的博客上有更多细节:http://www.sgrottel.de/?p=1581&lang=en

4

除了锚点不起作用之外:我想再进一步说,精确定位(即使用位置属性)与字体缩放不太兼容。我已经在两个不同的项目中解决了这个问题。在这两个项目中,我们不得不将所有WinForms控件的定位转换为使用TableLayoutPanel和FlowLayoutPanel。在TableLayoutPanel中使用Dock属性(通常设置为Fill)非常好用,可以很好地缩放系统字体DPI。


3
我不得不处理一堆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,而不是Font。这样一次性缩放所有内容,而不是在用户控件中进行缩放,然后在添加到窗体时重新缩放内容。
在更改窗体的AutoScaleMode之前,我会递归地访问所有控件,并将任何未停靠且锚点不是Top|Left的控件临时将锚点设置为Top|Left,在设置AutoScaleMode后恢复其原始值。
做这三件事可以让我完成90%的工作,几乎所有东西都可以自动处理好。这三件事共同确保一次性缩放所有内容,并以相同的比例进行缩放。任何偏离这种模式的做法似乎都会导致布局混乱。
在应用程序开头调用PInvoke user32.dll SetProcessDPIAware()也是一个好主意。这似乎允许程序化缩放即使达到了150%。我没有成功地使SetProcessDpiAwareness()或SetProcessDpiAwarenessContext()正常工作,无论我做什么都似乎会导致布局混乱。

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