How to get the QGraphicsLayoutItem clicked in a QGraphicsWidget with QGraphicsLinearLayout - qt

I have QGraphicsLayoutItem placed inside a QGraphicsLinearLayout.
This is added to my QGraphicsWidget.
void MyCustomQGraphicsWidget::mousePressEvent(QGraphicsSceneMouseEvent * mouseEvent)
{
if (mouseEvent->button() == Qt::LeftButton) {
qDebug() << "clicked inventory";
}
}
I am having trouble finding a way to get e.g. the index of the item clicked in the layout.

There is no direct way to do it. But you could calculate and index and use the itemAt() method, suppose the mouse y coordinate is mouseY and item height is H. You can write:
int index = mouseY / H; // could be minus some margin
QGraphicsLayoutItem *item = layout.itemAt(index);

Related

Qgraphicsscene troubles to get scenePos() inside a function

I have subclassed a qgraphicsscene and trying to get the mouse coords inside a "normal" function. I only get it working on "mouse involved" function. Sorry I'm amateur programmer.
For exmample here scenePos() works:
void mousePressEvent(QGraphicsSceneMouseEvent *event)
{
// qDebug() << "Custom scene clicked.";
if(event->modifiers() == Qt::ControlModifier) {
if(event->button() == Qt::LeftButton) {
QPointF pos = {event->scenePos().x(), 70};
addChordnueve(pos); // crea 1 item at mouse x e y = 70
// } if(event->modifiers() == Qt::ControlModifier & event->modifiers() == Qt::ShiftModifier) {
qDebug() << "Control!!!";}}
Here it doesn't works at all, but got QCursor::pos() giving "weird" positions:
void preaddExtChord()
{
auto *hellos = scenePos(); //<- It doesn't works
int xplace = QCursor::pos().x()-620;
int yplace = QCursor::pos().y()-380;
QGraphicsSimpleTextItem *item = new QGraphicsSimpleTextItem("n");
item->setFont(QFont ("omheads", 20));
item->setPos(xplace, yplace);
addItem(item);
}
I searched a lot during months but couldn't find a solution,...
maybe I'm doing a wrong approach, or either there is some easier possibilitie to get the mouse coords inside this type of functions?
Thanks! :-)
If you want to obtain the position with respect to the cursor scene you must first obtain that QGraphicsView is below the cursor (a QGraphicsScene can be part of QGraphicsView), for this we must iterate and verify if it is inside the viewport, then calculate the position with respect to the scene using the mapToScene method of QGraphicsView:
QPoint p = QCursor::pos();
for(QGraphicsView *view: views()){
QWidget *viewport = view->viewport();
QRect vr = viewport->rect();
QPoint vp = viewport->mapFromGlobal(p);
if(vr.contains(vp)){
QPointF sp = view->mapToScene(vp);
QGraphicsSimpleTextItem *item = new QGraphicsSimpleTextItem("n");
item->setFont(QFont("omheads", 20));
item->setPos(sp);
addItem(item);
}
}

How can i change order of widget in layout by drag and drop with the mouse?

I make my own class from QWidget with redefine of paintEvent(), mousePressEvent(), mouseReleaseEvent() and mouseMoveEvent(). All that methods for move widgets over other widget (yellow).
When i create my widgets in a layout, it looks like this:
But when I move black widget to the bottom and red to the top like this:
and resize window, all widgets refresh to their align positions:
But i want, when i move one widget higher then another, the widgets should align in layout in new places, like this:
Which function i should redefine to do it?
P.S.
There is a piece of code, that can move widgets positions inside layout (change their indexes), but i don't know how find out their (x,y) position to calculate new indexes in layout. I think, that i can do it in resizeEvent().
But it when it event was emitted, positions already changed to old. (like before moveing on 1 picture), and i need positions after moveing (like on secon picture). How can i get position of widget before it will be aligned?
or How can i change order of widget in layout by drag and drop with the mouse?
I write my own widget, then redefine following methods: mouseReleaseEvent(), paintEvent(), mousePressEvent(), mouseMoveEvent(). In mousePressEvent() I hold old X and Y positions and mouse position on figure. Then in mouseMoveEvent() i calculate if minimum distance of mouse move is riched and move widget to new position (it not moves widget index in layout). After it, if emitted mouseReleaseEvent() i just calculate new index of moving widget and change and update parent layout. If widget moves less then it height, then layout just updates without changing widget index.
void SimpleWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if (!IsMinimumDistanceRiched(event))
{
return;
}
int y = event->globalY() - mouseClickY + oldY;
int BottomBorder = parentWidget->geometry().height() - this->geometry().height();
if(y < 0) y = 0;
else if(y > BottomBorder) y = BottomBorder;
move(oldX, y);
}
void SimpleWidget::mousePressEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton)
dragStartPosition = event->pos();
oldX = this->geometry().x();
oldY = this->geometry().y();
mouseClickX = event->globalX();
mouseClickY = event->globalY();
}
bool SimpleWidget::IsMinimumDistanceRiched(QMouseEvent *event)
{
return (event->pos() - dragStartPosition).manhattanLength() >= QApplication::startDragDistance();
}
bool SimpleWidget::moveInLayout(QWidget *widget, MoveDirection direction)
{
QVBoxLayout* myLayout = qobject_cast<QVBoxLayout*>(widget->parentWidget()->layout());
const int index = myLayout->indexOf(widget);
if (direction == MoveUp && index == 0)
{
return false;
}
if (direction == MoveDown && index == myLayout->count()-1 )
{
return false;
}
const int newIndex = direction == MoveUp ? index - 1 : index + 1;
myLayout->removeWidget(widget);
myLayout->insertWidget(newIndex , widget);
return true;
}
void SimpleWidget::paintEvent(QPaintEvent *)
{
QStyleOption o;
o.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this);
}
void SimpleWidget::mouseReleaseEvent(QMouseEvent *)
{
int y = geometry().y();
MoveDirection direct;
int offset;
if(oldY > y)
{
offset = oldY - y;
direct = MoveUp;
}
else if(oldY < y)
{
offset = y - oldY;
direct = MoveDown;
}
int count = offset/height();
for(int i = 0; i < count; i++)
{
moveInLayout(this, direct);
}
update();
QVBoxLayout* myLayout = qobject_cast<QVBoxLayout*>(this->parentWidget->layout());
myLayout->update();
this->saveGeometry();
}

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

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.

Prevent QGraphicsItem from moving outside of QGraphicsScene

I have a scene which has fixed dimensions from (0;0) to (481;270):
scene->setSceneRect(0, 0, 481, 270);
Inside of it, I have a custom GraphicsItem and I can move it thanks to the flag ItemisMovable, but I would like it to stay within the Scene; I actually mean I don't want it to have coordinates neither under (0;0) nor over (481;270).
I tried several solutions like overriding QGraphicsItem::itemChange() or even QGraphicsItem::mouseMoveEvent() but I still cannot manage to reach what I want to do.
What is the suitable solution for my needs? Do I use QGraphicsItem::itemChange() badly?
Thanks in advance.
You can override QGraphicsItem::mouseMoveEvent() like this:
YourItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseMoveEvent(event); // move the item...
// ...then check the bounds
if (x() < 0)
setPos(0, y());
else if (x() > 481)
setPos(481, y());
if (y() < 0)
setPos(x(), 0);
else if (y() > 270)
setPos(x(), 270);
}
This code keeps your complete item in scene. Not only the upper left pixel of your item.
void YourItem::mouseMoveEvent( QGraphicsSceneMouseEvent *event )
{
QGraphicsItem::mouseMoveEvent(event);
if (x() < 0)
{
setPos(0, y());
}
else if (x() + boundingRect().right() > scene()->width())
{
setPos(scene()->width() - boundingRect().width(), y());
}
if (y() < 0)
{
setPos(x(), 0);
}
else if ( y()+ boundingRect().bottom() > scene()->height())
{
setPos(x(), scene()->height() - boundingRect().height());
}
}
Warning: The suggested solutions won't work for multiply selected items. The problem is, only one of the items receives a mouse move event in that case.
Actually, the Qt Documentation on QGraphicsItem provides an example which exactly solves the problem of limiting the movement of items to the scene rect:
QVariant Component::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionChange && scene()) {
// value is the new position.
QPointF newPos = value.toPointF();
QRectF rect = scene()->sceneRect();
if (!rect.contains(newPos)) {
// Keep the item inside the scene rect.
newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left())));
newPos.setY(qMin(rect.bottom(), qMax(newPos.y(), rect.top())));
return newPos;
}
}
return QGraphicsItem::itemChange(change, value);
}
Note I: You'd have to enable the QGraphicsItem::ItemSendsScenePositionChanges flag:
item->setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemSendsScenePositionChanges);
Note II: If you only want to react on finished movement, consider using the GraphicsItemChange flag ItemPositionHasChanged

Qt tracing QGraphicsScene::itemAt() back to data model

I have a list of objects that I use to add objects into a QGraphicsScene:
for(int i = 0; i < levelObjects.length(); i++)
{
QRect objRect;
objRect = spriteSheetLocations.value(levelObjects.at(i).value("frame_name"));
//Q_ASSERT_X(objRect != QRect(0,0,0,0), "MainWindow::loadFile()", "Could not find sprite location!");
QImage img = spriteSheet.copy(objRect);
int height = levelObjects.at(i).value("height").toInt();
int width = levelObjects.at(i).value("width").toInt();
int x = levelObjects.at(i).value("x").toInt();
int y = levelObjects.at(i).value("y").toInt();
img = img.scaled(QSize(width, height), Qt::IgnoreAspectRatio);
item = scene->addPixmap(QPixmap::fromImage(img));
int xPos = x - width/2;
int yPos = levelPlist.value("level_height").toInt() - (y + height/2);
item->setPos(xPos, yPos);
}
Later on, in the GraphicsScene class, I detect when the user clicks on an item and drags it to move it:
void LevelGraphicsView::mousePressEvent(QMouseEvent *event)
{
if (QGraphicsItem *item = itemAt(event->pos())) {
qDebug() << "You clicked on item" << item;
draggedItem = item;
int mouseX = draggedItem->pos().x() - mapToScene(event->pos()).x();
int mouseY = draggedItem->pos().y() - mapToScene(event->pos()).y();
mouseOffset = QPointF(mouseX, mouseY);
} else {
qDebug() << "You didn't click on an item.";
draggedItem = NULL;
mouseOffset = QPointF(0,0);
}
}
void LevelGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
if(!draggedItem) // no item selected
return;
QPointF pos = mapToScene(event->pos()) + mouseOffset;
draggedItem->setPos(pos);
}
This works fine for moving the items in the graphics view, but I'm having trouble tracing the QGraphicsItem back to the list item that created it.
What's the best way to link the QGraphicsItem with the list item from which it was made so that the list item can be changed to reflect the change of position?
You could assign each item in your domain object a QUuid property and pass this along to a property in your QGraphicsItem. I have used this on a project and it works quite well. I added a QHash lookup table to my domain model to make it more efficient, but this would not be necessary for shorter lists.
The best way would be a way that does not require to manualy sync items from your list and items on the scene.
The best way to do that depends on your design - may be your items can become pointers to the items on the scene or they can hold ones.

Resources