Why QMouseEvent passing multiple events for single movement on QWidget?
I'm implementing simple dragging effect, but the result is not what I expected.
The following code will move the widget to new location but instantly move it back to the original location.
customwidget.h
#ifndef CUSTOMWIDGET_H
#define CUSTOMWIDGET_H
#include <QWidget>
#include <fstream>
class CustomWidget : public QWidget
{
Q_OBJECT
public:
explicit CustomWidget(QWidget *parent = nullptr);
~CustomWidget();
protected:
// define the painting agorithm to see the area of this widget
void paintEvent(QPaintEvent* ev);
// handle the pressing event to initialize the dragging algorithm
// and to track the start of moving event
void mousePressEvent(QMouseEvent* ev);
// implement the dragging algorithm
void mouseMoveEvent(QMouseEvent* ev);
// handle the releasing event to track the end of moving event
void mouseReleaseEvent(QMouseEvent* ev);
private:
std::ofstream fout; // open file "debug.txt"
QPoint prev; // to save the previous point of cursor.
};
#endif // CUSTOMWIDGET_H
customwidget.cpp
#include "customwidget.h"
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
#include <QBrush>
CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent)
{
// open file for output
fout.open("debug.txt");
// set the widget size and position
setGeometry(0, 0, 100, 100);
}
CustomWidget::~CustomWidget()
{
// close file when program ended
fout.close();
}
void CustomWidget::paintEvent(QPaintEvent *ev)
{
// draw the area with blue color
QPainter painter(this);
QBrush brush(Qt::GlobalColor::blue);
painter.setBrush(brush);
painter.setBackground(brush);
painter.drawRect(ev->rect());
}
void CustomWidget::mousePressEvent(QMouseEvent *ev)
{
ev->accept();
// debug output
fout << "pressed at (" << ev->x() << ',' << ev->y() << ')' << std::endl;
// initialize the dragging start point
prev = ev->pos();
}
void CustomWidget::mouseMoveEvent(QMouseEvent *ev)
{
ev->accept();
// get the cursor position of this event
const QPoint& pos = ev->pos();
// debug output
fout << "moved from (" << prev.x() << ',' << prev.y() << ") to ("
<< pos.x() << ',' << pos.y() << ')' << std::endl;
// calculate the cursor movement
int dx = pos.x() - prev.x();
int dy = pos.y() - prev.y();
// move the widget position to match the direction of the cursor.
move(geometry().x() + dx, geometry().y() + dy);
// update the cursor position for the next event
prev = pos;
}
void CustomWidget::mouseReleaseEvent(QMouseEvent *ev)
{
ev->accept();
fout << "released at (" << ev->x() << ',' << ev->y() << ')' << std::endl;
}
main.cpp
#include "customwidget.h"
#include <QApplication>
#include <QMainWindow>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// generate simple main window.
QMainWindow w;
// set the size of the window.
w.setGeometry(0, 0, 800, 800);
// generate the CustomWidget
CustomWidget *widget = new CustomWidget(&w);
// display the window containing the widget
w.show();
return a.exec();
}
And the result of debug.txt for one single movement of cursor is
CustomWidget pressed at (79,83)
CustomWidget moved from (79,83) to (79,83)
CustomWidget moved from (79,83) to (80,83)
CustomWidget moved from (80,83) to (79,83)
CustomWidget released at (80,83)
The result is moving the widget to new location for a little time then move it back to its original location.
The look of this program will almost looked like the widget is never being moved no mater how you dragging the widget.
My theory is the event manager pass the event when you moved the cursor. But
after the first event is processed, the manager passes another event related to the new location of the widget and the cursor current position. Then the process will move the widget back to where it was.
Although I can change the method of getting location of cursor from
ev->pos()
to
ev->globalPos()
to solve the problem.
But still want to know why the event manager act like that.
You have to do the following:
On mouse press event store the mouse cursor offset relative to the widget,
Move your widget so that the mouse cursor will always preserve the initial non zero offset,
Reset the offset on mouse release event.
The code (draft) might look like:
void CustomWidget::mousePressEvent(QMouseEvent* event)
{
// m_offset is a member variable of CustomWidget
m_offset = event->globalPos() - pos();
QWidget::mousePressEvent(event);
}
void CustomWidget::mouseMoveEvent(QMouseEvent* event)
{
if (!m_offset.isNull()) {
move(event->globalPos() - m_offset);
}
QWidget::mouseMoveEvent(event);
}
void CustomWidget::mouseReleaseEvent(QMouseEvent* event)
{
// Reset the offset value to prevent the movement.
m_offset = QPoint();
QWidget::mouseReleaseEvent(event);
}
Related
I'm trying to create a pseudo remote control widget, the remote control widget (an overridden QLabel) receives a screenshot (pixmap) of the widget to be controlled every 2 seconds or so through a REST api. I've overridden the QLabel mouse events and can store the positions of clicks on the overridden QLabel.
How can I convert these positions into mouse events and execute them on the "remote controlled" widget?
I've attached the cpp of the overridden QLabel and would appreciate any input.
#include "RemoteControlLabel.h"
#include <QDebug>
#include <QMouseEvent>
RemoteControlLabel::RemoteControlLabel(QWidget* parent) : QLabel(parent)
{
}
RemoteControlLabel::~RemoteControlLabel()
{
}
void RemoteControlLabel::mousePressEvent(QMouseEvent* event)
{
QPoint pos = event->pos();
qDebug() << "mouse pressed at " << pos;
}
void RemoteControlLabel::mouseReleaseEvent(QMouseEvent* event)
{
qDebug() << "Mouse released";
}
You can try this:
virtual void RemoteControlLabel::mousePressEvent(QMouseEvent* event) override
{
QPoint pos = event->pos();
qDebug() << "mouse pressed at " << pos;
// create new event on the stack
QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0);
// use sendEvent - it sends the event directly
QApplication::sendEvent(remotelyControlledWidget, &event);
// at the end of scope event will be automatically deleted, which is our intention
}
I have a derived class of QGraphicsView where I set the drag mode to ScrollHandDrag and also implement the zoom functionality:
Header
#ifndef CUSTOMGRAPHICSVIEW_H
#define CUSTOMGRAPHICSVIEW_H
#include <QGraphicsView>
class CustomGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
CustomGraphicsView(QWidget* parent = nullptr);
protected:
virtual void wheelEvent(QWheelEvent* event) override;
};
#endif // CUSTOMGRAPHICSVIEW_H
Implementation
#include "customview.h"
#include <QWheelEvent>
CustomGraphicsView::CustomGraphicsView(QWidget* parent) : QGraphicsView(parent)
{
setScene(new QGraphicsScene);
setDragMode(ScrollHandDrag);
}
void CustomGraphicsView::wheelEvent(QWheelEvent* event)
{
// if ctrl pressed, use original functionality
if (event->modifiers() & Qt::ControlModifier)
QGraphicsView::wheelEvent(event);
// otherwise, do yours
else
{
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
if (event->delta() > 0)
{
scale(1.1, 1.1);
}
else
{
scale(0.9, 0.9);
}
}
}
When I use this class in a program (see below), I can move around the scene and zoom in and out. However, when the image is bigger in one of the dimension than the viewport, but not in the other one (see attached image) I can only drag along the axis that coincides with the image being bigger than the. This, in the attached image, is vertical as it can be seen by the presence of the right-hand side scroll bar.
My question is: is there a way to not restrict the movement? Can I set the scroll mode that allows me to move freely regardless of the scene being contained in the view? Is my only option to reimplement mouseMoveEvent?
Application
#include <QApplication>
#include <QGraphicsPixmapItem>
#include "customview.h"
int main(int argc, char** argv)
{
QApplication app(argc, argv);
CustomGraphicsView cgv;
QGraphicsPixmapItem* item = new QGraphicsPixmapItem(QPixmap::fromImage(QImage("clouds-country-daylight-371633.jpg")));
cgv.scene()->addItem(item);
cgv.show();
return app.exec();
}
The image I used is this one.
After a careful read of the documentation, my conclusion is that it is not possible to move outside the scene. However, one can manually set the limits of the scene to something bigger than the actual scene. The easiest solution is to set a big enough scene at the beginning as suggested here. However, this is not dynamic and has limitations. I solved this issue by auto-computing the scene limits whenever the scene is updated. For that, I connect the QGraphicsScene::changed to a slot where the auto size of the scene is computed and I manually force the scene to be updated with the mouse move. The final class with the desired behavior is:
Header
#ifndef CUSTOMGRAPHICSVIEW_H
#define CUSTOMGRAPHICSVIEW_H
#include <QGraphicsView>
class CustomGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
CustomGraphicsView(QWidget* parent = nullptr);
protected:
virtual void wheelEvent(QWheelEvent* event) override;
virtual void mouseMoveEvent(QMouseEvent* event) override;
virtual void mousePressEvent(QMouseEvent* event) override;
virtual void mouseReleaseEvent(QMouseEvent* event) override;
void autocomputeSceneSize(const QList<QRectF>& region);
};
#endif // CUSTOMGRAPHICSVIEW_H
CPP
#include "customview.h"
#include <QWheelEvent>
CustomGraphicsView::CustomGraphicsView(QWidget* parent) : QGraphicsView(parent)
{
// Set up new scene
setScene(new QGraphicsScene);
// Do not show scroll bars
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Connect scene update to autoresize
connect(scene(), &QGraphicsScene::changed, this, &CustomGraphicsView::autocomputeSceneSize);
}
void CustomGraphicsView::wheelEvent(QWheelEvent* event)
{
// if ctrl pressed, use original functionality
if (event->modifiers() & Qt::ControlModifier)
QGraphicsView::wheelEvent(event);
// Rotate scene
else if (event->modifiers() & Qt::ShiftModifier)
{
if (event->delta() > 0)
{
rotate(1);
}
else
{
rotate(-1);
}
}
// Zoom
else
{
ViewportAnchor previous_anchor = transformationAnchor();
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
if (event->delta() > 0)
{
scale(1.1, 1.1);
}
else
{
scale(0.9, 0.9);
}
setTransformationAnchor(previous_anchor);
}
}
void CustomGraphicsView::mouseMoveEvent(QMouseEvent* event)
{
QGraphicsView::mouseMoveEvent(event);
if (event->buttons() & Qt::LeftButton)
// If we are moveing with the left button down, update the scene to trigger autocompute
scene()->update(mapToScene(rect()).boundingRect());
}
void CustomGraphicsView::mousePressEvent(QMouseEvent* event)
{
if (event->buttons() & Qt::LeftButton)
// Set drag mode when left button is pressed
setDragMode(QGraphicsView::ScrollHandDrag);
QGraphicsView::mousePressEvent(event);
}
void CustomGraphicsView::mouseReleaseEvent(QMouseEvent* event)
{
if (dragMode() & QGraphicsView::ScrollHandDrag)
// Unset drag mode when left button is released
setDragMode(QGraphicsView::NoDrag);
QGraphicsView::mouseReleaseEvent(event);
}
void CustomGraphicsView::autocomputeSceneSize(const QList<QRectF>& region)
{
Q_UNUSED(region);
// Widget viewport recangle
QRectF widget_rect_in_scene(mapToScene(-20, -20), mapToScene(rect().bottomRight() + QPoint(20, 20)));
// Copy the new size from the old one
QPointF new_top_left(sceneRect().topLeft());
QPointF new_bottom_right(sceneRect().bottomRight());
// Check that the scene has a bigger limit in the top side
if (sceneRect().top() > widget_rect_in_scene.top())
new_top_left.setY(widget_rect_in_scene.top());
// Check that the scene has a bigger limit in the bottom side
if (sceneRect().bottom() < widget_rect_in_scene.bottom())
new_bottom_right.setY(widget_rect_in_scene.bottom());
// Check that the scene has a bigger limit in the left side
if (sceneRect().left() > widget_rect_in_scene.left())
new_top_left.setX(widget_rect_in_scene.left());
// Check that the scene has a bigger limit in the right side
if (sceneRect().right() < widget_rect_in_scene.right())
new_bottom_right.setX(widget_rect_in_scene.right());
// Set new scene size
setSceneRect(QRectF(new_top_left, new_bottom_right));
}
I have QGraphicsView, QGraphicsScene and QGraphicsRectItem.
QGraphicsRectItem in the QGraphicsScene and the last one in the QGraphicsView. I want to move QGraphicsRectItem with mouse by clicking on it only! But in my implementation it moves if I click on any position on my QGraphicsScene. Whether it is my QGraphicsRectItem or some other place. And the second issue. The item has been moved to the center of the scene. Clicking on it again it starts to move from the home location.
void Steer::mousePressEvent(QMouseEvent *click)
{
offset = click->pos();
}
void Steer::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() & Qt::LeftButton)
{
p1->setPos(event->localPos() - offset); //p1 movable item
}
}
What do I do wrong?
UPDATE:
main.cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Steer w;
w.show();
return a.exec();
}
widget.h
#ifndef STEER_H
#define STEER_H
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QMouseEvent>
#include <QPoint>
#include <QGraphicsRectItem>
class Steer : public QGraphicsView
{
Q_OBJECT
private:
QGraphicsScene *scene;
QGraphicsRectItem *p1;
QPoint offset;
public:
explicit Steer(QGraphicsView *parent = 0);
~Steer(){}
public slots:
void mousePressEvent(QMouseEvent * click);
void mouseMoveEvent(QMouseEvent * event);
};
#endif // STEER_H
widget.cpp
#include "widget.h"
#include <QBrush>
Steer::Steer(QGraphicsView *parent)
: QGraphicsView(parent)
{
scene = new QGraphicsScene;
p1 = new QGraphicsRectItem;
//add player
p1->setRect(760, 160, 10, 80);
//add scene
scene->setSceneRect(0, 0, 800, 400);
//add moveable item
scene->addItem(p1);
//set scene
this->setScene(scene);
this->show();
}
void Steer::mousePressEvent(QMouseEvent *click)
{
offset = click->pos();
}
void Steer::mouseMoveEvent(QMouseEvent *event)
{
if(event->buttons() & Qt::LeftButton)
{
p1->setPos(event->localPos() - offset);
}
}
I'd try a different approach that is a little easier to understand:
#include <QtWidgets>
class Steer : public QGraphicsView
{
public:
Steer()
{
scene = new QGraphicsScene;
p1 = new QGraphicsRectItem;
//add player
p1->setRect(0, 0, 10, 80);
p1->setX(760);
p1->setY(160);
//add scene
scene->setSceneRect(0, 0, 800, 400);
//add moveable item
scene->addItem(p1);
//set scene
this->setScene(scene);
this->show();
}
protected:
void mousePressEvent(QMouseEvent * click)
{
if (p1->contains(p1->mapFromScene(click->localPos()))) {
lastMousePos = click->pos();
} else {
lastMousePos = QPoint(-1, -1);
}
}
void mouseMoveEvent(QMouseEvent * event)
{
if(!(event->buttons() & Qt::LeftButton)) {
return;
}
if (lastMousePos == QPoint(-1, -1)) {
return;
}
p1->setPos(p1->pos() + (event->localPos() - lastMousePos));
lastMousePos = event->pos();
}
private:
QGraphicsScene *scene;
QGraphicsRectItem *p1;
QPoint lastMousePos;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Steer w;
w.show();
return a.exec();
}
There are a few things to point out here:
Don't use setRect() to set the position of a QGraphicsRectItem. It doesn't work the way you think it might. Always use setPos() to change the position of an item.
Rename offset to something more descriptive. I chose lastMousePos. Instead of just updating it once when the mouse is pressed, also update it whenever the mouse is moved. Then, it's simply a matter of getting the difference between the two points and adding that to the position of the item.
Check if the mouse is actually over the item before reacting to move events. If the mouse isn't over the item, you need some way of knowing that, hence the QPoint(-1, -1). You may want to use a separate boolean flag for this purpose. This solves the problem that you saw, where it was possible to click anywhere in the scene to get the item to move.
Also, note the mapFromScene() call: the contains() function works in local coordinates, so we must map the mouse position which is in scene coordinates before testing if it's over the item.
The event functions are not slots, they're virtual, protected functions.
You could also consider handling these events in the items themselves. You don't need to do it from within QGraphicsView, especially if you have more than one of these items that need to be dragged with the mouse.
In Qt with C++, I created a window with a small QWidget inside.
The small QWidget show a message every time QEvent::Enter, QEvent::Leave or QEvent::MouseMove is triggered.
When any mouse button is pressed (and holded) outside of the small QWidget, and the mouse is moved on the top of this small QWidget (While holding), QEvent::MouseMove is not triggered for this small QWidget. Additionally, QEvent::Enter is postponed to after the mouse button is released.
In the reverse situation: when the mouse is pressed on the small QWidget (and holded), and then the mouse is moved outside, the QEvent::Leave is postponed to after the mouse button is released.
IS there any solution to retrieve QEvent::MouseMove all the time, even when the mouse button is holded?
Additional data: Yes, setMouseTracking(true) is set.
Testing example:
Widget:
#ifndef MYWIDGET_HPP
#define MYWIDGET_HPP
#include <QWidget>
#include <QStyleOption>
#include <QPainter>
#include <QEvent>
#include <QDebug>
class MyWidget: public QWidget
{
Q_OBJECT
public:
MyWidget( QWidget* parent=nullptr ): QWidget(parent)
{
setMouseTracking(true);
}
protected:
// Paint for styling
void paintEvent(QPaintEvent *)
{
// Needed to allow stylesheet.
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
// Show Enter and Leave event for debugging purpose
bool event( QEvent *e)
{
static int counting=0;
if (e->type() ==QEvent::Enter)
{
qDebug() << counting++ << " Enter: " << this->objectName();
}
if (e->type() ==QEvent::Leave)
{
qDebug() << counting++ << " Leave: " << this->objectName();
}
if (e->type() ==QEvent::MouseMove)
{
qDebug() << counting++ << " Move: " << this->objectName();
}
return QWidget::event(e);
}
};
#endif // MYWIDGET_HPP
Main
#include <QApplication>
#include <QDebug>
#include <QWidget>
#include <QTimer>
#include "Testing.hpp"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// Create a main window
QWidget main;
main.setWindowTitle("Cursor blocked for 5s - wait and see");
main.resize(500, 200);
main.move(200, 200);
// Create a MyWidget
MyWidget sub(&main);
sub.setObjectName("sub");
sub.resize(50, 50);
sub.move(50, 50);
// Style the button with a hover
main.setStyleSheet
(
"QWidget#sub{background-color: rgba(0,0,128,0.5);}"
"QWidget#sub:hover{background-color: rgba(128,0,0,0.5);}"
);
// Show the window
main.show();
return a.exec();
}
Project
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
SOURCES +=\
main.cpp
HEADERS +=\
Testing.hpp
RESOURCES +=\
CONFIG += c++11 -Wall
TARGET = Testing
TEMPLATE = app
It is standard behavior. When you press mouse button, widget begin to grab it (make a call of QWidget::grabMouse). I think that you should redesign your behavior, or explain some real use-cases, when you need to track mouse globally.
If you really need to track mouse, you may use event filters.
Pseudo-code (without checks):
QWidget *otherWidget = /*...*/;
QWidget *myWidget = /*...*/;
otherWidget->installEventFilter( myWidget );
// you need to install filter on each widget,
// that you want to track.
// Care with performance
MyWidget : QWidget
{
void handleMouseMove( QPoint pos ) { /*...you code...*/ }
void mouseMove( QMouseEvent *e ) override;
{
handleMouseMove( e->pos() );
QWidget::mouseMove( e );
}
bool eventFilter( QObject *obj, QEvent *e )
{
auto srcWidget = qobject_cast< QWidget * >( obj );
switch ( e->type() )
{
case QEvent::MouseMove:
{
auto me = static_cast< QMouseEvent * >( e );
auto globalPos = srcWidget->mapToGlobal( me->pos() );
auto localPos = this->mapFromGlobal( globalPos ); // Possible, you need to invalidate that poing belongs to widget
handleMouseMove( localPos );
}
break;
};
return QWidget::eventFilter( obj, e );
}
};
I'm novice in programming and need a help.
I have a class Station, which contains X and Y fields:
Class Station {
int x
int y
...
}
All the stations are drawing on a QGraphicsScene as a circles and text:
this->scene.addEllipse(x1, y1, diam, diam, pen, QBrush(...));
I need a function getClickedStation, which is waiting for click on a QGraphicsScene, finding the circle and returns the station appropiate of its coordinates:
Station* getClickedStation(...) { ... }
Is there any ways to do it?
I've tried this just to get coordinates:
QList<QGraphicsItem*> listSelectedItems = scene.selectedItems();
QGraphicsItem* item = listSelectedItems.first();
ui->textBrowserMenu->append(QString::number(item->boundingRect().x()));
ui->textBrowserMenu->append(QString::number(item->boundingRect().y()));
but the program crashes with it...
No, you do it wrong. I wrote small example. You should subclass QGraphicsScene and reimplement mousePressEvent and process clicks in it. For example:
*.h
#ifndef GRAPHICSSCENE_H
#define GRAPHICSSCENE_H
#include <QGraphicsScene>
#include <QPoint>
#include <QMouseEvent>
class GraphicsScene : public QGraphicsScene
{
Q_OBJECT
public:
explicit GraphicsScene(QObject *parent = 0);
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent);
};
#endif // GRAPHICSSCENE_H
In cpp
void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
//qDebug() << "in";
if (mouseEvent->button() == Qt::LeftButton)
{
QGraphicsItem *item = itemAt(mouseEvent->scenePos(), QTransform());// it is your clicked item, you can do everything what you want. for example send it somewhere
QGraphicsEllipseItem *ell = qgraphicsitem_cast<QGraphicsEllipseItem *>(item);
if(ell)
{
ell->setBrush(QBrush(Qt::black));
}
else
qDebug() << "not ell" << mouseEvent->scenePos();
}
}
On the scene there are a few ellipses, when you click somewhere in scene we get item under cursor and check it is it ellipse for example. If is, then we set new background to it.
Main idea are itemAt method and qgraphicsitem_cast