在 Qt 中调整带有大图像的小部件的大小的常用习惯是什么?

What is the common idiom for resizing a widget with a large image in Qt?

这可能看起来像是过早的优化,但我想了解内部发生了什么,以及这通常是如何使用 Qt 库进行编程的。

想象一个不断生成填满整个 window 图像的应用程序,例如一个 3D 实时渲染器。 (照片编辑器似乎没有这个问题,因为它是为了保留输出图像的大小,而不是在图像不适合时添加滚动条。)显然,输出(缓冲区)图像应该在 window 调整大小。

现在,在 Qt 中,似乎无法调整 QImage 的大小,相反,必须取消分配当前图像并分配一个新图像。分辨率为 1280x1024 和 3 个 8 位通道的图像占用 3.75 Mb。调整大小事件(我已经测试过)经常到达,即 window 移动角的每隔几个像素(即在 64 位 Linux 下的 X11 上使用 Qt5)。因此,问题:

熟悉"the machine is going to handle it just fine"的可能答案,但如果我那样对待它,我会认为自己是一个文盲程序员。不管今天 CPU 有多强大,我都想了解它是如何工作的。

On a modern desktop CPU (considering the whole platform, i.e the RAM, the bus, and other aspects), is it any significant load to reallocate a few Mb a few times a second?

在典型的现代分配器上,一次分配的成本是固定的,并且与 "small" 分配的分配大小无关。对于较大的分配,分配大小为 O(N),比例常数非常低。

顶级 Qt 小部件由 QImage 缓冲区支持,如果您使用 QOpenGLWidget,则由 OpenGL 上下文支持。 window-backing buffer 的大小调整由 Qt 自动处理——它已经发生了,你甚至都没有注意到它!从性能方面来说,这没什么大不了的。现代分配器并不愚蠢,也不会碎片化堆。

On the kind of platform described above, When the reallocation occurs, does it take place in the cache or in RAM, if possible to tell?

没关系,因为无论如何您都会覆盖它。当然,如果有可用的高速缓存行,它会有所帮助,并且为对象重复使用相同的地址会有所帮助。

What is the common idiom in Qt to handle this kind of problem?

  1. 有一个插槽用于更新要显示的数据(例如更新图像或某些参数),并调用QWidget::update()

  2. paintEvent 中渲染它。

其余的自动发生。 paintEvent 花费多长时间并不重要 - 如果花费很长时间,UI 的响应能力将会下降,但它永远不会尝试显示过时的数据。事件没有累积。


图像缩放通常由 QImage::scaled 返回一个临时图像,然后您使用 QPainter::drawImage 绘制。是的,那里有分配,但是这些分配很快。

图像制作者的事件风暴非常容易解决:制作者在新图像可用时发出信号。图像消费者有一个接受图像的插槽,将其复制到内部成员,并触发更新。更新在控件returns进入事件循环时生效,并使用最近设置的图像。当没有其他事件要处理时,重绘将继续进行,因此花费多长时间并不重要:它始终会显示最新的图像。它永远不会 "lag".

验证此行为很容易。在下面的示例中,ImageSource 尽可能快地生成新帧(大约 1kHz)。每帧显示当前时间。 Viewer 在其 paintEvent 中休眠,将屏幕刷新率限制在 4Hz 以下:除非您 运行 在严重过热的核心上,否则它在现实生活中永远不会那么慢。每次屏幕刷新至少有 25 个新帧。然而,您在屏幕上看到的时间是当前时间。过时的帧会被自动丢弃。

// https://github.com/KubaO/Whosebugn/tree/master/questions/update-storm-image-40111359
#include <QtWidgets>

class ImageSource : public QObject {
  Q_OBJECT
  QImage m_frame{640, 480, QImage::Format_ARGB32_Premultiplied};
  QBasicTimer m_timer;
  double m_period{};
  void timerEvent(QTimerEvent * event) override {
    if (event->timerId() != m_timer.timerId()) return;
    m_frame.fill(Qt::blue);
    QElapsedTimer t;
    t.start();
    QPainter p{&m_frame};
    p.setFont({"Helvetica", 48});
    p.setPen(Qt::white);
    p.drawText(m_frame.rect(), Qt::AlignCenter,
               QStringLiteral("Hello,\nWorld!\n%1").arg(
                 QTime::currentTime().toString(QStringLiteral("hh:mm:ss.zzz"))));
    auto const alpha = 0.001;
    m_period = (1.-alpha)*m_period + alpha*(t.nsecsElapsed()*1E-9);
    emit newFrame(m_frame, m_period);
  }
public:
  ImageSource() {
    m_timer.start(0, this);
  }
  Q_SIGNAL void newFrame(const QImage &, double period);
};

class Viewer : public QWidget {
  Q_OBJECT
  double m_framePeriod;
  QImage m_image;
  QImage m_scaledImage;
  void paintEvent(QPaintEvent *) override {
    qDebug() << "Waiting events" << d_ptr->postedEvents;
    QPainter p{this};
    if (m_image.isNull()) return;
    if (m_scaledImage.isNull() || m_scaledImage.size() != size())
      m_scaledImage = m_image.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    p.drawImage(0, 0, m_scaledImage);
    p.drawText(rect(), Qt::AlignTop | Qt::AlignLeft, QStringLiteral("%1 FPS").arg(1./m_framePeriod));
    if (true) QThread::msleep(250);
  }
public:
  Q_SLOT void setImage(const QImage & image, double period) {
    Q_ASSERT(QThread::currentThread() == thread());
    m_image = image;
    m_scaledImage = {};
    m_framePeriod = period;
    update();
  }
};

class Thread final : public QThread { public: ~Thread() { quit(); wait(); } };

int main(int argc, char ** argv) {
  QApplication app{argc, argv};
  Viewer viewer;
  viewer.setMinimumSize(200, 200);
  ImageSource source;
  Thread thread;
  QObject::connect(&source, &ImageSource::newFrame, &viewer, &Viewer::setImage);
  QObject::connect(&thread, &QThread::destroyed, [&]{ source.moveToThread(app.thread()); });
  source.moveToThread(&thread);
  thread.start();
  viewer.show();
  return app.exec();
}
#include "main.moc"

将图像缩放卸载到 GPU 通常是有意义的。 提供了一个完整的解决方案。