简体   繁体   English

在Qt中使用大图像调整窗口小部件大小的常见习惯是什么?

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

This may look like premature optimization, but I want to understand what happens on the inside and how this is typically programmed using the Qt library. 这可能看起来像过早的优化,但是我想了解内部发生了什么以及通常如何使用Qt库对其进行编程。

Imagine an application that constantly produces an image that fills the complete window, eg a 3D realtime renderer. 想象一下一个不断产生可填满整个窗口的图像的应用程序,例如3D实时渲染器。 (A photo editor doesn't seem to have this problem since it's meant to preserve the output image size, instead adding scrollbars when the image doesn't fit.) Obviously, the output (buffer) image should get resized when the window gets resized. (照片编辑器似乎不存在此问题,因为它旨在保留输出图像的大小,而在图像不合适时添加滚动条。)显然,当窗口调整大小时,输出(缓冲)图像应调整大小。 。

Now, in Qt, there appears no way to resize a QImage , instead, one has to deallocate the current image and allocate a new one. 现在,在Qt中,似乎无法调整QImage大小,而是必须重新分配当前图像并分配一个新图像。 An image with a resolution of 1280x1024 and 3 8-bit channels take 3.75 Mb. 分辨率为1280x1024和3个8位通道的图像占用3.75 Mb。 Resize events arrive (I've tested this) really often, ie every few pixels of the window movement corner (that's using Qt5 on X11 under 64 bit Linux). 调整大小事件真的很经常到达(我已经测试过),即窗口移动角的每几个像素(在64位Linux下的X11上使用Qt5)。 Hence, the questions: 因此,问题是:

  • On a modern desktop CPU (considering the whole platform, ie the RAM, the bus, and other aspects), is it any significant load to reallocate a few Mb a few times a second? 在现代台式机CPU(考虑整个平台,即RAM,总线和其他方面)上,每秒几次重新分配数Mb是否有很大的负担?
  • 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? 在上述那种平台上,重新分配发生时,它是否发生在高速缓存或RAM中?
  • What is the common idiom in Qt to handle this kind of problem? Qt处理此类问题的常见习语是什么? There is event compression, but even with it applied, events arrive a few times a second (see introduction). 尽管有事件压缩,但是即使应用了事件压缩,事件每秒也会到达几次(请参阅简介)。 Is using a single-shot QTimer with a timeout in the range of 100-200ms to wait for the resize events to stop flowing in a good idea? 是否正在使用超时在100-200ms范围内的单次QTimer来等待调整大小事件停止流动,这是一个好主意吗?

Familiar with the possible answer of "the machine is going to handle it just fine" but if I treated it that way, I'd consider myself to be an illiterate programmer. 熟悉“机器可以很好地处理它”的可能答案,但是如果我这样处理,我会认为自己是个文盲的程序员。 Regardless of how strong CPU's are today, I'd like to understand how this works. 不管今天的CPU有多强大,我都想了解它的工作原理。

On a modern desktop CPU (considering the whole platform, ie the RAM, the bus, and other aspects), is it any significant load to reallocate a few Mb a few times a second? 在现代台式机CPU(考虑整个平台,即RAM,总线和其他方面)上,每秒几次重新分配数Mb是否有很大的负担?

On typical modern allocators, the cost of one allocation is fixed and independent of the allocation size for "small" allocations. 在典型的现代分配器上,一次分配的成本是固定的,并且与“小”分配的分配大小无关。 For larger allocations it is O(N) in allocation size with a very low proportionality constant. 对于较大的分配,分配大小为O(N),比例常数非常低。

A top-level Qt widget is backed by either a QImage buffer, or an OpenGL context if you use a QOpenGLWidget . 如果使用QOpenGLWidget ,则顶级Qt小部件将由QImage缓冲区或OpenGL上下文QOpenGLWidget The resizing of the window-backing buffer is handled automatically by Qt - it already happens and you don't even notice it! 窗口后备缓冲区的大小调整是由Qt自动处理的-它已经发生,您甚至没有注意到! It's not a big deal, performance-wise. 就性能而言,这并不重要。 Modern allocators aren't dumb and are not fragmenting the heap. 现代的分配器并不笨拙,也不会使堆碎片化。

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? 在上述那种平台上,重新分配发生时,它是否发生在高速缓存或RAM中?

That doesn't matter since you're going to overwrite it anyway. 没关系,因为您还是要覆盖它。 Of course it helps if there are cachelines available, and reusing the same address for an object would help with that. 当然,如果有可用的缓存行,它会有所帮助,并且为对象重用相同的地址将对此有所帮助。

What is the common idiom in Qt to handle this kind of problem? Qt处理此类问题的常见习语是什么?

  1. Have a slot that is used to update the data to be shown (eg update an image, or some parameter), and invoke QWidget::update() 具有用于更新要显示的数据的插槽(例如,更新图像或某些参数),并调用QWidget::update()

  2. Render it in paintEvent . paintEvent渲染它。

The rest happens automagically. 其余的自动发生。 It doesn't matter how long paintEvent takes - if it takes long, the responsiveness of the UI will drop, but it won't ever be attempting to display out-of-date data. paintEvent花费多长时间无关紧要-如果花费很长时间,UI的响应性将下降,但它永远不会尝试显示过时的数据。 There is no cumulation of events. 没有事件累积。


The image scaling would be ordinarily handled by QImage::scaled returning a temporary image that you then draw using QPainter::drawImage . 图像缩放通常由QImage::scaled处理,返回一个临时图像,然后使用QPainter::drawImage进行绘制。 Yes, there are allocations there, but these allocations are quick. 是的,那里有分配,但是这些分配很快。

The image producer's event storm is very simple to work around: the producer signals when a new image is available. 图像生成器的事件风暴很容易解决:图像生成器在有新图像可用时发出信号。 The image consumer has a slot that accepts the image, copies it to an internal member, and triggers an update. 图像使用者有一个插槽,用于接收图像,将其复制到内部成员并触发更新。 The update takes effect when the control returns to the event loop, and uses the most recently set image. 当控件返回事件循环并使用最近设置的图像时,更新将生效。 The repaint will proceed when there are no other events to process, thus it doesn't matter how long it takes: it will always show the most recent image. 当没有其他事件需要处理时,重新绘制将继续进行,因此它花费多长时间都无关紧要:它将始终显示最新图像。 It won't ever "lag". 它永远不会“滞后”。

It's easy to verify this behavior. 验证此行为很容易。 In the example below, the ImageSource produces new frames as fast as it can (on the order of 1kHz). 在下面的示例中, ImageSource尽可能快地生成新帧(大约1kHz)。 Each frame displays the current time. 每帧显示当前时间。 The Viewer sleeps in its paintEvent , limiting the screen refresh rate to less than 4Hz: it won't ever be that slow in real life unless you run on a seriously overheated core. Viewer处于其paintEvent ,将屏幕刷新率限制在4Hz以下:除非您在严重过热的内核上运行,否则它在现实生活中永远不会那么慢。 There are at least 25 new frames per each screen refresh. 每个屏幕刷新至少有25个新帧。 Yet the time you see on screen is the current time. 但是,您在屏幕上看到的时间是当前时间。 The out-of-date frames are automatically discarded. 过时的帧将被自动丢弃。

// https://github.com/KubaO/stackoverflown/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"

It usually makes sense to offload image scaling to the GPU. 将图像缩放比例卸载到GPU通常是有意义的。 This answer offers a complete solution to that. 这个答案提供了一个完整的解决方案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM