Qt 5.10 中的 QPainter::drawImage() 故障

Posted

技术标签:

【中文标题】Qt 5.10 中的 QPainter::drawImage() 故障【英文标题】:QPainter::drawImage() glitches in Qt 5.10 【发布时间】:2018-04-19 21:19:06 【问题描述】:

我的应用程序将 3D 渲染绘制到离线 QImage(由于历史原因没有使用 OpenGL),然后在这样的小部件中绘制 QImage

void Render3dModel::paintEvent(QPaintEvent *event) 
    // Update model and render new image
    m_model.calculate();
    QImage image = m_model.getImage();

    // Draw image in widget
    QPainter painter(this);
    painter.drawImage(QPointF(0, 0), image);

使用 QWidget::update() 根据鼠标移动重新绘制图像:

void Render3dModel::mouseMoveEvent(QMouseEvent *event) 
    m_model.rotate(event);
    update();

这种方法在 Qt 5.6 和 Qt 5.9 中运行迅速且完美无缺。然而,在 Qt 5.10 中重建完全相同的代码会导致奇怪的故障。两个帧的一部分同时可见。这不是临时效果(例如延迟绘图),因为如果我停止移动鼠标,它实际上会一直显示两帧部分。下面是一个例子(为了清晰起见,使用交替的灰色和白色背景来表示偶数/奇数帧):

我尝试在QImage 环形缓冲区中使用image.copy() 创建深层副本,以查看这是否是QImage 的销毁/重用和一些延迟/延迟绘制之间的某种竞争条件,但这无济于事。

显然QPainter 的机制在 Qt 5.9 和 Qt 5.10 之间发生了变化,但我在文档中找不到任何线索。

请指教

更新

在玩了几个窗口大小之后,我得出的结论是,故障以某种方式对应于左上角与“截面尺寸”标签的“S”对齐的相同大小的矩形。但是,正如您在屏幕截图中看到的那样,具有白色背景的框架将圆锥的那部分绘制在正确的位置。所以图像不是移位,而是以某种方式剪辑?!

我已经尝试添加

painter.setClipping(false);
painter.setClipRect(QRect(0, 0, size().width(), size().height()));

但这没有任何效果。我的Render3dModel 偶尔会从它的父母那里得到paintEvent 吗?

更新 2

在下面 Kuba Ober 的最小示例中添加 mouseMoveEvent 处理程序,可以在 MacBook(OS X 10.12.6,Intel HD Graphics 515)上重现该问题:

这可能是 Qt 5.10 中的回归,因为我无法在 Qt 5.9 中重现它吗?我应该提交错误报告吗?

更新 3

这似乎确实是 OS X 上 Qt 5.10 中的回归。已提交错误报告 https://bugreports.qt.io/browse/QTBUG-67998

【问题讨论】:

不,Render3dModel 没有从它的父母那里获得绘画事件。如果是这样,那么您已经破坏了进程的执行状态。 Qt 不做这些事情。 我想我知道您的问题可能是什么:您在某处使用了错误的尺寸。检查图像 Render3dModel 的大小是否正确! 它只需重复发出update()即可重现,无需鼠标事件。这确实是一个 Qt 5.10 回归。 【参考方案1】:

我测试了@KubaOber 提供的最小示例,按照 OP 的建议添加了 mouseMoveEvent() 处理程序,但我无法在我的平台上重现该问题(Qt 5.10. 1 - Linux)。

在我看来,这可能是一个特定于平台的问题。 Qt 5.9 和 Qt 5.10 之间的 cocoa 平台插件有很多不同之处,特别是关于 QCocoaBackingStore 及其 flush() 方法 (changelog here)。

如果最小、完整的示例很好地显示了问题,我建议将其报告给 Qt 的错误跟踪器https://bugreports.qt.io。

【讨论】:

【参考方案2】:

单线程代码中不能有竞争条件。没有延迟/延迟绘画之类的东西。当paintEvent 返回时,该小部件的绘制就完成了。

所见即所得。 getImage() 方法可能会返回垃圾 - 可能是尺寸过小的图像 - 然后你绘制那个垃圾。将getImage() 替换为简单的交替填充,您会发现它工作正常:

// https://github.com/KubaO/***n/tree/master/questions/glitchy-paint-49930405
#include <QtWidgets>
#include <array>

class Render3dModel : public QWidget 
   struct Model 
      mutable int counter = ;
      QSize size;
      QFont font"Helvetica", 48;
      QImage getImage() const 
         static auto const format = QPixmap1,1.toImage().format();
         QImage imagesize, format;
         image.fill((counter & 1) ? Qt::blue : Qt::yellow);
         QPainter p(&image);
         p.setFont(font);
         p.setPen((counter & 1) ? Qt::yellow : Qt::blue);
         p.drawText(image.rect(), QString::number(counter));
         counter++;
         return image;
      
    m_model;
   void paintEvent(QPaintEvent *) override 
      m_model.size = size();
      auto image = m_model.getImage();
      QPainterthis.drawImage(QPoint0, 0, image);
   
;
int main(int argc, char **argv) 
   QApplication appargc, argv;
   QWidget win;
   QVBoxLayout topLayout&win;
   QTabWidget tabs;
   topLayout.addWidget(&tabs);
   // Tabs
   for (auto text :  "Shape", "Dimensions", "Layout") tabs.addTab(new QWidget, text);
   tabs.setCurrentIndex(1);
   QHBoxLayout tabLayouttabs.currentWidget();
   QGroupBox dims"Section Dimensions", model"3D Model";
   QGridLayout dimsLayout&dims, modelLayout&model;
   for (auto w : &dims, &model) tabLayout.addWidget(w);
   // Section Dimensions
   for (auto text : "Diameter 1", "Diameter 2", "Length") 
      auto row = dimsLayout.rowCount();
      std::array<QWidget*, 3> widgetsnew QLabeltext, new QDoubleSpinBox, new QLabel"inch";
      for (auto *w : widgets)
         dimsLayout.addWidget(w, row, dimsLayout.count() % widgets.size());
   
   tabLayout.setAlignment(&dims, Qt::AlignLeft | Qt::AlignTop);
   dims.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
   // Model
   Render3dModel render;
   modelLayout.addWidget(&render);
   model.setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
   model.setMinimumSize(250, 250);
   win.show();
   return app.exec();

【讨论】:

感谢您的建议。不幸的是,尝试您的代码同时给出蓝色和黄色矩形,类似于我的示例。所以我们可以排除m_model.getImage()。 Qt 或我使用它的方式似乎确实存在问题。 @JasonSmith 太棒了。我只能告诉你的是,它可以在一个独立的例子中工作。您的代码中还有一些其他的东西把事情搞砸了,可能是写入了不应该写入的内存,等等。如果您使用多线程,请记住给定的 QImage 实例一次只能从一个线程使用!将它从一个线程传递到另一个线程是有效的,但只能作为(浅)副本。 Qt 的隐式共享是线程安全的,因此只要两个线程在QImage 的单独实例上操作,即使一个是从另一个分配的,它也能正常工作。但是,任何访问都必须通过 QImage 实例。如果您持有指向图像中位的任何指针,则如果图像已对其进行复制,则无法对它们进行操作。在将第二张图像传递到其他地方之前,您必须分离并因此深拷贝第二张图像。这是隐式共享的一个常见警告:对象本身必须知道写入,并且会阻止第一个写入,直到它可以分离(如果共享,则为深拷贝)。 感谢您的精心回复。请将 mouseMoveEvent() 处理程序添加到您的最小示例中,您将看到问题重现!我已经用相应的屏幕截图更新了我的问题。【参考方案3】:

这是你的错误:

void Render3dModel::paintEvent(QPaintEvent *event) 
    // Update model and render new image
    m_model.calculate();
    ...

您不应将模型更新与绘制事件联系起来。绘制事件应该绘制模型的当前状态,并且您可能会获得多个后续和/或部分绘制事件,例如露出窗户的一部分。

您在应用程序中看到的是部分公开事件,在这种情况下是 Qt 中的错误导致的,但通常您可能会收到部分公开事件并且应该准备好处理它们。

【讨论】:

感谢您对此进行调查。与这个最小示例不同,实际应用程序在mouseMoveEvent 中进行模型更新。但是即使我们通过将counter++ 移动到mouseMoveEvent() 处理程序来更新最小示例,问题仍然存在。所以接收多个后续和/或部分事件似乎不是问题。 对,修复代码后,您现在可以点击bugreports.qt.io/browse/QTBUG-68023

以上是关于Qt 5.10 中的 QPainter::drawImage() 故障的主要内容,如果未能解决你的问题,请参考以下文章

使用 Python (PyQT 5.10) 在 QT 5.10 中加载自己的字体

如何在Windows XP上部署Qt 5.10 Quick 2应用程序?

Qt 5.2 和 5.10 上 OpenGL 的差异

Qt 5.10 QGraphicsView 无法将 QGraphicsScene 缩放到全屏

moc在Docker容器中使用Qt 5.10的“未定义接口”失败

如何在 windows xp 上部署 Qt 5.10 Quick 2 应用程序?