在Qt5中绘制大量独立字符的最佳方法?

A best way to draw a lot of independent characters in Qt5?

我正在编写一个显示大量文本的应用程序。它不是单词和句子,而是以 CP437 字符集显示的二进制数据。当前形式:

我在绘制这些字符时遇到了问题。我需要一个一个地绘制每个字符,因为稍后我想应用不同的颜色。这些字符也应该有透明背景,因为稍后我想在背景中绘制不同颜色的部分和范围(根据某些标准对这些字符进行分组)。

应用程序支持同时打开多个文件,但是当打开多个文件时,在fast i7上绘图开始明显,所以可能是写得不好。

在 Qt5 中绘制此类数据的最佳方法是什么?我应该只将字符预渲染到位图并从那里开始,还是实际上可以通过使用普通的 Qt 函数绘制文本来绘制大量字符?

编辑:我正在使用一个正常的 QFrame 小部件,它使用 QPainterpaintEvent 中绘图。这是一种错误的方法吗?我已经阅读了一些关于 QGraphicsScene 的文档,从中我记得它最适用于小部件需要对其绘制的对象进行一些控制的情况。我不需要对我画的东西有任何控制;我只需要绘制它就可以了。我不会引用任何特定字符之后我会画它。[=​​28=]

widget有2000行,所以我就不贴全部代码了,目前我的画法是这样的:

我也在尝试一种不同的方法,在一次 QPainter::drawText 调用中渲染整行,确实速度更快,但我已经失去了用不同颜色为每个字符着色的可能性。我想有这个可能。

我有时使用的一种解决方案是保留预渲染线的缓存。我通常使用一个双向链接的 LRU 条目列表,其中的行数大约是屏幕上可以看到的行数的两倍。每次用于渲染的一行被移到列表的前面;当我需要创建一个新行并且当前缓存计数超过限制时,我会重新使用列表中的最后一个条目。

通过存储单个线条的最终结果,您可以非常快速地重新绘制显示,因为在许多情况下,大多数线条可能不会从一帧更改到下一帧(包括滚动时)。

增加的复杂性也合理地限制在更改内容时必须使该行无效。

QGraphicsScene 的使用不会改善事情 - 它是 QWidget 之上的附加层。您追求的是原始性能,因此不应该使用它。

您可以将 QTextDocument 实现为内存可见部分的视图模型 buffer/file,但是每次滚动时绘制新的 QTextDocument 不会比直接在 QWidget.

上画东西

使用 QStaticText 是朝着正确方向迈出的一步,但还不够:渲染 QStaticText 仍然需要对字形的形状进行光栅化。你可以做得更好,缓存每个你想渲染的 QChar, QColor 组合的像素图:这将比光栅化字符轮廓快得多,无论是否使用 QStaticText

您可以从缓存中绘制像素图,而不是绘制单个字符。 This commit 演示了这种方法。人物画法为:

void drawChar(const QPointF & pos, QChar ch, QColor color, QPainter & p) {
    auto & glyph = m_cache[{ch, color}];
    if (glyph.isNull()) {
        glyph = QPixmap{m_glyphRect.size().toSize()};
        glyph.fill(Qt::white);
        QPainter p{&glyph};
        p.setPen(color);
        p.setFont(m_font);
        p.drawText(m_glyphPos, {ch});
    }
    p.drawPixmap(pos, glyph);
}

您还可以缓存每个(字符、前景、背景)元组。 las,当有很多 foreground/background 组合时,这很快就会失控。

如果您的所有背景都是相同颜色(例如白色),您希望存储角色的负片蒙版:glyph 具有白色背景和透明形状。 This commit 演示了这种方法。字形矩形填充字形颜色,然后在顶部应用白色遮罩:

void drawChar(const QPointF & pos, QChar ch, QColor color, QPainter & p) {
    auto & glyph = m_glyphs[ch];
    if (glyph.isNull()) {
        glyph = QImage{m_glyphRect.size().toSize(), QImage::Format_ARGB32_Premultiplied};
        glyph.fill(Qt::white);
        QPainter p{&glyph};
        p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
        p.setFont(m_font);
        p.drawText(m_glyphPos, {ch});
    }
    auto rect = m_glyphRect;
    rect.moveTo(pos);
    p.fillRect(rect, color);
    p.drawImage(pos, glyph);
}

您可以只存储 alpha 蒙版并按需合成它们,而不是存储给定颜色的完全预渲染字符:

  1. 从透明背景上的预渲染白色字形开始 (CompositionMode_Source)。
  2. CompositionMode_SourceOut 中用背景填充字形矩形:背景将保留字符本身的孔。
  3. CompositionMode_DestinationOver 中用前景填充字形矩形:前景将填充孔。
  4. (可选)如果您尚未在小部件上绘制,则在小部件上绘制合成图。

事实证明这相当快,并且渲染是完全可并行的 - 请参见下面的示例。

注意:预渲染的字形可以使用颜色与 alpha 的进一步预乘以显得不那么粗。

另一种具有出色性能的方法是使用 GPU 模拟文本模式显示。将预渲染的字形轮廓存储在纹理中,将要渲染的字形索引和颜色存储在数组中,并使用 OpenGL 和两个着色器进行渲染。 This example 可能是实施这种方法的起点。

使用 CPU 跨多线程呈现的完整示例如下。

我们从后备存储视图开始,用于生成 QImage 作为给定小部件后备存储视图的视图,可用于并行化绘制。

在 2013 款 iMac 上,此代码在大约 8 毫秒内重新绘制全屏小部件。

// https://github.com/KubaO/Whosebugn/tree/master/questions/hex-widget-40458515
#include <QtConcurrent>
#include <QtWidgets>
#include <algorithm>
#include <array>
#include <cmath>

struct BackingStoreView {
    QImage *dst = {};
    uchar *data = {};
    const QWidget *widget = {};
    explicit BackingStoreView(const QWidget *widget) {
        if (!widget || !widget->window()) return;
        dst = dynamic_cast<QImage*>(widget->window()->backingStore()->paintDevice());
        if (!dst || dst->depth() % 8) return;
        auto byteDepth = dst->depth()/8;
        auto pos = widget->mapTo(widget->window(), {});
        data = const_cast<uchar*>(dst->constScanLine(pos.y()) + byteDepth * pos.x());
        this->widget = widget;
    }
    // A view onto the backing store of a given widget
    QImage getView() const {
        if (!data) return {};
        QImage ret(data, widget->width(), widget->height(), dst->bytesPerLine(), dst->format());
        ret.setDevicePixelRatio(widget->devicePixelRatio());
        return ret;
    }
    // Is a given image exactly this view?
    bool isAView(const QImage &img) const {
        return data && img.bits() == data && img.depth() == dst->depth()
                && img.width() == widget->width() && img.height() == widget->height()
                && img.bytesPerLine() == dst->bytesPerLine() && img.format() == dst->format();
    }
};

那么,CP437字符集:

static auto const CP437 = QStringLiteral(
            " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼▶◀↕‼¶§▬↨↑↓→←∟↔▲▼"
            "␣!\"#$%&'()*+,-./0123456789:;<=>?"
            "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_"
            "`abcdefghijklmnopqrstuvwxyz{|}~ "
            "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒ"
            "áíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐"
            "└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀"
            "αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ");

HexView 小部件派生自 QAbstractScrollArea 并可视化内存映射数据块:

class HexView : public QAbstractScrollArea {
    Q_OBJECT
    QImage const m_nullImage;
    const int m_addressChars = 8;
    const int m_dataMargin = 4;
    const char * m_data = {};
    size_t m_dataSize = 0;
    size_t m_dataStart = 0;
    QSize m_glyphSize;
    QPointF m_glyphPos;
    int m_charsPerLine, m_lines;
    QMap<QChar, QImage> m_glyphs;
    QFont m_font{"Monaco"};
    QFontMetricsF m_fm{m_font};
    struct DrawUnit { QPoint pos; const QImage *glyph; QColor fg, bg; };
    QFutureSynchronizer<void> m_sync;
    QVector<DrawUnit> m_chunks;
    QVector<QImage> m_stores;
    using chunk_it = QVector<DrawUnit>::const_iterator;
    using store_it = QVector<QImage>::const_iterator;

    static inline QChar decode(char ch) { return CP437[uchar(ch)]; }
    inline int xStep() const { return m_glyphSize.width(); }
    inline int yStep() const { return m_glyphSize.height(); }
    void initData() {
        int const width = viewport()->width() - m_addressChars*xStep() - m_dataMargin;
        m_charsPerLine = (width > 0) ? width/xStep() : 0;
        m_lines = viewport()->height()/yStep();
        if (m_charsPerLine && m_lines) {
            verticalScrollBar()->setRange(0, m_dataSize/m_charsPerLine);
            verticalScrollBar()->setValue(m_dataStart/m_charsPerLine);
        } else {
            verticalScrollBar()->setRange(0, 0);
        }
    }
    const QImage &glyph(QChar ch) {
        auto &glyph = m_glyphs[ch];
        if (glyph.isNull()) {
            QPointF extent = m_fm.boundingRect(ch).translated(m_glyphPos).bottomRight();
            glyph = QImage(m_glyphSize, QImage::Format_ARGB32_Premultiplied);
            glyph.fill(Qt::transparent);
            QPainter p{&glyph};
            p.setPen(Qt::white);
            p.setFont(m_font);
            p.translate(m_glyphPos);
            p.scale(std::min(1.0, (m_glyphSize.width()-1)/extent.x()),
                    std::min(1.0, (m_glyphSize.height()-1)/extent.y()));
            p.drawText(QPointF{}, {ch});
        }
        return glyph;
    }

并行渲染在 class 方法中完成 - 除了访问只读数据和渲染到后备存储之外,它们不修改小部件的状态。每个线程都作用于商店中的孤立线。

    static void drawChar(const DrawUnit & u, QPainter &p) {
        const QRect rect(u.pos, u.glyph->size());
        p.setCompositionMode(QPainter::CompositionMode_Source);
        p.drawImage(u.pos, *u.glyph);
        p.setCompositionMode(QPainter::CompositionMode_SourceOut);
        p.fillRect(rect, u.bg);
        p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
        p.fillRect(rect, u.fg);
    }
    static QFuture<void> submitChunks(chunk_it begin, chunk_it end, store_it store) {
        return QtConcurrent::run([begin, end, store]{
            QPainter p(const_cast<QImage*>(&*store));
            for (auto it = begin; it != end; it++)
                drawChar(*it, p);
        });
    }

此方法在线程之间分配工作块:

    int processChunks() {
        m_stores.resize(QThread::idealThreadCount());
        BackingStoreView view(viewport());
        if (!view.isAView(m_stores.last()))
            std::generate(m_stores.begin(), m_stores.end(), [&view]{ return view.getView(); });
        std::ptrdiff_t jobSize = std::max(128, (m_chunks.size() / m_stores.size())+1);
        auto const cend = m_chunks.cend();
        int refY = 0;
        auto store = m_stores.cbegin();
        for (auto it = m_chunks.cbegin(); it != cend;) {
            auto end = it + std::min(cend-it, jobSize);
            while (end != cend && (end->pos.y() == refY || (refY = end->pos.y(), false)))
                end++; // break chunks across line boundaries
            m_sync.addFuture(submitChunks(it, end, store));
            it = end;
            store++;
        }
        m_sync.waitForFinished();
        m_sync.clearFutures();
        m_chunks.clear();
        return store - m_stores.cbegin();
    }

实施的其余部分没有争议:

protected:
    void paintEvent(QPaintEvent *ev) override {
        QElapsedTimer time;
        time.start();
        QPainter p{viewport()};
        QPoint pos;
        QPoint const step{xStep(), 0};
        auto dividerX = m_addressChars*xStep() + m_dataMargin/2.;
        p.drawLine(dividerX, 0, dividerX, viewport()->height());
        int offset = 0;
        QRect rRect = ev->rect();
        p.end();
        while (offset < m_charsPerLine*m_lines && m_dataStart + offset < m_dataSize) {
            const auto address = QString::number(m_dataStart + offset, 16);
            pos += step * (m_addressChars - address.size());
            for (auto c : address) {
                if (QRect(pos, m_glyphSize).intersects(rRect))
                    m_chunks.push_back({pos, &glyph(c), Qt::black, Qt::white});
                pos += step;
            }
            pos += {m_dataMargin, 0};
            auto bytes = std::min(m_dataSize - offset, (size_t)m_charsPerLine);
            for (int n = bytes; n; n--) {
                if (QRect(pos, m_glyphSize).intersects(rRect))
                    m_chunks.push_back({pos, &glyph(decode(m_data[m_dataStart + offset])), Qt::red, Qt::white});
                pos += step;
                offset ++;
            }
            pos = {0, pos.y() + yStep()};
        }
        int jobs = processChunks();
        newStatus(QStringLiteral("%1ms n=%2").arg(time.nsecsElapsed()/1e6).arg(jobs));
    }
    void resizeEvent(QResizeEvent *) override {
        initData();
    }
    void scrollContentsBy(int, int dy) override {
        m_dataStart = verticalScrollBar()->value() * (size_t)m_charsPerLine;
        viewport()->scroll(0, dy * m_glyphSize.height(), viewport()->rect());
    }
public:
    HexView(QWidget * parent = nullptr) : HexView(nullptr, 0, parent) {}
    HexView(const char * data, size_t size, QWidget * parent = nullptr) :
        QAbstractScrollArea{parent}, m_data(data), m_dataSize(size)
    {
        QRectF glyphRectF{0., 0., 1., 1.};
        for (int i = 0x20; i < 0xE0; ++i)
            glyphRectF = glyphRectF.united(m_fm.boundingRect(CP437[i]));
        m_glyphPos = -glyphRectF.topLeft();
        m_glyphSize = QSize(std::ceil(glyphRectF.width()), std::ceil(glyphRectF.height()));
        initData();
    }
    void setData(const char * data, size_t size) {
        if (data == m_data && size == m_dataSize) return;
        m_data = data;
        m_dataSize = size;
        m_dataStart = 0;
        initData();
        viewport()->update();
    }
    Q_SIGNAL void newStatus(const QString &);
};

我们利用现代 64 位系统和内存映射要由小部件可​​视化的源文件。出于测试目的,还可以查看字符集:

int main(int argc, char ** argv) {
    QApplication app{argc, argv};
    QFile file{app.applicationFilePath()};
    if (!file.open(QIODevice::ReadOnly)) return 1;
    auto *const map = (const char*)file.map(0, file.size(), QFile::MapPrivateOption);
    if (!map) return 2;

    QWidget ui;
    QGridLayout layout{&ui};
    HexView view;
    QRadioButton exe{"Executable"};
    QRadioButton charset{"Character Set"};
    QLabel status;
    layout.addWidget(&view, 0, 0, 1, 4);
    layout.addWidget(&exe, 1, 0);
    layout.addWidget(&charset, 1, 1);
    layout.addWidget(&status, 1, 2, 1, 2);
    QObject::connect(&exe, &QPushButton::clicked, [&]{
        view.setData(map, (size_t)file.size());
    });
    QObject::connect(&charset, &QPushButton::clicked, [&]{
        static std::array<char, 256> data;
        std::iota(data.begin(), data.end(), char(0));
        view.setData(data.data(), data.size());
    });
    QObject::connect(&view, &HexView::newStatus, &status, &QLabel::setText);
    charset.click();
    ui.resize(1000, 800);
    ui.show();
    return app.exec();
}

#include "main.moc"