不能直接用std::string拼接富文本标签,因为富文本是带语义的结构化数据,需解析为节点树(如textspan、imagespan)以支持样式嵌套、精准排版与高效交互,而非简单字符串操作。

为什么不能直接用 std::string 拼接富文本标签
因为富文本本质是带语义的结构化数据,不是纯字符串。你用 std::string 存 "<color>HP:</color><value>"</value>,后续想改某个 color 属性、提取所有图片占位符、或计算某段文字实际像素宽高时,就得反复正则匹配+子串切分——性能差、易出错、无法嵌套。
真正要做的,是把原始文本解析成节点树(比如 TextSpan、ImageSpan、StyleSpan),每个节点携带样式、尺寸、绘制逻辑。这样排版时遍历一次树即可累积宽度,点击响应也能精确到某个 Span。
常见错误现象:
– 文字换行后图标错位(没把 ImageSpan 当作“等高字符”参与行高计算)
– 颜色嵌套失效(<color>外<color>内</color>外</color> 解析成扁平列表,丢失嵌套关系)
– 中英文混排时,font_metrics::get_advance() 返回值对中文全角字符不准确(没按 Unicode 字符而非 UTF-8 字节计数)
怎么设计轻量级解析器:避开 XML/HTML 完整语法
游戏 UI 不需要支持 <script></script> 或 DTD,只需处理有限标签:color、size、img、br、sub。手写一个单次遍历状态机比引入 TinyXML 更快更可控。
- 用
std::vector<:shared_ptr>></:shared_ptr>存解析结果,避免深拷贝 - 遇到
<img src="icon_hp.png" w="16" h="16">时,立即加载纹理句柄并缓存到全局资源池(别等到绘制时才加载) - 属性值统一用双引号包裹,不支持单引号或无引号——省去词法分析歧义
- 忽略非法闭合标签(如
前没有对应<size></size>),只按开标签生效,避免栈失衡
示例解析逻辑片段:
while (pos ', pos) != std::string::npos) {
auto end = text.find('>', pos);
auto tag = parse_tag(text.substr(pos+1, end-pos-1)); // 提取 name+attrs
if (tag.name == "img") spans.push_back(std::make_shared<imagespan>(tag.attrs));
pos = end + 1;
} else {
spans.push_back(std::make_shared<textspan>(extract_text_run()));
pos++;
}
}
</textspan></imagespan>
图文混排排版的关键三步:基线对齐、行高统合、换行断点
图像不是文字,但用户感知上它该和周围文字“站同一排”。核心是把 ImageSpan 的绘制原点设为当前行基线(baseline)位置,而不是左上角。
- 每行高度 = max(所有 TextSpan 的
font_metrics::line_height, 所有 ImageSpan 的height) - ImageSpan 的 y 偏移 = 行 baseline -
ImageSpan::height * 0.25(典型值,需根据美术规范微调) - 换行不能切断
<img>或<color></color>标签内部;断点只允许在 TextSpan 的空格、标点或强制<br>处 - 使用
FT_Get_Advance()(FreeType)或IDWriteFontFace::GetDesignGlyphMetrics()(DirectWrite)获取真实字宽,别用字体字号简单乘法估算
运行时性能陷阱:哪些操作必须预计算
每一帧都重新解析、测量、布局,UI 会掉帧。必须拆离“可变”与“不变”部分。
- 解析结果(Span 树)和样式属性(颜色、字号)可缓存,只要原始字符串没变就复用
- 每种字体+字号组合的字符度量表(advance width)应预先构建哈希表:
std::map<:pair int>, std::unordered_map<char32_t float>></char32_t></:pair> - 图片 Span 的纹理尺寸在加载时就转成像素值存入节点,避免每次排版都查 GPU 纹理对象
- 不要在布局循环里调用
std::to_string()或std::regex—— 这些是帧率杀手
最容易被忽略的是:不同语言的换行规则差异。中文可以任意字间断行,英文需按单词,日文还有「禁则处理」。如果 UI 要支持多语言,TextSpan::break_opportunities() 必须按 locale 分支实现,不能只依赖空格。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











