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

(动画截图 by Wong Shao Voon)
怎样实现这样的效果呢? 下面我粗略介绍下目前常见的几种方法 。最后介绍我构思的一种简单而实用的实现方法 。
一般方法
第一种思路: 文字即图片
- 将 TextBlock 转换为 WriteableBitmap
- 对 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 时 ,介绍过一种简单的通过重复绘制文字实现高光效果的方法 。
如下图所示 。当底层文字和顶层文字相差 1px 时 ,就会呈现出不同的阴影/高光效果 。如果把这个思路拓展下 ,把上 、下 、左 、右四个方向的偏差结合到一起 ,就会像下图最后显示的效果一样 ,得到了 「描边」 文字的效果 。

接下来在 GDI+里面实现它 。
1 // Code by Conmajia
2 // txtPoint 是绘制文字的定位点
3 txtPoint.Offset(-1, 0); // 绘制左背景文字
4 e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
5 txtPoint.Offset(2, 0); // 绘制右背景文字
6 e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
7 txtPoint.Offset(-1, -1); // 绘制下背景文字
8 e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
9 txtPoint.Offset(0, 2); // 绘制上背景文字
10 e.Graphics.DrawString(this.Text, this.Font, backBrush, txtPoint);
11 txtPoint.Offset(0, -1); // 定位点归位
12
13 // 绘制前景文字
14 e.Graphics.DrawString(this.Text, this.Font, foreBrush, txtPoint);
下面请欣赏效果

结语
遇到问题 ,不一定需要完全跟着问题走 。就像这次 ,需要 「描边」 ,但谁说的 ,非得要 「描」 边呢? 你要的只是效果 ,而不是过程 ,所以要跳出问题表象的禁锢 ,就能获得更加宽广的视野 。
© 版权所有 conmajia 2012
(全文完)
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构