【分享】在不同DPI下设计和运行Windows Forms解决方案(不是高分屏也要看)
acmilan2016/03/17软件综合 IP:四川
设计阶段

Windows Forms设计器是为100%缩放(96DPI)的屏幕设计的,它在高分屏下有两个毛病,一是初始控件尺寸过小,二是在不同DPI缩放的设计环境间移植时,会产生整除误差,界面可能会变形。

Windows Forms不能用高分屏设计界面吗?非也!解决这个问题有两种方法(如果觉得以下方案不能满足你的需要,是时候选择WPF了):

第一种方法:使用网格避免整除误差

在选项中将Windows窗体设计器的LayoutMode(布局模式)改成SnapToGrid(对齐到网格),并将Default Grid Cell Size(默认网格大小)设为最小可缩放单元(或它的倍数),以避免移植时产生整除误差。同时由于这些单元是可见的,也使得将控件拖到合适的尺寸非常简单。

同时,应该将窗体的AutoScaleMode改为Dpi。默认的Font缩放使用系统默认字体的大小进行缩放,但是系统默认字体并不和DPI完全等比例,这样也会造成整除误差。

如果文字不能对齐的话,可以考虑调整TextAlign属性,比如单行Label推荐使用MiddleLeft、MiddleCenter或MiddleRight对齐方式,而不是默认的TopLeft对齐方式,以和其它控件的对齐方式统一。

最小可缩放单元如下表所示(最小可缩放单元=缩放/25%=DPI/24):
缩放    DPI值   最小可缩放单元
100%    96      4
125%    120     5
150%    144     6
175%    168     7
200%    192     8
225%    216     9
250%    240     10

Windows窗体设计器选项(150%):
Capture.png

效果:
Capture2 - Copy (2).png

第二种方法:使用布局容器进行布局

如果认真学习过WPF,就会知道WPF可通过Grid、StackPanel、WrapPanel等布局容器进行布局。实际上,Windows Forms也有两个这样的容器,叫做TableLayoutPanelFlowLayoutPanel,其中前者和Grid一样是表格布局容器,而后者和WrapPanel一样是流式布局容器。

在新建了窗体之后,按照以下流程使用TableLayoutPanel(表格布局面板)布局控件:

1.拖拉父窗口到合适大小,属性设置如下:
Padding:设置合适的内边距【最小可缩放单元(如150%缩放下是6)或它的倍数】

2.拖一个TableLayoutPanel控件,属性设置如下:
Dock:设置为Fill(停靠方式=填充)

3.根据需要分割出Columns(列)和Rows(行)
要求:一个单元格最多只能放置一个控件(或容器),但是一个控件可跨多行或多列

4.拖其它控件(或容器)到对应单元格,设置
Dock:设置为Fill(停靠方式=填充)
Margin:设置合适的外边距【最小可缩放单元(如150%缩放下是6)或它的倍数】
ColumnSpan:需要跨的列数
RowSpan:需要跨的行数

注意:Windows Forms中某些控件的Height属性有时不起作用(比如单行TextBox等),设置Dock属性并不能保证单元格被完全填充,但是只要文字仍然能够对齐就可以了。

FlowLayoutPanel(流式布局面板)和TableLayoutPanel有所不同,子控件要设置Width、Height、Margin属性,不能设置Dock属性,并且Width、Height和Margin一样也必须是最小可缩放单元的倍数。也就是说,仍然需要使用“对齐到网格”功能来完成设计。

运行阶段

默认情况下,Windows Forms程序由于高DPI支持不完整,在Windows Vista以后的系统中是由系统的DWM来缩放的(称为DPI虚拟化),这样的话会导致界面模糊。可以修改Program.cs并调用SetProcessDPIAware函数关闭DPI虚拟化,由程序自己来管理界面:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; // 导入System.Runtime.InteropServices命名空间
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        // 外部函数声明
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetModuleHandle(string name);
        // 这个函数只能接受ASCII,所以一定要设置CharSet = CharSet.Ansi,不然会失败
        [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
        private static extern IntPtr GetProcAddress(IntPtr hmod, string name);
        private delegate void FarProc();
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // SetProcessDPIAware是Vista以上才有的函数,需兼容XP的话不能直接调用,需按如下所示间接调用
            IntPtr hUser32 = GetModuleHandle("user32.dll");
            IntPtr addrSetProcessDPIAware = GetProcAddress(hUser32, "SetProcessDPIAware");
            if (addrSetProcessDPIAware != IntPtr.Zero)
            {
                FarProc SetProcessDPIAware = (FarProc)Marshal.GetDelegateForFunctionPointer(addrSetProcessDPIAware, typeof(FarProc));
                SetProcessDPIAware();
            }
            // C#的原有代码
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

如果不需要兼容Windows XP的话,可以更简单地直接调用SetProcessDPIAware:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; // 导入System.Runtime.InteropServices命名空间
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        // 外部函数声明
        [DllImport("user32.dll")]
        private static extern void SetProcessDPIAware();
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // SetProcessDPIAware是Vista以上才有的函数,这样直接调用会使得程序不兼容XP
            SetProcessDPIAware();
            // C#的原有代码
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

.NET Framework 4.5.1提供了优化的高DPI支持,但需要手动开启,在工程中添加XXXXXXnfig(发布时需要将.exe文件和.XXXXXXnfig文件一起发布),内容如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appsettings>
        <add key="EnableWindowsFormsHighDpiAutoResizing" value="true">
    </add></appsettings>
</configuration>

[修改于 8年10个月前 - 2016/03/21 12:26:57]

来自:计算机科学 / 软件综合
5
 
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
acmilan 作者
8年10个月前 修改于 8年10个月前 IP:四川
813061
WIndows对DPI缩放的支持

DPI设置的位置
【Windows XP】——右键桌面——属性——设置——高级
【Windows Vista】——右键桌面——个性化——调整字体大小(DPI)
【Windows 7、8、8.1】——右键桌面——屏幕分辨率——放大或缩小文本和其它项目
【Windows 10】——右键桌面——显示设置

可见随着高分辨率LCD显示屏的普及,分辨率设置越来越不重要,而DPI设置越来越重要了。

支持的缩放比例
【Windows XP、Vista、7、8】——最高可开到199%(191 DPI),超过这个比例,会导致鼠标指针不正常、抗锯齿失效
【Windows 8.1、10】——最高可开到500%(480 DPI)

注意:199%缩放比例不能当作200%缩放比例使用,不要在199%缩放比例下进行Windows Forms设计。

如何应用DPI设置
【Windows XP、Vista】——需重新启动
【Windows 7、8、8.1】——需重新登录
【Windows 10】——不需重新登录【对于未实现DPI动态调整的Win32程序,需重新登录】

适用范围
【Windows XP、Vista、7、8】——系统级DPI
【Windows 8.1、10】——系统级DPI、逐显示器级DPI【对于未实现DPI动态调整的Win32程序,不支持逐显示器级DPI】
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年10个月前 修改于 8年10个月前 IP:四川
813183
Windows Forms有很多控件限制Height不能改变,或者只能步进改变,这是很无理的做法。
Windows Forms还不允许对TableLayoutPanel中的控件进行垂直居中。
这两点让界面布局变得十分困难,只能通过设计器手动对齐文本来解决,这是非常难以维护的。
至于为什么限制Height,可能只是为了演示一个.NET特性:属性。这显然是微软“为了编程而编程”的思想在起作用。

WPF从界面到图形绘制,参数都是double类型,不会产生整除误差。
WPF完全自适应DPI缩放,无需人工干预。
WPF中Height取值自然也没有任何限制。
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年10个月前 修改于 8年10个月前 IP:四川
815348
第三种方法:使用Windows 8.1以上版本的DPI虚拟化锁定机制

在Windows 8.1以上版本中,新增了SetProcessDpiAwareness(int level)函数作为DPI支持级别的设置函数。这个函数有三个级别:0表示不支持DPI缩放,1表示支持系统级DPI缩放,2表示支持逐显示器DPI缩放。每个进程只能设置一次DPI级别,一旦设定了级别,其它任何改变级别的尝试都会失败(包括老的SetProcessDPIAware函数)。

在manifest资源中,可以设置<dpiAware>选项来设置DPI缩放级别,设置为false相当于调用SetProcessDpiAwareness(0),设置为true相当于调用SetProcessDpiAwareness(1),设置为true/pm或per monitor相当于调用SetProcessDpiAwareness(2)。

通过在manifest资源中设置<dpiAware>为false,可以让PE Loader在程序启动时调用SetProcessDpiAwareness(0),也就声明了程序不支持DPI缩放,同时屏蔽了任何后续函数调用,也就强制开启了DPI虚拟化。

此方法对Windows 7无效,因为Windows 7中不存在SetProcessDpiAwareness函数,设置<dpiAware>为true相当于调用SetProcessDPIAware,而设置为false相当于什么都没做,当然不可能锁定DPI级别了。

具体方法请见此帖:【技术】彻底解决WinForms在高DPI下的设计问题

Capture3.png

Capture4.png
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
acmilan作者
8年10个月前 修改于 8年9个月前 IP:四川
815360
【更正】Windows对DPI缩放的支持

【Windows XP、Vista】—— 最高可开到500% (480 DPI),但超过155% (149 DPI)鼠标指针会糊掉
【Windows 7、8】—— 最高可开到500% (480 DPI),但超过199% (191 DPI)鼠标指针会糊掉
【Windows 8.1、10】——最高可开到500% (480 DPI)
引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
acmilan
进士 学者 笔友
文章
461
回复
2934
学术分
4
2009/05/30注册,5年11个月前活动
暂无简介
主体类型:个人
所属领域:无
认证方式:邮箱
IP归属地:未同步
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}