QPainter:仅绘制放大图像的可见区域

时间:2018-07-21 12:05:35

标签: c++ qt qtquick2 qpainter

我有一个自定义的QQuickPaintedItem,它可以将用户用鼠标绘制的内容绘制到上面。到目前为止,实现非常简单,即使放大时也可以绘制整个图像。我注意到,在放大和平移图像时,FPS确实很慢,所以我决定逐步提高绘画性能。

我当前所处的步骤只是绘制可见图像的子集。为此,我正在使用this overload of 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
            }
        }
    }
}

screenshot

当缩放级别为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 :(得分: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);
   }

该示例的其余部分如下。 zoom spinbox的含义是sqrt(2)上的指数,即value=0 -> zoom=1value=-2 -> zoom=0.5,“ value = 4-> zoom = 2”等。画布支持正非零缩放值,也就是低于1的值。

// https://github.com/KubaO/stackoverflown/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
            }
        }
    }
}