Quite often there are questions in one way or another related to the work of the graphic scene, with custom figures, drawing lines on the graphic scene, and even all sorts of broken lines. And then I remembered about one project that I did as a test task.
Namely, it was a vector editor that can:
- Create rectangles
- Change the size of these rectangles
- Twist the rectangles around the center
- Fill rectangles
- Fill rectangles with a gradient
- Change the width of the outline of a rectangle
- Set the color of the outline of a rectangle
- Create lines
- Specify the width and color of the created line
- Make broken lines from lines by double clicking by adding points on the line
- Select all graphic objects and drag them with a handful
- Save the resulting image to an SVG file, and restore all graphic objects from this file
On the implementation of this project in its time (1.5 years ago), I spent about 36 hours of working time ... now it would take less time.
Project structure
And now we will deal with the key points in the project. Let's start with the structure.
As you can see, this is the largest structure of the project from all the articles presented on the site. In addition to these files, there are also GUI files, and icons for buttons.
Main Window
The main application window serves for loading and saving SVG files, displaying a graphic scene on which graphics objects work, as well as buttons that control the creation and editing of graphic objects.
Here are a few interface buttons.
- Opening an SVG file
- Saving an SVG file
- Select the tool cursor, with which you can select objects in the graphic scene
- Selecting a Polyline Tool
- Selecting the Rectangle Tool
The polyline tool allows you to create one line with the color and width options, and then using the cursor tool, you can create new points on the line by double clicking and move them, allowing you to make a broken line.
The rectangle tool allows you to create a rectangle by specifying a fill or gradient, as well as the width and color of the outline. Using the cursor tool, you can edit the parameters of the rectangle, as well as change its size and rotation angle.
Restoring objects from SVG files
The SVG format is an XML format that can have many different implementations, so this editor is primitive and can not load all possible formats, and works guaranteed only with those files that it can create. But even here there may be errors depending on which version of Qt the project was compiled with.
To restore graphical objects, use the auxiliary class SvgReader. It also uses the transformation matrix.
QList<QGraphicsItem *> SvgReader::getElements(const QString filename) { QList<QGraphicsItem *> graphicsList; QList<QLinearGradient> gradientList; QDomDocument doc; QFile file(filename); if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return graphicsList; QDomNodeList linearList = doc.elementsByTagName("linearGradient"); for(int i = 0; i < linearList.size(); i++) { QDomNode linearNode = linearList.item(i); QDomNodeList stopList = linearNode.childNodes(); QLinearGradient gradient; for(int j = 0; j < stopList.size(); j++){ QDomElement stopElement = stopList.item(j).toElement(); QColor color(stopElement.attribute("stop-color")); gradient.setColorAt(stopElement.attribute("offset").toFloat(),color); } gradientList.append(gradient); } QDomNodeList gList = doc.elementsByTagName("g"); for (int i = 0; i < gList.size(); i++) { QDomNode gNode = gList.item(i); QDomElement pathElement = gNode.firstChildElement("path"); if (!pathElement.isNull()){ VEPolyline *polyline = new VEPolyline(); auto pElement = gNode.toElement(); polyline->setBrush(QBrush(Qt::transparent)); QColor strokeColor(pElement.attribute("stroke", "#000000")); strokeColor.setAlphaF(pElement.attribute("stroke-opacity").toFloat()); polyline->setPen(QPen(strokeColor, pElement.attribute("stroke-width", "0").toInt())); QPainterPath path; QStringList listDotes = pathElement.attribute("d").split(" "); QString first = listDotes.at(0); QStringList firstElement = first.replace(QString("M"),QString("")).split(","); path.moveTo(firstElement.at(0).toInt(),firstElement.at(1).toInt()); for(int i = 1; i < listDotes.length(); i++){ QString other = listDotes.at(i); QStringList dot = other.replace(QString("L"),QString("")).split(","); path.lineTo(dot.at(0).toInt(),dot.at(1).toInt()); } polyline->setPath(path); graphicsList.append(polyline); continue; } QDomElement rectangle = gNode.firstChildElement("rect"); if (!rectangle.isNull()){ VERectangle *rect = new VERectangle(); auto gElement = gNode.toElement(); rect->setRect(rectangle.attribute("x").toInt(), rectangle.attribute("y").toInt(), rectangle.attribute("width").toInt(), rectangle.attribute("height").toInt()); QString fill = gElement.attribute("fill", "#ffffff"); if(fill.contains("url(#gradient")){ fill.replace(QString("url(#gradient"), QString("")); fill.replace(QString(")"), QString("")); QLinearGradient g = gradientList.at(fill.toInt() - 1); auto tmpRect = rect->rect(); g.setStart(tmpRect.left() + tmpRect.width()/2,tmpRect.top()); g.setFinalStop(tmpRect.left() + tmpRect.width()/2,tmpRect.bottom()); rect->setBrush(QBrush(g)); } else { QColor fillColor(gElement.attribute("fill", "#ffffff")); fillColor.setAlphaF(gElement.attribute("fill-opacity","0").toFloat()); rect->setBrush(QBrush(fillColor)); } QColor strokeColor(gElement.attribute("stroke", "#000000")); strokeColor.setAlphaF(gElement.attribute("stroke-opacity").toFloat()); QString transString = gElement.attribute("transform"); transString.replace(QString("matrix("),QString("")); transString.replace(QString(")"),QString("")); QStringList transList = transString.split(","); QTransform trans(rect->transform()); qreal m11 = trans.m11(); // Horizontal scaling qreal m12 = trans.m12(); // Vertical shearing qreal m13 = trans.m13(); // Horizontal Projection qreal m21 = trans.m21(); // Horizontal shearing qreal m22 = trans.m22(); // vertical scaling qreal m23 = trans.m23(); // Vertical Projection qreal m31 = trans.m31(); // Horizontal Position (DX) qreal m32 = trans.m32(); // Vertical Position (DY) qreal m33 = trans.m33(); // Addtional Projection Factor m11 = transList.at(0).toFloat(); m12 = transList.at(1).toFloat(); m21 = transList.at(2).toFloat(); m22 = transList.at(3).toFloat(); m31 = transList.at(4).toFloat(); m32 = transList.at(5).toFloat(); trans.setMatrix(m11,m12,m13,m21,m22,m23,m31,m32,m33); rect->setTransform(trans); rect->setPen(QPen(strokeColor,gElement.attribute("stroke-width", "0").toInt())); graphicsList.append(rect); continue; } } file.close(); return graphicsList; } QRectF SvgReader::getSizes(const QString filename) { QDomDocument doc; QFile file(filename); if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return QRectF(0,0,200,200); QDomNodeList list = doc.elementsByTagName("svg"); if(list.length() > 0) { auto svgElement = list.item(0).toElement(); auto parameters = svgElement.attribute("viewBox").split(" "); return QRectF(parameters.at(0).toInt(), parameters.at(1).toInt(), parameters.at(2).toInt(), parameters.at(3).toInt()); } return QRectF(0,0,200,200); }
Polyline
The whole point of working with graphic objects in this case is that you need to override the mouse event processing methods to react to mouse movement over an object, to clicks, etc.
vepolyline.h
#ifndef VEPOLYLINE_H #define VEPOLYLINE_H #include <QObject> #include <QGraphicsPathItem> class DotSignal; class QGraphicsSceneMouseEvent; class VEPolyline : public QObject, public QGraphicsPathItem { Q_OBJECT Q_PROPERTY(QPointF previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged) public: explicit VEPolyline(QObject *parent = 0); ~VEPolyline(); QPointF previousPosition() const; void setPreviousPosition(const QPointF previousPosition); void setPath(const QPainterPath &path); signals: void previousPositionChanged(); void clicked(VEPolyline *rect); void signalMove(QGraphicsItem *item, qreal dx, qreal dy); protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; public slots: private slots: void slotMove(QGraphicsItem *signalOwner, qreal dx, qreal dy); void checkForDeletePoints(); private: QPointF m_previousPosition; bool m_leftMouseButtonPressed; QList<DotSignal *> listDotes; int m_pointForCheck; void updateDots(); }; #endif // VEPOLYLINE_H
vepolyline.cpp
To correctly arrange line points on a graphic scene, you need to use an auxiliary variable, relative to which the position of the previous click on the graphic scene - m_leftMouseButtonPressed will be stored. Concerning this variable, the delta position is calculated to calculate the correct position of the new point, or the new position of the old point of the broken line.
To determine the location of creating a new point on the line, you use the mouseDoubleClickEvent method overrides. The essence of the method is that it is necessary to determine the position of the point in the path of the QPainterPath line, split the line into two lines into which the given point enters and set a new path to this graphic object.
Also interesting is the fact that moving points on the graphic scene is carried out using special graphic objects, signal points of the DotSignal class, which are ordinary rectangles that report the coordinates of points and the new positions of the points coordinates, with the change of which the QPainterPath automatically changes.
#include "vepolyline.h" #include <QGraphicsSceneMouseEvent> #include <QPainterPath> #include <QGraphicsScene> #include <QGraphicsPathItem> #include <QDebug> #include "dotsignal.h" VEPolyline::VEPolyline(QObject *parent) : QObject(parent) { setAcceptHoverEvents(true); setFlags(ItemIsSelectable|ItemSendsGeometryChanges); } VEPolyline::~VEPolyline() { } QPointF VEPolyline::previousPosition() const { return m_previousPosition; } void VEPolyline::setPreviousPosition(const QPointF previousPosition) { if (m_previousPosition == previousPosition) return; m_previousPosition = previousPosition; emit previousPositionChanged(); } void VEPolyline::setPath(const QPainterPath &path) { QGraphicsPathItem::setPath(path); } void VEPolyline::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (m_leftMouseButtonPressed) { auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); moveBy(dx,dy); setPreviousPosition(event->scenePos()); emit signalMove(this, dx, dy); } QGraphicsItem::mouseMoveEvent(event); } void VEPolyline::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_leftMouseButtonPressed = true; setPreviousPosition(event->scenePos()); emit clicked(this); } QGraphicsItem::mousePressEvent(event); } void VEPolyline::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_leftMouseButtonPressed = false; } QGraphicsItem::mouseReleaseEvent(event); } void VEPolyline::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { QPointF clickPos = event->pos(); QLineF checkLineFirst(clickPos.x() - 5, clickPos.y() - 5, clickPos.x() + 5, clickPos.y() + 5); QLineF checkLineSecond(clickPos.x() + 5, clickPos.y() - 5, clickPos.x() - 5, clickPos.y() + 5); QPainterPath oldPath = path(); QPainterPath newPath; for(int i = 0; i < oldPath.elementCount(); i++){ QLineF checkableLine(oldPath.elementAt(i), oldPath.elementAt(i+1)); if(checkableLine.intersect(checkLineFirst,0) == 1 || checkableLine.intersect(checkLineSecond,0) == 1){ if(i == 0){ newPath.moveTo(oldPath.elementAt(i)); newPath.lineTo(clickPos); } else { newPath.lineTo(oldPath.elementAt(i)); newPath.lineTo(clickPos); } } else { if(i == 0){ newPath.moveTo(oldPath.elementAt(i)); } else { newPath.lineTo(oldPath.elementAt(i)); } } if(i == (oldPath.elementCount() - 2)) { newPath.lineTo(oldPath.elementAt(i + 1)); i++; } } setPath(newPath); updateDots(); QGraphicsItem::mouseDoubleClickEvent(event); } void VEPolyline::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { if(!listDotes.isEmpty()){ foreach (DotSignal *dot, listDotes) { dot->deleteLater(); } listDotes.clear(); } QGraphicsItem::hoverLeaveEvent(event); } void VEPolyline::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { QGraphicsItem::hoverMoveEvent(event); } void VEPolyline::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { QPainterPath linePath = path(); for(int i = 0; i < linePath.elementCount(); i++){ QPointF point = linePath.elementAt(i); DotSignal *dot = new DotSignal(point, this); connect(dot, &DotSignal::signalMove, this, &VEPolyline::slotMove); connect(dot, &DotSignal::signalMouseRelease, this, &VEPolyline::checkForDeletePoints); dot->setDotFlags(DotSignal::Movable); listDotes.append(dot); } QGraphicsItem::hoverEnterEvent(event); } void VEPolyline::slotMove(QGraphicsItem *signalOwner, qreal dx, qreal dy) { QPainterPath linePath = path(); for(int i = 0; i < linePath.elementCount(); i++){ if(listDotes.at(i) == signalOwner){ QPointF pathPoint = linePath.elementAt(i); linePath.setElementPositionAt(i, pathPoint.x() + dx, pathPoint.y() + dy); m_pointForCheck = i; } } setPath(linePath); } void VEPolyline::checkForDeletePoints() { if(m_pointForCheck != -1){ QPainterPath linePath = path(); QPointF pathPoint = linePath.elementAt(m_pointForCheck); if(m_pointForCheck > 0){ QLineF lineToNear(linePath.elementAt(m_pointForCheck-1),pathPoint); if(lineToNear.length() < 6.0) { QPainterPath newPath; newPath.moveTo(linePath.elementAt(0)); for(int i = 1; i < linePath.elementCount(); i++){ if(i != m_pointForCheck){ newPath.lineTo(linePath.elementAt(i)); } } setPath(newPath); } } if(m_pointForCheck < linePath.elementCount() - 1){ QLineF lineToNear(linePath.elementAt(m_pointForCheck+1),pathPoint); if(lineToNear.length() < 6.0) { QPainterPath newPath; newPath.moveTo(linePath.elementAt(0)); for(int i = 1; i < linePath.elementCount(); i++){ if(i != m_pointForCheck){ newPath.lineTo(linePath.elementAt(i)); } } setPath(newPath); } } updateDots(); m_pointForCheck = -1; } } void VEPolyline::updateDots() { if(!listDotes.isEmpty()){ foreach (DotSignal *dot, listDotes) { dot->deleteLater(); } listDotes.clear(); } QPainterPath linePath = path(); for(int i = 0; i < linePath.elementCount(); i++){ QPointF point = linePath.elementAt(i); DotSignal *dot = new DotSignal(point, this); connect(dot, &DotSignal::signalMove, this, &VEPolyline::slotMove); connect(dot, &DotSignal::signalMouseRelease, this, &VEPolyline::checkForDeletePoints); dot->setDotFlags(DotSignal::Movable); listDotes.append(dot); } }
Signal points
Signal points are used to indicate the movement of the parent of the signal point. That is, each signal point is attached to a certain parent and is responsible for the movement of some part of it.
To indicate the movement, a signal
void signalMove(QGraphicsItem *signalOwner, qreal dx, qreal dy);
dotsignal.h
#ifndef DOTSIGNAL_H #define DOTSIGNAL_H #include <QObject> #include <QGraphicsRectItem> class QGraphicsSceneHoverEventPrivate; class QGraphicsSceneMouseEvent; class DotSignal : public QObject, public QGraphicsRectItem { Q_OBJECT Q_PROPERTY(QPointF previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged) public: explicit DotSignal(QGraphicsItem *parentItem = 0, QObject *parent = 0); explicit DotSignal(QPointF pos, QGraphicsItem *parentItem = 0, QObject *parent = 0); ~DotSignal(); enum Flags { Movable = 0x01 }; QPointF previousPosition() const; void setPreviousPosition(const QPointF previousPosition); void setDotFlags(unsigned int flags); signals: void previousPositionChanged(); void signalMouseRelease(); void signalMove(QGraphicsItem *signalOwner, qreal dx, qreal dy); protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event); void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); public slots: private: unsigned int m_flags; QPointF m_previousPosition; }; #endif // DOTSIGNAL_H
dotsignal.cpp
As you can see from the following code, all method overrides serve only to calculate the delta of coordinates and the design of the point itself, for example, if the mouse cursor is above the point, the point will be red if the point area leaves the point, the point will be black.
#include "dotsignal.h" #include <QBrush> #include <QColor> #include <QGraphicsSceneHoverEvent> #include <QGraphicsSceneMouseEvent> DotSignal::DotSignal(QGraphicsItem *parentItem, QObject *parent) : QObject(parent) { setParentItem(parentItem); setAcceptHoverEvents(true); setBrush(QBrush(Qt::black)); setRect(-4,-4,8,8); setDotFlags(0); } DotSignal::DotSignal(QPointF pos, QGraphicsItem *parentItem, QObject *parent) : QObject(parent) { setParentItem(parentItem); setAcceptHoverEvents(true); setBrush(QBrush(Qt::black)); setRect(-4,-4,8,8); setPos(pos); setPreviousPosition(pos); setDotFlags(0); } DotSignal::~DotSignal() { } QPointF DotSignal::previousPosition() const { return m_previousPosition; } void DotSignal::setPreviousPosition(const QPointF previousPosition) { if (m_previousPosition == previousPosition) return; m_previousPosition = previousPosition; emit previousPositionChanged(); } void DotSignal::setDotFlags(unsigned int flags) { m_flags = flags; } void DotSignal::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if(m_flags & Movable){ auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); moveBy(dx,dy); setPreviousPosition(event->scenePos()); emit signalMove(this, dx, dy); } else { QGraphicsItem::mouseMoveEvent(event); } } void DotSignal::mousePressEvent(QGraphicsSceneMouseEvent *event) { if(m_flags & Movable){ setPreviousPosition(event->scenePos()); } else { QGraphicsItem::mousePressEvent(event); } } void DotSignal::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { emit signalMouseRelease(); QGraphicsItem::mouseReleaseEvent(event); } void DotSignal::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { Q_UNUSED(event) setBrush(QBrush(Qt::red)); } void DotSignal::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { Q_UNUSED(event) setBrush(QBrush(Qt::black)); }
Rectangle
The operation of the rectangle differs from the line in that we are able to change its dimensions and also rotate around the center. Signal points are also used for this. But two possible states are used. One state for resize, another state for rotation:
enum ActionStates { ResizeState = 0x01, RotationState = 0x02 };
To manage the signal points that are responsible for the rescheduling and rotation, enumerations are also used.
enum CornerFlags { Top = 0x01, Bottom = 0x02, Left = 0x04, Right = 0x08, TopLeft = Top|Left, TopRight = Top|Right, BottomLeft = Bottom|Left, BottomRight = Bottom|Right }; enum CornerGrabbers { GrabberTop = 0, GrabberBottom, GrabberLeft, GrabberRight, GrabberTopLeft, GrabberTopRight, GrabberBottomLeft, GrabberBottomRight };
If you change the position of one of the signal points, you must change the position of all the other points by using the setPositionGrabbers() method; among other things, depending on the editing mode, you will use the method of setting the visibility of signal points. setVisibilityGrabbers() .
To normalize the angle of rotation with the rotation change, the normalization function will be used so that the rotation angle in radians does not exceed 2Pi.
static const double Pi = 3.14159265358979323846264338327950288419717; static double TwoPi = 2.0 * Pi; static qreal normalizeAngle(qreal angle) { while (angle < 0) angle += TwoPi; while (angle > TwoPi) angle -= TwoPi; return angle; }
verectangle.h
#ifndef RECTANGLE_H #define RECTANGLE_H #include <QObject> #include <QGraphicsRectItem> class DotSignal; class QGraphicsSceneMouseEvent; class VERectangle : public QObject, public QGraphicsRectItem { Q_OBJECT Q_PROPERTY(QPointF previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged) public: explicit VERectangle(QObject * parent = 0); ~VERectangle(); enum ActionStates { ResizeState = 0x01, RotationState = 0x02 }; enum CornerFlags { Top = 0x01, Bottom = 0x02, Left = 0x04, Right = 0x08, TopLeft = Top|Left, TopRight = Top|Right, BottomLeft = Bottom|Left, BottomRight = Bottom|Right }; enum CornerGrabbers { GrabberTop = 0, GrabberBottom, GrabberLeft, GrabberRight, GrabberTopLeft, GrabberTopRight, GrabberBottomLeft, GrabberBottomRight }; QPointF previousPosition() const; void setPreviousPosition(const QPointF previousPosition); void setRect(qreal x, qreal y, qreal w, qreal h); void setRect(const QRectF &rect); signals: void rectChanged(VERectangle *rect); void previousPositionChanged(); void clicked(VERectangle *rect); void signalMove(QGraphicsItem *item, qreal dx, qreal dy); protected: void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; private: unsigned int m_cornerFlags; unsigned int m_actionFlags; QPointF m_previousPosition; bool m_leftMouseButtonPressed; DotSignal *cornerGrabber[8]; void resizeLeft( const QPointF &pt); void resizeRight( const QPointF &pt); void resizeBottom(const QPointF &pt); void resizeTop(const QPointF &pt); void rotateItem(const QPointF &pt); void setPositionGrabbers(); void setVisibilityGrabbers(); void hideGrabbers(); }; #endif // RECTANGLE_H
verectangle.cpp
#include "verectangle.h" #include <QPainter> #include <QDebug> #include <QCursor> #include <QGraphicsScene> #include <QGraphicsSceneMouseEvent> #include <QGraphicsRectItem> #include <math.h> #include "dotsignal.h" static const double Pi = 3.14159265358979323846264338327950288419717; static double TwoPi = 2.0 * Pi; static qreal normalizeAngle(qreal angle) { while (angle < 0) angle += TwoPi; while (angle > TwoPi) angle -= TwoPi; return angle; } VERectangle::VERectangle(QObject *parent) : QObject(parent), m_cornerFlags(0), m_actionFlags(ResizeState) { setAcceptHoverEvents(true); setFlags(ItemIsSelectable|ItemSendsGeometryChanges); for(int i = 0; i < 8; i++){ cornerGrabber[i] = new DotSignal(this); } setPositionGrabbers(); } VERectangle::~VERectangle() { for(int i = 0; i < 8; i++){ delete cornerGrabber[i]; } } QPointF VERectangle::previousPosition() const { return m_previousPosition; } void VERectangle::setPreviousPosition(const QPointF previousPosition) { if (m_previousPosition == previousPosition) return; m_previousPosition = previousPosition; emit previousPositionChanged(); } void VERectangle::setRect(qreal x, qreal y, qreal w, qreal h) { setRect(QRectF(x,y,w,h)); } void VERectangle::setRect(const QRectF &rect) { QGraphicsRectItem::setRect(rect); if(brush().gradient() != 0){ const QGradient * grad = brush().gradient(); if(grad->type() == QGradient::LinearGradient){ auto tmpRect = this->rect(); const QLinearGradient *lGradient = static_cast<const QLinearGradient *>(grad); QLinearGradient g = *const_cast<QLinearGradient*>(lGradient); g.setStart(tmpRect.left() + tmpRect.width()/2,tmpRect.top()); g.setFinalStop(tmpRect.left() + tmpRect.width()/2,tmpRect.bottom()); setBrush(g); } } } void VERectangle::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { QPointF pt = event->pos(); if(m_actionFlags == ResizeState){ switch (m_cornerFlags) { case Top: resizeTop(pt); break; case Bottom: resizeBottom(pt); break; case Left: resizeLeft(pt); break; case Right: resizeRight(pt); break; case TopLeft: resizeTop(pt); resizeLeft(pt); break; case TopRight: resizeTop(pt); resizeRight(pt); break; case BottomLeft: resizeBottom(pt); resizeLeft(pt); break; case BottomRight: resizeBottom(pt); resizeRight(pt); break; default: if (m_leftMouseButtonPressed) { setCursor(Qt::ClosedHandCursor); auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); moveBy(dx,dy); setPreviousPosition(event->scenePos()); emit signalMove(this, dx, dy); } break; } } else { switch (m_cornerFlags) { case TopLeft: case TopRight: case BottomLeft: case BottomRight: { rotateItem(pt); break; } default: if (m_leftMouseButtonPressed) { setCursor(Qt::ClosedHandCursor); auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); moveBy(dx,dy); setPreviousPosition(event->scenePos()); emit signalMove(this, dx, dy); } break; } } QGraphicsItem::mouseMoveEvent(event); } void VERectangle::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_leftMouseButtonPressed = true; setPreviousPosition(event->scenePos()); emit clicked(this); } QGraphicsItem::mousePressEvent(event); } void VERectangle::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_leftMouseButtonPressed = false; } QGraphicsItem::mouseReleaseEvent(event); } void VERectangle::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { m_actionFlags = (m_actionFlags == ResizeState)?RotationState:ResizeState; setVisibilityGrabbers(); QGraphicsItem::mouseDoubleClickEvent(event); } void VERectangle::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { setPositionGrabbers(); setVisibilityGrabbers(); QGraphicsItem::hoverEnterEvent(event); } void VERectangle::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) { m_cornerFlags = 0; hideGrabbers(); setCursor(Qt::CrossCursor); QGraphicsItem::hoverLeaveEvent( event ); } void VERectangle::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { QPointF pt = event->pos(); // The current position of the mouse qreal drx = pt.x() - rect().right(); // Distance between the mouse and the right qreal dlx = pt.x() - rect().left(); // Distance between the mouse and the left qreal dby = pt.y() - rect().top(); // Distance between the mouse and the top qreal dty = pt.y() - rect().bottom(); // Distance between the mouse and the bottom // If the mouse position is within a radius of 7 // to a certain side( top, left, bottom or right) // we set the Flag in the Corner Flags Register m_cornerFlags = 0; if( dby < 7 && dby > -7 ) m_cornerFlags |= Top; // Top side if( dty < 7 && dty > -7 ) m_cornerFlags |= Bottom; // Bottom side if( drx < 7 && drx > -7 ) m_cornerFlags |= Right; // Right side if( dlx < 7 && dlx > -7 ) m_cornerFlags |= Left; // Left side if(m_actionFlags == ResizeState){ QPixmap p(":/icons/arrow-up-down.png"); QPixmap pResult; QTransform trans = transform(); switch (m_cornerFlags) { case Top: case Bottom: pResult = p.transformed(trans); setCursor(pResult.scaled(24,24,Qt::KeepAspectRatio)); break; case Left: case Right: trans.rotate(90); pResult = p.transformed(trans); setCursor(pResult.scaled(24,24,Qt::KeepAspectRatio)); break; case TopRight: case BottomLeft: trans.rotate(45); pResult = p.transformed(trans); setCursor(pResult.scaled(24,24,Qt::KeepAspectRatio)); break; case TopLeft: case BottomRight: trans.rotate(135); pResult = p.transformed(trans); setCursor(pResult.scaled(24,24,Qt::KeepAspectRatio)); break; default: setCursor(Qt::CrossCursor); break; } } else { switch (m_cornerFlags) { case TopLeft: case TopRight: case BottomLeft: case BottomRight: { QPixmap p(":/icons/rotate-right.png"); setCursor(QCursor(p.scaled(24,24,Qt::KeepAspectRatio))); break; } default: setCursor(Qt::CrossCursor); break; } } QGraphicsItem::hoverMoveEvent( event ); } QVariant VERectangle::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { switch (change) { case QGraphicsItem::ItemSelectedChange: m_actionFlags = ResizeState; break; default: break; } return QGraphicsItem::itemChange(change, value); } void VERectangle::resizeLeft(const QPointF &pt) { QRectF tmpRect = rect(); // if the mouse is on the right side we return if( pt.x() > tmpRect.right() ) return; qreal widthOffset = ( pt.x() - tmpRect.right() ); // limit the minimum width if( widthOffset > -10 ) return; // if it's negative we set it to a positive width value if( widthOffset < 0 ) tmpRect.setWidth( -widthOffset ); else tmpRect.setWidth( widthOffset ); // Since it's a left side , the rectange will increase in size // but keeps the topLeft as it was tmpRect.translate( rect().width() - tmpRect.width() , 0 ); prepareGeometryChange(); // Set the ne geometry setRect( tmpRect ); // Update to see the result update(); setPositionGrabbers(); } void VERectangle::resizeRight(const QPointF &pt) { QRectF tmpRect = rect(); if( pt.x() < tmpRect.left() ) return; qreal widthOffset = ( pt.x() - tmpRect.left() ); if( widthOffset < 10 ) /// limit return; if( widthOffset < 10) tmpRect.setWidth( -widthOffset ); else tmpRect.setWidth( widthOffset ); prepareGeometryChange(); setRect( tmpRect ); update(); setPositionGrabbers(); } void VERectangle::resizeBottom(const QPointF &pt) { QRectF tmpRect = rect(); if( pt.y() < tmpRect.top() ) return; qreal heightOffset = ( pt.y() - tmpRect.top() ); if( heightOffset < 11 ) /// limit return; if( heightOffset < 0) tmpRect.setHeight( -heightOffset ); else tmpRect.setHeight( heightOffset ); prepareGeometryChange(); setRect( tmpRect ); update(); setPositionGrabbers(); } void VERectangle::resizeTop(const QPointF &pt) { QRectF tmpRect = rect(); if( pt.y() > tmpRect.bottom() ) return; qreal heightOffset = ( pt.y() - tmpRect.bottom() ); if( heightOffset > -11 ) /// limit return; if( heightOffset < 0) tmpRect.setHeight( -heightOffset ); else tmpRect.setHeight( heightOffset ); tmpRect.translate( 0 , rect().height() - tmpRect.height() ); prepareGeometryChange(); setRect( tmpRect ); update(); setPositionGrabbers(); } void VERectangle::rotateItem(const QPointF &pt) { QRectF tmpRect = rect(); QPointF center = boundingRect().center(); QPointF corner; switch (m_cornerFlags) { case TopLeft: corner = tmpRect.topLeft(); break; case TopRight: corner = tmpRect.topRight(); break; case BottomLeft: corner = tmpRect.bottomLeft(); break; case BottomRight: corner = tmpRect.bottomRight(); break; default: break; } QLineF lineToTarget(center,corner); QLineF lineToCursor(center, pt); // Angle to Cursor and Corner Target points qreal angleToTarget = ::acos(lineToTarget.dx() / lineToTarget.length()); qreal angleToCursor = ::acos(lineToCursor.dx() / lineToCursor.length()); if (lineToTarget.dy() < 0) angleToTarget = TwoPi - angleToTarget; angleToTarget = normalizeAngle((Pi - angleToTarget) + Pi / 2); if (lineToCursor.dy() < 0) angleToCursor = TwoPi - angleToCursor; angleToCursor = normalizeAngle((Pi - angleToCursor) + Pi / 2); // Result difference angle between Corner Target point and Cursor Point auto resultAngle = angleToTarget - angleToCursor; QTransform trans = transform(); trans.translate( center.x(), center.y()); trans.rotateRadians(rotation() + resultAngle, Qt::ZAxis); trans.translate( -center.x(), -center.y()); setTransform(trans); } void VERectangle::setPositionGrabbers() { QRectF tmpRect = rect(); cornerGrabber[GrabberTop]->setPos(tmpRect.left() + tmpRect.width()/2, tmpRect.top()); cornerGrabber[GrabberBottom]->setPos(tmpRect.left() + tmpRect.width()/2, tmpRect.bottom()); cornerGrabber[GrabberLeft]->setPos(tmpRect.left(), tmpRect.top() + tmpRect.height()/2); cornerGrabber[GrabberRight]->setPos(tmpRect.right(), tmpRect.top() + tmpRect.height()/2); cornerGrabber[GrabberTopLeft]->setPos(tmpRect.topLeft().x(), tmpRect.topLeft().y()); cornerGrabber[GrabberTopRight]->setPos(tmpRect.topRight().x(), tmpRect.topRight().y()); cornerGrabber[GrabberBottomLeft]->setPos(tmpRect.bottomLeft().x(), tmpRect.bottomLeft().y()); cornerGrabber[GrabberBottomRight]->setPos(tmpRect.bottomRight().x(), tmpRect.bottomRight().y()); } void VERectangle::setVisibilityGrabbers() { cornerGrabber[GrabberTopLeft]->setVisible(true); cornerGrabber[GrabberTopRight]->setVisible(true); cornerGrabber[GrabberBottomLeft]->setVisible(true); cornerGrabber[GrabberBottomRight]->setVisible(true); if(m_actionFlags == ResizeState){ cornerGrabber[GrabberTop]->setVisible(true); cornerGrabber[GrabberBottom]->setVisible(true); cornerGrabber[GrabberLeft]->setVisible(true); cornerGrabber[GrabberRight]->setVisible(true); } else { cornerGrabber[GrabberTop]->setVisible(false); cornerGrabber[GrabberBottom]->setVisible(false); cornerGrabber[GrabberLeft]->setVisible(false); cornerGrabber[GrabberRight]->setVisible(false); } } void VERectangle::hideGrabbers() { for(int i = 0; i < 8; i++){ cornerGrabber[i]->setVisible(false); } }
Graphic scene
Working with a graphic scene combines the work of all instruments depending on the current type of instrument.
enum ActionTypes { DefaultType, LineType, RectangleType, SelectionType };
As you can see, two types of tools are used here. Two of them are a broken line and a rectangle. One tool is used to create a selection of all objects. And the first one is used for the normal cursor tool, which allows you to select objects and edit them.
veworkplace.h
#ifndef WORKPLACE_H #define WORKPLACE_H #include <QObject> #include <QGraphicsScene> class QGraphicsSceneMouseEvent; class QKeyEvent; class VEWorkplace : public QGraphicsScene { Q_OBJECT Q_PROPERTY(int currentAction READ currentAction WRITE setCurrentAction NOTIFY currentActionChanged) Q_PROPERTY(QPointF previousPosition READ previousPosition WRITE setPreviousPosition NOTIFY previousPositionChanged) public: explicit VEWorkplace(QObject *parent = 0); ~VEWorkplace(); enum ActionTypes { DefaultType, LineType, RectangleType, SelectionType }; int currentAction() const; QPointF previousPosition() const; void setCurrentAction(const int type); void setPreviousPosition(const QPointF previousPosition); signals: void previousPositionChanged(); void currentActionChanged(int); void signalSelectItem(QGraphicsItem *item); void signalNewSelectItem(QGraphicsItem *item); protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private slots: void deselectItems(); public slots: void slotMove(QGraphicsItem *signalOwner, qreal dx, qreal dy); private: QGraphicsItem *currentItem; int m_currentAction; int m_previousAction; QPointF m_previousPosition; bool m_leftMouseButtonPressed; }; #endif // WORKPLACE_H
veworkplace.cpp
An important moment when working with a graphic scene is that all graphic objects in this case simply cast to the desired type, depending on which tool we are working with at the moment.
In general, to be honest, after a year and a half I would have written differently. The fact is that the QGraphicsItem object has a virtual type() method when you override which you can return the type of the object. And this, in turn, opens up the possibility of using a single method for performing various functions. For example, to move an object or change its color, and for a variety of objects. Therefore, when you take for osonovu this project to develop your editor, then try to take this moment into account. Assigning a specified type in the form of enumeration to graphic objects can greatly simplify the development.
For example, if you take a move method with a clipped button for all these graphic objects, you could create one setEndPoint() method altogether and controlling the installation of the endpoint will greatly simplify and reduce the number, for example, here in this method.
void VEWorkplace::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { switch (m_currentAction) { case LineType: { if (m_leftMouseButtonPressed) { VEPolyline * polyline = qgraphicsitem_cast<VEPolyline *>(currentItem); QPainterPath path; path.moveTo(m_previousPosition); path.lineTo(event->scenePos()); polyline->setPath(path); } break; } case RectangleType: { if (m_leftMouseButtonPressed) { auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); VERectangle * rectangle = qgraphicsitem_cast<VERectangle *>(currentItem); rectangle->setRect((dx > 0) ? m_previousPosition.x() : event->scenePos().x(), (dy > 0) ? m_previousPosition.y() : event->scenePos().y(), qAbs(dx), qAbs(dy)); } break; } case SelectionType: { if (m_leftMouseButtonPressed) { auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); VESelectionRect * selection = qgraphicsitem_cast<VESelectionRect *>(currentItem); selection->setRect((dx > 0) ? m_previousPosition.x() : event->scenePos().x(), (dy > 0) ? m_previousPosition.y() : event->scenePos().y(), qAbs(dx), qAbs(dy)); } break; } default: { QGraphicsScene::mouseMoveEvent(event); break; } } }
Полный текст класса
#include "veworkplace.h" #include <QApplication> #include <QGraphicsSceneMouseEvent> #include <QKeyEvent> #include <QDebug> #include "verectangle.h" #include "veselectionrect.h" #include "vepolyline.h" VEWorkplace::VEWorkplace(QObject *parent) : QGraphicsScene(parent), currentItem(nullptr), m_currentAction(DefaultType), m_previousAction(0), m_leftMouseButtonPressed(false) { } VEWorkplace::~VEWorkplace() { delete currentItem; } int VEWorkplace::currentAction() const { return m_currentAction; } QPointF VEWorkplace::previousPosition() const { return m_previousPosition; } void VEWorkplace::setCurrentAction(const int type) { m_currentAction = type; emit currentActionChanged(m_currentAction); } void VEWorkplace::setPreviousPosition(const QPointF previousPosition) { if (m_previousPosition == previousPosition) return; m_previousPosition = previousPosition; emit previousPositionChanged(); } void VEWorkplace::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) { m_leftMouseButtonPressed = true; setPreviousPosition(event->scenePos()); if(QApplication::keyboardModifiers() & Qt::ShiftModifier){ m_previousAction = m_currentAction; setCurrentAction(SelectionType); } } switch (m_currentAction) { case LineType: { if (m_leftMouseButtonPressed && !(event->button() & Qt::RightButton) && !(event->button() & Qt::MiddleButton)) { deselectItems(); VEPolyline *polyline = new VEPolyline(); currentItem = polyline; addItem(currentItem); connect(polyline, &VEPolyline::clicked, this, &VEWorkplace::signalSelectItem); connect(polyline, &VEPolyline::signalMove, this, &VEWorkplace::slotMove); QPainterPath path; path.moveTo(m_previousPosition); polyline->setPath(path); emit signalNewSelectItem(polyline); } break; } case RectangleType: { if (m_leftMouseButtonPressed && !(event->button() & Qt::RightButton) && !(event->button() & Qt::MiddleButton)) { deselectItems(); VERectangle *rectangle = new VERectangle(); currentItem = rectangle; addItem(currentItem); connect(rectangle, &VERectangle::clicked, this, &VEWorkplace::signalSelectItem); connect(rectangle, &VERectangle::signalMove, this, &VEWorkplace::slotMove); emit signalNewSelectItem(rectangle); } break; } case SelectionType: { if (m_leftMouseButtonPressed && !(event->button() & Qt::RightButton) && !(event->button() & Qt::MiddleButton)) { deselectItems(); VESelectionRect *selection = new VESelectionRect(); currentItem = selection; addItem(currentItem); } break; } default: { QGraphicsScene::mousePressEvent(event); break; } } } void VEWorkplace::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { switch (m_currentAction) { case LineType: { if (m_leftMouseButtonPressed) { VEPolyline * polyline = qgraphicsitem_cast<VEPolyline *>(currentItem); QPainterPath path; path.moveTo(m_previousPosition); path.lineTo(event->scenePos()); polyline->setPath(path); } break; } case RectangleType: { if (m_leftMouseButtonPressed) { auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); VERectangle * rectangle = qgraphicsitem_cast<VERectangle *>(currentItem); rectangle->setRect((dx > 0) ? m_previousPosition.x() : event->scenePos().x(), (dy > 0) ? m_previousPosition.y() : event->scenePos().y(), qAbs(dx), qAbs(dy)); } break; } case SelectionType: { if (m_leftMouseButtonPressed) { auto dx = event->scenePos().x() - m_previousPosition.x(); auto dy = event->scenePos().y() - m_previousPosition.y(); VESelectionRect * selection = qgraphicsitem_cast<VESelectionRect *>(currentItem); selection->setRect((dx > 0) ? m_previousPosition.x() : event->scenePos().x(), (dy > 0) ? m_previousPosition.y() : event->scenePos().y(), qAbs(dx), qAbs(dy)); } break; } default: { QGraphicsScene::mouseMoveEvent(event); break; } } } void VEWorkplace::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->button() & Qt::LeftButton) m_leftMouseButtonPressed = false; switch (m_currentAction) { case LineType: case RectangleType: { if (!m_leftMouseButtonPressed && !(event->button() & Qt::RightButton) && !(event->button() & Qt::MiddleButton)) { currentItem = nullptr; } break; } case SelectionType: { if (!m_leftMouseButtonPressed && !(event->button() & Qt::RightButton) && !(event->button() & Qt::MiddleButton)) { VESelectionRect * selection = qgraphicsitem_cast<VESelectionRect *>(currentItem); if(!selection->collidingItems().isEmpty()){ foreach (QGraphicsItem *item, selection->collidingItems()) { item->setSelected(true); } } selection->deleteLater(); if(selectedItems().length() == 1){ signalSelectItem(selectedItems().at(0)); } setCurrentAction(m_previousAction); currentItem = nullptr; } break; } default: { QGraphicsScene::mouseReleaseEvent(event); break; } } } void VEWorkplace::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) { switch (m_currentAction) { case LineType: case RectangleType: case SelectionType: break; default: QGraphicsScene::mouseDoubleClickEvent(event); break; } } void VEWorkplace::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Delete: { foreach (QGraphicsItem *item, selectedItems()) { removeItem(item); delete item; } deselectItems(); break; } case Qt::Key_A: { if(QApplication::keyboardModifiers() & Qt::ControlModifier){ foreach (QGraphicsItem *item, items()) { item->setSelected(true); } if(selectedItems().length() == 1) signalSelectItem(selectedItems().at(0)); } break; } default: break; } QGraphicsScene::keyPressEvent(event); } void VEWorkplace::deselectItems() { foreach (QGraphicsItem *item, selectedItems()) { item->setSelected(false); } selectedItems().clear(); } void VEWorkplace::slotMove(QGraphicsItem *signalOwner, qreal dx, qreal dy) { foreach (QGraphicsItem *item, selectedItems()) { if(item != signalOwner) item->moveBy(dx,dy); } }
Conclusion
Going back to this code, I probably copied a lot of things here, and I would also take up my own programming business - "Code Removing" Seriously, my favorite tool in programming is the Occam razor.
But as I said before, I spent 36 hours working on developing this project with my skills a year and a half ago, and I still can not afford to spend 8-10 hours to rework the project, but we can always discuss with you the points at issue forum site.
This project also shows how much code you need to write and how much time to spend to realize even a small functionality. And personally, I'm currently working on a project in which about several thousand source files, and the build folder swells at the end of the project assembly to 12 GB, I think this project is microscopic. And nevertheless, even for the implementation of such a functional, you can spend quite a lot of time. Therefore, one of the important skills of a software developer, I think the ability to soberly assess their capabilities and anticipated deadlines, and in the process of work, do not be distracted by side tasks or tasks that are not relevant to your current assignment.
Really awesome tutorial sir, thank you very much :)
Sir could you please explain me,how can I design oval shape same as this rectangle design.what should I need to do ?
Try inherit your Oval Graphics Item from QGraphicsEllipseItem and QObject, and implement logic, which similar to logic for Rectangle.
Sir,In this form design how did you add verectanglesettings.ui,vepolylinesettings.ui UI's to this mainwindow.ui ? have any QT tool to add so. this image shows what I meaning.
You need add common QWidget to form, after that you need right-click on this QWidget in the form and select "Promote to..." in Context Menu. After that You will see dialog. In dialog choose Base class name, write promoted class name and header file. After that click add and promote
Thank you very much !!!
As far as I can see your classes inherit from both QObject and QGraphics*. QGraphics* are already QObjects and multiple inheritance from QObjects lead to some problems with moc.
If you see sources of class QGraphicsObject or if you see documentation of QGraphicsObject, then you will understand, that it is not problem in this case, because of QGraphicsRectItem inherits from QGraphicsItem , which has not inheritance from QObject. You have invalid information about inheritance of these classes.
In there you design rectangle using mouse,how to design a rectangle when button click...
Is this works only windows OS ?
It should work on Windows, Mac OS and Linux. Because it not contains platform-dependent components.
доброго времени,
большое спасибо за пример для начинающего)
при адаптации к своему проекту столкнулся с таким ньансом:
в vepolyline.h в 47 строке нужна инициализация по умолчанию: int m_pointForCheck = -1;
у меня в проекте если нажать на дот, не двигать и отпустить, то в эту переменную попадает случайное число
Добрый день! Спасибо за комментарий. Там действительно лучше будет сделать с инициализацией по умолчанию.
thanks for the application, it helps me a lot