Auto-scrolling on dragging an item in QGraphicsScene
-
I am playing around with the Qt
"diagramscene"
example intending to develop something similar.When I have added an item (derives from QGraphicsItem) and try to drag it around the scene, one can drag it completely off of the scene, i.e. to a negative position. I was able to successfully implement auto-scrolling so that if I drag it further than the visible scrollable area, it will adjust the viewport -- until I reach one of the edges. After that, instead of stopping, I can continue to drag the item until it is completely out of view, and it is impossible to get it back!
Here is what I have so far, editing the code in the
DiagramScene::mouseMoveEvent()
:void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { // *** ADDITION: // *** prevent recursion when calling `ensureVisible()` later on: static bool IN_EVENT = false; if (!IN_EVENT) { // *** back to original code: if (myMode == InsertLine && line != nullptr) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { QGraphicsScene::mouseMoveEvent(mouseEvent); // *** added by me: -------------------------------------- QGraphicsItem *pItem = nullptr; if (!selectedItems().isEmpty()) { pItem = selectedItems().first(); QList<QGraphicsView*> viewsList = views(); if (pItem && !viewsList.isEmpty()) { QGraphicsView *pView = viewsList.at(0); IN_EVENT = true; pView->ensureVisible(pItem, 2, 2); IN_EVENT = false; } } // *** end of added code -------------------------------------- } } }
The documentation states that "If the specified rect cannot be reached, the contents are scrolled to the nearest valid position." However, when I try to adjust the item's position within the mouse event, nothing happens, and I can still drag it off of the scene..
-
I suppose I should be more specific about the problem, which doesn't necessarily have anything to do with scrolling the view.
What I really need to know is how to keep the graphics item from moving too far out of the scene in any direction. Using the
diagramscene
example, what do I need to do to ensure that the following constraints are satisfied:-
When inserting an item by clicking on the scene, if the mouse position is less than the absolute value of the item's center minus the x-coordinate of the item's
boundingRect()
(orsceneBoundingRect()
?) relative to the scene (i.e. the object would be clipped on the left side), it should adjust the new item so that it is totally visible; -
When dragging a selected item too close to the left or right scene edge, it should stop moving in the
x
direction, but possibly move up or down depending on the mouse movement; -
Same thing applies if dragging the object too close to the top or bottom of the scene.
Of course, this might make drag & drop more complicated, but I imagine that dragging the item out of the scene and into a different place which accepts drag & drop could still be accomplished by cut & paste.
Thanks in advance for any ideas!
-
-
In the meantime, I found a solution.
In
diagramscene.h
I added these lines:#define DIAGRAMSCENE_BORDER_WIDTH 10 // ... in the "private" section, an additional member function: void adjustDragPositions(QGraphicsView *pv, qreal border = DIAGRAMSCENE_BORDER_WIDTH);
In
diagramscene.cpp
I changed themouseMoveEvent()
implementation to this:void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { static bool IN_EVENT = false; // Find the current view: QGraphicsView* pv = nullptr; if (mouseEvent->widget()) pv = qobject_cast<QGraphicsView *>(mouseEvent->widget()->parentWidget()); if (!IN_EVENT) { if (myMode == InsertLine && line != nullptr) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { //-------------------------------------- if (pv) { IN_EVENT = true; QGraphicsScene::mouseMoveEvent(mouseEvent); adjustDragPositions(pv); IN_EVENT = false; } //-------------------------------------- } } }
The implementation of
adjustDragPositions()
will adjust the whole group of selected items, if necessary:void DiagramScene::adjustDragPositions(QGraphicsView *pv, qreal border) { Q_ASSERT_X(pv, __func__, "Pointer to view was NULL."); if (border < DIAGRAMSCENE_BORDER_WIDTH) border = DIAGRAMSCENE_BORDER_WIDTH; // Get the selected items: QList<QGraphicsItem*> selected = selectedItems(); if (selected.isEmpty()) return; QGraphicsItemGroup * gp = createItemGroup(selected); if (gp) { QRectF br = gp->boundingRect(); qreal diffX = 0; qreal diffY = 0; // check left border: if (br.x() < border) { diffX = border - br.x(); } // check right border: if (br.x()+br.width() > this->width() - border) { Q_ASSERT_X(diffX == 0, __func__, "diffX was already set for left border adjustment."); diffX = -((br.x() + br.width()) - (width() - border)); } // check top border: if (br.y() < border) { diffY = border - br.y(); } // check bottom border: if (br.y()+br.height() > this->height() - border) { Q_ASSERT_X(diffY == 0, __func__, "diffY was already set for top border adjustment."); diffY = -((br.y() + br.height()) - (height() - border)); } if (diffX == 0.0 && diffY == 0.0) { pv->ensureVisible(gp, border, border); } else { // move the group: gp->moveBy(diffX, diffY); } destroyItemGroup(gp); } }
Presently, when I am dragging an item or a selected group across the scene up to one of the borders, it sometimes leaves the edge in the direction of mouse motion partially clipped, but if I move it again it immediately respects the border. I don't know why, but at least I can live with this quirk for now.
-
-
In the meantime, I found a solution.
In
diagramscene.h
I added these lines:#define DIAGRAMSCENE_BORDER_WIDTH 10 // ... in the "private" section, an additional member function: void adjustDragPositions(QGraphicsView *pv, qreal border = DIAGRAMSCENE_BORDER_WIDTH);
In
diagramscene.cpp
I changed themouseMoveEvent()
implementation to this:void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { static bool IN_EVENT = false; // Find the current view: QGraphicsView* pv = nullptr; if (mouseEvent->widget()) pv = qobject_cast<QGraphicsView *>(mouseEvent->widget()->parentWidget()); if (!IN_EVENT) { if (myMode == InsertLine && line != nullptr) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { //-------------------------------------- if (pv) { IN_EVENT = true; QGraphicsScene::mouseMoveEvent(mouseEvent); adjustDragPositions(pv); IN_EVENT = false; } //-------------------------------------- } } }
The implementation of
adjustDragPositions()
will adjust the whole group of selected items, if necessary:void DiagramScene::adjustDragPositions(QGraphicsView *pv, qreal border) { Q_ASSERT_X(pv, __func__, "Pointer to view was NULL."); if (border < DIAGRAMSCENE_BORDER_WIDTH) border = DIAGRAMSCENE_BORDER_WIDTH; // Get the selected items: QList<QGraphicsItem*> selected = selectedItems(); if (selected.isEmpty()) return; QGraphicsItemGroup * gp = createItemGroup(selected); if (gp) { QRectF br = gp->boundingRect(); qreal diffX = 0; qreal diffY = 0; // check left border: if (br.x() < border) { diffX = border - br.x(); } // check right border: if (br.x()+br.width() > this->width() - border) { Q_ASSERT_X(diffX == 0, __func__, "diffX was already set for left border adjustment."); diffX = -((br.x() + br.width()) - (width() - border)); } // check top border: if (br.y() < border) { diffY = border - br.y(); } // check bottom border: if (br.y()+br.height() > this->height() - border) { Q_ASSERT_X(diffY == 0, __func__, "diffY was already set for top border adjustment."); diffY = -((br.y() + br.height()) - (height() - border)); } if (diffX == 0.0 && diffY == 0.0) { pv->ensureVisible(gp, border, border); } else { // move the group: gp->moveBy(diffX, diffY); } destroyItemGroup(gp); } }
Presently, when I am dragging an item or a selected group across the scene up to one of the borders, it sometimes leaves the edge in the direction of mouse motion partially clipped, but if I move it again it immediately respects the border. I don't know why, but at least I can live with this quirk for now.
@Robert-Hairgrove It seems that the scroll bars were sometimes lagging behind after moving the objects; the objects were not really clipped, but the view was not completely scrolled which made it look like they were clipped.
Here is my current changed version of
diagramscene.h
anddiagramscene.cpp
including additional headers which needed#include
directives:diagramscene.h:
/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #ifndef DIAGRAMSCENE_H #define DIAGRAMSCENE_H #include "diagramitem.h" #include "diagramtextitem.h" #include <QGraphicsScene> QT_BEGIN_NAMESPACE class QGraphicsSceneMouseEvent; class QMenu; class QPointF; class QGraphicsLineItem; class QFont; class QGraphicsTextItem; class QColor; QT_END_NAMESPACE #define DIAGRAMSCENE_BORDER_WIDTH 10 //! [0] class DiagramScene : public QGraphicsScene { Q_OBJECT public: enum Mode { InsertItem, InsertLine, InsertText, MoveItem }; explicit DiagramScene(QMenu *itemMenu, QObject *parent = nullptr); QFont font() const { return myFont; } QColor textColor() const { return myTextColor; } QColor itemColor() const { return myItemColor; } QColor lineColor() const { return myLineColor; } void setLineColor(const QColor &color); void setTextColor(const QColor &color); void setItemColor(const QColor &color); void setFont(const QFont &font); public slots: void setMode(Mode mode); void setItemType(DiagramItem::DiagramType type); void editorLostFocus(DiagramTextItem *item); signals: void itemInserted(DiagramItem *item); void textInserted(QGraphicsTextItem *item); void itemSelected(QGraphicsItem *item); protected: void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override; private: bool isItemChange(int type) const; // Added:----- void adjustDragPositions(QGraphicsView *pv, qreal border = DIAGRAMSCENE_BORDER_WIDTH); //------------ DiagramItem::DiagramType myItemType; QMenu *myItemMenu; Mode myMode; bool leftButtonDown; QPointF startPoint; QGraphicsLineItem *line; QFont myFont; DiagramTextItem *textItem; QColor myTextColor; QColor myItemColor; QColor myLineColor; }; //! [0] #endif // DIAGRAMSCENE_H
diagramscene.cpp:
/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** BSD License Usage ** Alternatively, you may use this file under the terms of the BSD license ** as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "diagramscene.h" #include "arrow.h" #include <QApplication> #include <QGraphicsSceneMouseEvent> #include <QGraphicsItem> #include <QGraphicsView> #include <QScrollBar> #include <QTextCursor> //! [0] DiagramScene::DiagramScene(QMenu *itemMenu, QObject *parent) : QGraphicsScene(parent) { myItemMenu = itemMenu; myMode = MoveItem; myItemType = DiagramItem::Step; line = nullptr; textItem = nullptr; myItemColor = Qt::white; myTextColor = Qt::black; myLineColor = Qt::black; } //! [0] //! [1] void DiagramScene::setLineColor(const QColor &color) { myLineColor = color; if (isItemChange(Arrow::Type)) { Arrow *item = qgraphicsitem_cast<Arrow *>(selectedItems().first()); item->setColor(myLineColor); update(); } } //! [1] //! [2] void DiagramScene::setTextColor(const QColor &color) { myTextColor = color; if (isItemChange(DiagramTextItem::Type)) { DiagramTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); item->setDefaultTextColor(myTextColor); } } //! [2] //! [3] void DiagramScene::setItemColor(const QColor &color) { myItemColor = color; if (isItemChange(DiagramItem::Type)) { DiagramItem *item = qgraphicsitem_cast<DiagramItem *>(selectedItems().first()); item->setBrush(myItemColor); } } //! [3] //! [4] void DiagramScene::setFont(const QFont &font) { myFont = font; if (isItemChange(DiagramTextItem::Type)) { QGraphicsTextItem *item = qgraphicsitem_cast<DiagramTextItem *>(selectedItems().first()); //At this point the selection can change so the first selected item might not be a DiagramTextItem if (item) item->setFont(myFont); } } //! [4] void DiagramScene::setMode(Mode mode) { myMode = mode; } void DiagramScene::setItemType(DiagramItem::DiagramType type) { myItemType = type; } //! [5] void DiagramScene::editorLostFocus(DiagramTextItem *item) { QTextCursor cursor = item->textCursor(); cursor.clearSelection(); item->setTextCursor(cursor); if (item->toPlainText().isEmpty()) { removeItem(item); item->deleteLater(); } } //! [5] //! [6] void DiagramScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (mouseEvent->button() != Qt::LeftButton) return; DiagramItem *item; switch (myMode) { case InsertItem: item = new DiagramItem(myItemType, myItemMenu); item->setBrush(myItemColor); addItem(item); item->setPos(mouseEvent->scenePos()); emit itemInserted(item); break; //! [6] //! [7] case InsertLine: line = new QGraphicsLineItem(QLineF(mouseEvent->scenePos(), mouseEvent->scenePos())); line->setPen(QPen(myLineColor, 2)); addItem(line); break; //! [7] //! [8] case InsertText: textItem = new DiagramTextItem(); textItem->setFont(myFont); textItem->setTextInteractionFlags(Qt::TextEditorInteraction); textItem->setZValue(1000.0); connect(textItem, &DiagramTextItem::lostFocus, this, &DiagramScene::editorLostFocus); connect(textItem, &DiagramTextItem::selectedChange, this, &DiagramScene::itemSelected); addItem(textItem); textItem->setDefaultTextColor(myTextColor); textItem->setPos(mouseEvent->scenePos()); emit textInserted(textItem); //! [8] //! [9] default: ; } QGraphicsScene::mousePressEvent(mouseEvent); } //! [9] //! [10] void DiagramScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) { static bool IN_EVENT = false; // Find the current view: QGraphicsView* pv = nullptr; if (mouseEvent->widget()) pv = qobject_cast<QGraphicsView *>(mouseEvent->widget()->parentWidget()); if (!IN_EVENT) { if (myMode == InsertLine && line != nullptr) { QLineF newLine(line->line().p1(), mouseEvent->scenePos()); line->setLine(newLine); } else if (myMode == MoveItem) { //-------------------------------------- if (pv) { IN_EVENT = true; QGraphicsScene::mouseMoveEvent(mouseEvent); adjustDragPositions(pv); IN_EVENT = false; } //-------------------------------------- } } } //! [10] //! [11] void DiagramScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) { if (line != nullptr && myMode == InsertLine) { QList<QGraphicsItem *> startItems = items(line->line().p1()); if (startItems.count() && startItems.first() == line) startItems.removeFirst(); QList<QGraphicsItem *> endItems = items(line->line().p2()); if (endItems.count() && endItems.first() == line) endItems.removeFirst(); removeItem(line); delete line; //! [11] //! [12] if (startItems.count() > 0 && endItems.count() > 0 && startItems.first()->type() == DiagramItem::Type && endItems.first()->type() == DiagramItem::Type && startItems.first() != endItems.first()) { DiagramItem *startItem = qgraphicsitem_cast<DiagramItem *>(startItems.first()); DiagramItem *endItem = qgraphicsitem_cast<DiagramItem *>(endItems.first()); Arrow *arrow = new Arrow(startItem, endItem); arrow->setColor(myLineColor); startItem->addArrow(arrow); endItem->addArrow(arrow); arrow->setZValue(-1000.0); addItem(arrow); arrow->updatePosition(); } } //! [12] //! [13] line = nullptr; QGraphicsScene::mouseReleaseEvent(mouseEvent); } //! [13] //! [14] bool DiagramScene::isItemChange(int type) const { const QList<QGraphicsItem *> items = selectedItems(); const auto cb = [type](const QGraphicsItem *item) { return item->type() == type; }; return std::find_if(items.begin(), items.end(), cb) != items.end(); } //! [14] void DiagramScene::adjustDragPositions(QGraphicsView *pv, qreal border) { Q_ASSERT_X(pv, __func__, "Pointer to view was NULL."); if (border < DIAGRAMSCENE_BORDER_WIDTH) border = DIAGRAMSCENE_BORDER_WIDTH; // Get the selected items: QList<QGraphicsItem*> selected = selectedItems(); if (selected.isEmpty()) return; QGraphicsItemGroup * gp = createItemGroup(selected); if (gp) { QRectF br = gp->boundingRect(); qreal diffX = 0; qreal diffY = 0; // check left border: if (br.x() < border) { diffX = border - br.x(); } // check right border: if (br.x()+br.width() > this->width() - border) { Q_ASSERT_X(diffX == 0, __func__, "diffX was already set for left border " "adjustment."); diffX = -((br.x() + br.width()) - (width() - border)); } // check top border: if (br.y() < border) { diffY = border - br.y(); } // check bottom border: if (br.y()+br.height() > this->height() - border) { Q_ASSERT_X(diffY == 0, __func__, "diffY was already set for top border " "adjustment."); diffY = -((br.y() + br.height()) - (height() - border)); } if (diffX == 0.0 && diffY == 0.0) { pv->ensureVisible(gp, border, border); } else { // move the group: gp->moveBy(diffX, diffY); QScrollBar *sb = nullptr; if (diffX) { // Scroll all the way to the left or right // depending on whether diffX is positive or negative: sb = pv->horizontalScrollBar(); if (sb) { if (diffX > 0) { // we adjusted the left: sb->setValue(sb->minimum()); } else { // we adjusted the right: sb->setValue(sb->maximum()); } } } if (diffY) { // Scroll all the way to the top or bottom // depending on whether diffX is positive or negative: sb = pv->verticalScrollBar(); if (sb) { if (diffY > 0) { // we adjusted the top: sb->setValue(sb->minimum()); } else { // we adjusted the bottom: sb->setValue(sb->maximum()); } } } QApplication::processEvents(); } destroyItemGroup(gp); } }
You will notice the extra code in
DiagramScene::adjustDragPosition()
which adjusts the scroll factor if needed, then callsQApplication::processEvents()
to ensure that everything runs smoothly.