Update QGraphicsLineItem correctly - c++

I'm trying to draw a line using the mouseEvent functions from my QGraphicsScene, when i press the mouse button and move over the scene, the line begin from the top left [0,0] when it should start at the point where i pressed the mouse button, but when i release and do that again the line is drawn normally, what is the reason for this behavior and how to resolve it ?
Here is the full code:
Scene.h:
#ifndef SCENE_H
#define SCENE_H
#include <QGraphicsScene>
#include <QGraphicsLineItem>
#include <QGraphicsSceneMouseEvent>
class Scene : public QGraphicsScene
{
public:
Scene();
private:
QGraphicsLineItem* line;
QPointF startPoint;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
};
#endif // SCENE_H
Scene.cpp:
#include "Scene.h"
Scene::Scene() : startPoint(0,0)
{
line = new QGraphicsLineItem();
this->addItem(line);
}
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
startPoint = event->scenePos();
}
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
qreal x = event->scenePos().x();
qreal y = event->scenePos().y();
line->setLine(startPoint.x(),startPoint.y(),x,y);
}
Main:
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include "Scene.h"
int main(int argc,char* argv[])
{
QApplication app(argc,argv);
Scene scene;
QGraphicsView view(&scene);
view.setMinimumSize(800,600);
view.setAlignment(Qt::AlignLeft|Qt::AlignTop);
view.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
view.show();
return app.exec();
}

The problem is caused because the QGraphicsScene object is not initialized with a bounding box. In the constructor use
Scene::Scene() : QGraphicsScene(0, 0, 400, 400), startPoint(0,0)
(for example), and it will work as expected.
If you do not do this, at the time of the first click the scene coordinates will relocate so that the starting coordinates of your line (say, (x0, y0)) will be the scene coordinates of the top-left corner. So if you move a mouse just a bit, e.g. to (x0+1,y0), the actual scene coordinates will become (2*x0+1,2*y0).
Also, it is good practice to call the parent class' event handlers at the end of your overloaded ones.
Finally, this can be helpful:
https://www.walletfox.com/course/qgraphicsitemruntimedrawing.php

Related

Issue moving an QGraphicsItem in a custom QGraphicsView

I am having issues moving a QGraphicItem in a custom QGraphicView class. What I would like to be able to to do is select the item by a left mouse click and then move it to where I've done a right mouse click.
I stongly suspect that my problem is that QGraphicsItem::setPos() requires the coordinates to be in parent coordinates, and I'm unsure which for of QMouseEvent::*Pos() to use, and how to convert it to parent coordinates.
Screens shots of what is happening, versus what I what follow the code.
main.cpp: (simple main here, standard test harness)
#include "QtTest.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QtTest w;
w.show();
return a.exec();
}
QtTest.h: (This defines the main application window)
#pragma once
#include <QtWidgets/QMainWindow>
class QGraphicsView;
class QGraphicsScene;
class QGraphicsItem;
class QMouseEvent;
class QtTest : public QMainWindow
{
Q_OBJECT
public:
QtTest(QWidget *parent = Q_NULLPTR);
private:
QGraphicsView* m_gv;
QGraphicsScene* m_pScene;
void setupUI();
};
QtTest.cpp: (implementation of the main application window)
#include "QtTest.h"
#include "testGV.h"
#include <QVariant>
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QMainWindow>
#include <QMouseEvent>
#include <QWidget>
QtTest::QtTest(QWidget *parent): QMainWindow(parent)
{
setupUI();
}
void QtTest::setupUI()
{
QWidget *centralWidget;
if (objectName().isEmpty())
setObjectName("QtTestClass");
resize(600, 400);
centralWidget = new QWidget(this);
centralWidget->setObjectName("centralWidget");
m_gv = new testGV(centralWidget);
m_gv->setObjectName("graphicsView");
m_gv->setGeometry(QRect(100, 10, 441, 331));
setCentralWidget(centralWidget);
}
testGV.h: (definition of custom widget)
#pragma once
#include <QGraphicsView>
#include <QGraphicsItem>
class testGV : public QGraphicsView
{
Q_OBJECT
public:
testGV(QWidget* parent);
protected:
void mousePressEvent(QMouseEvent*);
private:
QGraphicsScene* m_pScene;
QGraphicsItem* m_pItem;
void createScene();
};
testGV.cpp: (implementation of custom widget)
#include "testGV.h"
#include <QGraphicsView>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QMouseEvent>
testGV::testGV(QWidget* parent) : QGraphicsView(parent)
{
createScene();
}
void testGV::createScene()
{
m_pScene = new QGraphicsScene();
m_pScene->addRect(QRect(30, 30, 150, 150), QPen(Qt::black), QBrush(Qt::black, Qt::NoBrush));
QGraphicsEllipseItem* pTemp = m_pScene->addEllipse(QRect(0, 0, 15, 15), QPen(Qt::black), QBrush(Qt::red, Qt::SolidPattern));
pTemp->setFlag(QGraphicsItem::ItemIsMovable);
pTemp->setFlag(QGraphicsItem::ItemSendsGeometryChanges);
setScene(m_pScene);
}
void testGV::mousePressEvent(QMouseEvent* pEvent)
{
if (pEvent->button() == 1) // left button click
{
m_pItem = itemAt(pEvent->pos());
}
else if (pEvent->button() == 2) // right button click
{
m_pItem->setPos(pEvent->pos());
m_pScene->update();
}
}
The image on the left is the initial display, when I right click on the red dot and then click in the square at about where the black dot is I get the image on the right. What I'm after is the red dot moving to where I clicked.
The cause of the problem is that the QMouseEvent has the position information in the coordinates of the view but the item uses the coordinates of the scene. The solution in that case is to map the coordinates of the view to the coordinates of the scene:
void testGV::mousePressEvent(QMouseEvent* pEvent)
{
if (pEvent->button() == Qt::LeftButton)
m_pItem = itemAt(pEvent->pos());
else if (pEvent->button() == Qt::RightButton)
if(m_pItem)
m_pItem->setPos(mapToScene(pEvent->pos()));
}
But even so there is a problem, the position for your items is with respect to the topLeft so the displacement will have an error, if you want to avoid that deviation then you must consider the position of the left click:
void testGV::mousePressEvent(QMouseEvent* pEvent)
{
if (pEvent->button() == Qt::LeftButton){
m_pItem = itemAt(pEvent->pos());
m_pItem->setData(0, mapToScene(pEvent->pos()));
}
else if (pEvent->button() == Qt::RightButton)
if(m_pItem){
QPointF p = m_pItem->data(0).toPointF();
QPointF sp = mapToScene(pEvent->pos());
m_pItem->setPos(m_pItem->pos() + sp - p);
m_pItem->setData(0, sp);
}
}
Note: When a pointer is created it points to any memory location so that can cause problems so I recommend initializing it to nullptr and also checking if the pointer is valid:
testGV::testGV(QWidget* parent) : QGraphicsView(parent), m_pItem(nullptr)
{
createScene();
}

How to make a canvas QWidget that can be zoomed and drawn on?

I'm trying to create a canvas to draw on with the mouse similar to most digital painting applications that I can also zoom in on (zoom in on the drawn image)
So far I've created a class that uses QWidget and added it to the ui then use the mouse events and QPaintEvent to draw on this widget which works. However the problem I'm not sure how do I zoom on this as well? I tried placing the QWidget inside of a scrollable area but it stops it from registering click events. I also tried extending from QGraphicsViewer instead of QWidget but this stops me from being able to paint as well.
//Class definition
PaintArea::PaintArea(QWidget *parent) : QWidget(parent)
{
this->setMouseTracking(true);
}
I'm mostly looking for a recommendation of how to scrolling and drawing with a mouse on the same widget (Possibly with scroll bar but just wheel scrolling for sure)
Thanks
If you follow the QWidget way, you may want to look carefully to the scribble example that is included with Qt docs. In this example, the drawing is made off-screen on a QImage object, which is then painted by the widget. The problem is to zoom the image.
I prefer your second way: QGraphicsView has a scale() function among many other excellent features. You may do something similar to the scribble example: draw off-screen on a QPixmap image which is set (every time you change the image) into a QGraphicsPixmapItem which belongs to the QGraphicsScene. I've implemented this crude example, borrowing some elements from the scribble example. Use the mouse wheel to zoom the image (it screws the scrolling a bit, sorry).
test.pro
QT += core gui widgets
CONFIG += c++11
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
drawablescene.cpp \
main.cpp \
mainwindow.cpp
HEADERS += \
drawablescene.h \
mainwindow.h
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "drawablescene.h"
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
void wheelEvent(QWheelEvent *event) override;
private:
QGraphicsView *m_view;
DrawableScene *m_scene;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include <QGraphicsView>
#include <QWheelEvent>
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
m_view(new QGraphicsView(this)),
m_scene(new DrawableScene(this))
{
setCentralWidget(m_view);
m_scene->setSceneRect(0,0,640,480);
m_view->setScene(m_scene);
}
void MainWindow::wheelEvent(QWheelEvent *event)
{
qreal delta = 1 + (event->delta() > 0 ? 0.1 : -0.1);
m_view->scale(delta, delta);
event->accept();
}
drawablescene.h
#ifndef DRAWABLESCENE_H
#define DRAWABLESCENE_H
#include <QObject>
#include <QGraphicsScene>
#include <QGraphicsPixmapItem>
class DrawableScene : public QGraphicsScene
{
public:
explicit DrawableScene(QObject *parent = nullptr);
private:
void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override;
void drawLineTo(const QPointF &endPoint);
bool m_modified;
bool m_scribbling;
int m_penWidth;
QColor m_penColor;
QPointF m_lastPoint;
QPixmap *m_image;
QGraphicsPixmapItem *m_item;
};
#endif // DRAWABLESCENE_H
drawablescene.cpp
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include "drawablescene.h"
DrawableScene::DrawableScene(QObject *parent)
: QGraphicsScene(parent),
m_modified(false),
m_scribbling(false),
m_penWidth(3),
m_penColor(Qt::blue)
{
m_image = new QPixmap(640, 480);
m_image->fill(Qt::white);
m_item = addPixmap(*m_image);
}
void DrawableScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if ((event->buttons() & Qt::LeftButton) && m_scribbling) {
drawLineTo(event->scenePos());
event->accept();
}
else QGraphicsScene::mouseMoveEvent(event);
}
void DrawableScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
m_lastPoint = event->scenePos();
m_scribbling = true;
event->accept();
}
else QGraphicsScene::mousePressEvent(event);
}
void DrawableScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton && m_scribbling) {
drawLineTo(event->scenePos());
m_scribbling = false;
event->accept();
}
else QGraphicsScene::mouseReleaseEvent(event);
}
void DrawableScene::drawLineTo(const QPointF &endPoint)
{
QPainter painter(m_image);
painter.setPen(QPen(m_penColor, m_penWidth, Qt::SolidLine, Qt::RoundCap,Qt::RoundJoin));
painter.drawLine(m_lastPoint, endPoint);
m_modified = true;
m_lastPoint = endPoint;
m_item->setPixmap(*m_image);
}

Color rectangles using QGraphicsRectItem

I'am stuck in one case. I have scene (using QGraphicsScene) and I fill that scene with squares (using QGraphicsRectItem). I want make every square color to black as I move mouse over squares with mouse button pressed. Can you please give me any idea how to make that happen ? I was trying to solve that using mousePressEvent, mouseMoveEvent, dragEnterEvent etc. and I think that this is a proper way to do that but I have no idea how to push that through. To put more light on my case I have added sample of my code. Thanks for help.
main.cpp
#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include "square.h"
#include "background.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// create a scene
QGraphicsScene * scene = new QGraphicsScene(0,0,200,250);
Background * background = new Background();
background->fillBackgroundWithSquares(scene);
// add a view
QGraphicsView * view = new QGraphicsView(scene);
view->show();
return a.exec();
}
background.h
#ifndef BACKGROUND_H
#define BACKGROUND_H
#include <QGraphicsScene>
#include <square.h>
class Background
{
public:
Background();
void fillBackgroundWithSquares(QGraphicsScene *scene);
};
#endif // BACKGROUND_H
background.cpp
#include "background.h"
Background::Background()
{
}
void Background::fillBackgroundWithSquares(QGraphicsScene *scene)
{
// create an item to put into the scene
Square *squares[20][25];
// add squares to the scene
for (int i = 0; i < 20; i++)
for (int j = 0; j < 25; j++) {
squares[i][j] = new Square(i*10,j*10);
scene->addItem(squares[i][j]);
}
}
square.h (EDIT)
#ifndef SQUARE_H
#define SQUARE_H
#include <QGraphicsRectItem>
#include <QGraphicsView>
class Square : public QGraphicsRectItem
{
public:
Square(int x, int y);
private:
QPen pen;
protected:
void hoverEnterEvent(QGraphicsSceneHoverEvent * event);
};
#endif // SQUARE_H
square.cpp (EDIT)
#include "square.h"
Square::Square(int x, int y)
{
// draw a square
setRect(x,y,10,10);
pen.setBrush(Qt::NoBrush);
setPen(pen);
setBrush(Qt::cyan);
setAcceptHoverEvents(true);
setAcceptedMouseButtons(Qt::LeftButton);
show();
}
void Square::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
if ( brush().color() != Qt::black && QApplication::mouseButtons() == Qt::LeftButton)
{
setBrush( Qt::black );
update();
}
}
Try calling:
square[j][j]->setAcceptedMouseButtons(...)
and
square[i][j]->show()
after creating it.
You can also reimplement hoverEnterEvent() and hoverLeaveEvent() if you want to change the color on the hover event.
If the mouse butten needs to be pressed when you hover: you store button state within mouse down/up event in a variable for ex. bool isMouseButtonDown and check this in your hover event handler.
You can also use: QApplication::mouseButtons() to check the buttons states.

QGraphicsRectItem move with mouse. How to?

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.

How to draw a line on a QPixmap using points

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.