Reported to Qt as a bug: https://bugreports.qt.io/browse/QTBUG-93475
I am re-drawing a QPixmap
multiple times in different locations, with differnt rotations by transforming the QPainter. In certain situations the QPixmap
is not drawing correctly. The GIF below shows my initial discovery of this issue with a QPixmap
containing a green cylinder, notice how the rendering behaves as expected to the left of the GIF, but there is a boundary beyond which the rendering is incorrect. The QPixmap
content appears to stick in place, and the pixels at the edge appear to smear out accross the rest of the pixmap. In the GIF there is a magenta background to the QPixmap
, this because the targetRect
used by QPainter::drawPixmap()
is also beuing used to seperately fill a rectangle underneath the pixmap, this was because I wanted to check that the target rect was being computed correctly.
To keep things simple I am simply filling the QPixmap
with magenta pixels, with a 1 pixel wide transparent edge so that the smearing causes the pixmaps to dissapear completely. It doesn't show the image "sticking" in place but it clearly shows the boundary as beyond it the pixmaps seem to dissapear.
I have been experimenting with this myself and I believe this to be entirely caused by the rotating of the QPainter
.
The angle of rotation seems to have an effect, if all of the pixmaps are rotated to the same angle then the boundary changes from a fuzzy diagonal line (where fuzzy means the boundary for dissapearing is different for each pixmap) to a sharp 90 degree corner (where sharp means that the boundary for dissapearing is the same for all pixmaps).
The range of different angles also seems to play a part, if the randomly generated angles are in a small 10 degree range, then the boundary is just a slightly fuzzier right angle, with a bevelled corner. There seems to be a progression from sharp right angle to fuzzy diagonal line as the number of different rotations is applied.
QtTestBed/pro:
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();
}
MainWindow.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);
}
Top left above shows all pixmaps drawing correctly, at random angles
Top right above shows the same instance as top left, panned so that the pixmaps are over the fuzzy boundary, with the bottom rightmost pixmaps not being drawn (or to be more accurate, are being drawn as entirely transparent pixels, due to the transparent edge being smeared over the entire image)
Bottom left shows all pixmaps being rotated by 0.1 degree, this leads to a sharp boundary, which when the square is panned to overlap it, clips the square to a rectangle.
Bottom right shows a small range of random angles, between 0.0 and 10.0, this leads to a slightly fuzzier, but still vertical edge, this looks similar to bottom left, but as well as the sharp clipped edge, there is also a slight gradient effect as some of the pixmaps closer to the edge have also not be rendered correctly.
I have tried turning clipping off in the QPainter
when drawing the pixmaps, this has had no effect.
I have tried seperately saving a copy of the QPainter
s transform and setting it back afterwards, this had no effect.
I have tried upgrading to Qt 6.0.3 (which claimed to have solved a number of graphical bugs), issue still present.
The absolute coordinates don't matter, I can offset all of the locations by QPointF(-10000, 10000)
, pan over to them and the dissapearing point is in the same relative position in the window.
To see the bug in action, scroll out, then click and drag in the window to move the pixmaps to the lower right of the screen, depending on how far out you have zoomed, a number of the pixmaps will no longer be drawn.
I have also discovered that making the original QPixmap
larger makes the issue worse, i.e. the boundary becomes apparent at more zoomed in levels, plus further rendering abberations occur. Note that they are still being scaled down to the same size as before, there are just more source pixels.
I changed pixmap_(30, 50)
to pixmap_(300, 500)
The image above shows that when panning to move the pixmaps towards the bottom right, they dissapear sooner than before (i.e. while zoomed further in and more towards the top left), the curved arrow indicates the movement of the pixmaps drawn in an arc beyond the dissapearance boundary, they seem to be moving faster than the correct pixmaps are drawn as they are moved.
EDIT: Closer inspection shows that the apparent circular motion is not real, the order in which pixmaps are appearing and dissapearing just made it look that way. With the below update, you can see that there are concentric rings where the pixmaps that have dissapeard re-appear (very briefly) in the correct place, but the re-appeaance is only for a thin window that seems to be narrower than the size of the pixmap, so the content that is drawn appears again to be stuck in place, but the part shown is clipped.
To see the pixmaps "stick" at the boundary, you can adjust the contents of MainWindow::MainWindow()
to
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, 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 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();
}
Which results in see right hand edge for squares that appear to have been clipped. When moving them around, the square seems to get stuck in place and instead of the edge at the boundary dissapearing, the edge furthest from the boundary dissapears first.
This issue is very interesting. As far as I could test, your code looks good, I feel like this is a Qt bug, and I think you need to report it to Qt: https://bugreports.qt.io/. You should post a single piece of code to illustrate the issue, your second one from your "Update" edit is good: it makes it easy to reproduce the issu. Maybe you should also post a small video to illustrate how things are getting wrong when you zoom in/out or move the area with the mouse.
I tried some alternatives to hopefully find a workaround, but I found none:
QImage
rather than a QPixmap
, same issueQTransform
to play with scale/translation/rotation, same issueNote that:
paint.rotate(entity.rotation_);
), the issue is not visiblepixmap_.fill(QColor::fromRgba(0x12345600));
), the issue is not visible anymore. That's the most surprising, looks like a pixel from the image is being reused as background and messes things up but if all the image pixels are the same it does not lead to any display issue.Workaround proposed by the Qt team
"The issue can easily be worked around by enabling the SmoothPixmapTransform render hint on the painter"
User contributions licensed under CC BY-SA 3.0