在Qt5中绘制大量独立角色的最佳方法是什么?

ant*_*one 3 user-interface qt draw

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

我当前应用程序的截图

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

该应用程序同时支持多个打开的文件,但是当打开多个文件时,快速i7上的绘图开始变得明显,因此可能写得很糟糕.

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

编辑:我正在使用一个正常的QFrame小部件,它paintEvent使用QPainter.这是一种错误的做法吗?我已经阅读了一些文档QGraphicsScene,我记得它最适用于窗口小部件需要对它绘制的对象进行一些控制的情况.我不需要任何控制我画的东西; 我只需要绘制它,就是这样.我画完之后,我不会引用任何特定的字符.

小部件有2000行,所以我不会粘贴整个代码,但目前我的绘图方法是这样的:

  • 首先,创建一个cache包含256个条目的table(),将迭代器计数器放到i变量中,
  • 对于每个条目,创建一个QStaticText对象,其中包含有关由i变量中的ASCII代码标识的字符的绘图信息,
  • 后来,在绘图功能,对于输入流(即,从文件)中的每个字节,绘制使用所述数据QStaticText从所述cache的表.因此,要绘制ASCII字符0x7A,我将从表中的QStaticText索引0x7acache查找,并将此QStaticText对象提供给QPainter对象.

我也尝试了一种不同的方法,在一次QPainter::drawText调用中渲染整行,实际上它更快,但我已经失去了用不同颜色着色每个角色的可能性.我想有这种可能性.

Rei*_*ica 8

使用a QGraphicsScene不会改善事物 - 它是一个额外的层QWidget.你是在原始表现之后,所以你不应该使用它.

您可以QTextDocument将内存缓冲区/文件的可见部分实现为视图模型,但QTextDocument每次滚动时绘制新内容都不会比直接在a上绘制内容更快QWidget.

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

然后,您可以从缓存中绘制pixmaps,而不是绘制单个字符.此提交演示了此方法.角色绘制方法是:

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);
}
Run Code Online (Sandbox Code Playgroud)

您还可以缓存每个(字符,前景,背景)元组.唉,当有许多前景/背景组合时,这很快就会失控.

如果您的所有背景颜色相同(例如白色),则您希望存储角色的负面效果:glyph具有白色背景和透明形状.此提交演示了此方法.字形矩形用字形颜色填充,然后在顶部应用白色蒙版:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

注意:预渲染的字形可以使用颜色与alpha的进一步预乘,看起来不那么厚.

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

下面是一个完整的示例,使用跨多个线程的CPU呈现.

该示例的屏幕截图

我们从后备存储视图开始,用于生成QImages,这些s是对给定窗口小部件的后备存储的视图,并且可用于并行化绘制.

在2013 iMac上,此代码在大约8ms内重新绘制全屏小部件.

// https://github.com/KubaO/stackoverflown/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();
    }
};
Run Code Online (Sandbox Code Playgroud)

然后,CP437字符集:

static auto const CP437 = QStringLiteral(
            " ??????•????????????¶§??????????"
            "?!\"#$%&'()*+,-./0123456789:;<=>?"
            "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
            "`abcdefghijklmnopqrstuvwxyz{|}~ "
            "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥?ƒ"
            "áíóúñѪº¿?¬½¼¡«»????????????????"
            "????????????????????????????????"
            "?ß????µ??????????±????÷?°?·??²? ");
Run Code Online (Sandbox Code Playgroud)

所述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;
    }
Run Code Online (Sandbox Code Playgroud)

并行化渲染是在类方法中完成的 - 除了访问只读数据和渲染到后备存储之外,它们不会修改窗口小部件的状态.线程每个都作用于商店中的隔离线.

    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);
        });
    }
Run Code Online (Sandbox Code Playgroud)

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

    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();
    }
Run Code Online (Sandbox Code Playgroud)

实施的其余部分是无可争议的:

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 &);
};
Run Code Online (Sandbox Code Playgroud)

我们利用现代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"
Run Code Online (Sandbox Code Playgroud)