在 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)。因此,问题:
- 在现代桌面上 CPU(考虑到整个平台,即 RAM、总线和其他方面),每秒重新分配几次 Mb 是否会产生重大负载?
- 在上述那种平台上,当重新分配发生时,它是发生在缓存中还是在 RAM 中,如果可以的话?
- Qt 中处理此类问题的常用习惯用法是什么?有事件压缩,但即使应用它,事件也会每秒到达几次(请参阅介绍)。使用超时范围为 100-200 毫秒的单次
QTimer
来等待调整大小事件停止流动是个好主意吗?
熟悉"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?
有一个插槽用于更新要显示的数据(例如更新图像或某些参数),并调用QWidget::update()
在 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 通常是有意义的。 提供了一个完整的解决方案。
这可能看起来像是过早的优化,但我想了解内部发生了什么,以及这通常是如何使用 Qt 库进行编程的。
想象一个不断生成填满整个 window 图像的应用程序,例如一个 3D 实时渲染器。 (照片编辑器似乎没有这个问题,因为它是为了保留输出图像的大小,而不是在图像不适合时添加滚动条。)显然,输出(缓冲区)图像应该在 window 调整大小。
现在,在 Qt 中,似乎无法调整 QImage
的大小,相反,必须取消分配当前图像并分配一个新图像。分辨率为 1280x1024 和 3 个 8 位通道的图像占用 3.75 Mb。调整大小事件(我已经测试过)经常到达,即 window 移动角的每隔几个像素(即在 64 位 Linux 下的 X11 上使用 Qt5)。因此,问题:
- 在现代桌面上 CPU(考虑到整个平台,即 RAM、总线和其他方面),每秒重新分配几次 Mb 是否会产生重大负载?
- 在上述那种平台上,当重新分配发生时,它是发生在缓存中还是在 RAM 中,如果可以的话?
- Qt 中处理此类问题的常用习惯用法是什么?有事件压缩,但即使应用它,事件也会每秒到达几次(请参阅介绍)。使用超时范围为 100-200 毫秒的单次
QTimer
来等待调整大小事件停止流动是个好主意吗?
熟悉"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?
有一个插槽用于更新要显示的数据(例如更新图像或某些参数),并调用
QWidget::update()
在
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 通常是有意义的。