作者:billy
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
前言
前面的章节介绍了 Graphics View 绘图架构,终于到实战了,真的是千呼万唤始出来!这一章节就用 Graphics View 绘图架构来做一个绘图工具,实现一些基础图形的绘制,废话不多说先来看一下结果演示:
图形绘制介绍
我们知道简单的几何图形,只要确定两个点就可以完成绘制。比如要画一个圆,一个点可以定为圆心,由另一个点则可以计算出到圆心的距离,即圆的为半径,确定了圆心和半径那么这个圆就完成了。
同样的道理,椭圆、正方形、矩形、圆端矩形,也可以由一个中心点和一个可拖动来改变图形的形状和大小的点(这里我们管这个点叫边缘点)来确定,边缘点的坐标可以直接用来确定图形的宽度和高度。
而在饼、和弦中,边缘点不仅仅是用来决定图形的宽度和高度,还需要用来确定角度(与X轴正方向的夹角)。
最麻烦的是多边形,因为每点击一下就需要绘制一个点并且完成连线,所以需要把每个点的坐标从场景传递到图形,然后在图形中完成绘制。
自定义功能图元 - 点
点依附于图形之上,所以构造函数中需要传入一个图形,代表这个点在这个图形上,并且需要初始化点的坐标和类型;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class BPointItem : public QObject, public QAbstractGraphicsShapeItem { Q_OBJECT public: enum PointType { Center = 0, // 中心点 Edge, // 边缘点(可拖动改变图形的形状、大小) Special // 特殊功能点 }; BPointItem(QAbstractGraphicsShapeItem* parent, QPointF p, PointType type); QPointF getPoint() { return m_point; } void setPoint(QPointF p) { m_point = p; } protected: virtual QRectF boundingRect() const override; virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; private: QPointF m_point; PointType m_type; }; |
根据类型不同,绘制的形状也不同。中心点是一个圆,并且光标是 OpenHandCursor,而其他类型的点则是一个正方形,光标是 PointingHandCursor;
当我们移动点的时候,如果是中心点则会整个图形一起移动,而移动边缘点的时候会根据边缘点的坐标来动态改变图形的形状,这里代码比较多就不贴了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | BPointItem::BPointItem(QAbstractGraphicsShapeItem* parent, QPointF p, PointType type) : QAbstractGraphicsShapeItem(parent) , m_point(p) , m_type(type) { this->setPos(m_point); this->setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsFocusable); switch (type) { case Center: this->setCursor(Qt::OpenHandCursor); break; case Edge: this->setCursor(Qt::PointingHandCursor); break; case Special: this->setCursor(Qt::PointingHandCursor); break; default: break; } } QRectF BPointItem::boundingRect() const { return QRectF(-4, -4, 8, 8); } void BPointItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->setPen(this->pen()); painter->setBrush(this->brush()); this->setPos(m_point); switch (m_type) { case Center: painter->drawEllipse(-4, -4, 8, 8); break; case Edge: painter->drawRect(QRectF(-4, -4, 8, 8)); break; case Special: painter->drawRect(QRectF(-4, -4, 8, 8)); break; default: break; } } void BPointItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if ( event->buttons() == Qt::LeftButton ) { qreal dx = event->scenePos().x() - event->lastScenePos().x(); qreal dy = event->scenePos().y() - event->lastScenePos().y(); BGraphicsItem* item = static_cast<BGraphicsItem *>(this->parentItem()); BGraphicsItem::ItemType itemType = item->getType(); switch (m_type) { case Center: { item->moveBy(dx, dy); this->scene()->update(); } break; case Edge: { 略... } break; case Special: { 略... } break; default: break; } } } |
自定义图元基类
定义了一个图元基类,每一个图元都至少需要一个中心点、一个边缘点和图元类型。重写了焦点事件,当获得焦点时会改变颜色;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | class BGraphicsItem : public QObject, public QAbstractGraphicsShapeItem { Q_OBJECT public: enum ItemType { Circle = 0, // 圆 Ellipse, // 椭圆 Concentric_Circle, // 同心圆 Pie, // 饼 Chord, // 和弦 Rectangle, // 矩形 Square, // 正方形 Polygon, // 多边形 Round_End_Rectangle,// 圆端矩形 Rounded_Rectangle // 圆角矩形 }; QPointF getCenter() { return m_center; } void setCenter(QPointF p) { m_center = p; } QPointF getEdge() { return m_edge; } void setEdge(QPointF p) { m_edge = p; } ItemType getType() { return m_type; } protected: BGraphicsItem(QPointF center, QPointF edge, ItemType type); virtual void focusInEvent(QFocusEvent *event) override; virtual void focusOutEvent(QFocusEvent *event) override; protected: QPointF m_center; QPointF m_edge; ItemType m_type; BPointItemList m_pointList; QPen m_pen_isSelected; QPen m_pen_noSelected; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | BGraphicsItem::BGraphicsItem(QPointF center, QPointF edge, ItemType type) : m_center(center), m_edge(edge), m_type(type) { m_pen_noSelected.setColor(QColor(0, 160, 230)); m_pen_noSelected.setWidth(2); m_pen_isSelected.setColor(QColor(255, 0, 255)); m_pen_isSelected.setWidth(2); this->setPen(m_pen_noSelected); this->setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsFocusable); } void BGraphicsItem::focusInEvent(QFocusEvent *event) { Q_UNUSED(event); this->setPen(m_pen_isSelected); } void BGraphicsItem::focusOutEvent(QFocusEvent *event) { Q_UNUSED(event); this->setPen(m_pen_noSelected); } |
矩形
以矩形为例来介绍一下图元,构造函数参数为中心点坐标和矩形的宽、高、类型。初始化中心点和边缘点之后即可绘制出矩形,重写 paint 函数把矩形的宽和高绑定到 m_edge 的坐标,矩形的大小就由边缘点的坐标决定,当我们拖到边缘点时只需更新 m_edge 的坐标,矩形的形状就能动态改变;
1 2 3 4 5 6 7 8 9 10 11 12 | class BRectangle : public BGraphicsItem { public: BRectangle(qreal x, qreal y, qreal width, qreal height, ItemType type); protected: virtual QRectF boundingRect() const override; virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | BRectangle::BRectangle(qreal x, qreal y, qreal width, qreal height, ItemType type) : BGraphicsItem(QPointF(x,y), QPointF(width/2,height/2), type) { BPointItem *point = new BPointItem(this, m_edge, BPointItem::Edge); point->setParentItem(this); m_pointList.append(point); m_pointList.append(new BPointItem(this, m_center, BPointItem::Center)); m_pointList.setRandColor(); } QRectF BRectangle::boundingRect() const { return QRectF(m_center.x() - m_edge.x(), m_center.y() - m_edge.y(), m_edge.x() * 2, m_edge.y() * 2); } void BRectangle::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->setPen(this->pen()); painter->setBrush(this->brush()); QRectF ret(m_center.x() - m_edge.x(), m_center.y() - m_edge.y(), m_edge.x() * 2, m_edge.y() * 2); painter->drawRect(ret); } |
自定义场景
当我们绘制多边形时,需要每点击一下绘制点和连线。此时我们的鼠标点击事件是在场景中的,而我们需要把场景中的坐标传递到图元中,所以我们需要自定义场景来重写场景中的鼠标事件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class BQGraphicsScene : public QGraphicsScene { Q_OBJECT public: BQGraphicsScene(QObject *parent = nullptr); void startCreate(); protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent *event); signals: void updatePoint(QPointF p, QList<QPointF> list, bool isCenter); void createFinished(); protected: QList<QPointF> m_list; bool is_creating_BPolygon; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | BQGraphicsScene::BQGraphicsScene(QObject *parent) : QGraphicsScene(parent) { is_creating_BPolygon = false; } void BQGraphicsScene::startCreate() { is_creating_BPolygon = true; m_list.clear(); } void BQGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (is_creating_BPolygon) { QPointF p(event->scenePos().x(), event->scenePos().y()); switch ( event->buttons() ) { case Qt::LeftButton: { m_list.push_back(p); emit updatePoint(p, m_list, false); } break; case Qt::RightButton: { if (m_list.size() >= 3) { emit updatePoint(p, m_list, true); emit createFinished(); is_creating_BPolygon = false; m_list.clear(); } } break; default: break; } } else { QGraphicsScene::mousePressEvent(event); } } |
1 2 3 4 5 6 7 8 9 10 11 12 | void MainWindow::on_polygonBtn_clicked() { m_scene.startCreate(); setBtnEnabled(false); BPolygon *m_polygon = new BPolygon(BGraphicsItem::ItemType::Polygon); m_scene.addItem(m_polygon); connect(&m_scene, SIGNAL(updatePoint(QPointF, QList<QPointF>, bool)), m_polygon, SLOT(pushPoint(QPointF, QList<QPointF>, bool))); connect(&m_scene, &BQGraphicsScene::createFinished, [=](){ setBtnEnabled(true); }); } |
右键弹窗修改属性
右键跳出弹窗可以重写 contextMenuEvent 函数来完成,弹窗中需要哪些控件可以自定义实现;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | void BEllipse::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { if ( !this->isSelected() ) return; QMenu* menu = new QMenu(); menu->setStyleSheet("QMenu { background-color:rgb(89,87,87); border: 5px solid rgb(235,110,36); }"); QSpinBox* width_spinBox = new QSpinBox(menu); width_spinBox->setStyleSheet("QSpinBox{ width:120px; height:30px;}"); width_spinBox->setRange(0, 1000); width_spinBox->setPrefix("w: "); width_spinBox->setSuffix(" mm"); width_spinBox->setSingleStep(1); width_spinBox->setValue(2 * abs(m_edge.x())); connect(width_spinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [=](int v){ m_edge.setX(v/2); m_pointList.at(0)->setPoint(m_edge); this->hide(); this->update(); this->show(); }); QSpinBox* height__spinBox = new QSpinBox(menu); height__spinBox->setStyleSheet("QSpinBox{ width:120px; height:30px;}"); height__spinBox->setRange(0, 1000); height__spinBox->setPrefix("h: "); height__spinBox->setSuffix(" mm"); height__spinBox->setSingleStep(1); height__spinBox->setValue(2 * abs(m_edge.y())); connect(height__spinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [=](int v){ m_edge.setY(v/2); m_pointList.at(0)->setPoint(m_edge); this->hide(); this->update(); this->show(); }); QWidgetAction* width_widgetAction = new QWidgetAction(menu); width_widgetAction->setDefaultWidget(width_spinBox); menu->addAction(width_widgetAction); QWidgetAction* height_widgetAction = new QWidgetAction(menu); height_widgetAction->setDefaultWidget(height__spinBox); menu->addAction(height_widgetAction); menu->exec(QCursor::pos()); delete menu; QGraphicsItem::contextMenuEvent(event); } |
更多请参考
- Qt之QGraphicsView入门篇
- Qt之QGraphicsView进阶篇