2016-11-07 81 views
2

我在写一个显示大量文本的应用程序。它不是单词和句子,而是CP437字符集中显示的二进制数据。目前的形式:在Qt5中绘制大量独立字符的最佳方式是?

Screenshot of my current application

我有一个问题,但与绘制这些字符。我需要逐个绘制每个字符,因为后来我想应用不同的着色。这些角色也应该有一个透明的背景,因为后来我想在背景中绘制不同颜色的部分和范围(根据一些标准对这些角色进行分组)。

该应用程序同时支持多个打开的文件,但是当打开多个文件时,绘图开始在快速i7上显而易见,因此可能写得很差。

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

编辑:我使用的是正常QFrame小部件,它在paintEvent提取使用QPainter。这是一个错误的方法?我已经阅读了QGraphicsScene上的一些文档,我记得它最好用于小部件需要对其绘制的对象进行一些控制的情况。我不需要任何控制我画的东西;我只需要画出它就是了。之后我不会引用任何特定的字符我会画出它。

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

  • 首先,创建256个条目的表(cache),把迭代器计数器i变量,
  • 对于每个条目,创建一个QStaticText对象,它包含关于由从i可变采取ASCII码识别的字符的绘图信息,
  • 后来,在绘图功能,对于输入流中的每个字节(即从文件中),画出t他使用cache表中的QStaticText数据。因此,要绘制ASCII字符0x7A,我将在cache表中查找0x7a索引QStaticText,并将此QStaticText对象送入QPainter对象。

我还用不同的方法进行实验,使整条生产线中的一个QPainter::drawText电话,确实是快,但我已经失去了着色与不同颜色的每个字符的可能性。我想有这种可能性。

+0

有助于了解代码现在是什么?你在使用QTextEdit吗?您是否使用QTextCursor插入文本?你只是在绘制QGraphicsScene? – GabeWeiss

+0

您是否建立了基线在QTextEdit或QGraphicsScene中甚至在QML中绘制文本? – rubenvb

+0

用相关信息更新了帖子。我无法真正使用'QTextEdit',因为它似乎管理着自己的缓冲区。我需要有自己的缓冲区,因为我需要我的应用程序来允许管理(复制/粘贴)大量数据区域(如10 TB)而不会有任何延迟。 – antonone

回答

5

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

你可以实现一个QTextDocument为您的内存缓冲区/文件的可见部分中的视图模型,但您滚动新鲜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); 
} 

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

如果您的所有背景都具有相同的颜色(例如白色),则您希望存储该字符的负面蒙版: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); 
} 

而不是存储在给定颜色的完全预渲染的字符,你也可以只把阿尔法遮掩和复合他们 - 要求:

  1. 从透明背景上的预先呈现的白色字形开始。
  2. CompositionMode_SourceOut中填充带背景的字形直方图:背景将为该字符本身留下一个空洞。
  3. CompositionMode_DestinationOver前面填充字形矩形:前景将填充该孔。
  4. 在小部件上绘制复合材料。

事实证明这是相当快的,如果您希望这样做(我把它作为练习读者),复合材料的渲染是完全可并行化的。

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

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

下面是一个使用CPU渲染的完整示例。

enter image description here

我们先从CP437字符集:

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

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

HexView部件从QAbstractScrollArea派生和可视化数据的存储器映射块:

class HexView : public QAbstractScrollArea { 
    const int m_addressChars = 8; 
    const qreal m_dataMargin = 4.; 
    const char * m_data; 
    size_t m_size; 
    size_t m_start = 0; 
    QRectF m_glyphRect{0.,0.,1.,1.}; 
    QPointF m_glyphPos; 
    int m_chars, m_lines; 
    QMap<QChar, QImage> m_glyphs; 
    QFont m_font{"Monaco"}; 
    qreal xStep() const { return m_glyphRect.width(); } 
    qreal yStep() const { return m_glyphRect.height(); } 
    static QChar decode(char ch) { return CP437()[(uchar)ch]; } 
    void drawChar(const QPointF & pos, QChar ch, QColor fg, QColor bg, QPainter & p) { 
     auto & glyph = m_glyphs[ch]; 
     if (glyph.isNull()) { 
      glyph = QImage{m_glyphRect.size().toSize(), QImage::Format_ARGB32_Premultiplied}; 
      glyph.fill(Qt::transparent); 
      QPainter p{&glyph}; 
      p.setPen(Qt::white); 
      p.setFont(m_font); 
      p.drawText(m_glyphPos, {ch}); 
     } 
     QImage composite = glyph; 
     { 
      QPainter p{&composite}; 
      p.setCompositionMode(QPainter::CompositionMode_SourceOut); 
      p.fillRect(composite.rect(), bg); 
      p.setCompositionMode(QPainter::CompositionMode_DestinationOver); 
      p.fillRect(composite.rect(), fg); 
     } 
     auto rect = m_glyphRect; 
     rect.moveTo(pos); 
     p.drawImage(pos, composite); 
    } 
    void initData() { 
     qreal width = viewport()->width() - m_addressChars*xStep() - m_dataMargin; 
     m_chars = (width > 0.) ? width/xStep() : 0.; 
     m_lines = viewport()->height()/yStep(); 
     if (m_chars && m_lines) { 
      verticalScrollBar()->setRange(0, m_size/m_chars); 
      verticalScrollBar()->setValue(m_start/m_chars); 
     } else { 
      verticalScrollBar()->setRange(0, 0); 
     } 
    } 
    void paintEvent(QPaintEvent *) override { 
     QPainter p{viewport()}; 
     QPointF pos; 
     QPointF step{xStep(), 0.}; 
     auto dividerX = m_addressChars*xStep() + m_dataMargin/2.; 
     p.drawLine(dividerX, 0, dividerX, viewport()->height()); 
     int offset = 0; 
     while (offset < m_chars*m_lines && m_start + offset < m_size) { 
      auto rawAddress = QString::number(m_start + offset, 16); 
      auto address = QString{m_addressChars-rawAddress.size(), ' '} + rawAddress; 
      for (auto c : address) { 
       drawChar(pos, c, Qt::black, Qt::white, p); 
       pos += step; 
      } 
      pos += QPointF{m_dataMargin, 0.}; 
      auto bytes = std::min(m_size - offset, (size_t)m_chars); 
      for (int n = bytes; n; n--) { 
       drawChar(pos, decode(m_data[m_start + offset++]), Qt::red, Qt::white, p); 
       pos += step; 
      } 
      pos = QPointF{0., pos.y() + yStep()}; 
     } 
    } 
    void resizeEvent(QResizeEvent *) override { 
     initData(); 
    } 
    void scrollContentsBy(int, int) override { 
     m_start = verticalScrollBar()->value() * (size_t)m_chars; 
     viewport()->update(); 
    } 
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_size(size) 
    { 
     auto fm = QFontMetrics(m_font); 
     for (int i = 0x20; i < 0xE0; ++i) 
      m_glyphRect = m_glyphRect.united(fm.boundingRect(CP437()[i])); 
     m_glyphPos = {-m_glyphRect.left(), -m_glyphRect.top()}; 
     initData(); 
    } 
    void setData(const char * data, size_t size) { 
     if (data == m_data && size == m_size) return; 
     m_data = data; 
     m_size = size; 
     m_start = 0; 
     initData(); 
     viewport()->update(); 
    } 
}; 

我们充分利用现代64位系统和存储器映射源文件以便由小部件可​​视化。出于测试目的,字符集的视图也可用:

int main(int argc, char ** argv) { 
    QApplication app{argc, argv}; 
    QFile file{app.applicationFilePath()}; 
    if (!file.open(QIODevice::ReadOnly)) return 1; 
    const char * const map = (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"}; 
    layout.addWidget(&view, 0, 0, 1, 3); 
    layout.addWidget(&exe, 1, 0); 
    layout.addWidget(&charset, 1, 1); 
    QObject::connect(&exe, &QPushButton::clicked, [&]{ 
     view.setData(map, (size_t)file.size()); 
    }); 
    QObject::connect(&charset, &QPushButton::clicked, [&]{ 
     static QByteArray data; 
     if (data.isNull()) { 
      data.resize(256); 
      for (int i = 0; i < data.size(); ++i) data[i] = (char)i; 
     } 
     view.setData(data.constData(), (size_t)data.size()); 
    }); 
    charset.click(); 
    ui.show(); 
    return app.exec(); 
} 
+0

非常有用的信息的完美答案。谢谢! – antonone

2

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

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

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

+0

这似乎是一个很好的优化,但如果我正确地理解它,那么只有在逐行滚动视图时最好才能看到它。我想用PageDown/Up滚动视图来加快速度(所以,一次滚动整个屏幕),或者使用滚动条随机滚动一个大文件(例如,当加载大小为6 GB)。不过,我可能会在稍后使用您建议的最优化! – antonone

+0

@antonone:使用行缓存对于一般情况(当您平滑滚动并进行小的更改,选择区域等等),一切运行都很快,无需编写特殊情况。根据我的经验,如果wiew内容完全改变,那么即使是20fps的“慢”帧率(例如低于键重复率)也不是一个大问题。我实现这个功能的是一个带有复杂语法高亮显示的IDE,在移动,选择或输入文本时,比键盘慢是完全不可接受的。 – 6502

相关问题