「玩一玩」换个思路更简单:方便好用的文字描边效果实现方法

Conmajia 2012-05-27 09:54:23
加精

最近做的玩意儿走的是艺术路线,大量底图,文字不容易看清,所以需要描边效果。但是网上搜了一圈,都没有让我满意的,于是自己实现。在做的时候,想到能不能用最简单的谁都会的方法来做,于是有感而发,写了这篇帖子,大家一起探讨。

© 版权所有 野比 2012

当我们需要在色彩丰富的图片上显示文字的时候,由于背景色变化的关系,文字往往不能清晰呈现。就像很多早期电影使用纯白色字幕,在蓝天白云的画面下常常看不清楚字幕。这时候,我们就需要文字描边这种能够突出显示文字的效果了,就像这样。



(动画截图 by Wong Shao Voon)

那么,怎样实现这样的效果呢?下面我粗略介绍下目前常见的几种方法。最后介绍我构思的一种简单而实用的实现方法。
一般方法
第一种思路:文字即图片
1.将TextBlock 转换为WriteableBitmap
2.对WriteableBitmap 的Pixels 进行循环,判断每一个像素点的值最终达到描边的效果。
参考《Silverlight字体描边
野比点评
这种思路将文字转为图片,根据判断图片上每个像素点,效率低下,最终效果简单,如下图所示。


第二种思路:像素着色器
该方法基本思路为:假定只会对TextBlock应用像素着色器,那么TextBlock是一个矩形,文字所在像素的 alpha分量必定大于0,否则必定是透明像素。判断如果当前像素的上,下,左,右任意一个像素不透明,则说明本像素需要被描边,否则就输出文字颜色。由于需要知道相邻像素,所以还需要传入TextBlock的ActualWidth和ActualHeight。 这样, 当前位置的 x+ 1/width 就是相邻像素的坐标,就可以用tex2D函数来提取它的颜色值。还需要输入描边的颜色,还有文字的颜色。
参考《SILVERLIGHT像素着色器编写简明指南 附送文字描边效果
野比点评
仍然是一个「大炮打蚊子」的思路,将文字进行逐像素的处理。和思路1唯一的区别在于使用了像素着色器,让把部分工作交给GPU来完成,「看起来」很高效很快速。但是如果禁用了硬件加速呢?就变成和1一样了。下面是该方法的效果截图。


第三种思路:GDI+路径绘图
这种方法不再是逐像素处理了。其基本思路是将文字字符串添加到GDI+的绘图路径中(GraphicsPath),然后再DrawPath()。利用不同的笔刷,这种方法可以「画」出非常华丽的描边效果,就像这样。


野比点评
这是扩展性最好的方法。由于利用了GDI+的高级特性,所以可以利用不同的笔刷如纹理、渐变,以及多次绘图等方法做出非常精美的描边、阴影等效果。唯一的遗憾就是代码量较大(比前面2种要少很多了)。
参考文章《C# 水印图片+文字描边+发光文字。看示图及Demo
关于该方法的高级效果设计,参考《Outline Text》一文(Code Project「Best C++/MFC article of Sep 2009」比赛获奖文章)。

野比的简易方法
又到了野比的偷懒时间。
看了前面几种设计思路后,你有没有一头雾水的感觉?或者眼花缭乱的感觉?难道我们只有这样实现吗?需要「描边」文字,就一定要「描」吗?
其实完全没有必要。曾经我在山寨Safari时,介绍过一种简单的通过重复绘制文字实现高光效果的方法(参见《一步一步玩控件:TabControl——从制作山寨Safari窗体开始》)。
如下图所示。当底层文字和顶层文字相差1px时,就会呈现出不同的阴影/高光效果。那么我们如果把这个思路拓展下,把上、下、左、右四个方向的偏差结合到一起,就会像下图最后显示的效果一样,得到了「描边」文字的效果。

接下来在GDI+里面实现它。
// Code by Conmajia
// txtPoint是绘制文字的定位点
txtPoint.Offset(-1, 0); // 绘制左背景文字
e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
txtPoint.Offset(2, 0); // 绘制右背景文字
e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
txtPoint.Offset(-1, -1); // 绘制下背景文字
e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
txtPoint.Offset(0, 2); // 绘制上背景文字
e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
txtPoint.Offset(0, -1); // 定位点归位

// 绘制前景文字
e.Graphics.DrawString(this.Text, this.Font, foreBrush, txtPoint);


下面请欣赏效果


结语
这篇文字,重点不在于技术,而是如何简单方便的在我们能力范围之内实现我们想要的效果。
这两天在网上看到有「高手」数落新人没用标准架构标准技术标准手法,有感而发。
比如一个C#新手,他/她也许刚接触C#,学了一点点GDI+,现在就需要做描边文字。但是他/她根本还没学到什么Shader、什么GraphicsPath这样的高级内容,你觉得该怎么办?
我的信条是,让标准流「滚他奶奶的蛋」,在实用主义的道路上,没有所谓的「标准做法」。谁说的一定要把边描出来?谁说的一定要多线程?谁说的一定要三层?只要我做出来,只要「看起来和用起来」没问题,谁会管你具体怎么实现的啊,你以为用户也是程序猿,还要看你源代码啊?骚年,省省别2了吧

很多时候,我们遇到问题,不需要完全跟着问题走。就像这次,我们需要「描边」,但谁说的,我们非得要「描」边呢?我们要的只是效果,而不是过程,所以要跳出问题表象的禁锢,就能获得更加宽广的视野。
© 版权所有 野比 2012
(全文完)


以上蓝字内容很冲,最近感染了喷子病毒。。标准流鄙视流请尽情的喷吧,让我甘拜下风。
...全文
3211 112 打赏 收藏 转发到动态 举报
写回复
用AI写文章
112 条回复
切换为时间正序
请发表友善的回复…
发表回复
TONG126623 2012-06-07
  • 打赏
  • 举报
回复
挺好的,呵呵呵
LAONINGA098 2012-06-07
  • 打赏
  • 举报
回复
不错的思路
Conmajia 2012-06-06
  • 打赏
  • 举报
回复
[Quote=引用 156 楼 的回复:]

文字可以只画一遍,其他四遍用内存拷贝。
[/Quote]

会不会麻烦了点?个人认为没这必要,性价比不高。请指正。
dongpy 2012-06-06
  • 打赏
  • 举报
回复
文字可以只画一遍,其他四遍用内存拷贝。
Conmajia 2012-06-04
  • 打赏
  • 举报
回复
[Quote=引用 152 楼 的回复:]

为什么你给的“汉字”有圆角,而你给的代码示例里面没有圆角的处理?难道那个很大的汉字是PS出来的?
我不认为4次画图就为了个背景能效率很高,为什么不考虑将Font改变下呢?背景用大点的Font,而前面的字用小点的Font。
[/Quote]

首先,感谢回帖。

然后回答问题:

那叫「示意图」,的确是ps做的。

我说效率高,是因为我实地写过代码,实验过。

我说不用大小字体,也是因为我实地写过代码,试验过。

字体变大,就意味着「字间距也变大,行间距也变大」,可是小字体又没有变,那么你就要每个字独立的去画,去计算坐标,不然就「不可能对齐」。

而且就算对齐了,你可以在ps里试试,一大一小两个字重叠出来的效果,绝对不是你想象中那种「描边」的效果。

你觉的这是描边吗?




有自己的想法是好的,但是这些简单的细节,还是要稳重点,稍微思考下先。
qldsrx 2012-06-04
  • 打赏
  • 举报
回复
为什么你给的“汉字”有圆角,而你给的代码示例里面没有圆角的处理?难道那个很大的汉字是PS出来的?
我不认为4次画图就为了个背景能效率很高,为什么不考虑将Font改变下呢?背景用大点的Font,而前面的字用小点的Font。
stmant 2012-06-02
  • 打赏
  • 举报
回复
很好很不错!
  • 打赏
  • 举报
回复
看评论的
Conmajia 2012-06-02
  • 打赏
  • 举报
回复
[Quote=引用 148 楼 的回复:]

渐变色能不能 依靠多次offset实现?
[/Quote]

画背景字的时候试试改用gradient的brush
Sheldon_Lou 2012-06-02
  • 打赏
  • 举报
回复
渐变色能不能 依靠多次offset实现?
xiao0915 2012-06-02
  • 打赏
  • 举报
回复
好思路。
  • 打赏
  • 举报
回复
我觉得野比的想法没错,技术不是用来学习限制发挥的,而是用来解决问题的。
cbao123 2012-06-01
  • 打赏
  • 举报
回复
还能这样啊!学习一下,谢谢!
  • 打赏
  • 举报
回复
图好,人更好,呵呵
Conmajia 2012-05-31
  • 打赏
  • 举报
回复
[Quote=引用 139 楼 的回复:]

我最烦的就是那种断章取义,看不惯别人发些自己的想法,哪怕是非主流的,或者确实不是最好的,就在底下装自己多NB,喷人家的,人家发帖只是想说说自己发现的一些新问题,拿出来跟大家分享讨论,人家又没说自己的最好,叫大家以后都这么干,没有那种方法可以成为“最”的吧,总会有一个“更”来替代他,但是如果大家都不去尝试,都不去思考,我们上哪里去发现那个“更”?就只因为我们没发现“更”就认为现在所谓的“最”就是真……
[/Quote]

我从来不介绍某某技术,某某模式,这样会限制读者的思维。

我更希望的是能激发大家的创意,能反过来让我有所收获。

遗憾的是发了这么多帖子,只有2、3贴有朋友动脑思考了,动手制作了。

我不是吃it这碗饭的,只是以玩的心态在研究学习。所以在这里显得有些非主流,有人喷我也是正常的。

我用编程是为了解决我思考的问题,所以我用的还是老旧的技术和手段,我也没有理由非要去学习去用最新的。

技术本身只是个工具。但不是说只能用工具,用最新的,才能达到目的。

比如我们杀鸡,可以用最新最贵的进口双立人来杀,也可以用最便宜的10块钱菜刀来杀,甚至我为什么要用刀?我还可以一把拧断鸡脖子。

记得有个帖子楼主问表达式解析,实际就是个拆分字符串,结果有牛人回帖说计算机专业出身的都应该学过编译原理啊,你怎么没学过,言下之意这都不会你还来玩编程?建议你去看看某某章节某某知识点,看完你就会做了。

对于这样的「专业人士」的思维逻辑,我真的深表遗憾。
alextienpai 2012-05-31
  • 打赏
  • 举报
回复
我最烦的就是那种断章取义,看不惯别人发些自己的想法,哪怕是非主流的,或者确实不是最好的,就在底下装自己多NB,喷人家的,人家发帖只是想说说自己发现的一些新问题,拿出来跟大家分享讨论,人家又没说自己的最好,叫大家以后都这么干,没有那种方法可以成为“最”的吧,总会有一个“更”来替代他,但是如果大家都不去尝试,都不去思考,我们上哪里去发现那个“更”?就只因为我们没发现“更”就认为现在所谓的“最”就是真的“最”了?那不是在自欺欺人么?

我觉得野比的帖子都很好!有些时候,就是为了解决问题,问题解决了就可以了,现在可能觉得有些是偷懒或者不标准之类的,但是或许用用就会发现其他新的问题,会发现其他更有意思的东西,重点在于,要开放眼界思路,循规蹈矩,有神马意思。。。
Conmajia 2012-05-31
  • 打赏
  • 举报
回复
[Quote=引用 135 楼 的回复:]

[/Quote]

Conmajia 2012-05-31
  • 打赏
  • 举报
回复
[Quote=引用 136 楼 的回复:]

用GetGlyphOutline搞字模
这篇文章最后提到的描边方法也是这种偷懒方法,呵呵
[/Quote]

哈哈,懒人到处都有。
showlie 2012-05-31
  • 打赏
  • 举报
回复
用GetGlyphOutline搞字模
这篇文章最后提到的描边方法也是这种偷懒方法,呵呵
鑫鑫 2012-05-31
  • 打赏
  • 举报
回复
加载更多回复(92)

110,566

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧