2

I am trying to allow users to add new "widgets" (images, text, perhaps other custom data too. Image is good enough for now) to a kind of design area. And then I would like them to be able to resize/move those conveniently. The best way for the moving part seems to be to use QGraphicsView. Can be done nicely with 4 lines of code:

auto const scene = new QGraphicsScene{this};
auto const item = scene->addPixmap(QPixmap{":/img/example.png"});
item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
ui->graphicsView->setScene(scene);

This results in something like this:

enter image description here

It's nice, but cannot be resized with the mouse. I've seen (on this site) multiple ways to make this, sort of, resizable with a mouse, but they are all kind of hacky.

I've seen this example on Qt website which takes a different approach of creating a custom container for a moveable-and-resizeable container. It seems like it can be made work with a bit more tweaking, but it's again, not a nice solution. The widget doesn't really look like it's resizable. When selected, the borders don't have the nice the clue that it's a dynamically placed/sized thing.

Having ms-paint like moveable widgets must be a common use case, so I reckon there has got to be a nice way to get this happen. Is this the case? QGraphicsScene seems like a good candidate honestly. Perhaps I am missing something?

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • check this out: https://github.com/scopchanov/SO_QProxy – scopchanov Oct 22 '20 at 15:26
  • 1
    @scopchanov Well that's not really helpful, a ton of code with no explanation. Would love if you would write the outline as an answer here. – ypnos Oct 22 '20 at 15:28
  • then check out my answer here: https://stackoverflow.com/a/52026817/5366641 – scopchanov Oct 22 '20 at 15:35
  • and probably also this answer: https://stackoverflow.com/a/64408630/5366641 – scopchanov Oct 22 '20 at 15:42
  • The best way to do is with handling event. Check this out: https://doc.qt.io/qt-5/qtquick-input-mouseevents.html – G. De Mitri Oct 22 '20 at 16:43
  • @G.DeMitri, the question is tagged c++. It goes about widgets, not qml. – scopchanov Oct 22 '20 at 16:51
  • @scopchanov The event handler can also be used with c++. It is a general concept, then through the doc he can choose the best way to implement it. f.e. this class is for the mouse: https://doc.qt.io/qt-5/qmouseevent.html , instead this is the basis for all event objects: https://doc.qt.io/qt-5/qevent.html – G. De Mitri Oct 22 '20 at 16:57

2 Answers2

2

Ok, so I had this problem and my solution was to link the creation of the handlers with the selection of the item:

mainwindow.h

#pragma once

#include <QMainWindow>
#include <QGraphicsItem>
#include <QPainter>

class Handler: public QGraphicsItem
{
public:
    enum Mode
    {
        Top         = 0x1,
        Bottom      = 0x2,
        Left        = 0x4,
        TopLeft     = Top | Left,
        BottomLeft  = Bottom | Left,
        Right       = 0x8,
        TopRight    = Top | Right,
        BottomRight = Bottom | Right,
        Rotate      = 0x10
    };

    Handler(QGraphicsItem *parent, Mode mode);
    ~Handler(){}
    void updatePosition();

    QRectF boundingRect() const override;
protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
    QPointF iniPos;
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
private:
    Mode mode;
    bool isMoving = false;
};

class ObjectResizerGrip: public QGraphicsItem
{
public:
    ObjectResizerGrip(QGraphicsItem *parent): QGraphicsItem(parent)
    {
        setFlag(QGraphicsItem::ItemHasNoContents, true);
        setFlag(QGraphicsItem::ItemIsSelectable, false);
        setFlag(QGraphicsItem::ItemIsFocusable, false);
    }
    void updateHandlerPositions();
    virtual QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override{Q_UNUSED(painter) Q_UNUSED(option) Q_UNUSED(widget)}

protected:
    QList<Handler*> handlers;
};

class Object4SidesResizerGrip: public ObjectResizerGrip
{
public:
    Object4SidesResizerGrip(QGraphicsItem *parent);
};

class Item:public QGraphicsItem
{
public:
    Item(QGraphicsItem *parent=nullptr): QGraphicsItem(parent)
    {
        setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
        setAcceptHoverEvents(true);
    }
    QRectF boundingRect() const override
    {
        return boundingBox;
    }
    void setWidth(qreal value)
    {
        auto width = boundingBox.width();
        if(width == value) return;
        width = qMax(value, 100.0);
        setDimensions(width, boundingBox.height());
    }

    void setHeight(qreal value)
    {
        auto height = boundingBox.height();
        if(height == value) return;
        height = qMax(value, 100.0);
        setDimensions(boundingBox.width(), height);
    }

    void setDimensions(qreal w, qreal h)
    {
        prepareGeometryChange();
        boundingBox = QRectF(-w/2.0, -h/2.0, w, h);
        if(resizerGrip) resizerGrip->updateHandlerPositions();
        update();
    }

private:
    ObjectResizerGrip* resizerGrip = nullptr;

    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
    {
        if(change == ItemSelectedHasChanged && scene())
        {
            if(value.toBool())
            {
                if(!resizerGrip)
                    resizerGrip = newSelectionGrip();
            }
            else
            {
                if(resizerGrip)
                {
                    delete resizerGrip;
                    resizerGrip = nullptr;
                }
            }
        }

        return QGraphicsItem::itemChange(change, value);
    }
    QRectF boundingBox;
    virtual ObjectResizerGrip *newSelectionGrip() =0;
};

class CrossItem:public Item
{
public:
    CrossItem(QGraphicsItem *parent=nullptr): Item(parent){};

private:
    virtual ObjectResizerGrip *newSelectionGrip() override
    {
        return new Object4SidesResizerGrip(this);
    }

    virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
    {
        painter->drawLine(boundingRect().topLeft(), boundingRect().bottomRight());
        painter->drawLine(boundingRect().topRight(), boundingRect().bottomLeft());
    }
};


class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
};

mainwindow.cpp

#include "mainwindow.h"
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QApplication>
#include <QGraphicsSceneMouseEvent>
#include <QHBoxLayout>
// Return nearest point along the line to a given point
// http://stackoverflow.com/questions/1459368/snap-point-to-a-line
QPointF getClosestPoint(const QPointF &vertexA, const QPointF &vertexB, const QPointF &point, const bool segmentClamp)
{
    QPointF AP = point - vertexA;
    QPointF AB = vertexB - vertexA;
    qreal ab2 = AB.x()*AB.x() + AB.y()*AB.y();
    if(ab2 == 0) // Line lenth == 0
        return vertexA;
    qreal ap_ab = AP.x()*AB.x() + AP.y()*AB.y();
    qreal t = ap_ab / ab2;
    if (segmentClamp)
    {
         if (t < 0.0f) t = 0.0f;
         else if (t > 1.0f) t = 1.0f;
    }
    return vertexA + AB * t;
}

Object4SidesResizerGrip::Object4SidesResizerGrip(QGraphicsItem* parent) : ObjectResizerGrip(parent)
{
    handlers.append(new Handler(this, Handler::Left));
    handlers.append(new Handler(this, Handler::BottomLeft));
    handlers.append(new Handler(this, Handler::Bottom));
    handlers.append(new Handler(this, Handler::BottomRight));
    handlers.append(new Handler(this, Handler::Right));
    handlers.append(new Handler(this, Handler::TopRight));
    handlers.append(new Handler(this, Handler::Top));
    handlers.append(new Handler(this, Handler::TopLeft));
    handlers.append(new Handler(this, Handler::Rotate));
    updateHandlerPositions();
}

QRectF ObjectResizerGrip::boundingRect() const
{
    return QRectF();
}

void ObjectResizerGrip::updateHandlerPositions()
{
    foreach (Handler* item, handlers)
        item->updatePosition();
}

Handler::Handler(QGraphicsItem *parent, Mode mode): QGraphicsItem(parent), mode(mode)
{
    QPen pen(Qt::white);
    pen.setWidth(0);
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemIsSelectable, false);

    setAcceptHoverEvents(true);
    setZValue(100);
    setCursor(Qt::UpArrowCursor);
    updatePosition();
}

void Handler::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    QPen pen(isMoving ? QColor(250,214,36) : QColor(100,100,100));
    pen.setWidth(0);
    pen.setBrush(pen.color());
    painter->setPen(pen);
    painter->setBrush(QColor(100,100,100,150));
    if(mode & Rotate)
    {
        auto rect_ = ((Item*) parentItem()->parentItem())->boundingRect();
        auto topPos = QPointF(rect_.left() + rect_.width() / 2 - 1, rect_.top());
        painter->drawLine(mapFromParent(topPos), mapFromParent(topPos - QPointF(0, 175)));
        painter->drawEllipse(boundingRect());
    }
    else
        painter->drawRect(boundingRect());
}

QRectF Handler::boundingRect() const
{
    return QRectF(-25, -25, 50, 50);
}

void Handler::updatePosition()
{
    auto rect_ = ((Item*) parentItem()->parentItem())->boundingRect();
    switch (mode)
    {
        case TopLeft:
            setPos(rect_.topLeft());
            break;
        case Top:
            setPos(rect_.left() + rect_.width() / 2 - 1,rect_.top());
            break;
        case TopRight:
            setPos(rect_.topRight());
            break;
        case Right:
            setPos(rect_.right(),rect_.top() + rect_.height() / 2 - 1);
            break;
        case BottomRight:
            setPos(rect_.bottomRight());
            break;
        case Bottom:
            setPos(rect_.left() + rect_.width() / 2 - 1,rect_.bottom());
            break;
        case BottomLeft:
            setPos(rect_.bottomLeft());
            break;
        case Left:
            setPos(rect_.left(), rect_.top() + rect_.height() / 2 - 1);
            break;
        case Rotate:
            setPos(0, rect_.top() - 200);
            break;
    }
}

void Handler::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(mode & Rotate)
    {
        Item* item = (Item*) parentItem()->parentItem();
        auto angle =  QLineF(item->mapToScene(QPoint()), event->scenePos()).angle();
        if(!(QApplication::keyboardModifiers() & Qt::AltModifier))  // snap to 45deg
        {
            auto modAngle = fmod(angle+180, 45);
            if(modAngle < 10 || modAngle > 35)
                angle = round(angle/45)*45;
        }
        item->setRotation(0);
        angle = QLineF(item->mapFromScene(QPoint()), item->mapFromScene(QLineF::fromPolar(10, angle).p2())).angle();
        item->setRotation(90 - angle);
        item->update();
    }
    else
    {
        Item* item = (Item*) parentItem()->parentItem();
        auto diff = mapToItem(item, event->pos()) - mapToItem(item, event->lastPos());
        auto bRect = item->boundingRect();
        if(mode == TopLeft || mode == BottomRight)
            diff = getClosestPoint(bRect.topLeft(), QPoint(0,0), diff, false);
        else if(mode == TopRight || mode == BottomLeft)
            diff = getClosestPoint(bRect.bottomLeft(), QPoint(0,0), diff, false);

        if(mode & Left || mode & Right)
        {
            item->setPos(item->mapToScene(QPointF(diff.x()/2.0, 0)));
            if(mode & Left)
                item->setWidth(item->boundingRect().width() - diff.x());
            else
                item->setWidth(item->boundingRect().width() + diff.x());
        }
        if(mode & Top || mode & Bottom)
        {
            item->setPos(item->mapToScene(QPointF(0, diff.y()/2.0)));
            if(mode & Top)
                item->setHeight(item->boundingRect().height() - diff.y());
            else
                item->setHeight(item->boundingRect().height() + diff.y());
        }
        item->update();
    }
    ((ObjectResizerGrip*) parentItem())->updateHandlerPositions();
}

void Handler::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    Q_UNUSED(event);
    isMoving = true;
}

void Handler::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    Q_UNUSED(event);
    isMoving = false;
}

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{    
    auto const graphicsView = new QGraphicsView(this);
    graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
    auto const scene = new QGraphicsScene(this);
    auto const item = new CrossItem();
    item->setWidth(100);
    item->setHeight(100);
    scene->addItem(item);
    item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsSelectable);
    graphicsView->setScene(scene);
    graphicsView-> fitInView(scene->sceneRect(), Qt::KeepAspectRatio);

    setCentralWidget(graphicsView);
}

MainWindow::~MainWindow()
{
}

main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

This solution is far from perfect but it works and can be a good start for improvements. Known issues:

  • rotation grip requires FullViewportUpdate because I was too lazy to implement it in a separate child item and it is drawing outside the bounding box.
  • there are probably better architectures like using proxies or signals/event.
Adriel Jr
  • 2,451
  • 19
  • 25
0

When it comes to using mouse, keyboard and in general capturing operating system events, you have to rely on the event system. The base class is QEvent, which in your specific case allows you to "QResizeEvent, QMouseEvent, QScrollEvent, ..." and many more fun things.

G. De Mitri
  • 115
  • 10
  • 1
    `QWidgets` are intended to be used as building block of the GUI, not the document. Would you advise someone to use a QWidget to represent a letter in a text document? I hope not. It could certainly be done, however it would be a really major design flaw. Then why are you advising to OP to use widgets for graphical documents? – scopchanov Oct 22 '20 at 17:42
  • 1
    Furthermore, the very basic idea of doing everything from scratch is far from apropriate. The OP is complaining that the QGraphics framework is lacking SOME functiontionality and you are telling him/her: ahh, forget it - take QWidget and do EVERYTING by yourself. It does not make sense. – scopchanov Oct 22 '20 at 17:47
  • By the way, excuse me, if I sound harsh. This is not my intention. However, a lot of people, including me and probably you, are using SO as a reference. It should be clear to the future readers, why this is not a good idea. So, no hard feelings. – scopchanov Oct 22 '20 at 17:51
  • @scopchanov I agree 100% with you. It feels awkward to fit widgets in this situation. I am not familiar with the `QGraphics` api, so I am not sure if it's the right choice in here, and that's mostly the question in here. Does qt have a better (fit) solution, or should I try to make `QGraphics` work (somehow)? – Aykhan Hagverdili Oct 22 '20 at 18:07
  • 1
    @AyxanHaqverdili, you can familiarize yourself with the Graphics View Framework [here](https://doc.qt.io/qt-5/graphicsview.html). However, you have correctly noticed, that some basic things as resizing are missing. You could go this way and follow the examples, including the ones I have provided. However, with Qt 6 knocking on the door and promising an improved QML, I think it would be better for you to go directly in that direction. This is some mixture from widgets and QGraphicsView and is the (from Qt) recommended way to build contemporary looking apps. – scopchanov Oct 22 '20 at 18:17
  • 1
    @AyxanHaqverdili, you still have to implement a lot of things by yourself, however it is the way to go nowadays. – scopchanov Oct 22 '20 at 18:20
  • @scopchanov thank you for the response. From what I read, I got the impression that QML is really for mobile devices and I should prefer Widgets on desktop. I should probably try it. – Aykhan Hagverdili Oct 22 '20 at 19:01
  • @AyxanHaqverdili, you shouldn't worry about this. There are many valid use cases for QML and Qt Quick for Desktop. – scopchanov Oct 22 '20 at 19:12
  • In every graphical view examples Qt uses events. The Boxes example implements "Scrolling the mouse wheel zooms in and out of the scene." using events. https://code.qt.io/cgit/qt/qtbase.git/tree/examples/widgets/graphicsview/boxes/qtbox.cpp?h=5.15 – G. De Mitri Oct 22 '20 at 20:01
  • @Pavel Strakhow also uses this method, as written in this post https://stackoverflow.com/questions/19113532/qgraphicsview-zooming-in-and-out-under-mouse-position-using-mouse-wheel/19114517#19114517 – G. De Mitri Oct 22 '20 at 20:22
  • @G.DeMitri, OK, let's settle the confusion. The events you have mentioned in your answer are **widget** events. The boxes example you've linked in the comment uses **scene** events. The QWidgets and the Qt Graphics Framework are two different paradigms. Together with the claim, that _QGraphicsScene is certainly not for you_, I understand your post like this: Do not use the Qt Graphics Framework, but use QWidgets. If you've meant something else than I have understood, please make it clear in your answer. – scopchanov Oct 22 '20 at 20:42
  • yes, maybe I was too hasty to write, unfortunately it is also the fault of my bad English, however I have never mentioned QWidgets. I hope it has made a little more clarity. – G. De Mitri Oct 22 '20 at 20:51
  • Don't worry! As I've said, it's not personal. It goes about correctness of the information for future reference. Unfortunatelly the answer is still not correct. The `QGraphicsItem` does not have a `resizeEvent` - QWidgets do. Also an example would improve it from a comment to an answer. Btw, Qt5 was released on 19 Dec. 2012. The post of Pavel Strakhow is from less than an year later. It's still a great post, don't get me wrong. However, we have the beta of Qt6 now. Today was the Qt World Sumit. Less than two months ago was the Qt Desktop Days Conference. The things are definitely changing. – scopchanov Oct 22 '20 at 21:03