问题描述
作为错误报告给 Qt: https://bugreports.qt.io/browse/QTBUG-93475
我通过转换 QPainter 以不同的旋转在不同位置多次重新绘制 Qpixmap
。在某些情况下,Qpixmap
未正确绘制。下面的 GIF 显示了我对包含绿色圆柱体的 Qpixmap
问题的初步发现,请注意 GIF 左侧的渲染行为与预期一样,但存在一个边界,超出该边界渲染是不正确的。 Qpixmap
内容似乎固定到位,边缘的像素似乎在像素图的其余部分涂抹。在 GIF 中,Qpixmap
的背景是洋红色,这是因为 targetRect
使用的 QPainter::drawpixmap()
也用于单独填充像素图下方的矩形,这是因为我想检查目标矩形是否正确计算。
最小可重复示例:
为了简单起见,我只是用洋红色像素填充 Qpixmap
,并带有 1 像素宽的透明边缘,以便涂抹会导致像素图完全消失。它没有显示图像“固定”到位,但它清楚地显示了边界,因为超出它的像素图似乎消失了。
我自己一直在试验这个,我相信这完全是由 QPainter
的旋转引起的。
旋转角度似乎有影响,如果所有像素图都旋转到相同的角度,则边界将从模糊的对角线(其中模糊意味着每个像素图的消失边界不同)变为锐利的90 度角(锐利意味着消失的边界对于所有像素图都是相同的)。
不同角度的范围似乎也有影响,如果随机生成的角度在 10 度的小范围内,那么边界只是一个稍微模糊的直角,带有斜角。随着应用不同的旋转次数,似乎有一个从尖锐的直角到模糊的对角线的进展。
代码
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);
}
我对这个问题的研究
右上角显示与左上角相同的实例,经过平移以使像素图在模糊边界上,最右下角的像素图没有被绘制(或者更准确地说,被绘制为完全透明的像素,由于透明边缘被涂抹在整个图像上)
左下角显示所有像素图都旋转了 0.1 度,这导致了一个清晰的边界,当正方形被平移以与它重叠时,它会将正方形剪裁成一个矩形。
右下角显示了一个小范围的随机角度,在 0.0 和 10.0 之间,这会导致稍微模糊但仍然是垂直边缘,这看起来与左下角相似,但除了锐利的剪裁边缘外,还有一个轻微的渐变效果,因为一些靠近边缘的像素图也没有正确渲染。
我尝试在绘制像素图时关闭 QPainter
中的剪辑,但没有效果。
我曾尝试单独保存 QPainter
转换的副本并在之后将其重新设置,这没有任何效果。
我尝试升级到 Qt 6.0.3(声称已解决了许多图形错误),问题仍然存在。
绝对坐标无关紧要,我可以将所有位置偏移 QPointF(-10000,10000)
,平移到它们并且消失点在窗口中的相对位置相同。
要查看正在运行的错误,请向外滚动,然后在窗口中单击并拖动以将像素图移动到屏幕的右下方,具体取决于您缩放的距离,一些像素图将不再显示绘制。
更新
我还发现使原始 Qpixmap
变大会使问题变得更糟,即边界在更放大的级别上变得明显,并且会发生进一步的渲染畸变。请注意,它们仍在按比例缩小到与以前相同的大小,只是源像素更多。
我将 pixmap_(30,50)
改为 pixmap_(300,500)
上图显示,当平移以将像素图向右下方移动时,它们比以前更早消失(即进一步放大并更多地向左上方放大时),弯曲的箭头表示以超出消失边界的弧线,它们的移动速度似乎比移动时绘制的正确像素图要快。
编辑:仔细检查表明明显的圆周运动不是真实的,像素图出现和消失的顺序只是让它看起来那样。通过以下更新,您可以看到有同心环,其中已消失的像素图重新出现(非常短暂)在正确的位置,但重新出现仅适用于似乎比尺寸更窄的薄窗口的像素图,所以绘制的内容再次出现卡在原位,但显示的部分被剪掉了。
更新
要看到像素图“粘”在边界处,可以将 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();
}
这导致 看到似乎已被剪裁的正方形的右侧边缘。移动它们时,方块似乎卡在原地,而不是边界处的边缘消失,离边界最远的边缘首先消失。
解决方法
这个问题很有趣。据我测试,你的代码看起来不错,我觉得这是一个 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 渲染提示可以轻松解决该问题”