QTextLayout 的封装与功能测试

一、问题背景

QFontMetrics类可计算字符串的宽度和高度,但它只适用于字符串单行显示的情形,对于多行显示的字符串,就显得力不从心了。

QTextLayout很好的支持字符串换行的情景,当然了,它还集成了QPainter,能有效地处理渲染的问题。

【注】测试Demo程序在第五节

二、定义:

QTextLayout 是 Qt 中一个核心且强大的底层类,用于处理单段文本的布局、换行和渲染。它位于文本处理栈的较低层(在 QTextDocument 和 QPainter 之间),为需要精细控制文本外观或处理复杂文本特性(如双向文本、复杂脚本)的场景提供了基础。

三、核心功能:

  1. 文本存储与属性: 存储要布局的文本字符串。

  2. 字体与格式: 应用字体、画笔、笔刷等格式属性(通过 QTextOption 或直接设置)。

  3. 布局计算:

    • 换行: 根据给定的宽度、对齐方式和 QTextOption 中的换行模式(WrapMode)自动将文本分割成多行(QTextLine)。

    • 对齐: 对行内文本进行左对齐、右对齐、居中对齐或两端对齐。

    • 基线对齐: 管理文本行的基线位置。

    • 双向文本处理: 正确处理包含从左向右(LTR)和从右向左(RTL)文本混合的段落(例如:阿拉伯语、希伯来语与英语混合)。

    • 制表符处理: 根据 QTextOption 中设置的制表位位置或默认行为处理制表符 (\t)。

    • 边界计算: 计算文本段落的自然大小(未换行时)、布局后的矩形边界、每一行的位置和尺寸。

  4. 字形访问: 提供访问布局后产生的实际字形(glyphRun())及其位置信息的能力,这对于自定义渲染(如文本路径效果、文本选择高亮、文本分析)至关重要。

  5. 缓存: 布局计算相对昂贵。QTextLayout 可以缓存布局结果,当文本或宽度未改变时,后续绘制可以复用缓存,提高性能。

  6. 与 QPainter 集成: 提供 draw() 方法直接将布局好的文本绘制到 QPainter 设备上。

四、主要组件和概念:

  1. QTextOption

    • 封装了影响段落布局的选项。

    • 关键属性:

      • alignment:文本对齐方式 (Qt::AlignLeftQt::AlignRightQt::AlignCenterQt::AlignJustify)。

      • wrapMode:换行模式 (QTextOption::NoWrapQTextOption::WordWrapQTextOption::WrapAnywhereQTextOption::WrapAtWordBoundaryOrAnywhere)。

      • tabs:制表位列表 (QList<QTextOption::Tab>),定义制表符 \t 的对齐位置和类型。

      • flags:其他标志,如 IncludeTrailingSpaces (影响两端对齐时是否考虑行尾空格)。

      • textDirection:段落的默认书写方向(Qt::LeftToRightQt::RightToLeft),通常由文本内容自动确定。

    • 在创建 QTextLayout 时传入,或之后通过 setTextOption() 设置。

  2. 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>)。

  3. 字形 (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)会有惊喜,效果如下图。第四个显示不全是因为组件宽度不够,而且字母、数字等字符中,如果字符串是连续的,中间没有空格,就不会自动换行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宏笋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值