Grid in GraphicsView - c++

I want to implement grid in my graphicsView such that it fits to the graphicsView automatically and when I zoom in the graphicsView only the block size of grid should increase but not the line width of the grid. I tried the following but nothing happened.
void CadGraphicsScene::grid(QPainter *painter, const QRectF &rect)
{
QPen pen;
painter->setPen(pen);
qreal left = int(rect.left()) - (int(rect.left()) % gridSize);
qreal top = int(rect.top()) - (int(rect.top()) % gridSize);
QVector<QPointF> points;
for (qreal x = left; x < rect.right(); x += gridSize){
for (qreal y = top; y < rect.bottom(); y += gridSize){
points.append(QPointF(x,y));
}
}
painter->drawPoints(points.data(), points.size());
}
Please help me out to make a grid.

1) Use cosmetic pen (with zero width)
2) By QT idiom graphics scene are independent from view (about your question of zoom in graphicsview), but you can extract zoom coefficients of view from passed QPainter object (QPainter *painter) - QPainter::worldTransform -> QTransform::m11 (horz_Scale) & QTransform::m22 (vert_Scale) - in this case you can recalculate grid anchors (for 100% zoom QTransform::m11 == QTransform::m22 == 1.) on 'fly'

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"

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?

the zoom in and zoom out a Qgraphicsscene with QSlider

I have a QGraphicsScene and I want to do zooming (in and out) with a QSlider, I have this code:
void fonction(){
Scene = new QGraphicsScene(this);
ui->graphicsView->setScene(Scene);
QPen Pen ;
QBrush Brush(Qt::red) ;
Pen.setWidth(5);
ellipse = Scene->addEllipse(2,2,30,30,Pen,Brush);
}
void HomePage::on_verticalSlider_valueChanged(int value)
{
float z ;
if(value==0 )
z=0.1;
else
z = value*0.01;
Scene->update();
ui->graphicsView->transform();
ui->graphicsView->scale(z,z);
}
The interval of QSlider is [0.1 -> 1 ]
My problem is the zoom out does not work, why ?
how do I resolve this problem?
Your problem is that the matrix controlling the view of the scene does not reset every update of the slider, it is just a running multiplication. A simple approach would be to save the current value in a member variable for HomePage (lets call it m_current_scale) and undo the zoom before you do another one.
void HomePage::on_verticalSlider_valueChanged(int value)
{
float z ;
if(value==0 )
z=0.1;
else
z = value*0.01;
Scene->update();
ui->graphicsView->transform();
const double scale_inverse = 1.0 / (double)m_current_scale;
ui->graphicsView->scale(scale_inverse, scale_inverse);
m_current_scale = z;
ui->graphicsView->scale(z,z);
}

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

QT QGraphicsItemAnimation

I've got a class: mySquare which inherits from QGraphicsRectItem
added only my constructor, painter and an animation:
ANIMATION:
void mySquare::animation(mySquare *k)
{
QTimeLine *timeLine = new QTimeLine();
timeLine->setLoopCount(1);
QGraphicsItemAnimation *animation = new QGraphicsItemAnimation();
animation->setItem(k);
animation->setTimeLine(timeLine);
int value = 30;
animation->setTranslationAt(0.3, value, value);
timeLine->start();
// (*)
// x += 30;
// y += 30;
}
PAINTER:
void Klocek::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *widget)
{
bokKwadratu = (min(widget->width(), widget->height()))/5;
setRect(x * 30, y * 30, 30 - 3, 30 - 3);
QRectF rect = boundingRect();
painter->setBrush(brush);
painter->setPen(pen);
QFont font;
font.setPixelSize(bokKwadratu/3);
painter->setFont(font);
painter->drawRect(rect);
painter->drawText(rect,Qt::AlignCenter, QString::number(wartosc));
}
CONSTRUCTOR:
mySquare::mySquare(qreal x, qreal y) : QGraphicsRectItem(x * 10, y * 10, 10, 10)
{
setAcceptHoverEvents(true);
this->x = x;
this->y = y;
pen.setColor(Qt::red);
pen.setWidth(2);
brush.setColor(Qt::blue);
brush.setStyle(Qt::SolidPattern);
}
after performing animation (translation) I need to change the object coordinates so they are compatible which the situation on the screen. In other words after the translation (30, 30) I want the coordinates of the rectangle to be change (x += 30, y += 30)
my problem is that when i try to do this ( (*) fragment in the code) the triangle is put far away from its position (just as if the translation was performed twice)
My question is how to translate it and change the coordinates without such complications.
To begin with, I think you're misunderstanding the use of the function setTranslationAt in the QGraphicsItem Animation.
An animation has a normalised value over time, so can start at 0.0 and end at 1.0 (or the reverse). Therefore, by calling
animation->setTranslationAt(0.3, value, value);
You've stated that at the point when the normalised value reaches 0.3, you want the x and y position to be set to 'value'. That's fine, but you also need to set other values for the animation to occur (especially at the val of 1.0!). If you use a for loop, you can iterate through values from 0.0 to 1.0 and set where you want the position of your item to be. Take a look at the example code in the Qt help files for QGraphicsItemAnimation. The QGraphicsItemAnimation uses interpolation to work out the position of your object between the known points that you've given it. If you're interested: -
http://en.wikipedia.org/wiki/Linear_interpolation
Secondly, the item's rect is the definition of the item in its local coordinate space. So if you wanted a rect with its axis in the centre, you'd define it with x,y,w,h of (-w/2, -h/2, w, h). As these are local coordinates, they then get mapped into world coordinates in the GraphicsScene, which is where you set its actual position in the world.
Once you've setup your QGraphicsItemRect's local coordinates and world position, you can then simply paint it with drawRect and should not be setting positions in the paint function.