Qt: resizing a QLabel containing a QPixmap while keeping its aspect ratio - c++

I use a QLabel to display the content of a bigger, dynamically changing QPixmap to the user. It would be nice to make this label smaller/larger depending on the space available. The screen size is not always as big as the QPixmap.
How can I modify the QSizePolicy and sizeHint() of the QLabel to resize the QPixmap while keeping the aspect ratio of the original QPixmap?
I can't modify sizeHint() of the QLabel, setting the minimumSize() to zero does not help. Setting hasScaledContents() on the QLabel allows growing, but breaks the aspect ratio thingy...
Subclassing QLabel did help, but this solution adds too much code for just a simple problem...
Any smart hints how to accomplish this without subclassing?

In order to change the label size you can select an appropriate size policy for the label like expanding or minimum expanding.
You can scale the pixmap by keeping its aspect ratio every time it changes:
QPixmap p; // load pixmap
// get label dimensions
int w = label->width();
int h = label->height();
// set a scaled pixmap to a w x h window keeping its aspect ratio
label->setPixmap(p.scaled(w,h,Qt::KeepAspectRatio));
There are two places where you should add this code:
When the pixmap is updated
In the resizeEvent of the widget that contains the label

I have polished this missing subclass of QLabel. It is awesome and works well.
aspectratiopixmaplabel.h
#ifndef ASPECTRATIOPIXMAPLABEL_H
#define ASPECTRATIOPIXMAPLABEL_H
#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>
class AspectRatioPixmapLabel : public QLabel
{
Q_OBJECT
public:
explicit AspectRatioPixmapLabel(QWidget *parent = 0);
virtual int heightForWidth( int width ) const;
virtual QSize sizeHint() const;
QPixmap scaledPixmap() const;
public slots:
void setPixmap ( const QPixmap & );
void resizeEvent(QResizeEvent *);
private:
QPixmap pix;
};
#endif // ASPECTRATIOPIXMAPLABEL_H
aspectratiopixmaplabel.cpp
#include "aspectratiopixmaplabel.h"
//#include <QDebug>
AspectRatioPixmapLabel::AspectRatioPixmapLabel(QWidget *parent) :
QLabel(parent)
{
this->setMinimumSize(1,1);
setScaledContents(false);
}
void AspectRatioPixmapLabel::setPixmap ( const QPixmap & p)
{
pix = p;
QLabel::setPixmap(scaledPixmap());
}
int AspectRatioPixmapLabel::heightForWidth( int width ) const
{
return pix.isNull() ? this->height() : ((qreal)pix.height()*width)/pix.width();
}
QSize AspectRatioPixmapLabel::sizeHint() const
{
int w = this->width();
return QSize( w, heightForWidth(w) );
}
QPixmap AspectRatioPixmapLabel::scaledPixmap() const
{
return pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
void AspectRatioPixmapLabel::resizeEvent(QResizeEvent * e)
{
if(!pix.isNull())
QLabel::setPixmap(scaledPixmap());
}
Hope that helps!
(Updated resizeEvent, per #dmzl's answer)

I just use contentsMargin to fix the aspect ratio.
#pragma once
#include <QLabel>
class AspectRatioLabel : public QLabel
{
public:
explicit AspectRatioLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
~AspectRatioLabel();
public slots:
void setPixmap(const QPixmap& pm);
protected:
void resizeEvent(QResizeEvent* event) override;
private:
void updateMargins();
int pixmapWidth = 0;
int pixmapHeight = 0;
};
#include "AspectRatioLabel.h"
AspectRatioLabel::AspectRatioLabel(QWidget* parent, Qt::WindowFlags f) : QLabel(parent, f)
{
}
AspectRatioLabel::~AspectRatioLabel()
{
}
void AspectRatioLabel::setPixmap(const QPixmap& pm)
{
pixmapWidth = pm.width();
pixmapHeight = pm.height();
updateMargins();
QLabel::setPixmap(pm);
}
void AspectRatioLabel::resizeEvent(QResizeEvent* event)
{
updateMargins();
QLabel::resizeEvent(event);
}
void AspectRatioLabel::updateMargins()
{
if (pixmapWidth <= 0 || pixmapHeight <= 0)
return;
int w = this->width();
int h = this->height();
if (w <= 0 || h <= 0)
return;
if (w * pixmapHeight > h * pixmapWidth)
{
int m = (w - (pixmapWidth * h / pixmapHeight)) / 2;
setContentsMargins(m, 0, m, 0);
}
else
{
int m = (h - (pixmapHeight * w / pixmapWidth)) / 2;
setContentsMargins(0, m, 0, m);
}
}
Works perfectly for me so far. You're welcome.

Adapted from Timmmm to PYQT5
from PyQt5.QtGui import QPixmap
from PyQt5.QtGui import QResizeEvent
from PyQt5.QtWidgets import QLabel
class Label(QLabel):
def __init__(self):
super(Label, self).__init__()
self.pixmap_width: int = 1
self.pixmapHeight: int = 1
def setPixmap(self, pm: QPixmap) -> None:
self.pixmap_width = pm.width()
self.pixmapHeight = pm.height()
self.updateMargins()
super(Label, self).setPixmap(pm)
def resizeEvent(self, a0: QResizeEvent) -> None:
self.updateMargins()
super(Label, self).resizeEvent(a0)
def updateMargins(self):
if self.pixmap() is None:
return
pixmapWidth = self.pixmap().width()
pixmapHeight = self.pixmap().height()
if pixmapWidth <= 0 or pixmapHeight <= 0:
return
w, h = self.width(), self.height()
if w <= 0 or h <= 0:
return
if w * pixmapHeight > h * pixmapWidth:
m = int((w - (pixmapWidth * h / pixmapHeight)) / 2)
self.setContentsMargins(m, 0, m, 0)
else:
m = int((h - (pixmapHeight * w / pixmapWidth)) / 2)
self.setContentsMargins(0, m, 0, m)

I tried using phyatt's AspectRatioPixmapLabel class, but experienced a few problems:
Sometimes my app entered an infinite loop of resize events. I traced this back to the call of QLabel::setPixmap(...) inside the resizeEvent method, because QLabel actually calls updateGeometry inside setPixmap, which may trigger resize events...
heightForWidth seemed to be ignored by the containing widget (a QScrollArea in my case) until I started setting a size policy for the label, explicitly calling policy.setHeightForWidth(true)
I want the label to never grow more than the original pixmap size
QLabel's implementation of minimumSizeHint() does some magic for labels containing text, but always resets the size policy to the default one, so I had to overwrite it
That said, here is my solution. I found that I could just use setScaledContents(true) and let QLabel handle the resizing.
Of course, this depends on the containing widget / layout honoring the heightForWidth.
aspectratiopixmaplabel.h
#ifndef ASPECTRATIOPIXMAPLABEL_H
#define ASPECTRATIOPIXMAPLABEL_H
#include <QLabel>
#include <QPixmap>
class AspectRatioPixmapLabel : public QLabel
{
Q_OBJECT
public:
explicit AspectRatioPixmapLabel(const QPixmap &pixmap, QWidget *parent = 0);
virtual int heightForWidth(int width) const;
virtual bool hasHeightForWidth() { return true; }
virtual QSize sizeHint() const { return pixmap()->size(); }
virtual QSize minimumSizeHint() const { return QSize(0, 0); }
};
#endif // ASPECTRATIOPIXMAPLABEL_H
aspectratiopixmaplabel.cpp
#include "aspectratiopixmaplabel.h"
AspectRatioPixmapLabel::AspectRatioPixmapLabel(const QPixmap &pixmap, QWidget *parent) :
QLabel(parent)
{
QLabel::setPixmap(pixmap);
setScaledContents(true);
QSizePolicy policy(QSizePolicy::Maximum, QSizePolicy::Maximum);
policy.setHeightForWidth(true);
this->setSizePolicy(policy);
}
int AspectRatioPixmapLabel::heightForWidth(int width) const
{
if (width > pixmap()->width()) {
return pixmap()->height();
} else {
return ((qreal)pixmap()->height()*width)/pixmap()->width();
}
}

If your image is a resource or a file you don't need to subclass anything; just set image in the label's stylesheet; and it will be scaled to fit the label while keeping its aspect ratio, and will track any size changes made to the label. You can optionally use image-position to move the image to one of the edges.
It doesn't fit the OP's case of a dynamically updated pixmap (I mean, you can set different resources whenever you want but they still have to be resources), but it's a good method if you're using pixmaps from resources.
Stylesheet example:
image: url(:/resource/path);
image-position: right center; /* optional: default is centered. */
In code (for example):
QString stylesheet = "image:url(%1);image-position:right center;";
existingLabel->setStyleSheet(stylesheet.arg(":/resource/path"));
Or you can just set the stylesheet property right in Designer:
Icon source: Designspace Team via Flaticon
The caveat is that it won't scale the image larger, only smaller, so make sure your image is bigger than your range of sizes if you want it to grow (note that it can support SVG, which can improve quality).
The label's size can be controlled as per usual: either use size elements in the stylesheet or use the standard layout and size policy strategies.
See the documentation for details.
This style has been present since early Qt (position was added in 4.3 circa 2007 but image was around before then).

I finally got this to work as expected. It is essential to override sizeHint as well as resizeEvent, and to set the minimum size and the size policy.
setAlignment is used to centre the image in the control either horizontally or vertically when the control is a different aspect ratio to the image.
class ImageDisplayWidget(QLabel):
def __init__(self, max_enlargement=2.0):
super().__init__()
self.max_enlargement = max_enlargement
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setAlignment(Qt.AlignCenter)
self.setMinimumSize(1, 1)
self.__image = None
def setImage(self, image):
self.__image = image
self.resize(self.sizeHint())
self.update()
def sizeHint(self):
if self.__image:
return self.__image.size() * self.max_enlargement
else:
return QSize(1, 1)
def resizeEvent(self, event):
if self.__image:
pixmap = QPixmap.fromImage(self.__image)
scaled = pixmap.scaled(event.size(), Qt.KeepAspectRatio)
self.setPixmap(scaled)
super().resizeEvent(event)

The Qt documentations has an Image Viewer example which demonstrates handling resizing images inside a QLabel. The basic idea is to use QScrollArea as a container for the QLabel and if needed use label.setScaledContents(bool) and scrollarea.setWidgetResizable(bool) to fill available space and/or ensure QLabel inside is resizable.
Additionally, to resize QLabel while honoring aspect ratio use:
label.setPixmap(pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::FastTransformation));
The width and height can be set based on scrollarea.width() and scrollarea.height().
In this way there is no need to subclass QLabel.

Nothing new here really.
I mixed the accepted reply
https://stackoverflow.com/a/8212120/11413792
and
https://stackoverflow.com/a/43936590/11413792
which uses setContentsMargins,
but just coded it a bit my own way.
/**
* #brief calcMargins Calculate the margins when a rectangle of one size is centred inside another
* #param outside - the size of the surrounding rectanle
* #param inside - the size of the surrounded rectangle
* #return the size of the four margins, as a QMargins
*/
QMargins calcMargins(QSize const outside, QSize const inside)
{
int left = (outside.width()-inside.width())/2;
int top = (outside.height()-inside.height())/2;
int right = outside.width()-(inside.width()+left);
int bottom = outside.height()-(inside.height()+top);
QMargins margins(left, top, right, bottom);
return margins;
}
A function calculates the margins required to centre one rectangle inside another. Its a pretty generic function that could be used for lots of things though I have no idea what.
Then setContentsMargins becomes easy to use with a couple of extra lines
which many people would combine into one.
QPixmap scaled = p.scaled(this->size(), Qt::KeepAspectRatio);
QMargins margins = calcMargins(this->size(), scaled.size());
this->setContentsMargins(margins);
setPixmap(scaled);
It may interest somebody ... I needed to handle mousePressEvent and to know where I am within the image.
void MyClass::mousePressEvent(QMouseEvent *ev)
{
QMargins margins = contentsMargins();
QPoint labelCoordinateClickPos = ev->pos();
QPoint pixmapCoordinateClickedPos = labelCoordinateClickPos - QPoint(margins.left(),margins.top());
... more stuff here
}
My large image was from a camera and I obtained the relative coordinates [0, 1) by dividing by the width of the pixmap and then multiplied up by the width of the original image.

This is the port of #phyatt's class to PySide2.
Apart from porting i added an additional aligment in the resizeEvent in order to make the newly resized image position properly in the available space.
from typing import Union
from PySide2.QtCore import QSize, Qt
from PySide2.QtGui import QPixmap, QResizeEvent
from PySide2.QtWidgets import QLabel, QWidget
class QResizingPixmapLabel(QLabel):
def __init__(self, parent: Union[QWidget, None] = ...):
super().__init__(parent)
self.setMinimumSize(1,1)
self.setScaledContents(False)
self._pixmap: Union[QPixmap, None] = None
def heightForWidth(self, width:int) -> int:
if self._pixmap is None:
return self.height()
else:
return self._pixmap.height() * width / self._pixmap.width()
def scaledPixmap(self) -> QPixmap:
scaled = self._pixmap.scaled(
self.size() * self.devicePixelRatioF(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
scaled.setDevicePixelRatio(self.devicePixelRatioF());
return scaled;
def setPixmap(self, pixmap: QPixmap) -> None:
self._pixmap = pixmap
super().setPixmap(pixmap)
def sizeHint(self) -> QSize:
width = self.width()
return QSize(width, self.heightForWidth(width))
def resizeEvent(self, event: QResizeEvent) -> None:
if self._pixmap is not None:
super().setPixmap(self.scaledPixmap())
self.setAlignment(Qt.AlignCenter)

Related

Qt - Resize Text on QGraphicsScene after drawing

Im doing a little Plotting program with a group of students , we are using Qt's QGraphicsScene on a QGraphicsView to let the user plot custom Points on Specific Positions (x , y) , each point has to have a text on top of it.
Here is the function responsible for adding Points to the Scene :
void MainWindow::AddPoint(float x, float y, QString name)
{
y = y * -1; // To Flip Y-Axis
float Radius = 1; // Point's (Eclipse) Radius
QGraphicsItem *Point = p_Scene->addEllipse(x , y , Radius , Radius , QPen(QColor(Qt::red)) , QBrush(QColor(Qt::red))); // Creates a Red Colored Point on the given Coordinates (x , y)
/*
QGraphicsTextItem *Text = p_Scene->addText(name); // Creates a Text
Text->setDefaultTextColor(QColor(Qt::red)); // Sets Text's Color to Red
Text->setFont(QFont("Courier New" , 4)); // Sets Text's Font Size
Text->setPos(x , y - 10); // Set Text's Position (On top of the Point)
ui->graphicsView->setScene(p_Scene); // Adds Text to the Scene
*/
}
so the Implementation would be like :
AddPoint(0 , 0 , "P1"); // Point 1
AddPoint(50 , 100 , "P2"); // Point 2
AddPoint(100 , 0 , "P3"); // Point 3
This will results in :
We are using :
ui->graphicsView->fitInView(ui->graphicsView->scene()->sceneRect() , Qt::KeepAspectRatio);
to make sure that QGraphicsView shows only whats visible (pretty important).
so the problem here is , if we were to make the drawing larger , say for example :
AddPoint(0 , 0 , "P1");
AddPoint(0 , 1000 , "P2"); // y = 1000
This will draw a very long line which will make the Points + Text we created so small that it cant even be seen :
So what we need here is to somehow calculate the SceneRect (i think) and find out the radius value + font size that we should use for both the Point and the Text so they stay the same size regardless of the Scene's Size.
EDIT :
This is the NEW code (according to vcloarec's solution) :
GraphicsWindow.h (QGraphicsView Subclass) :
#ifndef GRAPHICSVIEW_H
#define GRAPHICSVIEW_H
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QDebug>
class GraphicsView : public QGraphicsView
{
Q_OBJECT
public:
explicit GraphicsView(QWidget *parent = 0);
void AddPoint(float x , float y , QString name = "");
void resize();
private:
QGraphicsScene *p_Scene;
int p_SizeInView;
};
#endif // GRAPHICSVIEW_H
GraphicsWindow.cpp :
#include "GraphicsView.h"
GraphicsView::GraphicsView(QWidget *parent) : QGraphicsView(parent)
{
p_PointRadius = 0.0;
p_PointsLastN = 0;
p_SizeInView = 5;
p_Scene = new QGraphicsScene(this);
this->setScene(p_Scene);
}
void GraphicsView::AddPoint(float x, float y, QString name)
{
y = y * -1;
QGraphicsItem *_Point = p_Scene->addEllipse(x , y , 1 , 1 , QPen(QColor(Qt::red)) , QBrush(QColor(Qt::red)));
this->fitInView(scene()->sceneRect() , Qt::KeepAspectRatio);
resize();
}
void GraphicsView::resize()
{
qreal scale = p_SizeInView / this->transform().m11();
for(int i = 0; i < this->scene()->items().count(); i++)
{
this->scene()->items().at(i)->setScale(scale);
}
}
MainWindow.cpp :
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->toolBar->addWidget(ui->ZoomUp_Button);
ui->toolBar->addWidget(ui->ZoomDown_Button);
setCentralWidget(ui->graphicsView);
ui->graphicsView->AddPoint(0 , 0);
ui->graphicsView->AddPoint(1000 , 0);
ui->graphicsView->AddPoint(1000 , 50);
ui->graphicsView->AddPoint(0 , 50);
}
MainWindow::~MainWindow()
{
delete ui;
}
This code scales the Points according to a fixed Scale but still results in Scrollbars which is something we have to solve.
Somehow it ignores fitInView() , OR it does actually fit it but when the Points are resized it resizes the SceneRect or something
Here is the result :
PS : We subclassed QGraphicsView because we will need MouseEvents and other things later.
EDIT : Solved by vcloarec :
The solution was to insert the Points at (-0.5 , -0.5) and than setPose(x , y) which will set the Position to the x , y we pass to the AddPoint(x , y).
The Points now keep the same size regardless of the Scene's size , and it will show all the Points created at once without any scrollbars or anything.
Thank You !
The dimensions of the points end the text is define in the scene coordinate, not in the viewport (the windows) coordinate .
If you want the points and text keep their dimension on the display, you have to update the dimension depending of the "zoom" of your viewport.
Edit :
I try an analogy :
QGraphicsView is a camera
QGraphicScene is the real life
QGraphicsItems are people ant things
If you want to see a particular part of the scene, you use QGraphicsView::setSceneRect(const QRectF & rect) to "zoom" on the part define by rect.
When you "zoom" or "unzoom" with the camera on objects, this objects don't change their size in the real life, but in the screen the size change. It is the same behaviour with QGraphicsView.
If you want a fix size of the representation of your object, you have to adapt the size of your object with the scale of your scene. In your example with addPoint(0 , 0 , "P1"); addPoint(0 , 1000 , "P2"), the two points are far away from each other, and the points and texts are very small in comparison of this distance.
The solution of your problem depends of the type of the representation you want (dynamic, static,...)
Maybe you can use the matrix returned by QTransform QGraphicsView::transform() const and their diagonal elements to find the scale to use.
Look at this :
class MyView:public QGraphicsView
{
public:
MyView(QWidget *parent):QGraphicsView(parent),sizeInView(5) {}
void resize();
protected:
void wheelEvent(QWheelEvent *event);
private:
int sizeInView;
};
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void putPoint();
private:
Ui::MainWindow *ui;
QGraphicsScene *scene;
MyView *view;
};
And
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
view=new MyView(this);
centralWidget()->setLayout(new QHBoxLayout());
centralWidget()->layout()->addWidget(view);
scene=new QGraphicsScene(this);
view->setScene(scene);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::putPoint()
{
QGraphicsEllipseItem *point1= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point1->setPos(0,0);
QGraphicsEllipseItem *point2= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point2->setPos(0,100);
QGraphicsEllipseItem *point3= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point3->setPos(0,1000);
QGraphicsEllipseItem *point4= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point4->setPos(100,0);
QGraphicsEllipseItem *point5= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point5->setPos(100,100);
QGraphicsEllipseItem *point6= new QGraphicsEllipseItem(-0.5,-0.5,1,1);
point6->setPos(100,1000);
scene->addItem(point1);
scene->addItem(point2);
scene->addItem(point3);
scene->addItem(point4);
scene->addItem(point5);
scene->addItem(point6);
view->fitInView(scene->sceneRect(),Qt::KeepAspectRatio);
view->resize();
}
void MyView::resize()
{
qreal scale=sizeInView/transform().m11();
for (int i=0;i<scene()->items().count();++i)
scene()->items().at(i)->setScale(scale);
}
void MyView::wheelEvent(QWheelEvent *event)
{
float fact=1.5;
if (event->delta()>=120)
{
setTransform(transform()*fact);
resize();
}
if (event->delta()<=-120)
{
setTransform(transform()/fact);
resize();
}
}
Be careful, the insertion point of your QGraphicsItem in the scene is define by QGraphicsItem::setPos. The (x,y), you use when you create the point, is the position in the local coordinate system, not in the scene coordinate systeme and it is not the center on you point but the topleft rectangle containing the ellipse.
So if the center of your point is not on the point of insertion, when you resize, the point move ... That's why i place the point at (-0.5,-0.5) in local coordinate with a height and width equal to 1. Then, I place the point with setPos in the scene coordinate.
If you want to disable the scrollbar :
setHorizontalScrollBarPolicy ( Qt::ScrollBarAlwaysOff )

QGraphicsScene/View Scale Understanding

I'm lost with understanding the scale value of QGraphicsScene/View.
Here is how I'm placing my targets in the scene.
QPointF Mainwindow::pointLocation(double bearing, double range){
int offset = 90; //used to offset Cartesian system
double centerX = baseSceneSize/2;//push my center location out to halfway point
double centerY = baseSceneSize/2;
double newX = centerX + qCos(qDegreesToRadians(bearing - offset)) * range;
double newY = centerY + qSin(qDegreesToRadians(bearing - offset)) * range;
QPointF newPoint = QPointF(newX, newY);
return newPoint;
}
So each target has a bearing and range. As long as I don't scale, or zoom, the scene, these values work sufficiently. My problem is that I need to implement the zooming.
Here's where things go wrong:
I have a target at Bearing 270, Range 10.
When the app runs, and my vertical slider is at a value of zero, I can see this target in my view. I should not. I need for this target to only come into view when the slider has gotten to a value of 10. Just think each position value on the slider equates to 1 nautical mile. So if a target is at 10 NMs it should only be visible once the slider is >= 10.
here is how I'm doing the zooming:
void MainWindow:: on_PlotSlider_sliderMoved(int position){
const qreal factor = 1.01;
viewScaleValue = qPow(factor, -position);//-position to invert the scale
QMatrix matrix;
matrix.scale(viewScaleValue, viewScaleValue);
view->setMatrix(matrix);
}
I've tried making the View bigger, the Scene bigger, but nothing is having the proper effect.
Here is my Scene setup:
view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 355;
scene->setSceneRect(0,0,baseSize,baseSize);
baseSceneSize = scene->sceneRect().width();
view->setScene(scene);
How do I take the range of my target and push it out into the scene so that it lines up with the slider value?
QGraphicsView::fitInView is everything you need to select the displayed range and center the view.
Here's how you might do it. It's a complete example.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-radar-40680065
#include <QtWidgets>
#include <random>
First, let's obtain random target positions. The scene is scaled in e.g. Nautical Miles: thus any coordinate in the scene is meant to be in these units. This is only a convention: the scene otherwise doesn't care, nor does the view. The reference point is at 0,0: all ranges/bearings are relative to the origin.
QPointF randomPosition() {
static std::random_device dev;
static std::default_random_engine eng(dev());
static std::uniform_real_distribution<double> posDis(-100., 100.); // NM
return {posDis(eng), posDis(eng)};
}
Then, to aid in turning groups of scene items on and off (e.g. graticules), it helps to have an empty parent item for them:
class EmptyItem : public QGraphicsItem {
public:
QRectF boundingRect() const override { return QRectF(); }
void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
};
A scene manager sets up the display. The empty items act as item collections and they can be easily made hidden/visible without having to modify child items. They also enforce the relative Z-order of their children.
class SceneManager : public QObject {
Q_OBJECT
Q_PROPERTY(bool microGraticuleVisible READ microGraticuleVisible WRITE setMicroGraticuleVisible)
QGraphicsScene m_scene;
QPen m_targetPen{Qt::green, 1};
EmptyItem m_target, m_center, m_macroGraticule, m_microGraticule;
An event filter can be installed on the view to signal when the view has been resized. This can be used to keep the view centered in spite of resizing:
bool eventFilter(QObject *watched, QEvent *event) override {
if (event->type() == QEvent::Resize
&& qobject_cast<QGraphicsView*>(watched))
emit viewResized();
return QObject::eventFilter(watched, event);
}
Scene has the following Z-order: center cross, macro- and micro-graticule, then the targets are on top.
public:
SceneManager() {
m_scene.addItem(&m_center);
m_scene.addItem(&m_macroGraticule);
m_scene.addItem(&m_microGraticule);
m_scene.addItem(&m_target);
m_targetPen.setCosmetic(true);
addGraticules();
}
We can monitor a graphics view for resizing; we also expose the visibility of the micro graticule.
void monitor(QGraphicsView *view) { view->installEventFilter(this); }
QGraphicsScene * scene() { return &m_scene; }
Q_SLOT void setMicroGraticuleVisible(bool vis) { m_microGraticule.setVisible(vis); }
bool microGraticuleVisible() const { return m_microGraticule.isVisible(); }
Q_SIGNAL void viewResized();
Targets can be randomly generated. A target has a fixed size in view coordinates. Its position, though, is subject to any scene-to-view transformations.
The pens for targets and graticules are cosmetic pens: their width is given in the view device units (pixels), not scene units.
void newTargets(int count = 200) {
qDeleteAll(m_target.childItems());
for (int i = 0; i < count; ++i) {
auto target = new QGraphicsEllipseItem(-1.5, -1.5, 3., 3., &m_target);
target->setPos(randomPosition());
target->setPen(m_targetPen);
target->setBrush(m_targetPen.color());
target->setFlags(QGraphicsItem::ItemIgnoresTransformations);
}
}
The graticules are concentric circles centered at the origin (range reference point) and a cross at the origin. The origin cross has fixed size in view units - this is indicated by the ItemIgnoresTransformations flag.
void addGraticules() {
QPen pen{Qt::white, 1};
pen.setCosmetic(true);
auto center = {QLineF{-5.,0.,5.,0.}, QLineF{0.,-5.,0.,5.}};
for (auto l : center) {
auto c = new QGraphicsLineItem{l, &m_center};
c->setFlags(QGraphicsItem::ItemIgnoresTransformations);
c->setPen(pen);
}
for (auto range = 10.; range < 101.; range += 10.) {
auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_macroGraticule);
circle->setPen(pen);
}
pen = QPen{Qt::white, 1, Qt::DashLine};
pen.setCosmetic(true);
for (auto range = 2.5; range < 9.9; range += 2.5) {
auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_microGraticule);
circle->setPen(pen);
}
}
};
The mapping between the scene units and the view is maintained as follows:
Each time the view range is changed (from e.g. the combo box), the QGraphicsView::fitInView method is called with a rectangle in scene units (of nautical miles). This takes care of all of the scaling, centering, etc.. E.g. to select a range of 10NM, we'd call view.fitInView(QRect{-10.,-10.,20.,20.), Qt::KeepAspectRatio)
The graticule(s) can be disabled/enabled as appropriate for a given range to unclutter the view.
int main(int argc, char ** argv) {
QApplication app{argc, argv};
SceneManager mgr;
mgr.newTargets();
QWidget w;
QGridLayout layout{&w};
QGraphicsView view;
QComboBox combo;
QPushButton newTargets{"New Targets"};
layout.addWidget(&view, 0, 0, 1, 2);
layout.addWidget(&combo, 1, 0);
layout.addWidget(&newTargets, 1, 1);
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view.setBackgroundBrush(Qt::black);
view.setScene(mgr.scene());
view.setRenderHint(QPainter::Antialiasing);
mgr.monitor(&view);
combo.addItems({"10", "25", "50", "100"});
auto const recenterView = [&]{
auto range = combo.currentText().toDouble();
view.fitInView(-range, -range, 2.*range, 2.*range, Qt::KeepAspectRatio);
mgr.setMicroGraticuleVisible(range <= 20.);
};
QObject::connect(&combo, &QComboBox::currentTextChanged, recenterView);
QObject::connect(&mgr, &SceneManager::viewResized, recenterView);
QObject::connect(&newTargets, &QPushButton::clicked, [&]{ mgr.newTargets(); });
w.show();
return app.exec();
}
#include "main.moc"
So as Kuba suggested, I was overcomplicating this a bit. With his help this is what ended up getting me the result I needed. Not 100% sure on some of it, but for now it's working the way I need it to.
view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 1000; // MAGIC value that works, anything other than this, not so much
view->setSceneRect(0,0,baseSize,baseSize);
baseViewSize = view->sceneRect().width();
view->setScene(scene);
My drawPoint method works fine, no changes were needed.
Finally, here is my slider
void MainWindow:: on_PlotSlider_sliderMoved(int position){
const qreal factor = 1.01;
viewScaleValue = qPow(factor, -position);//-position to invert the scale
QMatrix matrix;
// below is the update, again 6 is a MAGIC number, no clue why 6 works...
matrix.scale((baseViewSize/6 / position, baseViewSize/6 / position);
view->setMatrix(matrix);
}
While my problem is solved, I would love some explanation as to my 2 MAGIC numbers.
Why does it all only work is the baseSize is 1000?
Why does it only scale correctly if I divide the BaseViewSize by 6?

Keeping the aspect ratio of a sub-classed QWidget during resize

I'm creating a new widget, by subclassing the QWidget class. I'd like to be able to set a ratio (for its height and its width) for this widget, which will always be maintained.
For this, I've always searched, using the Qt5 documentation, Google, and Stackoverflow. Obviously, I've found answers: in particular, this one. But, unfortunately, not even one is fully effective:
Setting the sizeIncrement does totally nothing, even if the widget is a window
I tried to overload resizeEvent, but I really don't know how to do this...
If I follow this answer, two things:
If the widget is a top-level window, the ratio isn't maintained at all, I can resize it as I want.
If I place this widget in a layout, if I just increase both width and height of the window, the ratio is maintained. But as soon as I increase the width or the height to much, the widget is flattened. Instead, I would like that the layout automatically adjust its size to keep the widget's ratio.
So, how could I manage to keep the aspect ratio of a subclassed QWidget?
Create a parent widget (e.g., AspectRatioWidget) in which to place your widget. For the parent widget, subclass QWidget and give it a QBoxLayout. Put your widget into the center, and QSpacerItems on either side. Then in the parent widget's QWidget::resizeEvent adjust the direction and stretches as needed. I've provided an example below. To use, just create an instance of AspectRatioWidget and pass the constructor a pointer to your widget and the desired aspect ratio.
// header
class AspectRatioWidget : public QWidget
{
public:
AspectRatioWidget(QWidget *widget, float width, float height, QWidget *parent = 0);
void resizeEvent(QResizeEvent *event);
private:
QBoxLayout *layout;
float arWidth; // aspect ratio width
float arHeight; // aspect ratio height
};
// cpp
AspectRatioWidget::AspectRatioWidget(QWidget *widget, float width, float height, QWidget *parent) :
QWidget(parent), arWidth(width), arHeight(height)
{
layout = new QBoxLayout(QBoxLayout::LeftToRight, this);
// add spacer, then your widget, then spacer
layout->addItem(new QSpacerItem(0, 0));
layout->addWidget(widget);
layout->addItem(new QSpacerItem(0, 0));
}
void AspectRatioWidget::resizeEvent(QResizeEvent *event)
{
float thisAspectRatio = (float)event->size().width() / event->size().height();
int widgetStretch, outerStretch;
if (thisAspectRatio > (arWidth/arHeight)) // too wide
{
layout->setDirection(QBoxLayout::LeftToRight);
widgetStretch = height() * (arWidth/arHeight); // i.e., my width
outerStretch = (width() - widgetStretch) / 2 + 0.5;
}
else // too tall
{
layout->setDirection(QBoxLayout::TopToBottom);
widgetStretch = width() * (arHeight/arWidth); // i.e., my height
outerStretch = (height() - widgetStretch) / 2 + 0.5;
}
layout->setStretch(0, outerStretch);
layout->setStretch(1, widgetStretch);
layout->setStretch(2, outerStretch);
}
I have rewritten Anthony's code in Python/PySide2:
from PySide2.QtWidgets import QBoxLayout, QSpacerItem, QWidget
class AspectRatioWidget(QWidget):
def __init__(self, widget, parent):
super().__init__(parent)
self.aspect_ratio = widget.size().width() / widget.size().height()
self.setLayout(QBoxLayout(QBoxLayout.LeftToRight, self))
# add spacer, then widget, then spacer
self.layout().addItem(QSpacerItem(0, 0))
self.layout().addWidget(widget)
self.layout().addItem(QSpacerItem(0, 0))
def resizeEvent(self, e):
w = e.size().width()
h = e.size().height()
if w / h > self.aspect_ratio: # too wide
self.layout().setDirection(QBoxLayout.LeftToRight)
widget_stretch = h * self.aspect_ratio
outer_stretch = (w - widget_stretch) / 2 + 0.5
else: # too tall
self.layout().setDirection(QBoxLayout.TopToBottom)
widget_stretch = w / self.aspect_ratio
outer_stretch = (h - widget_stretch) / 2 + 0.5
self.layout().setStretch(0, outer_stretch)
self.layout().setStretch(1, widget_stretch)
self.layout().setStretch(2, outer_stretch)

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);
}

Fixed QGraphicsItem position, without changing behaviour of other QGraphicsItems in scene

This question is related to: Forcing QGraphicsItem To Stay Put
I'd like to have a QGraphicsItem on a fixed location when moving around in the scene.
The suggested solution is to override the void paintEvent(QPaintEvent*) of the sub-classed QGraphicsView.
void MyGraphicsView::paintEvent(QPaintEvent*) {
QPointF scenePos = mapToScene(0,0); // map viewport's top-left corner to scene
myItem->setPos(scenePos);
}
However, the problem is that I want everything else in the scene to stay intact, i.e. if I zoom or move I want all other QGraphicsItems to behave as default.
One poor way of solving this is to call void QGraphicsView::paintEvent(QPaintEvent*) from within void MyGraphicsView::paintEvent(QPaintEvent*).
void MyGraphicsView::paintEvent(QPaintEvent* event) {
QGraphicsView::paintEvent(event);
QPointF scenePos = mapToScene(0,0); // map viewport's top-left corner to scene
myItem->setPos(scenePos);
}
However, this adds a flickering behaviour to my_item since it's positioned first using QGraphicsView::paintEvent(event); and then using the added code
QPointF scenePos = mapToScene(0,0); // map viewport's top-left corner to scene
myItem->setPos(scenePos);
The question is, do I have to re-implement void MyGraphicsView::paintEvent(QPaintEvent*) from scratch and write code for both the desired behaviour of myItem and the default behaviour of all other QGraphicsItems, or is there an easier way to do this?
Thank you.
I think this is what you are looking for:
http://qt-project.org/doc/qt-4.8/qgraphicsitem.html#setFlag
QGraphicsItem::ItemIgnoresTransformations
Description from the docs:
The item ignores inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). This flag is useful for keeping text label items horizontal and unscaled, so they will still be readable if the view is transformed. When set, the item's view geometry and scene geometry will be maintained separately. You must call deviceTransform() to map coordinates and detect collisions in the view. By default, this flag is disabled. This flag was introduced in Qt 4.3. Note: With this flag set you can still scale the item itself, and that scale transformation will influence the item's children.
You may also want to parent everything that does pan around to something else. Then, you move or scale or rotate a single graphics group to affect everything except your "un-transformable" objects.
https://qt-project.org/doc/qt-4.8/graphicsview.html#the-graphics-view-coordinate-system
https://qt-project.org/doc/qt-4.8/painting-transformations.html (a cool example, though it doesn't show this feature really)
http://qt-project.org/doc/qt-4.8/demos-chip.html (great example of using QGraphicsView)
Hope that helps.
EDIT:
Example showing how you can achieve a static layer using parenting:
main.cpp
#include <QApplication>
#include "mygraphicsview.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyGraphicsView w;
w.show();
return a.exec();
}
mygraphicsview.h
#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H
#include <QGraphicsView>
#include <QGraphicsItemGroup>
#include <QMouseEvent>
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
MyGraphicsView(QWidget *parent = 0);
~MyGraphicsView();
public slots:
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
private:
bool down;
QPointF m_last_pos;
QGraphicsItemGroup * m_group;
};
#endif // MYGRAPHICSVIEW_H
mygraphicsview.cpp
#include "mygraphicsview.h"
#include <QGraphicsItem>
#include <QGraphicsEllipseItem>
#include <QGraphicsTextItem>
MyGraphicsView::MyGraphicsView(QWidget *parent)
: QGraphicsView(parent)
{
down = false;
this->setScene(new QGraphicsScene);
// Anything not added to the "group" will stay put
this->scene()->addEllipse(20, 20, 50, 50);
this->scene()->addEllipse(180, 180, 50, 50);
this->scene()->addText("Click and drag with the mouse to move only the tiny dots.");
// This group will receive all transformations
m_group = new QGraphicsItemGroup;
for(int r = 0; r < 20; r ++)
{
for(int c = 0; c < 20; c++)
{
if(c % 5 == 0 && r % 5 == 0)
{
QGraphicsTextItem * txt = new QGraphicsTextItem(QString::number(r) + "," + QString::number(c));
m_group->addToGroup(txt);
txt->setPos(r*100, c*100);
}
m_group->addToGroup(new QGraphicsEllipseItem(r *100, c*100, 5, 5));
}
}
this->scene()->addItem(m_group);
}
MyGraphicsView::~MyGraphicsView()
{
}
void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
m_last_pos = mapToScene(event->pos());
down = true;
}
void MyGraphicsView::mouseReleaseEvent(QMouseEvent *)
{
down = false;
}
void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
if(down)
{
QPointF temp = mapToScene(event->pos());
QPointF delta = temp - m_last_pos;
m_last_pos = temp;
// Apply transformation to the group, not the scene!
m_group->translate(delta.x(), delta.y());
}
}