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.
Related
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
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've created my own class that inherits from QGraphicsScene. I've also make two methods for mouse events. Later I do qDebug() to check if position of my click is correct, and it looks that it's not correct. It always returns me QPoint(0,0).
I've tried many mapfrom - things, but nothing worked. Is there way to make those positions work correctly?
Some code:
MyScene.cpp
#include "pianoscene.h"
#include <QDebug>
#include <QGraphicsView>
MyScene::MyScene()
{
/*setRect(0,0,100,100);
QGraphicsRectItem *kek = new QGraphicsRectItem;
QPen pen;
pen.setColor(Qt::red);
kek->setRect(0,0,50,50);
kek->setPen(pen);
this->addItem(kek);*/
}
void MyScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QPoint punkt = views().first()->mapFromScene(event->pos().toPoint());
qDebug()<<"wcisk"<<punkt;
}
void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
qDebug()<<"wcisk"<<event->pos();
}
pos() contains the item coordinates, not the scene coordinates. To get scene coordinates, use scenePos():
void MyScene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QPoint punkt = views().first()->mapFromScene(event->scenePos().toPoint());
qDebug()<<"wcisk"<<punkt;
}
void MyScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
qDebug()<<"wcisk"<<event->scenePos();
}
Also, if you just need to propagate coordinates to other functions, don't use toPoint(). If you have no actual reason to convert to a QPoint, just use QPointF, as returned by scenePos() and pos(). No need for needless conversions.
Okay, so I've started making a game using Qt so that I can learn both Qt and C++ at the same time :D
However, I'm stuck with an issue at the moment.
I'm trying to make a textbox using a QGraphicsRectItem as a container (parent), and a QGraphicsTextItem as the text itself (child). The problem I'm facing is the child's relative position to the parent. If I set a font on the QGraphicsTextItem, the positioning will be completely wrong, and it will flow outside of the container for that matter.
TextBox.h:
#ifndef TEXTBOX_H
#define TEXTBOX_H
#include <QGraphicsTextItem>
#include <QGraphicsRectItem>
#include <QTextCursor>
#include <QObject>
#include <qDebug>
class TextBox: public QObject, public QGraphicsRectItem {
Q_OBJECT
public:
TextBox(QString text, QGraphicsItem* parent=NULL);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
QString getText();
QGraphicsTextItem* playerText;
};
#endif // TEXTBOX_H
TextBox.cpp
#include "TextBox.h"
TextBox::TextBox(QString text, QGraphicsItem* parent): QGraphicsRectItem(parent) {
// Draw the textbox
setRect(0,0,400,100);
QBrush brush;
brush.setStyle(Qt::SolidPattern);
brush.setColor(QColor(157, 116, 86, 255));
setBrush(brush);
// Draw the text
playerText = new QGraphicsTextItem(text, this);
int xPos = rect().width() / 2 - playerText->boundingRect().width() / 2;
int yPos = rect().height() / 2 - playerText->boundingRect().height() / 2;
playerText->setPos(xPos,yPos);
}
void TextBox::mousePressEvent(QGraphicsSceneMouseEvent *event) {
this->playerText->setTextInteractionFlags(Qt::TextEditorInteraction);
}
Game.cpp (where the code for creating the object and such is located - only included the relevant part):
// Create the playername textbox
for(int i = 0; i < players; i++) {
TextBox* textBox = new TextBox("Player 1");
textBox->playerText->setFont(QFont("Times", 20));
textBox->playerText->setFlags(QGraphicsItem::ItemIgnoresTransformations);
scene->addItem(textBox);
}
Using the default font & size for the QGraphicsTextItem:
Setting a font & size for the QGraphicsTextItem:
The problem, as you may see, is that when I set a font and a size, the text is no longer in the center of the parent element. (Please do not unleash hell on me for bad code, I'm very new to both Qt and C++, and I'm doing this for learning purposes only).
You are calling the boundingRect() method in the constructor, so the position is set before the font is set to it's final value. If you either make a method to set the position and call it after setting the font or set the font before setting the position in the constructor it should work.
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