Qt "mousePressEvent" modify the clickable area - c++

I am having a problem with mousePressEvent(QGraphicsSceneMouseEvent *event), indeed the clickable area seems small and off centre to the QGraphicsPixmapItem it is linked to.
The red line is where the QGraphicsPixmapItem is clickable.
How would I centre it and eventually make it bigger and change it's shape ?
Here are the portions of my code that can be useful :
In player.h
class Player:public QObject, public QGraphicsPixmapItem{
Q_OBJECT
public:
Player();
void place_player(int x, int y);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
};
In player.cpp
Player::Player(): QGraphicsPixmapItem(){
}
void Player::place_player(int x,int y)
{
this->setPixmap(QPixmap("test.png"));
this->setPos(x,y);
game->scene->addItem(this);
}
void Player::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
qDebug()<< event;
};
in game.cpp
Game::Game(){
setFixedSize(1600,900);
scene = new QGraphicsScene(this);
scene->setSceneRect(0,0,1600,900);
setScene(scene);
}
void Game::start(){
player1 = new Player();
player1->place_player(300,300);
}
void Game::mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event);
}
And finally the main.cpp
int main(int argc, char *argv[]){
QApplication a(argc, argv);
game = new Game();
game->show();
game->start();
return a.exec();
}
Thanks a lot for your help

The clickable area of a QGraphicsItem is defined by its boundingRect and shape functions.
I would start by not using QGraphicsPixmapItem. You want a custom graphics item, which has the functionality of signals and slots, so derive from QGraphicsObject.
class Player : public QGraphicsObject
{
};
As we've now derived from this class, we need to override a couple of pure, virtual functions; namely boundingRect, paint
class Player : public QGraphicsObject
{
public:
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
};
The boundingRect function defines the object in local coordinates. For an example, let's assume the character will have a width and height of 100. If we set the boundingRect to return (0, 0, 100, 100), this would be oriented about the top left corner. Instead, we want to centre the bounding rect on our Player:
QRectF Player::boundingRect() const
{
return QRectF(-50, -50, 100, 100); // local coordinates, centered on the Player
}
To draw our Player, store a QPixmap in the class
class Player : public QGraphicsObject
{
public:
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
private:
QPixmap m_playerPixmap;
};
I'll assume you know how to load the pixmap and can do that in the constructor of the player.
All we need now is to render the player and we'll also show the clickable area, which is defined by the boundingRect() function: -
void Player::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget)
{
// draw the player
painter->drawPixmap(0, 0, m_playerPixmap);
// set the pen to draw debug rect
painter->setPen(QColor(255, 0, 0, 127));
// for debug purposes, show the bounding rect (clickable area)
painter->drawRect(boundingRect());
}
Originally I mentioned that the clickable area is defined by the boundingRect and shape functions. As the Player is of uniform shape (a rectangle), we only care about the boundingRect. In the case of an irregular shape, you would also override the shape function.
How would I centre it and eventually make it bigger and change it's shape ?
Hopefully you now know that to make the Player bigger, it's just a matter of increasing its local coordinates returned in the boundingRect function. So, if we want to double its width and height, we'd do this:
QRectF Player::boundingRect() const
{
return QRectF(-100, -100, 200, 200); // local coordinates, centered on the Player
}
To change its shape, implement the shape() function and to debug, paint the painterPath returned from that function, instead of drawing the boundingRect.
For example, let's have a circular, clickable area.
Assuming you've added the shape declaration to the Player header:
QPainterPath Player::shape() const
{
QPainterPath path;
path.addEllipse(-100, -100, 200, 200);
return path;
}
void Player::paint(QPainter * painter, const QStyleOptionGraphicsItem, QWidget*)
{
// draw the player
painter->drawPixmap(0, 0, m_playerPixmap);
// set the pen to draw debug path
painter->setPen(QColor(255, 0, 0, 127));
// for debug purposes, show the path (clickable area)
painter->drawPath(shape());
}
One final thing to note is that if you're overriding the shape function, you must still implement boundingRect.

Related

How to use the QGraphicsItem::setPos() function

I can't figure out how the setPos() function of the QGraphicsItem class works.
My Rect class has no parent, so its origin is relative to the scene.
I try to put the rectangle back at (0, 0) after it is moved with the mouse but it is placed in a different place depending on where I had moved it.
I suppose that means that the origin of the scene moves but what causes this change?
class Rect : public QGraphicsItem {
public:
Rect(): QGraphicsItem()
{
setFlag(ItemIsMovable);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
painter->drawRect(0, 0, 20, 20);
}
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override
{
setPos(0, 0);
update();
QGraphicsItem::mouseReleaseEvent(event);
}
QRectF boundingRect() const
{
return QRectF(0, 0, 20, 20);
}
private:
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene scene;
QGraphicsView view(&scene);
Rect obj;
scene.addItem(&obj);
view.show();
return a.exec();
}
When you create a QGraphicsView you initially accept the default settings. A standard setting is, for example, that it is horizontally centered.
Another factor is that the default area size is probably up to the maximum size.
what you can do set a custom size for the scene. You do that with graphicsView->setSceneRect(0,0,300,300); (for example)
scene = new QGraphicsScene(this);
ui->graphicsView->setScene(scene);
ui->graphicsView->setRenderHint(QPainter::Antialiasing);
ui->graphicsView->setSceneRect(0,0, 300,300);
rectItem = new QGraphicsRectItem(0,0, 100, 100);
rectItem->setPen(QPen(Qt::darkMagenta, 2));
rectItem->setBrush(QGradient(QGradient::SaintPetersburg));
rectItem->setPos(190,10);
scene->addItem(rectItem);
So in summary: if you want to work with fixed values. maybe it is better to know the total size. (that was not clear from your code, that's why I gave this example)

How to keep the size and position of QGraphicsItem when scaling the view?

I have some QGraphicsItems in the QGraphicsScene which should keep the same size and position when scaling. I've tried QGraphicsItem::ItemIgnoresTransformations but it turns out that the items get wrong positions. Below is a sample code:
I have subclassed QGraphicsView like this:
class Graphics : public QGraphicsView
{
public:
Graphics();
QGraphicsScene *scene;
QGraphicsRectItem *rect;
QGraphicsRectItem *rect2;
protected:
void wheelEvent(QWheelEvent *event);
};
And in its constructor:
Graphics::Graphics()
{
scene = new QGraphicsScene;
rect = new QGraphicsRectItem(100,100,50,50);
rect2 = new QGraphicsRectItem(-100,-100,50,50);
scene->addLine(0,200,200,0);
rect->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
scene->addItem(rect);
scene->addItem(rect2);
setScene(scene);
scene->addRect(scene->itemsBoundingRect());
}
The wheelEvent virtual function:
void Graphics::wheelEvent(QWheelEvent *event)
{
if(event->delta() < 0)
scale(1.0/2.0, 1.0/2.0);
else
scale(2, 2);
scene->addRect(scene->itemsBoundingRect());
qDebug() << rect->transform();
qDebug() << rect->boundingRect();
qDebug() << rect2->transform();
qDebug() << rect2->boundingRect();
}
orginal view looks like this:
1
take the line as road and rect aside as a symbol. When zoomed out, the rect maintain its size but jumps out of the scene:
2
which should be that topleft of rect to middle of line. I'm also confused with debug info showing that the boundingRect and transform stays the same, which seems that nothing has changed! What causes the problem and is there any way to solve it? Could someone help? Thank you!
Sorry for delay, now I've solved the problem myself.
I found QGraphicsItem::ItemIgnoresTransformations only works when the point you want stick to is at (0,0) in item's coordinate. You need also update boundingRect manually in this way. Nevertheless, the best solution I've found is subclass QGraphicsItem and set matrix in paint() according to world matrix. Below is my code .
QMatrix stableMatrix(const QMatrix &matrix, const QPointF &p)
{
QMatrix newMatrix = matrix;
qreal scaleX, scaleY;
scaleX = newMatrix.m11();
scaleY = newMatrix.m22();
newMatrix.scale(1.0/scaleX, 1.0/scaleY);
qreal offsetX, offsetY;
offsetX = p.x()*(scaleX-1.0);
offsetY = p.y()*(scaleY-1.0);
newMatrix.translate(offsetX, offsetY);
return newMatrix;
}
And the paint function:
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
QPointF p(left, top);
painter->setMatrix(stableMatrix(painter->worldMatrix(), p));
painter->drawRect(left, top, width, height);
}
The second argument of stableMatrix is sticked point, in my sample code it's top-left of the item. You can change it to your preference. It works really fine!
Hope this post help :)
The solution to this is even simpler.
QGraphicsItem::ItemIgnoresTransformations
The item ignores inherited transformations (i.e., its position is still anchored to its parent, but the parent or view rotation, zoom or shear transformations are ignored). [...]
And that's the key! Item ignores all transformations, but is still bound to its parent. So you need two items: a parent item that will keep the relative position (without any flags set) and a child item that will do the drawing (with QGraphicsItem::ItemIgnoresTransformations flag set) at parent's (0,0) point.
Here is some working code of a crosshair that have constant size and rotation, while keeping the relative position to its parent:
#include <QGraphicsItem>
#include <QPainter>
class CrossHair : public QGraphicsItem
{
private:
class CrossHairImpl : public QGraphicsItem
{
public:
CrossHairImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
painter->setPen(QPen(Qt::red, 1));
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
}
private:
qreal m_len;
};
public:
CrossHair (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF();
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty
}
private:
CrossHairImpl m_impl;
};

QMouseEvents in Graphics Scene not working

I am fairly new to Qt and creating a simple application that initializes a number of custom QGraphicsItems in a custom QGraphicsScene. Each item is initialized with a random start position and a Weight value which is dependent on the position of the item. On a mouse move event, i want the Weight value of the items to update based on the position of the mouse cursor
I think my the mouseMoveEvent is not recognized within the graphicsScene, it seems to work fine in the main window where i implemented a label in the status bar to show the number of mouseMoveEvents and the X-Y position of the mouseMoveEvent
Here is the code:
Custom graphics Scene .h:
class ParticleScene : public QGraphicsScene
{
public:
ParticleScene();
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
private:
qreal WTotal;
Particle *particle;
}
Custom Graphics Scene .cpp:
ParticleScene::ParticleScene()
{
//this->setBackgroundBrush(Qt::gray);
this->setSceneRect(0,0,500,500);
WTotal=0;
int ParticleCount =5;
for (int i =0; i<ParticleCount; i++)
{
particle= new Particle();
particle->StartX= rand()%500;
particle->StartY= rand()%500;
particle->W= qSqrt(qPow(particle->StartX,2) + qPow(particle->StartY,2));
particle->setPos(particle->StartX,particle->StartY);
this->addItem(particle);
particle->setFocus();
WTotal+=particle->W;
}
}
void ParticleScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
update();
QGraphicsScene::mouseMoveEvent(event);
}
Particle.h:
I added the Keypress event function and this moved only the last item that was added to the scene, i assume only one item can get focus.
The mouseMove event on the other hand didn't do anything
class Particle :public QGraphicsItem
{
public:
Particle();
QRectF boundingRect() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
int StartX;
int StartY;
qreal W;
protected:
//added keyPressEvent to test
virtual void keyPressEvent(QKeyEvent *event);
virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
};
Particle.cpp:
Particle::Particle()
{
// setFlag(ItemIsMovable);
setFlag(ItemIsFocusable);
}
QRectF Particle::boundingRect() const
{
return QRect(0,0,120,30);
}
void Particle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
QRectF rec= boundingRect();
QBrush Brush(Qt::white);
painter->fillRect(rec,Brush);
painter->drawText(15,15,"Weight: "+QString::number(W));
painter->drawRect(rec);
}
void Particle::keyPressEvent(QKeyEvent *event)
{
switch(event->key()){
case Qt::Key_Right:{
moveBy(30,0);
break;}
case Qt::Key_Left:{
moveBy(-30,0);
break;}
case Qt::Key_Up:{
moveBy(0,-30);
break;}
case Qt::Key_Down:{
moveBy(0,30);
break;}
}
update();
}
void Particle::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
this->W= this->W / qSqrt(qPow(event->pos().x(),2) + qPow(event->pos().y(),2));
moveBy(30,0);
update();
}
MainWindow .h and cpp: the status bar label here displays the mouse coordinates correctly i.e. mouseMoveEvent functions here
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
void mouseMoveEvent(QMouseEvent *event);
protected:
private:
Ui::MainWindow *ui;
ParticleScene *scene;
QLabel *statlabel;
int moves;
};
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
statlabel=new QLabel(this);
ui->statusBar->addWidget(statlabel);
statlabel->setText ("Mouse Coordinates");
setCentralWidget(ui->graphicsView);
centralWidget()->setAttribute(Qt::WA_TransparentForMouseEvents);
ui->graphicsView->setMouseTracking(true);
scene= new ParticleScene();
ui->graphicsView->setScene(scene);
ui->graphicsView->setRenderHint(QPainter::Antialiasing);
moves=0;
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
moves+=1;
statlabel->setText("MouseMoves " +QString::number(moves)+ " X:"+QString::number(event->pos().x())+"-- Y:"+QString::number(event->pos().y()));
}
What am I missing in the program that causes the mousemoveevent to not function and Is there a way to focus all the items together? Would i need to perhaps, make them into QList?
In the Next step of the program, I would like the items to update their weight value based on the sum of all their weights and also move based on an algorithm that uses the new weight value to determine a new position.
You are missing some kind of container in which you can save all your Particles. In the current implementation ParticleScene has particle pointer as a private variable. You are creating multiple Particles in the loop (which are QGraphicsItem's) but only the last pointer is stored in you custom Scene. As you said, you can use, for example, QList to save your Particles.
Also I assume you would like to move you items around the scene. For that you can also make you own implementation for MousePress event (to catch when item was pressed and at what coordinate), MouseMove(for actual moving the item around..you have done this already, and mouseRelease (for instance, for computing weights or sending the signal which will trigger computation of weights for all the items)
Instead of keeping all the items inside of custom scene better create a new class, for instance, ItemsManager in which all the items will be stored and the pointer to the scene. In this class you can perform all the operations concerning computation of weights for items or other operations which are required.

Qt: Overlapping semitransparent QgraphicsItem

I've been working with QGraphicsView for a while and I am facing a requisite that I am not sure if it can be fulfilled by using this framework.
Putting it as simple as possible, I have 2 overlapping RectItem with a semitransparent QBrush (the same one for both). Is it possible to prevent the overlapping area from becoming more opaque? I just want the whole area to have the same color (this will occur only if both rects are fully opaque, but sometimes that is not the case)
I know it might seem a weird requisite, but the old graphics engine my colleagues used allowed it.
Any ideas?
Qt provides various blend (composition) modes for the QPainter. Deriving your RectItem class from QGraphicsItem or QGraphicsObject, allows you to customise the painting and using the composition modes, create various effects, as demonstrated in the Qt Example.
If you want two semi-transparent items overlapping without changing the colour (assuming their colour is the same), either the QPainter::CompositionMode_Difference mode, or CompositionMode_Exclusion will do this. Here's example code of such an object: -
Header
#ifndef RECTITEM_H
#define RECTITEM_H
#include <QGraphicsItem>
#include <QColor>
class RectItem : public QGraphicsItem
{
public:
RectItem(int width, int height, QColor colour);
~RectItem();
QRectF boundingRect() const;
private:
QRectF m_boundingRect;
QColor m_colour;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0);
};
#endif // RECTITEM_H
Implementation
#include "rectitem.h"
#include <QPainter>
RectItem::RectItem(int width, int height, QColor colour)
: QGraphicsItem(), m_boundingRect(-width/2, -height/2, width, height), m_colour(colour)
{
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemIsMovable);
}
RectItem::~RectItem()
{
}
QRectF RectItem::boundingRect() const
{
return m_boundingRect;
}
void RectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->setCompositionMode(QPainter::CompositionMode_Difference);
painter->setBrush(m_colour);
painter->drawRect(m_boundingRect);
}
You can now create two RectItem objects of the same semi-transparent colour and add them to the scene
// assuming the scene and view are setup and m_pScene is a pointer to the scene
RectItem* pItem = new RectItem(50, 50, QColor(255, 0, 0, 128));
pItem->setPos(10, 10);
m_pScene->addItem(pItem);
pItem = new RectItem(50, 50, QColor(255, 0, 0, 128));
pItem->setPos(80, 80);
m_pScene->addItem(pItem);

Calling Qpainter's method paint to refresh image and change color

I have created a object call Player which inherits from QGraphicsObject.
What I am trying to do is to change the image of the player and the colour of the bounding shape when I click on it with the mouse. The thing is i don't know what values to send to player->paint() to update the image.
I override the two pure virtual functions as follows
In player.h :
class Player : public QGraphicsObject
{
public:
Player();
QRectF boundingRect() const;
QPainterPath shape() const;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget);
void mousePressEvent(QGraphicsSceneMouseEvent *event);
}
in player.cpp
Player::Player()
{
QPixmap m_playerPixmap(":/images/images/chevalier/test1.png");
}
QRectF Player::boundingRect() const
{
return QRectF(-15, 0, 128, 130);
}
QPainterPath Player::shape() const
{
QPainterPath path;
path.addEllipse(-15, 70, 100, 60);
return path;
}
void Player::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,QWidget *widget)
{
QPen linepen;
linepen.setWidth(5);
linepen.setColor(Qt::red);
painter->setPen(linepen);
painter->drawPath(shape());
painter->drawPixmap(0, 0, m_playerPixmap);
}
void Player::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
this->paint();
}
Thanks a lot for your help.
You have to call update().
This will mark the item to be updated, and issue a paint event, which will then call paint with the correct parameters