2016-11-07 12 views
2

Я пишу приложение, которое отображает много текста. Однако это не слова и предложения, это двоичные данные, отображаемые в кодировке CP437. Текущая форма:Лучший способ привлечь много независимых персонажей в Qt5?

Screenshot of my current application

У меня возникли проблемы, хотя с нанесением этих персонажей. Мне нужно рисовать каждого персонажа один за другим, потому что позже я хотел бы применить различную раскраску. Эти символы также должны иметь прозрачный фон, потому что позже я хотел бы рисовать разделы и диапазоны с разными цветами в фоновом режиме (чтобы сгруппировать эти символы на основе некоторых критериев).

Приложение поддерживает несколько открытых файлов одновременно, но при открытии нескольких файлов рисунок становится заметным на быстром i7, поэтому он, вероятно, плохо написан.

Что было бы лучшим подходом для такого рода данных в Qt5? Должен ли я просто указывать символы в растровые изображения и начинать оттуда или на самом деле можно рисовать много символов, используя обычные функции Qt для рисования текста?

Edit: Я использую обычный QFrame виджет, который делает рисование в paintEvent, используя QPainter. Это неправильный подход? Я прочитал несколько документов по адресу QGraphicsScene, из которых я вспомнил, что он лучше всего используется в ситуациях, когда виджет должен иметь некоторый контроль над объектами, которые он рисует. Мне не нужен контроль над тем, что я рисую; Мне просто нужно нарисовать его, и все. Я не буду ссылаться на какой-либо конкретный символ после Я нарисую его.

Виджет имеет 2000 строк, поэтому я не буду вставлять весь код, но в настоящее время мой рисунок подход таков:

  • Во-первых, создать таблицу (cache) с 256 записей, положить итератор счетчик для i переменных,
  • для каждой записи, создать QStaticText объект, который содержит рисунок информации о характере идентифицированного ASCII коды, взятой из i переменных,
  • Позже, в функции рисования, для каждого байта во входном потоке (т.е. из файла), нарисуйте t он использует данные QStaticText из таблицы cache. Итак, чтобы нарисовать символ ASCII 0x7A, я найду QStaticText из индекса 0x7a в таблице cache и отправлю этот объект QStaticText в объект QPainter.

Я также экспериментировал с другим подходом, делая всю линию в одном QPainter::drawText вызова, и на самом деле это было быстрее, но я потерял возможность окраски каждый персонажа с другим цветом. Я хотел бы иметь такую ​​возможность.

+0

Было бы полезно знать, что код сейчас? Вы используете QTextEdit? Вы используете QTextCursor для вставки текста? Вы просто рисуете QGraphicsScene? – GabeWeiss

+0

Установили ли вы базовую линию, например. рисование текста в QTextEdit или QGraphicsScene или даже в QML? – rubenvb

+0

Обновлено сообщение с соответствующей информацией. Я не могу использовать 'QTextEdit', поскольку он управляет собственным буфером. Мне нужно иметь собственный буфер, потому что мне нужно мое приложение, позволяющее управлять (копировать/вставлять) огромные области данных (например, 10 терабайт) без каких-либо задержек. – antonone

ответ

5

Использование QGraphicsScene не улучшит ситуацию - это дополнительный слой поверх QWidget. Вы после сырой производительности, поэтому вы не должны использовать его.

Вы могли бы реализовать QTextDocument как ViewModel для видимой части вашего буфера памяти/файла, но картина свежий QTextDocument каждый раз, когда вы прокрутки не будет быстрее, чем рисование вещи непосредственно на QWidget.

Использование QStaticText является шагом в правильном направлении, но недостаточно: рендеринг QStaticText по-прежнему требует растеризации формы глифа. Вы можете сделать лучше и кэшировать pixmap каждой комбинации 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); 
} 

Вместо того, чтобы хранить полностью предварительно оказанный характер данного цвета, можно хранить только альфа-маску и композит их на -demand:

  1. Начните с предварительно представленного белого глифа на прозрачном фоне.
  2. Заполните выделение глифом фоном в CompositionMode_SourceOut: фон останется с отверстием для самого символа.
  3. Заполните грань глифа с передним планом в CompositionMode_DestinationOver: передняя часть заполнит отверстие.
  4. Нарисуйте композит на виджетах.

Это оказывается достаточно быстрым, и рендеринг композитов полностью распараллеливается, если вы этого хотите (я оставляю его как упражнение для читателя).

Примечание: Предваренный глиф может использовать дополнительное премультипликацию цвета с альфой, чтобы он выглядел менее толстым.

Еще один подход с отличной производительностью - эмулировать отображение текстового режима с использованием графического процессора. Храните предварительно визуализированные контуры глифов в текстуре, сохраняйте индексы глифов и цвета, которые нужно визуализировать в массиве, и используйте OpenGL и два шейдера для рендеринга. This example может стать отправной точкой для реализации такого подхода.

Полный пример с использованием рендеринга ЦП.

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 ГБ). Тем не менее, я, вероятно, воспользуюсь оптимизацией, которую вы предложили позже. – antonone

+0

@antonone: при использовании линейного кэша все работает быстро для обычных случаев (когда вы плавно прокручиваетесь и делаете небольшие изменения, выбираете области и т. Д.), Не требуя кода для особых случаев. По моему опыту, если содержание wiew полностью изменяется, то даже «медленная» частота кадров, такая как 20fps (ниже, например, частота повторения клавиш), не является большой проблемой. То, что я реализовал для этого, - это IDE со сложной подсветкой синтаксиса, где медленнее, чем клавиатура, совершенно неприемлемо при перемещении, выборе или вводе текста. – 6502

 Смежные вопросы

  • Нет связанных вопросов^_^