WPF 绘制对齐像素的清晰显示的线条
此前有小伙伴询问我为何他 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 像素线条,有时居然完全看不见!!!
如果你都作用对象上还有其它视觉对象,它们也会一并变成了“硬边缘”,是可以看得见一个个像素的边缘。
各种方法适用范围总结
- 如果画粗线条粗边框,那么
RenderOptions.EdgeMode最适合了,因为设置起来最方便,可以设置到所有的 UI 元素上。由于边框很粗,所以多一个少一个像素用户也注意不到。 - 如果是画细边框,那么使用
Border配合SnapsToDevicePixels可以解决,无论是 0.8 像素还是 1.0 像素,1.2 像素,都能在准确地显示其粗细的基础之上还保证像素对齐。 - 如果图形比较复杂,比如绘制表格或者其它各种交叉了线条的图形,那么使用
DrawingContext绘制,并设置GuidelineSet对齐。 - 如果窗口非常简单,既没有缩放,UI 元素也不多,可以考虑使用
UseLayoutRounding碰碰运气,万一界面简单到只需要整数对齐就够了呢? - 特别说明,上面四种方法不足与应对所有的像素对齐情况,如果还是没办法对齐……节哀把……我们一起找偏方……
WPF 绘制对齐像素的清晰显示的线条的更多相关文章
- WPF绘制矢量图形模糊的问题
WPF默认提供了抗锯齿功能,通过向外扩展的半透明边缘来实现模糊化.由于WPF采用了设备无关单位,当设备DPI大于系统DPI时,可能会产生像素自动扩展问题,这就导致线条自动向外扩展一个像素,并且与边缘相 ...
- WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片
原文:WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片 我们知道,在 WPF 中的坐标单位不是屏幕像素单位,所以如果需要知道某个控件的像素尺寸,以便做一些与屏幕像素尺寸相关 ...
- iOS: 如何正确的绘制1像素的线
iOS 绘制1像素的线 一.Point Vs Pixel iOS中当我们使用Quartz,UIKit,CoreAnimation等框架时,所有的坐标系统采用Point来衡量.系统在实际渲染到设置时会帮 ...
- iOS 绘制1像素的线
一.Point Vs Pixel iOS中当我们使用Quartz,UIKit,CoreAnimation等框架时,所有的坐标系统采用Point来衡量.系统在实际渲染到设置时会帮助我们处理Point到P ...
- WPF绘制深度不同颜色的3D模型填充图和线框图
原文:WPF绘制深度不同颜色的3D模型填充图和线框图 在机械测量过程中,测量的数据需要进行软件处理.通常测量一个零件之后,需要重建零件的3D模型,便于观察测量结果是否与所测工件一致. 重建的3D模型需 ...
- 封装:WPF绘制曲线视图
原文:封装:WPF绘制曲线视图 一.目的:绘制简单轻量级的曲线视图 二.实现: 1.动画加载曲线 2.点击图例显示隐藏对应曲线 3.绘制标准基准线 4.绘制蒙板显示标准区域 曲线图示例: 心电图示例: ...
- WPF下载远程文件,并显示进度条和百分比
WPF下载远程文件,并显示进度条和百分比 1.xaml <ProgressBar HorizontalAlignment="Left" Height="10&quo ...
- C#:WPF绘制问题
1.问题描述:切换画笔后,鼠标呈现画笔,但绘制界面需要点击后才能绘制,体验比较差 注:如果将切换为画笔或橡皮擦的功能放在二级菜单中则无次问题 解决方法(大体如此): 1)在第三方中,先创建完绘制画面和 ...
- WPF绘制党徽(立体效果,Cool)
原文:WPF绘制党徽(立体效果,Cool) 前面用WPF方式绘制了党旗(WPF制作的党旗) ,去年3月份利用C# 及GDI+绘制过党徽,这次使用WPF来绘制党徽. ------------------ ...
随机推荐
- 关于Vue的component制作dialog组件
其实原理很简单,兴个粟子, 点击按钮出现 dialog 弹出杠, 将dialog做成一个组件,components/dialog.vue 就是在components里面新建一个vue.将这个vue做为 ...
- webstorm自动换行
1.文件 — — 设置 2. 编辑器 — — 编辑器 — — 在编辑窗口使用软换行(勾选)
- tags
运行tags在你的.vimrc 中加一个路径,set tags=/home/lh/1407k/arm/tags 注意此文件下的东西要注销必须用“执行一个ctags -R *;ctrl + ]过去,ct ...
- nyoj——113 getline
字符串替换 时间限制:3000 ms | 内存限制:65535 KB 难度:2 描述 编写一个程序实现将字符串中的所有"you"替换成"we" 输入 ...
- angularJS---初识指令
angularJS 什么是angularJS AngularJS 诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀的前端JS框架,已经被用于Google的多款 ...
- centos7 iptables替换firewall
Disable Firewalld Service. [root@rhel-centos7-tejas-barot-linux ~]# systemctl mask firewalld Stop Fi ...
- 在js中做数字字符串补0
转自(http://blog.csdn.net/aimingoo/article/details/4492592) 通常遇到的一个问题是日期的“1976-02-03 HH:mm:ss”这种格式 ,我的 ...
- poj 1258 Agri-Net 最小生成树 prim算法+heap不完全优化 难度:0
Agri-Net Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 41230 Accepted: 16810 Descri ...
- git重要命令
body, table{font-family: 微软雅黑; font-size: 13.5pt} table{border-collapse: collapse; border: solid gra ...
- Week14《Java程序设计》第14次作业总结
Week14<Java程序设计>第14次作业总结 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结与数据库相关内容. 2. 使用数据库技术改造你的系统 2.1 简述如何 ...