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));
}
Related
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.
I'm creating a marquee selection type tool in a QWidget and it all works fine, except for a rendering bug when dragging the marquee. If I call update without any arguments everything works beautifully, but if I call update to only include the region of the marquee as the user is dragging, then two of the edges get cutoff if the mouse is moving at a moderate speed.
Here's an image of what it looks like while dragging towards the lower right corner:
Screenshot while clicking and dragging in the widget
I thought replacing update() with repaint() might fix it but that didn't work either.
What is the correct way that I should be doing this? I've included some very basic code that demonstrates the problem.
#include <QPainter>
#include <QPaintEvent>
#include <QWidget>
class Widget : public QWidget
{
public:
Widget(QWidget *parent = nullptr)
: QWidget(parent)
{
}
void mousePressEvent(QMouseEvent *e) override
{
startPt = e->pos();
rect = QRect();
update(); // clear the entire widget
}
void mouseMoveEvent(QMouseEvent *e) override
{
rect = QRect(startPt, e->pos()).normalized();
update(rect.adjusted(-2,-2,2,2)); // adjusted to include stroke
}
void paintEvent(QPaintEvent *event) override
{
QPainter p(this);
p.setBrush(Qt::NoBrush);
p.setPen(QPen(Qt::black, 2));
p.drawRect(rect);
}
private:
QRect rect;
QPoint startPt;
};
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.
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
I have a label in my GUI that displays an image as a QPixmap. I want to be able to draw a continuous line on my image by simply clicking anywhere on the image to select the start point and then make a second point somewhere else by clicking on a other part of the image. The two points should connect immediately after placing the second point and I want to be able to continue that same line by placing more points on the image.
While I know how to draw something on a QPixmap, the mouse event which I need to use to get the coordinates for the points is really confusing me as I’m still fairly new to Qt.
Any examples for a solution would be much appreciated.
I suggest you to use QGraphicsView for this purpose. Use my code snippet which works perfectly.
Subclass QGraphicsScene:
#ifndef GRAPHICSSCENE_H
#define GRAPHICSSCENE_H
#include <QGraphicsScene>
#include <QPoint>
#include <QMouseEvent>
class GraphicsScene : public QGraphicsScene
{
Q_OBJECT
public:
explicit GraphicsScene(QObject *parent = 0);
signals:
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent);
public slots:
private:
QPolygon pol;
};
#endif // GRAPHICSSCENE_H
.cpp file:
#include "graphicsscene.h"
#include <QDebug>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
GraphicsScene::GraphicsScene(QObject *parent) :
QGraphicsScene(parent)
{
addPixmap(QPixmap("G:/2/qt.jpg"));//your pixmap here
}
void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
//qDebug() << "in";
if (mouseEvent->button() == Qt::LeftButton)
{
QPoint pos = mouseEvent->scenePos().toPoint();
pol.append(pos);
if(pol.size() > 1)
{
QPainterPath myPath;
myPath.addPolygon(pol);
addPath(myPath,QPen(Qt::red,2));
}
}
}
Usage:
#include "graphicsscene.h"
//...
GraphicsScene *scene = new GraphicsScene(this);
ui->graphicsView->setScene(scene);
ui->graphicsView->show();
Result:
If you want save new pixmap (or just get pixmap) as image, use this code:
QPixmap pixmap(ui->graphicsView->scene()->sceneRect().size().toSize());
QString filename("example.jpg");
QPainter painter( &pixmap );
painter.setRenderHint(QPainter::Antialiasing);
ui->graphicsView->scene()->render( &painter, pixmap.rect(),pixmap.rect(), Qt::KeepAspectRatio );
painter.end();
pixmap.save(filename);
With render() you can also grab different areas of your scene.
But this code can be better: we create and paint same polygon. If we can remember last painted point, then we can paint line by line (begin of line is end of last line). In this case we don't need all points, we need just last point.
As I promised(Code improvement):just provide additional variable QPoint last; instead of QPolygon pol; and use next code:
void GraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent)
{
//qDebug() << "in";
if (mouseEvent->button() == Qt::LeftButton)
{
QPoint pos = mouseEvent->scenePos().toPoint();
if(last.isNull())
{
last = pos;
}
else
{
addLine(QLine(last,pos),QPen(Qt::red,2));
last = pos;
}
}
}
As you can see, you store only last point and paint only last line. User can clicks thousands time and now you not need to store this unnecessary points and do this unnecessary repainting.