mousePressEvent() method is not working as expected when calling setPos() method - qt

I have a class MenuItem which inherits from QGraphicsItem and reimplemented boundingRect(), shape(), paint(), outlineRect():
MenuItem::MenuItem(const QString& qsText, qreal qrYPos)
{
m_qsText = qsText;
m_BackgroundColor = Qt::white;
m_OutlineColor = Qt::darkBlue;
m_TextColor = Qt::darkGreen;
qDebug() << pos();
setPos(mapToParent(200,200)); //<-- when calling this method, mousePressEvent()
// behaves not as expected
qDebug() << pos();
}
QRectF MenuItem::boundingRect() const
{
const int iMargin = 1;
return outlineRect().adjusted(-iMargin, -iMargin, +iMargin, +iMargin);
}
QPainterPath MenuItem::shape() const
{
QRectF rect = outlineRect();
QPainterPath path;
path.addRect(rect);
return path;
}
void MenuItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
QPen pen(m_OutlineColor);
painter->setPen(pen);
painter->setBrush(m_BackgroundColor);
QRectF rect = outlineRect();
painter->drawRect(rect);
painter->setPen(m_TextColor);
painter->drawText(rect, Qt::AlignCenter, m_qsText);
}
void MenuItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
qDebug() << "Item Mouse Pressed";
}
QString MenuItem::getText() const
{
return m_qsText;
}
QRectF MenuItem::outlineRect() const
{
const int iPadding = 8;
QFontMetricsF metrics = QFontMetricsF(QApplication::font());
QRectF rect = metrics.boundingRect(m_qsText);
rect.adjust(-iPadding, -iPadding, +iPadding, +iPadding);
rect.translate(-rect.center());
return rect;
}
In another class, called Menu which inherits from QGraphicsScene, I added one instance of MenuItem:
Menu::Menu()
: QGraphicsScene()
{
setSceneRect(0, 0, 800, 600);
m_miNewGame = new MenuItem("New Game", 300);
this->addItem(m_miNewGame);
//m_miNewGame->setPos(200,200);
}
The Menu class reimplements mousePressEvent
void Menu::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
//qDebug() << "Menu Mouse Pressed";
MenuItem *gi = dynamic_cast<MenuItem*>(itemAt(event->pos(), QTransform()));
if (gi)
qDebug() << gi->getText();
QGraphicsScene::mousePressEvent(event); // this forwards the event to the item
if (itemAt(event->pos(), QTransform()))
{
qDebug() << "You Pressed an Item";
}
}
If I use setPos() method inside the MenuItem constructor the MenuItem gets positioned right but inside the Menu::mousePressEvent() method, MenuItem* returned from itemAt() is always NULL.
Omitting the setPos() method, the MenuItem stays in the top left corner (0,0) of the scene and mousePressEvents are handled as expected: returning the MenuItems Text with the getText() method.
Why is the MenuItem* NULL when calling setPos()?
Do I have to reimplement setPos() or what am I doing wrong?
Any help is welcome.

In MenuItem() constructor you use mapToParent. But your item doesn't have any parent item. So using mapToParent is pointless, it's equivalent to mapToScene in this case. And since your item's initial position is (0, 0) and no transformation has been applied, mapToScene will return its argument's value without changes. So it's equivalent to setPos(200, 200). It seems strange to use the result of mapToParent or mapToScene in setPos. I don't understand what you were trying to do.
QGraphicsSceneMouseEvent::pos returns coordinates of the event in target item's coordinates. Since you're using it in QGraphicsScene::keyPressEvent, the event has not been propagated to any item, and pos() always returns (0, 0). The documentation isn't clear about it, but I've checked it.
If you didn't use setPos, your item's position will be (0, 0) and itemAt(0, 0) will find your item (regardless of the point the user have actually clicked). But if you did use setPos, itemAt(0, 0) returns 0 because there is no item at this point. If you replace event->pos() with event->scenePos(), it will work correct.
However, it's unusual to reimplement QGraphicsScene::keyPressEvent to catch clicking on item. You should reimplement QGraphicsItem::mousePressEvent instead. It will be called only if the item has been clicked, and you don't have to check event's coordinates to determine that.

Related

QGraphicsItem::itemChange notified for position change but not for size change

I've got a class derived from QGraphicsEllipseItem in which I need to know when its position or size changes in any way. I handle resizing with a mouse and calls to QGraphicsEllipse::setRect.
OK, so I dutifully overrode the itemChange() method in the class and then was careful to set the ItemSendsGeometryChanges flag after creating it
// Returns a human readable string for any GraphicsItemChange enum value
inline std::string EnumName(QGraphicsItem::GraphicsItemChange e);
// Simple test ellipse class
class MyEllipse : public QGraphicsEllipseItem
{
public:
MyEllipse(int x, int y, int w, int h) : QGraphicsEllipseItem(x, y, w, h)
{
setFlags(
QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemSendsGeometryChanges);
}
// QGraphicItem overrides
virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override
{
std::stringstream oss;
oss << "ItemChange " << EnumName(change) << std::endl;
OutputDebugString(oss.str().c_str());
return __super::itemChange(change, value);
}
};
My main code creates one of these, adds it to the scene and then tries moving/resizing it.
And while I do always receive notifications after calling setPos() on the ellipse, I get NO notification after calling setRect(). I can use setRect to completely change the ellipse's geometry but my itemChange override is never called. Not with any flags.
Now obviously changing the item's rect is changing its geometry, so what am I missing?
Is there some other flag I should set? Some other way to change the size of the ellipse I should use? Some other notification virtual I can override?
The problem is that QGraphicsItem's position is not related with QGraphicsEllipseItem's rectangle. The first one is a position of the item relative to it's parent item or, if it is NULL, to it's scene. The last one is a rectangle relative to the item position where an ellipse should be drawn. The scene and QGraphicsItem's core don't know about any changes of it.
Let's take a look at this test:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QGraphicsScene scene;
QGraphicsView view(&scene);
QGraphicsEllipseItem item(10, 20, 30, 40);
scene.addItem(&item);
qDebug() << item.pos() << item.scenePos() << item.boundingRect();
item.setRect(110, 120, 30, 40);
qDebug() << item.pos() << item.scenePos() << item.boundingRect();
view.resize(500, 500);
view.show();
return app.exec();
}
Output is:
QPointF(0,0) QPointF(0,0) QRectF(9.5,19.5 31x41)
QPointF(0,0) QPointF(0,0) QRectF(109.5,119.5 31x41)
Possible ways out:
use setTransform. Transform matrix changes are tracked by standard QGraphicsitems, and itemChange will receive corresponding change. But I guess that non-ident matrices can decrease performance (didn't check).
implement your own function like setRect in which you will track geometry changes manually.
subclass QGraphicsItem, not QGraphicsEllipseItem. In this case you can prevent untrackable geometry changes as they are performed through your rules. It looks like this:
class EllipseItem: public QGraphicsItem
{
public:
// Here are constructors and a lot of standard things for
// QGraphicsItem subclassing, see Qt Assistant.
...
// This method is not related with QGraphicsEllipseItem at all.
void setRect(const QRectF &newRect)
{
setPos(newRect.topLeft());
_width = newRect.width();
_height = newRect.height();
update();
}
QRectF boundingRect() const override
{
return bRect();
}
void paint(QPainter * painter, const QStyleOptionGraphicsItem * option,
QWidget * widget = nullptr) override
{
painter->drawRect(bRect());
}
private:
qreal _width;
qreal _height;
QRectF bRect() const
{
return QRectF(0, 0, _width, _height);
}
};
You also should track item transformations and moves through QGraphicsItem::itemChange.

Get mouse position in child QGraphicsScene

In Qt5, I have a main window with a scene:
MyWindow::MyWindow(QWidget *parent) : QMainWindow(parent)
{
view = new QGraphicsView();
scene = new QGraphicsScene();
scene->installEventFilter(this);
view->setScene(scene);
...
setCentralWidget(view);
}
view and scene are both private members of MyWindow. I want to know, in the MyWindow class, the mouse position when I click on the scene. That's why I use installEventFilter above. And I have tried to catch the event with this:
bool MyWindow::eventFilter(QObject *target, QEvent *event)
{
if (target == scene)
{
if (event->type() == QEvent::GraphicsSceneMousePress)
{
const QGraphicsSceneMouseEvent* const me = static_cast<const QGraphicsSceneMouseEvent*>(event);
const QPointF position = me->pos();
cout << position.x() << "," << position.y() << endl;
}
}
return QMainWindow::eventFilter(target, event);
}
This code does not work as expected: The position it prints when I click on the scene is always 0,0. Any clue about what is wrong?
QGraphicsSceneMouseEvent.pos() returns position in coordinates of item on which you clicked. Your scene has no items so it returns (0,0). If you want to get position in scene coordinates use scenePos().
const QPointF position = me->scenePos();

Can't get positions of QGraphicsItems in scene

I am trying to get the positions of the graphicsitems in the scene.
But their QPointF value always remains (0,0).
I am painting when mouse-click event occurs. On debugging scene->items(), I get
(QGraphicsItem(this =0x22edff0, parent =0x0, pos =QPointF(0, 0) , z = 0 , flags = ( ) ) )
for each graphics item in scene but with different memory address.
This is my mainwindow.cpp code:
#include "mainwindow.h"
#include <QDebug>
MainWindow::MainWindow()
{
scene = new QGraphicsScene;
view = new QGraphicsView;
view->setScene(scene);
button = new QPushButton("Item");
QGridLayout *layout = new QGridLayout;
layout->addWidget(button);
layout->addWidget(view);
setLayout(layout);
connect(button, SIGNAL(clicked()), this, SLOT(createItem()));
}
void MainWindow::createItem()
{
myEntity = new Item;
scene->addItem(myEntity);
count_items();
}
void MainWindow::count_items()
{
qDebug() << scene->items().count();
qDebug() << scene->items();
}
MainWindow::~MainWindow()
{}
This is my item.cpp code:
#include "item.h"
Item::Item()
{
ClickFlag = true;
PaintFlag = false;
}
Item::~Item(){}
QRectF Item::boundingRect() const
{
// outer most edges
return QRectF(0,0,1450,1400);
}
void Item::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if(event->button()==Qt::LeftButton){
if(ClickFlag){
x = event->pos().x();
y = event->pos().y();
PaintFlag = true;
ClickFlag = false;
}
}
}
void Item::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
QWidget *widget)
{
if(PaintFlag){
QPen paintPen;
paintPen.setWidth(4);
pt.setX(x);
pt.setY(y);
painter->setPen(paintPen);
painter->drawPoint(x,y);
update();
}
}
I can't seem to find the position of these items correctly.
This task is supposed to be implemented in another way. For example:
Use QGraphicsScene::addEllipse to add small ellipse (which will look like a point) to the scene. Save the pointer to it in a class variable. The ellipse itself should be at the center, e.g. (-1, -1, 2, 2).
Reimplement QGraphicsScene::mousePressEvent, detect mouse clicks and call setPos for the ellipse item (or add new ellipse each time and immediately call setPos if you need multiple points).
Use QGraphicsItem::pos to get previously set positions.
Reimplementing QGraphicsItem::paint is usually an over-complication. Qt have plenty of item classes for all common needs. Just build your scene from geometric primitives, pixmaps, etc.

Need QGraphicsScene signal or event for _after_ change

I use QGraphicsScene of the Qt framework. Inside the scene I have some QGraphicsItems which the user can select and move.
I would like to have an info label where the current x and y coordinate of the currently moved selection (can consist of many items) is displayed.
I have tried with the signal changed of QGraphicsScene. But it is fired before the x() and y() property of the items is set to the new values. So the labels always show the second-to-last coordinates. If one moves the mouse slowly, the display is not very wrong. But with fast moves and sudden stops, the labels are wrong. I need a signal that is fired after the scene hast changed.
I have also tried to override the itemChange method of QGraphicsItem. But it is the same. It is fired before the change. (The new coordinates are inside the parameters of this method, but I need the new coordinates of all selected items at once)
I have also tried to override the mouseMove events of QGraphicsScene and of QGraphicsView but they, too, are before the new coordinates are set.
I did a test: I used a oneshot timer so that the labels are updated 100 ms after the signals. Then everything works fine. But a timer is no solution for me.
What can I do?
Make all items un-moveable and handle everything by my own?
QGraphicsItem::itemChange() is the correct approach, you were probably just checking the wrong flag. Something like this should work fine:
QVariant::myGraphicsItem( GraphicsItemChange change, const QVariant &value )
{
if( change == QGraphicsItem::ItemPositionHasChanged )
{
// ...
}
}
Note the use of QGraphicsItem::ItemPositionHasChanged rather than QGraphicsItem::ItemPositionChange, the former is called after the position changes rather than before.
The solution is to combine various things that you're already doing. Instrument itemChange, looking for and count the items with updated geometry. Once you've counted as many items as there are in the current selection, fire off a signal that will have everything ready for updating your status. Make sure you've set the QGraphicsItem::ItemSendsGeometryChanges flag on all your items!
This code was edited to remove the lag inherent in using a zero-timer approach. Below is a sscce that demonstrates it.
You create circles of random radius by clicking in the window. The selection is toggled with Ctrl-click or ⌘-click. When you move the items, a centroid diamond follows the centroid of the selected group. This gives a visual confirmation that the code does indeed work. When the selection is empty, the centroid is not displayed.
I've gratuitously added code to show how to leverage Qt's property system so that the items can be generic and leverage the notifier property of a scene if it has one. In its absence, the items simply don't notify, and that's it.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scenemod-11232425
#include <QtWidgets>
const char kNotifier[] = "notifier";
class Notifier : public QObject
{
Q_OBJECT
int m_count = {};
public:
int count() const { return m_count; }
void inc() { m_count ++; }
void notify() { m_count = {}; emit notification(); }
Q_SIGNAL void notification();
};
typedef QPointer<Notifier> NotifierPointer;
Q_DECLARE_METATYPE(NotifierPointer)
template <typename T> class NotifyingItem : public T
{
protected:
QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override {
QVariant v;
if (change == T::ItemPositionHasChanged &&
this->scene() &&
(v=this->scene()->property(kNotifier)).isValid())
{
auto notifier = v.value<NotifierPointer>();
notifier->inc();
if (notifier->count() >= this->scene()->selectedItems().count()) {
notifier->notify();
}
}
return T::itemChange(change, value);
}
};
// Note that all you need to make Circle a notifying item is to derive from
// NotifyingItem<basetype>.
class Circle : public NotifyingItem<QGraphicsEllipseItem>
{
QBrush m_brush;
public:
Circle(const QPointF & c) : m_brush(Qt::lightGray) {
const qreal r = 10.0 + (50.0*qrand())/RAND_MAX;
setRect({-r, -r, 2.0*r, 2.0*r});
setPos(c);
setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemSendsGeometryChanges);
setPen({Qt::red});
setBrush(m_brush);
}
};
class View : public QGraphicsView
{
Q_OBJECT
QGraphicsScene scene;
QGraphicsSimpleTextItem text;
QGraphicsRectItem centroid{-5, -5, 10, 10};
Notifier notifier;
int deltaCounter = {};
public:
explicit View(QWidget *parent = {});
protected:
Q_SLOT void gotUpdates();
void mousePressEvent(QMouseEvent *event) override;
};
View::View(QWidget *parent) : QGraphicsView(parent)
{
centroid.hide();
centroid.setRotation(45.0);
centroid.setPen({Qt::blue});
centroid.setZValue(2);
scene.addItem(&centroid);
text.setPos(5, 470);
text.setZValue(1);
scene.addItem(&text);
setRenderHint(QPainter::Antialiasing);
setScene(&scene);
setSceneRect(0,0,500,500);
scene.setProperty(kNotifier, QVariant::fromValue(NotifierPointer(&notifier)));
connect(&notifier, &Notifier::notification, this, &View::gotUpdates);
connect(&scene, &QGraphicsScene::selectionChanged, &notifier, &Notifier::notification);
}
void View::gotUpdates()
{
if (scene.selectedItems().isEmpty()) {
centroid.hide();
return;
}
centroid.show();
QPointF centroid;
qreal area = {};
for (auto item : scene.selectedItems()) {
const QRectF r = item->boundingRect();
const qreal a = r.width() * r.height();
centroid += item->pos() * a;
area += a;
}
if (area > 0) centroid /= area;
auto st = QStringLiteral("delta #%1 with %2 items, centroid at %3, %4")
.arg(deltaCounter++).arg(scene.selectedItems().count())
.arg(centroid.x(), 0, 'f', 1).arg(centroid.y(), 0, 'f', 1);
this->centroid.setPos(centroid);
text.setText(st);
}
void View::mousePressEvent(QMouseEvent *event)
{
const auto center = mapToScene(event->pos());
if (! scene.itemAt(center, {})) scene.addItem(new Circle{center});
QGraphicsView::mousePressEvent(event);
}
int main(int argc, char *argv[])
{
QApplication app{argc, argv};
View v;
v.show();
return app.exec();
}
#include "main.moc"

Position of icon in QTreeWidgetItem

My QTreeWidget has a single column. Its items have a check box, an icon, and text. If the user clicks inside an item, I want to know whether the icon was clicked. How can I find the position and size of the icon in a QTreeWidgetItem?
Updated to add: Here is the code for my eventual solution, as requested by webclectic.
First, I sub-classed QItemDelegate so that I could access the coordinates of each part of a QTreeWidgetItem (check box, icon, and text). Here is the header file:
#include <QItemDelegate>
class MyItemDelegate : public QItemDelegate
{
Q_OBJECT
public:
explicit MyItemDelegate (MyTreeWidget *parent)
: QItemDelegate (parent), ParentView (parent) { }
~MyItemDelegate() { }
void GetRects (const QModelIndex &index, QRect& CheckBox, QRect& Icon, QRect& Text) const ;
private:
MyTreeWidget* ParentView ;
} ;
And here is the source file:
void MyItemDelegate::GetRects (const QModelIndex &index, QRect& CheckBox, QRect& Icon, QRect& Text) const
{
QStyleOptionViewItem option = ParentView -> viewOptions() ;
CheckBox = rect (option, index, Qt::CheckStateRole) ;
Icon = rect (option, index, Qt::DecorationRole) ;
Text = rect (option, index, Qt::DisplayRole) ;
doLayout (option, &CheckBox, &Icon, &Text, true) ;
QRect VisualRect = ParentView -> visualRect (index) ;
CheckBox.translate (VisualRect.topLeft()) ;
Icon.translate (VisualRect.topLeft()) ;
Text.translate (VisualRect.topLeft()) ;
}
Then I added a MyItemDelegate* member to MyTreeWidget, and set it as the item view's delegate. In the header:
class MyTreeWidget : public QTreeWidget
{
...
MyItemDelegate* Delegate ;
...
} ;
In the source :
MyTreeWidget::MyTreeWidget (QObject* parent)
{
...
Delegate = new MyItemDelegate (this) ;
setItemDelegate (ItemDelegate) ;
}
Now, to get the coordinates of each part of a QTreeWidgetItem:
QTreeWidgetItem* item ;
...
QModelIndex ModelIndex = indexFromItem (item) ;
QRect CheckBoxRect, IconRect, TextRect ;
ItemDelegate -> GetRects (ModelIndex, &CheckBoxRect, &IconRect, &TextRect) ;
Unfortunately there is no simple way to achieve what you want. The problem is that QTreeWidget is responsible for painting its items so the item itself has no information about the position of its elements in the view.
First of all you have to subclass QTreeWidget and reimplement the mousePressEvent (or mouseReleaseEvent if you prefer). Inside the event you should calculate the position of the icon and handle it correspondingly.
Sample code (but untested) follows:
void mousePressEvent(QMouseEvent *event)
{
QModelIndex clickedIndex = indexAt(event->pos());
// make sure the event was on a valid item
if (clickedIndex.isValid() == false)
return;
// Get the tree widget's x position
int treeX = header()->sectionViewportPosition(0);
// Get the x coordinate of the root item. It is required in order to calculate
// the identation of the item
int rootX = visualRect(rootIndex()).x();
// Get the rectangle of the viewport occupied by the pressed item
QRect vrect = visualRect(clickedIndex);
// Now we can easily calculate the x coordinate of the item
int itemX = treeX + vrect.x() - rootX;
// The item is a checkbox, then an icon and finally the text.
// 1. Get the rect surrounding the checkbox
QRect checkboxRect = QRect(itemX,
vrect.y(),
style()->pixelMetric(QStyle::PM_IndicatorWidth),
vrect.height());
// 2. Get the rect surrounding the icon
QRect iconRect = QRect(itemX + checkboxRect.width(),
vrect.y(),
iconSize().width(),
vrect.height());
// 3. Finally get the rect surrounding the text
QRect textRect = QRect(itemX + checkboxRect.width() + iconRect.width(),
vrect.y(),
vrect.width() - checkboxRect.width() - iconRect.width(),
vrect.height());
// Now check where the press event took place and handle it correspondingly
if(checkboxRect.contains(event->pos()))
{
qDebug() << "Checkbox pressed";
QTreeWidget::mousePressEvent(event);
return;
}
else if (iconRect.contains(event->pos()))
{
qDebug() << "Icon pressed";
QTreeWidget::mousePressEvent(event);
return;
}
else
{
qDebug() << "Text pressed";
QTreeWidget::mousePressEvent(event);
return;
}
}
I repeat that the code is untested but you get the idea about how to achieve what you want.

Resources