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

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.

Related

Qt (Python): Change the appearance of a dragged QWidget [duplicate]

I have a QTableView with my own implemented QAbstractItemModel, in which I can drag and drop multiple items inside. My problem is that when dragging the items and while trying to drop them in a destination cell, it is not so obvious for the user what the result is going to be. For example, I have the following,but I would prefer sth like the default widows displaying, which makes all 3 items like one item:
my QT Table
vs
windows dragging n dropping folders
After eyllanesc's suggestion for QPixmap, I found the correct solution to my problem, so that I can keep the mime data coming from my model. I have re-implemented startDrag(Qt::DropActions supportedActions) in my QTreeView class, so that when multiple objects are moved, one icon will be displayed along with the number of items moved. Now looks like this:
void MyTreeView::startDrag(Qt::DropActions supportedActions)
{
QModelIndexList indexes = selectedIndexes();
if (indexes.size() == 1)
return QAbstractItemView::startDrag(supportedActions);
if (indexes.count() > 0)
{
QMimeData *data = model()->mimeData(indexes);
if (!data)
return;
QRect rect;
rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
QDrag *drag = new QDrag(this);
ActionTreeItem* pItem = static_cast<ActionTreeItem*>(indexes[0].internalPointer());
if (pItem != NULL)
{
QPixmap pixmap = myIcon.pixmap(myIcon.actualSize(QSize(32, 32)));
QPainter *paint = new QPainter(&pixmap);
paint->setPen(Qt::black);
paint->setBrush(QBrush(Qt::white));
QRect numberRect(18, 18, 13, 13);
paint->drawRect(numberRect);
paint->drawText(numberRect, Qt::AlignHCenter | Qt::AlignVCenter, QString("%1").arg(indexes.count()));
drag->setPixmap(pixmap);
}
drag->setMimeData(data);
Qt::DropAction defaultDropAction = Qt::MoveAction;
drag->exec(supportedActions, defaultDropAction);
}
}
Taking this tutorial as a reference, the mousePressEvent method is overwritten, and a new QPixmap is placed in QDrag:
void mousePressEvent(QMouseEvent *event){
if (event->button() == Qt::LeftButton){
QDrag *drag = new QDrag(this);
drag->setMimeData(new QMimeData());
drag->setPixmap(QPixmap("image.png"));
drag->exec();
}
QTableView::mousePressEvent(event);
}
Output:

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);
}
}

Issue propagating simulated MouseEvents (Drag purpose) to parents

I'm working on a Qt/Qml application.
I'm currently trying to emulate drag behavior through Qt on drag QML elements (Listview, Flickable...) for tests purpose.
I have a specific issue that I wanted to solve with the most generic solution possible, my component is a non-interactive ListView, nested by an interactive ListView, nested itself with a MouseArea :
ListView {
anchors.fill: parent
interactive: false
ListView {
anchors.fill: parent
MouseArea {
...
}
}
}
So. My idea was : take a QML object, local coordinates (x, y) where the movement starts, find it's most nested child at position and apply the movement (dx, dy) to this child. If I'm understanding correctly how QT/QML works, it should send the event to parent if not used by the child, and Flickable components should be able to detect drags and catch them.
void xx::touchAndDrag(QObject *object, const int x, const int y, const int dx, const int dy)
{
timer = new QTimer(this);
timerIteration = 0;
deltaPoint = new QPointF(dx, dy);
parentItem = qobject_cast<QQuickItem *>(object);
item = parentItem;
startPoint = QPointF(x, y);
QPointF tempPoint = startPoint;
for( ;; ) {
//Find the most nested child at coordinate
QQuickItem* child = item->childAt(tempPoint.x(), tempPoint.y());
if(child) {
item = child;
tempPoint = child->mapFromItem(parentItem, tempPoint);
qDebug() << "child found " << item;
} else {
break;
}
}
timer->setInterval(movementDuration / nbIteration);
qDebug() << "interval " << timer->interval();
timer->setSingleShot(false);
connect(timer, SIGNAL(timeout()), this, SLOT(mouseMove()));
// Send press event at starting point
QMouseEvent *e = new QMouseEvent(QEvent::MouseButtonPress, item->mapFromItem(parentItem, startPoint), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
qDebug() << "press " << e;
qApp->postEvent(item, e);
timer->start();
}
void xx::mouseMove()
{
timerIteration++;
QMouseEvent *e;
if(timerIteration < nbIteration) {
int x = startPoint.x() + deltaPoint->x() * timerIteration / nbIteration;
int y = startPoint.y() + deltaPoint->y() * timerIteration / nbIteration;
QPointF point = QPointF(x, y);
// Send moveEvent
e = new QMouseEvent(QEvent::MouseMove, item->mapFromItem(parentItem, point), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
qDebug() << "move! " << e ;
}else {
//End reached, send end event
QPointF point = QPointF(startPoint.x() + deltaPoint->x(), startPoint.y() + deltaPoint->y());
e = new QMouseEvent(QEvent::MouseButtonRelease, item->mapFromItem(parentItem, point), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
timer->stop();
qDebug() << "end! " << e ;
}
qApp->postEvent(item, e);
}
So... It is not working. What happens? From my tests:
-If I remove the nested child part, and give directly the interactive (to drag) QML Component (here the nested ListView), the result is good. But this is not a good solution for me, since I would have exactly to know which component should react. However, this seems to override the "interactive: false" of a component, which is a bad idea when the purpose is... to test the component.
-The mouse area receives all events. The mouseX property is updated. This is an issue. With non simulated event, MouseArea should receive the press event, some move event, then events should captured by the ListView / Flickable. Even worst, even if positions are way different (400px) between mousePress and mouseRelease events, Qt detects a MouseClicked event and triggers it on QML side...
So, not sure where to go from there. I could do some crappy child detection (checks the type of the nested child, and only accepts the good one), but I'm not very happy with it. Any idea?
Ok, so I went a bit deeper in QT code, and I understood my mistake:
To use sendEvent correctly, you have to pass them to the root view (on my project it is a QQuickView, but it can also be a QWindow). If you do not send the event on the root, it won't filter the mouseEvent as expected (through childMouseEventFilter() method)
I also discover than the MouseRelease event needs a NoButton parameter. So my code works now and looks like this:
//Set mouse timer
mouseTimer = new QTimer(this);
mouseTimer->setInterval(m_movementDuration / m_nbIteration);
mouseTimer->setSingleShot(false);
connect(mouseTimer, SIGNAL(timeout()), this, SLOT(mouseMove()));
}
void xx::pressAndDrag(QObject *object, const int x, const int y, const int dx, const int dy)
{
m_pressAndDragCompleted = false;
//Reset timer iteration
m_timerIteration = 0;
//Keep all coordinates
startPoint = ((QQuickItem *) object)->mapToScene(QPointF(x, y));
deltaPoint = new QPointF(dx, dy);
QMouseEvent *e = new QMouseEvent(QEvent::MouseButtonPress, startPoint, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
qApp->postEvent(parent(), e);
//Start timer
mouseTimer->start();
}
void xx::mouseMove()
{
m_timerIteration++;
if (m_timerIteration < m_nbIteration + 2) {
//Move mouse
int x = startPoint.x() + deltaPoint->x() * m_timerIteration / m_nbIteration;
int y = startPoint.y() + deltaPoint->y() * m_timerIteration / m_nbIteration;
QMouseEvent *e = new QMouseEvent(QEvent::MouseMove, QPointF(x, y), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier );
qApp->postEvent(parent(), e);
// Let some time to not trigger a flick
} else if (m_timerIteration - m_nbIteration >= 400 / mouseTimer->interval()) {
//End movement
QPointF point = QPointF(startPoint.x() + deltaPoint->x(), startPoint.y() + deltaPoint->y());
QMouseEvent *e = new QMouseEvent(QEvent::MouseButtonRelease, point, Qt::NoButton, Qt::NoButton, Qt::NoModifier );
qApp->postEvent(parent(), e);
//Stop timer
mouseTimer->stop();
m_pressAndDragCompleted = true;
}
}
If it can help. :)

How to get info from clicked QGraphicsItem?

The idea is that the user clicks on a shape and the information of the shape is shown on a table. This works well if the user selects the shape (drag the mouse over the shape). I'm trying to modify this code to do that action, but not lucky. This is what I'm doing for the select mode:
I have a a call in the:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
When the mouse is released I will update the data:
//enter the mode for selecting
if(theMode == SelectObject){
if (this->items().isDetached()){
//we check if the object is selected
if (this->selectedItems().isEmpty()){
qDebug() << "not selected";
isSelected = false;
return;
}
//we get a list of the shapes
QList<QGraphicsItem*> stackOfShapes = this->items();
//we get index of selected shape
QGraphicsItem *selected = this->selectedItems().first();
int indexOfShape = stackOfShapes.indexOf(selected);
//we see which shape is (For a Rectangle)
switch (selected->type()) {
case 346:
{
updateDataOfRect();
}
break;
}
}
The problem is that:
//we get index of selected shape
QGraphicsItem *selected = this->selectedItems().first();
How to do this when the shape is clicked not selected?
I tried to modify the subclass of the shape in the mousePressEvent :
if (event->button() == Qt::MouseButton::LeftButton) {
this->setSelected(true);
}
Can any one help to find a solution?
Thanks.
QList<QGraphicsItem *> QGraphicsView::items(const QPoint &pos) const
Returns a list of all the items at the position pos in the view. The
items are listed in descending stacking order (i.e., the first item in
the list is the uppermost item, and the last item is the lowermost
item). pos is in viewport coordinates.
Example use by overloading QGraphicsView::mousePressEvent():
void CustomView::mousePressEvent(QMouseEvent *event) {
qDebug() << "There are" << items(event->pos()).size()
<< "items at position" << mapToScene(event->pos());
}

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();
}

Resources