PDA

View Full Version : How to implement a shape resizer with QGraphicsPolygonItem?



xd123
10th April 2020, 22:20
I have used this code to implement resizing in my QGraphicsRectItem and QGraphicsEllipseItem, since both Rect and Ellipse have implemented function SetRect(qreal x, qreal y, qreal width, qreal height) it was not a problem, but with QGraphicsPolygonItem the nearest function is setPolygon() but it only takes a QPolygonf& Polygon as parameter which doesn't work with my code of sizeGripItem

mainwindow.cpp


class RectResizer : public SizeGripItem::Resizer // resize class for rectagle
{
public:
virtual void operator()(QGraphicsItem* item, const QRectF& rect)
{
QGraphicsRectItem* rectItem =
dynamic_cast<QGraphicsRectItem*>(item);

if (rectItem)
{
rectItem->setRect(rect);
}
}
};

class EllipseResizer : public SizeGripItem::Resizer //resize class for ellipse
{
public:
virtual void operator()(QGraphicsItem* item, const QRectF& rect)
{
QGraphicsEllipseItem* ellipseItem =
dynamic_cast<QGraphicsEllipseItem*>(item);

if (ellipseItem)
{
ellipseItem->setRect(rect);
}
}
};

class PolygonResizer : public SizeGripItem::Resizer // resize class for polygon
{
public:
virtual void operator()(QGraphicsItem* item, const QRectF& rect)
{
QGraphicsPolygonItem* polygonItem =
dynamic_cast<QGraphicsPolygonItem*>(item);

if (polygonItem)
{
polygonItem->setPolygon(rect);
} //^ doesn't work when i attempt to resize shape, it changes to rectangle
}
};



sizegripitem.cpp


#include "sizegripitem.h"
#include <QBrush>

SizeGripItem::HandleItem::HandleItem(int positionFlags, SizeGripItem* parent)
: QGraphicsRectItem(-4, -4, 8, 8, parent),
positionFlags_(positionFlags),
parent_(parent)
{
setBrush(QBrush(Qt::lightGray));
setFlag(ItemIsMovable);
setFlag(ItemSendsGeometryChanges);
}

int SizeGripItem::HandleItem::positionFlags() const
{
return positionFlags_;
}

QVariant SizeGripItem::HandleItem::itemChange(GraphicsItemC hange change,
const QVariant &value)
{
QVariant retVal = value;

if (change == ItemPositionChange)
{
retVal = restrictPosition(value.toPointF());
}
else if (change == ItemPositionHasChanged)
{
QPointF pos = value.toPointF();

switch (positionFlags_)
{
case TopLeft:
parent_->setTopLeft(pos);
break;
case Top:
parent_->setTop(pos.y());
break;
case TopRight:
parent_->setTopRight(pos);
break;
case Right:
parent_->setRight(pos.x());
break;
case BottomRight:
parent_->setBottomRight(pos);
break;
case Bottom:
parent_->setBottom(pos.y());
break;
case BottomLeft:
parent_->setBottomLeft(pos);
break;
case Left:
parent_->setLeft(pos.x());
break;
}
}

return retVal;
}

QPointF SizeGripItem::HandleItem::restrictPosition(const QPointF& newPos)
{
QPointF retVal = pos();

if (positionFlags_ & Top || positionFlags_ & Bottom)
retVal.setY(newPos.y());

if (positionFlags_ & Left || positionFlags_ & Right)
retVal.setX(newPos.x());

if (positionFlags_ & Top && retVal.y() > parent_->rect_.bottom())
retVal.setY(parent_->rect_.bottom());
else if (positionFlags_ & Bottom && retVal.y() < parent_->rect_.top())
retVal.setY(parent_->rect_.top());

if (positionFlags_ & Left && retVal.x() > parent_->rect_.right())
retVal.setX(parent_->rect_.right());
else if (positionFlags_ & Right && retVal.x() < parent_->rect_.left())
retVal.setX(parent_->rect_.left());

return retVal;
}

SizeGripItem::SizeGripItem(Resizer* resizer, QGraphicsItem* parent)
: QGraphicsItem(parent),
resizer_(resizer)
{
if (parentItem())
rect_ = parentItem()->boundingRect();

handleItems_.append(new HandleItem(TopLeft, this));
handleItems_.append(new HandleItem(Top, this));
handleItems_.append(new HandleItem(TopRight, this));
handleItems_.append(new HandleItem(Right, this));
handleItems_.append(new HandleItem(BottomRight, this));
handleItems_.append(new HandleItem(Bottom, this));
handleItems_.append(new HandleItem(BottomLeft, this));
handleItems_.append(new HandleItem(Left, this));
updateHandleItemPositions();
}

SizeGripItem::~SizeGripItem()
{
if (resizer_)
delete resizer_;
}

QRectF SizeGripItem::boundingRect() const
{
return rect_;
}

void SizeGripItem::paint(QPainter* painter,
const QStyleOptionGraphicsItem* option,
QWidget* widget)
{
}

#define IMPL_SET_FN(TYPE, POS) \
void SizeGripItem::set ## POS (TYPE v) \
{ \
rect_.set ## POS (v); \
doResize(); \
}

IMPL_SET_FN(qreal, Top)
IMPL_SET_FN(qreal, Right)
IMPL_SET_FN(qreal, Bottom)
IMPL_SET_FN(qreal, Left)
IMPL_SET_FN(const QPointF&, TopLeft)
IMPL_SET_FN(const QPointF&, TopRight)
IMPL_SET_FN(const QPointF&, BottomRight)
IMPL_SET_FN(const QPointF&, BottomLeft)

void SizeGripItem::doResize()
{
if (resizer_)
{
(*resizer_)(parentItem(), rect_);
updateHandleItemPositions();
}
}

void SizeGripItem::updateHandleItemPositions()
{
foreach (HandleItem* item, handleItems_)
{
item->setFlag(ItemSendsGeometryChanges, false);

switch (item->positionFlags())
{
case TopLeft:
item->setPos(rect_.topLeft());
break;
case Top:
item->setPos(rect_.left() + rect_.width() / 2 - 1,
rect_.top());
break;
case TopRight:
item->setPos(rect_.topRight());
break;
case Right:
item->setPos(rect_.right(),
rect_.top() + rect_.height() / 2 - 1);
break;
case BottomRight:
item->setPos(rect_.bottomRight());
break;
case Bottom:
item->setPos(rect_.left() + rect_.width() / 2 - 1,
rect_.bottom());
break;
case BottomLeft:
item->setPos(rect_.bottomLeft());
break;
case Left:
item->setPos(rect_.left(),
rect_.top() + rect_.height() / 2 - 1);
break;
}

item->setFlag(ItemSendsGeometryChanges, true);
}
}


sizegripitem.h


#ifndef SIZEGRIPITEM_H
#define SIZEGRIPITEM_H


#include <QGraphicsItem>
#include <QGraphicsRectItem>

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

class HandleItem : public QGraphicsRectItem
{
public:
HandleItem(int positionFlags, SizeGripItem* parent);
int positionFlags() const;

protected:
virtual QVariant itemChange(GraphicsItemChange change,
const QVariant &value);

private:
QPointF restrictPosition(const QPointF& newPos);

int positionFlags_;
SizeGripItem* parent_;
};

public:
class Resizer
{
public:
virtual void operator()(QGraphicsItem* item,
const QRectF& rect) = 0;

};

SizeGripItem(Resizer* resizer = 0, QGraphicsItem* parent = 0);
virtual ~SizeGripItem();
virtual QRectF boundingRect() const;
virtual void paint(QPainter* painter,
const QStyleOptionGraphicsItem* option,
QWidget* widget = 0);
void setTopLeft(const QPointF& pos);
void setTop(qreal y);
void setTopRight(const QPointF& pos);
void setRight(qreal x);
void setBottomRight(const QPointF& pos);
void setBottom(qreal y);
void setBottomLeft(const QPointF& pos);
void setLeft(qreal x);

private:
void doResize();
void updateHandleItemPositions();

QList<HandleItem*> handleItems_;
QRectF rect_;
Resizer* resizer_;
};

#endif // SIZEGRIPITEM_H


I can't really think of a way to resize a polygon since the library misses on a lot of functions Rect and Ellipse do have implemented. Thanks for any help or suggestions on how to proceed with resizing a polygon item

d_stranz
10th April 2020, 23:01
Your call to setPolygon() using a QRect doesn't work because there is an overload for the QPolygon constructor that creates a rectangle polygon from a QRect.

For any QGraphicsItem, you can retrieve the boundingRect(). If you have the new rect, you can compute the scaling factor(s) from the sizes of the bounding rect and the new rect, and then use those to create a QTransform that will scale the polygon. You can then set the scaled polygon as the new one. Depending on where the origin of your polygon is, you may have to translate it to (0,0) before scaling it, otherwise it might result in a translation as well. See the section on "Related Non-Members" in QTransform.

xd123
11th April 2020, 18:24
Hi,
Yes that sounds like a solution that would solve my issue, is there any example available online that I could bounce from? I looked through QtTransform library but I can't see a way I could implement it with a mouse event when the user would try to drag for example a triangle shape like this:


QPolygonF Triangle;
Triangle.append(QPointF(-200.,0));
Triangle.append(QPointF(0.,-200));
Triangle.append(QPointF(200.,0));


QModelIndex index = ui->listView->currentIndex(); //annotating the shape with a selected name from a text file
QString itemText = index.data(Qt::DisplayRole).toString();

QGraphicsPolygonItem* triangleItem = new QGraphicsPolygonItem(Triangle);
QColor brush_color(Qt::green); //fill color
brush_color.setAlpha(50); // alpha index makes brush color more opaque
QPen blackpen(Qt::black); //border color
blackpen.setWidth(2); // border width
triangleItem->setBrush(brush_color);
triangleItem->setPen(blackpen);
triangleItem->setFlag(QGraphicsItem::ItemIsMovable); // making the object draggable across graphics view
scene->addItem(triangleItem);

QGraphicsTextItem *RectText = new QGraphicsTextItem(itemText, triangleItem);


All my polygon items are done in this way, just different shapes.
Thanks for your help.

d_stranz
11th April 2020, 23:52
I don't see what this code has to do with your PolygonResizer code you posted earlier. As I said, in the operator() of that class, you have the new rect and you can retrieve the current boundingRect() of the polygon. The ratio of the two heights gives you the y scale, the ratio of the two widths gives the x scale. You retrieve the QPolygon from the QGraphicsPolygonItem, construct a QTransform from the two scales, and apply that to the polygon to get a new scaled polygon. Set that polygon on the QGraphicsPolygonItem instance.

Wikipedia (https://en.wikipedia.org/wiki/Scaling_(geometry)) will tell you how to construct the transform matrix using homogeneous coordinates.

The only trick is that the transform scales about the origin (0, 0) point. If this origin is not at the center of your polygon, scaling may also result in translation in one direction and/or the other. So to fix this, you translate to (0,0), scale, then translate back to the original point.

xd123
15th April 2020, 16:16
Well my polygon resizer would work with this triangle I just posted, and similar shapes with 5 and more points. But I can't figure the transformation out from what you are saying, if you could maybe please post a example or code snippet that might help me get on the right track. I watched a few videos from the official Qt youtube channel for transformation and translation of graphics items but can't really see the correlation with my issue, besides using bounding rect.

d_stranz
15th April 2020, 19:25
Maybe I don't understand your issue. I thought you were trying to implement resizing of graphics items using resize handles on the bounding rectangles of the items.

What you implemented in your initial post works fine for rectangles and ellipses because they are defined by their bounding rectangles. Polygons and other items are defined by an arbitrary closed set of points connected by lines, but they do have a bounding rectangle, the smallest rectangle that contains all of the points. QPolygon (on which QGraphicsPolygonItem is based) has a constructor that takes a QRect argument, but that creates a rectangle polygon. That is the problem you ran into when you tried to implement the same code for resizing polygons that you used for the rectangle and ellipse.

The Wikipedia article I pointed you to shows how to construct a scale transformation using homogenous coordinates, which is what QTransform uses. Your resize code needs to look something like this (and of course you will add checks to avoid divide by zero):



float sx = float( rect.width() ) / float( boundingRect.width() );
float sy = float( rect.height() ) / float( boundingRect.height() );

QTransform transform;
transform.scale( sx, sy );

QPolygonF oldPolygon = item->polygon();
QPolygonF newPolygon = oldPolygon * transform;

item->setPolygon( newPolygon );


This code will change the aspect ratio. If you want to keep the same aspect ratio, then you will need to change one of the scale factors so that the new width and height preserve the same ratio as the old width and height.

But like I said earlier, the scale transformation scales the object around its (0,0) point. If the polygon's origin (0,0) is not at the center of the polygon, then scaling will probably also move it. If that happens to you, you will have to scale in three steps: translate your polygon to (0,0), scale, translate back. But by default, QGraphicsItem coordinates have the origin at their center (except for QGraphicsTextItem, QGraphicsPixmapItem, and maybe a few others that have their origin at topLeft).

xd123
16th April 2020, 15:14
Thank you so much. I now completely see what you meant, I'm sorry for not understanding it the first time :D working with Qt for just about 4 months now. I implemented your idea and it works just as I wanted it to. Really appreciate your help and willingness to explain your solution.

If anybody stumbles on this issue too here is the working polygon resizer class:


class PolygonResizer : public SizeGripItem::Resizer // resize class for polygon
{
public:
virtual void operator()(QGraphicsItem* item, const QRectF& rect)
{
QGraphicsPolygonItem* polygonItem =
dynamic_cast<QGraphicsPolygonItem*>(item);

QRectF itemBoundingRect = item->boundingRect();
float sx = float( rect.width() ) / float( itemBoundingRect.width() );
float sy = float( rect.height() ) / float( itemBoundingRect.height() );

QTransform transform;
transform.scale( sx, sy );

QPolygonF oldPolygon = polygonItem->polygon();
QPolygonF newPolygon = oldPolygon * transform;

if (polygonItem)
{
polygonItem->setPolygon( newPolygon );
}
}
};

d_stranz
17th April 2020, 01:05
Happy to help. A couple of suggestions:

- Since itemBoundingRect and rect are already QRectF (which is floating point based), there is no need to cast width() and height() to float. They already are. I was assuming they were QRect, which is integer based and thus needed casting to avoid rounding errors on division. (if either height or width of the rect is less than the height or width of the bounding rect, integer division would always yield zero as the scale factor).

- You should move the check on polygonItem validity to right after the dynamic_cast. If it is null, then retrieving the oldPolygon a few lines later will blow up.

- You should also use qgraphicsitem_cast<> instead of dynamic_cast<> in all of your resizing methods.