qt5base-lts/tests/manual/highdpi/pixelgadget/main.cpp
Morten Johan Sørvig 6e806b5339 Say hello to PixelGadget
Utility for visualizing and debugging high-dpi rendering
using QPainter, at different (fractional) scale factors.

In addition contains prototype code for mitigating
painting artifacts such as drawing outside the clip
rect when scaling.

Change-Id: I44f39315ad9674790d51413dddf41e3a51043da6
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
2020-09-18 16:07:20 +02:00

425 lines
16 KiB
C++

/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtGui>
#include <QtWidgets>
class PixelGridViewWidget: public QWidget
{
public:
PixelGridViewWidget() {
setMinimumSize(200, 200);
}
QImage sampleImage;
qreal scale = 1;
qreal deviceIndependentPixelSize = 40;
bool drawDipGrid = true;
bool drawDpGrid = false;
QVector<QRectF> dpClipRects;
QVector<QRectF> dipClipRects;
void paintEvent(QPaintEvent *ev) override {
QPainter p(this);
const qreal devicePixelSize = deviceIndependentPixelSize / scale;
QSize widgetSize = geometry().size();
p.setClipRect(ev->rect());
p.fillRect(ev->rect(), QColorConstants::Svg::gray);
// draw device pixel grid and content
for (qreal y = 0; y < widgetSize.height(); y += devicePixelSize) {
for (qreal x = 0; x < widgetSize.width(); x += devicePixelSize) {
QRectF pixelRect = QRect(x,y, qCeil(devicePixelSize), qCeil(devicePixelSize));
QPen pen;
pen.setWidth(1);
pen.setColor(QColor(100, 100, 100, 100));
// draw pixel outline
if (drawDpGrid)
p.drawRect(pixelRect);
// draw content (if in QImage range)
QPoint imagePos(qRound(x / devicePixelSize), qRound(y / devicePixelSize));
if (imagePos.x() < sampleImage.width() && imagePos.y() < sampleImage.height()) {
QColor pixel = sampleImage.pixelColor(imagePos);
p.fillRect(pixelRect, pixel);
}
}
}
// draw device-independent pixel grid
if (drawDipGrid)
for (qreal y = 0; y < widgetSize.height(); y += deviceIndependentPixelSize) {
for (qreal x = 0; x < widgetSize.width(); x += deviceIndependentPixelSize) {
QRectF pixelRect = QRect(x,y, deviceIndependentPixelSize, deviceIndependentPixelSize);
QPen pen;
pen.setWidth(1);
pen.setColor(QColor(250, 100, 100, 255));
p.setPen(pen);
p.drawRect(pixelRect); // pixel outline
}
}
// draw clip rects
for (auto it = dpClipRects.begin(); it != dpClipRects.end(); ++it) {
QRect clipRectRect(it->x() * devicePixelSize, it->y() * devicePixelSize,
it->width() * devicePixelSize, it->height() * devicePixelSize);
QColor yellow(QColorConstants::Svg::yellow);
p.fillRect(clipRectRect, yellow);
}
for (auto it = dipClipRects.begin(); it != dipClipRects.end(); ++it) {
QRect clipRectRect(it->x() * deviceIndependentPixelSize, it->y() * deviceIndependentPixelSize,
it->width() * deviceIndependentPixelSize, it->height() * deviceIndependentPixelSize);
QColor yellow(QColorConstants::Svg::yellow);
p.fillRect(clipRectRect, yellow);
}
}
};
class PixelGadgetWidget : public QWidget
{
public:
std::function<void ()> updateSampleImage;
QVector<QRectF> dpClipRects;
QVector<QRectF> dipClipRects;
PixelGadgetWidget() {
QHBoxLayout *layout = new QHBoxLayout();
PixelGridViewWidget *pixelGridView = new PixelGridViewWidget();
layout->addWidget(pixelGridView, 10);
QVBoxLayout *controlLayout = new QVBoxLayout();
layout->addLayout(controlLayout);
controlLayout->addWidget(new QLabel("<b>Content</b>"));
QComboBox *contentSelect = new QComboBox();
contentSelect->addItem("<empty>");
contentSelect->addItem("lines");
contentSelect->addItem("CE_ShapedFrame (fusion)");
contentSelect->addItem("CC_ScrollBar (fusion)");
connect(contentSelect, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(contentSelect);
QCheckBox *clipping = new QCheckBox("Clipping");
connect(clipping, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(clipping);
controlLayout->addWidget(new QLabel("Start Point"));
QSpinBox *startX = new QSpinBox();
startX->setValue(1);
startX->setMinimum(-1000);
startX->setMaximum(1000);
connect(startX, QOverload<int>::of(&QSpinBox::valueChanged), [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(startX);
QSpinBox *startY = new QSpinBox();
startY->setValue(1);
startX->setMinimum(-1000);
startX->setMaximum(1000);
connect(startY, QOverload<int>::of(&QSpinBox::valueChanged), [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(startY);
controlLayout->addWidget(new QLabel("<b>Paint Settings</b>"));
controlLayout->addWidget(new QLabel("Scale"));
QCheckBox *quarterIncrement = new QCheckBox("25% Scale Increments");
quarterIncrement->setChecked(true);
controlLayout->addWidget(quarterIncrement);
QSpinBox *scale = new QSpinBox();
scale->setSuffix("%");
scale->setSingleStep(25);
connect(quarterIncrement, &QCheckBox::stateChanged, [scale](int val){
scale->setSingleStep(val > 0 ? 25 : 1);
});
scale->setMinimum(100);
scale->setMaximum(200);
scale->setValue(100); // 1x
connect(scale, QOverload<int>::of(&QSpinBox::valueChanged), [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(scale);
QCheckBox *antialising = new QCheckBox("Antialiasing");
connect(antialising, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(antialising);
QCheckBox *offset = new QCheckBox("+0.5 Offset");
connect(offset, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(offset);
QCheckBox *dpAllign = new QCheckBox("Device Pixel Aligned Painting");
connect(dpAllign, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(dpAllign);
controlLayout->addWidget(new QLabel("<b>Visualization Settings</b>"));
QSpinBox *pixelSize = new QSpinBox();
pixelSize->setValue(40);
pixelSize->setMinimum(10);
connect(pixelSize, QOverload<int>::of(&QSpinBox::valueChanged), [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(pixelSize);
QCheckBox *dipGrid = new QCheckBox("Device Independent Pixel Grid");
dipGrid->setChecked(true);
connect(dipGrid, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(dipGrid);
QCheckBox *dpGrid = new QCheckBox("Device Pixel Grid");
dpGrid->setChecked(false);
connect(dpGrid, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(dpGrid);
QCheckBox *dipClipRects = new QCheckBox("Device Independent Clip Rects");
dipClipRects->setChecked(false);
connect(dipClipRects, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(dipClipRects);
QCheckBox *dpClipRects = new QCheckBox("Device Clip Rects");
dpClipRects->setChecked(false);
connect(dpClipRects, &QCheckBox::stateChanged, [this](int){ this->updateSampleImage(); });
controlLayout->addWidget(dpClipRects);
controlLayout->addStretch(10);
setLayout(layout);
auto setClip = [this](QPainter *p, QRect clipRect, bool enableClip, qreal currentScale, QPoint currentStart) {
if (enableClip) {
// Set the clip rect without the sub-pixel offset in order to
// get the same device clip rect as the no-offset case. (This
// simulates the case where painting code does not control clip
// rects which have already been set.)
p->setClipRect(clipRect);
// Print/record clip debug info - device and device independent rects.
p->save();
p->translate(-currentStart.x(), -currentStart.y());
QRegion dipClip = p->clipRegion();
p->scale(qreal(1) / currentScale, qreal(1) / currentScale);
QRegion dpClip = p->clipRegion();
this->dipClipRects.append(dipClip.boundingRect());
this->dpClipRects.append(dpClip.boundingRect());
p->restore();
} else {
// p->setClipEnabled(false);
}
};
auto initPainter_1 = [](QPainter *p, QPoint start, qreal scale) {
p->scale(scale, scale);
p->translate(start);
};
auto initPainter_2 = [](QPainter *p, bool antialias, QPointF offset) {
p->setRenderHint(QPainter::Antialiasing, antialias);
p->translate(offset); // sub-(device-independent) pixel offset
};
auto drawLines = [=](QPainter *p, QPoint start, qreal scale, bool antialias, bool clip, QPointF offset) {
QPen pen;
pen.setColor(QColor(50, 50, 250));
// 1-width line
p->save(); {
QPoint drawPoint = start + QPoint(0, 0);
pen.setWidth(1);
p->setPen(pen);
initPainter_1(p, drawPoint, scale);
setClip(p, QRect(0, 0, 1, 7), clip, scale, drawPoint);
initPainter_2(p, antialias, offset);
p->drawLine(0, 0, 0, 6);
} p->restore();
// 2-width line
p->save(); {
QPoint drawPoint = start + QPoint(4, 0);
pen.setWidth(2);
p->setPen(pen);
initPainter_1(p, drawPoint, scale);
setClip(p, QRect(-1, -1, 2, 9), clip, scale, drawPoint);
initPainter_2(p, antialias, offset);
p->drawLine(0, 0, 0, 7);
} p->restore();
// cosmetic line
p->save(); {
QPoint drawPoint = start + QPoint(8, 0);
pen.setWidth(0);
p->setPen(pen);
initPainter_1(p, drawPoint, scale);
setClip(p, QRect(0, 0, 1, 7), clip, scale, drawPoint);
initPainter_2(p, antialias, offset);
p->drawLine(0, 0, 0, 7);
} p->restore();
};
auto drawCE_ShapedFrame = [=](QPainter *p, QPoint start, qreal scale, bool antialias, bool clip, QPointF offset) {
QRect frameRect(0, 0, 8, 8);
QStyleOptionFrame opt;
opt.initFrom(this);
opt.rect = frameRect;
opt.frameShape = QFrame::StyledPanel;
initPainter_1(p, start, scale);
setClip(p, frameRect, clip, scale, start);
initPainter_2(p, antialias, offset);
QStyle *style = QStyleFactory::create("fusion");
style->drawControl(QStyle::CE_ShapedFrame, &opt, p, nullptr);
};
auto drawCC_ScrollBar = [=](QPainter *p, QPoint start, qreal scale, bool antialias, bool clip, QPointF offset) {
QRect scrollBarRect(0, 0, 100, 18);
QStyleOptionSlider opt;
opt.initFrom(this);
// opt.palette = QPalette(QColor(200, 200, 200)); // force light mode
opt.rect = scrollBarRect;
opt.subControls = QStyle::SC_All;
opt.orientation = Qt::Horizontal;
opt.minimum = 0;
opt.maximum = 10;
opt.sliderPosition = 0;
opt.sliderValue = 0;
opt.singleStep = 1;
opt.pageStep = 5;
opt.upsideDown = false;
opt.state |= QStyle::State_Horizontal;
//opt.state |= QStyle::State_On;
initPainter_1(p, start, scale);
setClip(p, scrollBarRect.adjusted(0, 0, 0, 2), clip, scale, start);
initPainter_2(p, antialias, offset);
QStyle *style = QStyleFactory::create("fusion");
style->drawComplexControl(QStyle::CC_ScrollBar, &opt, p, nullptr);
};
updateSampleImage = [=]() {
bool clip = clipping->isChecked();
QPoint start(startX->value(), startY->value());
qreal _scale = qreal(scale->value()) / 100.0;
bool antialias = antialising->isChecked();
// Set up sub-pixel offset
QPointF _offset(0, 0);
if (offset->isChecked()) {
_offset += QPointF(0.5, 0.5);
}
if (dpAllign->isChecked()) {
// Align to the closest device pixel, in accordance
QPointF dpStart = QPointF(start) * _scale;
QPointF dpStartRounded(qCeil(dpStart.x()), qCeil(dpStart.y())); // down/right
// QPointF dpStartRounded(qRound(dpStart.x()), qRound(dpStart.y())); // nearest
QPointF offset = dpStartRounded - dpStart;
/*
qDebug() << "";
qDebug() << "start" << start;
qDebug() << "dpStart" << dpStart;
qDebug() << "dpStartRounded" << dpStartRounded;
qDebug() << "offset" << offset;
*/
_offset += offset;
}
// qDebug() << "offset" << _offset;
QImage img(200, 200, QImage::Format_ARGB32_Premultiplied);
img.fill(QColorConstants::Svg::gray);
QPainter painter(&img);
// Prepare for recording clip rects during paint
this->dipClipRects.clear();
this->dpClipRects.clear();
// paint currently selected content
switch (contentSelect->currentIndex()) {
case 0:
break;
case 1:
drawLines(&painter, start, _scale, antialias, clip, _offset);
break;
case 2:
drawCE_ShapedFrame(&painter, start, _scale, antialias, clip, _offset);
break;
case 3:
drawCC_ScrollBar(&painter, start, _scale, antialias, clip, _offset);
break;
};
img.save("sampleimage.png");
// Update the pixel grid view
pixelGridView->sampleImage = img;
pixelGridView->scale = _scale;
pixelGridView->deviceIndependentPixelSize = pixelSize->value();
pixelGridView->drawDipGrid = dipGrid->isChecked();
pixelGridView->drawDpGrid = dpGrid->isChecked();
pixelGridView->dipClipRects.clear();
if (dipClipRects->isChecked())
pixelGridView->dipClipRects = this->dipClipRects;
pixelGridView->dpClipRects.clear();
if (dpClipRects->isChecked())
pixelGridView->dpClipRects = this->dpClipRects;
pixelGridView->update();
};
updateSampleImage();
}
};
int main(int argc, char **argv) {
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
QApplication app(argc, argv);
PixelGadgetWidget pixelGadget;
pixelGadget.resize(400, 300);
pixelGadget.show();
return app.exec();
}