Scaling a QGrahicsView when MouseWheenTurned first scrolls, then scales - c++

I'm trying to add zoom in/out functionality to a graphic I'm drawing in Qt.
What I first did was extend QGraphicsScene with my own class GraphicsScene and overload the wheel event.
class GraphicsScene : public QGraphicsScene
{
Q_OBJECT
public:
GraphicsScene(QObject *parent, bool drawAxes){ /*drawing stuff here.. */}
virtual void wheelEvent(QGraphicsSceneWheelEvent *mouseEvent);
signals:
void mouseWheelTurned(int);
};
void GraphicsScene::wheelEvent(QGraphicsSceneWheelEvent* mouseEvent) {
int numDegrees = mouseEvent->delta() / 8;
int numSteps = numDegrees / 15; // see QWheelEvent documentation
emit mouseWheelTurned(numSteps);
}
When the wheel is turned, an event is sent to the view which contains the scene, and there a scale is performed:
class GraphicsView : public QGraphicsView{
Q_OBJECT
qreal m_currentScale;
public:
GraphicsView(QWidget * parent): QGraphicsView(parent){ m_currentScale = 1.0; }
public slots:
void onMouseWheelTurned (int);
};
void GraphicsView::onMouseWheelTurned(int steps) {
qreal sign = steps>0?1:-1;
qreal current = sign* pow(0.05, abs(steps));
if(m_currentScale+current > 0){
m_currentScale += current;
QMatrix matrix;
matrix.scale(m_currentScale, m_currentScale);
this->setMatrix(matrix);
}
}
This works, but I noticed if I zoom in a lot, for example to the top of the graphic, so that the graphic is no longer fully in the viewport, and then I zoom out, the program first scrolls to the botton of the graphic. I can see the vertical scrollbar sliding down. Only when it has reached bottom, does it start to zoom out. What could be the problem?
I would want to zoom in/ out without this scroll up / down behaviour.

You'd be better off handling this in the Scene and sending an event to the View.
Simply handle the event directly in the view with QGraphicsView::wheelEvent‌​, then call its scale function.

Related

How to undo-redo features like zoom in, zoom out using Command pattern in Qt?

I have a QGraphicsView, which contains rectangle, polyline, text etc. I also have some features to transform the view like zoom in, zoom out, Fit in view, change the color on mouse right click etc. I want to add few more features like Undo and Redo.
Undo means, user should cancel the effect of last command executed and
show previous transformation. So If user did transformation like zoom
in -> zoom in -> fit in -> zoom in and now pressed Undo then view's
fit in position should get displayed.
I have tried Command pattern to implement Undo-Redo feature. But I did it to add/remove rectangle,polyline in design. And it worked.
HCommand.h
#include <QUndoCommand>
#include<QGraphicsItem>
#include<QGraphicsScene>
class myCommand : public QUndoCommand
{
public:
HCommand(QGraphicsItem* mItem, QGraphicsScene* scene);
HCommand(QGraphicsScene* scene, QGraphicsView* mView);// for fitIn
private:
QGraphicsItem* mItem;
QGraphicsScene* mScene;
QGraphicsView* mView;
void undo();
void redo();
};
HCommand.cpp
#include "hcommand.h"
HCommand::HCommand(QGraphicsItem* item, QGraphicsScene* scene):
mItem(item), mScene(scene)
{}
void HCommand::undo()
{
if(mItem)
mScene->removeItem(mItem);
}
void HCommand::redo()
{
if(mItem)
mScene->addItem(mItem);
}
Widget.h
class Widget : public QGraphicsView
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
Rectangle r;
PolyLine p;
QUndoStack* undoStack;
void undo();
void redo();
private:
Ui::Widget *ui;
QGraphicsScene* scene;
QGraphicsView* view;
}
Widget.cpp
Widget::Widget(QWidget *parent)
: QGraphicsView(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
scene = new QGraphicsScene(this);
view = new QGraphicsView(this);
view->setScene(scene);
undoStack = new QUndoStack(this);
ui->verticalLayout_2->addWidget(view);
}
void Widget::undo()
{
undoStack->undo();
}
void Widget::redo()
{
undoStack->redo();
}
void Widget::on_schematicBtn_clicked()
{
QGraphicsRectItem* in = r.createRect(20,160,20,20);
scene->addItem(in);
HCommand* com = new HCommand(in,scene);
undoStack->push(com);
// many rectangles and polyline code, same as above
}
But now I want to do Undo-Redo for Zoom In , Zoom Out, Fit In. But whenever I zoom in/out, I zoomed in/out full view
void Widget::on_zoomIn_clicked()
{
double sFactor = 1.2;
view->scale(sFactor,sFactor);
}
void Widget::on_zoomOut_clicked()
{
double sFactor = 1.2;
view->scale(1/sFactor,1/sFactor);
}
void Widget::on_fitInBtn_clicked()
{
view->resetTransform();
}
So the question is :
In above code I am pushing particular rectangle in stack so for zoom
in/out how to push full view in stack ? So that, later I can pull it
out from stack ? Or this can be implemented differently ?
Any help is appreciated.

Scale x-axis of QChartView using mouse wheel

In my application I use QChart to show line graph. Unfortunately Qt Charts doesn't support such basic functions as zooming using mouse wheel and scrolling by mouse. Yes, there is RubberBand functionality but that still doesn't support scrolling etc end that isn't so intuitive to users. Also I need to scale only x-axis, some kind of setRubberBand(QChartView::HorizontalRubberBand) but using mouse wheel.
So far, after diving into QChartView I've use the following workaround:
class ChartView : public QChartView {
protected:
void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE
{
QRectF rect = chart()->plotArea();
if(event->angleDelta().y() > 0)
{
rect.setX(rect.x() + rect.width() / 4);
rect.setWidth(rect.width() / 2);
}
else
{
qreal adjustment = rect.width() / 2;
rect.adjust(-adjustment, 0, adjustment, 0);
}
chart()->zoomIn(rect);
event->accept();
QChartView::wheelEvent(event);
}
}
That works but zooming in and then zooming out doesn't lead to the same result. There is a small deviation. After debuging I've found that chart()->plotArea() always returns the same rect, so this workaround was useless.
It there some way to get rect of a visible area only?
Or may be someone could point me to the right solution how to do zooming/scrolling by mouse for QChartView?
Instead of using zoomIn() and zoomOut() you could use zoom() as shown below:
class ChartView : public QChartView {
protected:
void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE
{
qreal factor = event->angleDelta().y() > 0? 0.5: 2.0;
chart()->zoom(factor);
event->accept();
QChartView::wheelEvent(event);
}
};
Regarding zoomIn() and zoomOut(), it is not clear to what kind of coordinates it refers, I am still investing, when I have more information I will update my answer.
update:
As I observe one of the problems is the multiplication of floating point, and the other is to locate the center of the figure, to not have those problems my solution resets the zoom and then sets the change:
class ChartView : public QChartView {
qreal mFactor=1.0;
protected:
void wheelEvent(QWheelEvent *event) Q_DECL_OVERRIDE
{
chart()->zoomReset();
mFactor *= event->angleDelta().y() > 0 ? 0.5 : 2;
QRectF rect = chart()->plotArea();
QPointF c = chart()->plotArea().center();
rect.setWidth(mFactor*rect.width());
rect.moveCenter(c);
chart()->zoomIn(rect);
QChartView::wheelEvent(event);
}
};
I got this working for both x and y zooming with the following code:
void wheelEvent(QWheelEvent *event){
qreal factor;
if ( event->delta() > 0 )
factor = 2.0;
else
factor = 0.5;
QRectF r = QRectF(chart()->plotArea().left(),chart()->plotArea().top(),
chart()->plotArea().width()/factor,chart()->plotArea().height()/factor);
QPointF mousePos = mapFromGlobal(QCursor::pos());
r.moveCenter(mousePos);
chart()->zoomIn(r);
QPointF delta = chart()->plotArea().center() -mousePos;
chart()->scroll(delta.x(),-delta.y());}

How to keep the size and position of QGraphicsItem when scaling the view?

I have some QGraphicsItems in the QGraphicsScene which should keep the same size and position when scaling. I've tried QGraphicsItem::ItemIgnoresTransformations but it turns out that the items get wrong positions. Below is a sample code:
I have subclassed QGraphicsView like this:
class Graphics : public QGraphicsView
{
public:
Graphics();
QGraphicsScene *scene;
QGraphicsRectItem *rect;
QGraphicsRectItem *rect2;
protected:
void wheelEvent(QWheelEvent *event);
};
And in its constructor:
Graphics::Graphics()
{
scene = new QGraphicsScene;
rect = new QGraphicsRectItem(100,100,50,50);
rect2 = new QGraphicsRectItem(-100,-100,50,50);
scene->addLine(0,200,200,0);
rect->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
scene->addItem(rect);
scene->addItem(rect2);
setScene(scene);
scene->addRect(scene->itemsBoundingRect());
}
The wheelEvent virtual function:
void Graphics::wheelEvent(QWheelEvent *event)
{
if(event->delta() < 0)
scale(1.0/2.0, 1.0/2.0);
else
scale(2, 2);
scene->addRect(scene->itemsBoundingRect());
qDebug() << rect->transform();
qDebug() << rect->boundingRect();
qDebug() << rect2->transform();
qDebug() << rect2->boundingRect();
}
orginal view looks like this:
1
take the line as road and rect aside as a symbol. When zoomed out, the rect maintain its size but jumps out of the scene:
2
which should be that topleft of rect to middle of line. I'm also confused with debug info showing that the boundingRect and transform stays the same, which seems that nothing has changed! What causes the problem and is there any way to solve it? Could someone help? Thank you!
Sorry for delay, now I've solved the problem myself.
I found QGraphicsItem::ItemIgnoresTransformations only works when the point you want stick to is at (0,0) in item's coordinate. You need also update boundingRect manually in this way. Nevertheless, the best solution I've found is subclass QGraphicsItem and set matrix in paint() according to world matrix. Below is my code .
QMatrix stableMatrix(const QMatrix &matrix, const QPointF &p)
{
QMatrix newMatrix = matrix;
qreal scaleX, scaleY;
scaleX = newMatrix.m11();
scaleY = newMatrix.m22();
newMatrix.scale(1.0/scaleX, 1.0/scaleY);
qreal offsetX, offsetY;
offsetX = p.x()*(scaleX-1.0);
offsetY = p.y()*(scaleY-1.0);
newMatrix.translate(offsetX, offsetY);
return newMatrix;
}
And the paint function:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
QPointF p(left, top);
painter->setMatrix(stableMatrix(painter->worldMatrix(), p));
painter->drawRect(left, top, width, height);
}
The second argument of stableMatrix is sticked point, in my sample code it's top-left of the item. You can change it to your preference. It works really fine!
Hope this post help :)
The solution to this is even simpler.
QGraphicsItem::ItemIgnoresTransformations
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). [...]
And that's the key! Item ignores all transformations, but is still bound to its parent. So you need two items: a parent item that will keep the relative position (without any flags set) and a child item that will do the drawing (with QGraphicsItem::ItemIgnoresTransformations flag set) at parent's (0,0) point.
Here is some working code of a crosshair that have constant size and rotation, while keeping the relative position to its parent:
#include <QGraphicsItem>
#include <QPainter>
class CrossHair : public QGraphicsItem
{
private:
class CrossHairImpl : public QGraphicsItem
{
public:
CrossHairImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->setPen(QPen(Qt::red, 1));
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
}
private:
qreal m_len;
};
public:
CrossHair (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF();
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty
}
private:
CrossHairImpl m_impl;
};

Remove scroll functionality on mouse wheel QGraphics view

I have a QGraphicsView window on my widget and have just put in an event for mouse wheel which zooms in on the image.
However as soon as i zoom in scroll bars are displayed and the scroll functionality on the mouse wheel overrides the zoom function i have.
i was wondering if there is any way that i can remove scrolling all together and add a drag to move option or maybe a CTRL and mouse wheel to zoom and mouse wheel alone would control scrolling
here is my zoom function (Which im aware isnt perfect) but if anyone could shed some light on that it would be a bonus
cheers in advance
void Test::wheelEvent(QWheelEvent *event)
{
if(event->delta() > 0)
{
ui->graphicsView->scale(2,2);
}
else
{
ui->graphicsView->scale(0.5,0.5);
}
}
You reimplemented wheelEvent for QWidget/QMainWindow that contains your QGraphicsView, however, wheelEvent of QGraphicsView remains intact.
You can derive from QGraphicsView, reimplement wheelEvent for derived class and use derive class instead of QGraphicsView - this way you won't even need wheelEvent in your QWidget/QMainWindow, and you can customize reimplemented wheelEvent to do what you want. Something like that:
Header file:
class myQGraphicsView : public QGraphicsView
{
public:
myQGraphicsView(QWidget * parent = nullptr);
myQGraphicsView(QGraphicsScene * scene, QWidget * parent = nullptr);
protected:
virtual void wheelEvent(QWheelEvent * event);
};
Source file:
myQGraphicsView::myQGraphicsView(QWidget * parent)
: QGraphicsView(parent) {}
myQGraphicsView::myQGraphicsView(QGraphicsScene * scene, QWidget * parent)
: QGraphicsView(scene, parent) {}
void myQGraphicsView::wheelEvent(QWheelEvent * event)
{
// your functionality, for example:
// if ctrl pressed, use original functionality
if (event->modifiers() & Qt::ControlModifier)
{
QGraphicsView::wheelEvent(event);
}
// otherwise, do yours
else
{
if (event->delta() > 0)
{
scale(2, 2);
}
else
{
scale(0.5, 0.5);
}
}
}
Scrolling can be disabled with the following code:
ui->graphicsView->verticalScrollBar()->blockSignals(true);
ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
ui->graphicsView->horizontalScrollBar()->blockSignals(true);
ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
I think your question has a bit simpler answer.. To disable scroll bars just set scroll bar policy (QGraphicsView is just QScrollView), so step 1)
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
that will disable scroll bars..
step 2) (if you want to keep it simple)
QGraphicsView * pView; // pointer to your graphics view
pView->setInteractive(true);
pView->setDragMode(QGraphicsView::ScrollHandDrag);
thats the fastest way to get results you want

Select & moving Qwidget in the screen

I'm using QTCreator and I created a QWidget, then I have hidden the title bar with setWindowFlags(Qt::CustomizeWindowHint);.
But I can't select or move my widget. How can I use the mouseEvent to solve that?
If you want to be able to move your window around on your screen by just clicking and dragging (while maintaining the mouse button pressed), here's an easy way to do that:
#include <QtGui>
class W: public QWidget
{
Q_OBJECT
public:
explicit W(QWidget *parent=0) : QWidget(parent) { }
protected:
void mousePressEvent(QMouseEvent *evt)
{
oldPos = evt->globalPos();
}
void mouseMoveEvent(QMouseEvent *evt)
{
const QPoint delta = evt->globalPos() - oldPos;
move(x()+delta.x(), y()+delta.y());
oldPos = evt->globalPos();
}
private:
QPoint oldPos;
};
In mousePressEvent, you save the global (screen-coordinate) position of where the mouse was, and then in the mouseMoveEvent, you calculate how far the mouse moved and update the widget's position by that amount.
Note that if you have enabled mouse tracking, you'll need to add more logic to only move the window when a mouse button is actually pressed. (With mouse tracking disabled, which is the default, mouseMoveEvents are only generated when a button is held down).