一.引子
在Window7中,有一个可以“调整字体大小”的功能。这个功能只要在桌面的空白区域,点击右键,然后在弹出的菜单中选择“个性化”,就会弹出如下的界面。如图1:
点击“显示”之后就会在右侧的区域内看到是“使阅读屏幕上的内容更容易”。在其下有三个选项,分别是“较小(S)-100%(默认)”、“中等(M)-125%和较大(L)-150%”。在正常的状态下,当前屏幕处于“较小(S)-100%(默认)”的这个选项。如下图2:
当然如果你不知道这个的功能除了看上面的文字解释。还可以直接尝试换一个选项试一下效果。这里就不多解释它的作用效果是怎么样的了。当你选择之后,在重启过电脑,再次打开系统的时候。首先可以看到屏幕的高宽发生了一些变化,也可以看到屏幕上的内容被放大了很多。
其实在背后,是屏幕的dpi在作怪。
这个除了会影响我们的使用电脑的时候的感受,对于我们开发人员来说,会对我们开发人员的程序造成很大的影响。不但会使程序中的内容大小不统一,还会造成控件显示布局错乱,甚至会造成程序无法正常运行的问题。
下面我们就此问题进行一下研究和讨论。这里我们称这个调整为“SCT
S DPI”调整。
二.目录
在进行很深入的探究之前,我们先对一些比较基本的概念进行一下阐述。以便对之后的讨论更好的叙述。为了使大家看的方便,这边无论内容是多是少,还是分了一个目录进行阐述,以便有一个清晰的条理。
目录
四. WPF和Winform的DPI的区别、总结以及一些注意的事项
1. WPF和Winform在设置了SCTS DPI时的行为可能会不一样
4.关于Webbrower在“SCTS DPI”下的放大问题
(1)关于WPF中使用WindowsFormsHost承载Winform控件时大小的问题
三.一些基本的概念
这里有一篇对基础知识讲的很不错的文章,来自博客园杨新华的文章《WPF中的DPI问题》 。
四. WPF和Winform的DPI的区别、总结以及一些注意的事项
这边为了描述方便概念,把上面的个性化中的放大设置统称为“SCTS DPI”,这也是从上面第二点中的文章中“Set Custom Text Size(DPI)”用大写字母的简称。
除此之外,引入一个“Winform域”的概念。就是在Winform窗体或者控件的直级管辖的子控件或者多级管辖但不使用任何WPF的“程序域”叫做“Winform域”。同理“WPF域”也类似定义。下面我就来讲讲相关事宜:
1. WPF和Winform在设置了SCTS DPI时的行为可能会不一样
在说区别之前要明确一个概念,就是你在系统个性化中设置的“100%”、“125%”和“150%”就真的会在WPF和Winform的窗体或者控件中,对应放大“125%”或者“150%”吗?答案当然是不是。
这个“SCTS DPI”在WPF和Winform中会有不同的作用结果。在WPF中,你放大的“SCTS DPI”可以看做是WPF窗体或者控件的放大倍数,由于WPF的机制,使得它的放大完全是按照设置的“125%”或者“150%”的值对应倍数放大。
相对于Winform的话,Winform就不一定会在“125%”或者“150%”的情况下放大成对应的倍数。经过测试发现,在我公司的虚拟机上跑的时候,的确会放大为对应的倍数;而在单机模式下(不使用虚拟机的情况下),无论是选择“125%”和“150%”,Winform的窗体或者控件都不会放大。
最后,经过多次尝试得出如下结论。放大的倍数和在Winform域或者WPF域中的dpi有直接关系,而不是直接等同于选了什么“SCTS DPI”就是放大多少,需要通过下面的计算而得到。有如下公式:
放大倍数=在Winform域或者WPF域中的dpi / 96
实际上某些程序的行为会影响winform的放大倍数,如下代码:
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.Font = new System.Drawing.Font("Arial", 14F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel, ((byte)(0)));
2.WPF和Winform的SCTS DPI的区别:
(1)首先第一个区别就是,WPF在SCTS DPI下的放大完全是由于WPF的“独立设备单位”进行测量“独立”分辨率计算方式造成的,长和宽在对应的“125%”和“150%”会出现放大对应的1.25倍和1.5倍;而Winform的放大并不是如此,而是系统为了“迎合”看不清文字的“老年人”按照一定的算法规则故意放大的。
(2)WPF在SCTS DPI下,长和宽的放大的系数完全是一致的,即在“125%”的情况下长和宽都会放大1.25倍,而在“150%”的情况下长和宽会放大1.5倍;而Winform在长度上确实也满足这个规则,而在宽度上并不是满足这个规则,而是以另外的一个系数进行放大。WPF和Winform的放大情况如下表1和表2:
| 长度(高度)放大系数 | 宽度放大系数 |
---|---|---|
100%(当前dpi/96) | 1 | 1 |
125%(当前dpi/96) | 1.25 | 4/3 |
150%(当前dpi/96) | 1.5 | 1.5 |
| 长度(高度)放大系数 | 宽度放大系数 |
---|---|---|
100%(当前dpi/96) | 1 | 1 |
125%(当前dpi/96) | 1.25 | 1.25 |
150%(当前dpi/96) | 1.5 | 1.5 |
注:上面的百分比,在第1点中已经说了,这里是通过dpi的计算得到的。
3.WPF和Winform的控件和窗体之间互相嵌套规则:
(1)Winform中的直接控件的规则按照上面Winform的表的规则来进行放大;
(2)WPF中的直接控件规则按照上面的WPF的表的规则来进行放大;
(3)Winform中有WPF Control,对于WPF Control来说,大小什么按照Winform的规则来处理。而其内部的元素按照WPF的规则来放大。
(4)Winform中有Winform Control,对于他们来说都按照Winform的表来进行放大;WPF中有WPF Control也是同理按WPF规则放大。
(5)WPF中通过了WindowsFormsHost来承载在Winform Control,那么对于WindowsFormsHost来说它会按照WPF的规则来,而里面的Winform Control会按照Winform的规则来进行放大。
(6)无论是控件如何相互嵌套,都会按照(1)~(5)的规则进行解释和处理;
4.关于Webbrower在“SCTS DPI”下的放大问题
无论是WPF还是Winform,在“SCTS DPI”下,Webbrower控件都会按照上面说的规则进行放大和缩小。但是对于Webbrower中的内容来说,它不会受到“SCTS DPI”的影响,即在任何的情况下,都会按照正常的大小显示。
5.关于Image在“SCTS DPI”下的放大问题
对于加载的图片来说,当然是会按照上面讲的规则来进行放大。但是这个不是讨论的重点。由于放大图片的时候,会导致图片失真,所以完全可以使用缩小图片或者使用矢量图的方法来规避这个问题。
四.如何获取DPI
在上面的第三点的讨论中,我们提到了通过dpi的计算来得到,那么我们应该用什么技术来获取dpi呢?下面我们就来讨论各种方法。
DPI的获取要分为WPF和Winform两种环境进行讨论:
1. Winform环境
★(1)通过窗体或者控件的句柄来获得(推荐使用)
首先引入using System.Drawing.dll ;
代码如下:
Graphics currentGraphics = Graphics.FromHwnd(form.Handle);
double dpixRatio = currentGraphics.DpiX/96;
double dpiyRatio = currentGraphics.DpiY/96;
(2)通过Graphics来获取
首先引入using System.Drawing.dll ;
代码如下:
using (Graphics graphics = Graphics.FromHwnd(IntPtr.Zero))
{
float dpiX = graphics.DpiX;
float dpiY = graphics.DpiY;
}
(3)用ManagementClass来获取
首先引入using System.Management.dll;
代码如下:
using (ManagementClass mc = new ManagementClass("Win32_DesktopMonitor"))
{
using (ManagementObjectCollection moc = mc.GetInstances())
{
int PixelsPerXLogicalInch = 0; // dpi for x
int PixelsPerYLogicalInch = 0; // dpi for y
foreach (ManagementObject each in moc)
{
PixelsPerXLogicalInch = int.Parse((each.Properties["PixelsPerXLogicalInch"].Value.ToString()));
PixelsPerYLogicalInch = int.Parse((each.Properties["PixelsPerYLogicalInch"].Value.ToString()));
}
}
}
2.WPF环境
由于WPF获取句柄的方式和Winform有很大的区别。
所以这边以窗体和控件进行分别的讨论:
(1)WPF窗体获取dpi
WindowInteropHelper winHelper = new WindowInteropHelper((MainWindow)App.Current.MainWindow);
IntPtr mainWindowHandle = winHelper.Handle;
Graphics currentGraphics = Graphics.FromHwnd(mainWindowHandle);
double currentDpiX = currentGraphics.DpiX;
double currentDpiY = currentGraphics.DpiY;
double dpiXRatio = currentDpiX / 96;
double dpiYRatio = currentDpiY / 96;
注:这种方法必须有App.xaml的存在才可以使用。
(2)WPF控件获取dpi
<1>通过当前WPF控件对象获取
代码如下:
PresentationSource source = PresentationSource.FromVisual(this); double dpiX, dpiY;
if (source != null)
{
dpiX = 96.0 * source.CompositionTarget.TransformToDevice.M11;
dpiY = 96.0 * source.CompositionTarget.TransformToDevice.M22;
}
double dpixRatio = dpiX/96;
double dpiyRatio = dpiY/96;
注:由于使用了PresentationSource.FromVisual,所以必须在加载完控件之后才能使用。即最好放在Control的Loaded事件中使用。
<2>通过WPF的空间的句柄获得
代码如下:
IntPtr hwnd = ((HwndSource)PresentationSource.FromVisual(wpfControl)).Handle
Graphics currentGraphics = Graphics.FromHwnd(hwnd);
double dpixRatio = currentGraphics.DpiX/96;
double dpiyRatio = currentGraphics.DpiY/96;
注:同<1>中的注意要点。
★<3>无任何的前提的方法(来自StackFlow)(推荐使用)
代码如下:
var dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
var dpiYProperty = typeof(SystemParameters).GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
var dpiX = (int)dpiXProperty.GetValue(null, null);
var dpiY = (int)dpiYProperty.GetValue(null, null);
var dpixRatio = dpiX/96;
var dpiyRatio = dpiY/96;
五. WPF和Winform的DPI处理事项和技巧
当通过程序获得dpi的放大系数之后(即除以96),我们可以对应上面的表格获得Winform或者WPF下面,长和宽的不同的放大系数(一定要WPF和Winform分两次获得dpi)。
在获得系数之后,就可以通过除以相应的系数来实现对窗体大小的还原。在这个过程中有的地方需要注意。
(1)关于WPF中使用WindowsFormsHost承载Winform控件时大小的问题
在承载时,要在Winform控件中使用如下的代码进行调整:
代码如下:
this.AutoScaleMode = AutoScaleMode.Inherit;
this.BackgroundImageLayout = ImageLayout.Stretch;
注:如果不使用,那么Host中的空间会出现变形的问题。而且放大的大小比放大之后来的更加的大。
(2)winform中通过获取屏幕大小来限制窗体的全屏显示
代码如下:
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height
(3)WPF中通过获取屏幕大小来限制窗体的全屏显示
代码如下:
SystemParameters.WorkArea.Size.Width
SystemParameters.WorkArea.Size.Height
注:返回当前屏幕工作区的宽和高(除去任务栏)
(3)WPF中可以使用绑定和转换器来处理
转换器代码如下(转载自博客园水手《WPF 屏蔽DPI改变对程序的影响的解决方案》):
DPIConverter类
[ValueConversion(typeof(object), typeof(object))]
public class DPIConverter : IvalueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
WindowInteropHelper winHelper = new WindowInteropHelper((MainWindow)App.Current.MainWindow);
IntPtr mainWindowHandle = winHelper.Handle;
Graphics currentGraphics = Graphics.FromHwnd(mainWindowHandle);
double currentDpiX = currentGraphics.DpiX;
double dpiXRatio = currentDpiX / 96;
if (targetType == typeof(GridLength))
{
double data = double.Parse(parameter.ToString());
GridLength gridLength = new GridLength(data / dpiXRatio, GridUnitType.Pixel);
return gridLength;
}
if (targetType == typeof(Thickness))
{
Thickness margin = (Thickness)parameter;
if (margin != null)
{
margin.Top = margin.Top / dpiXRatio;
margin.Left = margin.Left / dpiXRatio;
margin.Right = margin.Right / dpiXRatio;
margin.Bottom = margin.Bottom / dpiXRatio;
return margin;
}
}
if (targetType == typeof(double))
{
double fontSize = double.Parse(parameter.ToString());
return fontSize / dpiXRatio;
}
if(targetType==typeof(System.Windows.Point))
{
System.Windows.Point point=(System.Windows.Point)parameter;
point.X=point.X/dpiXRatio;
point.Y=point.Y/dpiXRatio;
return point;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
上端使用先用资源字典引入直接进行绑定即可
代码如下:
Xaml
<Image Name="imageIcon" Stretch="Fill" HorizontalAlignment="Left" VerticalAlignment="Top" Width="{Binding Converter={StaticResource DPIConverter},ConverterParameter=32}" Height="{Binding Converter={StaticResource DPIConverter},ConverterParameter=32}">
<Image.Margin>
<Binding Converter="{StaticResource DPIConverter}">
<Binding.ConverterParameter>
<Thickness Left="18" Top="24" Right="0" Bottom="48"/>
</Binding.ConverterParameter>
</Binding>
</Image.Margin>
</Image>