ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RGF 文本内联对象(STextInlineObj/TextInlineObj)实战指南:在仓颉中实现图文混排与自释放资源管理

RGF 文本内联对象(STextInlineObj/TextInlineObj)实战指南:在仓颉中实现图文混排与自释放资源管理
  • 图形学
  • 桌面应用

【免费下载链接】RGF_CJ

RGF是仓颉Windows系统下的通用渲染框架(兼应用开发框架)。RGF仓颉版(后续简称"RGF")基于RGF(C/C++版)封装优化而来。RGF为开发者提供轻量化、安全、高性能以及高度一致性的2D渲染能力以及常用的桌面应用开发工具,并且提供对接Direct3D的相关接口,以满足开发者对3D画面渲染的需求。在Windows开发方面,RGF在子包内也提供开箱即用的Windows API,确保熟悉Win32的开发者快速上手。此外,RGF还提供一些常用功能的封装(如XAudio2、MF),以辅助开发者实现音频、摄像头等扩展功能的管理。

项目地址:https://gitcode.com/Cangjie-SIG/RGF_CJ
点击查看免费下载

在 Windows 桌面应用中,"文本 + 图片"混合排版是常见诉求,而 RGF 仓颉版(RGF_CJ)通过文本内联对象(TextInlineObj)提供了开箱即用的解决方案:它允许开发者把位图(Bitmap)或图层(Layer)作为行内元素嵌入文本布局(TextLayout),随文本一起参与排版与渲染。本指南将围绕 STextInlineObj 类文档 展开,讲解其与父类TextInlineObj的继承关系、自释放机制、创建与嵌入文本的完整流程,并结合仓库源码揭示其底层实现原理。读完本文,你将能够在自己的 RGF 窗口中轻松实现图片替换文本字符的图文混排效果,并正确管理相关资源的生命周期。

一、什么是文本内联对象

文本内联对象(TextInlineObj)是 RGF 文本体系中的一种"设备相关资源",它把一张图片包装成一个可以被文本布局引擎识别和排版的"字符级"元素。当文本中某个位置被内联对象替换后,该位置的原始字符不再渲染文字,而是渲染为内联对象对应的图片,从而实现"文中带图"的混合排版。

在 RGF 的 API 文档中,其定位清晰:

  • TextInlineObj:文本内联对象,设备相关资源,见 类 TextInlineObj;
  • STextInlineObj:设备相关资源 -自释放- 文本内联对象,见 类 STextInlineObj。

从源码看,两者的类声明为(rgf_text.cj 与 rgf_auto_class.cj):

// 基础类:文本内联对象(设备相关资源) public open class TextInlineObj <: SfcSubObjBase{ @When[debug]/* 创建者 */ internal var creatorHash:Int64 = 0; public init(){ // 创建空文本内联对象实例 super(CPointer<Unit>(),SfcSubObjType.TextInlineObj) } internal init(oPtr:CPointer<Unit>){ super(oPtr,SfcSubObjType.TextInlineObj) } } // 自释放子类:设备相关资源 - 自释放 - 文本内联对象 public class STextInlineObj <: TextInlineObj { ~init(){ if(objPtr.isNotNull()){ // 释放对象 unsafe { rApiSfcSubObjRelease(objPtr,SfcSubObjType.e2Num(objType)) } objPtr = CPointer<Unit>() // 自动回收 HashCode _hashCodeMgr[1].add(_hashCodeVal) } } }

二者的核心差异在于资源释放方式:TextInlineObj需要开发者显式调用release();而STextInlineObj通过仓颉的析构器~init()在对象被回收时自动调用底层释放接口rApiSfcSubObjRelease,并对objPtr置空、回收 HashCode,属于"自释放"的便捷形态。RGF 为每种设备相关资源都提供了对应的 S 前缀自释放版本(如SBitmap、STextFormat、STextLayout等),STextInlineObj即其中一员。

二、类层次与资源体系

2.1 继承链与基础能力

TextInlineObj的父类是SfcSubObjBase,其完整继承链为(rgf_base.cj):

SfcSubObjBase -> SfcSubObjBaseHash -> SfcSubObjBaseLink -> IObjLink & ToString

其中IObjLink接口定义了每个资源对象的基础契约(rgf_base.cj):

方法说明
isValid():Bool有效性验证,内部通过objPtr.isNotNull()判断底层对象指针是否有效
release():Bool对象释放,调用底层rApiSfcSubObjRelease后置空指针
get(vType:Int32):Option<Any>底层内部对象获取接口(vType=0返回指针,vType=1返回类型枚举)
set(vAny:Any,vType:Int32):Bool底层内部对象设置接口
toString():String输出{"ptr":...,"type":"TextInlineObj"}形式的格式化描述

2.2 类型标识:SfcSubObjType

RGF 通过内部枚举SfcSubObjType区分各类子对象,TextInlineObj的枚举值为6(rgf_base.cj):

internal enum SfcSubObjType { | Brush | StrokeStyle | Bitmap | Path | Matrix | TextCore | TextInlineObj // 6u8 | TextFormat | TextLayout | MemBitmap | Layer ... }

这个类型值会被传入rApiSfcSubObjRelease等底层接口,用于告诉底层库当前释放/识别的是哪一类设备资源。

2.3 自动释放宏支持

在 RGF 的元编程体系中,@LifeCycle等宏会通过isDeviceResources()判断成员类型是否为设备相关资源(macros/rgf_utils.cj),TextInlineObj被明确列入其中。这意味着当你在@LifeCycle标注的窗口类中声明TextInlineObj成员时,框架可以自动编排其创建与销毁生命周期,这也是后续实战示例得以简化的基础。

三、实战:在窗口中创建文本内联对象

3.1 前置依赖:文本渲染三件套

RGF 中渲染文本需要依赖TextCore(文本渲染核心)、TextFormat(文本格式)、TextLayout(文本布局)三个资源(见配套课程 013_渲染文本.md)。内联对象需要嵌入到TextLayout中参与布局,因此这三者同样缺一不可:

@LifeCycle class WinSelf <: WinCore{ let txtCore:TextCore // 文本渲染核心 let txtfmt:TextFormat // 文本格式 let txtlyt:TextLayout // 文本布局 let txtInline:TextInlineObj // 文本内联对象(本次主题) }

3.2 在设备资源创建事件中初始化

根据配套课程 014_文本内联对象.md,在createDeviceResources()生命周期事件中完成依赖创建:

public override func createDeviceResources():Bool{ // 先创建文本渲染依赖 surface .createTextCore(txtCore) // 创建渲染核心 .createTextFormat(txtfmt,24.0/*字体大小*/) // 创建格式 .createTextLayout(txtlyt,"绘制的文本",txtfmt,Size(Int32.Max,60)/*布局区宽高*/) // 加载一张图片 let bmp:Bitmap = surface.createBitmap("./res/test.png") // 从图片创建一个内联对象(16x16 大小的行内元素,置于 (0,0)) surface.createTextInlineObject(txtInline,0.0,0.0,16.0,16.0,bmp); // 图片位图本身已不再需要,立即释放 bmp.release() return true; }

注意:createDeviceResources()在窗口周期内可能被多次调用(参见 IRenderEventCore 的接口注释),因此这里创建的资源应确保在destroyDeviceResources()中正确释放,或直接使用自释放形态的STextInlineObj交由析构器托管。

3.3 createTextInlineObject 全量重载

createTextInlineObject由渲染表面(Surface/SurfaceCore)提供,源码位于 rgf_renderer.cj,共提供 6 种重载:

重载签名说明
createTextInlineObject(x:Float32,y:Float32,width:Float32,height:Float32,img:Image):TextInlineObj按位置与宽高创建,返回新对象
createTextInlineObject(rect:FRect, img:Image):TextInlineObj按FRect(浮点矩形)区域创建
createTextInlineObject(rect:Rect, img:Image):TextInlineObj按Rect(整型矩形)区域创建,内部转为Float32
createTextInlineObject(ref:TextInlineObj, x, y, width, height, img:Image):SurfaceCore复用传入对象,先把旧对象release()再重建(链式调用返回this)
createTextInlineObject(ref:TextInlineObj, rect:FRect, img:Image):SurfaceCore复用式 + 浮点矩形
createTextInlineObject(ref:TextInlineObj, rect:Rect, img:Image):SurfaceCore复用式 + 整型矩形

其中Rect/FRect重载的内部实现如下(以Rect为例):

public func createTextInlineObject(rect:Rect, img:Image):TextInlineObj{ return createTextInlineObject( Float32(rect.left),Float32(rect.top),Float32(rect.right - rect.left),Float32(rect.bottom - rect.top), img ) }

即宽高由矩形坐标直接推导:width = right - left,height = bottom - top。

3.4 底层分发:位图与图层的差异化路径

在 rgf_renderer.cj 中,createTextInlineObject会根据图片类型分流到不同的底层接口:

if(img.objType == SfcSubObjType.Bitmap){ return TextInlineObj(unsafe{ rApiDctClassCreateTextInlineObject(objPtr,x,y,width,height,img.objPtr) }) }else if(img.objType == SfcSubObjType.Layer){ return TextInlineObj(unsafe{ rApiDctClassCreateTextInlineObjectLay(objPtr,x,y,width,height,img.objPtr) }) } throw Exception(rgfErrorCpluapluaObjNotExist)

对应的 FFI 外部函数声明在 rgf_api.cj:

foreign func rApiDctClassCreateTextInlineObject(dctClass:RgfDct,x:Float32,y:Float32,width:Float32,height:Float32,bmp:RgfImage):InlineObj foreign func rApiDctClassCreateTextInlineObjectLay(dctClass:RgfDct,x:Float32,y:Float32,width:Float32,height:Float32,bmp:RgfImage):InlineObj

而InlineObj在 rgf_type.cj 中定义为type InlineObj = CPointer<Unit>,即底层原生对象指针。可以看出:

  • Bitmap 位图走rApiDctClassCreateTextInlineObject;
  • Layer 图层走rApiDctClassCreateTextInlineObjectLay(带Lay后缀);
  • 传入既非位图也非图层的图片时,会抛出rgfErrorCpluapluaObjNotExist异常;
  • 创建前会先校验表面与图片的isValid(),任一无效同样抛异常。

因此,内联对象的"内容源"实际上可以是任意位图或图层,这为后续在图层上绘制后再嵌入文本提供了想象空间(例如先在一个图层中组合多张图片,再整体作为内联对象置入文本)。

四、将内联对象嵌入文本布局

创建好的内联对象只有通过TextLayout.setInlineObject()置入文本布局,才会真正参与排版与渲染。该方法的源码在 rgf_text.cj:

public func setInlineObject(object:TextInlineObj,startPosition:UInt32,length:UInt32):Bool{ if(!isValid() || !object.isValid()){ throw Exception(rgfErrorCpluapluaObjNotExist) } unsafe{ rApiTxtLayoutSetInlineObject(objPtr,object.objPtr,startPosition,length) } } public func setInlineObject(object:TextInlineObj,textRange:TextRange):Bool{ setInlineObject(object,textRange.startPosition,textRange.length) }

4.1 参数语义

参数类型说明
objectTextInlineObj要嵌入的行内对象(需isValid(),否则抛异常)
startPositionUInt32字符开始位置(从 0 起)
lengthUInt32字符长度,即内联对象替换掉的字符数量
textRangeTextRange文本范围封装(startPosition+length,见 类 TextRange)

对应的底层 FFI 声明在 rgf_text_dw.cj:

foreign func rApiTxtLayoutSetInlineObject(textLayoutClass:Layout,object:InlineObj,startPosition:UInt32,length:UInt32):Bool

4.2 使用示例

沿用配套课程 014_文本内联对象.md 的写法:

// 将文本内联对象替换入文本 txtlyt.setInlineObject(txtInline, 0/*字符开始位置*/, 1/*字符长度*/);

这段代码的含义是:用内联对象(图片)替换布局文本中第 0 个字符起、长度为 1 的原始字符。例如布局文本是"绘制的文本",则最终渲染时首字符位置显示的是图片而非文字。

4.3 读取内联对象:getInlineObject 与 RangeInlineObj

RGF 同样提供了反向查询能力。getInlineObject(currentPosition)(rgf_text.cj)可以查询某个字符位置是否挂有内联对象,返回Option<RangeInlineObj>:

public func getInlineObject(currentPosition:UInt32):Option<RangeInlineObj>{ ... let retv:RangeInlineObj = RangeInlineObj( obj:if(inlineObj.isNull()){ Option<TextInlineObj>.None } else { TextInlineObj(inlineObj) }, range:unsafe{textRange.read()}.toClass() ) ... }

RangeInlineObj定义于 rgf_text_dw.cj,包含两个字段:

字段类型说明
objOption<TextInlineObj>命中的内联对象(无则为None)
rangeTextRange该内联对象覆盖的文本范围

注意:getInlineObject返回的是新包装的TextInlineObj实例(由底层指针构造),与最初创建时传入的对象并非同一实例,但其底层指针相同;实际使用时应以isValid()和底层指针为准。

五、内联对象的其他应用:文本修剪符号

除了替换正文文字,TextInlineObj还有一个重要用途:作为文本修剪(Trimming)的省略号符号。RGF 的TextFormat.setTrimming()允许指定一个内联对象作为超出布局宽度时的裁剪标记(rgf_text.cj):

public func setTrimming(trimmingOptions:Trimming,trimmingSign:Option<TextInlineObj>):Bool

配套的TrimmingPck包装类同样持有一个Option<TextInlineObj>类型的trimmingSign字段(rgf_text_dw.cj):

public class TrimmingPck{ public let trimmingOptions:Trimming public let trimmingSign:Option<TextInlineObj> const public init( trimmingOptions!:Trimming = Trimming(), trimmingSign!:Option<TextInlineObj> = Option<TextInlineObj>.None ) { ... } }

这意味着开发者完全可以自定义一个图片型省略号(例如"…"样式图标),替代默认的文本修剪符,实现更灵活、更品牌化的文本溢出表现。

六、资源管理与最佳实践

6.1 自释放 vs 手动释放

  • 手动释放:TextInlineObj创建后,应在合适的时机调用release()(如destroyDeviceResources()事件,或如示例中bmp.release()般用完即放)。release()会调用底层rApiSfcSubObjRelease并置空objPtr(rgf_base.cj),同时回收 HashCode。
  • 自释放:STextInlineObj通过~init()在 GC 回收时自动完成上述释放动作,适合"创建后不便手工管理"的场景。但请注意 GC 释放时机并不确定,若内联对象占用大量显存,仍建议显式release()以保证资源及时回收。

6.2 生命周期注意事项

  1. 校验优先:所有对TextInlineObj的操作(创建、嵌入、查询)都会在底层对象未创建时抛出rgfErrorCpluapluaObjNotExist异常,因此应确保内联对象在createDeviceResources()中创建完成后再被setInlineObject()引用;
  2. 多次调用安全:createDeviceResources()可能被多次触发,使用ref:形式的重载会在重建前自动release()旧对象,避免资源泄漏;
  3. 源图片及时释放:内联对象创建后会持有底层图片数据,示例中创建完内联对象立即bmp.release()是安全的——因为底层内联对象已经独立持有图片内容;
  4. 替换语义:setInlineObject是"替换"而非"插入",指定的length个字符将不再渲染文本,规划布局字符串时需留出被替换的位置。

七、小结

文本内联对象是 RGF 文本渲染能力中实现"图文混排"的关键一环:

  • 概念上,TextInlineObj把位图/图层包装为字符级行内元素;STextInlineObj是其自释放变体,通过析构器自动释放底层资源;
  • 使用上,三步即可完成:在窗口类声明内联对象成员 → 在createDeviceResources()中通过surface.createTextInlineObject()从图片创建 → 通过txtlyt.setInlineObject()嵌入指定文本范围;
  • 原理上,创建路径根据Bitmap/Layer分流到rApiDctClassCreateTextInlineObject/...Lay两个底层 FFI,嵌入路径经由rApiTxtLayoutSetInlineObject交由原生文本布局引擎完成排版;
  • 扩展上,内联对象还可作为文本修剪的自定义省略号,配合Trimming/TrimmingPck实现个性化溢出样式。

掌握TextInlineObj的使用,再结合 013_渲染文本.md 中的文本三件套流程与 014_文本内联对象.md 的完整示例,你就能在 RGF 中构建出丰富多彩的图文混排界面。相关 API 全量签名可进一步查阅 类 TextInlineObj、类 STextInlineObj 与 类 TextLayout。

  • 图形学
  • 桌面应用

【免费下载链接】RGF_CJ

RGF是仓颉Windows系统下的通用渲染框架(兼应用开发框架)。RGF仓颉版(后续简称"RGF")基于RGF(C/C++版)封装优化而来。RGF为开发者提供轻量化、安全、高性能以及高度一致性的2D渲染能力以及常用的桌面应用开发工具,并且提供对接Direct3D的相关接口,以满足开发者对3D画面渲染的需求。在Windows开发方面,RGF在子包内也提供开箱即用的Windows API,确保熟悉Win32的开发者快速上手。此外,RGF还提供一些常用功能的封装(如XAudio2、MF),以辅助开发者实现音频、摄像头等扩展功能的管理。

项目地址:https://gitcode.com/Cangjie-SIG/RGF_CJ
点击查看免费下载

相关推荐

上一篇:LoadingStateView 终极指南:快速实现优雅的 Android 加载状态管理
下一篇:RecordBuilder:Java记录构建工具

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进