QTextDocument 功能和用法详解
QTextDocument 是 Qt 富文本系统的核心文档模型。它本身不负责“像浏览器那样显示网页”,而是把文本、段落、图片、表格、列表、块级格式、字符级格式这些内容组织成一个可布局、可绘制、可编辑的文档树,然后交给布局器去排版,再交给绘制层输出到屏幕、打印机或任意 QPainter 目标。
如果一句话概括它的职责,就是:
- 存储文档内容
- 存储格式信息
- 把内容交给布局系统排版
- 为视图控件和绘制代码提供统一的数据模型
一、QTextDocument 到底是什么
QTextDocument 不是控件,不继承 QWidget。它更像“文档数据模型”。
它通常被这些东西使用:
- QTextEdit / QTextBrowser
- QPlainTextEdit 的一部分机制不同,但思想类似
- 自定义 QWidget 中手工绘制富文本
- 打印、导出 PDF、文档测量
它和这些类的关系可以这样理解:
-
QTextDocument
文档本体,存内容和格式 -
QTextCursor
编辑器,负责插入、删除、选择、应用格式 -
QTextBlock
段落节点,文档按块组织,通常一段就是一个 block -
QTextFragment
块内带统一字符格式的一段文本片段 -
QTextFrame
框架容器,文档根也是 frame,表格等也基于 frame 体系 -
QTextTable / QTextList
结构化内容对象 -
QTextCharFormat / QTextBlockFormat / QTextTableFormat / QTextListFormat / QTextFrameFormat
各种格式描述对象 -
QAbstractTextDocumentLayout
布局接口 -
QTextDocumentLayout
Qt 默认的富文本布局实现 -
QTextLayout
单个文本块内部的行布局器
所以 QTextDocument 的底层思想不是 DOM + CSS 盒模型 + JS 引擎,而是:
文档对象模型 + 富文本格式对象 + 文本布局器 + 绘制器
二、它能做什么
QTextDocument 的能力可以按职责分成几类。
1. 文本内容管理
它可以存纯文本、HTML、Markdown、图片占位、对象替换字符、表格、列表等内容。
常见能力:
- 设置整篇内容
- 获取纯文本
- 获取 HTML
- 获取 Markdown
- 清空文档
- 按块遍历内容
- 通过 cursor 增删改
2. 富文本格式管理
它支持:
-
字符格式
字体、字号、粗体、斜体、下划线、前景色、背景色、锚点、垂直对齐、文本轮廓等 -
段落格式
对齐、缩进、上下边距、行高、tab、列表关联、分页策略等 -
框架格式
边框、内边距、外边距、宽高约束、浮动等 -
表格格式
列宽、边框、单元格间距、padding、背景等
3. 布局与测量
它能:
- 根据可用宽度进行自动换行
- 计算文档尺寸
- 计算某个 block 或 frame 的几何位置
- 支持分页尺寸
- 支持打印和高分辨率输出
4. 资源管理
它可加载:
- 图片
- 样式资源
- HTML 引用的资源
- 自定义对象资源
通常通过资源类型 + URL 机制完成。
5. 撤销重做和编辑事务
它原生支持:
- undo
- redo
- beginEditBlock / endEditBlock 事务式编辑
6. 文本搜索和光标操作
它支持:
- 查找文本
- 根据位置获得块
- 根据位置获得 frame
- 用 QTextCursor 做选择与格式化
三、它的接口该怎么理解
你说“所有接口详解”,如果按文档逐个重载去列,会变成 API 手册,不利于理解。更有效的是按职责完整梳理。下面基本覆盖 QTextDocument 最重要的公共接口面。
1. 内容设置与导出
核心接口是:
-
setPlainText
设置纯文本 -
toPlainText
导出纯文本 -
setHtml
设置 HTML 富文本 -
toHtml
导出 HTML -
setMarkdown
设置 Markdown,Qt 版本支持时可用 -
toMarkdown
导出 Markdown -
clear
清空文档
注意一点:setHtml 不是保留原始 HTML 源码再原样显示,它会把支持的 HTML 子集解析成文档结构和格式对象,再由布局系统重新排版。之后再 toHtml,通常是 Qt 重新生成的 HTML,不一定和你输入的完全一致。
2. 基本文档属性
常用接口包括:
-
isEmpty
是否为空 -
characterCount
字符数,通常比你视觉看到的多一点,因为内部包含段落终止符 -
blockCount
段落数 -
lineCount
文档行数,依赖布局结果,和 blockCount 不是一个概念 -
metaInformation / setMetaInformation
设置文档元信息,比如标题、URL -
defaultFont / setDefaultFont
默认字体 -
defaultTextOption / setDefaultTextOption
默认文本选项,比如换行模式、tab 行为、方向等 -
baseUrl / setBaseUrl
资源相对路径的基准 URL,HTML 里图片和链接相对路径解析依赖它 -
documentMargin / setDocumentMargin
文档外边距 -
indentWidth / setIndentWidth
缩进宽度基准 -
useDesignMetrics / setUseDesignMetrics
是否使用设计度量而不是设备度量,常用于更稳定的排版或打印场景
3. 布局控制
这是理解高度计算的关键接口。
-
size
返回当前文档布局后的尺寸 -
idealWidth
返回理想宽度,通常是不受当前视口强约束下内容自然需要的宽度 -
textWidth / setTextWidth
设置文本布局宽度。这个最重要。
如果不设置 textWidth,文档会按默认宽度或未约束状态布局,得到的高度常常不是你想要的。绝大多数“计算 HTML 高度”问题,本质上都是没有先给定宽度。
-
pageSize / setPageSize
设置分页大小,常用于打印或分页布局 -
adjustSize
要求文档重新调整尺寸 -
documentLayout / setDocumentLayout
获取或替换布局器 -
markContentsDirty
标记范围失效,要求重排 -
sizeChanged 相关通知一般来自布局器或控件层,QTextDocument 本身也会发 contentsChanged 等信号
4. 结构访问
这些接口用于遍历文档结构。
-
begin / end
得到首尾 QTextBlock,用于遍历段落 -
firstBlock / lastBlock
首尾段落 -
findBlock
按字符位置找 block -
findBlockByNumber
按段落编号找 block -
rootFrame
获取文档根 frame -
frameAt
按位置找 frame -
object
按对象索引获取文档对象 -
allFormats
获取文档里使用到的格式集合
5. 搜索与查找
-
find
按字符串或正则查找 -
availableUndoSteps / availableRedoSteps
撤销重做栈状态
6. 编辑与撤销
QTextDocument 自身只负责存储和命令栈,真正编辑多由 QTextCursor 操作。
相关接口:
- undo / redo
- clearUndoRedoStacks
- isUndoAvailable / isRedoAvailable
- setUndoRedoEnabled / isUndoRedoEnabled
配合 QTextCursor:
- beginEditBlock
- endEditBlock
- joinPreviousEditBlock
这些接口很重要,因为 QTextDocument 是富文本编辑器的事务基础。
7. 资源系统
-
loadResource
当 HTML 中有图片、样式、其他资源引用时,布局或绘制阶段会通过它取资源 -
addResource
提前向文档注册资源,比如图片
资源类型常见有:
- ImageResource
- HtmlResource
- StyleSheetResource
- MarkdownResource
- UserResource
这也是你在自定义文档显示或离线帮助系统里常用的入口。
8. 信号
最常用的信号有:
-
contentsChanged
内容变了 -
contentsChange
某段范围发生变化 -
cursorPositionChanged
通常与编辑器联动 -
undoAvailable
-
redoAvailable
-
modificationChanged
-
blockCountChanged
-
baseUrlChanged
-
documentLayoutChanged
这些信号让控件层能做增量刷新、状态栏更新、按钮启停。
四、QTextDocument 的内部数据结构是什么
这是理解底层原理的关键。
它不是把 HTML 直接存成字符串再一边显示一边解析,而是转换成自己的文档内部模型。可以把它想象成下面几层:
1. 文档层
整个文档由 QTextDocument 表示,内部有一个根 frame。
2. 块层
文档由多个 QTextBlock 组成。每个 block 基本对应一个段落。HTML 里的 p、h1-h6、li、某些块级元素最终都会转成 block 或 block 相关结构。
3. 片段层
每个 block 内部是若干 QTextFragment。每个 fragment 是一段文本,附着一个 QTextCharFormat。
举例:
一段文字里“普通文本 + 粗体 + 红字”,通常会分成多个 fragment。
4. 对象层
列表、表格、图片、frame、用户自定义对象等会作为文档对象存在,通过 object index 与文本位置关联。
5. 格式池
格式不是每个字符复制一份,而是内部有共享的格式表,多个片段引用同一个格式对象,降低内存和比较成本。
这套结构比浏览器 DOM 简单得多,但对编辑器和富文本展示非常高效。
五、QTextDocument 是怎么“实现 HTML 渲染”的
核心结论先说:
QTextDocument 不是 HTML 引擎,它做的是“把 HTML 子集映射成 Qt 富文本文档模型,再用文本布局系统绘制”。
大致流程是下面这样。
1. 解析 HTML
调用 setHtml 后,Qt 会执行自己的 HTML 解析流程。
这个流程做的不是完整浏览器标准解析,而是:
- 识别 Qt 支持的 HTML 标签
- 解析简单样式
- 把标签映射成 block、fragment、frame、table、list、image 等内部对象
- 把样式映射成 QTextCharFormat、QTextBlockFormat、QTextTableFormat 等
例如:
-
b / strong
转成字符格式里的字重加粗 -
i / em
转成斜体 -
span style=color:red
转成字符前景色 -
p
转成一个 block,附带段落格式 -
ul / ol / li
转成 QTextList + block -
table / tr / td
转成 QTextTable 和 cell 结构 -
img
转成一个内嵌对象,后续通过资源系统取图像尺寸和内容
2. 构建文档结构
解析完 HTML 后,不再依赖原始 HTML 结构来直接绘制,而是生成 QTextDocument 的内部结构:
- block 序列
- frame 树
- fragment 片段
- 格式对象池
- 文档对象索引
3. 交给布局器排版
QTextDocument 本身只管内容模型,真正的布局由 QAbstractTextDocumentLayout 的具体实现负责,默认通常是 QTextDocumentLayout。
布局器会做这些事情:
- 遍历 frame、table、block 结构
- 为每个 block 创建或更新 QTextLayout
- 按当前 textWidth 或 pageSize 进行换行
- 计算每一行的 glyph、advance、ascent、descent、leading
- 计算段间距、缩进、表格列宽、单元格高度、图片占位大小
- 得到每个元素的几何区域
4. 绘制
当控件或自定义 paintEvent 调用绘制时,实际上是布局器遍历已经排好的版面,把内容画到 QPainter 上。
绘制会分几类:
-
文本
通过底层字体引擎和 glyph run 输出 -
背景、边框、选区
根据格式对象绘制矩形、边线、填充 -
图片
从资源系统取图后绘制到占位区域 -
表格和列表标记
按计算后的几何区域绘制
所以“HTML 绘制”的本质不是浏览器式实时盒模型渲染,而是:
HTML 子集解析成富文本文档模型
→ 富文本文档模型布局成几何盒
→ 布局结果绘制到 QPainter
六、QTextDocument 是怎么计算 HTML 高度的
这是最容易说清楚、也最容易被误用的一部分。
核心原则只有一句:
高度不是单独算出来的,而是布局后的副产品。
也就是说,高度计算必须依赖宽度。因为换行会改变总高度。
举个简单例子:
同一段 HTML,
宽度 200 时可能换成 10 行,
宽度 400 时可能只要 5 行。
所以任何“计算 HTML 高度”的正确步骤都必须先给宽度。
正确思路是:
- 创建 QTextDocument
- setHtml
- setTextWidth(目标宽度)
- 让布局完成
- 读取 size().height() 或 documentLayout() 的尺寸信息
内部发生了什么?
1. 宽度约束进入布局器
当你调用 setTextWidth,布局器知道每个 block 的可用行宽。
2. 每个 block 进行行分解
block 内部的 QTextLayout 开始逐行布局。它会:
- 从字符流中取一段可放入当前行的文本
- 根据字体度量和脚本 shaping 结果决定断行点
- 遇到空格、换行、断词规则、对象替换字符时调整
- 生成一行 QTextLine
3. 累积行高
每一行都有自己的:
- ascent
- descent
- leading
- line spacing
block 的总高度是这些行高再加上段前段后间距、边框、padding 等。
4. 累积块高度
文档总高度是所有 block、table、frame 等布局对象的垂直位置累计结果,再加文档边距。
如果有表格,还要额外考虑:
- 单元格内容重排
- 单元格 padding
- 行高由该行最高单元格决定
- 边框和间距
如果有图片,还要考虑:
- 图片声明尺寸
- 图片真实尺寸
- 缩放策略
- 基线对齐或行内对象对行高的影响
最终布局器得到整个文档的 bounding size,这就是 size().height() 的来源。
七、为什么 setHtml 后马上取高度有时不准
常见原因有几个。
1. 没有先设置 textWidth
这是第一大原因。没有宽度,布局无法确定最终换行,自然高度不稳定。
2. 资源还没准备好
比如 HTML 里有图片,布局时图片尺寸尚未可用,后续资源加载后又会触发重新布局。
3. 控件宽度和文档宽度不一致
例如 QTextBrowser 视口宽度和文档 textWidth 不一致,测出来的高度和最终显示不一致。
4. 文档边距或控件内边距没算进去
文档高度和控件总高度不是同一件事。
八、底层原理:它不是浏览器,而是文本版式系统
如果从底层抽象看,QTextDocument 更接近这类系统:
- 富文本编辑器排版引擎
- 桌面文档排版引擎
- 轻量级文本框架
而不是:
- 浏览器渲染引擎
- CSS 盒模型全实现
- JavaScript DOM 环境
它底层依赖的关键机制是:
1. Unicode 文本处理
Qt 文本系统会处理 Unicode 文本、双向文本、复杂脚本等。
2. 字体度量和 shaping
字符不是一个个简单按宽度相加,真实布局要经过字体引擎 shaping,把字符序列变成 glyph 序列,再决定 advance、ligature、kerning、脚本特性。
3. 分块布局
文档不是整篇一次性粗暴绘制,而是按 block、frame、table 递归布局。这样更利于编辑器增量更新。
4. 增量失效与重排
修改文档某段内容后,不需要所有内容都重新解析和布局。Qt 会标记 dirty range,然后从受影响区域开始更新布局。
5. 绘制与模型分离
QTextDocument 负责内容模型,QAbstractTextDocumentLayout 负责布局和绘制策略。这个分层让它能用于控件显示、打印、PDF 输出等不同场景。
九、它和浏览器 HTML/CSS 排版的本质区别
这部分很重要,因为很多误解都来自把 QTextDocument 当网页引擎。
1. 支持的是 HTML 子集
不是完整 HTML5。
2. CSS 支持有限
通常是一些简单的内联样式和少量结构样式,不是完整 CSS cascade、selector、flex、grid、position、transform 体系。
3. 没有 JS
没有 DOM 事件循环,没有脚本执行。
4. 布局目标不同
浏览器追求网页规范兼容和动态交互。
QTextDocument 追求桌面应用里的富文本展示和编辑。
5. 盒模型不完整
QTextDocument 更像“段落和行盒 + 格式修饰”,不是完整浏览器盒树。
十、常见的高度计算方法
如果你实际开发里要测 HTML 高度,思路如下:
- 创建 QTextDocument
- 设置默认字体,使其与最终控件一致
- 设置 documentMargin
- setHtml
- setTextWidth(可用内容宽度)
- 读取 size().height()
要注意的点:
- 如果放进 QTextBrowser,要用视口宽度,不是控件整体宽度
- 如果控件有 frame、margin、scrollbar,要把额外空间算进去
- 如果有图片资源,资源就绪后可能要重新测量
- 如果字体不同,结果会不同
十一、QTextDocument 的优势
- 轻量
- 与 QWidget 体系深度集成
- 富文本显示和编辑统一模型
- 支持打印和高质量文本输出
- 适合帮助文档、富文本标签、注释编辑器、日志面板、邮件正文编辑器等
十二、QTextDocument 的局限
- 不是网页引擎
- 复杂 HTML/CSS 支持有限
- 高度依赖宽度,不是简单静态值
- 大文档和复杂格式下布局成本不可忽视
- 图片和自定义对象会增加重排复杂度
十三、如果你想真正掌握它,最该记住的 5 个点
- QTextDocument 是文档模型,不是控件。
- HTML 会先被解析成 Qt 的富文本内部结构,而不是直接按网页绘制。
- 布局核心在 QAbstractTextDocumentLayout 和 QTextLayout。
- 高度计算本质上是“给定宽度后的排版结果”。
- 它更像富文本编辑器引擎,不像浏览器引擎。
======================= 以下的扩展性的知识====================
QTextDocument 相关类关系图,将QTextCursor、QTextBlock、QTextLayout、QAbstractTextDocumentLayout 一次串起来
先看最重要的一张关系图:
QTextEdit / QTextBrowser / 自定义 QWidget
↓ 使用
QTextDocument
├─ 管内容
├─ 管格式
├─ 管资源
├─ 管撤销重做
│
├─ 段落层:QTextBlock
│ └─ 片段层:QTextFragment
│ └─ 字符格式:QTextCharFormat
│
├─ 容器层:QTextFrame
│ ├─ 根 frame
│ └─ QTextTable / 列表等结构
│
├─ 编辑入口:QTextCursor
│
└─ 布局出口:QAbstractTextDocumentLayout
└─ 默认实现:QTextDocumentLayout
└─ 对每个 block 使用 QTextLayout 做分行排版
└─ 最终交给 QPainter 绘制
这张图里最核心的结论只有 4 个:
QTextDocument 是“模型层”,不是控件。
QTextCursor 是“编辑器”,不是文档本身。
QTextBlock / QTextFragment 是“内容结构层”。
QAbstractTextDocumentLayout / QTextLayout 是“排版绘制层”。
一、每个类到底负责什么
QTextDocument
它是整篇富文本的文档模型,负责:
保存文本内容
保存格式信息
维护块、片段、表格、列表、图片对象
提供 HTML / Markdown / 纯文本导入导出
维护资源、修改状态、撤销重做
把内容交给布局器去排版
你可以把它理解成“富文本内存数据库”。
QTextCursor
它是对 QTextDocument 的编辑游标,负责:
插入文本
删除文本
选择范围
应用字符格式和段落格式
插入表格、列表、图片、frame
控制编辑事务
它不是可见光标,而是“文档编辑句柄”。
一个很准确的类比是:
QTextDocument 像文档本体
QTextCursor 像拿着笔修改文档的人
QTextBlock
它表示一个文本块,通常可以近似理解成“一个段落”。
比如这段 HTML:
<h1>标题</h1> <p>第一段</p> <p>第二段</p>
最终通常会变成多个 block:
一个标题 block
一个正文 block
一个正文 block
QTextBlock 主要承载:
段落文本
段落格式 QTextBlockFormat
该段内的片段序列
布局结果对应的 QTextLayout
QTextFragment
它是一个 block 内部的“同格式文本片段”。
比如一段文字:
普通 + 粗体 + 红色
通常会被拆成多个 fragment,每个 fragment 绑定一个 QTextCharFormat。
所以结构上是:
QTextBlock
├─ QTextFragment: 普通文字
├─ QTextFragment: 粗体文字
└─ QTextFragment: 红色文字
QTextFrame
它是更高一级的容器结构。
根文档本身就有一个 root frame。除此之外,表格、某些复合结构也依附 frame 体系组织。
你可以把它理解成“块级容器树”。
QTextTable
它本质上是文档里的结构化对象,建立在 frame/object 体系之上。它不是简单字符串,而是有行列、单元格、单元格格式、列宽约束的真实文档对象。
QAbstractTextDocumentLayout
这是布局接口层。它负责:
根据文档内容做排版
计算尺寸
提供位置映射
把排版结果绘制出来
这个类是桥梁,连着两边:
上游是 QTextDocument 的内容结构
下游是 QPainter 的绘制设备
QTextDocumentLayout
这是 Qt 默认的富文本布局实现。大多数 QTextDocument 默认就配它。它会:
遍历 frame / block / table
对每个 block 做分行排版
计算高度、宽度、位置
绘制背景、边框、文本、图片、列表标记等
QTextLayout
这是“单个文本块内部”的排版器。
注意它的职责范围比 QTextDocumentLayout 小很多:
QTextDocumentLayout 管整篇文档
QTextLayout 管一个 block 如何拆成多行
所以常见链路是:
QTextDocumentLayout
遍历每个 QTextBlock
为该 block 使用 QTextLayout
计算 line1 / line2 / line3 ...
得到每行的几何信息
汇总成整篇文档的尺寸和绘制结果
二、这几个类的上下游关系
如果按“数据流”看,关系更清楚:
输入层
HTML / 纯文本 / Markdown / 光标编辑操作
模型层
QTextDocument
结构层
QTextFrame
QTextBlock
QTextFragment
QTextTable
格式对象
布局层
QAbstractTextDocumentLayout
QTextDocumentLayout
QTextLayout
输出层
QTextEdit / QTextBrowser / QPainter / 打印设备 / PDF
你可以把它记成一句话:
输入内容先变成文档结构,再变成排版结果,最后变成像素输出。
三、HTML 从哪里变成可绘制对象
如果从 setHtml 开始追踪,链路是这样:
调用 QTextDocument::setHtml
Qt 解析 HTML 子集
把标签和样式映射到文档结构
生成 block / fragment / frame / table / image object
生成对应格式对象
标记文档脏区,触发布局失效
QTextDocumentLayout 重新排版
每个 block 用 QTextLayout 分行
最终 drawContents 或控件重绘时输出到 QPainter
这一步里最关键的是:
HTML 不是直接“拿来画”,而是先翻译成 QTextDocument 的内部结构。
所以 QTextDocument 的核心不是“HTML 显示器”,而是“富文本文档模型”。
四、为什么 QTextBlock 和 QTextLayout 要分开
因为一个 block 是逻辑段落,QTextLayout 是这个段落的排版结果,两者不是一回事。
举例:
一段 100 个字的正文,在宽度不同的情况下,可能排成:
宽度大:2 行
宽度中:4 行
宽度小:8 行
block 还是同一个 block,但 QTextLayout 生成的 line 数量和几何结果完全不同。
所以:
QTextBlock 解决“这段是什么”
QTextLayout 解决“这段怎么排”
这是高度计算的关键。
五、HTML 高度为什么一定依赖 QTextLayout
因为文本高度不是字符数能决定的,而是排版结果决定的。
高度计算流程实际是:
QTextDocument 拿到内容
QTextDocumentLayout 遍历所有 block
每个 block 内部交给 QTextLayout
QTextLayout 按给定宽度做断行
得到每一行的 ascent、descent、leading、lineHeight
累加成 block 高度
所有 block / table / frame 再叠加成文档总高度
所以“计算 HTML 高度”的本质不是解析 HTML,而是“完成布局”。
换句话说:
没有宽度,就没有最终高度。
没有 QTextLayout 的逐行排版,就没有可信的高度。
六、从绘制角度再看一遍
如果你在自定义 QWidget 里手工绘制 QTextDocument,底层逻辑是这样:
创建 QTextDocument
setHtml
setTextWidth
文档触发布局
paintEvent 里创建 QPainter
调用 QTextDocument::drawContents
实际由 QTextDocumentLayout 遍历布局结果并绘制
也就是说,真正画东西的不是 QTextDocument 本身,而是它背后的布局器。
可以把调用关系记成:
QWidget::paintEvent
→ QTextDocument::drawContents
→ QAbstractTextDocumentLayout::draw
→ 绘制 frame / block / line / image / table
→ QPainter 输出
七、把常见类放进一张“职责边界图”
这张图很适合记忆:
内容模型
QTextDocument
编辑操作
QTextCursor
结构节点
QTextBlock
QTextFragment
QTextFrame
QTextTable
QTextList
格式对象
QTextCharFormat
QTextBlockFormat
QTextFrameFormat
QTextTableFormat
QTextListFormat
排版引擎
QAbstractTextDocumentLayout
QTextDocumentLayout
QTextLayout
QTextLine
最终输出
QTextEdit
QTextBrowser
QWidget + QPainter
Printer / PDF
八、最容易混淆的几组概念
QTextDocument 和 QTextEdit
QTextDocument 是数据模型
QTextEdit 是显示和交互控件
QTextCursor 和光标闪烁条
QTextCursor 是逻辑编辑游标
屏幕上闪烁的插入符只是控件层表现
QTextBlock 和 QTextLine
QTextBlock 是逻辑段落
QTextLine 是排版后的一行
QTextFragment 和字符
fragment 是同格式文本段
不是一个字符一个 fragment
QTextDocumentLayout 和 QTextLayout
前者负责整篇文档
后者负责单段文本分行
九、如果你想从源码思维理解它,最重要的是这条链
你可以把整个系统记成下面这句:
QTextCursor 修改 QTextDocument;
QTextDocument 组织成 QTextBlock / QTextFragment / QTextFrame;
QTextDocumentLayout 遍历这些结构;
每个 QTextBlock 用 QTextLayout 断行;
最后由 QPainter 输出。
这就是 Qt 富文本系统最核心的工作流。
十、一个最实用的心智模型
如果你平时写业务代码,不需要把它想得太底层,可以直接用这个模型:
QTextDocument:文档数据库
QTextCursor:编辑器命令对象
QTextBlock:段落节点
QTextFragment:段内样式片段
QTextLayout:段落排版器
QTextDocumentLayout:全文排版器
QPainter:最终画笔
只要这个模型清楚了,下面几个问题都会自动变简单:
为什么同样 HTML 宽度变了高度就变
为什么 setHtml 后要先 setTextWidth 再测高度
为什么 QTextBrowser 能显示 HTML
为什么自定义 QWidget 可以直接画 QTextDocument
为什么复杂网页 CSS 在这里不工作
十一、最后给你一个“从用户代码到像素”的完整流程
你写入 HTML。
QTextDocument 把它转成富文本文档结构。
文档结构分成 block、fragment、table、image 等对象。
布局器读取这些对象。
每个 block 用 QTextLayout 生成多行。
布局器汇总所有对象的位置和尺寸。
drawContents 或控件重绘时,把结果画到 QPainter。
屏幕上出现文本、图片、表格、链接样式。
画一张“HTML → QTextDocument → QTextLayout → QPainter”的时序图,重点只看两次转换:
- HTML 转成 QTextDocument 的内部结构
- 内部结构转成可绘制的排版结果
主时序图
用户代码
|
| 调用 setHtml(html)
v
QTextDocument
|
| 1. 解析 Qt 支持的 HTML 子集
| 2. 把标签和样式映射为内部对象
| - QTextBlock:段落
| - QTextFragment:段内同格式文本片段
| - QTextFrame:容器
| - QTextTable / QTextList:结构化对象
| - QTextCharFormat / QTextBlockFormat 等格式对象
| 3. 建立文档内容树和格式表
| 4. 标记内容失效 dirty
| 5. 发出 contentsChanged 一类通知
v
QAbstractTextDocumentLayout
|
| 6. 收到“需要重新布局”的信号
| 7. 读取当前布局约束
| - textWidth
| - pageSize
| - documentMargin
| 8. 开始全文布局
v
QTextLayout(逐个 QTextBlock)
|
| 9. 对 block 内文本做字形整形 shaping
| 10. 按给定宽度做断行
| 11. 生成多个 QTextLine
| 12. 计算每行的 ascent / descent / leading
| 13. 累加得到 block 高度
v
QAbstractTextDocumentLayout
|
| 14. 汇总所有 block / frame / table / image 的几何信息
| 15. 得到整篇文档的 size
| 16. 缓存布局结果
v
QPainter
|
| 17. drawContents 或 layout->draw
| 18. 按布局结果绘制:
| - 文本 glyph
| - 背景
| - 边框
| - 图片
| - 表格
| - 列表项目符号
v
屏幕 / Printer / PDF
控件视角的实际链路
如果你用的是 QTextBrowser 或 QTextEdit,外面会再包一层控件逻辑:
QTextBrowser 或 QTextEdit
|
| paintEvent
v
内部文本控制层
|
| 取 QTextDocument
v
QAbstractTextDocumentLayout::draw
|
v
QPainter 输出到控件视口
所以真正画内容的核心不是 QTextBrowser,也不是 QTextDocument 本体,而是文档对应的布局器。
高度是在什么时候算出来的
关键结论只有一句:
高度不是在 setHtml 时“直接算出来”的,而是在布局阶段由 QTextLayout 逐行排版后累加出来的。
更准确地说:
- setHtml 负责把 HTML 变成文档结构
- setTextWidth 或 pageSize 提供布局约束
- QTextLayout 对每个段落分行
- QAbstractTextDocumentLayout 汇总每个段落、表格、图片的高度
- 最终得到 document size
所以文档高度本质上是:
![]()
而每个段落高度又来自:
![]()
这就是为什么同一段 HTML,在不同宽度下高度会变。
宽度变化时的次时序
这张图对理解“为什么测高必须先给宽度”最重要:
用户代码
|
| 调用 setTextWidth(w)
v
QTextDocument
|
| 标记布局失效
v
QAbstractTextDocumentLayout
|
| 重新遍历 block
v
QTextLayout
|
| 重新断行
| 行数变化
| 每行高度重新计算
v
QAbstractTextDocumentLayout
|
| 文档总高度更新
v
size() / idealWidth() / documentSize() 一类结果变化
所以“HTML 高度”从来不是独立值,而是“给定宽度后的排版结果”。
把它想成浏览器会误解的地方
QTextDocument 的真实模型更像:
文档模型 + 段落排版引擎 + 绘制器
而不是:
DOM + CSS 全套盒模型 + JavaScript 引擎
因此:
- HTML 只是输入格式之一,不是底层存储格式。
- 底层真正参与测高和绘制的是 block、fragment、format、layout、line。
- 复杂网页 CSS 不生效,不是因为“没画出来”,而是因为它根本不属于这套排版模型。
你最该记住的 4 个拐点
- setHtml:只负责“解析并建模”,不是最终测高。
- setTextWidth:提供约束,是高度计算的前提。
- QTextLayout:真正负责逐段断行和行高计算。
- drawContents:消费布局结果,把内容画到 QPainter 上。
更多推荐
所有评论(0)