wpf 在不同DPI下如何在DrawingVisual中画出清晰的图形
环境Win10 VS2017 .Net Framework4.7.1
本文仅讨论在DrawingVisual中进行的画图.
- WPF单位,系统DPI,显示器DPI三者的定义及关系
- WPF单位:一种与设备无关的单位,以1/96逻辑英寸为一个单位,也就是说如果将一个对象的长度设为96,那么在任何设备上WPF都会试图将其显示为1逻辑英寸长.
- 系统DPI:将多少个显示器的像素点定义为1逻辑英寸,默认是96个点
在win10中,图中所设置的 100%即为96DPI; 125%即为120DPI; 150%即为144DPI; 175%即为168DPI.
-
- 显示器DPI:每实际英寸有多少个像素点. 大多数显示器每逻辑英寸可显示96个点,不过近年开始流行的高分辨率屏可能会高出很多.
WPF单位决定可视元素的尺寸是多少逻辑英寸,但逻辑每英寸是多少则由系统DPI决定.(此处的"逻辑英寸"已不是生活中的那个绝对不变的单位了,而是由系统DPI决定,只不过默认是与实际英寸一致的)在默认情况下:WPF的一个单位=1/96逻辑英寸=1/96实际英寸 =显示器的1个像素.此时完美点对点,所以不会模糊.但如果在系统中更改了DPI设置,比如144(150%)DPI,此时WPF的一个单位=1/96逻辑英寸=1.5/96实际英寸=显示器的1.5个像素.此时点对点被破坏,所以模糊.
- DrawingVisual如何才能做到与显示器点对点?
- 所有的绘图参数都需要根据系统DPI进行调整,比如在96系统DPI环境下的宽度为1的Pen对象,想要在144系统DPI下继续精确保持1个像素的宽度,那么就要将其缩小为1/(144/96)≈ 0.66666
- 当WPF对DrawingVisual进行渲染时,渲染DPI设置要与系统DPI一致.如果不一致则WPF会先以渲染设置的DPI进行渲染 ,再缩放到系统DPI,一旦缩放就破坏了点对点,进而导致模糊.比如下面通过RenderTargetBitmap对象来使用DrawingVisual,其中的144就是要与系统DPI一致.
Dim RenderTargetBitmap As New RenderTargetBitmap(1920, 1080, 144, 144,PixelFormats.Pbgra32) RenderTargetBitmap.Render(drawingVisual)
- 例子
现在假设系统DPI为默认100%设置,在DrawingVisual画一条一个像素宽的清晰横线
Public Class Window1 Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs) Dim drawingVisual As New DrawingVisual() Dim drawingContext As DrawingContext = drawingVisual.RenderOpen() Dim Thickness As Double = 1 '线宽1个单位,此处等同于1个像素 Dim HalfThickness As Double = Thickness * 0.5 drawingContext.DrawLine(New Pen(Brushes.Black, Thickness), New Point(0, 100 - HalfThickness), New Point(1920, 100 - HalfThickness) ) drawingContext.Close() Dim RenderTargetBitmap As New RenderTargetBitmap(1920, 1080, 96, 96, PixelFormats.Pbgra32) '注意这里创建的是96DPI的图像对象 RenderTargetBitmap.Render(drawingVisual) Dim image As New Image image.Source = RenderTargetBitmap Canvas1.Children.Add(image) End Sub End Class
<Window x:Class="Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WpfApp3" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="Window1" Width="300" Height="300" mc:Ignorable="d" Loaded="Window_Loaded"> <Canvas x:Name="Canvas1"> </Canvas> </Window>
运行结果:线是清晰锐利的
接下来把系统DPI改为150%(修改后别忘了注销windows的当前用户后重新登入以使DPI的修改生效),仍然是要求在相同位置画出一条一个像素宽的清晰横线.
Public Class Window1 Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs) Dim drawingVisual As New DrawingVisual() Dim drawingContext As DrawingContext = drawingVisual.RenderOpen() Dim Thickness As Double = 1 / 1.5 '在144(150%)DPI的情况下,线宽1个像素 Dim HalfThickness As Double = Thickness * 0.5 drawingContext.DrawLine(New Pen(Brushes.Black, Thickness), New Point(0, 100 / 1.5 - HalfThickness), New Point(1920, 100 / 1.5 - HalfThickness) ) drawingContext.Close() Dim RenderTargetBitmap As New RenderTargetBitmap(1920, 1080, 144, 144, PixelFormats.Pbgra32) '注意这里创建的是144DPI的图像对象 RenderTargetBitmap.Render(drawingVisual) Dim image As New Image image.Source = RenderTargetBitmap Canvas1.Children.Add(image) End Sub End Class
运行结果:线依然是是清晰锐利的
- 其它
在写此笔记之前在网上查阅过很多文章,基本上都是以使用参考线(GuidelineSet)进行像素对齐的方式避免模糊,但参考线也不是完美的解决方案,因为当要求在两个显示器像素之间进行绘制时WPF到底会将其对齐到哪个像素呢?其结果并不一定是我们想要的,
左侧是使用了参考线得到的清晰图形,右侧是没有使用参考线得到的模糊图形(注意原文中写反了).
请仔细放大观察左侧的清晰方框,可以发现四边的每条线段宽度并不相同.
使用参考线进行像素对齐的方法实际上就是对绘制坐标参数进行类似四舍五入的舍入,这个方框的四边虽然是使用同样参数绘制的线条,但在具体绘制时有的舍了有的入了,导致最终宽度不一样.