首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在Qt5中绘制许多独立字符的最佳方法?

在Qt5中绘制许多独立字符的最佳方法?
EN

Stack Overflow用户
提问于 2016-11-07 05:41:41
回答 2查看 1.9K关注 0票数 3

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

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

应用程序同时支持多个已打开的文件,但是当有多个文件打开时,快速i7上的绘图就开始引人注目,因此它可能编写得很糟糕。

在Qt5中绘制这类数据的最佳方法是什么?我应该先将字符预写到位图中,然后从那里开始,还是可以通过使用普通的Qt函数来绘制文本来绘制大量字符?

编辑:我使用的是一个普通的QFrame小部件,它使用QPainterpaintEvent中绘图。这是错误的做法吗?我读过一些关于QGraphicsScene的文档,从这些文档中我记得,在小部件需要对其绘制的对象有一些控制的情况下,最好使用它。我不需要对我画的东西有任何控制,我只需要画它,仅此而已。在我画完它之后,我不会引用任何特定的字符。

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

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

我还在尝试一种不同的方法,在一个QPainter::drawText调用中呈现整行,这确实更快,但我已经失去了用不同颜色着色每个字符的可能性。我希望有这种可能性。

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2016-11-07 23:18:04

QGraphicsScene的使用不会改善任何事情--它是QWidget之上的一个附加层。你追求的是原始的表现,所以你不应该使用它。

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

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

然后,您将从缓存中绘制像素映射,而不是绘制单个字符。此承诺演示了这种方法。字符绘制方法是:

代码语言:javascript
复制
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有一个白色的背景和透明的形状。此承诺演示了这种方法。字形矩形填充字形颜色,然后在顶部涂上白色掩膜:

代码语言:javascript
复制
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和两个着色器进行渲染。这个例子可能是实现这种方法的起点。

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

我们从备份存储视图开始,用于生成QImage,这些视图是给定小部件的后备存储中的视图,可以用于并行绘制。

在2013年的iMac上,这段代码在大约8ms内重新绘制了全屏小部件。

代码语言:javascript
复制
// 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字符集:

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

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

代码语言:javascript
复制
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;
    }

并行化呈现是在类方法中完成的-它们不会修改小部件的状态,只会访问只读数据并呈现到备份存储中。每个线程都作用于存储区中的孤立行。

代码语言:javascript
复制
    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);
        });
    }

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

代码语言:javascript
复制
    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();
    }

其余的执行工作是没有争议的:

代码语言:javascript
复制
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位系统和内存映射源文件来可视化小部件。出于测试目的,还可以获得字符集的视图:

代码语言:javascript
复制
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"
票数 12
EN

Stack Overflow用户

发布于 2016-11-07 07:09:42

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

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

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

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/40458515

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档