使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本

2023-12-14

我的问题

我正在尝试创建一个TreeView这会将与搜索词匹配的节点文本部分加粗。我的代码被采用于这个问题。我有相同/类似的问题,有或没有ImageList使用过,但我会在这里发布未使用的版本。当我渲染文本时,我得到类似这样的结果,其中文本的最后一部分被切断,但是only对于某些节点。 IE。Version看起来不错,但其余的文本从边界上被截断的数量不同。

enter image description here

我认为我的TextFormatFlags标志正在影响这一点,但如果我在文本的测量/渲染期间不传递这些标志,则节点会在左侧切断。

如果我绘制粗体文本,我还会有vertical引入了间距问题。当我使用时你可以看到Plan作为搜索词,它比文本的其余部分要高一些。

enter image description here

如果我选择一个节点,您可以看到我再次遇到水平间距问题。

enter image description here

我的问题

  1. 没有粗体渲染的节点水平尺寸问题
  2. 使用粗体部分渲染的节点的垂直尺寸问题
  3. 粗体渲染节点的水平尺寸问题and active.

更新代码

感谢@jimi,我能够确定一些东西。当他回答时我很接近,但以下是我根据他的建议所做的更改。我确实做了一些与他不同的事情。

  1. 我立即退出tree_DrawNode when ( formClosing || e.Bounds.X == -1 )是真的,以避免一些图形故障。你可以在下面看到我的评论。

  2. 我喜欢如何BuildDrawingString清理了代码。我添加了一个可计算的 Width 属性,此外还修复了有关返回匹配文本的错误。

  3. 当节点具有焦点时,我绘制了突出显示背景;当节点没有焦点时,我绘制了窗口背景,以更好地模拟默认值TreeView行为。特别是当鼠标在一个节点上向下移动并在其他地方向上移动时。

  4. 而不是仅仅使用e.Bounds为了绘制背景矩形,我根据 e.Node.Bounds x/y、渲染文本所需的宽度和一点填充创建了自己的矩形。

    private void tree_DrawNode( object sender, DrawTreeNodeEventArgs e )
    {
        var textPadding = 2;
    
        // formClosing - don't need to redraw when shutting down, avoids seeing a little glitch with text offset
        // e.Bounds.X == -1 - when form loads, all *non-top level* nodes seem to draw on top of each other on first line
        //                    causing a big 'black blur' to happen when form loads b/c text is mashed together
        if ( formClosing || e.Bounds.X == -1 )
        {
            return;
        }
    
        using ( var boldFont = new Font( tree.Font, FontStyle.Bold ) )
        {
            var stringParts = BuildDrawingString( e, fieldSearch.Text, boldFont ).ToArray();
    
            // To better emulate default behavior, draw the 'selected' look only when focused, so if
            // you click down on item, originally selected item draws 'normal' and item clicking on is 'selected'
            // and if you let up on mouse outside of node, it reverts back to how it was.
            var isSelected = e.State.HasFlag( TreeNodeStates.Focused );
            var color = isSelected ? Color.White : tree.ForeColor;
    
            // Use e.NodeBounds X,Y and width of measured text with just a little bit of 
            // padding on left and right, e.Bounds was too wide.
            var nodeRectangle = new Rectangle(
                    e.Node.Bounds.X,
                    e.Node.Bounds.Y,
                    stringParts.Sum( p => p.Width ) + textPadding * 2,
                    e.Node.Bounds.Height
                );
    
            e.Graphics.FillRectangle( isSelected ? SystemBrushes.Highlight : SystemBrushes.Window, nodeRectangle );
    
            if ( isSelected )
            {
                using ( var focusPen = new Pen( Color.Black ) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot } )
                {
                    nodeRectangle.Size = new Size( nodeRectangle.Width - 1, nodeRectangle.Height - 1 );
                    e.Graphics.DrawRectangle( focusPen, nodeRectangle );
                }
            }
    
            var point = new Point( e.Node.Bounds.X + textPadding, e.Node.Bounds.Y );
    
            foreach ( var part in stringParts )
            {
                var font = part.Selected ? boldFont : tree.Font;
                RenderNodeText( part.Text, e, font, point, color );
                point.Offset( part.Width, 0 );
            }
        }
    }
    
    private void RenderNodeText( string text, DrawTreeNodeEventArgs e, Font font, Point offset, Color color )
    {
        var size = e.Node.Bounds.Size;
        var rect = new Rectangle( offset, size );
        TextRenderer.DrawText( e.Graphics, text, font, rect, color, e.Node.BackColor, treeFlags );
    }
    
    private IEnumerable<(string Text, bool Selected, int Width)> BuildDrawingString( DrawTreeNodeEventArgs e, string pattern, Font boldFont )
    {
        var itemContent = e.Node.Text;
    
        int measureText( string t, bool s ) => TextRenderer.MeasureText( e.Graphics, t, s ? boldFont : tree.Font, e.Bounds.Size, treeFlags ).Width;
    
        if ( pattern.Length == 0 )
        {
            yield return (itemContent, false, measureText( itemContent, false ));
        }
        else
        {
            var matches = Regex.Split( itemContent, $"(?i){pattern}" );
            var currentCharacter = 0;
            var patternLength = pattern.Length;
    
            for ( int i = 0; i < matches.Length; i++ )
            {
                if ( matches[ i ].Length >= 0 )
                {
                    yield return (
                        matches[ i ], 
                        false, 
                        measureText( matches[ i ], false ) 
                    );
    
                    currentCharacter += matches[ i ].Length;
                }
    
                if ( i < matches.Length - 1 )
                {
                    var matchText = itemContent.Substring( currentCharacter, patternLength );
                    yield return (
                        matchText,
                        true,
                        measureText( matchText, true )
                    );
    
                    currentCharacter += patternLength;
                }
            }
        }
    }
    

新花样

我将此处创建的所有最终代码从 WinForms 应用程序移至 VSTO Word AddIn 项目/表单内的表单,并且由于某种原因字体渲染有所不同。

  1. 一般的字体(普通字体)看起来更细更小。
  2. 粗体字体看起来比普通字体偏移得更高一些。

在下图中,最上面的表单是来自 Word 的表单,第二个表单(Form1 的标题)是我的 WinForms 应用程序。作为 VSTO 加载项运行时是否存在兼容性问题或其他问题?

enter image description here

原始代码

    private void Form1_Load( object sender, EventArgs e )
    {
        tree.DrawMode = TreeViewDrawMode.OwnerDrawText;
        tree.DrawNode += tree_DrawNode;
        tree.Font = new Font( "Microsoft YaHei UI", 10F, FontStyle.Regular, GraphicsUnit.Point, 0 );
        // tree.ImageList = imageList;
        tree.Nodes.Add( "PlanInfo" );
        tree.Nodes[ 0 ].Nodes.Add( "Version" );
        tree.Nodes[ 0 ].Nodes.Add( "Plan Name" );
        tree.Nodes[ 0 ].Nodes.Add( "Plan Sponsor" );
    }

    TextFormatFlags treeFlags = TextFormatFlags.Top | TextFormatFlags.Left | TextFormatFlags.NoPadding;

    private void tree_DrawNode( object sender, DrawTreeNodeEventArgs e )
    {
        var currentX = 0;
        var searchText = e.Node.Text;
        var searchTerm = fieldSearch.Text;
        var matches = Regex.Split( searchText, "(?i)" + searchTerm );
        var point = new Point( e.Node.Bounds.X + currentX, e.Node.Bounds.Y );
        var isSelected = ( e.State & TreeNodeStates.Selected ) != 0;
        var color = isSelected ? Color.White : tree.ForeColor;

        if ( isSelected )
        {
            e.Graphics.FillRectangle( SystemBrushes.Highlight, e.Node.Bounds );
        }

        if ( !string.IsNullOrEmpty( searchTerm ) && matches != null )
        {
            var currentCharacter = 0;
            var currentMatch = 0;
            var keyLength = searchTerm.Length;

            foreach ( var m in matches )
            {
                if ( !string.IsNullOrEmpty( m ) )
                {
                    point.Offset(
                        RenderNodeText( m, e, FontStyle.Regular, point, color ).Width,
                        0
                    );

                    currentCharacter += m.Length;
                }

                currentMatch++;

                if ( currentMatch < matches.Length || ( string.IsNullOrEmpty( m ) && currentMatch == 1 ) )
                {
                    var boldText = searchText.Substring( currentCharacter, keyLength );

                    point.Offset(
                        RenderNodeText( boldText, e, FontStyle.Bold, point, color ).Width,
                        0
                    );

                    currentCharacter += keyLength;
                }
            }
        }
        else
        {
            RenderNodeText( e.Node.Text, e, FontStyle.Regular, point, color );
        }
    }

    private Size RenderNodeText( string text, DrawTreeNodeEventArgs e, FontStyle altStyle, Point offset, Color color )
    {
        using ( var font = new Font( tree.Font, altStyle ) )
        {
            var size = e.Node.Bounds.Size;
            var textWidth = TextRenderer.MeasureText( e.Graphics, text, font, size, treeFlags );
            var rect = new Rectangle( offset, size );
            TextRenderer.DrawText( e.Graphics, text, font, rect, color, Color.Transparent, treeFlags );
            return textWidth;
        }
    }

查到的部分信息上一个问题没有晋级。

  • 文本格式标志很重要:这些设置对文本的呈现方式有很大影响。此外,每个控件都有自己的特定要求,也许差异很小 - 就像在本例中一样 - 但无论如何我们都需要适应。
    当文本左对齐并垂直居中时,TreeView 控件的效果会更好。
  • 文本渲染器非常精确,但我们希望(如前所述)始终使用矩形作为参考容器来测量和绘制文本。可能不要使用 Point,您会注意到使用此简单引用时,在类似情况下结果可能会发生变化。当在控件上绘图时,我们真的不希望出现这种情况。
  • 你删除了TextFormatFlags.NoClipping从原来的代码看,不好,这是一个keeper。除非您确实想要剪辑文本,但您需要指定如何剪辑它。可以为此组合其他标志。

具体到这个问题:

  • e.State == TreeNodeStates.Selected还不够,我们还需要测试TreeNodeStates.Focused,否则我们有一个weird选择或聚焦节点时文本渲染的差异;这些是不同的状态:可以选择一个节点,并聚焦另一个节点,两者必须同等渲染。
  • 的 Graphics 边界之间存在细微的差异DrawTreeNodeEventArgs以及节点项的边界。绘制背景时,使用前者,定义Node文本的约束,而使用后者。
  • 对同一部分文本使用不同粗细的 Font,我们必须使用 Node 的边界作为起始位置,使用由TextRenderer.MeasureText,对这些度量求和并偏移文本位置manually(如上所述,依赖于精度MeasureText).
  • 节点是否有图像并不重要,我们只需要考虑初始偏移量,等于e.Node.Bounds.X。在代码中,它存储在int drawingPosition = e.Node.Bounds.X;.

视觉结果:

TreeView custom drawing TextRenderer


TextFormatFlags twFormat = TextFormatFlags.Left | TextFormatFlags.VerticalCenter | 
                           TextFormatFlags.NoClipping | TextFormatFlags.NoPadding;

private void tree_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
    Color textColor = e.Node.ForeColor;
    Color backColor = e.Node.BackColor == Color.Empty ? tree.BackColor : e.Node.BackColor;

    if (e.State.HasFlag(TreeNodeStates.Selected) || e.State.HasFlag(TreeNodeStates.Focused)) {
        textColor = SystemColors.HighlightText;
        backColor = SystemColors.Highlight;
    }
    using (var brush = new SolidBrush(backColor)) {
        e.Graphics.FillRectangle(brush, e.Bounds);
    }

    string searchText = fieldSearch.Text;  // Search string from TextBox
    int drawingPosition = e.Node.Bounds.X;
    foreach (var part in BuildDrawingString(e.Node.Text, searchText)) {
        var style = part.Selected ? FontStyle.Bold : FontStyle.Regular;
        drawingPosition += RenderNodeText(part.Text, e, style, new Point(drawingPosition, e.Node.Bounds.Y), textColor).Width;
    }
}

private Size RenderNodeText(string text, DrawTreeNodeEventArgs e, FontStyle altStyle, Point offset, Color foreColor)
{
    using (var font = new Font(tree.Font, altStyle)) {
        var size = e.Node.Bounds.Size;
        var textWidth = TextRenderer.MeasureText(e.Graphics, text, font, size, twFormat);
        var rect = new Rectangle(offset, size);
        TextRenderer.DrawText(e.Graphics, text, font, rect, foreColor, e.Node.BackColor, twFormat);
        return textWidth;
    }
}

private IEnumerable<(string Text, bool Selected)> BuildDrawingString(string itemContent, string pattern)
{
    if (pattern.Length == 0) {
        yield return (itemContent, false);
    }
    else {
        var matches = Regex.Split(itemContent, $"(?i){pattern}");
        int pos = itemContent.IndexOf(pattern, StringComparison.CurrentCultureIgnoreCase);
        for (int i = 0; i < matches.Length; i++) {
            if (matches[i].Length == 0 && i < matches.Length - 1) {
                yield return (itemContent.Substring(pos, pattern.Length), matches[i].Length > 0 ? false : true);
            }
            else {
                yield return (matches[i], false);
                if (i < matches.Length - 1) {
                    yield return (itemContent.Substring(pos, pattern.Length), true);
                }
            }
        }
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本 的相关文章

  • asm、asm 易失性内存和破坏性内存之间的区别

    在实现无锁数据结构和定时代码时 通常需要抑制编译器的优化 通常人们使用asm volatile with memory在 clobber 列表中 但有时你会看到asm volatile或者只是一个简单的asm破坏记忆 这些不同的语句对代码生
  • Linq Any 始终返回 true

    我已经使用 Linq to Entities 多年 但这是我第一次遇到这个问题 我有Tips and Items表 每个提示可以有很多项目 我的数据库中只有 3 个项目 编辑项目时 我想确保GivenId对于具有相同提示的项目 字段是唯一的
  • 委托 System.Action 不接受 1 个参数

    那个行动 readonly Action execute public RelayCommand Action execute this execute null public RelayCommand Action execute Fun
  • Motif 库的水平绘制的 RowColumn 类 (C)?

    我正在使用 Motif Library 来完成我的工作 如果有人不熟悉这个库 您可以在这里找到文件列表https packages ubuntu com xenial amd64 libmotif dev filelist https pa
  • 带有成员 (operator[]) 函数的 invoke_result

    如何为成员函数正确调用invoke result 或者专门用于运算符成员函数 我试过std invoke result
  • 如何使用 CMake 链接多个库

    我有一些与 DCMTK 相关的代码 如果我从命令行使用 g 我可以成功构建并运行它 这是代码 include dcmtk config osconfig h include dcmtk dcmdata dctk h int main Dcm
  • 具有 Nhibernate 设计问题的领域模型

    我正在尝试进入 DDD with C 世界 我使用NHibernate作为我的ORM工具 因此尝试开发一个PI Persistence Ignorance 模型 但是 在我的一些实体 表示为 POCOS 中 我的属性设置器中有业务规则 例如
  • 文件已创建但无法写入

    我的计划 检查Settings txt 文件 如果该文件不存在 则创建文本并自动写入其中 如果 Settings txt 文件已存在 请忽略 不要创建或写入现有文件 我的问题 当文件不存在时 Settings txt 文件会创建 但它是空的
  • C++ 访问嵌套类的私有成员

    标题可能有点误导 我有以下问题 我有一棵由叶子和内部节点组成的树 用户应该能够在叶子中存储任何信息and该树有一些方法可以获取一组用户定义的值 并且需要在恒定时间内 未摊销 访问相应的叶子 我提出了以下想法 但它不起作用 因为不幸的是我无法
  • 使用 ServiceStack JsonSerializer 反序列化包含 Dictionary 属性的类型

    下面的代码片段显示了我可以实现此目的的两种方法 第一个是使用MsgPack https github com msgpack msgpack cli第二个测试是使用ServiceStack 的 JSONSerializer https gi
  • 使用 MemoryCache 而不是普通的旧 Dictionary 的令人信服的理由是什么

    我刚刚遇到内存缓存 http msdn microsoft com en us library system runtime caching memorycache aspx这是 NET 4 中的新增功能 我知道如果你想的话它会很有用 限制
  • 在 Visual Studio 2017 mac 上安装扩展 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我正在尝试在 Visual Studio for Mac 上安装 Visual Studio Marketplace 扩展 但是 Vi
  • llvm clang 编译器上的dynamic_cast失败

    我看到一个奇怪的失败dynamic cast正在返回NULL在 clang 编译器上 但相同的代码可以在 gcc 环境下运行 您能否指出根本原因是什么 之间可能有什么区别dynamic cast关于 llvm 和 gcc 我正在使用两个编译
  • WiX 安装程序在 vs 2012 上不起作用

    我想为我的应用程序创建一个安装程序 我已经下载了 WiX 3 6 并将其安装在 vs 2012 上 创建简单的winform应用程序 将 WiX 安装项目添加到我的解决方案中 右键单击参考并将我的 winform 应用程序添加到安装程序的参
  • 是否可以使用 struct stat 描述符获取和设置文件名?

    是否可以在获取或设置 重命名 文件名时给出文件的绝对路径 并将 struct stat 实例作为 lstat 函数的参数 正如我在文档结构中发现的那样 struct stat dev t st dev ID of device contai
  • 成员函数的Decltype

    class A int f int x int j return 2 decltype f p 给我错误 error decltype cannot resolve address of overloaded function 我不明白为什
  • Web API 2 c# 中的 Google reCaptcha

    我有一个 ASP NET Web API 2 项目 我正在尝试从表单中读取 Google Captcha 我尝试了这段代码 public string Post FoundingRequest model var response Requ
  • 如何在 C 中将 int 和数组保存在共享内存中?

    我正在尝试编写一个程序 让子进程在 Linux 上相互通信 这些进程都是从同一个程序创建的 因此它们共享代码 我需要它们能够访问两个整数变量以及一个整数数组 我不知道共享内存是如何工作的 我搜索过的每一个资源除了让我困惑之外什么也没做 任何
  • SQlite 查询 - 如何检索多列数据?

    我很难在网上找到一个关于使用 xcode 和 cocos2dx 从 SQlite DB 获取多个值的工作示例 这是我的sql查询 char sql query 100 sprintf sql query SELECT FROM SQList
  • Windows 安装程序 (C#) 错误代码 2869

    我在 VS 2005 中有一个项目 其中有一个控制台应用程序和一个与安装该应用程序关联的安装项目 我在控制台应用程序中还有一个安装程序类 安装项目将使用它在安装前进行一些验证 这些任务正在检查数据库连接字符串并检查某些目录位置以确保它们在安

随机推荐

  • 有没有办法用 Pillow 绘制渐变颜色的文本?

    我即将创建一个带有文字的图像 到目前为止一切正常 现在 为了进行微调 我认为使用渐变颜色的文本会很好 这就是我的重点现在 at 这就是我want具有 我已经成功生成了以下图像 使用该脚本 from PIL import Image Imag
  • Java中易失性和同步的区别

    我想知道将变量声明为volatile并且总是访问 a 中的变量synchronized this Java 中的块 根据这篇文章http www javamex com tutorials synchronization volatile
  • 表格行折叠/展开 css

    我正在使用以下示例Demo由 PSL 在网站上的帖子中提供 我有一个大表 当页面仅加载标题时 我不希望显示所有行 我应该改变border collapse collapse CSS 中的属性 在 dom 准备好后 你可以隐藏非 header
  • SQL java获取分配给自动增量主键的值[重复]

    这个问题在这里已经有答案了 我的表中有一个主键自动增量属性 我想知道为使用 statements executeUpdate 插入的行分配给它的值 如何以最好的方式实现这一目标 Use Statement getGeneratedKeys
  • Java 中的 Math.pow 错误

    我 显然 只是在学习编程 我似乎不知道该怎么做才能摆脱这个错误 错误位于倒数第二行 之前的行 System out print windChill 这里 写在下面 是 Java 生成的针对我遇到的错误的 可能提示 列表 expected m
  • 如何访问 iframe 复选框元素

    我有一个由复选框 父级 组成的网页 在同一个网页上 我还有一个来自另一个页面的 iframe 该页面显示许多记录 该页面还针对每条记录都有一个复选框 子级 如果我勾选父复选框 我想将其级联到 iframe 中的所有子复选框 并禁用这些子复选
  • 如何将参数从 Swift 本机代码“放入”Flutter

    我正在尝试将参数从 Swift 中的回调方法传递到 Flutter 这是我想要在我的本机 Java 代码中实现的示例 Override public void onRewardRequest final TJPlacement tjPlac
  • 阻止 Adob​​e Edge preload.js 文件加载 jquery

    首先我必须说 我不是一个专业的程序员 而是一个从实践中学习的设计师 因此 如果可能的话 恐怕我需要简单的解释 我在特定脚本的帮助下使用 Edge animate 作为网站的一部分 由 Andrew Trice 编写 请参见此处 http w
  • AngularJS:重用具有不同父级的组件

    假设我有 A 和 B 可重用组件 我希望 B 调用 A 上的方法 当然只有当 B 是 A 的子级时 此外 我希望 B 用作独立组件 没有 A 作为父级 在这种情况下 不应调用不存在的 A 中的方法 我的第一次尝试是在链接功能中获取控制器 与
  • Flask-Admin针对不同角色的不同表单和column_list

    跟进这个问题Flask Admin Role Based Access 根据角色修改访问权限我不明白如何实现基于角色的视图 特别是关于表单和列列表 说我想要MyModelView如果用户是普通用户或超级用户 则显示不同的列 覆盖is acc
  • Spring security AntMatcher 不工作

    这是我配置 spring security 的方式 在控制器中我获得 ROLE ANONYMOUS 作为权限 看起来安全性并没有拦截请求并检查 JWT 如何配置antmatcher Configuration EnableResourceS
  • 如何添加、编辑和删除数据库的逗号分隔值。?

    我创建了一个名为角色的表 字段类似于 角色ID 角色 禁止进程 禁止端口 这里角色ID是唯一的 我有一个逗号分隔的 prohibitedprocess 字段值 例如 prohibitedprocess gt skype teamviwer
  • Foundation 中的 Google 地图 API 显示模式无法正确显示 [重复]

    这个问题在这里已经有答案了 可能的重复 Reveal Modal 内的 Google Map API 未完全显示 我有一个位于 Reveal Modal 中的 Google Map API 对于那些不知道那是什么的人来说 它基本上是一个隐藏
  • spring-boot 基本 JSP 404 未找到

    无法使用 spring boot 加载非常简单的 JSP 页面 出现 404 Not Found src main java SampleWebJspApplication java Configuration EnableAutoConf
  • 在类中实现 Spinner 类型对象作为子进程

    我是一个完全的初学者 今天才开始上课 我试图制作一种 旋转器 对象 我可以称之为这样的东西 我感到困惑的一件事是是否使用 线程 线程 或 过程 我刚刚在某处读到 一个线程实例的成本为 8meg 因为这是一个简单的文本旋转器 它不保证使用大量
  • 使用 C 的 Beaglebone 黑色 PWM

    我用 C 语言为 beaglebone black 编写了一个示例 pwm 函数 每当我在其他模块或 main 中进行函数调用时 我都会遇到分段错误 请帮助我在哪里犯了错误以及如何处理这个问题 下面是代码 int trigger pwm o
  • 在渲染视图之前测量视图

    我需要找出将视图附加到其父视图后会有多大 我已经重写了这个方法 onMeasure int int 但看起来只有当我实际使用以下方法将自定义视图添加到其容器时才会调用此方法 addView myView 您认为有没有办法在渲染视图本身之前获
  • 编译OpenSSL时.rodata和-fPIC是什么意思?

    我正在尝试编译 openssl 但遇到错误 使用的 CFLAGS 是 O2 fPIC fno strict overflow 有人可以向我解释一下是什么吗 rodata下面这句话是什么意思 usr bin ld libcrypto a wp
  • 查找最近的观察结果 - R

    我有两组 已排序的 POSIXct 时间序列 如下所示 set seed 123 ll sort strptime 16 07 2015 format d m Y 10 3600 1 3600 round rnorm 3600 digits
  • 使用 OwnerDrawText 模式定位和突出显示 TreeView 节点文本

    我的问题 我正在尝试创建一个TreeView这会将与搜索词匹配的节点文本部分加粗 我的代码被采用于这个问题 我有相同 类似的问题 有或没有ImageList使用过 但我会在这里发布未使用的版本 当我渲染文本时 我得到类似这样的结果 其中文本