6

WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本

 3 years ago
source link: https://lindexi.gitee.io/post/WPF-%E7%AE%80%E5%8D%95%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-DrawGlyphRun-%E7%BB%98%E5%88%B6%E6%96%87%E6%9C%AC.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
WPF 简单聊聊如何使用 DrawGlyphRun 绘制文本

在 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);

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

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

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

在新的 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);

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

字体回滚策略

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

在 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 文件夹


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/WPF-%E7%AE%80%E5%8D%95%E8%81%8A%E8%81%8A%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-DrawGlyphRun-%E7%BB%98%E5%88%B6%E6%96%87%E6%9C%AC.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者前往 CSDN 关注我的主页

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号
lindexi%2F201985113622445

欢迎进入 Eleven 老师组建的 .NET 社区
lindexi%2F20209121930471745.jpg

以上广告全是友情推广,无盈利


Recommend

  • 96

    canvas文本绘制自动换行、字间距、竖排等实现

  • 81
    • 掘金 juejin.im 6 years ago
    • Cache

    轻量富文本异步绘制框架

    前言 如果遇到上面一个需求, 你会怎么处理, 若干个 UILabel + UIImageView? NSAttributedString拼接? CoreText? 我相信不论是哪种方式代码量都不小, 并且难以复用, 其他语言写富文本是那么轻松, Androi

  • 36
    • 掘金 juejin.im 6 years ago
    • Cache

    如何使用 css 绘制心形

    常遇到心形图案,比如点赞和取消点赞的使用场景。之前的使用方式是图片接入,作为img 或 backgroundImage 插入到 dom 中去。现在自己动手用css绘制一个心形图案。 心形 准备一个dom元素如下,为其id赋值为heart &lt;div id=

  • 45
    • 掘金 juejin.im 6 years ago
    • Cache

    如何使用css绘制钻石

    听说你想要钻石??买不起,还是用css来画一个吧,但你敢送给自己女朋友,不保证不被打。 下午两点要相亲,要不把这个送相亲对象? 效果 先看下效果吧,想一想怎么构图先。 上图是已经完成的效果。钻石整体都是由三角形构成,上五下三。上边是五个等边三角形,其中...

  • 3

    dotnet 读 WPF 源代码笔记 简单聊聊文本布局换行逻辑在 WPF 里面,带了基础的文本库功能,如 TextBlock 等。文本库排版的重点是在文本的分行逻辑,也就是换行逻辑,如何计算当前的文本字符串到达哪个字符就需要换到下一行的逻辑就是文本布局的重点模块。本...

  • 7

    dotnet OpenXML 聊聊文本段落对齐方式 本文来和大家聊聊在 OpenXML 里面,文本段落对齐方式。在 Word 和 PPT 的文本段落对齐规则是相同的,对齐的规则比较多,本文将一一告诉大家 文本的段落对齐,需要设置给段落属性上,在 OpenXML...

  • 6

    dotnet OpenXML 聊聊 PPT 文本行距行高计算公式 在 Office 的 PPT 里面,将根据储存文档的行距以及字号,计算出渲染出来的每一行的文本行高。本文将根据 Office 2016 和 M365 两个版本,加上 QQ 截图测量,通过魔法的计算方式加上逗比的算法,从而...

  • 7

    聊聊 Sharding-Jdbc 的简单使用 发表于 2021-12-12 分类于 Java 阅读次数: 8 阅读次数: 9 Disqus:

  • 8

    canvas核心技术-如何绘制图片和文本July 27, 2018/「 canvas 」/ Edit on...

  • 8

    就是这么简单!Pyecharts绘制可视化地图专辑 作者:云朵君 2023-02-07 11:44:02 总体来说Pyecharts地图绘图还是比较友好,在不需要多么炫酷的配置前提下,只需要将输入数据格式和类型弄清楚,其余默认配置即可。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK