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

时间:2016-11-07 05:41:41

标签: qt user-interface draw

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

Screenshot of my current application

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

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

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

修改:我使用普通的QFrame窗口小部件,使用paintEvent绘制QPainter。这是一种错误的做法吗?我已经在QGraphicsScene上阅读了一些文档,我从中记得它最适用于窗口小部件需要对其绘制的对象进行某些控制的情况。我不需要对我画的东西有任何控制权;我只需要绘制它,这就是全部。我之后我不会引用任何特定字符。我会画它。

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

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

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

2 个答案:

答案 0 :(得分:8)

使用QGraphicsScene不会改善事情 - 它是QWidget之上的附加图层。你是在原始表现之后,所以你不应该使用它。

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

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

您可以从缓存中绘制pixmaps,而不是绘制单个字符。 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);
}

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

如果您的所有背景颜色相同(例如白色),则您希望存储角色的负面效果: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. (可选)如果您尚未在窗口小部件上绘制,则在窗口小部件上绘制合成。
  5. 结果相当快,渲染完全可并行化 - 请参阅下面的示例。

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

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

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

    screenshot of the example

    我们从后备存储视图开始,用于生成QImage 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();
        }
    };
    

    然后,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;
        }
    

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

        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"
    

答案 1 :(得分:2)

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

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

当您更改内容时,必须使行无效时,复杂性的增加也是合理的。