现代Qt开发教程新手篇2.4——QFont 与文本渲染基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt地址https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeQt/1. 前言 / 为什么文本渲染值得单独学很多初学者觉得文字显示有什么好学的给 QLabel 设个文本不就行了。说实话我一开始也是这么想的。直到有一天我需要在一个自定义的 Widget 上精确排列多行文字——要在指定位置画出标题、正文、注释每段的字体大小还不一样还要在文字之间留出精确的间距。那会儿我才意识到Qt 的文本渲染远不只是drawText()一行代码那么简单。QPainter 的drawText()确实能画文字但如果你需要知道一段文字画出来之后占多大空间、需要在哪里换行、多行文字的行间距怎么算——这些都需要 QFont 和 QFontMetrics 的配合。而如果你需要渲染带格式的富文本——加粗、斜体、不同颜色混排——那就得请出 QTextDocument 了。这篇文章我们一起来把 Qt 的文本渲染体系搞清楚QFont 怎么设置字体属性、drawText 的各种用法和坐标含义、QFontMetrics 怎么帮我们计算文字尺寸做精确布局、以及 QTextDocument 的富文本渲染基础。搞懂这些之后你在自定义控件里排列文字就跟玩积木一样顺手。2. 环境说明本篇代码适用于 Qt 6.5 版本示例基于 Qt 6.9.1 验证CMake 3.26C17 标准。示例代码依赖 QtGui 和 QtWidgets 模块——QFont 和 QFontMetrics 在 QtGui 中QTextDocument 也在 QtGui 中但我们的展示窗口需要 QWidget所以两个模块都要链接。关于字体示例中使用了一些通用字体名称如 “Arial”、“Sans”、“Monospace”Qt 会根据平台自动映射到可用的实际字体你不需要额外安装什么字体包。所有代码在 Linux、Windows、macOS 上都可以编译运行。3. 核心概念讲解3.1 QFont 构造与属性设置QFont 是 Qt 里描述字体的类。它本身不包含任何文字数据它只是告诉 Qt “我想用什么字体来显示文字”。创建 QFont 最直接的方式就是指定字体族名和大小// 基本构造族名 大小QFontfont1(Arial,12);// 更完整的构造族名 大小 加粗 斜体QFontfont2(Times New Roman,16,QFont::Bold,true);// 加粗 斜体字体族名是一个容易搞混的概念。你传入的 “Arial” 或者 “Times New Roman” 不一定在当前系统上存在——特别是在 Linux 上可能根本没有 Arial 这个字体。Qt 会按照一定的规则做字体替换先找你指定的族名找不到就找同类的替代字体比如 sans-serif 类型的字体再找不到就用系统默认字体。这个替换过程是自动的你不会收到任何警告所以如果发现文字显示出来的字体跟你想的不一样大概率就是系统上没有你指定的字体。如果你希望跨平台使用统一的字体外观可以用 Qt 提供的通用字体族名QFontsansFont(Sans,12);// 无衬线字体Linux 映射到 Sans SerifQFontserifFont(Serif,12);// 衬线字体QFontmonoFont(Monospace,12);// 等宽字体QFont 有很多属性可以调整日常开发中最常用的就是族名、大小、加粗、斜体这四个。其他如字间距、字母间距、下划线、删除线等属于精细调整用到的时候查文档就行QFontfont(Arial,14);font.setBold(true);// 加粗font.setItalic(true);// 斜体font.setUnderline(true);// 下划线font.setStrikeOut(true);// 删除线font.setPointSize(16);// 设置字号点大小font.setPixelSize(20);// 设置字号像素大小font.setLetterSpacing(QFont::AbsoluteSpacing,2);// 字母间距 2 像素font.setWordSpacing(5);// 词间距 5 像素这里有个值得注意的区别setPointSize()和setPixelSize()。点大小是跟 DPI 无关的绝对单位——一个 12pt 的字体在 1x 屏幕和 2x 屏幕上看起来物理大小一样Qt 会自动做 DPI 缩放。像素大小则是跟 DPI 相关的——一个 20px 的字体在 2x 屏幕上看起来只有 1x 屏幕上的一半大。日常开发建议用setPointSize()这样在不同 DPI 的屏幕上显示效果更一致。3.2 QPainter::drawText() 绘制文字与对齐有了 QFont 之后我们在 paintEvent 里就可以用 drawText 把文字画出来了。drawText 有好几个重载版本用法和坑都不一样我们一个一个来看。最简单的版本是drawText(x, y, text)这里的 x 是文字左边缘的位置y 是文字基线的位置。基线是什么就是英文字母 “a”、“b”、“c” 底部对齐的那条线字母 “g”、“p”、“y” 有部分会延伸到基线以下。注意 y 不是文字矩形的顶部这一点非常关键也是新手最容易踩的坑voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setFont(QFont(Arial,20));painter.setPen(Qt::black);// y 30 是基线位置不是顶部位置painter.drawText(10,30,Hello Qt!);// 如果 y 设成 0文字大部分会在 Widget 上方被裁剪掉painter.drawText(10,0,这段文字大部分看不见);}实际开发中更推荐使用 QRect 版本的 drawText配合对齐标志这样你不用操心基线的问题voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setFont(QFont(Arial,16));// 在整个 Widget 区域内居中显示painter.drawText(rect(),Qt::AlignCenter,居中文字);// 左上角对齐painter.drawText(rect(),Qt::AlignTop|Qt::AlignLeft,左上角);// 指定一个矩形区域右下角对齐QRectarea(50,50,300,100);painter.drawText(area,Qt::AlignBottom|Qt::AlignRight,右下角);// 自动换行需要加上 Qt::TextWordWrap 标志painter.drawText(QRect(10,200,200,200),Qt::TextWordWrap,这是一段比较长的文字当它超出矩形宽度时会自动换行显示。);}QRect 版本的 drawText 会自动把文字放在矩形内按对齐标志定位不需要你手动计算基线位置。而且加上Qt::TextWordWrap之后文字超出矩形宽度会自动换行非常方便。我个人的经验是除非你有非常精确的像素级定位需求否则永远用 QRect 版本而不是 x/y 版本。drawText 还有一个不太常用但很有用的返回值它返回一个 QRect表示文字实际占据的矩形区域。这在做后续布局的时候很有用QRect boundingRectpainter.drawText(100,100,300,50,Qt::AlignLeft|Qt::AlignVCenter,Hello World);// boundingRect 就是文字实际占据的区域可以用它来定位下一个元素intnextXboundingRect.right()10;// 文字右侧留 10px 间距3.3 QFontMetrics 计算文字尺寸当我们需要做精确的文字布局——比如把一段文字和另一个控件并排排列或者根据文字宽度来决定控件大小——就需要 QFontMetrics 出场了。QFontMetrics 能告诉你给定字体下一段文字的精确像素尺寸。获取 QFontMetrics 最简单的方式是从 QPainter 或者 QFont 直接拿// 方式 1从当前 painter 的字体获取QPainterpainter(this);painter.setFont(QFont(Arial,14));QFontMetrics fmpainter.fontMetrics();// 方式 2直接从 QFont 构造QFontfont(Arial,14);QFontMetricsfm(font);QFontMetrics 最常用的几个函数QFontMetricsfm(QFont(Arial,14));// 单个字符的宽度intcharWidthfm.horizontalAdvance(A);// 字符串的总宽度inttextWidthfm.horizontalAdvance(Hello World);// 字体的高度 ascent descentintfontHeightfm.height();// ascent基线到文字顶部的距离intascentfm.ascent();// descent基线到文字底部的距离intdescentfm.descent();// 两行文字之间的推荐行间距intlineSpacingfm.lineSpacing();// 包围某个字符串的紧密矩形比 boundingRect 更精确QRect tightRectfm.boundingRect(Hello World);我们来做一个实际的应用场景在一个自定义 Widget 上画多行文字每行用不同的字体大小行间距精确控制。这就是 QFontMetrics 的经典用武之地voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setPen(Qt::black);inty20;// 起始 y 位置// 第一行大标题QFonttitleFont(Sans,24,QFont::Bold);painter.setFont(titleFont);QFontMetricstitleFm(titleFont);painter.drawText(10,ytitleFm.ascent(),文章标题);ytitleFm.lineSpacing()5;// 标题后多留 5px// 第二行副标题QFontsubFont(Sans,14);painter.setFont(subFont);QFontMetricssubFm(subFont);painter.setPen(Qt::darkGray);painter.drawText(10,ysubFm.ascent(),作者AwesomeQt | 2025-04-22);ysubFm.lineSpacing()10;// 正文前多留 10px// 第三行开始正文QFontbodyFont(Sans,11);painter.setFont(bodyFont);painter.setPen(Qt::black);QFontMetricsbodyFm(bodyFont);QStringList lines{这是正文的第一行。,这是正文的第二行。,这是正文的第三行。};for(constQStringline:lines){painter.drawText(10,ybodyFm.ascent(),line);ybodyFm.lineSpacing();}}这里的关键点是每一行文字的 y 坐标 当前基线位置 ascent这样才能保证文字的顶部在基线位置对齐。每行之后 y 递增lineSpacing()而不是height()因为lineSpacing()包含了 Qt 推荐的行间距视觉效果更好。还有一个你可能遇到的问题horizontalAdvance()在 Qt 5.11 之前叫width()。如果你在网上看到老代码用fm.width(text)在新版 Qt 里这个函数已经被标记为废弃了用horizontalAdvance()替代即可。3.4 富文本渲染QTextDocument 基础前面我们用的 drawText 只能画纯文本。如果你需要在一个区域内渲染带格式的文字——一部分加粗、另一部分变色、有的地方是链接——QTextDocument 就是专门干这个的。QTextDocument 是 Qt 富文本框架的核心类它内部维护一棵文档树支持 HTML 子集的标记语法。你可以用 HTML 格式的字符串来创建富文本内容然后用 QTextDocument 的 drawContents 方法画到 QPainter 上voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);QTextDocument doc;doc.setHtml(h1 stylecolor: #2C3E50;标题文字/h1p这是正文段落b这里加粗了/bi这里斜体了/ispan stylecolor: red;这里变红了/span。/pp stylecolor: gray; font-size: 10pt;这是灰色的小字注释。/p);// 设置文档的绘制宽度超出了会自动换行doc.setTextWidth(width()-20);// 把文档内容画到 QPainter 上从 (10, 10) 开始painter.translate(10,10);doc.drawContents(painter);}QTextDocument 支持的 HTML 标签包括h1到h6、p、b、i、u、s、font、span、a、br、ul、ol、li、table等。CSS 样式方面支持color、background-color、font-family、font-size、font-weight、font-style、text-align、margin、padding等常用属性。不过它不是完整的浏览器引擎别指望它支持 CSS3 动画或者 Flexbox 布局。QTextDocument 有一个很实用的功能你可以用size()获取文档的实际内容尺寸。这在做动态布局的时候非常有用——比如你需要知道富文本渲染出来之后到底有多高才能决定下方其他控件的位置QTextDocument doc;doc.setHtml(h1标题/h1p一段文字/p);doc.setTextWidth(400);// 先设定宽度// 获取文档的实际尺寸QSizeF docSizedoc.size();qDebug()文档高度:docSize.height();// 根据文档高度设置 Widget 的固定高度setFixedHeight(static_castint(docSize.height())20);还有一个细节你可能需要注意QTextDocument 默认会有一些页边距margin导致画出来的文字跟你预期的位置有偏移。你可以用setDocumentMargin()把它设成 0然后自己控制偏移量QTextDocument doc;doc.setDocumentMargin(0);// 去掉默认边距doc.setHtml(p无边距的文字/p);到这里你可以思考一个实际场景如果你要实现一个带格式化的通知气泡控件——标题加粗、正文普通、底部时间灰色小字——用 QTextDocument 应该怎么组织 HTML气泡的背景矩形大小又该怎么根据文字内容动态计算想清楚这个QFont、QFontMetrics、QTextDocument 三者如何配合你就全通了。4. 踩坑预防第一个坑就是前面反复强调的 drawText(x, y) 的 y 坐标问题。y 是基线位置不是顶部位置这个概念说起来简单但一到实际编码的时候特别容易忘。如果你用 x/y 版本的 drawText 发现文字显示不全或者位置偏了第一反应就应该是检查 y 是不是设成了文字矩形的顶部。最简单的验证方式改成 QRect 版本 Qt::AlignTop如果显示正常了就说明是基线坐标的问题。第二个坑是字体族名不存在的情况。你指定了 “Helvetica”但系统上没有这个字体常见于 LinuxQt 会默默替换成系统默认的 sans-serif 字体不会给你任何提示。如果你的界面在某个平台上字体看起来不一样可以用QFontInfo(font).family()来查看实际使用的字体族名QFontfont(Helvetica,14);QFontInfoinfo(font);qDebug()请求的字体:font.family()实际使用的字体:info.family();第三个坑是 QFontMetrics 的函数行为在高 DPI 缩放下可能跟你想的不一样。Qt6 默认开启了高 DPI 缩放fontMetrics.height()返回的是逻辑像素高度。如果你在 2x 的屏幕上一个 14pt 的字体height()可能返回 20但实际物理像素是 40。日常使用中你不需要手动处理这个缩放——QPainter 在画的时候会自动缩放——但如果你要把文字尺寸跟 QImage 的像素坐标混在一起算就要小心了。第四个坑是 QTextDocument 在 paintEvent 里重复创建和解析 HTML 的性能问题。每次 paintEvent 都 new 一个 QTextDocument、setHtml、drawContents这个开销在频繁重绘的场景下会非常明显。如果你的富文本内容不变应该把 QTextDocument 存成成员变量只在内容变化时重新 setHtmlpaintEvent 里只做 drawContents。5. 练习项目我们来做一个实战练习实现一个简易的富文本信息卡片控件。卡片中央显示一段格式化的内容——顶部是加粗的标题20pt、中间是正文段落12pt支持自动换行、底部右对齐显示日期和作者信息10pt 灰色。卡片的背景是带圆角的白色矩形外面加一圈浅灰色边框。卡片大小根据文字内容自适应宽度固定 400 像素高度随内容自动扩展。完成标准是继承 QWidget 实现自定义控件对外提供setTitle()、setBody()、setFooter()方法设置内容内部用 QTextDocument 渲染标题、正文、注释三部分用 QFontMetrics 计算各部分高度加上上下边距得到卡片总高度在 paintEvent 里先画圆角矩形背景再画文字内容提供setCardContent(title, body, footer)一次性设置所有内容并自动刷新。几个提示QTextDocument 的size().height()可以获取每段文字的实际渲染高度圆角矩形用drawRoundedRect()半径 8-12 像素比较好看背景填充用QColor(255, 255, 255)加浅灰边框QColor(220, 220, 220)是一种常见的信息卡片风格。6. 官方文档参考链接Qt 文档 · QFont Class – QFont 的完整 API字体族名、大小、粗细、斜体等属性设置Qt 文档 · QFontMetrics Class – QFontMetrics 的完整 API文字宽度、高度、基线相关的所有计算函数Qt 文档 · QTextDocument Class – QTextDocument 富文本渲染的完整文档HTML 子集支持和文档结构Qt 文档 · QPainter::drawText – drawText 所有重载版本的详细说明Qt 文档 · Rich Text Processing – Qt 富文本框架的总览文档Scribe 框架架构说明到这里Qt 文本渲染的几个核心工具你都已经上手了QFont 负责描述字体、QFontMetrics 负责计算尺寸、drawText 负责绘制、QTextDocument 负责富文本。记住一个原则——凡是需要精确布局文字的地方先用 QFontMetrics 量好尺寸再画别靠猜。下一篇文章我们进入 OpenGL 的世界用 QOpenGLWidget 把 GPU 渲染能力嵌入到 Qt 界面中。相关阅读现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 100%通用GUI编程技术——Win32 原生编程实战五十四——Hook 机制 - 相似度 100%通用GUI编程技术——图形渲染实战四十四——D3D12命令列表、队列与围栏GPU同步核心 - 相似度 100%
现代Qt开发教程(新手篇)2.4——QFont 与文本渲染基础
发布时间:2026/5/22 8:09:49
现代Qt开发教程新手篇2.4——QFont 与文本渲染基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt地址https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeQt/1. 前言 / 为什么文本渲染值得单独学很多初学者觉得文字显示有什么好学的给 QLabel 设个文本不就行了。说实话我一开始也是这么想的。直到有一天我需要在一个自定义的 Widget 上精确排列多行文字——要在指定位置画出标题、正文、注释每段的字体大小还不一样还要在文字之间留出精确的间距。那会儿我才意识到Qt 的文本渲染远不只是drawText()一行代码那么简单。QPainter 的drawText()确实能画文字但如果你需要知道一段文字画出来之后占多大空间、需要在哪里换行、多行文字的行间距怎么算——这些都需要 QFont 和 QFontMetrics 的配合。而如果你需要渲染带格式的富文本——加粗、斜体、不同颜色混排——那就得请出 QTextDocument 了。这篇文章我们一起来把 Qt 的文本渲染体系搞清楚QFont 怎么设置字体属性、drawText 的各种用法和坐标含义、QFontMetrics 怎么帮我们计算文字尺寸做精确布局、以及 QTextDocument 的富文本渲染基础。搞懂这些之后你在自定义控件里排列文字就跟玩积木一样顺手。2. 环境说明本篇代码适用于 Qt 6.5 版本示例基于 Qt 6.9.1 验证CMake 3.26C17 标准。示例代码依赖 QtGui 和 QtWidgets 模块——QFont 和 QFontMetrics 在 QtGui 中QTextDocument 也在 QtGui 中但我们的展示窗口需要 QWidget所以两个模块都要链接。关于字体示例中使用了一些通用字体名称如 “Arial”、“Sans”、“Monospace”Qt 会根据平台自动映射到可用的实际字体你不需要额外安装什么字体包。所有代码在 Linux、Windows、macOS 上都可以编译运行。3. 核心概念讲解3.1 QFont 构造与属性设置QFont 是 Qt 里描述字体的类。它本身不包含任何文字数据它只是告诉 Qt “我想用什么字体来显示文字”。创建 QFont 最直接的方式就是指定字体族名和大小// 基本构造族名 大小QFontfont1(Arial,12);// 更完整的构造族名 大小 加粗 斜体QFontfont2(Times New Roman,16,QFont::Bold,true);// 加粗 斜体字体族名是一个容易搞混的概念。你传入的 “Arial” 或者 “Times New Roman” 不一定在当前系统上存在——特别是在 Linux 上可能根本没有 Arial 这个字体。Qt 会按照一定的规则做字体替换先找你指定的族名找不到就找同类的替代字体比如 sans-serif 类型的字体再找不到就用系统默认字体。这个替换过程是自动的你不会收到任何警告所以如果发现文字显示出来的字体跟你想的不一样大概率就是系统上没有你指定的字体。如果你希望跨平台使用统一的字体外观可以用 Qt 提供的通用字体族名QFontsansFont(Sans,12);// 无衬线字体Linux 映射到 Sans SerifQFontserifFont(Serif,12);// 衬线字体QFontmonoFont(Monospace,12);// 等宽字体QFont 有很多属性可以调整日常开发中最常用的就是族名、大小、加粗、斜体这四个。其他如字间距、字母间距、下划线、删除线等属于精细调整用到的时候查文档就行QFontfont(Arial,14);font.setBold(true);// 加粗font.setItalic(true);// 斜体font.setUnderline(true);// 下划线font.setStrikeOut(true);// 删除线font.setPointSize(16);// 设置字号点大小font.setPixelSize(20);// 设置字号像素大小font.setLetterSpacing(QFont::AbsoluteSpacing,2);// 字母间距 2 像素font.setWordSpacing(5);// 词间距 5 像素这里有个值得注意的区别setPointSize()和setPixelSize()。点大小是跟 DPI 无关的绝对单位——一个 12pt 的字体在 1x 屏幕和 2x 屏幕上看起来物理大小一样Qt 会自动做 DPI 缩放。像素大小则是跟 DPI 相关的——一个 20px 的字体在 2x 屏幕上看起来只有 1x 屏幕上的一半大。日常开发建议用setPointSize()这样在不同 DPI 的屏幕上显示效果更一致。3.2 QPainter::drawText() 绘制文字与对齐有了 QFont 之后我们在 paintEvent 里就可以用 drawText 把文字画出来了。drawText 有好几个重载版本用法和坑都不一样我们一个一个来看。最简单的版本是drawText(x, y, text)这里的 x 是文字左边缘的位置y 是文字基线的位置。基线是什么就是英文字母 “a”、“b”、“c” 底部对齐的那条线字母 “g”、“p”、“y” 有部分会延伸到基线以下。注意 y 不是文字矩形的顶部这一点非常关键也是新手最容易踩的坑voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setFont(QFont(Arial,20));painter.setPen(Qt::black);// y 30 是基线位置不是顶部位置painter.drawText(10,30,Hello Qt!);// 如果 y 设成 0文字大部分会在 Widget 上方被裁剪掉painter.drawText(10,0,这段文字大部分看不见);}实际开发中更推荐使用 QRect 版本的 drawText配合对齐标志这样你不用操心基线的问题voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setFont(QFont(Arial,16));// 在整个 Widget 区域内居中显示painter.drawText(rect(),Qt::AlignCenter,居中文字);// 左上角对齐painter.drawText(rect(),Qt::AlignTop|Qt::AlignLeft,左上角);// 指定一个矩形区域右下角对齐QRectarea(50,50,300,100);painter.drawText(area,Qt::AlignBottom|Qt::AlignRight,右下角);// 自动换行需要加上 Qt::TextWordWrap 标志painter.drawText(QRect(10,200,200,200),Qt::TextWordWrap,这是一段比较长的文字当它超出矩形宽度时会自动换行显示。);}QRect 版本的 drawText 会自动把文字放在矩形内按对齐标志定位不需要你手动计算基线位置。而且加上Qt::TextWordWrap之后文字超出矩形宽度会自动换行非常方便。我个人的经验是除非你有非常精确的像素级定位需求否则永远用 QRect 版本而不是 x/y 版本。drawText 还有一个不太常用但很有用的返回值它返回一个 QRect表示文字实际占据的矩形区域。这在做后续布局的时候很有用QRect boundingRectpainter.drawText(100,100,300,50,Qt::AlignLeft|Qt::AlignVCenter,Hello World);// boundingRect 就是文字实际占据的区域可以用它来定位下一个元素intnextXboundingRect.right()10;// 文字右侧留 10px 间距3.3 QFontMetrics 计算文字尺寸当我们需要做精确的文字布局——比如把一段文字和另一个控件并排排列或者根据文字宽度来决定控件大小——就需要 QFontMetrics 出场了。QFontMetrics 能告诉你给定字体下一段文字的精确像素尺寸。获取 QFontMetrics 最简单的方式是从 QPainter 或者 QFont 直接拿// 方式 1从当前 painter 的字体获取QPainterpainter(this);painter.setFont(QFont(Arial,14));QFontMetrics fmpainter.fontMetrics();// 方式 2直接从 QFont 构造QFontfont(Arial,14);QFontMetricsfm(font);QFontMetrics 最常用的几个函数QFontMetricsfm(QFont(Arial,14));// 单个字符的宽度intcharWidthfm.horizontalAdvance(A);// 字符串的总宽度inttextWidthfm.horizontalAdvance(Hello World);// 字体的高度 ascent descentintfontHeightfm.height();// ascent基线到文字顶部的距离intascentfm.ascent();// descent基线到文字底部的距离intdescentfm.descent();// 两行文字之间的推荐行间距intlineSpacingfm.lineSpacing();// 包围某个字符串的紧密矩形比 boundingRect 更精确QRect tightRectfm.boundingRect(Hello World);我们来做一个实际的应用场景在一个自定义 Widget 上画多行文字每行用不同的字体大小行间距精确控制。这就是 QFontMetrics 的经典用武之地voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setPen(Qt::black);inty20;// 起始 y 位置// 第一行大标题QFonttitleFont(Sans,24,QFont::Bold);painter.setFont(titleFont);QFontMetricstitleFm(titleFont);painter.drawText(10,ytitleFm.ascent(),文章标题);ytitleFm.lineSpacing()5;// 标题后多留 5px// 第二行副标题QFontsubFont(Sans,14);painter.setFont(subFont);QFontMetricssubFm(subFont);painter.setPen(Qt::darkGray);painter.drawText(10,ysubFm.ascent(),作者AwesomeQt | 2025-04-22);ysubFm.lineSpacing()10;// 正文前多留 10px// 第三行开始正文QFontbodyFont(Sans,11);painter.setFont(bodyFont);painter.setPen(Qt::black);QFontMetricsbodyFm(bodyFont);QStringList lines{这是正文的第一行。,这是正文的第二行。,这是正文的第三行。};for(constQStringline:lines){painter.drawText(10,ybodyFm.ascent(),line);ybodyFm.lineSpacing();}}这里的关键点是每一行文字的 y 坐标 当前基线位置 ascent这样才能保证文字的顶部在基线位置对齐。每行之后 y 递增lineSpacing()而不是height()因为lineSpacing()包含了 Qt 推荐的行间距视觉效果更好。还有一个你可能遇到的问题horizontalAdvance()在 Qt 5.11 之前叫width()。如果你在网上看到老代码用fm.width(text)在新版 Qt 里这个函数已经被标记为废弃了用horizontalAdvance()替代即可。3.4 富文本渲染QTextDocument 基础前面我们用的 drawText 只能画纯文本。如果你需要在一个区域内渲染带格式的文字——一部分加粗、另一部分变色、有的地方是链接——QTextDocument 就是专门干这个的。QTextDocument 是 Qt 富文本框架的核心类它内部维护一棵文档树支持 HTML 子集的标记语法。你可以用 HTML 格式的字符串来创建富文本内容然后用 QTextDocument 的 drawContents 方法画到 QPainter 上voidpaintEvent(QPaintEvent*)override{QPainterpainter(this);painter.setRenderHint(QPainter::Antialiasing);QTextDocument doc;doc.setHtml(h1 stylecolor: #2C3E50;标题文字/h1p这是正文段落b这里加粗了/bi这里斜体了/ispan stylecolor: red;这里变红了/span。/pp stylecolor: gray; font-size: 10pt;这是灰色的小字注释。/p);// 设置文档的绘制宽度超出了会自动换行doc.setTextWidth(width()-20);// 把文档内容画到 QPainter 上从 (10, 10) 开始painter.translate(10,10);doc.drawContents(painter);}QTextDocument 支持的 HTML 标签包括h1到h6、p、b、i、u、s、font、span、a、br、ul、ol、li、table等。CSS 样式方面支持color、background-color、font-family、font-size、font-weight、font-style、text-align、margin、padding等常用属性。不过它不是完整的浏览器引擎别指望它支持 CSS3 动画或者 Flexbox 布局。QTextDocument 有一个很实用的功能你可以用size()获取文档的实际内容尺寸。这在做动态布局的时候非常有用——比如你需要知道富文本渲染出来之后到底有多高才能决定下方其他控件的位置QTextDocument doc;doc.setHtml(h1标题/h1p一段文字/p);doc.setTextWidth(400);// 先设定宽度// 获取文档的实际尺寸QSizeF docSizedoc.size();qDebug()文档高度:docSize.height();// 根据文档高度设置 Widget 的固定高度setFixedHeight(static_castint(docSize.height())20);还有一个细节你可能需要注意QTextDocument 默认会有一些页边距margin导致画出来的文字跟你预期的位置有偏移。你可以用setDocumentMargin()把它设成 0然后自己控制偏移量QTextDocument doc;doc.setDocumentMargin(0);// 去掉默认边距doc.setHtml(p无边距的文字/p);到这里你可以思考一个实际场景如果你要实现一个带格式化的通知气泡控件——标题加粗、正文普通、底部时间灰色小字——用 QTextDocument 应该怎么组织 HTML气泡的背景矩形大小又该怎么根据文字内容动态计算想清楚这个QFont、QFontMetrics、QTextDocument 三者如何配合你就全通了。4. 踩坑预防第一个坑就是前面反复强调的 drawText(x, y) 的 y 坐标问题。y 是基线位置不是顶部位置这个概念说起来简单但一到实际编码的时候特别容易忘。如果你用 x/y 版本的 drawText 发现文字显示不全或者位置偏了第一反应就应该是检查 y 是不是设成了文字矩形的顶部。最简单的验证方式改成 QRect 版本 Qt::AlignTop如果显示正常了就说明是基线坐标的问题。第二个坑是字体族名不存在的情况。你指定了 “Helvetica”但系统上没有这个字体常见于 LinuxQt 会默默替换成系统默认的 sans-serif 字体不会给你任何提示。如果你的界面在某个平台上字体看起来不一样可以用QFontInfo(font).family()来查看实际使用的字体族名QFontfont(Helvetica,14);QFontInfoinfo(font);qDebug()请求的字体:font.family()实际使用的字体:info.family();第三个坑是 QFontMetrics 的函数行为在高 DPI 缩放下可能跟你想的不一样。Qt6 默认开启了高 DPI 缩放fontMetrics.height()返回的是逻辑像素高度。如果你在 2x 的屏幕上一个 14pt 的字体height()可能返回 20但实际物理像素是 40。日常使用中你不需要手动处理这个缩放——QPainter 在画的时候会自动缩放——但如果你要把文字尺寸跟 QImage 的像素坐标混在一起算就要小心了。第四个坑是 QTextDocument 在 paintEvent 里重复创建和解析 HTML 的性能问题。每次 paintEvent 都 new 一个 QTextDocument、setHtml、drawContents这个开销在频繁重绘的场景下会非常明显。如果你的富文本内容不变应该把 QTextDocument 存成成员变量只在内容变化时重新 setHtmlpaintEvent 里只做 drawContents。5. 练习项目我们来做一个实战练习实现一个简易的富文本信息卡片控件。卡片中央显示一段格式化的内容——顶部是加粗的标题20pt、中间是正文段落12pt支持自动换行、底部右对齐显示日期和作者信息10pt 灰色。卡片的背景是带圆角的白色矩形外面加一圈浅灰色边框。卡片大小根据文字内容自适应宽度固定 400 像素高度随内容自动扩展。完成标准是继承 QWidget 实现自定义控件对外提供setTitle()、setBody()、setFooter()方法设置内容内部用 QTextDocument 渲染标题、正文、注释三部分用 QFontMetrics 计算各部分高度加上上下边距得到卡片总高度在 paintEvent 里先画圆角矩形背景再画文字内容提供setCardContent(title, body, footer)一次性设置所有内容并自动刷新。几个提示QTextDocument 的size().height()可以获取每段文字的实际渲染高度圆角矩形用drawRoundedRect()半径 8-12 像素比较好看背景填充用QColor(255, 255, 255)加浅灰边框QColor(220, 220, 220)是一种常见的信息卡片风格。6. 官方文档参考链接Qt 文档 · QFont Class – QFont 的完整 API字体族名、大小、粗细、斜体等属性设置Qt 文档 · QFontMetrics Class – QFontMetrics 的完整 API文字宽度、高度、基线相关的所有计算函数Qt 文档 · QTextDocument Class – QTextDocument 富文本渲染的完整文档HTML 子集支持和文档结构Qt 文档 · QPainter::drawText – drawText 所有重载版本的详细说明Qt 文档 · Rich Text Processing – Qt 富文本框架的总览文档Scribe 框架架构说明到这里Qt 文本渲染的几个核心工具你都已经上手了QFont 负责描述字体、QFontMetrics 负责计算尺寸、drawText 负责绘制、QTextDocument 负责富文本。记住一个原则——凡是需要精确布局文字的地方先用 QFontMetrics 量好尺寸再画别靠猜。下一篇文章我们进入 OpenGL 的世界用 QOpenGLWidget 把 GPU 渲染能力嵌入到 Qt 界面中。相关阅读现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 100%通用GUI编程技术——Win32 原生编程实战五十四——Hook 机制 - 相似度 100%通用GUI编程技术——图形渲染实战四十四——D3D12命令列表、队列与围栏GPU同步核心 - 相似度 100%