How to zoom in a QgraphicsView using pushbuttons? - c++

I'm building a very simple image editor on Qt creator.I have my image displayed on a QGraphicsView and i want to give the user the ability to zoom in and out by a pushbutton.
I've searched a lot and found how to zoom in and out through the mouse wheel.As i am very new to Qt i can't adjust it to the pushbutton because i don't understand everything clearly.
I' ve tried this(without understanding completely what i'm doing)but the result isn't the wanted.It zooms in only once and quite abruptly.I want a smoother zoom and as many times as i want.
void MainWindow::on_pushButton_clicked(){
QMatrix matrix;
ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorViewCenter);
matrix.scale(1.0,1.0);
ui->graphicsView->setMatrix(matrix);
ui->graphicsView->scale(1,-1);
}
I would be very grateful if you guys can help

Below is how I implemented zooming in my subclass of QGraphicsView. Note that you'd need to pass in different values of "zoom" to get different magnifications as the zoom factor is an absolute value, not a relative one.
(The optMousePos argument can be set to point to a QPoint indicating the spot that should be the central-point of the zoom transformation, or it can be left NULL if you don't care about that. I use it because I zoom in and out based on the user turning the wheel in his mouse, and when doing that, the user usually wants to zoom in towards the point where his mouse point is currently positioned, rather than in towards the center of the graphics area)
qreal _zoom = 0.0;
[...]
void MyQGraphWidgetSubclass :: SetZoomFactor(qreal zoom, const QPoint * optMousePos)
{
if ((zoom != _zoom)&&(zoom >= 0.02f)&&(zoom <= 1000000.0f))
{
QPointF oldPos;
if (optMousePos) oldPos = mapToScene(*optMousePos);
// Remember what point we were centered on before...
_zoom = zoom;
QMatrix m;
m.scale(_zoom, _zoom);
setMatrix(m);
if (optMousePos)
{
const QPointF newPos = mapFromScene(oldPos);
const QPointF move = newPos-*optMousePos;
horizontalScrollBar()->setValue(move.x() + horizontalScrollBar()->value());
verticalScrollBar()->setValue(move.y() + verticalScrollBar()->value());
}
}
}
void MyQGraphWidgetSubclass :: wheelEvent(QWheelEvent* event)
{
QPoint pos = event->pos();
SetZoomFactor(_zoom*pow(1.2, event->delta() / 240.0), &pos);
event->accept();
}

Related

QPainter rotation prevents correct QPixmap rendering

Reported to Qt as a bug: https://bugreports.qt.io/browse/QTBUG-93475
I am re-drawing a QPixmap multiple times in different locations, with differnt rotations by transforming the QPainter. In certain situations the QPixmap is not drawing correctly. The GIF below shows my initial discovery of this issue with a QPixmap containing a green cylinder, notice how the rendering behaves as expected to the left of the GIF, but there is a boundary beyond which the rendering is incorrect. The QPixmap content appears to stick in place, and the pixels at the edge appear to smear out accross the rest of the pixmap. In the GIF there is a magenta background to the QPixmap, this because the targetRect used by QPainter::drawPixmap() is also beuing used to seperately fill a rectangle underneath the pixmap, this was because I wanted to check that the target rect was being computed correctly.
Minimum reproducable example:
To keep things simple I am simply filling the QPixmap with magenta pixels, with a 1 pixel wide transparent edge so that the smearing causes the pixmaps to dissapear completely. It doesn't show the image "sticking" in place but it clearly shows the boundary as beyond it the pixmaps seem to dissapear.
I have been experimenting with this myself and I believe this to be entirely caused by the rotating of the QPainter.
The angle of rotation seems to have an effect, if all of the pixmaps are rotated to the same angle then the boundary changes from a fuzzy diagonal line (where fuzzy means the boundary for dissapearing is different for each pixmap) to a sharp 90 degree corner (where sharp means that the boundary for dissapearing is the same for all pixmaps).
The range of different angles also seems to play a part, if the randomly generated angles are in a small 10 degree range, then the boundary is just a slightly fuzzier right angle, with a bevelled corner. There seems to be a progression from sharp right angle to fuzzy diagonal line as the number of different rotations is applied.
Code
QtTestBed/pro:
QT += widgets
CONFIG += c++17
CONFIG -= app_bundle
# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
MainWindow.cpp \
main.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
MainWindow.h
main.cpp:
#include "MainWindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
MainWindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QWidget>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QPainter>
#include <random>
class MainWindow : public QWidget {
Q_OBJECT
public:
MainWindow();
void wheelEvent(QWheelEvent* event) override;
void mouseReleaseEvent(QMouseEvent* /*event*/) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void resizeEvent(QResizeEvent* /*event*/) override;
void paintEvent(QPaintEvent* event) override;
private:
struct PixmapLocation {
QPointF location_;
qreal rotation_;
qreal radius_;
};
QPixmap pixmap_;
std::vector<PixmapLocation> drawLocations_;
qreal panX_ = 0.0;
qreal panY_ = 0.0;
qreal scale_ = 1.0;
bool dragging_ = false;
qreal dragX_ = 0.0;
qreal dragY_ = 0.0;
QPointF transformWindowToSimCoords(const QPointF& local) const;
QPointF transformSimToWindowCoords(const QPointF& sim) const;
static qreal randomNumber(qreal min, qreal max);
};
#endif // MAINWINDOW_H
MainWindow.cpp:
#include "MainWindow.h"
MainWindow::MainWindow()
: pixmap_(30, 50)
{
setAutoFillBackground(true);
constexpr int count = 10000;
constexpr qreal area = 10000.0;
constexpr qreal size = 44.0;
for (int i = 0; i < count; ++i) {
// qreal rotation = 0.0; // No rotation fixes the issue
// qreal rotation = 360.0; // No rotation fixes the issue
// qreal rotation = 180.0; // Mirroring also fixes the issue
// qreal rotation = 90.0; // The boundary is now a corner, and has a sharp edge (i.e. all images dissapear at the same point)
// qreal rotation = 0.1; // The boundary is now a corner, and has a sharp edge (i.e. all images dissapear at the same point)
// qreal rotation = randomNumber(0.0, 10.0); // The boundary is still a corner, with a bevel, with a fuzzy edge (i.e. not all images dissapear at the same point)
qreal rotation = randomNumber(0.0, 360.0); // The boundary appears to be a diagonal line with a fuzzy edge (i.e. not all images dissapear at the same point)
drawLocations_.push_back(PixmapLocation{ QPointF(randomNumber(-area, area), randomNumber(-area, area)), rotation, size });
}
// Make edges transparent (the middle will be drawn over)
pixmap_.fill(QColor::fromRgba(0x000000FF));
/*
* Fill with magenta almost up to the edge
*
* The transparent edge is required to see the effect, the misdrawn pixmaps
* appear to be a smear of the edge closest to the boundary between proper
* rendering and misrendering. If the pixmap is a solid block of colour then
* the effect is masked by the fact that the smeared edge looks the same as
* the correctly drawn pixmap.
*/
QPainter p(&pixmap_);
p.setPen(Qt::NoPen);
constexpr int inset = 1;
p.fillRect(pixmap_.rect().adjusted(inset, inset, -inset, -inset), Qt::magenta);
update();
}
void MainWindow::wheelEvent(QWheelEvent* event)
{
double d = 1.0 + (0.001 * double(event->angleDelta().y()));
scale_ *= d;
update();
}
void MainWindow::mouseReleaseEvent(QMouseEvent*)
{
dragging_ = false;
}
void MainWindow::mousePressEvent(QMouseEvent* event)
{
dragging_ = true;
dragX_ = event->pos().x();
dragY_ = event->pos().y();
}
void MainWindow::mouseMoveEvent(QMouseEvent* event)
{
if (dragging_) {
panX_ += ((event->pos().x() - dragX_) / scale_);
panY_ += ((event->pos().y() - dragY_) / scale_);
dragX_ = event->pos().x();
dragY_ = event->pos().y();
update();
}
}
void MainWindow::resizeEvent(QResizeEvent*)
{
update();
}
void MainWindow::paintEvent(QPaintEvent* event)
{
QPainter paint(this);
paint.setClipRegion(event->region());
paint.translate(width() / 2, height() / 2);
paint.scale(scale_, scale_);
paint.translate(panX_, panY_);
for (const PixmapLocation& entity : drawLocations_) {
paint.save();
QPointF centre = entity.location_;
const qreal scale = (entity.radius_ * 2) / std::max(pixmap_.width(), pixmap_.height());
QRectF targetRect(QPointF(0, 0), pixmap_.size() * scale);
targetRect.translate(centre - QPointF(targetRect.width() / 2, targetRect.height() / 2));
// Rotate our pixmap
paint.translate(centre);
paint.rotate(entity.rotation_);
paint.translate(-centre);
// paint.setClipping(false); // This doesn't fix it so it isn't clipping
paint.drawPixmap(targetRect, pixmap_, QRectF(pixmap_.rect()));
// paint.setClipping(true); // This doesn't fix it so it isn't clipping
paint.restore();
}
}
QPointF MainWindow::transformWindowToSimCoords(const QPointF& local) const
{
qreal x = local.x();
qreal y = local.y();
// Sim is centred on screen
x -= (width() / 2);
y -= (height() / 2);
// Sim is scaled
x /= scale_;
y /= scale_;
// Sim is transformed
x -= panX_;
y -= panY_;
return { x, y };
}
QPointF MainWindow::transformSimToWindowCoords(const QPointF& sim) const
{
qreal x = sim.x();
qreal y = sim.y();
// Sim is transformed
x += panX_;
y += panY_;
// Sim is scaled
x *= scale_;
y *= scale_;
// Sim is centred on screen
x += (width() / 2);
y += (height() / 2);
return { x, y };
}
qreal MainWindow::randomNumber(qreal min, qreal max)
{
static std::mt19937 entropy = std::mt19937();
std::uniform_real_distribution<qreal> distribution{ min, max };
// distribution.param(typename decltype(distribution)::param_type(min, max));
return distribution(entropy);
}
My research into the issue
Top left above shows all pixmaps drawing correctly, at random angles
Top right above shows the same instance as top left, panned so that the pixmaps are over the fuzzy boundary, with the bottom rightmost pixmaps not being drawn (or to be more accurate, are being drawn as entirely transparent pixels, due to the transparent edge being smeared over the entire image)
Bottom left shows all pixmaps being rotated by 0.1 degree, this leads to a sharp boundary, which when the square is panned to overlap it, clips the square to a rectangle.
Bottom right shows a small range of random angles, between 0.0 and 10.0, this leads to a slightly fuzzier, but still vertical edge, this looks similar to bottom left, but as well as the sharp clipped edge, there is also a slight gradient effect as some of the pixmaps closer to the edge have also not be rendered correctly.
I have tried turning clipping off in the QPainter when drawing the pixmaps, this has had no effect.
I have tried seperately saving a copy of the QPainters transform and setting it back afterwards, this had no effect.
I have tried upgrading to Qt 6.0.3 (which claimed to have solved a number of graphical bugs), issue still present.
The absolute coordinates don't matter, I can offset all of the locations by QPointF(-10000, 10000), pan over to them and the dissapearing point is in the same relative position in the window.
To see the bug in action, scroll out, then click and drag in the window to move the pixmaps to the lower right of the screen, depending on how far out you have zoomed, a number of the pixmaps will no longer be drawn.
Update
I have also discovered that making the original QPixmap larger makes the issue worse, i.e. the boundary becomes apparent at more zoomed in levels, plus further rendering abberations occur. Note that they are still being scaled down to the same size as before, there are just more source pixels.
I changed pixmap_(30, 50) to pixmap_(300, 500)
The image above shows that when panning to move the pixmaps towards the bottom right, they dissapear sooner than before (i.e. while zoomed further in and more towards the top left), the curved arrow indicates the movement of the pixmaps drawn in an arc beyond the dissapearance boundary, they seem to be moving faster than the correct pixmaps are drawn as they are moved.
EDIT: Closer inspection shows that the apparent circular motion is not real, the order in which pixmaps are appearing and dissapearing just made it look that way. With the below update, you can see that there are concentric rings where the pixmaps that have dissapeard re-appear (very briefly) in the correct place, but the re-appeaance is only for a thin window that seems to be narrower than the size of the pixmap, so the content that is drawn appears again to be stuck in place, but the part shown is clipped.
Update
To see the pixmaps "stick" at the boundary, you can adjust the contents of MainWindow::MainWindow() to
MainWindow::MainWindow()
: pixmap_(500, 500)
{
setAutoFillBackground(true);
constexpr int count = 10000;
constexpr qreal area = 10000.0;
constexpr qreal size = 44.0;
for (int i = 0; i < count; ++i) {
qreal rotation = randomNumber(0.0, 360.0);
drawLocations_.push_back(PixmapLocation{ QPointF(randomNumber(-area, area), randomNumber(-area, area)), rotation, size });
}
// Make edges transparent (the middle will be drawn over)
pixmap_.fill(QColor::fromRgba(0x000000FF));
/*
* Fill with magenta almost up to the edge
*
* The transparent edge is required to see the effect, the misdrawn pixmaps
* appear to be a smear of the edge closest to the boundary between proper
* rendering and misrendering. If the pixmap is a solid block of colour then
* the effect is masked by the fact that the smeared edge looks the same as
* the correctly drawn pixmap.
*/
QPainter p(&pixmap_);
p.setPen(Qt::NoPen);
constexpr int smallInset = 1;
const int bigInset = std::min(pixmap_.width(), pixmap_.height()) / 5;
p.fillRect(pixmap_.rect().adjusted(smallInset, smallInset, -smallInset, -smallInset), Qt::magenta);
p.fillRect(pixmap_.rect().adjusted(bigInset, bigInset, -bigInset, -bigInset), Qt::green);
update();
}
Which results in see right hand edge for squares that appear to have been clipped. When moving them around, the square seems to get stuck in place and instead of the edge at the boundary dissapearing, the edge furthest from the boundary dissapears first.
This issue is very interesting. As far as I could test, your code looks good, I feel like this is a Qt bug, and I think you need to report it to Qt: https://bugreports.qt.io/. You should post a single piece of code to illustrate the issue, your second one from your "Update" edit is good: it makes it easy to reproduce the issu. Maybe you should also post a small video to illustrate how things are getting wrong when you zoom in/out or move the area with the mouse.
I tried some alternatives to hopefully find a workaround, but I found none:
Tried to use a QImage rather than a QPixmap, same issue
Tried to load the pixmap from a frozen png/qrc file, same issue
Tried to use QTransform to play with scale/translation/rotation, same issue
Tried Linux and Windows 10: same issue observed
Note that:
If you don't rotate (comment paint.rotate(entity.rotation_);), the issue is not visible
If your pixmap is a simple mono-colored square (simply fill your pixmap with a single color using pixmap_.fill(QColor::fromRgba(0x12345600));), the issue is not visible anymore. That's the most surprising, looks like a pixel from the image is being reused as background and messes things up but if all the image pixels are the same it does not lead to any display issue.
Workaround proposed by the Qt team
"The issue can easily be worked around by enabling the SmoothPixmapTransform render hint on the painter"

Qt QXYSeries and ChartView - modify hovered behavior to trigger within a range

I have a scatter plot represented by a QXYSeries and viewed with a ChartView from Qt Charts 5.7.
I want to hover my mouse over the plot, have "hovered" trigger within a certain distance, rather than only when my cursor is directly on top of a point. Imagine a circle around the mouse, that will trigger hovered whenever any part of the series is within it.
Is there a way to get this behavior?
Eventually, I got this behavior by creating a class that inherits from QChartView and overriding mouseMoveEvent(QMouseEvent* event) thusly:
void ScatterView::mouseMoveEvent(QMouseEvent* event)
{
if(!this->chart()->axisX() || !this->chart()->axisY())
{
return;
}
QPointF inPoint;
QPointF chartPoint;
inPoint.setX(event->x());
inPoint.setY(event->y());
chartPoint = chart()->mapToValue(inPoint);
handleMouseMoved(chartPoint);
}
void ScatterView::handleMouseMoved(const QPointF &point)
{
QPointF mousePoint = point;
qreal distance(0.2); //distance from mouse to point in chart axes
foreach (QPointF currentPoint, scatterSeries->points()) {
qreal currentDistance = qSqrt((currentPoint.x() - mousePoint.x())
* (currentPoint.x() - mousePoint.x())
+ (currentPoint.y() - mousePoint.y())
* (currentPoint.y() - mousePoint.y()));
if (currentDistance < distance) {
triggerPoint(currentPoint);
}
}
}

QT Graphic scene/view - moving around with mouse

I created my own classes (view and scene) to display image and objects I added to it, even got zoom in/out function implemented to my view, but now I have to add new functionality and I don't even know how to start looking for it.
Whenever I press the scroll button of my mouse and hold it - I wish to move around the scene, to see different parts of it - just like I would with sliders. It is supposed to be similar to any other program allowing to zoom in/out to image and move around zoomed picture to see different parts of it.
Unfortunately - I don't even know how to look for some basic, because "moving" and similar refer to dragging objects around.
EDIT 1
void CustomGraphicView::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() == Qt::MidButton)
{
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
translate(event->x(),event->y());
}
}
Tried this - but it is working in reverse.
I suppose you know how to handle events using Qt.
So, to translate (move) your view use the QGraphicsView::translate() method.
EDIT
How to use it:
void CustomGraphicsView::mousePressEvent(QMouseEvent* event)
{
if (e->button() == Qt::MiddleButton)
{
// Store original position.
m_originX = event->x();
m_originY = event->y();
}
}
void CustomGraphicsView::mouseMoveEvent(QMouseEvent* event)
{
if (e->buttons() & Qt::MidButton)
{
QPointF oldp = mapToScene(m_originX, m_originY);
QPointF newP = mapToScene(event->pos());
QPointF translation = newp - oldp;
translate(translation.x(), translation.y());
m_originX = event->x();
m_originY = event->y();
}
}

QGraphicsView Zooming in and out under mouse position using mouse wheel

I have an application with a QGraphicsView window in the middle of the screen. I want to be able to zoom in and out using a mouse wheel scroll.
Currently I have re-implemented QGraphicsView and overriden the mouse scroll function so that it doesn't scroll the image (like it does by default).
void MyQGraphicsView::wheelEvent(QWheelEvent *event)
{
if(event->delta() > 0)
{
emit mouseWheelZoom(true);
}
else
{
emit mouseWheelZoom(false);
}
}
so when I scroll, I'm emitting a signal true if mouse wheel forward false if mouse wheel back.
I have then connected this signal to a slot (zoom function see below) in the class that handles my GUI stuff. Now basically I think my zoom function just isn't the best way to do it at all I have seen some examples of people using the overriden wheelevent function to set scales but I couldn't really find a complete answer.
So instead I have done this but it's not perfect by any means so I'm looking for this to be tweaked a bit or for a working example using scale in the wheel event function.
I initialize m_zoom_level to 0 in the constructor.
void Display::zoomfunction(bool zoom)
{
QMatrix matrix;
if(zoom && m_zoom_level < 500)
{
m_zoom_level = m_zoom_level + 10;
ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
matrix.scale(m_zoom_level, m_zoom_level);
ui->graphicsView->setMatrix(matrix);
ui->graphicsView->scale(1,-1);
}
else if(!zoom)
{
m_zoom_level = m_zoom_level - 10;
ui->graphicsView->setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
matrix.scale(m_zoom_level, m_zoom_level);
ui->graphicsView->setMatrix(matrix);
ui->graphicsView->scale(1,-1);
}
}
As you can see above I'm using a QMatrix and scaling that and setting it to the Graphicsview and setting the transformation anchor to under mouse, but its just not working perfectly sometimes if I'm scrolling loads it will just start to zoom in only (which I think is to do with the int looping over or something).
As I said help with this or a good example of scale under mouse would be great.
Such zooming is a bit tricky. Let me share my own class for doing that.
Header:
#include <QObject>
#include <QGraphicsView>
/*!
* This class adds ability to zoom QGraphicsView using mouse wheel. The point under cursor
* remains motionless while it's possible.
*
* Note that it becomes not possible when the scene's
* size is not large enough comparing to the viewport size. QGraphicsView centers the picture
* when it's smaller than the view. And QGraphicsView's scrolls boundaries don't allow to
* put any picture point at any viewport position.
*
* When the user starts scrolling, this class remembers original scene position and
* keeps it until scrolling is completed. It's better than getting original scene position at
* each scrolling step because that approach leads to position errors due to before-mentioned
* positioning restrictions.
*
* When zommed using scroll, this class emits zoomed() signal.
*
* Usage:
*
* new Graphics_view_zoom(view);
*
* The object will be deleted automatically when the view is deleted.
*
* You can set keyboard modifiers used for zooming using set_modified(). Zooming will be
* performed only on exact match of modifiers combination. The default modifier is Ctrl.
*
* You can change zoom velocity by calling set_zoom_factor_base().
* Zoom coefficient is calculated as zoom_factor_base^angle_delta
* (see QWheelEvent::angleDelta).
* The default zoom factor base is 1.0015.
*/
class Graphics_view_zoom : public QObject {
Q_OBJECT
public:
Graphics_view_zoom(QGraphicsView* view);
void gentle_zoom(double factor);
void set_modifiers(Qt::KeyboardModifiers modifiers);
void set_zoom_factor_base(double value);
private:
QGraphicsView* _view;
Qt::KeyboardModifiers _modifiers;
double _zoom_factor_base;
QPointF target_scene_pos, target_viewport_pos;
bool eventFilter(QObject* object, QEvent* event);
signals:
void zoomed();
};
Source:
#include "Graphics_view_zoom.h"
#include <QMouseEvent>
#include <QApplication>
#include <QScrollBar>
#include <qmath.h>
Graphics_view_zoom::Graphics_view_zoom(QGraphicsView* view)
: QObject(view), _view(view)
{
_view->viewport()->installEventFilter(this);
_view->setMouseTracking(true);
_modifiers = Qt::ControlModifier;
_zoom_factor_base = 1.0015;
}
void Graphics_view_zoom::gentle_zoom(double factor) {
_view->scale(factor, factor);
_view->centerOn(target_scene_pos);
QPointF delta_viewport_pos = target_viewport_pos - QPointF(_view->viewport()->width() / 2.0,
_view->viewport()->height() / 2.0);
QPointF viewport_center = _view->mapFromScene(target_scene_pos) - delta_viewport_pos;
_view->centerOn(_view->mapToScene(viewport_center.toPoint()));
emit zoomed();
}
void Graphics_view_zoom::set_modifiers(Qt::KeyboardModifiers modifiers) {
_modifiers = modifiers;
}
void Graphics_view_zoom::set_zoom_factor_base(double value) {
_zoom_factor_base = value;
}
bool Graphics_view_zoom::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::MouseMove) {
QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
QPointF delta = target_viewport_pos - mouse_event->pos();
if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) {
target_viewport_pos = mouse_event->pos();
target_scene_pos = _view->mapToScene(mouse_event->pos());
}
} else if (event->type() == QEvent::Wheel) {
QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
if (QApplication::keyboardModifiers() == _modifiers) {
if (wheel_event->orientation() == Qt::Vertical) {
double angle = wheel_event->angleDelta().y();
double factor = qPow(_zoom_factor_base, angle);
gentle_zoom(factor);
return true;
}
}
}
Q_UNUSED(object)
return false;
}
Usage example:
Graphics_view_zoom* z = new Graphics_view_zoom(ui->graphicsView);
z->set_modifiers(Qt::NoModifier);
Here is a solution using PyQt:
def wheelEvent(self, event):
"""
Zoom in or out of the view.
"""
zoomInFactor = 1.25
zoomOutFactor = 1 / zoomInFactor
# Save the scene pos
oldPos = self.mapToScene(event.pos())
# Zoom
if event.angleDelta().y() > 0:
zoomFactor = zoomInFactor
else:
zoomFactor = zoomOutFactor
self.scale(zoomFactor, zoomFactor)
# Get the new position
newPos = self.mapToScene(event.pos())
# Move scene to old position
delta = newPos - oldPos
self.translate(delta.x(), delta.y())
You can simply use builtin functionality AnchorUnderMouse or AnchorViewCenter to maintain focus under mouse or in the center.
This works for me in Qt 5.7
void SceneView::wheelEvent(QWheelEvent *event)
{
if (event->modifiers() & Qt::ControlModifier) {
// zoom
const ViewportAnchor anchor = transformationAnchor();
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
int angle = event->angleDelta().y();
qreal factor;
if (angle > 0) {
factor = 1.1;
} else {
factor = 0.9;
}
scale(factor, factor);
setTransformationAnchor(anchor);
} else {
QGraphicsView::wheelEvent(event);
}
}
Here's the python version works for me. Comes from the combination of answers from #Stefan Reinhardt and #rengel .
class MyQGraphicsView(QtGui.QGraphicsView):
def __init__ (self, parent=None):
super(MyQGraphicsView, self).__init__ (parent)
def wheelEvent(self, event):
# Zoom Factor
zoomInFactor = 1.25
zoomOutFactor = 1 / zoomInFactor
# Set Anchors
self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
self.setResizeAnchor(QtGui.QGraphicsView.NoAnchor)
# Save the scene pos
oldPos = self.mapToScene(event.pos())
# Zoom
if event.delta() > 0:
zoomFactor = zoomInFactor
else:
zoomFactor = zoomOutFactor
self.scale(zoomFactor, zoomFactor)
# Get the new position
newPos = self.mapToScene(event.pos())
# Move scene to old position
delta = newPos - oldPos
self.translate(delta.x(), delta.y())
It's a bit late
but i walked through the same today only with Pyside, but should be the same...
The approach is "very simple", altough costed me a bit time...
First set all Anchors to NoAnchor, then take the point of the wheelevent, map it to the scene,
translate the scene by this value, scale and finally translate it back:
def wheelEvent(self, evt):
#Remove possible Anchors
self.widget.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor)
self.widget.setResizeAnchor(QtGui.QGraphicsView.NoAnchor)
#Get Scene Pos
target_viewport_pos = self.widget.mapToScene(evt.pos())
#Translate Scene
self.widget.translate(target_viewport_pos.x(),target_viewport_pos.y())
# ZOOM
if evt.delta() > 0:
self._eventHandler.zoom_ctrl(1.2)
else:
self._eventHandler.zoom_ctrl(0.83333)
# Translate back
self.widget.translate(-target_viewport_pos.x(),-target_viewport_pos.y())
This was the only solution that worked for my purpose.
IMHO it is also the most logical solution...
Here's a condensed version of the solution above; with just the code you need to put into the wheel event. This works with/without scroll bars in my testing, perfectly ;)
void MyGraphicsView::wheelEvent(QWheelEvent* pWheelEvent)
{
if (pWheelEvent->modifiers() & Qt::ControlModifier)
{
// Do a wheel-based zoom about the cursor position
double angle = pWheelEvent->angleDelta().y();
double factor = qPow(1.0015, angle);
auto targetViewportPos = pWheelEvent->pos();
auto targetScenePos = mapToScene(pWheelEvent->pos());
scale(factor, factor);
centerOn(targetScenePos);
QPointF deltaViewportPos = targetViewportPos - QPointF(viewport()->width() / 2.0, viewport()->height() / 2.0);
QPointF viewportCenter = mapFromScene(targetScenePos) - deltaViewportPos;
centerOn(mapToScene(viewportCenter.toPoint()));
return;
}
After much frustration, this seems to work. The issue seems to be that the QGraphicsView's transform has nothing to do with its scroll position, so the behavior of QGraphicsView::mapToScene(const QPoint&) const depends on both the scroll position and the transform. I had to look at the source for mapToScene to understand this.
With that in mind, here's what worked: remember the scene point the mouse is pointing to, scale, map that scene point to mouse coordinates, then adjust the scroll bars to make that point wind up under the mouse:
void ZoomGraphicsView::wheelEvent(QWheelEvent* event)
{
const QPointF p0scene = mapToScene(event->pos());
qreal factor = std::pow(1.01, event->delta());
scale(factor, factor);
const QPointF p1mouse = mapFromScene(p0scene);
const QPointF move = p1mouse - event->pos(); // The move
horizontalScrollBar()->setValue(move.x() + horizontalScrollBar()->value());
verticalScrollBar()->setValue(move.y() + verticalScrollBar()->value());
}
Smoother zoom
void StatusView::wheelEvent(QWheelEvent * event)
{
const QPointF p0scene = mapToScene(event->pos());
qreal factor = qPow(1.2, event->delta() / 240.0);
scale(factor, factor);
const QPointF p1mouse = mapFromScene(p0scene);
const QPointF move = p1mouse - event->pos(); // The move
horizontalScrollBar()->setValue(move.x() + horizontalScrollBar()->value());
verticalScrollBar()->setValue(move.y() + verticalScrollBar()->value());
}
Simple example:
class CGraphicsVew : public QGraphicsView
{
Q_OBJECT
protected:
void wheelEvent(QWheelEvent *event)
{
qreal deltaScale = 1;
deltaScale += event->delta() > 0 ? 0.1 : -0.1;
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
scale(deltaScale, deltaScale);
}
};
PyQt answered work well, here provide a c++ function, in case someone need in future.
void CanvasView::zoomAt(const QPoint &centerPos, double factor)
{
//QGraphicsView::AnchorUnderMouse uses ::centerOn() in it's implement, which must need scroll.
//transformationAnchor() default is AnchorViewCenter, you need set NoAnchor while change transform,
//and combine all transform change will work more effective
QPointF targetScenePos = mapToScene(centerPos);
ViewportAnchor oldAnchor = this->transformationAnchor();
setTransformationAnchor(QGraphicsView::NoAnchor);
QTransform matrix = transform();
matrix.translate(targetScenePos.x(), targetScenePos.y())
.scale(factor, factor)
.translate(-targetScenePos.x(), -targetScenePos.y());
setTransform(matrix);
setTransformationAnchor(oldAnchor);
}
void CanvasView::wheelEvent(QWheelEvent *event)
{
if(event->modifiers().testFlag(Qt::ControlModifier))
{
double angle = event->angleDelta().y();
double factor = qPow(1.0015, angle); //smoother zoom
zoomAt(event->pos(), factor);
return;
}
QGraphicsView::wheelEvent(event);
}
Scale around point matrix formula:rotate around point, which is same with scale.
On Mac OS, the solutions cited here sometimes fail when using QGraphicsView::setTransformationAnchor(AnchorUnderMouse):
1 - Qt doesn't update lastMouseMoveScenePoint when the windows doesn't have focus. Because of that zoom is performed using the mouse position when it lost focus, and not the current one. (https://bugreports.qt.io/browse/QTBUG-73033)
2 - Qt sometimes stops propagating mouse move events when switching windows using mission control, so zoom also misbehaves like in #1. (https://bugreports.qt.io/browse/QTBUG-73067). I made this video where Chips are not highlighted the second time I clicked the window because mouseMoveEvent is not called. I know that it is not a bug in my application because this is the 40000 chips example provided by Qt. I posted the workaround for this issue here.
3 - setInteractive(false) can not be used with AnchorUnderMouse because mouse position used as centre of transformation is not updated: https://bugreports.qt.io/browse/QTBUG-60672
It seems that Qt SDK was not well tested for mouse move events in uncommon scenarios like zooming with the mouse wheel.
Combining #veslam:s solution with the Smooth Zoom code from QT Wiki (https://wiki.qt.io/Smooth_Zoom_In_QGraphicsView) seems to work very well:
Source:
QGraphicsViewMap::QGraphicsViewMap(QWidget *parent) : QGraphicsView(parent)
{
setTransformationAnchor(QGraphicsView::NoAnchor);
setResizeAnchor(QGraphicsView::NoAnchor);
}
void QGraphicsViewMap::wheelEvent(QWheelEvent* event)
{
wheelEventMousePos = event->pos();
int numDegrees = event->delta() / 8;
int numSteps = numDegrees / 15; // see QWheelEvent documentation
_numScheduledScalings += numSteps;
if (_numScheduledScalings * numSteps < 0) // if user moved the wheel in another direction, we reset previously scheduled scalings
_numScheduledScalings = numSteps;
QTimeLine *anim = new QTimeLine(350, this);
anim->setUpdateInterval(20);
connect(anim, SIGNAL (valueChanged(qreal)), SLOT (scalingTime(qreal)));
connect(anim, SIGNAL (finished()), SLOT (animFinished()));
anim->start();
}
void QGraphicsViewMap::scalingTime(qreal x)
{
QPointF oldPos = mapToScene(wheelEventMousePos);
qreal factor = 1.0+ qreal(_numScheduledScalings) / 300.0;
scale(factor, factor);
QPointF newPos = mapToScene(wheelEventMousePos);
QPointF delta = newPos - oldPos;
this->translate(delta.x(), delta.y());
}
void QGraphicsViewMap::animFinished()
{
if (_numScheduledScalings > 0)
_numScheduledScalings--;
else
_numScheduledScalings++;
sender()->~QObject();
}
Header:
class QGraphicsViewMap : public QGraphicsView
{
Q_OBJECT
private:
qreal _numScheduledScalings = 0;
QPoint wheelEventMousePos;
public:
explicit QGraphicsViewMap(QWidget *parent = 0);
signals:
public slots:
void wheelEvent(QWheelEvent* event);
void scalingTime(qreal x);
void animFinished();
};
void GraphicsView::wheelEvent(QWheelEvent* event)
{
switch (event->modifiers()) {
case Qt::ControlModifier:
if (event->angleDelta().x() != 0)
QAbstractScrollArea::horizontalScrollBar()->setValue(QAbstractScrollArea::horizontalScrollBar()->value() - (event->delta()));
else
QAbstractScrollArea::verticalScrollBar()->setValue(QAbstractScrollArea::verticalScrollBar()->value() - (event->delta()));
break;
case Qt::ShiftModifier:
QAbstractScrollArea::horizontalScrollBar()->setValue(QAbstractScrollArea::horizontalScrollBar()->value() - (event->delta()));
break;
case Qt::NoModifier:
if (abs(event->delta()) == 120) {
if (event->delta() > 0)
zoomIn();
else
zoomOut();
}
break;
default:
QGraphicsView::wheelEvent(event);
return;
}
event->accept();
}
const double zoomFactor = 1.5;
void GraphicsView::zoomIn()
{
scale(zoomFactor, zoomFactor);
}
void GraphicsView::zoomOut()
{
scale(1.0 / zoomFactor, 1.0 / zoomFactor);
}

Programmatic QGraphicsView scrolling not updating properly

I have a custom class derived from QGraphicsView that implements a slot call scrollHorizontal(int dx), inside the code is simply
void CustomView::scrollHorizontal(int dx){
scrollContentsBy(dx, 0);
}
My problem is, scrolling like this works but doesn't update the scene properly, instead any pixels found on the edge of the view are repeated instead of having a fresh call to the item's paint() method.
I've attempted calling update() after, but nothing happens. I tried enabling scrolling by dragging and updates work fine! But I need it done programmatically, and since I have the scroll bars hidden things like horizontalScrollBar()->setValue() do not scroll the view.
I also tried :
scrollContentsBy(dx, 0);
this->scene()->invalidate(sceneRect());
this->update();
update:
QPointF center = mapToScene(viewport()->rect().center());
centerOn(center.x() - dx, center.y());
update();
is working, but now my top view is scrolling slower than my bottom view, which is a new problem. They are linked with signals and slots, in the bottom view i have scrollContentsBy(int dx, int dy) overrided to emit horizontalScroll(dx); which is caught by the above slot in the top view.
Any ideas why the scrolls happen at different rates? Might it have something to do with the scroll bars being a part of the bottom view effectively making it a "smaller" window?
update 2:
The different scroll rates seems to stem from some rounding happening to give me an integer based "center" using mapToScene(viewport()->rect().center()); , as you scroll and the slower you scroll the more this error adds up, the faster you scroll the less total error.
Is there a way for me to get around this? I don't see any way to get a floating point center point.
update 3:
So I have this mostly solved, turns out the mapToScene was needed(code I found elsewhere on the web).
I fixed this by storing QPointF of FP calculated center of the viewport, now the amount of error when scrolling the two views is unnoticeable.
The final issue is, the views no longer line up when you scroll ANY amount to the right, and then resize the window then scroll again. I assume this has something to do with the logical ordering of when the center point is calculated and when the centering happens.
Right now I use the following code snippet in QGraphicsScene::ResizeEvent() and elsewhere that updates the center as needed
QRectF viewPort(viewport()->rect());
QPointF rectCenter((viewPort.x() + viewPort.x() + viewPort.width())/2.0, (viewPort.y() + viewPort.y() + viewPort.height())/2.0);
viewCenter = rectCenter;
and my horizontalScroll(int dx) slot
void CustomView::horizontalScroll(int dx)
{
viewCenter.setX(viewCenter.x() - dx);
centerOn(viewCenter.x(), viewCenter.y());
update();
}
How can I fix the issue when re-sizing the window breaking the alignment of the two views? If any more clarification is needed please just ask, I can try to give skeletons of what I'm referring to if need be.
Update 4:
Rough code Skeleton
Class HeaderView:
class HeaderView View : public QGraphicsView
{
Q_OBJECT
public:
HeaderView(QWidget * parent = 0);
HeaderView(QGraphicsScene * scene, QWidget * parent = 0);
private:
QPointF viewCenter;
protected:
void resizeEvent ( QResizeEvent * event );
public slots:
void horizontalScroll(int);
void addModel(qreal, qreal, const QString&);
};
HeaderView.cpp
void HeaderView::resizeEvent(QResizeEvent *event)
{
QGraphicsView::resizeEvent(event);
QRectF viewPort(viewport()->rect());
QPointF rectCenter((viewPort.x() + viewPort.x() + viewPort.width())/2.0, (viewPort.y() + viewPort.y() + viewPort.height())/2.0);
viewCenter = rectCenter;
}
void HeaderView::horizontalScroll(int dx)
{
viewCenter.setX(viewCenter.x() - dx);
centerOn(viewCenter.x(), viewCenter.y());
update();
}
Class EventView:
class EventView : public QGraphicsView
{
Q_OBJECT
public:
EventView(QWidget * parent = 0);
EventView(QGraphicsScene * scene, QWidget * parent = 0);
QRectF visibleRect();
protected:
void scrollContentsBy ( int dx, int dy );
signals:
void horizontalScroll(int);
};
EventView.cpp
void EventView::scrollContentsBy(int dx, int dy)
{
QGraphicsView::scrollContentsBy(dx, dy);
if(dx != 0){
emit horizontalScroll(dx);
}
}
Somwhere in Class MainWindow:
connect(eventView, SIGNAL(horizontalScroll(int)), headerView, SLOT(horizontalScroll(int));
I've worked with QGraphicsView in Qt 4.6.3 - 4.7.2 and have to argue that you can use the respective QScrollBar in the following way:
//graphics view initialization
QGraphicsView *graphicsView = new QGraphicsView(parent);
QGraphicsScene *scene = new QGraphicsScene(0,0,widthOfScene,heightOfScene,parent);
graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
graphicsView->setScene(scene);
//in another method
QScrollBar* yPos=graphicsView->verticalScrollBar();
yPos->setValue((int) newValue);
It does not matter if they are hidden or not. They will still respond to setValue(int) as long as you have a graphics scene that is larger than the graphics view.
The QGraphicsView will also respond to ensureVisible, which moves the scrollbars to the appropriate location.
You are not supposed to call scrollContentsBy as explained here: http://qt-project.org/doc/qt-4.8/qabstractscrollarea.html#scrollContentsBy
I don't know if you can still call the hidden scrollbar to scroll it. If not, translate is an option.
Did you try to use the scroll bars? Hiding them doesn't make them non-existent, and the documentation says you should use QScrollBar::setValue to scroll to a given position.
Another option would be to use QGraphicsView::centerOn(QPointF) in conjunction with the current center point -- as you've also tried -- but directly calculating the center point within your method (do not precalculate and store the center point), by using QGraphicsView::mapToScene(int,int):
void CustomView::horizontalScroll(int dx)
{
QPointF viewCenter = mapToScene(width() / 2, height() / 2);
viewCenter += QPointF(dx, 0); // Why did you subtract instead of add dx?
centerOn(viewCenter); // BTW, you don't need to do .x(), .y()
// You can remove update(); as this is already called in centerOn().
}
Please note that if you have, as you said, "scrollContentsBy(int dx, int dy) overrided to emit horizontalScroll(dx)", you also have to call the super class method so that the view can scroll itself:
void CustomView::scrollContentsBy(int dx, int dy)
{
emit horizontalScrolled(dx); // (You should call it different than the slot!)
QGraphicsView::scrollContentsBy(dx, dy); // <-- This is what I mean!
}