Table of Contents
格式包裹器:
支持TMP的26个富文本标签,以及更多动态包裹器,更可自行扩展更多包裹器类型。
使用包裹器进行文本拼接的优点:
- 通过编辑器实现语法检查,如果直接编写长串的复杂富文本,可能会由于不起眼的语法问题导致引擎不识别,而转写为包裹器格式,则可以降低低级错误的出现,从我个人的使用情况来看,只要语法检查铜过就不会出现引擎无法识别的情况。
- 快速复用富文本,如果使用字面量的富文本字符串,则需要自行实现大篇文本拼接,或单独编写一个方法来实现这个固定格式的输出;
- 包裹器支持参数填充,也支持使用反射,委托等方式自动获取上下文参数。
- 包裹器的返回值类型一致,可以存放在同一个数据结构中。
- 包裹器拥有完整的数据结构语法,可以动态添加和移除标签结构。
- 包裹器有大量语法糖可供选择。
- 包裹器多种语法,满足绝大部分使用情况。(如果不喜欢下面的new语法,可以先翻到后面查阅#框架语法)
下面是所有已有的富文本标签:
Align // 对齐方式 Color // 文本颜色 B // 加粗 I // 斜体 Cspace // 间距 Font // 字体 LineHeight // 行高 LineIndent // 行缩进 ID // 链接 Lowercase // 小写 Uppercase // 大写 Smallcaps // 小型大写 Margin // 边缘距离 Mark // 马克 Mspace // 字符间距 Noparse // 阻止解析 Nobr // 不间断空格 Page // 分页符 Pos // 水平位置 Size // 字体大小 Space // 水平间隔 Sprite // 精灵图 S // 删除线 U // 下划线 Style // 样式 Sup // 上标 Sub // 下标 Voffset // 垂直偏移 Width // 宽度
基本语法
包裹器最简单的用法是直接通过嵌套构造函数进行创建,下面是一个简单的示例:
_blockBuilder = new TextBlockBuilder(
new ColorBlock(Color.white, "重量"),
new NumberValueBlock(1.23),
new SizeBlock(20, "kg")));
_blockBuilder.ToString(); // 结果:<color=white>重量</color>1.23<size=20>kg</size>
格式包裹器TextBlockBuilder是所有blocker的基类,这个结构内部是一个字符串拼接池,包括它自身在内,所有的blocker都相当于一个文本拼接器,只是它自带一定的格式。
在TextBlockBuilder里的三个不同的blocker的作用是使用相应的富文本标签包裹,比如ColorBlock是将括号内的成员使用<color=white>标签包裹,离开这个blocker语法范围后将自动添加标签结束标记</color>。
这里使用
TextBlockBuilder作为最外层的包裹器,并不是因为必须使用它,而是最外层没有可以贯通首尾的标签,也就是不需要任何标签进行拼接。
注意,包裹器是基于多态模型实现全部功能的结构,你可以在参数中填入任何对象,通过重写ToString方法来使其可以被序列化。
注意,这个实例中虽然没有明确写明,但强烈建议将
blocker对象提升到静态作用域,并复用它,而不是反复动态创建它。
后续也会介绍更合理的池语法来优化这一点。
作用域语法
包裹器支持使用大括号表示一系列对象成员,这样可以利用具有默认值的无参构造函数进行创建,下面是一个简单的示例:
_blockBuilder = new TextBlockBuilder()
{
new ColorBlock() // 默认值就是白色
{
"重量"
},
new NumberValueBlock(1.23),
new SizeBlock(20) // 默认单位为像素
{
"kg"
}
};
_blockBuilder.ToString(); // 结果:<color=white>重量</color>1.23<size=20>kg</size>
在面对复杂的富文本时,这样写可读性更强;
动态参数语法
除了26个常规富文本标签,还支持更多值包裹器用法,比如:
SetParamters: 在根包裹器上使用赋值方法,可以将一系列值一一对应地填入到后续一系列包裹器中。DelegateBlock: 委托包裹器允许声名一个返回值为字符串的委托来获取值。RefernceBlock: 反射包裹器允许传入一个对象,以及成员名称来访问属性(字段,访问器,方法均可)。ValueBlock: 值包裹器,搭配SetParamters来设置值,目标需要实现Tostring方法。NumberValueBlock: 数值类型包裹器,搭配SetParamters来设置值,设置值会自动限制在最最小范围内,如果给定的参数是字符串类型,将尝试通过默认的格式化方法转为数值。
注意,所有BlockBuilder都支持直接使用反射获取对象,可以直接将返回字符串类型的方法名称传入,BlockBuilder 会自动识别;
前提是开启对应 blocker 的compatibility兼容性检查,但这带来额外的性能开销,如果没有特别的需求,建议还是使用DelegateBlock进行多态封装,这样也会更快更安全。
值插入语法
在任意BlockBuilder上使用SetParamters()方法可以传递一个参数列表,并以深度优先的方式传播(或者说以符合默认阅读习惯的方式传播,从左往右逐一设置)。
默认只有
ValueBlock这类值包裹器会自动从参数列表中获取值,其他标签包裹器需要手动使用.SetParameterization()参数化包裹器.
说人话:在设置参数时,值包裹器是默认用于设置值的,但如果想设置普通的标签包裹器,比如ColorBlock上的颜色值,则需要在对应的标签上调用SetParameterization方法。
设置后,这个标签会占用一个或多个参数名额,比如ColorBlock有一个颜色参数,它就会占用一个名额,如果传递的所有参数列表的名额用完了,后续的blocker就得不到参数,进而不会被修改并提前退出。
在使用.SetParameterization()参数化一个包裹器时,可以手动指定期望参数化的成员,使用小于等于0的值表示使用默认值;
你可以使用比如.SetParameterization(2)的方式来让这个包裹器强制占用2个参数。
- 如果参数列表的数量少于需求,在匹配时将会提前退出,导致后续值保持不变;
- 如果参数列表数量多于需求,在匹配时将不会关注这部分内容。
- 如果参数与包裹器期望的值类型不一致,那么会忽略,注意包裹器不会尝试去寻找其他位置上的值是否能够填入,它是严格按照索引一一对应的。
另外,你可能希望了解的:
- 关于体积:可以看到相比直接写富文本格式,这样写的体积更大。但对于开始不了解富文本格式的情况下,这个功能可以当作一种拼写检查。
- 关于安全:包裹器允许链式语法赋值,隐式反向赋值,部分模块也会使用反射,生成树等功能,这可能会在部分平台上被认为是不安全的;同时不当的写法也可能导致维护,调试困难;不过经测试,代码可以运行在webgl平台(原生webgl);除非目标平台非常严苛,一般都是可以正常使用的。
- 关于性能:这种做法可能会带来大量的对象,使用时务必注意要声名在当前作用域的全局用作静态,或持实例唯一,在规定用法下,性能开销是完全可以接受的。
- 关于文本拼接:所有要拼接文本的地方都用了stringbuilder,不会产生多余的文本对象,不会因为文本拼接带来gc。
自定义Block示例
public class MyBlock : TextBlockBuilder // 继承自TextBlockBuilder
{
// 声名一个构造函数并调用到基类,它可以传入一个参数列表,可以是任何对象,或是反射方法的名称。
public MyBlock(params object[] content) : base(content)
{
parameterizateOccupy = 1; // 如果有参数的话,设置占用参数默认为1。
}
// 如果要设置这个包裹器的参数时,在这里开放参数。
public MyBlock(object settingObject, params object[] content) : base(content) { }
// 重载参数设置方法,在被设置参数列表时调用,第一个参数是当前列表的下标,第二个参数是参数列表。
internal override int SetParameterizateMember(int startIndex, object[] parameters)
{
if (TryParse(parameters[startIndex]))
{
// 使用parameters[startIndex]获取自己的参数,或者自行处理整个列表。
// do something ...
// 用完自增一下下标,表示这个下标的参数被使用过了。
startIndex++;
}
// 失败了就直接返回。
return startIndex;
// 注意:不允许返回的下标参数小于传入的,这会被认为是在欺骗父容器。
}
// 重载转文本方法
public override string ToString()
{
var sb = StartStringBuild(); // 这是拼接文本的对象池
sb.Append("something start"); // 这里可以拼接开头
AppendContents(sb); // 自动拼接子项
sb.Append("something end"); // 这里可以拼接结尾
return EndStringBuild(sb); // 释放并返回文本
}
// 重载文本转blockbuilder
public static bool TryParse(string context, out MyBlock blocker)
{
// context是一段包含任意标签的文本,可能是多行的,需要自行处理为自己的block语法。
// 这个方法将会逐层循环执行,每个block将处理自己的最外层的内容
// 如果成功,返回ture,否则false
}
// 如果使用using块语法,或池化语法,需要设定如何释放这个包裹器
protected override void DisposeAsRequired()
{
TextBlockPool<MyBlock>.Release(this);
}
}
工具集语法(文本包裹器框架语法)
在使用包裹器创建格式时,大量的new语句可能会劝退一些比较保守的程序员。
所以这里也提供了可以隐藏这些new语句的方法,当然考虑到各位可能会对性能存疑,所以我特意基于对象池来实现各种获取操作,不管这对性能是否有所提升,都可以暂时堵住其他随意质疑你这么写的人的嘴巴。
起因是我向其他人推荐这个写法,就有人会有这个疑问,说在java中如果这么new对象,怕是要被老板约谈。
嘛,说的也没错,其实我也不喜欢这样去写,但设计之初没有太多选择,需要先建立一个基本的框架,否则这些进阶语法都免谈。
我早之前也写了语法逆向,尝试将一个富文本转为blocker,不过这样性能消耗有点大,而且仔细想想有点奇怪。
需要注意的是,框架语法是基于上述的文本包裹器语法实现的,在彻底了解框架语法前建议先从基础的繁琐的包裹器语法开始了解。
直接展示示例:
_blockBuilder = Pure(Color(Color.white, Label("重量")), Value(1.23), Size(20, Label("kg"))).BuildBlockByAgentOnce();
_blockBuilder.ToString(); // 结果:<color=white>重量</color>1.23<size=20>kg</size>
// 函数式最后的`BuildBlockByAgentOnce`作用是返回这个工具集的包裹器,并将构建框架放回池中释放。
// 也可以使用`BuildBlockByAgent`获取,但它会返回一个object类型的TextBlockBuilder对象,但是会保留框架,也就是你可以重复使用这个框架去生成blockBuilder。
// 除非你已经完全了解如何利用框架生成,否则无论如何,都建议使用`BuildBlockByAgentOnce`来构建。
在框架上使用构建方法返回的文本包裹器,在内容上与直接使用new block的做法生成的内容几乎一致。
但框架格式在细节处理上相比直接使用文本包裹器相比,可能会产生更多的嵌套块用于处理特殊语法,通常这不会影响最终的使用。
实际相比之下将无用的语法全部化简了,对吧,不过这段代码前提是需要引用一些命名空间:
using static XericLibrary.Runtime.MacroLibrary.MacroMath;
using static XericLibrary.Runtime.MacroLibrary.MacroMath.AlignBlock.Align;
using static XericLibrary.Runtime.MacroLibrary.MacroMath.RichTextUnits;
using static XericLibrary.Runtime.Type.SpatialAlgorithm.BlockUtilKit;
using Color = UnityEngine.Color;
话说如果新开一个脚本编写,因为这些函数不是扩展函数,找不到工具集类的话就根本找不到方法。
这是个硬伤,相比之下 textBlockBuild 则可以通过自动补全找出来,但如果你不知道工具集这个玩意的话,可能光看 textBlockBuild 的代码也没想到还有这么个玩意。
嵌套框架语法 NestBlocker
使用NestBlocker可以快速拼接多个嵌套块,下面是一个示例:
// 1. 这是一个有多个无参数嵌套组合的包裹器对象
private static TextBlockBuilder MyBlock1 =
new UBlock(new BBlock(new IDBlock("blockid", new NumberValueBlock(0))));
// 2. 这是一个适合快速入门的嵌套语法优化方式,将多个无参的包裹器对象放在了一起(参数集是必填的,但是这里没有人会用到参数集,所以使用默认值代替即可)
private static TextBlockBuilder MyBlock2 =
NestBlocker<UBlock, BBlock, IBlock>(default, ID("blockid", Value(0))).BuildBlockByAgentOnce();
// 3. 这是一个充分利用嵌套语法优化,将所有嵌套的块放在了一起,然后将参数单独提出来的用法
private static TextBlockBuilder MyBlock2 =
NestBlocker<UBlock, BBlock, IBlock, IDBlock, ValueBlock>(param).BuildBlockByAgentOnce();
// 这里的参数集是嵌套块中所有块都会共用的参数,所以这里仅需要声名块所必要的内容即可。
private static TextBlockParam param = new TextBlockParam("") { ID = "blockid" };
// 还有一种铜过检查字段值变化差异设置方法,也可以将id值设置到default值上。
private static TextBlockParam param = TextBlockParam.Default.SetDifferenceByEmpty(new TextBlockParam(){ ID = "blockid" });
// 除了相比上面的方法,运行开销会很高之外,其他表现基本一致。
值得注意的是,如果需要获得默认值,构造参数中需要填入一个名称
这个名称并不重要,所以我都是直接用空字符串代替,如果不填写则全部使用空值,在设置valueBlock时可能会导致行为不及预期等问题,注意辨别。
嵌套框架语法最多支持10个参数。
拼接框架语法 SpreadBlocker
首先拼接语法存在一些限制,一般用于一串连续的包裹器框架,并包含字符串等其他类型成员的简化语法。
还是用一个简单的示例来看一下:
// 这里的 pure 方法的作用就是充当一个纯拼接器,并且由于它会返回一个包裹器框架类,而不是接口对象,所以可以调用 BuildBlockByAgentOnce 方法,直接返回我们需要的 TextBlockBuilder
_blockBuilder = Pure(Color(Color.white, Label("重量")), Value(1.23), Size(20, Label("kg"))).BuildBlockByAgentOnce();
// 这个示例为了展示字符串拼接的功能,所以移除了color块,可以看到包裹器框架可以直接与字符串拼接。
_blockBuilder = ((BlockBuilderAgent)("重量" + Value(1.23) + Size(20, Label("kg")))).BuildBlockByAgentOnce();
// 基于拼接函数,可以将参数提出来,然后将这些需要包裹的子项写在对应的参数位置上即可
_blockBuilder = SpreadBlocker<ColorBlock, ValueBlock, SizeBlock>(param, Label("重量"), Empty(), Label("kg")).BuildBlockByAgentOnce();
private static TextBlockParam param => new("") { NumberValue = 1.23, TextSize = 20 };
IBlockAgentBase 接口是所有包裹器框架都会继承的接口,这里由于 valueblock 没有子项,或者说它不会处理子项,所以我们可以直接用 empty 方法返回一个空的包裹单元。
实际上使用 default/null 代替 Empty() 也是可以的,区别是引用为空的框架成员不会被框架处理,而实际存在但内容为空的框架会被其他框架处理,知道最终的构建步骤中才会被文本包裹器忽略。
总之这些方式最终的表现都是一样的,可以随意使用。
拼接框架函数最多支持10个参数,超出后也可以使用 +或* 链接两个框架函数。
// 错误:这样会与string的运算混淆,返回的类型会被认为是string。
"前缀" + Value(123) + "后缀"
// 正确:使用 * 强制拼接框架和字符串
"前缀" + Value(123) * "后缀"
// 错误:使用 + 拼接的结果是 IBlockLinkAgent ,只能支持 BuildBlockByAgent
(Value(123) + "后缀").BuildBlockByAgentOnce()
// 正确:使用 * 强制拼接会尝试返回 BlockBuilderAgent 类型,只要运算符的两者至少有一个是 BlockBuilderAgent 即可
(Value(123) * "后缀").BuildBlockByAgentOnce()
延迟赋值
一般对于单独的我会使用属性进行一次封装:
_blockBuilder = Color(Color.white, Label("Hello world"), Value(0)) // 定义
.BuildBlockByAgentOnce() // 构建
public float Text
{
set => TMPText.text = _blockBuilder.SetParamters(value).ToString();
}
封装后直接向 Text 赋值就可以一次性完成格式化到文本赋值的过程。
很方便,但是不够优雅。
这里提供一种延迟赋值的语法:
_blockBuilder = Color(Color.white, Label("Hello world"), Value(0)) // 定义
.BuildBlockByAgentOnce() // 构建
.LoadDelaySetAction(a => TMPText.text = a); // 设定延迟委托
// 使用launch来激活一次延迟事件
_blockBuilder.SetParamters(value).LaunchDelaySetAction();
但这样还需要每次赋值时重新构建一遍参数列表,不够方便,可以将参数换成委托,或者使用反射实现的框架:
_blockBuilder = Color(Color.white, Label("Hello world"), Delegate(() => value)) // 定义
.BuildBlockByAgentOnce() // 构建
.LoadDelaySetAction(a => TMPText.text = a); // 设定延迟委托
// 这样直接调用launch就可以了
_blockBuilder.LaunchDelaySetAction();
更多示例
这里提供更多的示例参考:
带有名称的值
结果长这样:行程(mm) 123.45 名称和值在同一行中,但是两侧对齐,适合用在 值和数字关联的场景中。
富文本格式:
<link=行程><line-height=0><align=left><color=#FFFFFFFF>行程(mm)</color></align>
<align=right><size=26><b><u>--</u></b></size></align></line-height></link>
-
文本包裹器语法:
private TextBlockBuilder _tripCardBlockBuilder = new TextBlockBuilder(new IDBlock(RichTextHelper.Stroke, new LineHeightBlock(0) { new AlignBlock(left, new ColorBlock(Color.white, "行程(mm)")), "\n", new AlignBlock(right, new SizeBlock(26, new BBlock(new UBlock(new NumberValueBlock(-1, 0))))) })); -
等效框架语法:
private TextBlockBuilder _tripCardBlockBuilder = ID(RichTextHelper.Stroke, LineHeight(0, Align(left, Color(Color.white, Label("行程(mm)"))), Label("\n"), Align(right, Size(26, B(U(Value(-1, 0))))) ) ).BuildBlockByAgentOnce(); -
高级框架语法:
private TextBlockBuilder _tripCardBlockBuilder = ID(RichTextHelper.Stroke, LineHeight(0, NestBlocker<AlignBlock, ColorBlock>(LeftDataParam, Label("行程(mm)")) + "\n", NestBlocker<AlignBlock, SizeBlock, BBlock, UBlock, NumberValueBlock>(RightDataParam) )).BuildBlockByAgentOnce(); private static TextBlockParam LeftDataParam => new("") { Align = left, TextSize = 26, NumberValue = -1, NumberMinValid = 0, }; private static TextBlockParam RightDataParam => new("") { Align = right, TextSize = 26, NumberValue = -1, NumberMinValid = 0, };
如果有多个类似的参数就可以这样写,将所有定义折叠起来可以不占空间。
private TextBlockBuilder _tripCardBlockBuilder = GetBlockBuilder(RichTextHelper.Stroke, "行程(mm)");
private TextBlockBuilder _pushPowerCardBlockBuilder = GetBlockBuilder(RichTextHelper.TotalThrust, "总推力(kN)");
private TextBlockBuilder _penetrationCardBlockBuilder = GetBlockBuilder(RichTextHelper.PenetrationDegree, "贯入度(mm/r)");
private TextBlockBuilder _excavateSpeedCardBlockBuilder = GetBlockBuilder(RichTextHelper.BlastingSpeed, "掘进速度(mm/min)");
private TextBlockBuilder _pushOliPressureCardBlockBuilder = GetBlockBuilder(RichTextHelper.PropulsionOilPressure, "推进油压(Mpa)");
private TextBlockBuilder _oilTempCardBlockBuilder = GetBlockBuilder(RichTextHelper.HydraulicOilTankTemperature, "液压油箱温度(C\u00b0)");
private TextBlockBuilder _gearOilTempCardBlockBuilder = GetBlockBuilder(RichTextHelper.GearOilTemperature, "齿轮油温度(C\u00b0)");
private static TextBlockBuilder GetBlockBuilder(string id, string label) => ID(id, LineHeight(0, NestBlocker<AlignBlock, ColorBlock>(LeftDataParam, Label(label)) + "\n", NestBlocker<AlignBlock, SizeBlock, BBlock, UBlock, NumberValueBlock>(RightDataParam))).BuildBlockByAgentOnce();
无序列表
结果长这样:
● 重大风险 3个
● 较大风险 3个
● 一般风险 3个
● 低风险 30个
富文本格式:
<line-height=0><align=left><color=#FF797C><size=10>●</size></color> 重大风险
<pos=80%><u>3个</u></line-height><line-height=10>
</line-height>
<line-height=0><align=left><color=#FF851B><size=10>●</size></color> 较大风险
<pos=80%><u>3个</u></line-height><line-height=10>
</line-height>
<line-height=0><align=left><color=#FAE361><size=10>●</size></color> 一般风险
<pos=80%><u>3个</u></line-height><line-height=10>
</line-height>
<line-height=0><align=left><color=#3EE2EE><size=10>●</size></color> 低风险
<pos=80%><u>30个</u></line-height><line-height=10>
</line-height>
-
文本包裹器语法
public static readonly TextBlockBuilder RiskLegendBuilder = new TextBlockBuilder() { new LineHeightBlock(0, new IDBlock(RiskSourceLogRed, new AlignBlock(left, new MacroMath.ColorBlock("#FF797C", new SizeBlock(10, "●")), " 重大风险\n", new PosBlock(80, percent, new UBlock(new NumberValueBlock(0, -1), "个"))))), new LineHeightBlock(10, "\n"), "\n", new LineHeightBlock(0, new IDBlock(RiskSourceLogOrange, new AlignBlock(left, new MacroMath.ColorBlock("#FF851B", new SizeBlock(10, "●")), " 较大风险\n", new PosBlock(80, percent, new UBlock(new NumberValueBlock(0, -1), "个"))))), new LineHeightBlock(10, "\n"), "\n", new LineHeightBlock(0, new IDBlock(RiskSourceLogYellow, new AlignBlock(left, new MacroMath.ColorBlock("#FAE361", new SizeBlock(10, "●")), " 一般风险\n", new PosBlock(80, percent, new UBlock(new NumberValueBlock(0, -1), "个"))))), new LineHeightBlock(10, "\n"), "\n", new LineHeightBlock(0, new IDBlock(RiskSourceLogWhite, new AlignBlock(left, new MacroMath.ColorBlock("#43B1FF", new SizeBlock(10, "●")), " 低风险\n", new PosBlock(80, percent, new UBlock(new NumberValueBlock(0, -1), "个"))))), new LineHeightBlock(10, "\n"), "\n", }; -
等效框架语法
public static readonly TextBlockBuilder RiskLegendBuilder1 = Pure( LineHeight(0, ID(RiskSourceLogRed, Align(left, Color("#FF797C", Size(10, Label("●"))) + " 重大风险\n", Pos(80, percent, U( Value(0, -1) + "个"))))), LineHeight(10, Label("\n")) + "\n", LineHeight(0, ID(RiskSourceLogOrange, Align(left, Color("#FF851B", Size(10, Label("●"))) + " 较大风险\n", Pos(80, percent, U( Value(0, -1) + "个"))))), LineHeight(10, Label("\n")) + "\n", LineHeight(0, ID(RiskSourceLogYellow, Align(left, Color("#FAE361", Size(10, Label("●"))) + " 一般风险\n", Pos(80, percent, U( Value(0, -1) + "个"))))), LineHeight(10, Label("\n")) + "\n", LineHeight(0, ID(RiskSourceLogWhite, Align(left, Color("#43B1FF", Size(10, Label("●"))) + " 低风险\n", Pos(80, percent, U( Value(0, -1) + "个"))))), LineHeight(10, Label("\n")) + "\n" ).BuildBlockByAgentOnce(); -
高级框架语法:
public static readonly TextBlockBuilder RiskLegendBuilder = Pure( LegendPartblock(RiskSourceLogRed, "#FF797C", "重大风险"), LegendPartblock(RiskSourceLogRed, "#FF851B", "较大风险"), LegendPartblock(RiskSourceLogRed, "#FAE361", "一般风险"), LegendPartblock(RiskSourceLogRed, "#43B1FF", "低风险") ).BuildBlockByAgentOnce(); private static IBlockAgentBase LegendPartblock(string id, string colorString, string riskType) { var param = new TextBlockParam("") { ID = id, Align = left, ColorString = colorString, TextSize = 10 }; var param2 = new TextBlockParam("") { Pos = 80, Units = percent }; return LineHeight(0, NestBlocker<IDBlock, AlignBlock>(param, NestBlocker<MacroMath.ColorBlock, SizeBlock>(param, Label("●")) + $" {riskType}\n" + NestBlocker<PosBlock, UBlock>(param2, Value(0, -1) + "个")) + LineHeight(10, Label("\n")) * "\n"); }
超长文本拼接
富文本格式:
经推演,项目预计将于<color=#FF9838>2026年12月12日</color>在<color=#FF9838>DK23+110</color>实现对接并完成整体施工,较原计划<color=#FF9838>2026年10月28日</color>的完工日期
<color=#FF9838>提前45天</color>。目前<color=#FF9838>剩余工期523天</color>。
-
框架语法
private static BlockBuilderAgent WarnColorLabel => Color("#FF9838"); private static TextBlockBuilder ReportBuilder = Pure( "经推演,项目预计将于" + WarnColorLabel.Add(Value(TimingStamp.ZeroDatetime)) * "在" + WarnColorLabel.Add(Value(TimingStamp.ZeroDatetime)) * "实现对接并完成整体施工,较原计划" + WarnColorLabel.Add(Value(TimingStamp.ZeroDatetime)) * "的完工日期" + WarnColorLabel.Add("提前" + Value(0) * "天") * "。目前" + WarnColorLabel.Add("剩余工期" + Value(0) * "天")).BuildBlockByAgentOnce();
这可能又会引入一个抽象过度的问题,虽然定义简洁,但声明式较长,且引入了多个不直观的转义方法。
看个人喜好吧。
注意事项
-
注意在与包裹器交互时,传入参数尽量不要为null,这样分辨不出来类型,在部分包裹器上可能会产生报错。
-
注意包裹器在静态声明时的上下文顺序,由于包裹器在构建时的特殊性,如果存在依赖另一个包裹器的包裹器语法 而被依赖项落后于依赖项目定义顺序,会导致在实际使用时找不到被依赖项。