此前有小伙伴询问我为何他 1 像素的线条显示发虚,然后我告诉他是“像素对齐”的问题,然而他设置了各种对齐像素的属性依旧没有作用。于是我对此进行了一系列试验,对 WPF 像素对齐的各种方法进行了一次总结。此后在 StackOverflow 中,我回答了 graphics - WPF DrawingContext seems ignore SnapToDevicePixels - Stack Overflow 问题。

阅读本文,我们将了解解决 WPF 像素对齐的四种方法以及其各自的适用范围和副作用。


为什么要做像素对齐

看线条!这是 3 像素的线条:

然而论其原因,就是因为我们屏幕太渣~哦~不,是因为绘制的线条没有与屏幕像素对齐,具体来说是视觉对象(Visual)的位置不在整数像素上或尺寸不是整数像素。而与此同时屏幕的点距又太大以至于我们看出来绘制的线条和屏幕像素之间的差异。

然而为什么 WPF 不默认为我们对齐像素呢?这是因为要对齐像素必定带来尺寸上的偏差;这是绘制尺寸精度和最终呈现效果之间的平衡。在 MacBook、Surface Pro 这些高档显示屏上,根本不用管这样的平衡问题;但在渣渣显示器上,微软把这种平衡的控制交给了应用的开发者。

处理像素对齐的四种方法

方法一:布局取整 UseLayoutRounding

实际效果是:

根本就不起作用!

事实上我们从 .NET Framework 源码可以得知,UseLayoutRounding 实际只处理 UI 元素对自己子级控件的布局取整。一旦整棵布局树种有任何一个不是整数(或者 DPI 相乘后不是整数),那么就依然没有解决问题。

方法二:对齐设备像素 SnapsToDevicePixels

这是一个会沿着逻辑树继承的属性,只要最顶层设置了这个属性,里面的元素都会具备此特性。不过,他只处理矩形的渲染,也就是说,只对 Border Rectangle 这些类型的元素生效,其他的包括自己写的元素基本都是不管用的。

它有一个好处,是像素对齐的情况下同时能够保证显示不足或超过 1 像素时,也能带一点儿透明或者超过一点像素。

方法三:使用 DrwingContext 绘制并配合 GuidelineSet

如果自己处理绘制,则可以在 OnRender 方法中使用 DrawingContext 来绘制各种各样的形状。DrawingContext 有方法 PushGuidelineSet,而 PushGuidelineSet 就是用来处理对齐的。

以下是四种不同方式的对齐效果对比,其中上面一半是直接对齐(即绘制过程是紧贴着的),下面一半则是多个部分带上一点偏移(即并不是紧贴):


▲ 看不清的可以考虑方法看

于是要想像素对齐,必须:

  • 布局或绘制时,UI 元素之间一点偏移或空隙都不能有,一点都不行
  • SnapsToDevicePixels 和 GuidelineSet 在实际对齐中有效,而 UseLayoutRounding 就是在逗你

GuidelineSet 的使用可以参考我在 StackOverflow 上的回答:graphics - WPF DrawingContext seems ignore SnapToDevicePixels - Stack Overflow。

以下是我编写的用于辅助绘制对齐线条的扩展方法:

public static class SnapDrawingExtensions
{
public static void DrawSnappedLinesBetweenPoints(this DrawingContext dc,
Pen pen, double lineThickness, params Point[] points)
{
var guidelineSet = new GuidelineSet();
foreach (var point in points)
{
guidelineSet.GuidelinesX.Add(point.X);
guidelineSet.GuidelinesY.Add(point.Y);
}
var half = lineThickness / 2;
points = points.Select(p => new Point(p.X + half, p.Y + half)).ToArray();
dc.PushGuidelineSet(guidelineSet);
for (var i = 0; i < points.Length - 1; i = i + 2)
{
dc.DrawLine(pen, points[i], points[i + 1]);
}
dc.Pop();
}
}

注意添加到 GuidelineSet 的尺寸不需要是整数,也不需要计算对齐屏幕的位置,只需要随便指定一个值即可,但相邻的绘制元素的值需要在 double 级别完全相同,多一点少一点都不行。

在 OnRender 中调用它绘制:

protected override void OnRender(DrawingContext dc)
{
// Draw four horizontal lines and one vertical line.
// Notice that even the point X or Y is not an integer, the line is still snapped to device.
dc.DrawSnappedLinesBetweenPoints(_pen, LineThickness,
new Point(0, 0), new Point(320, 0),
new Point(0, 40), new Point(320, 40),
new Point(0, 80.5), new Point(320, 80.5),
new Point(0, 119.7777), new Point(320, 119.7777),
new Point(0, 0), new Point(0, 120));
}

方法四:RenderOptions.EdgeMode

这是纯渲染级别的附加属性,对所有 UI 元素有效。这个属性很神奇,一旦设置,元素就再也不会出现模糊的边缘了,一定是硬像素边缘。不足半像素的全部删掉,超过半像素的变为 1 个像素。

以为它可以解决问题?——Too young, too simple.

你希望能够绘制 1 像素的线条,实际上它会让你有时看得见 1 像素线条,有时看的是 2 像素线条,有时居然完全看不见!!!

如果你都作用对象上还有其它视觉对象,它们也会一并变成了“硬边缘”,是可以看得见一个个像素的边缘。

各种方法适用范围总结

  1. 如果画粗线条粗边框,那么 RenderOptions.EdgeMode 最适合了,因为设置起来最方便,可以设置到所有的 UI 元素上。由于边框很粗,所以多一个少一个像素用户也注意不到。
  2. 如果是画细边框,那么使用 Border 配合 SnapsToDevicePixels 可以解决,无论是 0.8 像素还是 1.0 像素,1.2 像素,都能在准确地显示其粗细的基础之上还保证像素对齐。
  3. 如果图形比较复杂,比如绘制表格或者其它各种交叉了线条的图形,那么使用 DrawingContext 绘制,并设置 GuidelineSet 对齐。
  4. 如果窗口非常简单,既没有缩放,UI 元素也不多,可以考虑使用 UseLayoutRounding 碰碰运气,万一界面简单到只需要整数对齐就够了呢?
  5. 特别说明,上面四种方法不足与应对所有的像素对齐情况,如果还是没办法对齐……节哀把……我们一起找偏方……

WPF 绘制对齐像素的清晰显示的线条的更多相关文章

  1. WPF绘制矢量图形模糊的问题

    WPF默认提供了抗锯齿功能,通过向外扩展的半透明边缘来实现模糊化.由于WPF采用了设备无关单位,当设备DPI大于系统DPI时,可能会产生像素自动扩展问题,这就导致线条自动向外扩展一个像素,并且与边缘相 ...

  2. WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片

    原文:WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片 我们知道,在 WPF 中的坐标单位不是屏幕像素单位,所以如果需要知道某个控件的像素尺寸,以便做一些与屏幕像素尺寸相关 ...

  3. iOS: 如何正确的绘制1像素的线

    iOS 绘制1像素的线 一.Point Vs Pixel iOS中当我们使用Quartz,UIKit,CoreAnimation等框架时,所有的坐标系统采用Point来衡量.系统在实际渲染到设置时会帮 ...

  4. iOS 绘制1像素的线

    一.Point Vs Pixel iOS中当我们使用Quartz,UIKit,CoreAnimation等框架时,所有的坐标系统采用Point来衡量.系统在实际渲染到设置时会帮助我们处理Point到P ...

  5. WPF绘制深度不同颜色的3D模型填充图和线框图

    原文:WPF绘制深度不同颜色的3D模型填充图和线框图 在机械测量过程中,测量的数据需要进行软件处理.通常测量一个零件之后,需要重建零件的3D模型,便于观察测量结果是否与所测工件一致. 重建的3D模型需 ...

  6. 封装:WPF绘制曲线视图

    原文:封装:WPF绘制曲线视图 一.目的:绘制简单轻量级的曲线视图 二.实现: 1.动画加载曲线 2.点击图例显示隐藏对应曲线 3.绘制标准基准线 4.绘制蒙板显示标准区域 曲线图示例: 心电图示例: ...

  7. WPF下载远程文件,并显示进度条和百分比

    WPF下载远程文件,并显示进度条和百分比 1.xaml <ProgressBar HorizontalAlignment="Left" Height="10&quo ...

  8. C#:WPF绘制问题

    1.问题描述:切换画笔后,鼠标呈现画笔,但绘制界面需要点击后才能绘制,体验比较差 注:如果将切换为画笔或橡皮擦的功能放在二级菜单中则无次问题 解决方法(大体如此): 1)在第三方中,先创建完绘制画面和 ...

  9. WPF绘制党徽(立体效果,Cool)

    原文:WPF绘制党徽(立体效果,Cool) 前面用WPF方式绘制了党旗(WPF制作的党旗) ,去年3月份利用C# 及GDI+绘制过党徽,这次使用WPF来绘制党徽. ------------------ ...

随机推荐

  1. monkey测试小记

    本篇中不记录环境搭建,只是介绍一些经验和小秘诀吧. 一.使用安卓模拟器进行测试. 在刚刚接触到monkey测试的时候,用的真机进行测试,点击几万次甚至更多的时候,发现系统变慢了.也许是错觉,但是系统经 ...

  2. Python笔试、面试 【必看】

    本文由EarlGrey@编程派独家编译,转载请务必注明作者及出处. 原文:Sheena@codementor 译文:编程派 引言 想找一份Python开发工作吗?那你很可能得证明自己知道如何使用Pyt ...

  3. Android Fragment解析(下)

    今天被人问到了什么是Fragment,真是一头雾水,虽然以前也用到过,但不知道它是叫这个名字,狂补一下. 以下内容来自互联网,原文链接:http://blog.csdn.net/lmj62356579 ...

  4. JavaScript深拷贝—我遇到的应用场景

    简述 深拷贝即拷贝实例,其作用是为了不影响拷贝后的数组对起原数组造成影响.这时我们就需要进行深拷贝.(JavaScript的继承) 我遇到的应用场景 我是在用vue的element-ui做项目的时候遇 ...

  5. [嵌入式培训 笔记]-----Vim编辑器使用简介

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 第一讲小结 1. 光标在屏幕文本中的移动既 ...

  6. ngnix配置自解

    全局配置 user [user] [group]; #只有被设置为用户或用户组的成员才有nginx的启动权限.(#user nobody nobody <=> user nobody no ...

  7. 解决spring boot在RabbitMQ堆积消息情况下无法启动问题

    最近遇到一个问题,服务站点上线之前,先去新建需要的rabbitmq并绑定关系,此时 如果发送消息方运行, 那边会造成新建的q消息部分堆积得不到及时消费 那么问题来了? 在消息堆积情况下,服务站点无法启 ...

  8. C# 与vb.net 的Dictionary(字典)的键、值排序

    项目中可能需要用到Dictionary 排序,于是先做了一个小demo ,网上搜索真的没有能满足我需要的,都是类似的,于是理解改造,一上午就在查找,实践中过去了.现在把它实现了,把代码贴出来,算是一个 ...

  9. Java进阶1. Synchronized 关键字

    Java进阶1. Synchronized 关键字 20131025 1.关于synchronized的简介: Synchronized 关键字代表对这个方法加锁,相当于不管那一个线程,运行到这个方法 ...

  10. Unity 3D 无法显示中文的解决方法

    大家开始用unity3D时想必都会遇到一个问题,使用中文时会乱码.这是由于编码方式不同导致的,具体解决方法如下: 程序写代码什么的最好下个像Notepad++类似的工具,这里使用Notepad++修改 ...