【问题标题】:QPainter::drawImage() glitches in Qt 5.10Qt 5.10 中的 QPainter::drawImage() 故障
【发布时间】: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 回归。

标签: qt qpainter


【解决方案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/stackoverflown/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 = QPixmap{1,1}.toImage().format();
             QImage image{size, 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();
          QPainter{this}.drawImage(QPoint{0, 0}, image);
       }
    };
    
    int main(int argc, char **argv) {
       QApplication app{argc, 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 tabLayout{tabs.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> widgets{{new QLabel{text}, 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
    猜你喜欢
    • 2013-09-28
    • 2015-08-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-30
    相关资源
    最近更新 更多