Qt 场景(QGraphicsScene)自绘可自由变换与移动的图元(QGraphicsObject)

效果图

在这里插入图片描述

  • scene上绘制一个图元QGraphicsObject的矩形,可以自由拖动且拖动四个角可以自由变换矩形需要如下处理。

矩形四个角

  1. 四个角的点需要独立处理继承于QGraphicsObject,当我们点击时拖动时发送信号给矩形,进行矩形变换。
  • 变量
public:
    /// @brief 点位
    int pointIndex;
    /// @brief 偏移位
    QPointF pressPoint;
RectanglePoint::RectanglePoint(ICanvasScene* canvasScene, int index, QGraphicsItem* parent)
    : QGraphicsObject(parent), pointIndex(index)
{
    setAcceptHoverEvents(true);
}

RectanglePoint::~RectanglePoint() { }

QList<RectanglePoint*> RectanglePoint::createRectanglePoints(ICanvasScene* canvasScene, QPolygonF& cachePoints)
{
    QList<RectanglePoint*> result;
    if (nullptr == canvasScene) {
        return result;
    }
    for (int i = 0; i < cachePoints.size(); ++i) {
        RectanglePoint* point = new RectanglePoint(canvasScene, i);
        if (point) {
            result.append(point);
        }
    }
    return result;
}

QRectF RectanglePoint::boundingRect() const
{
    return QRectF(-controlSize, -controlSize, controlSize * 2, controlSize * 2);
}

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

void RectanglePoint::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    if (event->button() != Qt::LeftButton) {
        return GraphicsLayer::mousePressEvent(event);
    }
    event->accept();
    pressPoint = event->scenePos();
    emit positionChanged(QPointF(), pointIndex, startedChanged);
}

void RectanglePoint::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
    if (!event->buttons().testFlag(Qt::LeftButton)) {
        return GraphicsLayer::mouseMoveEvent(event);
    }
    QPointF startP = pressPoint;
    QPointF offset = event->scenePos() - startP;
    event->accept();
    emit positionChanged(offset, pointIndex, inChanged);
}

void RectanglePoint::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
    if (event->button() != Qt::LeftButton) {
        return GraphicsLayer::mouseReleaseEvent(event);
    }
    QPointF startP = pressPoint;
    QPointF offset = event->scenePos() - startP;
    event->accept();
    emit positionChanged(offset, pointIndex, finishedChanged);
}
  • createRectanglePoints函数是静态函数,矩形生成时会调用,并传入四个点位。
  • positionChanged的第三个参数为当前操作的状态的枚举(鼠标点击,移动与释放)

矩形

  • 变量
private:
    /// @brief 当前矩形大小
    QRectF mboundingRect;
    /// @brief 矩形的四个点
    QPolygonF cachePoints;
    /// @brief 变换的点位
    QPolygonF startChangePoints;
    /// @brief 四个角的类
    QList<RectanglePoint*> rectanglePoints;
const qreal controlSize = 5.0;
GisGridCustomRectItem::GisGridCustomRectItem(QRectF boundingRect, ICanvasScene* canvasScene, QGraphicsItem* parent)
    : QGraphicsObject(parent),
      mboundingRect(boundingRect)
{
    setFlags(QGraphicsObject::ItemIsMovable | QGraphicsObject::ItemIsSelectable
             | QGraphicsObject::ItemSendsGeometryChanges); // 允许移动
    cachePoints << boundingRect.topLeft() << boundingRect.topRight() << boundingRect.bottomRight()
                << boundingRect.bottomLeft();

    rectanglePoints = RectanglePoint::createRectanglePoints(canvasScene, cachePoints);
    QListIterator<RectanglePoint*> RectanglePointIter(rectanglePoints);
    while (RectanglePointIter.hasNext()) {
        RectanglePoint* RectanglePointGraph = RectanglePointIter.next();
        if (RectanglePointGraph) {
            RectanglePointGraph->setParentItem(this);
            connect(RectanglePointGraph, &RectanglePoint::positionChanged, this,
                    &GisGridCustomRectItem::onRectPositionChange);
            RectanglePointGraph->setZValue(RectanglePointGraph->zValue() + 1);
        }
    }
    refreshPointPoistion();
}

GisGridCustomRectItem::~GisGridCustomRectItem() { }

void GisGridCustomRectItem::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*)
{
    painter->save();
    QPen pen;
    painter->setBrush(QColor(230, 230, 230, 100));
    if (isSelected()) {
        pen.setColor(QColor(0x2E9FE6));
    }
    painter->setPen(pen);
    QPainterPath path;
    path.addPolygon(cachePoints);
    path.closeSubpath();
    painter->drawPath(path);

    if (isSelected()) {
        drawPointsRect(painter);
    }
    painter->restore();
}

QRectF GisGridCustomRectItem::boundingRect() const
{
    return mboundingRect;
    return shape().controlPointRect();
}

QPainterPath GisGridCustomRectItem::shape() const
{
    return QGraphicsObject::shape();
    QPainterPath path;
    path.addPolygon(cachePoints);
    path.closeSubpath();

    QPainterPathStroker stroker;
    stroker.setWidth(15);
    path = stroker.createStroke(path);
    return path;
}

QVariant GisGridCustomRectItem::itemChange(GraphicsItemChange change, const QVariant& value)
{
    // if (change == QGraphicsItem::ItemPositionChange && scene()) {
    //     return Utility::pointAlignmentToGrid(value.toPointF(), 10);
    // }
    return QGraphicsObject::itemChange(change, value);
}

void GisGridCustomRectItem::drawPointsRect(QPainter* painter)
{
    double arrowLength = 7; // 箭头长度
    // 四个角落上的箭头,一个角绘制两条箭头构成一个双向箭头
    // 计算45度角(PI/4 弧度)和225度角(5*PI/4 弧度)
    double angle45 = M_PI / 4;
    double angle225 = 5 * M_PI / 4;

    // 左上角的双向箭头
    drawSingleArrow(painter, cachePoints[0], angle45, arrowLength);  // 对角向右下
    drawSingleArrow(painter, cachePoints[0], angle225, arrowLength); // 对角向左上

    // 右上角的双向箭头
    drawSingleArrow(painter, cachePoints[3], -angle45, arrowLength);       // 对角向左下
    drawSingleArrow(painter, cachePoints[3], M_PI - angle45, arrowLength); // 对角向右上

    // 左下角的双向箭头
    drawSingleArrow(painter, cachePoints[1], M_PI - angle45, arrowLength); // 对角向左下
    drawSingleArrow(painter, cachePoints[1], -angle45, arrowLength);       // 对角向右上

    // 右下角的双向箭头
    drawSingleArrow(painter, cachePoints[2], angle225 - M_PI, arrowLength); // 对角向右上
    drawSingleArrow(painter, cachePoints[2], angle225, arrowLength);        // 对角向左下
}

void GisGridCustomRectItem::onRectPositionChange(QPointF delta, int index, itemRectChange state)
{
    ICanvasScene* canvasScene = getCanvasScene();
    if (!canvasScene) {
        return;
    }
    ICanvasView* canvasView = canvasScene->getCanvasView();
    if (!canvasView) {
        return;
    }
    auto interactioMode = (InteractionMode)canvasView->getInteractionMode();
    if (interactioMode == kAreaAmplification) {
        return;
    }

    switch (state) {
    case startedChanged:
        startChangePoints = cachePoints;
        break;
    case inChanged:
        changeCachePoints(delta, index);
        break;
    case finishedChanged:
        changeCachePoints(delta, index);
        refreshPointPoistion();
    default:
        break;
    }
    update();
}

void GisGridCustomRectItem::changeCachePoints(QPointF delta, int index)
{
    QPolygonF pointF;
    cachePoints[index] = startChangePoints[index] + delta;
    if (index == 1 || index == 3) {
        pointF << cachePoints[1] << cachePoints[3];
    } else {
        pointF << cachePoints[0] << cachePoints[2];
    }
    QPointF p1 = pointF[0];
    QPointF p2 = pointF[1];
    QPointF p3(p2.x(), p1.y());
    QPointF p4(p1.x(), p2.y());
    pointF.clear();
    if (index == 1 || index == 3) {
        pointF << p3 << p1 << p4 << p2;
    } else {
        pointF << p1 << p3 << p2 << p4;
    }
    cachePoints = pointF;

    // 更新矩形大小
    double minX = std::min({ p1.x(), p2.x(), p3.x(), p4.x() });
    double minY = std::min({ p1.y(), p2.y(), p3.y(), p4.y() });
    double maxX = std::max({ p1.x(), p2.x(), p3.x(), p4.x() });
    double maxY = std::max({ p1.y(), p2.y(), p3.y(), p4.y() });
    QRectF rect(minX, minY, maxX - minX, maxY - minY);
    mboundingRect = rect;
}

void GisGridCustomRectItem::drawSingleArrow(QPainter* painter, QPointF basePoint, double angle, double length)
{
    // 计算箭头的末端点
    QPointF endPoint(basePoint.x() + length * qCos(angle), basePoint.y() + length * qSin(angle));

    // 绘制箭头的主线
    painter->drawLine(basePoint, endPoint);

    // 绘制箭头的两个小边,构成箭头尖端
    double arrowHeadAngle = M_PI / 5; // 箭头头部的夹角
    double arrowHeadLength = 5;       // 箭头头部长度

    QPointF arrowLeft(endPoint.x() - arrowHeadLength * qCos(angle - arrowHeadAngle),
                      endPoint.y() - arrowHeadLength * qSin(angle - arrowHeadAngle));

    QPointF arrowRight(endPoint.x() - arrowHeadLength * qCos(angle + arrowHeadAngle),
                       endPoint.y() - arrowHeadLength * qSin(angle + arrowHeadAngle));

    painter->drawLine(endPoint, arrowLeft);
    painter->drawLine(endPoint, arrowRight);
}

void GisGridCustomRectItem::refreshPointPoistion()
{
    QListIterator<RectanglePoint*> rectanglePointsIter(rectanglePoints);
    while (rectanglePointsIter.hasNext()) {
        RectanglePoint* ponit = rectanglePointsIter.next();
        if (ponit) {
            ponit->setPos(cachePoints[ponit->pointIndex]);
        }
    }
}
  • 四个角的双向箭头挺难画的,可以直接画一个简单的圆形就行。
  • 对于移动操作而言,应该设置ItemSendsGeometryChanges标志,这允许项目在几何变化时(如位置改变)发送通知。如果没有设置这个标志,项目在移动时将不会调用itemChange函数。
  • 拖动矩形有俩种处理形式(看shapeboundingRect函数)
    1. 点击边框拖动,这个需要计算shape路径函数,得到矩形的边框。
    2. 点击矩形内部拖动,这个需要再每次变换后更新一下矩形。
  • 矩形的变换关键在于changeCachePoints函数中的处理,其实一个矩形只需要俩个点,另外俩个点根据这俩个点变换,不然你拖动起来可能不是一个矩形了。
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值