gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 04:58:57 26 4
gpt4 key购买 nike

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

我当前的步骤只是绘制可见的图像子集。为此,我使用 this overload of QPainter::drawImage() .这是允许缩放和平移的最小示例(重要部分是 recalculateStuff()):

ma​​in.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"

ma​​in.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();

这是行不通的,因为图像会随着您的移动而逐渐拉伸(stretch)。将缩放设置为 2 向下平移,而不是保持相同的比例。

那么,计算目标矩形和源矩形的正确方法是什么,以确保在放大图像时只绘制图像的可见部分?

最佳答案

总体思路是将图像矩形与绘画区域矩形相交,即项目矩形 ({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 滚动 slider 分别在 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=1, value=-2 -> zoom= 0.5、`value=4 -> zoom=2' 等。 Canvas 支持正的非零缩放值,即小于 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
}
}
}
}

关于c++ - QPainter:仅绘制放大图像的可见区域,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51455895/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com