how to pan images in QGraphicsView - c++

I am currently able to load my image into a grahpics scene, and then again into a QGraphicsViewer.
I am able to implement a zoom feature by dtecting a QEvent::Wheel and then calling the graphicsViews's scale() function.
However, I can't seem to figure out how to get the pan functionality working. I basically want to detect when a mouse has clicked down on the image, and then move the image left, right, up or down along with the mouse.
As of right now, I basically have a MouseFilter class that is detecting events, and doing different things depending on the event type. I attached that listener to the QGraphicsView object

In case someone is wondering how to do it on their own, it's actually quite simple. Here's the code from my app:
class ImageView : public QGraphicsView
{
public:
ImageView(QWidget *parent);
~ImageView();
private:
virtual void mouseMoveEvent(QMouseEvent *event);
virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
bool _pan;
int _panStartX, _panStartY;
};
You need to store the start position of the drag, for example like this (I used the right button):
void ImageView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton)
{
_pan = true;
_panStartX = event->x();
_panStartY = event->y();
setCursor(Qt::ClosedHandCursor);
event->accept();
return;
}
event->ignore();
}
Also, you need to clear the flag and restore the cursor once the button is released:
void ImageView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton)
{
_pan = false;
setCursor(Qt::ArrowCursor);
event->accept();
return;
}
event->ignore();
}
To actually manage the drag, you need to override the mouse move event. QGraphicsView inherits a QAbstractScrollArea, and its scrollbars are easily accessible. You also need to update the pan position:
void ImageView::mouseMoveEvent(QMouseEvent *event)
{
if (_pan)
{
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - (event->x() - _panStartX));
verticalScrollBar()->setValue(verticalScrollBar()->value() - (event->y() - _panStartY));
_panStartX = event->x();
_panStartY = event->y();
event->accept();
return;
}
event->ignore();
}

QGraphicsView has build-in mouse-panning support. Set correct DragMode and it will handle the rest. You do need the enable scroll bars for that to work.

neuviemeporte solution requires to subclass a QGraphicsView.
Another working drag implementation can be obtained without subclassing the view using eventFilter. If you don't need to customize other behaviors of the QGraphicsView, this technique will save you some work.
Let's say your GUI logic is maintained by a QMainWindow subclass and the QGraphicsView & QGraphicsScene are declared as a private members of this subclass. You would have to implement the eventFilter function as follows:
bool MyMainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == scene && event->type() == Event::GraphicsSceneMouseMove)
{
QGraphicsSceneMouseEvent *m = static_cast<QGraphicsSceneMouseEvent*>(event);
if (m->buttons() & Qt::MiddleButton)
{
QPointF delta = m->lastScreenPos() - m->screenPos();
int newX = view->horizontalScrollBar()->value() + delta.x();
int newY = view->verticalScrollBar()->value() + delta.y();
view->horizontalScrollBar()->setValue(newX);
view->verticalScrollBar()->setValue(newY);
return true;
}
}
return QMainWindow::eventFilter(obj, event);
}
To filter events from the QGraphicsScene, you'll have to install MyMainWindow as an eventFilter of the scene. Perhaps you could do this in the same function where you setup your GUI.
void MyMainWindow::setupGUI()
{
// along with other GUI stuff...
scene->installEventFilter(this);
}
You can extend this idea to replace the cursor with the drag "hand" as previously shown.

Related

How to create a label with drag and drop function and with the clicked signal function QT

I'm developing a program that contains a MainWindow and a Widget called Diagrama from QWidget, which is the central widget of my mainwindow.
In this diagrama widget I have the ability to create a label in a the position that I clicked on the screen and the ability to drag an drop those same labels.
But now, I want to add an ability to get a clicked signal of the label every time that I click it.
I know that to enable the clicked signal function of a label, I have to create a class of a custom label, but when I do this and I replace the class QLabel to the customLabel class in the code, the drag and drop function stop working.
void Diagrama::dragEnterEvent(QDragEnterEvent *event)
{....}
void Diagrama::dragMoveEvent(QDragMoveEvent *event)
{....}
void Diagrama::dropEvent(QDropEvent *event)
{....}
void Diagrama::mousePressEvent(QMouseEvent *event)
{....}
I put this for just you guys know that I have the function to the whole process
And now I don't know what to do.
I though that there is a conflict of the function mousePressEvent of my customLabel class and the same function in my Diagrama class.
How can I solve it?
void Diagrama::dragMoveEvent(QDragMoveEvent *event)
{
if (event->mimeData()->hasFormat("application/x-dnditemdata")) {
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
void Diagrama::dropEvent(QDropEvent *event)
{
if (event->mimeData()->hasFormat("application/x-dnditemdata")) {
QByteArray itemData = event->mimeData()->data("application/x-dnditemdata");
QDataStream dataStream(&itemData, QIODevice::ReadOnly);
QPixmap pixmap;
QPoint offset;
dataStream >> pixmap >> offset;
QLabel *newIcon = new QLabel(this);
newIcon->setPixmap(pixmap);
newIcon->move(event->pos() - offset);
newIcon->show();
newIcon->setAttribute(Qt::WA_DeleteOnClose);
if (event->source() == this) {
event->setDropAction(Qt::MoveAction);
event->accept();
} else {
event->acceptProposedAction();
}
} else {
event->ignore();
}
}
void Diagrama::paintEvent(QPaintEvent *e)
{
QPainter painter(this);
painter.drawImage(0,0,*mImage);
e->accept();
}
void Diagrama::mousePressEvent(QMouseEvent *event)
{
if(modo=="trafo")
{
if(event->button()==Qt::LeftButton){
QLabel *child = static_cast<QLabel*>(childAt(event->pos()));
if (!child)
return;
QPixmap pixmap = *child->pixmap();
QByteArray itemData;
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
dataStream << pixmap << QPoint(event->pos() - child->pos());
QMimeData *mimeData = new QMimeData;
mimeData->setData("application/x-dnditemdata", itemData);
QDrag *drag = new QDrag(this);
drag->setMimeData(mimeData);
drag->setPixmap(pixmap);
drag->setHotSpot(event->pos() - child->pos());
QPixmap tempPixmap = pixmap;
QPainter painter;
painter.begin(&tempPixmap);
painter.fillRect(pixmap.rect(), QColor(127, 127, 127, 127));
painter.end();
child->setPixmap(tempPixmap);
if (drag->exec(Qt::CopyAction | Qt::MoveAction, Qt::CopyAction) == Qt::MoveAction) {
child->close();
} else {
child->show();
child->setPixmap(pixmap);
}
}
else if(event->button()==Qt::RightButton)
{
QLabel *child = new QLabel(this);
child->setPixmap(QPixmap(url_trafo));
child->move(event->x(),event->y());
child->show();
}
}
else if(modo=="linha")
{
if(event->button()==Qt::RightButton){
p_ini=event->pos();
drawing=true;
event->accept();}
else {
event->ignore();
drawing=false;
}
}
}
That is the responsible for the events of drag and drop and the event of appearing a label every time I click on the screen
I tried to create a customLabel class to emit a clicked signal every time I click in the label, but disable the drag and drop event
A click has to be registered on mouse release, not mouse press. A mouse press can evolve into different things, depending on what the user does next. Mouse press plus move or mouse press plus a long delay evolves into a drag operation.
So you need to override both mousePressEvent() as well as mouseReleaseEvent().
In your mousePressEvent() you need to save the time the press happened as well as the position. You then call QLabel::mousePressEvent() and pass it the event, so that QLabel can still detect drag operations.
In your mouseReleaseEvent() you need to compare the current time to the time of the press. If the difference is larger than QApplication::startDragTime, or the position of the mouse release compared to the mouse press position is further away than QApplication::startDragDistance, or the position is outside the label, then you don't treat the mouse release as a click. Finally, forward the event to the overriden QLabel::mouseReleaseEvent() so that the base class knows the mouse press event ended.
Here's an example ClickableQLabel implementation:
#include <QApplication>
#include <QElapsedTimer>
#include <QLabel>
#include <QPoint>
class ClickableQLabel: public QLabel
{
Q_OBJECT
public:
explicit ClickableQLabel(QWidget* parent = nullptr)
: QLabel(parent)
{}
signals:
void clicked();
protected:
void mousePressEvent(QMouseEvent* e) override
{
QLabel::mousePressEvent(e);
if (e->button() != Qt::LeftButton) {
rerurn;
}
mouse_press_time_.start();
mouse_press_pos_ = e->pos();
e->accept();
}
void mouseReleaseEvent(QMouseEvent* e) override
{
QLabel::mouseReleaseEvent(e);
if (!rect().contains(e->pos(), true)
|| e->button() != Qt::LeftButton
|| !mouse_press_time_.isValid()
|| mouse_press_pos_.isNull()
|| mouse_press_time_.hasExpired(QApplication::startDragTime())
|| (e->pos() - mouse_press_pos_).manhattanLength() >= QApplication::startDragDistance())
{
// Not a click.
return;
}
e->accept();
mouse_press_time_.invalidate();
mouse_press_pos_ = QPoint();
emit clicked();
}
private:
QElapsedTimer mouse_press_time_;
QPoint mouse_press_pos_;
};
If you now want something to happen when the label is clicked, connect the clicked() signal.

How to constrain a QRubberBand within a QLabel?

I am building a Qt5 application that allows a user to draw a rubberband with his mouse over an image, to select certain area of the image for further processing.
I got my code to only allow the user to start drawing the rubberband, by subclassing a QLabel to a custom class (frame_displayer) which mousePressEvent() is overridden and thus be only invoked when the mouse press happens within the custom classed widget.
The problem is that when the initial click is inside the frame_displayer, mouseMoveEvent(), the function that I use to change the rubberband size accordingly, keeps getting called even when the mouse cursor has been dragged outside of the frame_displayer.
I have tried using leaveEvent() and enterEvent() to control a class boolean flag that codes inside mouseMoveEvent could rely on to know whether the cursor is still within the widget. However, both leaveEvent() and enterEvent() are only called while a mouse button is not being held, thus rendering them no use for constraining the rubberband.
Also, the underMouse() always return true, for a reason unknown to me.
Segment of frame_displayer.cpp
frame_displayer::frame_displayer(QWidget * parent) : QLabel(parent)
{
_rubberBand = new QRubberBand(QRubberBand::Rectangle, this);
}
void frame_displayer::mousePressEvent(QMouseEvent *event)
{
_lastClickedBtn = event->button();
if (_lastClickedBtn == Qt::LeftButton)
{
_mouseOriginClickPoint = event->pos();
_rubberBand->setGeometry(QRect(_mouseOriginClickPoint, _mouseClickPoint));
_rubberBand->show();
}
}
void frame_displayer::mouseMoveEvent(QMouseEvent *event)
{
if(_rubberBand != nullptr)
{
if (this->underMouse())
{
if (_lastClickedBtn == Qt::LeftButton)
{
QPoint mouseCurrentPoint = event->pos();
_rubberBand->setGeometry(QRect(_mouseOriginClickPoint, mouseCurrentPoint).normalized());
}
}
}
}
void frame_displayer::mouseReleaseEvent(QMouseEvent *event)
{
_mouseOriginClickPoint = QPoint();
_lastClickedBtn = Qt::MidButton;
if(_rubberBand != nullptr)
{
_rubberBand->hide();
_rubberBand->clearMask();
}
}
void frame_displayer::leaveEvent(QEvent *event)
{
qDebug() << "Leaving";
}
void frame_displayer::enterEvent(QEvent *event)
{
qDebug() << "Entering";
}
Thanks in advance!
I think that's the expected behaviour. If you want to limit the extents of the rubber band then simply clamp them in the mouseMoveEvent override...
void frame_displayer::mouseMoveEvent(QMouseEvent *event)
{
if(_rubberBand != nullptr)
{
if (this->underMouse())
{
if (_lastClickedBtn == Qt::LeftButton)
{
QPoint mouseCurrentPoint = event->pos();
/*
* Clamp mouseCurrentPoint to the QRect of this widget.
*/
auto clamp_rect = rect();
mouseCurrentPoint.rx() = std::min(clamp_rect.right(), std::max(clamp_rect.left(), mouseCurrentPoint.x()));
mouseCurrentPoint.ry() = std::min(clamp_rect.bottom(), std::max(clamp_rect.top(), mouseCurrentPoint.y()));
_rubberBand->setGeometry(QRect(_mouseOriginClickPoint, mouseCurrentPoint).normalized());
}
}
}
}

QGestureEvent for mouse

I write desktop application that works with map,
and I want to react on pan and long press events.
It is possible to use QGestureEvent on Qt/Linux/X11 with ordinary mouse?
I took Qt gesture example, it works on tablet,
but not reaction on press left mouse button and move (I expect that application recognizes it as tap or swipe event).
Then I added to Qt gesture example app.setAttribute(Qt::AA_SynthesizeTouchForUnhandledMouseEvents, true); at main and such code to imagewidget.cpp:
void ImageWidget::mousePressEvent(QMouseEvent *e)
{
e->ignore();
}
void ImageWidget::mouseReleaseEvent(QMouseEvent *e)
{
e->ignore();
}
void ImageWidget::mouseMoveEvent(QMouseEvent *e)
{
e->ignore();
}
this code still works on tablet, but again no reaction on mouse on Linux/X11.
Any way to enable qgesture on linux/x11, should I write my own gesture recognition
for mouse?
The official way to make gestures out of mouse events in Qt is deriving from the QGestureRecognizer class, which allows to listen to relevant mouse events, set gesture properties accordingly, then trigger the gesture (or cancel it).
Here follows an example for pan gestures only, just to give an idea of what has to be done.
Have a QGestureRecognizer subclass like this:
#include <QGestureRecognizer>
#include <QPointF>
class PanGestureRecognizer : public QGestureRecognizer
{
QPointF startpoint;
bool panning;
public:
PanGestureRecognizer() : panning(false){}
QGesture *create(QObject *target);
Result recognize(QGesture *state, QObject *watched, QEvent *event);
};
The create method has been overridden to return a new instance of our gesture of interest:
QGesture *PanGestureRecognizer::create(QObject *target)
{
return new QPanGesture();
}
The recognize method override is the core of our recognizer class, where events are passed in, gesture properties set, gesture events triggered:
QGestureRecognizer::Result PanGestureRecognizer::recognize(QGesture *state, QObject *, QEvent *event)
{
QMouseEvent * mouse = dynamic_cast<QMouseEvent*>(event);
if(mouse != 0)
{
if(mouse->type() == QMouseEvent::MouseButtonPress)
{
QPanGesture * gesture = dynamic_cast<QPanGesture*>(state);
if(gesture != 0)
{
panning = true;
startpoint = mouse->pos();
gesture->setLastOffset(QPointF());
gesture->setOffset(QPointF());
return TriggerGesture;
}
}
if(panning && (mouse->type() == QMouseEvent::MouseMove))
{
QPanGesture * gesture = dynamic_cast<QPanGesture*>(state);
if(gesture != 0)
{
gesture->setLastOffset(gesture->offset());
gesture->setOffset(mouse->pos() - startpoint);
return TriggerGesture;
}
}
if(mouse->type() == QMouseEvent::MouseButtonRelease)
{
QPanGesture * gesture = dynamic_cast<QPanGesture*>(state);
if(gesture != 0)
{
QPointF endpoint = mouse->pos();
if(startpoint == endpoint)
{
return CancelGesture;
}
panning = false;
gesture->setLastOffset(gesture->offset());
gesture->setOffset(mouse->pos() - startpoint);
return FinishGesture;
}
}
if(mouse->type() == QMouseEvent::MouseButtonDblClick)
{
panning = false;
return CancelGesture;
}
return Ignore;
}
}
Basically, we track mouse events, updating a couple of properties of our own (panning and startpoint) and the passed in gesture properties as well. For each mouse event type, we also return a QGestureRecognizer::Result . All other events are discarded (the method returns Ignore).
This code can be tested with the Image Gestures Example, though: just add the class to the project and this line in the ImageWidget constructor:
QGestureRecognizer::registerRecognizer(new PanGestureRecognizer());
This should let the user grab the picture and move it around, using a mouse.
Look into this image widget gestures example. (search for mouseDoubleClickEvent)
http://doc.qt.io/qt-5/qtwidgets-gestures-imagegestures-imagewidget-cpp.html
Based on that you need to reimplement the required mouse events.
MyWidget::MyWidget() {
---
---
}
bool MyWidget::event(QEvent *ev)
{
---
---
}
void MyWidget::mouseReleaseEvent(QMouseEvent *event)
{
}
void MyWidget::mouseMoveEvent(QMouseEvent *event)
{
}
And declare those two functions in header
void mouseReleaseEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);

qt chart move view with pressed middle mouse button

I'm currently working with Qts Chart plotting tool. I have now a plot where I can zoom in and out by using the chartview class provieded by this example (with small adjustments).
I would like to see the ability to not only zoom, but also move my view a pressed middle mouse button (which is used a lot in other applications and therefore is very intuitive).
How can I do this in Qt? How can I check if a middle mouse button is pressed and released and change my view at the plot if the mouse moves during a pressed middle mouse button...
I'm sure someone has coded this before and would really appreciate a small example/help.
you need to derive a class from QChartView and overload the mouse events:
class ChartView: public QChartView
{
Q_OBJECT
public:
ChartView(Chart* chart, QWidget *parent = 0);
protected:
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
private:
QPointF m_lastMousePos;
};
ChartView::ChartView(Chart* chart, QWidget *parent)
: QChartView(chart, parent)
{
setDragMode(QGraphicsView::NoDrag);
this->setMouseTracking(true);
}
void ChartView::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::MiddleButton)
{
QApplication::setOverrideCursor(QCursor(Qt::SizeAllCursor));
m_lastMousePos = event->pos();
event->accept();
}
QChartView::mousePressEvent(event);
}
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
// pan the chart with a middle mouse drag
if (event->buttons() & Qt::MiddleButton)
{
QRectF bounds = QRectF(0,0,0,0);
for(auto series : this->chart()->series())
bounds.united(series->bounds())
auto dPos = this->chart()->mapToValue(event->pos()) - this->chart()->mapToValue(m_lastMousePos);
if (this->rubberBand() == QChartView::RectangleRubberBand)
this->chart()->zoom(bounds.translated(-dPos.x(), -dPos.y()));
else if (this->rubberBand() == QChartView::HorizontalRubberBand)
this->chart()->zoom(bounds.translated(-dPos.x(), 0));
else if (this->rubberBand() == QChartView::VerticalRubberBand)
this->chart()->zoom(bounds.translated(0, -dPos.y()));
m_lastMousePos = event->pos();
event->accept();
}
QChartView::mouseMoveEvent(event);
}
I'd like to offer a simpler version of Nicolas's mouseMoveEvent():
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
// pan the chart with a middle mouse drag
if (event->buttons() & Qt::MiddleButton)
{
auto dPos = event->pos() - lastMousePos_;
chart()->scroll(-dPos.x(), dPos.y());
lastMousePos_ = event->pos();
event->accept();
QApplication::restoreOverrideCursor();
}
QChartView::mouseMoveEvent(event);
}
Also, be sure to include QApplication::restoreOverrideCursor() so the cursor returns to usual after the move is done.

Click event for QGraphicsView Qt

I have made a GUI in Qt that is basically a widget with a QGraphicsView on it i have a function:
void GUI::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
QPointF mousePoint = ui->graphicsView->mapToScene(event->pos());
qDebug() << mousePoint;
}
}
which links to a public slot:
void mousePressEvent(QMouseEvent *event);
this shows me on the console the x,y coordinate of where i have clicked, however currently this is working on the entire widget and i ideally would like x,y(0,0) to be the top left of the QGraphicsView instead of the top left of the entire widget. does anyone have any idea how to make this work, i thought from my code that this is what it was doing but it turns out this is not so, iv had a look around for a while now but im coming up with nothing
any help would be really appreciated thanks.
Reimplement the mousePressEvent(QMouseEvent *event) of the QGraphicsView not your widget would be the 'proper' way of doing it. Otherwise you can hack it with:
// Detect if the click is in the view.
QPoint remapped = ui->graphicsView->mapFromParent( event->pos() );
if ( ui->graphicsView->rect().contains( remapped ) )
{
QPointF mousePoint = ui->graphicsView->mapToScene( remapped );
}
This assumes that the widget is the parent of the QGraphicsView.