2
votes

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?

2
@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
then check out my answer here: stackoverflow.com/a/52026817/5366641scopchanov
and probably also this answer: stackoverflow.com/a/64408630/5366641scopchanov
The best way to do is with handling event. Check this out: doc.qt.io/qt-5/qtquick-input-mouseevents.htmlG. De Mitri

2 Answers

1
votes

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.
0
votes

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.