You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写基础自定义QAbstractTextDocumentLayout子类并修复文本不显示问题?

问题描述

我正在做一个简易记事本风格的Qt应用,因为想要完全控制文本渲染,写了个自定义的QAbstractTextDocumentLayout类。代码能编译运行,但不管输入啥,文本区域都是空白的。draw()里的调试语句能正确输出输入的文本,说明输入已经传到draw()里了,但就是没渲染出来。

我想尽量少改代码,至少能显示部分文本就行,哪怕功能不全。另外,我找不到自定义QAbstractTextDocumentLayout的相关文档,要是有参考资料麻烦告知。


原代码

main.cpp

#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QTextEdit>
#include <QKeyEvent>

#include "./DocumentLayout.cpp"

class MainContent : public QWidget
{
    Q_OBJECT

    QTextEdit textEdit;
    DocumentLayout layout;
public:
    MainContent() : textEdit(this), layout(textEdit.document())
    {
        int innerPadding = 20;
        textEdit.document()->setDocumentMargin(innerPadding);

        int textColor = 0xD8DEE9;
        int backgroundColor = 0x2E3440;
        int selectionColor = 0x2E3440;
        int selectionBackgroundColor = 0x81A1C1;

        QPalette palette = textEdit.palette();
        palette.setColor(QPalette::Base, QColor(backgroundColor));
        palette.setColor(QPalette::Text, QColor(textColor));
        palette.setColor(QPalette::Highlight, QColor(selectionBackgroundColor));
        palette.setColor(QPalette::HighlightedText, QColor(selectionColor));
        textEdit.setPalette(palette);

        textEdit.setFrameStyle(QFrame::NoFrame);

        textEdit.document()->setDocumentLayout(&layout);
    };

    void resizeEvent(QResizeEvent *event)
    {
        textEdit.setGeometry(0, 0, this->width(), this->height());
    }
    
    void keyPressEvent(QKeyEvent *event)
    {
        if (event->key() == Qt::Key_W && event->modifiers() == Qt::ControlModifier)
        {
            qApp->quit();
        }
    }
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QMainWindow window;
    window.setWindowTitle("Hello, world!");
    window.resize(400, 500);

    MainContent content;
    window.setCentralWidget(&content);

    window.show();
    return app.exec();
}

#include "main.moc"

DocumentLayout.cpp

#include <QAbstractTextDocumentLayout>
#include <QPainter>
#include <QTextBlock>
#include <QDebug>

class DocumentLayout : public QAbstractTextDocumentLayout
{
public:
    DocumentLayout(QTextDocument *document) : QAbstractTextDocumentLayout(document)
    {
    }

    void draw(QPainter *painter, const PaintContext &context)
    {
        QTextDocument *document = this->document();
        QTextBlock block = document->begin();
        QPointF position(20, 20);

        painter->setPen(context.palette.color(QPalette::Text));
        QTextLayout *layout = block.layout();
        layout->draw(painter, position);
        position.ry() += layout->boundingRect().height();
        qDebug() << "drew" << block.text() << "at" << position;
    }

    int hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const
    {
        return 0;
    }

    int pageCount() const
    {
        return 1;
    }

    QSizeF documentSize() const
    {
        return QSizeF(400, 800);
    }

    QRectF frameBoundingRect(QTextFrame *frame) const
    {
        return QRectF(0, 0, 400, 800);
    }

    QRectF blockBoundingRect(const QTextBlock &block) const
    {
        return QRectF(0, 0, 400, 800);
    }

    void documentChanged(int from, int charsRemoved, int charsAdded)
    {
    }
};

解决方法

你的自定义布局有几个核心问题导致文本无法渲染:

  • 未初始化QTextLayout:直接调用block.layout()得到的是未配置的空布局,没有文本、字体、换行规则,自然画不出内容。
  • 只处理第一个文本块:输入多行时,后续的文本块完全被忽略。
  • 边界计算错误:blockBoundingRect和documentSize返回固定大矩形,导致文档无法正确触发重绘和滚动逻辑。

以下是最小修改后的DocumentLayout.cpp,能实现基础文本显示:

#include <QAbstractTextDocumentLayout>
#include <QPainter>
#include <QTextBlock>
#include <QDebug>
#include <QFontMetrics>

class DocumentLayout : public QAbstractTextDocumentLayout
{
public:
    DocumentLayout(QTextDocument *document) : QAbstractTextDocumentLayout(document)
    {
    }

    void draw(QPainter *painter, const PaintContext &context)
    {
        QTextDocument *document = this->document();
        QTextBlock block = document->begin();
        // 用文档边距确定起始位置,避免硬编码
        QPointF position(document->documentMargin(), document->documentMargin());

        painter->setPen(context.palette.color(QPalette::Text));
        
        // 遍历所有有效文本块
        while (block.isValid()) {
            if (!block.layout()) {
                // 初始化当前块的文本布局
                QTextLayout *layout = new QTextLayout(block.text(), block.charFormat().font());
                layout->setTextOption(document->defaultTextOption());
                // 设置换行宽度为文档可用宽度(减去左右边距)
                qreal lineWidth = document->pageSize().width() - 2 * document->documentMargin();
                layout->beginLayout();
                while (true) {
                    QTextLine line = layout->createLine();
                    if (!line.isValid()) break;
                    line.setLineWidth(lineWidth);
                }
                layout->endLayout();
                block.setLayout(layout);
            }

            QTextLayout *layout = block.layout();
            // 绘制当前文本块
            layout->draw(painter, position);
            // 计算下一个块的Y坐标(加上行间距)
            position.ry() += layout->boundingRect().height() + document->lineHeight();
            block = block.next();
        }
    }

    int hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const
    {
        return 0;
    }

    int pageCount() const
    {
        return 1;
    }

    QSizeF documentSize() const
    {
        // 动态计算文档总高度
        int blockCount = document()->blockCount();
        QFontMetrics fm(document()->defaultFont());
        qreal totalHeight = blockCount * (fm.height() + document()->lineHeight()) + 2 * document()->documentMargin();
        return QSizeF(document()->pageSize().width(), totalHeight);
    }

    QRectF frameBoundingRect(QTextFrame *frame) const
    {
        return QRectF(0, 0, document()->pageSize().width(), documentSize().height());
    }

    QRectF blockBoundingRect(const QTextBlock &block) const
    {
        if (!block.layout()) {
            // 临时计算未初始化块的边界
            QFontMetrics fm(block.charFormat().font());
            qreal width = document()->pageSize().width() - 2 * document()->documentMargin();
            qreal height = fm.height() + document()->lineHeight();
            return QRectF(document()->documentMargin(), 0, width, height);
        }
        // 返回当前块的实际位置和大小
        qreal yPos = block.blockNumber() * (block.layout()->boundingRect().height() + document()->lineHeight()) + document()->documentMargin();
        return block.layout()->boundingRect().translated(document()->documentMargin(), yPos);
    }

    void documentChanged(int from, int charsRemoved, int charsAdded)
    {
        // 文档内容变化时触发重绘
        update();
    }
};

关键修改点

  1. 初始化QTextLayout:为每个文本块创建并配置布局,设置字体、换行宽度,确保文本能正确排版。
  2. 遍历所有文本块:不再只处理第一个块,支持多行文本显示。
  3. 动态计算文档尺寸:根据文本块数量和行高计算文档总高度,确保滚动和重绘逻辑正常。
  4. 触发重绘:在documentChanged中调用update(),内容变化时自动刷新界面。

参考资料

Qt官方文档中QAbstractTextDocumentLayout的类参考是基础,但自定义实现的示例极少。最权威的参考是Qt源码中的默认布局实现:

  • QPlainTextDocumentLayout:纯文本布局,逻辑相对简单,适合入门学习自定义布局的核心流程。
  • QTextDocumentLayout:富文本布局的完整实现,虽然复杂,但能学习到完整的文本排版、样式处理逻辑。

内容的提问来源于stack exchange,提问作者Jack M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 10:30:57