Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make QGraphicsProxyWidget movable & selectable

Tags:

c++

qt

I want to put a QWidget into a QGraphicsView and make the widget selectable and movable by using QGraphicsProxyWidget. (This works perfectly for QGraphicsRectItem, QGraphicItem etc.)

This is the code I'm using currently:

// Create new QGraphicsScene and assign to graphicsView
scene = new QGraphicsScene(this);
ui->graphicsView->setScene(scene);

// Create widget and add to scene
MyWidget *widget = new MyWidget;
QGraphicsProxyWidget *proxy = scene->addWidget(widget);

// Make selectable
proxy->setFlag(QGraphicsItem::ItemIsSelectable, true);
// Make movable
proxy->setFlag(QGraphicsItem::ItemIsMovable, true);

The widget is displayed correctly, but it is neither movable nor selectable!

Any help would be greatly appreciated ;)

like image 567
dave Avatar asked Mar 14 '13 15:03

dave


1 Answers

I was asking myself the same question as you did here. Before I go on with the solution I came up with after several hours of struggling with this issue I have to mention that in general adding widgets to the scene is not a good idea if you want to have a large amount of those in it. You can read about the issue that come with this here.

Now back on track. Here are a couple of the things that arise as great issues when trying to implement the thing you want to:

  • If you mess with the mouseMove() and mouseHover() of your widget proxy you will screw up the way the widget's UI component behave.
  • At the same time you want it to be selectable and movable so you have to add somehow such features to the widget proxy

How do we do that? Well, directly this seems to be impossible. However I came up with a simple solution that gets the work done - combine a QGraphicsProxyWidget with a QGraphicsItem!

In order to preserve the UI components' functionality in your widget you need to add the proxy as a child to the graphics item. The graphics item (the parent of the proxy) on the other hand will cover the selection and moving part.

Here is a small demo (I do not exclude bugs since this is something that I'm currently researching myself and most of the time I use PyQt instead of C++ Qt):

scenewithmovableproxies.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SceneWithMovableProxies</class>
 <widget class="QWidget" name="SceneWithMovableProxies">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>SceneWithMovableProxies</string>
  </property>
  <layout class="QGridLayout" name="gridLayout">
   <item row="0" column="0">
    <widget class="QGraphicsView" name="graphicsView"/>
   </item>
  </layout>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

main.cpp

#include "scenewithmovableproxies.hpp"
#include <QApplication>

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);
  SceneWithMovableProxies w;
  w.show();

  return a.exec();
}

scenewithmovableproxies.hpp

#ifndef SCENEWITHMOVABLEPROXIES_HPP
#define SCENEWITHMOVABLEPROXIES_HPP

#include <QWidget>
#include <QPoint>

namespace Ui {
  class SceneWithMovableProxies;
}

class SceneWithMovableProxies : public QWidget
{
    Q_OBJECT

  public:
    explicit SceneWithMovableProxies(QWidget *parent = 0);
    ~SceneWithMovableProxies();

  private:
    Ui::SceneWithMovableProxies *ui;
    void addWidgetToScene(QPoint initPos);
};

#endif // SCENEWITHMOVABLEPROXIES_HPP

scenewithmovableproxies.cpp

#include "scenewithmovableproxies.hpp"
#include "ui_scenewithmovableproxies.h"
#include "scenewidgetitem.hpp"
#include <QGraphicsProxyWidget>

SceneWithMovableProxies::SceneWithMovableProxies(QWidget *parent) :
  QWidget(parent),
  ui(new Ui::SceneWithMovableProxies)
{
  ui->setupUi(this);
  ui->graphicsView->setRenderHint(QPainter::Antialiasing);
  ui->graphicsView->setScene(new QGraphicsScene(this));

  // Add widget proxies + their parenting graphics items to scene
  addWidgetToScene(QPoint(10, 10));
  addWidgetToScene(QPoint(300, 100));
  addWidgetToScene(QPoint(200, 200));
}

SceneWithMovableProxies::~SceneWithMovableProxies()
{
  delete ui;
}

void SceneWithMovableProxies::addWidgetToScene(QPoint initPos)
{
  // Create a widget
  SceneWidgetItem *widget = new SceneWidgetItem();
  // Create the graphics item that will be used to move the widget around the screen as well as be selectable (for example in case we want to delete a widget that is in the scene)
  // Depending on the position of the graphics item relative to its widget proxy you can adjust the size and location of both
  QGraphicsRectItem *proxyControl = ui->graphicsView->scene()->addRect(initPos.x(), initPos.y(), widget->width(), 20, QPen(Qt::black), QBrush(Qt::darkGreen)); // widget->width() works properly here because of the resize(layout->sizeHint()) that we have used inside it
  proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
  proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
  // Create the proxy by adding the widget to the scene
  QGraphicsProxyWidget * const proxy = ui->graphicsView->scene()->addWidget(widget);
  // In my case the rectangular graphics item is supposed to be above my widget so the position of the widget is shifted along the Y axis based on the height of the rectangle of that graphics item
  proxy->setPos(initPos.x(), initPos.y()+proxyControl->rect().height());
  proxy->setParentItem(proxyControl);

  // proxyControl->setRotation(45); // Because the widget is a child of the graphics item if we do some sort of transformation to it, the change will be propagated to the widget too!
}

scenewidgetitem.hpp

#ifndef SCENEWIDGETITEM_HPP
#define SCENEWIDGETITEM_HPP

#include <QWidget>
#include <QVBoxLayout>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>

class SceneWidgetItem : public QWidget
{
    Q_OBJECT
    QVBoxLayout *layout;
    QLabel *label;
    QCheckBox *checkbox;
    QComboBox *combobox;
    QPushButton *resetButton;
  public:
    explicit SceneWidgetItem(QWidget *parent = 0);
    ~SceneWidgetItem();

  signals:

  public slots:
    void reset();
};

#endif // SCENEWIDGETITEM_HPP

scenewidgetitem.cpp

#include "scenewidgetitem.hpp"

// Create a widget with whichever UI components you like
SceneWidgetItem::SceneWidgetItem(QWidget *parent) : QWidget(parent)
{
  layout = new QVBoxLayout(this);
  checkbox = new QCheckBox("Enable proxy", this);
  checkbox->setChecked(true);
  combobox = new QComboBox(this);
  combobox->addItem("---");
  combobox->addItem("Item 1");
  combobox->addItem("Item 2");
  combobox->addItem("Item 3");
  label = new QLabel(this);
  label->setText(combobox->itemText(0));
  resetButton = new QPushButton("Reset", this);

  // Maybe add some signals :P
  connect(checkbox, SIGNAL(toggled(bool)), combobox, SLOT(setEnabled(bool)));
  connect(checkbox, SIGNAL(toggled(bool)), resetButton, SLOT(setEnabled(bool)));
  connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(reset()));
  connect(combobox, SIGNAL(currentIndexChanged(QString)), label, SLOT(setText(QString)));

  layout->addWidget(checkbox);
  layout->addWidget(label);
  layout->addWidget(resetButton);
  layout->addWidget(combobox);

  // Resizing the widget to its layout's content is very important. If 
  // you don't do that the parenting graphics item will not visually fit 
  // to its child widget and you will get a mess
  resize(layout->sizeHint());
  setLayout(layout);
}

SceneWidgetItem::~SceneWidgetItem()
{

}

void SceneWidgetItem::reset()
{
  combobox->setCurrentIndex(0);
  label->setText("---");
}

Here are some screenshots:

Initial view enter image description here

Two out of three selected enter image description here

Moving enter image description here

Rotating

enter image description here

Interacting with widget - using the QComboBox enter image description here

Interacting with widget - using the QCheckBox enter image description here

Now due to the nature of QGraphicsItem (as well as QGraphicsProxyWidget) there are some issue the most annoying of which is the overlapping

Overlapping enter image description here

However overlapping can be relatively easily avoided by using collision detection and basically not allowing overlapping at all. Since QGraphicsProxyWidget can also use the QGraphicsItem function collisionWithItem(...) you can implement a mechanism for handling this situation. Since I'm new to QGraphicsScene and all that (started 2 days ago while answering a question here on SO :D) there might be some sort of integrated machanism in QGraphicsScene itself to handle this.

Distortion In the rotation screenshot you might have noticed that there is some visual weirdness going on with what were previously perfect looking straight lines. These are the so called jaggies. Currently I don't know how to get rid of those. I have tried using high quality antialiasing but the results is even worse than with just antialiasing (QPainter::Antialiasing).

Futher research I am actually currently working on a small project of mine where I want to create a composite-node-based UI for image processing. Looking this topic up online always returned solutions where people were using simple QGraphicsItems and the node configuration itself was partially outsourced to external to the QGraphicsViewer widgets. You can use the design I have described above and add more to it depending on what you want to do later on. Multiple QGraphicItems can also be attached to the widget proxy. You can go crazy with this but remember that there is a performance impact (read the blog post I've linked at the beginning of my answer again and again).

like image 178
rbaleksandar Avatar answered Sep 28 '22 03:09

rbaleksandar