What is the common idiom for resizing a widget wit

2019-01-29 07:00发布

问题:

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.

Imagine an application that constantly produces an image that fills the complete window, e.g. a 3D realtime renderer. (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. An image with a resolution of 1280x1024 and 3 8-bit channels take 3.75 Mb. Resize events arrive (I've tested this) really often, i.e. every few pixels of the window movement corner (that's using Qt5 on X11 under 64 bit Linux). Hence, the questions:

  • 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?
  • 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? 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?

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.

回答1:

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?

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.

A top-level Qt widget is backed by either a QImage buffer, or an OpenGL context if you use a QOpenGLWidget. The resizing of the window-backing buffer is handled automatically by Qt - it already happens and you don't even notice it! 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?

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?

  1. Have a slot that is used to update the data to be shown (e.g. update an image, or some parameter), and invoke QWidget::update()

  2. Render it in 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. 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. 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). 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. There are at least 25 new frames per each screen refresh. 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. This answer offers a complete solution to that.