在 WPF 里面,提供的使用底层的方法绘制文本是通过 DrawGlyphRun 的方式,此方法适合用在需要对文本进行精细控制的定制化控件上。此方法特别底层而让调用方法比较复杂,本文告诉大家一些简单的使用方法

本文也属于 WPF 渲染系列博客,更多渲染相关博客请看 渲染相关

在开始之前,我是来劝退的,如果没有特别的需求,还是不推荐使用 DrawGlyphRun 的方式进行文本绘制。本文不会告诉大家特别基础的知识,基础部分还请看官方文档: GlyphRun Class (System.Windows.Media)

如果可以的话,顺便也将 DirectWrite官方文档也读一次

使用 DrawGlyphRun 方法之前需要拿到一个 DrawingContext 对象,而在调用此方法时,重要的参数是 GlyphRun 对象,此对象包含了大量的参数,本文将来告诉大家这些的参数的用法

例子

新建一个空 WPF 项目用来做例子

在 MainWindow 的 Loaded 事件里面,创建 DrawingVisual 用来获取 DrawingContext 对象

        public MainWindow()
{
InitializeComponent(); Loaded += MainWindow_Loaded;
} private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var drawingVisual = new DrawingVisual();
using (var drawingContext = drawingVisual.RenderOpen())
{ }
Background = new VisualBrush(drawingVisual);
}

默认作为 Background 的 Brush 将会被撑开,为了让后续绘制的文本有指定的尺寸,绘制一个和窗口相同大小的矩形,这样就可以让 drawingVisual.Drawing.Bounds 的尺寸和窗口相同

using (var drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawRectangle(Brushes.Black, null, new Rect(0, 0, ActualWidth, ActualHeight));
}

准备

在使用 DrawGlyphRun 绘制需要创建 GlyphRun 对象,需要有以下参数才能构建出绘制的文本内容

  • 字体
  • 字号
  • 文本内容
  • 文本绘制画刷
  • 文本绘制的坐标

尽管 GlyphRun 对象需要的参数很多,然而很多参数都是可以默认获取的

字体

在 GlyphRun 里面需要的字体不是 FontFamily 而是需要传入的是 GlyphTypeface 对象。好在 GlyphTypeface 对象就是可以从 FontFamily 获取的

每个字体都相当于有一族,多个 Typeface 对象,如下面代码可以获取第一个 Typeface 对象

var fontFamily = new FontFamily("微软雅黑");
Typeface typeface = fontFamily.GetTypefaces().First();

如果此字体是成功安装的,清真的字体,那么可以通过如下代码获取到 GlyphTypeface 对象

bool success = typeface.TryGetGlyphTypeface(out GlyphTypeface glyphTypeface);

大部分字体都能成功拿到,如果不能成功那么,那么就需要自己走字体 Fallback 换个字体啦,或者炸掉。自己决定如果给定的字体创建失败了,则使用什么字体代替的方法叫做字体 Fallback 算法

关于如何做字体的回滚策略,还请参阅下文 字体回滚策略 内容

文字编号

每个文字在字体里面都可以有自己的编号,需要通过 CharacterToGlyphMap 获取对应的值

var text = "林德熙abc123ATdVACC";

List<ushort> glyphIndices = new List<ushort>();

for (var i = 0; i < text.Length; i++)
{
var c = text[i];
var glyphIndex = glyphTypeface.CharacterToGlyphMap[c];
glyphIndices.Add(glyphIndex);
}

需要同时在 GlyphRun 传入编号和 Unicode 的值

设置字号

在 GlyphRun 里面,支持输入多个文字和单个文字,在输入时,可以给每个文字指定字号。字号其实是一个上层的概念,而在 GlyphRun 需要使用底层的文本渲染概念,也就是字符的 AdvanceWidth 的值。简单的获取 AdvanceWidth 的方法如下

List<double> advanceWidths = new List<double>();

for (var i = 0; i < text.Length; i++)
{
var c = text[i]; var width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;
advanceWidths.Add(width);
}

以上代码将字符串每个文字都设置相同的字号,但是大家可以根据需求,给每个文字都设置字号。对于等宽字符来说,每个字符的 AdvanceWidths 对应的值都应该是相同的。对于非等宽字符,可以在特殊排版需求的时候,强行设置为等宽的值

字符都是等比的,因此只需要设置宽度即可,设置字宽等于设置字号

设置字体偏移

在 GlyphRun 的高级用法里面,是允许设置文字的偏移量。文字的偏移量是一个文字的排版的基础值,推荐大家写一点代码去摸索一下他的规则

List<Point> glyphOffsets = new List<Point>();
var fontSize = 30; for (var i = 0; i < text.Length; i++)
{
var c = text[i]; // 只是决定每个字的偏移量,记得加上 i 乘以哦。字符最好是叠加上 fontSize 的值,使用 fontSize 的倍数
glyphOffsets.Add(new Point(fontSize * i, 0));
}

在 GlyphRun 里面,文字的偏移量非必须的,可以传入为空值,因此以上代码是非必须的,只有需要控制每个字的偏移量的时候才需要用到。此偏移量不是相对坐标值,只是偏移量而已,相对来说比较绕

文本偏移

在 DrawGlyphRun 方法里面是不包含文本的坐标的参数的,需要在 GlyphRun 对象里面设置整个文本的起始坐标,如下面代码准备好文本的 X 和 Y 坐标值

    var location = new Point(10, 100);

上面代码只是例子而已,还请替换为你的业务代码的需要绘制的文本坐标

但是需要知道的是在 GlyphRun 里面传入的是 BaseLine 而不是 Location 的值,相互转换的逻辑需要根据 FontFamily 的 Baseline 的值才能计算,代码如下

        /// <summary>
/// 获取指定字体的baseline
/// </summary>
/// <param name="fontFamily"></param>
/// <param name="fontRenderingEmSize"></param>
/// <returns></returns>
public static double GetBaseline(this FontFamily fontFamily, double fontRenderingEmSize)
{
var baseline = fontFamily.Baseline; var renderingEmSize = fontRenderingEmSize; var value = baseline * renderingEmSize;
return value;
} location = new Point(location.X, location.Y + fontFamily.GetBaseline(fontSize));

以上代码是将 GetBaseline 的返回值给到 location 的 Y 值,这适合用在水平布局文本上。如果是垂直排版的文本,自然就需要放在水平方向。请根据你的业务代码修改以上逻辑

语言文化

如果需要支持特殊的文本内容,就需要设置特别的语言文化,默认使用 IetfLanguageTag 即可

                XmlLanguage defaultXmlLanguage =
XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.IetfLanguageTag);

DPI

在新的 GlyphRun 的构造里面要求传入 DPI 的值用于清晰化显示,在旧版本的,如 .NET Framework 4.5 版本是不需要的

官方推荐的获取 DPI 的方法是根据当前文本将要渲染出来的控件获取控件的 DPI 的值,通过此方法可以支持多屏幕不同 DPI 的感知。本文提供的方法是获取主窗口,因为本文的例子是在主窗口绘制文本

    var pixelsPerDip = (float) VisualTreeHelper.GetDpi(Application.Current.MainWindow).PixelsPerDip;

绘制文本

在准备完成之后,即可创建 GlyphRun 用来绘制

  var glyphRun = new GlyphRun
(
glyphTypeface,
bidiLevel: 0,
isSideways: false,
renderingEmSize: fontSize,
pixelsPerDip: pixelsPerDip, // 只有在高版本的 .NET 才有此参数
glyphIndices: glyphIndices,
baselineOrigin: location, // 设置文本的偏移量
advanceWidths: advanceWidths, // 设置每个字符的字宽,也就是字号
glyphOffsets: null, // 设置每个字符的偏移量,可以为空
characters: text.ToCharArray(),
deviceFontName: null,
clusterMap: null,
caretStops: null,
language: defaultXmlLanguage
); drawingContext.DrawGlyphRun(Brushes.White, glyphRun);

请将 Brushes.White 替换为字体前景色的画刷

以上即可完成文本的绘制,这是一个底层的方式,看起来也很简单

创建成本

创建一个 GlyphRun 对象的成本有多高?是否需要申请很多资源?其实创建时仅仅只是创建了一个 CLR 对象而已,里面也只有很多的字段,成本非常低。在创建时不会用到任何非托管的资源,只是一个对象而已

只有在被绘制的时候,才会申请 DirectWrite 的相关资源

获取几何对象

通过 BuildGeometry 方法可以从 GlyphRun 对象创建几何对象,如下面代码

var geometry = glyphRun.BuildGeometry();

获取几何对象可以用此几何对象做特殊的逻辑,如文字描边等

需要小心的是调用 BuildGeometry 方法是有一定成本的,底层将需要从文本渲染为 Geometry 对象,中间需要经过 MIL 层。建议是能复用就复用,而不要每次都创建

但是在复用时,需要了解的是,不同的字号,创建出来的 Geometry 对象,不一定是相同的,这是为了清晰化显示的考虑。如字体比较小的时候,将会删减一些笔画等

获取文本的渲染尺寸

可以通过如下代码获取文本的渲染尺寸,也可以通过如下方法获取单个字符的渲染尺寸

  var computeInkBoundingBox = glyphRun.ComputeInkBoundingBox();
var matrix = new Matrix();
matrix.Translate(location.X, location.Y);
computeInkBoundingBox.Transform(matrix);
//相对于run.BuildGeometry().Bounds方法,run.ComputeInkBoundingBox()会多出一个厚度为1的框框,所以要减去
if (computeInkBoundingBox.Width >= 2 && computeInkBoundingBox.Height >= 2)
{
computeInkBoundingBox.Inflate(-1, -1);
}

以上的 computeInkBoundingBox 就是文本的绘制的尺寸,相对的坐标是文本的左上角,因此需要通过 location 叠加变换才能让此矩形和文本渲染重叠

     drawingContext.DrawRectangle(Brushes.Blue, null, computeInkBoundingBox);

文本的渲染尺寸也就是文本的字墨尺寸,此概念是文本排版概念

获取文本的文字布局尺寸

可以通过以上代码的 width 获取文本的字面的布局宽度,而布局高度则需要根据 BaseLine 等属性获取,代码如下

        /// <summary>
/// 获取<see cref="GlyphRun"/>的Size
/// </summary>
/// <param name="run"></param>
/// <param name="lineSpacing"></param>
/// <returns></returns>
public static Size GetSize(this GlyphRun run, double lineSpacing)
{
var renderingEmSize = run.FontRenderingEmSize;
var height = lineSpacing * renderingEmSize;
double width = 0;
foreach (var index in run.GlyphIndices)
{
width += run.GlyphTypeface.AdvanceWidths[index];
} width = width * renderingEmSize;
return new Size(width, height);
}

调用方法是 var glyphSize = glyphRun.GetSize(fontFamily.LineSpacing); 即可拿到文字的布局尺寸

字体回滚策略

字体的回滚策略可以比较佛系,毕竟是找不到字体了,此时就是从已安装的字体找到一个还能用的字体代替上去

在 WPF 源代码里面,可以看到底层的 Fallback 字体是 #GLOBAL USER INTERFACE 这个特殊的字体,为了保持和 TextBlock 差不多的逻辑,可以使用如下方法作为字体回滚

    /// <summary>
/// 用于回滚的字体对象<see cref="FontFamily"/>
/// </summary>
public class FallBackFontFamily
{
private const string FallBackFontFamilyName = "#GLOBAL USER INTERFACE";
private FontFamily FallBack { get; } = new FontFamily(FallBackFontFamilyName); private FallBackFontFamily(CultureInfo culture)
{
FontFamilyItems = FallBack.FamilyMaps
.Where(map => map.Language == null || map.Language.MatchCulture(culture))
.Select(map => new FontFamilyMapItem(map)).ToList();
} private IEnumerable<FontFamilyMapItem> FontFamilyItems { get; } /// <summary>
/// 获取<see cref="FallBackFontFamily"/>对象的单例
/// </summary>
public static FallBackFontFamily Instance => FallBackFontFamilyLazy.Value; private static readonly Lazy<FallBackFontFamily> FallBackFontFamilyLazy =
new Lazy<FallBackFontFamily>(() => new FallBackFontFamily(CultureInfo.CurrentCulture)); /// <summary>
/// 尝试获取fallback的字体名称
/// </summary>
/// <param name="unicodeChar"></param>
/// <param name="familyName"></param>
/// <returns></returns>
public bool TryGetFallBackFontFamily(char unicodeChar, out string familyName)
{
var mapItem = FontFamilyItems.FirstOrDefault(item => item.InRange(unicodeChar));
familyName = null; if (mapItem !=null)
{
familyName = mapItem.Target;
return true;
}
return false;
}
}

以上字体也就是 FontFamily.FontFamilyGlobalUI 属性的值,请看以下的 WPF 框架源代码

        internal const string GlobalUI = "#GLOBAL USER INTERFACE";

        internal static FontFamily FontFamilyGlobalUI = new FontFamily(GlobalUI);

默认在 WPF 的 Typeface 创建就包含了此逻辑,请看 Typeface 的源代码

        public Typeface(
FontFamily fontFamily,
FontStyle style,
FontWeight weight,
FontStretch stretch
)
: this(
fontFamily,
style,
weight,
stretch,
FontFamily.FontFamilyGlobalUI
)
{}

因此以上的回滚代码的意义其实不大,不过可以通过以上代码添加自己期望的字体回滚列表,如自己在应用程序里面带了特殊的字体,期望在找不到字体的时候使用自己的字体,就可以使用上面提供的回滚策略代码,使用方法如下

            if (typeface.TryGetGlyphTypeface(out var glyph))
{
// 忽略代码
}
else if (FallBackFontFamily.Instance.TryGetFallBackFontFamily(unicodeChar, out var familyName))
{
// 上面代码的 unicodeChar 就是传入的文本的字符
// 通过上面代码可以拿到回滚的字体是否包含此字符的定义
}
else
{
// 没有可以支持此字符的字体,那就看业务逻辑的处理啦
}

代码

例子

本文所有代码放在 githubgitee 欢迎访问

可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 581ea123df0d1067ec1ed3527e8b85edb2fd082e

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

获取代码之后,进入 NiwejabainelFehargaye 文件夹

轻文本

实现一个和 TextBox 差很多的单行轻文本最简代码如下

    class Foo : UIElement
{
public string Text { set; get; } = string.Empty; protected override void OnRender(DrawingContext drawingContext)
{
var fontFamily = new FontFamily("微软雅黑"); var fontSize = 15;
var y = 0;
drawingContext.PushOpacity(0.3);
foreach (var typeface in fontFamily.GetTypefaces().Skip(1).Take(1))
{
double offset = 3; var baseLine = fontFamily.GetBaseline(fontSize); if (typeface.TryGetGlyphTypeface(out var glyphTypeface))
{
foreach (var c in Text)
{
if (glyphTypeface.CharacterToGlyphMap.TryGetValue(c, out var glyphIndex))
{
// 在排版,不适合将每个字符的宽度独立进行计算。有很多字符是需要重叠布局的
var width = glyphTypeface.AdvanceWidths[glyphIndex] * fontSize;
width = GlyphExtension.RefineValue(width); #pragma warning disable 618 // 忽略调用废弃构造函数
var glyphRun = new GlyphRun(
#pragma warning restore 618
glyphTypeface,
0,
false,
fontSize,
new[] { glyphIndex },
new Point(offset, baseLine + y),
new[] { width },
DefaultGlyphOffsetArray,
new char[] { c },
null,
null,
null, DefaultXmlLanguage); drawingContext.DrawLine(new Pen(Brushes.Black, 2), new Point(offset, y), new Point(offset + width, y)); drawingContext.DrawGlyphRun(Brushes.Coral, glyphRun); var glyphSize = glyphRun.GetSize(fontFamily.LineSpacing); drawingContext.DrawRectangle(null, new Pen(Brushes.Black, 2), new Rect(new Point(offset, y), glyphSize)); // 布局的字符宽度
offset += width;
}
}
} y += fontSize;
}
drawingContext.Pop();
} private static readonly Point[] DefaultGlyphOffsetArray = new Point[] { new Point() }; private static readonly XmlLanguage DefaultXmlLanguage =
XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.IetfLanguageTag);
}

以上代码只是单个字符进行绘制,用于了解每个字符对应的布局值,也就是如上的 DrawRectangle 绘制的内容

上面代码的 GetBaseline 等都是辅助方法,可以从本文上面找到代码,也可以通过如下方式获取代码

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin fe704afdd32edb05005b1f35bcc87dc59c900040

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git

获取代码之后,进入 NiwejabainelFehargaye 文件夹

WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本的更多相关文章

  1. WPF简单入门总结

    WPF简单总结 最近看了点关于WPF的东西,总结了点点入门的东西. XAML语法基础 1.  定义样式 <Window.Resources><!--窗体资源的定义--> < ...

  2. 使用WebBrowser控件时在网页元素上绘制文本或其他自定义内容

    原文:使用WebBrowser控件时在网页元素上绘制文本或其他自定义内容 第一次在CNBlogs上发Post是提出一个有关使用WebBrowser控件时对SELECT网页元素操作的疑惑,这个问题至今也 ...

  3. Quartz2D 之 绘制文本

    1. 基础概念 1.1. 字体(Font) 同一大小.同一样式的字形的集合. 1.2. 字符(Character) 字符表示信息本身,一般指某种编码,如Unicode编码. 1.3. 字形(Glyph ...

  4. Win32汇编学习(4):绘制文本

    这次,我们将学习如何在窗口的客户区"绘制"字符串.我们还将学习关于"设备环境"的概念. 理论: "绘制"字符串 Windows 中的文本是一 ...

  5. 简单聊聊SOA和微服务

    转自:https://juejin.im/post/592f87feb123db0064e5ef7c  (2017-06) 简单聊聊SOA和微服务 架构设计中的朴素主义 前两天和一个朋友聊天,他向我咨 ...

  6. Delphi GDI对象之绘制文本

    转载:http://www.cnblogs.com/pchmonster/archive/2012/07/06/2579185.html 基本绘图操作(Basic Drawing Operations ...

  7. html5 canvas 笔记三(绘制文本和图片)

    绘制文本 fillText(text, x, y [, maxWidth])   在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的. strokeText(text, x, y [, ma ...

  8. FontMetrics ----- 绘制文本,获取文本高度

    Canvas 绘制文本时,使用FontMetrics对象,计算位置的坐标. public static class FontMetrics { /** * The maximum distance a ...

  9. 使用GDI绘制文本

    /// <summary>        /// 定义一个绘制文本        /// </summary>        public void Texts()       ...

  10. 简单聊聊Storm的流分组策略

    简单聊聊Storm的流分组策略 首先我要强调的是,Storm的分组策略对结果有着直接的影响,不同的分组的结果一定是不一样的.其次,不同的分组策略对资源的利用也是有着非常大的不同,本文主要讲一讲loca ...

随机推荐

  1. 记录--uni-app在不同平台下拨打电话

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 场景 在App中拨打电话是一个比较常见的应用场景,但是我们通过搜索文章,发现,大部分的博文都是uni-app官网的copy, copy u ...

  2. C# ASP.NET MVC 配置 跨域访问

    在web.config文件中的 system.webServer 节点下 增加如下配置        <httpProtocol>             <customHeader ...

  3. linux系统centos7.9如何安装nginx

    1.官网下载nginx nginx官网:https://nginx.org/ 选择稳定版进行下载,也可以下载老版本,下载成功后上传到服务器. 2.使用wget下载 访问nginx官网,在下载页面鼠标右 ...

  4. 可变形卷积系列(二) MSRA提出升级版DCNv2,变形能力更强 | CVPR 2019

    论文提出DCNv2,不仅对DCNv1的结构进行了改进,还使用了有效的蒸馏学习策略,使得性能有很大的提升,各个方面都值得借鉴   来源:晓飞的算法工程笔记 公众号 论文: Deformable Conv ...

  5. KingbaseES V8R6 sys_squeeze 使用

    sys_squeeze介绍 sys_squeeze是KingbaseES的一个扩展插件,该组件将提供人工调用命令实现对表dead tuple的清理工作.该组件在清理表空间的过程中,不会全程加排他锁,能 ...

  6. C++设计模式 - 原型模式(Prototype)

    对象创建模式 通过"对象创建" 模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定.它是接口抽象之后的第一步工作. 典型模式 Fac ...

  7. 为什么js项目中金额强烈推荐使用分而不是元

    相信我们都已经知道在js中浮点数据精度的问题了 看下面的例子 0.1 + 0.2 0.30000000000000004 如何解决呢? 在前后端交互过程中统一使用分为单位进行通讯,在最后的表示层处理为 ...

  8. Go 实战|使用 Wails 构建轻量级的桌面应用:仿微信登录界面 Demo

    概述 本文探讨 Wails 框架的使用,从搭建环境到开发,再到最终的构建打包,本项目源码 GitHub 地址:https://github.com/mazeyqian/go-run-wechat-de ...

  9. 带你玩转OpenHarmony AI:基于Seetaface2的人脸识别

    简介 随着时代的进步,全民刷脸已经成为一种新型的生活方式,这也是全球科技进步的又一阶梯,人脸识别技术已经成为一种大趋势,无论在智慧出行.智能家居.智慧办公等场景均有较广泛的应用场景,本文介绍了基于Se ...

  10. 【直播回顾】OpenHarmony知识赋能六期第五课—WiFi子系统

    8月11日晚上19点,知识赋能第六期第五节直播 <OpenHarmony知识赋能-WiFi子系统> ,在OpenHarmony开发者成长计划社群内成功举行. 第六期直播由从事底层基础工作1 ...