I'm trying to move a QGraphicsRectItem after I add it to the scene. It moves, but appears with a certain offset from the mouse pointer. I think it is simply adding the mouse pointer position to its original position. I am at a loss as to how to work around this.
Here is my code:
class ucFilter : public QGraphicsItem {
std::shared_ptr<QGraphicsRectItem> m_rect;
std::shared_ptr<QGraphicsTextItem> m_text;
std::shared_ptr<QString> m_name;
std::shared_ptr<QPointF> m_pos;
QGraphicsItem* selectedItem;
bool m_mouseGrabbed;
public:
static const int default_x = 80, default_y=40;
ucFilter::ucFilter(QString &name, QPointF &pos){
m_name = shared_ptr<QString>(new QString(name));
m_pos = shared_ptr<QPointF>(new QPointF(pos));
m_rect = shared_ptr<QGraphicsRectItem>( new QGraphicsRectItem(pos.x()-default_x, pos.y()-default_y, 2*default_x, 2*default_y ));
m_text = shared_ptr<QGraphicsTextItem>( new QGraphicsTextItem(name));
m_text->setPos(pos.x() - m_text->boundingRect().width()/2, pos.y()- 30);
selectedItem = NULL;
m_mouseGrabbed = false;
}
QGraphicsRectItem* getRect() { return m_rect.get(); }
QGraphicsTextItem* getText() { return m_text.get(); }
QString* getName() { return m_name.get(); }
QPointF* getPos() { return m_pos.get(); }
void setPos(QPointF newPos) { m_pos->setX(newPos.x()); m_pos->setY(newPos.y()); }
QRectF ucFilter::boundingRect() const
{
return m_rect->boundingRect();
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(widget);
//QBrush brush(
if (!m_mouseGrabbed){ grabMouse(); m_mouseGrabbed = true; }
}
void mousePressEvent(QGraphicsSceneMouseEvent *event){
selectedItem = this;
QGraphicsItem::mousePressEvent(event);
}
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event){
selectedItem = NULL;
QGraphicsItem::mouseReleaseEvent(event);
}
void mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if(NULL != selectedItem){
m_text->setPos(event->pos());
m_rect->setPos(event->pos());
}
QGraphicsItem::mouseMoveEvent(event);
}
};
the ucFilter object is created in the scene dropEvent:
void cGraphicsScene::dropEvent(QGraphicsSceneDragDropEvent * event){
QTreeView* source = static_cast<QTreeView*>(event->source());
string name = event->mimeData()->text().toUtf8().constData();
if(0 == name.length()){
event->acceptProposedAction();
return ; // nothing to do anymore
}
QPointF pos = event->scenePos ();
shared_ptr<ucFilter> newFilter = shared_ptr<ucFilter>(new ucFilter(event->mimeData()->text(),event->scenePos ()));
m_filters.push_back(newFilter);
this->addItem(newFilter->getRect());
this->addItem(newFilter->getText());
this->addItem(newFilter.get()); // also add the item to grab mouse events
event->acceptProposedAction();
}
Where could the problem be ?
Here a screenshot of what I'm actually seeing :
I would like the rectangle to be drawn where the mouse is..
You have several issues:
The use of shared pointers to hold everything is completely unwarranted. The scene acts as a container for items - just like QObject is a container for objects.
ucFilter doesn't have children. It holds pointers to other items, but that is unnecessary. The base item can be a rectangle itself, and it can have the text as a child. That way you don't need to handle positioning in a special fashion.
ucFilter can be movable. Don't reimplement that functionality yourself.
When you pass things by reference, pass them as const references unless you are intending to pass the modified value out. If you wish to change the value inside of the body of the function, you can pass it by value instead.
The mouse is already grabbed when you are dragging the item.
Let's start with the ucFilter item. It is really simple and does everything you need. Note that m_text is held by value, and is made a child of the rectangle parent.
// https://github.com/KubaO/stackoverflown/tree/master/questions/graphics-item-drop-32574576
#include <QtWidgets>
class ucFilter : public QGraphicsRectItem {
QGraphicsTextItem m_text;
public:
ucFilter(const QString &name, const QPointF &pos, QGraphicsItem * parent = 0) :
QGraphicsRectItem(parent),
m_text(this)
{
static const QRect defaultRect(0, 0, 160, 80);
setPos(pos);
setRect(QRect(-defaultRect.topLeft()/2, defaultRect.size()));
setFlags(QGraphicsItem::ItemIsMovable);
setName(name);
}
void setName(const QString & text) {
m_text.setPlainText(text);
m_text.setPos(-m_text.boundingRect().width()/2, -30);
}
QString name() const {
return m_text.toPlainText();
}
};
Since we're dropping from a convenience widget (a QListWidget), we need to decode the text from a application/x-qabstractitemmodeldatalist mime type:
const char * kMimeType = "application/x-qabstractitemmodeldatalist";
QVariant decode(const QMimeData* data, Qt::ItemDataRole role = Qt::DisplayRole) {
auto buf = data->data(kMimeType);
QDataStream stream(&buf, QIODevice::ReadOnly);
while (!stream.atEnd()) {
int row, col;
QMap<int, QVariant> map;
stream >> row >> col >> map;
if (map.contains(role)) return map[role];
}
return QVariant();
}
The scene creates an item as soon as a drag enters it, and moves the item during the drag.
Ownership of the items remains with the scene: we don't need to delete any items unless we want to explicitly remove them from the scene. The m_dragItem is used to refer to the currently dragged item simply to move it and add it to m_filters upon completion of the drop. It the drag leaves the scene (or is aborted), the item is simply deleted from the scene.
class cGraphicsScene : public QGraphicsScene {
QList<ucFilter*> m_filters;
ucFilter* m_dragItem;
public:
cGraphicsScene(QObject * parent = 0) : QGraphicsScene(parent), m_dragItem(nullptr) {}
void dragEnterEvent(QGraphicsSceneDragDropEvent * event) Q_DECL_OVERRIDE {
if (!event->mimeData()->hasFormat(kMimeType)) return;
auto name = decode(event->mimeData()).toString();
if (name.isEmpty()) return;
QScopedPointer<ucFilter> filter(new ucFilter(name, event->scenePos()));
addItem(m_dragItem = filter.take());
event->acceptProposedAction();
}
void dragMoveEvent(QGraphicsSceneDragDropEvent * event) Q_DECL_OVERRIDE {
if (!m_dragItem) return;
m_dragItem->setPos(event->scenePos());
event->acceptProposedAction();
}
void dropEvent(QGraphicsSceneDragDropEvent * event) Q_DECL_OVERRIDE {
if (!m_dragItem) return;
m_dragItem->setPos(event->scenePos());
m_filters << m_dragItem;
event->acceptProposedAction();
}
void dragLeaveEvent(QGraphicsSceneDragDropEvent * event) Q_DECL_OVERRIDE {
delete m_dragItem;
m_dragItem = nullptr;
event->acceptProposedAction();
}
};
The test harness is very simple: our scene, a view displaying it, and a list with two items that you can drag onto the scene.
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QWidget w;
cGraphicsScene scene;
QGraphicsView view(&scene);
QListWidget list;
QHBoxLayout l(&w);
l.addWidget(&view);
l.addWidget(&list);
list.setFixedWidth(120);
list.addItem("Item1");
list.addItem("Item2");
list.setDragDropMode(QAbstractItemView::DragOnly);
view.setAcceptDrops(true);
w.resize(500, 300);
w.show();
return app.exec();
}
You can drag items from the list on the right to the scene on the left. You can also move the items within the scene, to relocate them.
Related
I'm working on a TreeViewNode class derived from QObject and QGraphicsItem, responsible for creating the individual nodes of the tree view (which displays a family tree) and adding them to the scene. I made sure to include the setFlag(QGraphicsItem::ItemIsSelectable); method in the class constructor and I overloaded mousePressEvent like so:
void TreeViewNode::mousePressEvent(QGraphicsSceneMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
qDebug() << "Mouse pressed on node: " << m_node->getPatient()->get_Name().c_str();
emit clicked(this);
}
QGraphicsItem::mousePressEvent(event);
}
The clicked signal is defined in the header file as void clicked(TreeViewNode* node);
Then I have a updateSelectedPatient slot in MainWindow
void MainWindow::updateSelectedPatient(TreeViewNode* node) {
selected->setSelectedPatient(node->getNode()->getPatient());
}
and the corresponding connect statement in the MainWindow constructor
connect(treeView, &TreeViewNode::clicked, this, &MainWindow::updateSelectedPatient);
When I execute I see the nodes that have been added to the scene in the view but when I click on them nothing happens (I know through debugging and also because the setSelectedPatient() method updates a widget that shows the patient information of the currently selected patient); it seems like mousePressEvent() is not being called at all.. any help would be really appreciated, thank you for your time!
edit:
//TreeViewNode.hpp
class TreeViewNode : public QObject, public QGraphicsItem {
Q_OBJECT
Q_INTERFACES(QGraphicsItem)
private:
node* m_node;
Family_tree* m_family;
QGraphicsScene* m_scene;
static std::set<node*> addedNodes;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent* event) override;
public:
TreeViewNode(node* node, Family_tree* family, QGraphicsScene* scene, QGraphicsItem* parent = nullptr);
TreeViewNode* getTreeViewNode(node* n);
node* getNode() const;
static void clearAddedNodes();
void updateNode(Patient& patient);
QRectF boundingRect() const override;
void drawBranches(QPainter* painter);
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
signals:
void clicked(TreeViewNode* node);
};
//TreeViewNode.cpp
TreeViewNode::TreeViewNode(node* node, Family_tree* family, QGraphicsScene* scene, QGraphicsItem* parent) : QGraphicsItem(parent), m_node(node), m_family(family), m_scene(scene) {
if (m_node == m_family->get_root()) {m_scene->addItem(this);}
addedNodes.insert(m_node);
setFlag(QGraphicsItem::ItemIsSelectable);
qDebug() << "Scene item count: " << m_scene->items().count();
if (m_node->getFather()) {
auto father = m_node->getFather();
if (addedNodes.count(father) > 0) {return;}
TreeViewNode* fatherNode = new TreeViewNode(m_node->getFather(), family, m_scene, this);
fatherNode->setPos(-50, -100);
}
if (m_node->getMother()) {
auto mother = m_node->getMother();
if (addedNodes.count(mother) > 0) {return;}
TreeViewNode* motherNode = new TreeViewNode(m_node->getMother(), family, m_scene, this);
motherNode->setPos(50, -100);
}
if (m_node->getSpouse()) {
auto spouse = m_node->getSpouse();
if (addedNodes.count(spouse) > 0) {return;}
TreeViewNode* spouseNode = new TreeViewNode(m_node->getSpouse(), family, m_scene, this);
spouseNode->setPos(100, 0);
}
int childCount = 0;
for (auto child : m_node->getChildren()) {
if (addedNodes.count(child) > 0) {return;}
TreeViewNode* childNode = new TreeViewNode(child, family, m_scene, this);
childCount++;
double xPos, yPos;
xPos = pos().x() + 50 + 100 * (childCount - (m_node->getChildren().size() + 1) / 2.0);
yPos = pos().y() + 100;
childNode->setPos(xPos, yPos);
}
}
You could use a normal event filter and check if is it a mouse press, release or move event then cast it to a mouse event and then just do whatever you want with it.
Example:
bool YourWidgetClass::eventFilter(QObject* object, QEvent* event)
{
if (object == YourInstance)
{
switch (event->type())
{
case QEvent::GraphicsSceneMousePress:
{
QMouseEvent * mouseEvent = static_cast<QMouseEvent*>(event);
switch (mouseEvent->button())
{
case Qt::LeftButton:
// Handle left button pressed here
return true;
case Qt::RightButton:
// Handle right button pressed here
return true;
default:
break;
}
break;
}
}
}
Don't forget to install the event filter on your class
YourClassCtor->installEventFilter(this)
I don't see from your question that you need it for anything special. the signal &QGraphicsScene::selectionChanged is not enough for you? Is the item set to interactive?
So simply put:
declare a slot:
public slots:
void selectionChanged();
define the slot:
void MainWindow::selectionChanged()
{
qDebug() << ...... ;
}
and connect:
connect(scene,&QGraphicsScene::selectionChanged,this,&MainWindow::selectionChanged);
I have an application where I can drag an item to a QGraphicsScene and create a new object depending on the item text, but how can I change the data being displayed while moving around the item?
for example, instead of a text, I want to show an icon:
I have a list with some itens:
OptionList::OptionList(QWidget *parent) : QListWidget(parent)
{
this->setDragEnabled(true);
this->setDropIndicatorShown(true);
this->setSelectionMode(QAbstractItemView::SingleSelection);
this->setDefaultDropAction(Qt::CopyAction);
this->setViewMode(QListView::ListMode);
for(const QString &color : {"Blue", "Red", "Green", "Yellow"})
{
OptionItem *item = new OptionItem;
item->setText(color);
item->setFlags(Qt::ItemIsEnabled| Qt::ItemIsSelectable| Qt::ItemIsDragEnabled);
addItem(item);
}
}
I drop the itens into the scene to create a new object:
MyScene::MyScene()
{
setBackgroundBrush(Qt::lightGray);
}
void MyScene::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
if(event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void MyScene::dragMoveEvent(QGraphicsSceneDragDropEvent *event)
{
if(event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
event->setAccepted(true);
}
void MyScene::dropEvent(QGraphicsSceneDragDropEvent *event)
{
QByteArray encoded = event->mimeData()->data("application/x-qabstractitemmodeldatalist");
QDataStream stream(&encoded, QIODevice::ReadOnly);
QStringList colors;
while (!stream.atEnd())
{
int row, col;
QMap<int, QVariant> roleDataMap;
stream >> row >> col >> roleDataMap;
colors << roleDataMap[Qt::DisplayRole].toString();
}
QPointF posView = event->scenePos() ;
for(const QString & color: colors)
{
Block *newBlock = new Block(color);
newBlock->setPos(posView);
addItem(newBlock);
}
}
Then, I created OptionItem class, derived from QListWidgetItem, and reimplemented the mousePressEvent, mouseMoveEvent and mouseReleaseEvent
OptionItem::OptionItem()
{
}
void OptionItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
event->setAccepted(true);
}
void OptionItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
QDrag *drag = new QDrag(event->widget());
QMimeData *mime = new QMimeData;
QImage image(":/images/MyIcon_icon.png");
mime->setImageData(image);
drag->setMimeData(mime);
drag->setPixmap(QPixmap::fromImage(image));
drag->setHotSpot(QPoint(15, 30));
drag->exec();
event->setAccepted(true);
}
void OptionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
event->setAccepted(true);
}
I tried to follow the drag drop robot example in the Qt Creator but it isn't the same thing
It seems that the image appears very quickly when I start dragging an item
Is there a way to show the icon while dragging the item through the whole operation?
The classes that inherit from QAbstractItemView support the default drag so they already have implemented methods, instead the example you point out shows how to implement this functionality for some class that does not have it, the task in your case is simple, you must overwrite the method startDrag of QListWidget.
optionilist.h
#ifndef OPTIONLIST_H
#define OPTIONLIST_H
#include <QListWidget>
class OptionList: public QListWidget{
public:
OptionList(QWidget* parent=nullptr);
protected:
void startDrag(Qt::DropActions supportedActions);
};
#endif // OPTIONLIST_H
optionlist.cpp
#include "optionlist.h"
#include <QDrag>
OptionList::OptionList(QWidget *parent): QListWidget(parent){
setDragEnabled(true);
setDropIndicatorShown(true);
setSelectionMode(QAbstractItemView::SingleSelection);
setDefaultDropAction(Qt::CopyAction);
setViewMode(QListView::ListMode);
for(const QString &color : {"Blue", "Red", "Green", "Yellow"}){
QListWidgetItem *blue = new QListWidgetItem;
blue->setText(color);
blue->setFlags(Qt::ItemIsEnabled| Qt::ItemIsSelectable| Qt::ItemIsDragEnabled);
addItem(blue);
}
}
void OptionList::startDrag(Qt::DropActions supportedActions){
if(supportedActions & Qt::CopyAction){
QList<QListWidgetItem *> m_items = selectedItems();
if(m_items.isEmpty())
return;
QMimeData *data = mimeData(m_items);
QDrag *drag = new QDrag(this);
QPixmap pixmap(":/images/MyIcon_icon.png");
drag->setPixmap(pixmap);
drag->setMimeData(data);
drag->setHotSpot(pixmap.rect().center());
drag->exec(Qt::CopyAction);
}
else
QListWidget::startDrag(supportedActions);
}
The complete code can be found at the following link.
The above is correct!
Also should note that drag->setMimeData(data); needs to be called after drag->setPixmap(pixmap);.
Otherwise, during the drag moving, it will show the original mimedata type instead of showing an image/icon.
I have a GraphicsBoxItem that holds a list of GraphicsImageItem (some kind of floating buttons around the box). I create them on the fly in focusInEvent() and destroy them in focusOutEvent().
Some of the GraphicsImageItem should move with the parent when clicked, some of them should stay at the same spot when clicked, until the box gets a focusOut by clicking outside of any graphics items.
Is there a way to prevent a child QGraphicsItem from being moved with the parent?
class GraphicsBoxItem : public QObject, public QGraphicsItem
{
Q_OBJECT
private:
QColor mColor;
QVector<GraphicsImageItem*> mItemList;
public:
GraphicsBoxItem(QGraphicsItem *parent = NULL)
:QGraphicsItem(parent)
{
mColor = Qt::lightGray;
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemIsFocusable);
}
QRectF boundingRect() const { return QRectF(50, 20, 100, 60); }
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->setBrush(mColor);
painter->drawRect(boundingRect());
}
virtual void focusOutEvent(QFocusEvent * event)
{
foreach(GraphicsImageItem *item1, mItemList)
{
item1->deleteLater();
item1 = NULL;
}
mItemList.clear();
mColor = Qt::lightGray;
QGraphicsItem::focusOutEvent(event);
}
virtual void focusInEvent(QFocusEvent * event)
{
GraphicsImageItem *movableItem = new GraphicsImageItem(this);
movableItem->setPos(150, 20);
mItemList.push_back(movableItem);
movableItem->installSceneEventFilter(this);
connect(movableItem, SIGNAL(SignalClicked()), this, SLOT(SlotButtonClicked()));
GraphicsImageItem *nonMovableItem = new GraphicsImageItem(this);
nonMovableItem->setPos(20, 20);
mItemList.push_back(nonMovableItem);
nonMovableItem->installSceneEventFilter(this);
connect(nonMovableItem, SIGNAL(SignalClicked()), this, SLOT(SlotButtonClicked()));
mColor = Qt::blue;
QGraphicsItem::focusInEvent(event);
}
bool sceneEventFilter(QGraphicsItem* target, QEvent* event)
{
if(event->type() == QEvent::GraphicsSceneMousePress || event->type() == QEvent::GraphicsSceneMouseDoubleClick)
{
GraphicsImageItem* item = dynamic_cast<GraphicsImageItem*>(target);
if(item)
{
item->SignalClicked();
qDebug() << "image button was clicked: Need to set the focus back to the box";
setFocus();
return true;
}
}
return QGraphicsItem::sceneEventFilter(target, event);
}
public slots:
void SlotButtonClicked()
{
setPos(pos().x() + 10, pos().y()+10);
}
};
SignalClicked() moves the GraphicsBoxItem by 10 pixels. Some of the GraphicsImageItem stay put, some move with GraphicsBoxItem:
class GraphicsImageItem : public QGraphicsSvgItem
{
Q_OBJECT
public:
GraphicsImageItem::GraphicsImageItem(QGraphicsItem *parent = NULL)
: QGraphicsSvgItem(QString(":/images/icon.svg"), parent)
{
setFlag(QGraphicsItem::ItemIsSelectable);
}
QRectF boundingRect() const { return QRectF(0, 0, 30, 30); }
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->setBrush(Qt::lightGray);
painter->drawRect(boundingRect());
renderer()->render(painter, boundingRect().adjusted(3, 3, -3, -3));
}
Q_SIGNALS:
void SignalClicked();
};
A child's position is always relative to its parent, if the parent moves, then the child's absolute position changes too.
If you want to create the illusion that this doesn't happen, you will have to manually move the child in the opposite direction, so it can remain in the same absolute position.
The other option would be to not have the items as children in the first place.
Maybe you should create an invisible item, then parent the movable item as well as the non-movable children to it, and the movable children - under the movable item. Then you simply only move the movable item - its children move, its siblings stay in the same position, as their parent doesn't move.
For the child items you don't want to move, try set this flag:
setFlag(QGraphicsItem::ItemIgnoresTransformations);
If you can subclass your child items, then you can override the itemChange() method and watch for:
ItemScenePositionHasChanged
The item's scene position has changed. This notification is sent if
the ItemSendsScenePositionChanges flag is enabled, and after the
item's scene position has changed (i.e., the position or
transformation of the item itself or the position or transformation of
any ancestor has changed). The value argument is the new scene
position (the same as scenePos()), and QGraphicsItem ignores the
return value for this notification (i.e., a read-only notification).
Then then return the child item's last scenePos() which you'll have to store as a member and update each scene position change.
Remember to set the flag:
ItemSendsGeometryChanges
in your child subclass's ctor.
I'm having trouble figuring out how to scale a custom sub classed QGraphicsObject in Qt.
I have the code working so that the mouse cursor changes when hovering over the edges object's boundingRect(Left,Right,Top,Bottom).
I have the overloaded mouse events working properly.
And I have custom methods that are trying to change the boundingRect.
But the object refuses to change size.
As a quick and simple test.
I tried to change the size of the object in the paint() method using hard coded values. And what happens is that the object does change size. But the collisions are still using the old boundingRect size.
What seems to be tripping me up is that QGraphicsObject uses a 'const' boundingRect value. And I can't figure out how to deal with that.
ResizableObject::ResizableObject( QGraphicsItem *parent ): QGraphicsObject( parent ), mResizeLeft( false ), mResizeRight( false ), mResizeBottom( false ), mResizeTop( false )
{
setAcceptHoverEvents( true );
setFlags( ItemIsMovable | ItemIsSelectable );
}
QRectF ResizableObject::boundingRect() const
{
return QRectF(0 , 0 , 100 , 100);
}
void ResizableObject::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED( option )
Q_UNUSED( widget )
painter->setRenderHint(QPainter::Antialiasing);
//Determine whether to draw a solid, or dashed, outline border
if (isSelected() )
{
//Draw a dashed outlined rectangle
painter->setPen( Qt::DashLine );
painter->drawRect( boundingRect() );
}
else painter->drawRect(0,0,20,20); //Draw a normal solid outlined rectangle
//No collisions are happening
if(scene()->collidingItems(this).isEmpty())
{
//qDebug()<< "empty";
}
//Collisions are happening
else
{
foreach(QGraphicsItem *item, collidingItems())
{
qDebug()<< item->pos();
}
}
}
The only examples I can find use Qwidget or QGraphicsRectItem. But those are slightly different and the constructors are not constants.
Can anyone tell me how to change the size of these QGraphicsObjects with the mouse?
The paint method's only job is to paint. It can't be doing anything else - that includes attempting to change the object's size.
Instead of making the item itself resizable, let's see if we can have a more generic way of resizing any item, of any class. First, we may not wish all objects to be resizable. We need a way to figure which of them are. We also need a way to tell an item to resize - a QGraphicsItem doesn't offer a way to do that. In generic programming, one pattern used to provide such functionality is a traits class. This class implements the application-specific traits that will customize the generic resize behavior to our needs.
For this simple example, we allow any QGraphicsEllipseItems to be resizable. For your application, you can implement any logic you desire - without any subclassing. You can certainly create a base class for resizable items, and expose it via the traits, if you have many custom, resizable items. Even then, the traits class can still support items of other classes.
In all cases, the items to be resized must be selectable, and we exploit the selection mechanism to hook into the scene. This could be also done without using selections.
// https://github.com/KubaO/stackoverflown/tree/master/questions/graphics-resizable-32416527
#include <QtWidgets>
class SimpleTraits {
public:
/// Determines whether an item is manually resizeable.
static bool isGraphicsItemResizeable(QGraphicsItem * item) {
return dynamic_cast<QGraphicsEllipseItem*>(item);
}
/// Gives the rectangle one can base the resize operations on for an item
static QRectF rectFor(QGraphicsItem * item) {
auto ellipse = dynamic_cast<QGraphicsEllipseItem*>(item);
if (ellipse) return ellipse->rect();
return QRectF();
}
/// Sets a new rectangle on the item
static void setRectOn(QGraphicsItem * item, const QRectF & rect) {
auto ellipse = dynamic_cast<QGraphicsEllipseItem*>(item);
if (ellipse) return ellipse->setRect(rect);
}
};
To implement a "wide rubber band" controller, we need a helper to tell what rectangle edges a given point intersects:
/// The set of edges intersecting a rectangle of given pen width
Qt::Edges edgesAt(const QPointF & p, const QRectF & r, qreal w) {
Qt::Edges edges;
auto hw = w / 2.0;
if (QRectF(r.x()-hw, r.y()-hw, w, r.height()+w).contains(p)) edges |= Qt::LeftEdge;
if (QRectF(r.x()+r.width()-hw, r.y()-hw, w, r.height()+w).contains(p)) edges |= Qt::RightEdge;
if (QRectF(r.x()-hw, r.y()-hw, r.width()+w, w).contains(p)) edges |= Qt::TopEdge;
if (QRectF(r.x()-hw, r.y()+r.height()-hw, r.width()+w, w).contains(p)) edges |= Qt::BottomEdge;
return edges;
}
Then, we have a "rubberband" resize helper item that tracks the selection on its scene. Whenever the selection contains one item, and the item is resizable, the helper makes itself a child of the item to be resized, and becomes visible. It tracks the mouse drags on the active edges of the rubber band, and resizes the parent items accordingly.
template <typename Tr>
class ResizeHelperItem : public QGraphicsObject {
QRectF m_rect;
QPen m_pen;
Qt::Edges m_edges;
void newGeometry() {
prepareGeometryChange();
auto parentRect = Tr::rectFor(parentItem());
m_rect.setTopLeft(mapFromParent(parentRect.topLeft()));
m_rect.setBottomRight(mapFromParent(parentRect.bottomRight()));
m_pen.setWidthF(std::min(m_rect.width(), m_rect.height()) * 0.1);
m_pen.setJoinStyle(Qt::MiterJoin);
}
public:
ResizeHelperItem() {
setAcceptedMouseButtons(Qt::LeftButton);
m_pen.setColor(QColor(255, 0, 0, 128));
m_pen.setStyle(Qt::SolidLine);
}
QRectF boundingRect() const Q_DECL_OVERRIDE {
auto hWidth = m_pen.widthF()/2.0;
return m_rect.adjusted(-hWidth, -hWidth, hWidth, hWidth);
}
void selectionChanged() {
if (!scene()) { setVisible(false); return; }
auto sel = scene()->selectedItems();
if (sel.isEmpty() || sel.size() > 1) { setVisible(false); return; }
auto item = sel.at(0);
if (! Tr::isGraphicsItemResizeable(item)) { setVisible(false); return; }
setParentItem(item);
newGeometry();
setVisible(true);
}
void paint(QPainter * p, const QStyleOptionGraphicsItem *, QWidget *) Q_DECL_OVERRIDE {
p->setPen(m_pen);
p->drawRect(m_rect);
}
void mousePressEvent(QGraphicsSceneMouseEvent * ev) Q_DECL_OVERRIDE {
m_edges = edgesAt(ev->pos(), m_rect, m_pen.widthF());
if (!m_edges) return;
ev->accept();
}
void mouseMoveEvent(QGraphicsSceneMouseEvent * ev) Q_DECL_OVERRIDE {
auto pos = mapToItem(parentItem(), ev->pos());
auto rect = Tr::rectFor(parentItem());
if (m_edges & Qt::LeftEdge) rect.setLeft(pos.x());
if (m_edges & Qt::TopEdge) rect.setTop(pos.y());
if (m_edges & Qt::RightEdge) rect.setRight(pos.x());
if (m_edges & Qt::BottomEdge) rect.setBottom(pos.y());
if (!!m_edges) {
Tr::setRectOn(parentItem(), rect);
newGeometry();
}
}
};
Finally, we create a simple scenario to try it all out:
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QGraphicsScene scene;
QGraphicsView view { &scene };
typedef ResizeHelperItem<SimpleTraits> HelperItem;
HelperItem helper;
QObject::connect(&scene, &QGraphicsScene::selectionChanged, &helper, &HelperItem::selectionChanged);
scene.addItem(&helper);
auto item = scene.addEllipse(0, 0, 100, 100);
item->setFlag(QGraphicsItem::ItemIsSelectable);
view.setMinimumSize(400, 400);
view.show();
return app.exec();
}
I think you need to declare QRectF variable in somewhere and modify boundingRect() to simply return that one. And override mouse event handlers to modify that QRectF variable where you need (like mouseReleaseEvent() maybe,,)
virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event)
virtual void mousePressEvent(QGraphicsSceneMouseEvent * event)
virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event)
And don't forget to call prepareGeometryChange() before changing that value to invalidate item's boundingRect() cache.
In my application I have two object type. One is field item, other is composite item.
Composite items may contain two or more field items.
Here is my composite item implementation.
#include "compositeitem.h"
CompositeItem::CompositeItem(QString id,QList<FieldItem *> _children)
{
children = _children;
}
CompositeItem::~CompositeItem()
{
}
QRectF CompositeItem::boundingRect() const
{
FieldItem *child;
QRectF rect(0,0,0,0);
foreach(child,children)
{
rect = rect.united(child->boundingRect());
}
return rect;
}
void CompositeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget )
{
FieldItem *child;
foreach(child,children)
{
child->paint(painter,option,widget);
}
}
QSizeF CompositeItem::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const
{
QSizeF itsSize(0,0);
FieldItem *child;
foreach(child,children)
{
// if its size empty set first child size to itsSize
if(itsSize.isEmpty())
itsSize = child->sizeHint(Qt::PreferredSize);
else
{
QSizeF childSize = child->sizeHint(Qt::PreferredSize);
if(itsSize.width() < childSize.width())
itsSize.setWidth(childSize.width());
itsSize.setHeight(itsSize.height() + childSize.height());
}
}
return itsSize;
}
void CompositeItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
qDebug()<<"Test";
}
My first question is how I can propagate context menu event to specific child.
Picture on the above demonstrates one of my possible composite item.
If you look on the code above you will see that I print "Test" when context menu event occurs.
When I right click on the line symbol I see that "Test" message is printed.
But when I right click on the signal symbol "Test" is not printed and I want it to be printed.
My second question what cause this behaviour.
How do I overcome this.
Could you try reimplementing your contextMenu with the mouseRelease event instead?
void CompositeItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if(event->button() == Qt::RightButton)
{
contextMenu->popup(QCursor::pos());
}
}
I figured out that there may be two solutions for catching events.
First one is reimplementing shape function.
In my case it will be implemented like this.
QPainterPath shape() const
{
QPainterPath path;
path.addRect(boundingRect());
return path;
}
Second one is to use QGraphicsItemGroup
It will be good idea to use QGraphicsItemGroup if you diretly adding your items to scene. But in my case I have to subclass QGraphicsItemGroup because of I am using a layout.
So temporarily I choose writing my own item.