QGraphicsPixmapItem mouseMoveEvent when not grabbing - c++

I am working on a C++ application with Qt 4.8 as the GUI framework:
A subclassed QGraphicsView rendering a subclassed QGraphicsScene containing one or more subclassed QGraphicsPixMapItem(s) in which I need to know the mouse cursor's relative pixel position.
Following How to show pixel position and color from a QGraphicsPixmapItem I subclass QGraphicsPixmapItem:
class MyGraphicsPixmapItem : public QObject, public QGraphicsPixmapItem
{
Q_OBJECT
public:
explicit MyGraphicsPixmapItem(QGraphicsItem *parent = 0);
signals:
public slots:
void mouseMoveEvent(QGraphicsSceneMouseEvent * event);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
};
and implement constructor and handler:
MyGraphicsPixmapItem::MyGraphicsPixmapItem(QGraphicsItem *parent) : QGraphicsPixmapItem(parent)
{
qDebug() << "constructor mygraphicspixmapitem";
setCacheMode(NoCache);
setAcceptHoverEvents(true);
setFlag(QGraphicsItem::ItemIsSelectable,true);
}
void MyGraphicsPixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent * event)
{
QPointF mousePosition = event->pos();
qDebug() << "mouseMoveEvent";
qDebug() << mousePosition;
QGraphicsPixmapItem::mouseMoveEvent(event);
}
I have also subclassed QGraphicsView and QGraphicsScene with respective mouseMoveEvent handlers that pass the event further down successfully:
void MyGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
qDebug() << "QGraphicsScene: mouseMoveEvent";
QGraphicsScene::mouseMoveEvent(event);
}
However, although MyGraphicsView is configured with ...
setMouseTracking(true);
setInteractive(true);
... MyGraphicsPixmapItem only executes the mouseMoveEvent handler when being clicked or after setting it as grabber:
void MyGraphicsPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
qDebug() << QString("item clicked at: %1, %2").arg( event->pos().x()).arg( event->pos().y() );
event->accept();
grabMouse();
}
I would like to know whether I am making a mistake or the solution given in the above-mentioned answer is flawed or incomplete. Further, I would like to know whether the mouseMoveEvent handler can be triggered without the QGraphicsPixmapItem being configured as 'grabber'. It is also acceptable if the configuration as grabber occurs automatically when the mouse first moves onto the item.
Here is a thread where a very similar problem seems to be related to the item's reference, but since I add the item to the scene like this:
thepixMapItem = new MyGraphicsPixmapItem();
scene->addItem(thepixMapItem);
I suppose this is not related.

The solution is to reimplement or filter the item's hoverMoveEvent, as mouseMoveEvent only triggers when receiving a mouse press:
If you do receive this event, you can be certain that this item also
received a mouse press event, and that this item is the current mouse
grabber.

Related

QGraphicsView artifacts when updating another views items/viewport in mouse move event handler

We are having an issue with artifacts in an application using multiple QGraphicsScene/QGraphicsViews. Essentially it seems the problem is when when in mousePosChanged event handler for a scene, and call setPos() on an item in a different scene, as well as update a region on the view for the other scene, it leaves artifacts.
I tried to set up a minimal example that hopefully will be easy to spot what is wrong
Essentially I have two scenes (scene 1 and scene 2), each with one ellipse item. When the mouse moves in the first scene, its ellipse item tracks the mouse. It also sends out a signal with mouse position. The second scene is connected to this signal, and updates the position of its ellipse item to the same location. The second graphics view is also connected to the signal, and it updates its viewport in an arbitrary location.
The second graphics view ends up with artifacts as you move the mouse around (see picture below)
This is the code from my minimal example:
class MyView : public QGraphicsView
{
Q_OBJECT
public:
MyView(QGraphicsScene *scene, QWidget *parent = 0);
public slots:
void onMouseMoveEvent();
};
class MyScene : public QGraphicsScene
{
Q_OBJECT
public:
MyScene(QObject *parent = 0);
public slots:
void setTrackerPos(const QPointF &pos);
signals:
void mousePosChanged(const QPointF &pos);
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent);
private:
QGraphicsEllipseItem *mCursorTracker;
};
MyScene::MyScene(QObject *parent) :
QGraphicsScene(QRectF(0.,0.,1000.,1000.), parent),
mCursorTracker(new QGraphicsEllipseItem(0., 0., 50., 50.))
{
mCursorTracker->setFlag(QGraphicsItem::ItemSendsGeometryChanges);
mCursorTracker->setBrush(QBrush(Qt::red, Qt::SolidPattern));
addItem(mCursorTracker);
}
void MyScene::setTrackerPos(const QPointF &pos)
{
mCursorTracker->setPos(pos);
}
void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
mCursorTracker->setPos(mouseEvent->scenePos());
emit mousePosChanged(mouseEvent->scenePos());
}
MyView::MyView(QGraphicsScene *scene, QWidget *parent) :
QGraphicsView(scene, parent)
{
setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
setMouseTracking(true);
}
void MyView::onMouseMoveEvent(const QPointF &pos)
{
viewport()->update(QRect(0,0,250,250));
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget *w = new QWidget;
QHBoxLayout *layout = new QHBoxLayout(w);
MyScene *scene1 = new MyScene(w);
MyScene *scene2 = new MyScene(w);
MyView *view1 = new MyView(scene1, w);
MyView *view2 = new MyView(scene2, w);
layout->addWidget(view1);
layout->addWidget(view2);
QObject::connect(scene1, SIGNAL(mousePosChanged(QPointF)), scene2, SLOT(setTrackerPos(QPointF)));
QObject::connect(scene1, SIGNAL(mousePosChanged(QPointF)), view2, SLOT(onMouseMoveEvent()));
w->show();
return a.exec();
}
I understand that changing to a full update on the view will fix this. However in our real application it is too expensive to repaint the whole scene. The update on the viewport is for a small foreground layer, and only one graphics item position changes (like in this example)

How can I transfer clicks in from one Qt Widget to another?

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
}

Changing the QPushButton region mask in its subclass to create a RoundButton

I am trying to create a round button by subclassing and setting the region mask so that I can reuse it in my project. I know we can override paintEvent method and draw a circle to show it as a round button. But the problem with this approach is that if user clicks outside the circle (but within button rect) it will be treated as a button click. This problem we don't see when set the region mask.
I tried to set the region by calling setmask method inside resizeEvent/paintEvent. In either of case, button will be blank. I am trying to figure out the place inside the subclass to set the region mask.
RoundAnimatingButton.h ->
#include <QPushButton>
namespace Ui {
class CRoundAnimatingBtn;
}
class CRoundAnimatingBtn : public QPushButton
{
Q_OBJECT
public:
explicit CRoundAnimatingBtn(QWidget *parent = nullptr);
~CRoundAnimatingBtn();
void StartAnimation(QColor r);
void StopAnimation();
public slots:
void timerEvent(QTimerEvent *e);
private:
Ui::CRoundAnimatingBtn *ui;
bool m_Spinning;
// QWidget interface
protected:
void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent * e) override;
};
#endif // ROUNDANIMATINGBTN_H
RoundAnimatingButton.cpp
CRoundAnimatingBtn::CRoundAnimatingBtn(QWidget *parent)
: QPushButton (parent)
, ui(new Ui::CRoundAnimatingBtn)
, m_Spinning(false)
{
ui->setupUi(this);
}
CRoundAnimatingBtn::~CRoundAnimatingBtn()
{
delete ui;
}
void CRoundAnimatingBtn::paintEvent(QPaintEvent *e)
{
QPushButton::paintEvent(e);
if(m_Spinning)
{
// Animating code
}
}
void CRoundAnimatingBtn::StartAnimation(QColor r)
{
m_Spinning=true;
startTimer(5);
}
void CRoundAnimatingBtn::StopAnimation()
{
m_Spinning=false;
this->update();
}
void CRoundAnimatingBtn::timerEvent(QTimerEvent *e)
{
if(m_Spinning)
this->update();
else
killTimer(e->timerId());
}
void CRoundAnimatingBtn::DrawRing()
{
}
void CRoundAnimatingBtn::resizeEvent(QResizeEvent *event)
{
// -----------------------------------
// This code didn't work
// -----------------------------------
QRect rect = this->geometry();
QRegion region(rect, QRegion::Ellipse);
qDebug() << "PaintEvent Reound button - " << region.boundingRect().size();
this->setMask(region);
// ----------------------------------
// ------------------------------------
// This code worked
// -------------------------------------
int side = qMin(width(), height());
QRegion maskedRegion(width() / 2 - side / 2, height() / 2 - side / 2, side,
side, QRegion::Ellipse);
setMask(maskedRegion);
}
Qt doc. provides a sample for “non-rectangular” widgets – Shaped Clock Example.
(Un-)Fortunately, I remembered this not before I got my own sample running.
I started in Qt doc. with
void QWidget::setMask(const QBitmap &bitmap)
Causes only the pixels of the widget for which bitmap has a corresponding 1 bit to be visible. If the region includes pixels outside the rect() of the widget, window system controls in that area may or may not be visible, depending on the platform.
Note that this effect can be slow if the region is particularly complex.
The following code shows how an image with an alpha channel can be used to generate a mask for a widget:
QLabel topLevelLabel;
QPixmap pixmap(":/images/tux.png");
topLevelLabel.setPixmap(pixmap);
topLevelLabel.setMask(pixmap.mask());
The label shown by this code is masked using the image it contains, giving the appearance that an irregularly-shaped image is being drawn directly onto the screen.
Masked widgets receive mouse events only on their visible portions.
See also mask(), clearMask(), windowOpacity(), and Shaped Clock Example.
(When reading this, I still missed the link to example.)
At first, I prepared a suitable pixmap for my purpose – dialog-error.png:
for which I converted an SVG from one of my applications.
I tried to apply it to a QPushButton as icon and as mask. This looked very strange. I'm not quite sure what exactly was the problem:
- using the resp. QPushButton as toplevel widget (i.e. main window)
- the fact that QPushButtons icon rendering and the mask may not match concerning position or size.
Without digging deeper, I changed the code and fixed both issues in next try:
making a derived button (like described by OP)
using the button as non-toplevel widget.
This worked soon. I added some code to make the effect more obvious:
a mouse press event handler for main window to show whether shape is considered correctly
a signal handler to show whether clicks on button (in shape) are received correctly.
So, I came to the following sample – testQPushButtonMask.cc:
#include <QtWidgets>
class MainWindow: public QWidget {
public:
explicit MainWindow(QWidget *pQParent = nullptr):
QWidget(pQParent)
{ }
virtual ~MainWindow() = default;
MainWindow(const MainWindow&) = delete;
MainWindow& operator=(const MainWindow&) = delete;
protected:
virtual void mousePressEvent(QMouseEvent *pQEvent) override;
};
void MainWindow::mousePressEvent(QMouseEvent *pQEvent)
{
qDebug() << "MainWindow::mousePressEvent:" << pQEvent->pos();
QWidget::mousePressEvent(pQEvent);
}
class RoundButton: public QPushButton {
private:
QPixmap _qPixmap;
public:
RoundButton(const QPixmap &qPixmap, QWidget *pQParent = nullptr):
QPushButton(pQParent),
_qPixmap(qPixmap)
{
setMask(_qPixmap.mask());
}
virtual ~RoundButton() = default;
RoundButton(const RoundButton&) = delete;
RoundButton& operator=(const RoundButton&) = delete;
virtual QSize sizeHint() const override;
protected:
virtual void paintEvent(QPaintEvent *pQEvent) override;
};
QSize RoundButton::sizeHint() const { return _qPixmap.size(); }
void RoundButton::paintEvent(QPaintEvent*)
{
QPainter qPainter(this);
const int xy = isDown() * -2;
qPainter.drawPixmap(xy, xy, _qPixmap);
}
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
QPixmap qPixmap("./dialog-error.png");
// setup GUI
MainWindow qWin;
qWin.setWindowTitle(QString::fromUtf8("QPushButton with Mask"));
QVBoxLayout qVBox;
RoundButton qBtn(qPixmap);
qVBox.addWidget(&qBtn);
qWin.setLayout(&qVBox);
qWin.show();
// install signal handlers
QObject::connect(&qBtn, &RoundButton::clicked,
[](bool) { qDebug() << "RoundButton::clicked()"; });
// runtime loop
return app.exec();
}
The corresponding Qt project file testQPushButtonMask.pro
SOURCES = testQPushButtonMask.cc
QT += widgets
Compiled and tested on cygwin64:
$ qmake-qt5 testQPushButtonMask.pro
$ make && ./testQPushButtonMask
Qt Version: 5.9.4
MainWindow::mousePressEvent: QPoint(23,22)
MainWindow::mousePressEvent: QPoint(62,24)
MainWindow::mousePressEvent: QPoint(62,61)
MainWindow::mousePressEvent: QPoint(22,60)
RoundButton::clicked()
Concerning the output:
I clicked into the four corners of button.
I clicked on the center of button.

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.

how to pan images in QGraphicsView

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.