summaryrefslogtreecommitdiffstats
path: root/tests/manual
diff options
context:
space:
mode:
authorMorten Johan Sørvig <morten.sorvig@qt.io>2020-09-18 15:23:15 +0200
committerMorten Johan Sørvig <morten.sorvig@qt.io>2020-09-18 16:07:20 +0200
commit6e806b5339edbd67dffee1dc7f11a24744328921 (patch)
treece8e3d88b7d9bb4e7d9880a7610de3b449b1d32a /tests/manual
parent30e776fb1b12e233ebf28094af6cb194fd29abc7 (diff)
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>
Diffstat (limited to 'tests/manual')
-rw-r--r--tests/manual/highdpi/pixelgadget/CMakeLists.txt18
-rw-r--r--tests/manual/highdpi/pixelgadget/main.cpp424
-rw-r--r--tests/manual/highdpi/pixelgadget/pixelgadget.pro3
3 files changed, 445 insertions, 0 deletions
diff --git a/tests/manual/highdpi/pixelgadget/CMakeLists.txt b/tests/manual/highdpi/pixelgadget/CMakeLists.txt
new file mode 100644
index 0000000000..e463b36441
--- /dev/null
+++ b/tests/manual/highdpi/pixelgadget/CMakeLists.txt
@@ -0,0 +1,18 @@
+cmake_minimum_required(VERSION 3.14)
+project(pixelgadget LANGUAGES CXX)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6 COMPONENTS Core)
+find_package(Qt6 COMPONENTS Gui)
+find_package(Qt6 COMPONENTS Widgets)
+
+add_qt_gui_executable(pixelgadget
+ main.cpp
+)
+
+target_link_libraries(pixelgadget PUBLIC
+ Qt::Core
+ Qt::Gui
+ Qt::Widgets
+)
+
diff --git a/tests/manual/highdpi/pixelgadget/main.cpp b/tests/manual/highdpi/pixelgadget/main.cpp
new file mode 100644
index 0000000000..2692f22a0e
--- /dev/null
+++ b/tests/manual/highdpi/pixelgadget/main.cpp
@@ -0,0 +1,424 @@
+/****************************************************************************
+ **
+ ** 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();
+}
+
diff --git a/tests/manual/highdpi/pixelgadget/pixelgadget.pro b/tests/manual/highdpi/pixelgadget/pixelgadget.pro
new file mode 100644
index 0000000000..ada4f6bea1
--- /dev/null
+++ b/tests/manual/highdpi/pixelgadget/pixelgadget.pro
@@ -0,0 +1,3 @@
+
+QT += widgets
+SOURCES += main.cpp