QPainter: рисует только видимую область увеличенного изображения
У меня есть пользовательский QQuickPaintedItem, который рисует все, что пользователь нарисовал на нем мышью. До сих пор реализация была очень простой, просто рисование всего изображения, даже при увеличении. Я заметил, что FPS очень медленно работает при увеличении и панорамировании изображения, поэтому я решил постепенно улучшить производительность рисования.
Текущий шаг, на котором я сейчас работаю - это рисование только подмножества видимого изображения. Для этого я использую эту перегрузкуQPainter::drawImage()
, Вот наименьший возможный пример, который позволяет масштабирование и панорамирование (важная часть recalculateStuff()
):
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>
#include <QQuickItem>
#include <QImage>
#include <QQuickPaintedItem>
#include <QPainter>
#include <QtMath>
class ImageCanvas : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(QPoint offset READ offset WRITE setOffset NOTIFY offsetChanged)
Q_PROPERTY(int zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(QRect sourceRect READ sourceRect NOTIFY sourceRectChanged)
Q_PROPERTY(QRect targetRect READ targetRect NOTIFY targetRectChanged)
public:
ImageCanvas() :
mZoom(1)
{
// Construct a test image from coloured squares.
mImage = QImage(500, 500, QImage::Format_ARGB32);
QPainter painter(&mImage);
for (int y = 0; y < mImage.width(); y += 50) {
for (int x = 0; x < mImage.width(); x += 50) {
const QColor colour((x / 500.0) * 255, (y / 500.0) * 255, 0);
painter.fillRect(x, y, 50, 50, colour);
}
}
recalculateStuff();
}
QPoint offset() const {
return mOffset;
}
void setOffset(const QPoint &offset) {
mOffset = offset;
recalculateStuff();
emit offsetChanged();
}
int zoom() const {
return mZoom;
}
void setZoom(int zoom) {
mZoom = qMax(1, zoom);
recalculateStuff();
emit zoomChanged();
}
QRect targetRect() const {
return mTargetRect;
}
QRect sourceRect() const {
return mSourceRect;
}
void recalculateStuff() {
const QRect oldTargetRect = mTargetRect;
const QRect oldSourceRect = mSourceRect;
mTargetRect = QRect(0, 0, mImage.width() * mZoom, mImage.height() * mZoom);
mSourceRect = QRect(0, 0, mImage.width(), mImage.height());
const int contentLeft = mOffset.x();
if (contentLeft < 0) {
// The left edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setX(qAbs(contentLeft));
mSourceRect.setX(qAbs(contentLeft));
}
const int contentTop = mOffset.y();
if (contentTop < 0) {
// The top edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setY(qAbs(contentTop));
mSourceRect.setY(qAbs(contentTop));
}
const int contentRight = mOffset.x() + mImage.width();
const int viewportRight = qFloor(width());
if (contentRight > viewportRight) {
// The right edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setWidth(mTargetRect.width() - (contentRight - viewportRight));
mSourceRect.setWidth(mSourceRect.width() - (contentRight - viewportRight));
}
const int contentBottom = mOffset.y() + mImage.height();
const int viewportBottom = qFloor(height());
if (contentBottom > viewportBottom) {
// The bottom edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setHeight(mTargetRect.height() - (contentBottom - viewportBottom));
mSourceRect.setHeight(mSourceRect.height() - (contentBottom - viewportBottom));
}
if (mTargetRect != oldTargetRect)
emit targetRectChanged();
if (mSourceRect != oldSourceRect)
emit sourceRectChanged();
update();
}
void paint(QPainter *painter) override {
painter->translate(mOffset);
painter->drawImage(mTargetRect, mImage, mSourceRect);
}
protected:
void geometryChanged(const QRectF &, const QRectF &) override {
recalculateStuff();
}
signals:
void offsetChanged();
void zoomChanged();
void sourceRectChanged();
void targetRectChanged();
private:
QPoint mOffset;
int mZoom;
QRect mSourceRect;
QRect mTargetRect;
QImage mImage;
};
int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<ImageCanvas>("App", 1, 0, "ImageCanvas");
QQmlApplicationEngine engine;
engine.load(QUrl("qrc:/main.qml"));
return app.exec();
}
#include "main.moc"
main.qml:
import QtQuick 2.10
import QtQuick.Controls 2.3
import App 1.0
ApplicationWindow {
id: window
width: 600
height: 600
visible: true
title: "targetRect=" + canvas.targetRect + " sourceRect=" + canvas.sourceRect
ImageCanvas {
id: canvas
anchors.fill: parent
offset: Qt.point(xOffsetSlider.value, yOffsetSlider.value)
zoom: zoomSpinBox.value
}
SpinBox {
id: zoomSpinBox
from: 1
to: 8
}
Slider {
id: xOffsetSlider
anchors.bottom: parent.bottom
width: parent.width - height
from: -window.width * canvas.zoom
to: window.width * canvas.zoom
ToolTip {
id: xOffsetToolTip
parent: xOffsetSlider.handle
visible: true
text: xOffsetSlider.value.toFixed(1)
Binding {
target: xOffsetToolTip
property: "visible"
value: !yOffsetToolTip.visible
}
}
}
Slider {
id: yOffsetSlider
anchors.right: parent.right
height: parent.height - width
orientation: Qt.Vertical
from: -window.height * canvas.zoom
scale: -1
to: window.height * canvas.zoom
ToolTip {
id: yOffsetToolTip
parent: yOffsetSlider.handle
text: yOffsetSlider.value.toFixed(1)
Binding {
target: yOffsetToolTip
property: "visible"
value: !xOffsetToolTip.visible
}
}
}
}
Это хорошо работает, когда уровень масштабирования равен 1, но как только вы увеличиваете масштаб, направленность цели и источника становятся неправильными. Я пытался это исправить, но не могу обернуть голову вокруг этого. Например, одна наивная идея состояла в том, чтобы сделать все вычисления с не масштабированными координатами, а затем затем масштабировать целевой прямоугольник:
diff --git a/main.cpp b/main.cpp
index 8409baf..06841b7 100644
--- a/main.cpp
+++ b/main.cpp
@@ -64,24 +64,24 @@ public:
const QRect oldTargetRect = mTargetRect;
const QRect oldSourceRect = mSourceRect;
- mTargetRect = QRect(0, 0, mImage.width() * mZoom, mImage.height() * mZoom);
+ mTargetRect = QRect(0, 0, mImage.width(), mImage.height());
mSourceRect = QRect(0, 0, mImage.width(), mImage.height());
- const int contentLeft = mOffset.x();
+ const int contentLeft = mOffset.x() / mZoom;
if (contentLeft < 0) {
// The left edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setX(qAbs(contentLeft));
mSourceRect.setX(qAbs(contentLeft));
}
- const int contentTop = mOffset.y();
+ const int contentTop = mOffset.y() / mZoom;
if (contentTop < 0) {
// The top edge of the content is outside of the viewport, so don't draw that portion.
mTargetRect.setY(qAbs(contentTop));
mSourceRect.setY(qAbs(contentTop));
}
- const int contentRight = mOffset.x() + mImage.width();
+ const int contentRight = (mOffset.x() / mZoom) + mImage.width();
const int viewportRight = qFloor(width());
if (contentRight > viewportRight) {
// The right edge of the content is outside of the viewport, so don't draw that portion.
@@ -89,7 +89,7 @@ public:
mSourceRect.setWidth(mSourceRect.width() - (contentRight - viewportRight));
}
- const int contentBottom = mOffset.y() + mImage.height();
+ const int contentBottom = (mOffset.y() / mZoom) + mImage.height();
const int viewportBottom = qFloor(height());
if (contentBottom > viewportBottom) {
// The bottom edge of the content is outside of the viewport, so don't draw that portion.
@@ -97,6 +97,11 @@ public:
mSourceRect.setHeight(mSourceRect.height() - (contentBottom - viewportBottom));
}
+ mTargetRect.setX(mTargetRect.x() * mZoom);
+ mTargetRect.setY(mTargetRect.y() * mZoom);
+ mTargetRect.setWidth(mTargetRect.width() * mZoom);
+ mTargetRect.setHeight(mTargetRect.height() * mZoom);
+
if (mTargetRect != oldTargetRect)
emit targetRectChanged();
Это не работает, так как изображение все больше растягивается, когда вы, например, перемещаетесь вниз с масштабом, установленным на 2, вместо того, чтобы оставаться в том же масштабе.
Итак, как правильно рассчитать целевые и исходные текты, чтобы я рисовал только видимую часть изображения, когда оно увеличено?
1 ответ
Общая идея состоит в том, чтобы пересечь прямоугольник изображения с прямоугольником области рисования, то есть прямоугольником элемента ({0, 0, width(), height()}
). Такое пересечение должно быть сделано в выбранной системе координат, а прямоугольник должен распространяться на другую систему координат. Давайте сделаем пересечение в целевой системе координат:
// **private
private:
QImage mImage;
QPointF mOffset;
double mZoom = 1.0;
double mRenderTime = 0.;
bool mRectDraw = true;
QRectF mSourceRect;
QRectF mTargetRect;
static void moveBy(QRectF &r, const QPointF &o) {
r = {r.x() + o.x(), r.y() + o.y(), r.width(), r.height()};
}
static void scaleBy(QRectF &r, qreal s) {
r = {r.x() * s, r.y() * s, r.width() * s, r.height() * s};
}
void recalculate() {
const auto oldTargetRect = mTargetRect;
const auto oldSourceRect = mSourceRect;
mTargetRect = {{}, mImage.size()};
moveBy(mTargetRect, -mOffset);
scaleBy(mTargetRect, mZoom);
mTargetRect = mTargetRect.intersected({{}, size()});
Теперь мы преобразуем этот прямоугольник обратно в систему координат источника (изображения):
mSourceRect = mTargetRect;
scaleBy(mSourceRect, 1.0/mZoom);
moveBy(mSourceRect, mOffset);
if (mTargetRect != oldTargetRect)
emit targetRectChanged(mTargetRect);
if (mSourceRect != oldSourceRect)
emit sourceRectChanged(mSourceRect);
update();
}
Затем нужно выбрать способ прокрутки - обычно диапазон прокрутки находится в любом месте прямоугольника исходного изображения (т.е. mImage.rect()
напоминая, что это {0, 0, mImage.width(), mImage.height()}
), таким образом, ползунки прокрутки x/y перемещаются между 0 и шириной / высотой изображения соответственно.
Рисование также может быть реализовано путем рисования всего изображения, но, к сожалению, движок рисования, поддерживающий художника, не знает, как справиться с обрезкой - так что даже если мы установим обрезку прямо перед этим drawImage
ничего не поделаешь: художник, с которым нам приходится работать, игнорирует вырезку. И, таким образом, при больших значениях масштабирования картина с mRectDraw = false
становится неэффективным. Это недостаток движка рисования, и он определенно может быть исправлен в самом Qt.
// **paint
void paint(QPainter *p) override {
QElapsedTimer timer;
timer.start();
if (mRectDraw) {
p->drawImage(mTargetRect, mImage, mSourceRect);
} else {
p->scale(mZoom, mZoom);
p->translate(-mOffset);
p->drawImage(0, 0, mImage);
}
mRenderTime = timer.nsecsElapsed() * 1E-9;
emit renderTimeChanged(mRenderTime);
}
Остальная часть примера следующая. Значение масштабирования Spinbox является показателем sqrt(2)
т.е. value=0 -> zoom=1
, value=-2 -> zoom=0.5
, `value = 4 -> zoom = 2 'и т. д. Холст поддерживает положительные ненулевые значения масштабирования, то есть также значения ниже 1.
// https://github.com/KubaO/stackrun/tree/master/questions/qml-zoom-imagecanvas-51455895
#include <QtQuick>
#include <limits>
class ImageCanvas : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(QImage image READ image WRITE setImage NOTIFY imageChanged)
Q_PROPERTY(QRectF imageRect READ imageRect NOTIFY imageRectChanged)
Q_PROPERTY(QPointF offset READ offset WRITE setOffset NOTIFY offsetChanged)
Q_PROPERTY(double zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(double renderTime READ renderTime NOTIFY renderTimeChanged)
Q_PROPERTY(bool rectDraw READ rectDraw WRITE setRectDraw NOTIFY rectDrawChanged)
Q_PROPERTY(QRectF sourceRect READ sourceRect NOTIFY sourceRectChanged)
Q_PROPERTY(QRectF targetRect READ targetRect NOTIFY targetRectChanged)
public:
ImageCanvas(QQuickItem *parent = {}) : QQuickPaintedItem(parent) {}
QImage image() const { return mImage; }
QRectF imageRect() const { return mImage.rect(); }
void setImage(const QImage &image) {
if (mImage != image) {
auto const oldRect = mImage.rect();
mImage = image;
recalculate();
emit imageChanged(mImage);
if (mImage.rect() != oldRect)
emit imageRectChanged(mImage.rect());
}
}
Q_SIGNAL void imageChanged(const QImage &);
Q_SIGNAL void imageRectChanged(const QRectF &);
QPointF offset() const { return mOffset; }
void setOffset(const QPointF &offset) {
mOffset = offset;
recalculate();
emit offsetChanged(mOffset);
}
Q_SIGNAL void offsetChanged(const QPointF &);
double zoom() const { return mZoom; }
void setZoom(double zoom) {
if (zoom != mZoom) {
mZoom = zoom ? zoom : std::numeric_limits<float>::min();
recalculate();
emit zoomChanged(mZoom);
}
}
Q_SIGNAL void zoomChanged(double);
// **paint
double renderTime() const { return mRenderTime; }
Q_SIGNAL void renderTimeChanged(double);
bool rectDraw() const { return mRectDraw; }
void setRectDraw(bool r) {
if (r != mRectDraw) {
mRectDraw = r;
recalculate();
emit rectDrawChanged(mRectDraw);
}
}
Q_SIGNAL void rectDrawChanged(bool);
QRectF sourceRect() const { return mSourceRect; }
QRectF targetRect() const { return mTargetRect; }
Q_SIGNAL void sourceRectChanged(const QRectF &);
Q_SIGNAL void targetRectChanged(const QRectF &);
protected:
void geometryChanged(const QRectF &, const QRectF &) override {
recalculate();
}
// **private
};
QImage sampleImage() {
QImage image(500, 500, QImage::Format_ARGB32_Premultiplied);
QPainter painter(&image);
for (int y = 0; y < image.height(); y += 50)
for (int x = 0; x < image.width(); x += 50) {
const QColor colour((x / 500.0) * 255, (y / 500.0) * 255, 0);
painter.fillRect(x, y, 50, 50, colour);
}
return image;
}
int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
qmlRegisterType<ImageCanvas>("App", 1, 0, "ImageCanvas");
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("sampleImage", sampleImage());
engine.load(QUrl("qrc:/main.qml"));
return app.exec();
}
#include "main.moc"
И qml:
import QtQuick 2.10
import QtQuick.Controls 2.3
import App 1.0
ApplicationWindow {
id: window
width: 600
height: 600
visible: true
title: "T=" + (canvas.renderTime*1E3).toFixed(1) + "ms t=" + canvas.targetRect + " s=" + canvas.sourceRect
ImageCanvas {
id: canvas
image: sampleImage
anchors.fill: parent
offset: Qt.point(xOffsetSlider.value, yOffsetSlider.value)
zoom: Math.pow(Math.SQRT2, zoomSpinBox.value)
rectDraw: rectDrawCheckBox.checked
}
SpinBox {
id: zoomSpinBox
anchors.bottom: xOffsetSlider.top
from: -10
to: 20
}
CheckBox {
id: rectDrawCheckBox
anchors.left: zoomSpinBox.right
anchors.bottom: xOffsetSlider.top
text: "rectDraw"
checked: true
}
Slider {
id: xOffsetSlider
anchors.bottom: parent.bottom
width: parent.width - height
from: 0
to: canvas.imageRect.width
ToolTip {
id: xOffsetToolTip
parent: xOffsetSlider.handle
visible: true
text: xOffsetSlider.value.toFixed(1)
Binding {
target: xOffsetToolTip
property: "visible"
value: !yOffsetToolTip.visible
}
}
}
Slider {
id: yOffsetSlider
anchors.right: parent.right
height: parent.height - width
orientation: Qt.Vertical
from: canvas.imageRect.height
to: 0
ToolTip {
id: yOffsetToolTip
parent: yOffsetSlider.handle
text: yOffsetSlider.value.toFixed(1)
Binding {
target: yOffsetToolTip
property: "visible"
value: !xOffsetToolTip.visible
}
}
}
}