首页 > 解决方案 > Qt:可视化大型二维数组的最有效方法是什么?

问题描述

我正在移植一个项目,该项目使用 Curses 来可视化一个大型(例如 5000x5000)二维字符数组。问题是,它必须是一个高性能项目,其中数组不断更新,但在当前状态下,无论我如何优化后端,输出到 stdout 都是一个瓶颈。该项目将受益于使用 Qt 可以提供的更快和面向对象的方法。我试过的:

#include <QtWidgets>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QGraphicsScene scene;
    QGraphicsView view(&scene);

    double totalY = 0;
    for (size_t i = 0; i < 5000; ++i) {
        double totalX = 0;
        for (size_t j = 0; j < 5000; ++j) {
            // Add an arbitrary char
            QGraphicsSimpleTextItem *simpleText = scene.addSimpleText(QChar('.'));
            simpleText->setPos(totalX, totalY);
            // Add cell's width and height respectively
            totalX += 10;
        }
        totalY += 10;
    }

    view.show();

    return a.exec();
}

但事实证明,创建 5000x5000 图形项目要慢得多。我的下一个想法是创建某种视口,从而消除使用图形项目的需要。它可能是一些画布(QImage),每次更新数组时我都会清除并绘制它。但是你推荐什么?

标签: c++qt

解决方案


第二尝试

在第一次尝试并不令人满意之后,我按照VK的提示进行操作:

我的意思是绘制一个字符是非常昂贵的操作。所以我建议你将每个可能的字符(我假设它们不多)绘制到内存中的一些小图像上。而不是bitblt(如果你不明白一些词,然后谷歌它),即复制字节块到最终的QImage。这将比绘画文本快得多。

testQLargeCharTable2.cc

#include <cassert>
#include <algorithm>
#include <random>
#include <vector>

#include <QtWidgets>

template <typename Value>
class MatrixT {
  private:
    size_t _nCols;
    std::vector<Value> _values; 
  public:
    MatrixT(size_t nRows, size_t nCols, Value value = Value()):
      _nCols((assert(nCols > 0), nCols)), _values(nRows * nCols, value)
    { }

    size_t rows() const { return _values.size() / _nCols; }
    size_t cols() const { return _nCols; }
    
    Value* operator[](size_t i)
    {
      assert(i < rows());
      return &_values[i * _nCols];
    }
    const Value* operator[](size_t i) const
    {
      assert(i < rows());
      return &_values[i * _nCols];
    }
};

using CharTable = MatrixT<char>;

class CharTableView: public QAbstractScrollArea {
  private:
    const CharTable* pTbl = nullptr;
    using CharCache = std::map<char, QPixmap>;
    int wCell;
    int hCell;
    CharCache cacheChars;

  public:
    CharTableView(QWidget* pQParent = nullptr);
    virtual ~CharTableView() = default;

    CharTableView(const CharTableView&) = delete;
    CharTableView& operator=(const CharTableView&) = delete;

    void set(CharTable* pTbl)
    {
      this->pTbl = pTbl;
      updateScrollBars();
      update();
    }

  protected:
    virtual void resizeEvent(QResizeEvent* pQEvent) override;
    virtual void paintEvent(QPaintEvent* pQEvent) override;

  private:
    void updateScrollBars();
    const QPixmap& getCharPixmap(char c);
};

void CharTableView::resizeEvent(QResizeEvent* pQEvent)
{
  updateScrollBars();
}

void CharTableView::paintEvent(QPaintEvent* pQEvent)
{
  if (!pTbl) return;
  const int xView = horizontalScrollBar()
    ? horizontalScrollBar()->value() : 0;
  const int yView = verticalScrollBar()
    ? verticalScrollBar()->value() : 0;
  const int wView = viewport()->width();
  const int hView = viewport()->height();
  const int iRow0 = yView / hCell;
  const int iCol0 = xView / wCell;
  const int iRowN = std::min((int)pTbl->rows(), (yView + hView) / hCell + 1);
  const int iColN = std::min((int)pTbl->cols(), (xView + wView) / wCell + 1);
  QPainter qPainter(viewport());
  for (int iRow = iRow0; iRow < iRowN; ++iRow) {
    const char*const row = (*pTbl)[iRow];
    const int yCell = iRow * hCell - yView;
    for (int iCol = iCol0; iCol < iColN; ++iCol) {
      const int xCell = iCol * wCell - xView;
      const QPixmap& qPixmap = getCharPixmap(row[iCol]);
      qPainter.drawPixmap(
        QRect(xCell, yCell, wCell, hCell),
        qPixmap);
    }
  }
}

CharTableView::CharTableView(QWidget* pQWidget):
  QAbstractScrollArea(pQWidget)
{
  QFontMetrics qFontMetrics(viewport()->font());
  wCell = 2 * qFontMetrics.averageCharWidth();
  hCell = qFontMetrics.height();
}

void CharTableView::updateScrollBars()
{
  const int w = (int)(pTbl ? pTbl->cols() : 0) * wCell;
  const int h = (int)(pTbl ? pTbl->rows() : 0) * hCell;
  const QSize sizeView = viewport()->size();
  QScrollBar*const pQScrBarH = horizontalScrollBar();
  pQScrBarH->setRange(0, w > sizeView.width() ? w - sizeView.width() : 0);
  pQScrBarH->setPageStep(sizeView.width());
  QScrollBar*const pQScrBarV = verticalScrollBar();
  pQScrBarV->setRange(0, h > sizeView.height() ? h - sizeView.height() : 0);
  pQScrBarV->setPageStep(sizeView.height());
}

const QPixmap& CharTableView::getCharPixmap(char c)
{
  const CharCache::iterator iter = cacheChars.find(c);
  if (iter != cacheChars.end()) return iter->second;
  QPixmap& qPixmap = cacheChars[c] = QPixmap(wCell, hCell);
  qPixmap.fill(QColor(0, 0, 0, 0));
  { QPainter qPainter(&qPixmap);
    qPainter.drawText(
      QRect(0, 0, wCell, hCell),
      Qt::AlignCenter,
      QString(QChar(c)));
  }
  return qPixmap;
}

int main(int argc, char** argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  const size_t n = 10;
  QApplication app(argc, argv);
  // setup data
  const char chars[]
    = "0123456789()[]{}/&%$!'+#?="
      "abcdefghijklmnopqrstuvwxyz"
      "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  CharTable tbl(5000, 5000);
  std::random_device rd;
  std::mt19937 rng(rd()); // seed the generator
  std::uniform_int_distribution<size_t> distr(0, std::size(chars) - 1);
  for (size_t i = 0; i < tbl.rows(); ++i) {
    char*const row = tbl[i];
    for (size_t j = 0; j < tbl.cols(); ++j) {
      row[j] = chars[distr(rng)];
    }
  }
  // setup GUI
  CharTableView qCharTableView;
  qCharTableView.setWindowTitle("Large Character Table View - 2nd Attempt");
  qCharTableView.resize(1024, 768);
  qCharTableView.set(&tbl);
  qCharTableView.show();
  // runtime loop
  return app.exec();
}

输出:

testQLargeCharTable2.exe 的快照(GIF 动画)

我用全屏窗口(2560×1280)做了同样的测试。性能仍然是可比的。(截取的 GIF 动画太大,无法在此处上传。)

与 VK 的提示相反,我使用了QPixmap. QImage也可以用 a 修改QPainter。还有一个QPainter::drawImage()可用。

一次尝试

我的第一次尝试是在paintEvent()QAbstractScrollArea. 因此,我小心地跳过了视图区域之外的所有行和列。乍一看,性能似乎没有那么糟糕,但在全屏窗口大小的情况下,该方法显示出弱点。在拖动滚动条时,输出明显落后。

testQLargeCharTable1.cc

#include <cassert>
#include <algorithm>
#include <random>
#include <vector>

#include <QtWidgets>

template <typename Value>
class MatrixT {
  private:
    size_t _nCols;
    std::vector<Value> _values; 
  public:
    MatrixT(size_t nRows, size_t nCols, Value value = Value()):
      _nCols((assert(nCols > 0), nCols)), _values(nRows * nCols, value)
    { }

    size_t rows() const { return _values.size() / _nCols; }
    size_t cols() const { return _nCols; }
    
    Value* operator[](size_t i)
    {
      assert(i < rows());
      return &_values[i * _nCols];
    }
    const Value* operator[](size_t i) const
    {
      assert(i < rows());
      return &_values[i * _nCols];
    }
};

using CharTable = MatrixT<char>;

class CharTableView: public QAbstractScrollArea {
  private:
    const CharTable* pTbl = nullptr;
    int wCell;
    int hCell;

  public:
    CharTableView(QWidget* pQParent = nullptr);
    virtual ~CharTableView() = default;

    CharTableView(const CharTableView&) = delete;
    CharTableView& operator=(const CharTableView&) = delete;

    void set(CharTable* pTbl)
    {
      this->pTbl = pTbl;
      updateScrollBars();
      update();
    }

  protected:
    virtual void resizeEvent(QResizeEvent* pQEvent) override;
    virtual void paintEvent(QPaintEvent* pQEvent) override;

  private:
    void updateScrollBars();
};

void CharTableView::resizeEvent(QResizeEvent* pQEvent)
{
  updateScrollBars();
}

void CharTableView::paintEvent(QPaintEvent* pQEvent)
{
  if (!pTbl) return;
  const int xView = horizontalScrollBar()
    ? horizontalScrollBar()->value() : 0;
  const int yView = verticalScrollBar()
    ? verticalScrollBar()->value() : 0;
  const int wView = viewport()->width();
  const int hView = viewport()->height();
  const int iRow0 = yView / hCell;
  const int iCol0 = xView / wCell;
  const int iRowN = std::min((int)pTbl->rows(), (yView + hView) / hCell + 1);
  const int iColN = std::min((int)pTbl->cols(), (xView + wView) / wCell + 1);
  QPainter qPainter(viewport());
  for (int iRow = iRow0; iRow < iRowN; ++iRow) {
    const char*const row = (*pTbl)[iRow];
    const int yCell = iRow * hCell - yView;
    const int yC = yCell + hCell / 2;
    for (int iCol = iCol0; iCol < iColN; ++iCol) {
      const int xCell = iCol * wCell - xView;
      const int xC = xCell + wCell / 2;
      qPainter.drawText(
        QRect(xCell, yCell, wCell, hCell),
        Qt::AlignCenter,
        QString(QChar(row[iCol])));
    }
  }
}

CharTableView::CharTableView(QWidget* pQWidget):
  QAbstractScrollArea(pQWidget)
{
  QFontMetrics qFontMetrics(viewport()->font());
  wCell = 2 * qFontMetrics.averageCharWidth();
  hCell = qFontMetrics.height();
}

void CharTableView::updateScrollBars()
{
  const int w = (int)pTbl->cols() * wCell;
  const int h = (int)pTbl->rows() * hCell;
  const QSize sizeView = viewport()->size();
  QScrollBar*const pQScrBarH = horizontalScrollBar();
  pQScrBarH->setRange(0, w - sizeView.width());
  pQScrBarH->setPageStep(sizeView.width());
  QScrollBar*const pQScrBarV = verticalScrollBar();
  pQScrBarV->setRange(0, h - sizeView.height());
  pQScrBarV->setPageStep(sizeView.height());
}

int main(int argc, char** argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  const size_t n = 10;
  QApplication app(argc, argv);
  // setup data
  const char chars[]
    = "0123456789()[]{}/&%$!'+#?="
      "abcdefghijklmnopqrstuvwxyz"
      "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  CharTable tbl(5000, 5000);
  std::random_device rd;
  std::mt19937 rng(rd()); // seed the generator
  std::uniform_int_distribution<size_t> distr(0, std::size(chars) - 1);
  for (size_t i = 0; i < tbl.rows(); ++i) {
    char*const row = tbl[i];
    for (size_t j = 0; j < tbl.cols(); ++j) {
      row[j] = chars[distr(rng)];
    }
  }
  // setup GUI
  CharTableView qCharTableView;
  qCharTableView.setWindowTitle("Large Character Table View - 1st Attempt");
  qCharTableView.resize(640, 480);
  qCharTableView.set(&tbl);
  qCharTableView.show();
  // runtime loop
  return app.exec();
}

输出:

testQLargeCharTable1.exe 的快照(GIF 动画)


推荐阅读