一、问题背景
QFontMetrics类可计算字符串的宽度和高度,但它只适用于字符串单行显示的情形,对于多行显示的字符串,就显得力不从心了。
QTextLayout很好的支持字符串换行的情景,当然了,它还集成了QPainter,能有效地处理渲染的问题。
【注】测试Demo程序在第五节
二、定义:
QTextLayout
是 Qt 中一个核心且强大的底层类,用于处理单段文本的布局、换行和渲染。它位于文本处理栈的较低层(在 QTextDocument
和 QPainter
之间),为需要精细控制文本外观或处理复杂文本特性(如双向文本、复杂脚本)的场景提供了基础。
三、核心功能:
-
文本存储与属性: 存储要布局的文本字符串。
-
字体与格式: 应用字体、画笔、笔刷等格式属性(通过
QTextOption
或直接设置)。 -
布局计算:
-
换行: 根据给定的宽度、对齐方式和
QTextOption
中的换行模式(WrapMode
)自动将文本分割成多行(QTextLine
)。 -
对齐: 对行内文本进行左对齐、右对齐、居中对齐或两端对齐。
-
基线对齐: 管理文本行的基线位置。
-
双向文本处理: 正确处理包含从左向右(LTR)和从右向左(RTL)文本混合的段落(例如:阿拉伯语、希伯来语与英语混合)。
-
制表符处理: 根据
QTextOption
中设置的制表位位置或默认行为处理制表符 (\t
)。 -
边界计算: 计算文本段落的自然大小(未换行时)、布局后的矩形边界、每一行的位置和尺寸。
-
-
字形访问: 提供访问布局后产生的实际字形(
glyphRun()
)及其位置信息的能力,这对于自定义渲染(如文本路径效果、文本选择高亮、文本分析)至关重要。 -
缓存: 布局计算相对昂贵。
QTextLayout
可以缓存布局结果,当文本或宽度未改变时,后续绘制可以复用缓存,提高性能。 -
与
QPainter
集成: 提供draw()
方法直接将布局好的文本绘制到QPainter
设备上。
四、主要组件和概念:
-
QTextOption
:-
封装了影响段落布局的选项。
-
关键属性:
-
alignment
:文本对齐方式 (Qt::AlignLeft
,Qt::AlignRight
,Qt::AlignCenter
,Qt::AlignJustify
)。 -
wrapMode
:换行模式 (QTextOption::NoWrap
,QTextOption::WordWrap
,QTextOption::WrapAnywhere
,QTextOption::WrapAtWordBoundaryOrAnywhere
)。 -
tabs
:制表位列表 (QList<QTextOption::Tab>
),定义制表符\t
的对齐位置和类型。 -
flags
:其他标志,如IncludeTrailingSpaces
(影响两端对齐时是否考虑行尾空格)。 -
textDirection
:段落的默认书写方向(Qt::LeftToRight
,Qt::RightToLeft
),通常由文本内容自动确定。
-
-
在创建
QTextLayout
时传入,或之后通过setTextOption()
设置。
-
-
QTextLine
:-
表示布局后文本段中的一行。
-
关键功能:
-
setLineWidth(width)
/naturalTextWidth()
:设置行宽(用于换行)或获取该行文本的自然宽度(未压缩/拉伸时)。 -
position()
:获取该行相对于整个QTextLayout
原点的位置 (QPointF
)。 -
rect()
:获取该行的边界矩形 (QRectF
)。 -
textStart()
/textLength()
:获取该行文本在原始字符串中的起始索引和长度。 -
cursorToX(index, edge = CursorBetweenCharacters)
:将文本索引转换为该行内的 x 坐标。 -
xToCursor(xpos, cursorPosition = CursorBetweenCharacters)
:将该行内的 x 坐标转换为最接近的文本索引。 -
horizontalAdvance()
:该行文本占据的水平宽度。 -
ascent()
/descent()
/height()
/leading()
:获取字体度量信息。 -
glyphRuns(from = -1, length = -1)
:获取该行(或部分)的字形运行 (QList<QGlyphRun>
)。
-
-
-
字形 (
QGlyphRun
):-
表示一个具有相同字体和连续位置的字形序列。这是
QTextLayout
输出的最底层单位。 -
关键信息:
-
字形索引 (
glyphIndexes()
):字体引擎中标识特定字符形状的整数。 -
位置 (
positions()
):每个字形基线的位置 (QPointF
列表)。 -
使用的字体 (
font()
)。 -
原始文本索引 (
stringIndexes()
):字形对应的原始字符串中的字符索引(在复杂脚本中,一个字形可能对应多个字符,一个字符也可能对应多个字形)。
-
-
访问字形是进行高级文本渲染(如自定义着色、特效、精确命中测试)的基础。
-
五、功能测试代码
使用Qt Creator(博主用的是Qt5.14)创建一个对话框(去掉Generate from)
1、自定义组件TextLayoutWgt
TextLayoutWgt.h
#ifndef TEXTLAYOUTWIDGET_H
#define TEXTLAYOUTWIDGET_H
#include <qwidget.h>
#include <qtextlayout.h>
class TextLayoutWgt : public QWidget
{
public:
explicit TextLayoutWgt(QWidget *parent = nullptr);
explicit TextLayoutWgt(const QString &text, QWidget *parent = nullptr);
~TextLayoutWgt();
void setText(const QString &text);
void setMargins(int left, int top, int right, int bottom);
void setBorder(qreal border);
void setFont(const QFont &fnt);
void setAlignment(Qt::Alignment align);
void setWrapMode(QTextOption::WrapMode eMode);
void setTextVerSpacing(int val);
void setBackground(const QColor &color);
void setBorderColor(const QColor &color);
void setTextColor(const QColor &color);
// 文本总高度
qreal textTotalHeight();
// 计算出的控件总高度
qreal totalHeight();
private:
void initData();
protected:
void paintEvent(QPaintEvent *) override;
private:
QMargins m_margin; // 文本与边框的间距
qreal m_textVerSpacing; // 换行文本之间的间距
qreal m_border; // 边框粗细
QColor m_background; // 背景色
QColor m_borderColor; // 边框色
QColor m_textColor; // 文本颜色
QTextOption m_textOpt;
QTextLayout m_textLay;
};
#endif // TEXTLAYOUTWIDGET_H
TextLayoutWgt.cpp
#include "TextLayoutWgt.h"
#include <qpainter.h>
#include <qalgorithms.h>
const qreal kDoublePrecise = 0.00000001;
TextLayoutWgt::TextLayoutWgt(QWidget *parent)
: QWidget(parent)
{
initData();
}
TextLayoutWgt::TextLayoutWgt(const QString &text, QWidget *parent)
: QWidget(parent)
{
initData();
setText(text);
}
void TextLayoutWgt::initData()
{
setBorder(1);
setTextVerSpacing(5);
setMargins(1, 1, 1, 1);
m_background.setRgb(240, 245, 250);
m_borderColor = Qt::gray;
m_textColor = Qt::darkBlue;
setAlignment(Qt::AlignLeft |Qt::AlignTop);
setWrapMode(QTextOption::WrapAnywhere); // 允许任意位置换行
QFont fnt;
fnt.setFamily("宋体");
fnt.setWeight(QFont::Normal);
fnt.setPixelSize(15);
setFont(fnt);
}
TextLayoutWgt::~TextLayoutWgt()
{
}
void TextLayoutWgt::setText(const QString &text)
{
m_textLay.setText(text);
}
void TextLayoutWgt::setMargins(int left, int top, int right, int bottom)
{
m_margin.setLeft(left);
m_margin.setTop(top);
m_margin.setRight(right);
m_margin.setBottom(bottom);
}
void TextLayoutWgt::setBorder(qreal border)
{
m_border = border;
}
void TextLayoutWgt::setFont(const QFont &fnt)
{
m_textLay.setFont(fnt);
}
void TextLayoutWgt::setAlignment(Qt::Alignment align)
{
m_textOpt.setAlignment(align);
m_textLay.setTextOption(m_textOpt);
}
void TextLayoutWgt::setWrapMode(QTextOption::WrapMode eMode)
{
m_textOpt.setWrapMode(eMode);
m_textLay.setTextOption(m_textOpt);
}
void TextLayoutWgt::setTextVerSpacing(int val)
{
if (val >= 0) {
m_textVerSpacing = val;
}
}
void TextLayoutWgt::setBackground(const QColor &color)
{
if (color.isValid()) {
m_background = color;
}
}
void TextLayoutWgt::setBorderColor(const QColor &color)
{
if (color.isValid()) {
m_borderColor = color;
}
}
void TextLayoutWgt::setTextColor(const QColor &color)
{
if (color.isValid()) {
m_textColor = color;
}
}
// 文本总高度
qreal TextLayoutWgt::textTotalHeight()
{
qreal retVal = 0;
m_textLay.beginLayout();
while (true) {
// 创建新行
QTextLine line = m_textLay.createLine();
if (!line.isValid())
break;
// 设置行宽与位置 (触发换行计算)
line.setLineWidth(this->width() - m_margin.left() - m_margin.right());
line.setPosition(QPointF(m_margin.left(), retVal));
if (line.lineNumber() > 0) {
// 如果有多行,要加上文本之间的行间距
retVal += line.height() + m_textVerSpacing; //line.leading()
} else {
retVal += line.height();
}
} // end while
m_textLay.endLayout();
return retVal;
}
// 计算出的控件总高度
qreal TextLayoutWgt::totalHeight()
{
return textTotalHeight() + m_margin.top() + m_margin.bottom();
}
void TextLayoutWgt::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), m_background);
// 设置边距
QRect contentRect = rect().adjusted(
m_margin.left(), m_margin.top(), -m_margin.right(), -m_margin.bottom());
// 开始布局计算
m_textLay.beginLayout();
qreal y = contentRect.top();
while (true) {
QTextLine line = m_textLay.createLine();
if (!line.isValid())
break;
// 设置行宽并定位
line.setLineWidth(contentRect.width());
line.setPosition(QPointF(contentRect.left(), y));
y += line.height() + m_textVerSpacing; //line.leading()
}
m_textLay.endLayout();
// 绘制文本
painter.setPen(m_textColor);
m_textLay.draw(&painter, QPoint(0, 0));
// 绘制边界
if (qAbs(m_border - kDoublePrecise) > 0) {
painter.setPen(QPen(m_borderColor, m_border, Qt::DashLine));
painter.drawRect(rect());
}
}
2、测试对话框
dialog.h
#ifndef DIALOG_H
#define DIALOG_H
#include <QDialog>
#include <qpushbutton.h>
#include "TextLayoutWgt.h"
const int kTextCnt = 5;
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog(QWidget *parent = nullptr);
~Dialog();
private:
void initUi();
void initLayout();
void buildConnection();
private slots:
void onTestButtonClicked();
private:
QButtonGroup *m_btnGroup = nullptr;
TextLayoutWgt *m_textWgt[kTextCnt] = {nullptr};
QPushButton *m_btnTest = nullptr;
};
#endif // DIALOG_H
dialog.cpp
#include "dialog.h"
#include <qboxlayout.h>
#include <qdebug.h>
Dialog::Dialog(QWidget *parent)
: QDialog(parent)
{
initUi();
initLayout();
buildConnection();
}
Dialog::~Dialog()
{
}
void Dialog::initUi()
{
setWindowTitle(QString("QTextLayout测试"));
setFixedSize(360, 500);
for (int i = 0; i < kTextCnt; ++i) {
m_textWgt[i] = new TextLayoutWgt(this);
m_textWgt[i]->setFixedWidth(300);
}
m_btnTest = new QPushButton(this);
m_btnTest->setFixedSize(80, 32);
m_btnTest->setText(QString("测试"));
}
void Dialog::initLayout()
{
QHBoxLayout *btnLay = new QHBoxLayout();
btnLay->setMargin(0);
btnLay->setSpacing(36);
btnLay->setAlignment(Qt::AlignCenter);
QVBoxLayout *mainLay = new QVBoxLayout(this);
mainLay->setMargin(0);
mainLay->setSpacing(20);
mainLay->setAlignment(Qt::AlignCenter);
for (int i = 0; i < kTextCnt; ++i) {
mainLay->addWidget(m_textWgt[i]);
}
mainLay->addWidget(m_btnTest, 0, Qt::AlignLeft);
}
void Dialog::buildConnection()
{
connect(m_btnTest, &QPushButton::clicked, this, &Dialog::onTestButtonClicked);
}
void Dialog::onTestButtonClicked()
{
m_textWgt[0]->setText("《枫桥夜泊》\t唐·张继\t月落乌啼霜满天,江枫渔火对愁眠。姑苏城外寒山寺,夜半钟声到客船。");
m_textWgt[0]->setFixedHeight(m_textWgt[0]->totalHeight());
m_textWgt[1]->setText("《枫桥夜泊》\n唐·张继\n月落乌啼霜满天,\n江枫渔火对愁眠。\n姑苏城外寒山寺,\n夜半钟声到客船。");
m_textWgt[1]->setFixedHeight(m_textWgt[1]->totalHeight());
m_textWgt[2]->setText("QTextLine::leading()的核心作用是提供文本行之间的垂直呼吸空间,它是专业排版中控制行距的关键指标");
m_textWgt[2]->setFixedHeight(m_textWgt[2]->totalHeight());
m_textWgt[3]->setText("wrapMode:换行模式(QTextOption::NoWrap, QTextOption::WordWrap, QTextOption::WrapAnywhere, QTextOption::WrapAtWordBoundaryOrAnywhere)");
m_textWgt[3]->setFixedHeight(m_textWgt[3]->totalHeight());
m_textWgt[4]->setText("wrapMode:换行模式(QTextOption::NoWrap, QTextOption::WordWrap, QTextOption::WrapAnywhere, QTextOption::WrapAtWord BoundaryOrAnywhere)");
m_textWgt[4]->setFixedHeight(m_textWgt[4]->totalHeight());
}
点击【测试】按钮后,界面如下:
总结:
1、可以看到,QTextLayout可以精确地计算出多行文本内容的高度,文本可以完美的填充在组件内;
2、QTextLayout能够支持字符串中的制表符(\t),但是不支持字符串中的换行符(\n),因为QTextLayout主要用于单段文本的布局,如果在传递给`QTextLayout`的字符串中包含换行符(`\n`),那么这些换行符会被视为普通空白字符,并不会导致换行。想实现换行效果,可将文本按`\n`分割成多个段落,然后为每个段落创建一个`QTextLayout`,分别进行布局和绘制,或者使用更高级的类QTextDocument,它能够处理多段落文本;
3、将 setWrapMode(QTextOption::WrapAnywhere) 改成setWrapMode(QTextOption::WordWrap)会有惊喜,效果如下图。第四个显示不全是因为组件宽度不够,而且字母、数字等字符中,如果字符串是连续的,中间没有空格,就不会自动换行。