QPainter 旋转会阻止正确的 QPixmap 渲染

问题描述

作为错误报告给 Qt: https://bugreports.qt.io/browse/QTBUG-93475


我通过转换 QPainter 以不同的旋转在不同位置多次重新绘制 Qpixmap。在某些情况下,Qpixmap 未正确绘制。下面的 GIF 显示了我对包含绿色圆柱体的 Qpixmap 问题的初步发现,请注意 GIF 左侧的渲染行为与预期一样,但存在一个边界,超出该边界渲染是不正确的。 Qpixmap 内容似乎固定到位,边缘的像素似乎在像素图的其余部分涂抹。在 GIF 中,Qpixmap 的背景是洋红色,这是因为 targetRect 使用的 QPainter::drawpixmap() 也用于单独填充像素图下方的矩形,这是因为我想检查目标矩形是否正确计算。

Strange behaviour of pixmap drawing

最小可重复示例:

为了简单起见,我只是用洋红色像素填充 Qpixmap,并带有 1 像素宽的透明边缘,以便涂抹会导致像素图完全消失。它没有显示图像“固定”到位,但它清楚地显示了边界,因为超出它的像素图似乎消失了。

我自己一直在试验这个,我相信这完全是由 QPainter 的旋转引起的。

旋转角度似乎有影响,如果所有像素图都旋转到相同的角度,则边界将从模糊的对角线(其中模糊意味着每个像素图的消失边界不同)变为锐利的90 度角(锐利意味着消失的边界对于所有像素图都是相同的)。

不同角度的范围似乎也有影响,如果随机生成的角度在 10 度的小范围内,那么边界只是一个稍微模糊的直角,带有斜角。随着应用不同的旋转次数,似乎有一个从尖锐的直角到模糊的对角线的进展。

代码

QtTestbed/专业版

QT += widgets

CONfig += c++17
CONfig -= app_bundle

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to kNow how to port your code away from it.
DEFInes += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so,uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFInes += QT_disABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
        MainWindow.cpp \
        main.cpp

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

HEADERS += \
    MainWindow.h

main.cpp:

#include "MainWindow.h"

#include <QApplication>

int main(int argc,char *argv[])
{
    QApplication a(argc,argv);
    MainWindow w;
    w.show();
    return a.exec();
}

主窗口.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QPainter>

#include <random>

class MainWindow : public QWidget {
    Q_OBJECT
public:
    MainWindow();

    void wheelEvent(QWheelEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* /*event*/) override;
    void mousepressEvent(QMouseEvent* event) override;
    void mouseMoveEvent(QMouseEvent* event) override;
    void resizeEvent(QResizeEvent* /*event*/) override;
    void paintEvent(QPaintEvent* event) override;

private:
    struct pixmapLocation {
        QPointF location_;
        qreal rotation_;
        qreal radius_;
    };

    Qpixmap pixmap_;
    std::vector<pixmapLocation> drawLocations_;

    qreal panX_ = 0.0;
    qreal panY_ = 0.0;
    qreal scale_ = 1.0;

    bool dragging_ = false;
    qreal dragX_ = 0.0;
    qreal dragY_ = 0.0;

    QPointF transformWindowToSimCoords(const QPointF& local) const;
    QPointF transformSimToWindowCoords(const QPointF& sim) const;

    static qreal randomNumber(qreal min,qreal max);
};

#endif // MAINWINDOW_H

MainWindow.cpp:

    #include "MainWindow.h"

MainWindow::MainWindow()
    : pixmap_(30,50)
{
    setAutoFillBackground(true);

    constexpr int count = 10000;
    constexpr qreal area = 10000.0;
    constexpr qreal size = 44.0;

    for (int i = 0; i < count; ++i) {
        // qreal rotation = 0.0; // No rotation fixes the issue
        // qreal rotation = 360.0; // No rotation fixes the issue
        // qreal rotation = 180.0; // Mirroring also fixes the issue
        // qreal rotation = 90.0; // The boundary is Now a corner,and has a sharp edge (i.e. all images dissapear at the same point)
        // qreal rotation = 0.1; // The boundary is Now a corner,and has a sharp edge (i.e. all images dissapear at the same point)
        // qreal rotation = randomNumber(0.0,10.0); // The boundary is still a corner,with a bevel,with a fuzzy edge (i.e. not all images dissapear at the same point)
        qreal rotation = randomNumber(0.0,360.0); // The boundary appears to be a diagonal line with a fuzzy edge (i.e. not all images dissapear at the same point)

        drawLocations_.push_back(pixmapLocation{ QPointF(randomNumber(-area,area),randomNumber(-area,area)),rotation,size });
    }

    // Make edges transparent (the middle will be drawn over)
    pixmap_.fill(QColor::fromrgba(0x000000FF));

    /*
     * Fill with magenta almost up to the edge
     *
     * The transparent edge is required to see the effect,the misdrawn pixmaps
     * appear to be a smear of the edge closest to the boundary between proper
     * rendering and misrendering. If the pixmap is a solid block of colour then
     * the effect is masked by the fact that the smeared edge looks the same as
     * the correctly drawn pixmap.
     */
    QPainter p(&pixmap_);
    p.setPen(Qt::nopen);
    constexpr int inset = 1;
    p.fillRect(pixmap_.rect().adjusted(inset,inset,-inset,-inset),Qt::magenta);

    update();
}

void MainWindow::wheelEvent(QWheelEvent* event)
{
    double d = 1.0 + (0.001 * double(event->angleDelta().y()));
    scale_ *= d;
    update();
}

void MainWindow::mouseReleaseEvent(QMouseEvent*)
{
    dragging_ = false;
}

void MainWindow::mousepressEvent(QMouseEvent* event)
{
    dragging_ = true;
    dragX_ = event->pos().x();
    dragY_ = event->pos().y();
}

void MainWindow::mouseMoveEvent(QMouseEvent* event)
{
    if (dragging_) {
        panX_ += ((event->pos().x() - dragX_) / scale_);
        panY_ += ((event->pos().y() - dragY_) / scale_);
        dragX_ = event->pos().x();
        dragY_ = event->pos().y();
        update();
    }
}

void MainWindow::resizeEvent(QResizeEvent*)
{
    update();
}

void MainWindow::paintEvent(QPaintEvent* event)
{
    QPainter paint(this);
    paint.setClipRegion(event->region());
    paint.translate(width() / 2,height() / 2);
    paint.scale(scale_,scale_);
    paint.translate(panX_,panY_);

    for (const pixmapLocation& entity : drawLocations_) {
        paint.save();
        QPointF centre = entity.location_;
        const qreal scale = (entity.radius_ * 2) / std::max(pixmap_.width(),pixmap_.height());

        QRectF targetRect(QPointF(0,0),pixmap_.size() * scale);
        targetRect.translate(centre - QPointF(targetRect.width() / 2,targetRect.height() / 2));

        // Rotate our pixmap
        paint.translate(centre);
        paint.rotate(entity.rotation_);
        paint.translate(-centre);

        // paint.setClipping(false); // This doesn't fix it so it isn't clipping
        paint.drawpixmap(targetRect,pixmap_,QRectF(pixmap_.rect()));
        // paint.setClipping(true); // This doesn't fix it so it isn't clipping
        paint.restore();
    }
}

QPointF MainWindow::transformWindowToSimCoords(const QPointF& local) const
{
    qreal x = local.x();
    qreal y = local.y();
    // Sim is centred on screen
    x -= (width() / 2);
    y -= (height() / 2);
    // Sim is scaled
    x /= scale_;
    y /= scale_;
    // Sim is transformed
    x -= panX_;
    y -= panY_;
    return { x,y };
}

QPointF MainWindow::transformSimToWindowCoords(const QPointF& sim) const
{
    qreal x = sim.x();
    qreal y = sim.y();
    // Sim is transformed
    x += panX_;
    y += panY_;
    // Sim is scaled
    x *= scale_;
    y *= scale_;
    // Sim is centred on screen
    x += (width() / 2);
    y += (height() / 2);
    return { x,y };
}

qreal MainWindow::randomNumber(qreal min,qreal max)
{
    static std::mt19937 entropy = std::mt19937();

    std::uniform_real_distribution<qreal> distribution{ min,max };
    //    distribution.param(typename decltype(distribution)::param_type(min,max));
    return distribution(entropy);
}

我对这个问题的研究

Screen captures of the minimum reproducable code example

左上角显示所有以随机角度正确绘制的像素图

右上角显示左上角相同的实例,经过平移以使像素图在模糊边界上,最右下角的像素图没有被绘制(或者更准确地说,被绘制为完全透明的像素,由于透明边缘被涂抹在整个图像上)

左下角显示所有像素图都旋转了 0.1 度,这导致了一个清晰的边界,当正方形被平移以与它重叠时,它会将正方形剪裁成一个矩形。

右下角显示一个小范围的随机角度,在 0.0 和 10.0 之间,这会导致稍微模糊但仍然是垂直边缘,这看起来与左下角相似,但除了锐利的剪裁边缘外,还有一个轻微的渐变效果,因为一些靠近边缘的像素图也没有正确渲染。

我尝试在绘制像素图时关闭 QPainter 中的剪辑,但没有效果

我曾尝试单独保存 QPainter 转换的副本并在之后将其重新设置,这没有任何效果

我尝试升级到 Qt 6.0.3(声称已解决了许多图形错误),问题仍然存在。

绝对坐标无关紧要,我可以将所有位置偏移 QPointF(-10000,10000),平移到它们并且消失点在窗口中的相对位置相同。

要查看正在运行的错误,请向外滚动,然后在窗口中单击并拖动以将像素图移动到屏幕的右下方,具体取决于您缩放的距离,一些像素图将不再显示绘制。

更新

我还发现使原始 Qpixmap 变大会使问题变得更糟,即边界在更放大的级别上变得明显,并且会发生进一步的渲染畸变。请注意,它们仍在按比例缩小到与以前相同的大小,只是源像素更多。

我将 pixmap_(30,50) 改为 pixmap_(300,500)

Further abberations

上图显示,当平移以将像素图向右下方移动时,它们比以前更早消失(即进一步放大并更多地向左上方放大时),弯曲的箭头表示以超出消失边界的弧线,它们的移动速度似乎比移动时绘制的正确像素图要快。

编辑:仔细检查表明明显的圆周运动不是真实的,像素图出现和消失的顺序只是让它看起来那样。通过以下更新,您可以看到有同心环,其中已消失的像素图重新出现(非常短暂)在正确的位置,但重新出现仅适用于似乎比尺寸更窄的薄窗口的像素图,所以绘制的内容再次出现卡在原位,但显示的部分被剪掉了。

更新

要看到像素图“粘”在边界处,可以将 MainWindow::MainWindow()内容调整为

MainWindow::MainWindow()
    : pixmap_(500,500)
{
    setAutoFillBackground(true);
    
    constexpr int count = 10000;
    constexpr qreal area = 10000.0;
    constexpr qreal size = 44.0;
    
    for (int i = 0; i < count; ++i) {
        qreal rotation = randomNumber(0.0,360.0);
        drawLocations_.push_back(pixmapLocation{ QPointF(randomNumber(-area,size });
    }
    
    // Make edges transparent (the middle will be drawn over)
    pixmap_.fill(QColor::fromrgba(0x000000FF));
    
    /*
     * Fill with magenta almost up to the edge
     *
     * The transparent edge is required to see the effect,the misdrawn pixmaps
     * appear to be a smear of the edge closest to the boundary between proper
     * rendering and misrendering. If the pixmap is a solid block of colour then
     * the effect is masked by the fact that the smeared edge looks the same as
     * the correctly drawn pixmap.
     */
    QPainter p(&pixmap_);
    p.setPen(Qt::nopen);
    constexpr int smallInset = 1;
    const int bigInset = std::min(pixmap_.width(),pixmap_.height()) / 5;
    p.fillRect(pixmap_.rect().adjusted(smallInset,smallInset,-smallInset,-smallInset),Qt::magenta);
    p.fillRect(pixmap_.rect().adjusted(bigInset,bigInset,-bigInset,-bigInset),Qt::green);
    
    update();
}

这导致

See bottom right for partially drawn pixmaps

看到似乎已被剪裁的正方形的右侧边缘。移动它们时,方块似乎卡在原地,而不是边界处的边缘消失,离边界最远的边缘首先消失。

解决方法

这个问题很有趣。据我测试,你的代码看起来不错,我觉得这是一个 Qt 错误,我认为你需要向 Qt 报告:https://bugreports.qt.io/。您应该发布一段代码来说明问题,“更新”编辑中的第二个代码很好:它可以轻松重现问题。也许您还应该发布一个小视频来说明在放大/缩小或使用鼠标移动区域时出现的问题。

我尝试了一些替代方法来希望找到解决方法,但我没有找到:

  • 尝试使用 QImage 而不是 QPixmap,同样的问题
  • 尝试从冻结的 png/qrc 文件中加载像素图,同样的问题
  • 尝试使用 QTransform 进行缩放/平移/旋转,同样的问题
  • 尝试过 Linux 和 Windows 10:观察到相同的问题

注意:

  • 如果您不旋转(评论 paint.rotate(entity.rotation_);),则问题不可见
  • 如果您的像素图是一个简单的单色方块(只需使用 pixmap_.fill(QColor::fromRgba(0x12345600)); 用单一颜色填充您的像素图),则问题不再可见。这是最令人惊讶的,看起来图像中的一个像素被重新用作背景并把事情搞砸了,但如果所有图像像素都相同,则不会导致任何显示问题。

Qt 团队提出的解决方法

“通过在画家上启用 SmoothPixmapTransform 渲染提示可以轻松解决该问题”