你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

自定义QGraphicsItem的选择、缩放、移动、旋转

2021/12/27 9:08:06

文章目录

  • 一、效果展示
    • 1.添加选择图元
    • 2.拖拽移动图元
    • 3.旋转图元
  • 二、实现步骤
    • 1.自定义图元
    • 2.图元移动
    • 3.图元缩放
    • 4.图元旋转
  • 提示


一、效果展示

1.添加选择图元

在这里插入图片描述

2.拖拽移动图元

在这里插入图片描述

3.旋转图元

在这里插入图片描述

二、实现步骤

1.自定义图元

对于自定义控件最重要的是需要实现boundingRect和paint两个函数,前者用于限制图元的绘制区域,后者用于实际图元的绘制。
考虑图元被选中时出现操作外框,因此paint函数内需要根据图元的选中状态进行区别绘制。

主要代码如下(示例):

AnchorItem.h文件内部分

public:
    AnchorItem(QRectF rect=QRectF(0,0,200,200), QPixmap map=QPixmap(":/icons/circle_green.png"));
    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
private:
    QRectF getItemRect() const;   //计算Item本身的尺寸
    void ItemResize();
    
   qreal m_ItemRatio;  //Item的横纵比
    int m_Internal=15;  //外框矩形距Item的pad
    int m_EllipseWidth=30;  //四角缩放控制点图像直径
    int m_LineLen = 30;   //旋转点距离外框矩形的距离
    int m_RotateEllipseWidth = 20;   //旋转点图像直径
    QPixmap m_RotatePixmap;   //存放旋转点图标
    QPixmap m_ScalePixmap;    //存放缩放点图标
    QPixmap m_ItemPixmap;     //存放Item图标
    //记录Item本身的尺寸大小
    QSizeF m_ItemSize;
    //存放Item选中时四个缩放控制点和旋转控制点的绘制位置
    QRectF m_TopLeftRect, m_TopRightRect, m_BottomLeftRect, m_BottomRightRect, m_RotateRect;
    //鼠标悬浮在旋转控制点时的光标样式
    QCursor m_RotateHoverCursor;
    //鼠标点击拖动旋转控制点时的光标样式
    QCursor m_RotatePressCursor;
    //记录Item添至Scene时,Item左上角在Scene的位置
    QPointF m_CurrentScenePos;

AnchorItem.cpp文件内部分

QRectF AnchorItem::getItemRect() const
{
    QPointF centerPos(0,0);
    return QRectF(centerPos.x() - this->m_ItemSize.width()/2, centerPos.y() - this->m_ItemSize.height()/2, this->m_ItemSize.width(), this->m_ItemSize.height());
}

AnchorItem::AnchorItem(QRectF rect, QPixmap map)
{
    this->m_ItemPixmap = map;
    this->m_ItemSize = QSizeF(rect.width(),rect.height());
    this->m_CurrentScenePos = rect.topLeft();
    this->m_ItemRatio = this->m_ItemSize.width()/this->m_ItemSize.height();
    this->setFlag(QGraphicsItem::ItemIsFocusable);
    this->setFlag(QGraphicsItem::ItemIsSelectable);
    this->setAcceptHoverEvents(true);        
    this->m_ScalePixmap = QPixmap(":/icons/circle_grey.png");
    this->m_RotatePixmap = QPixmap(":/icons/rotate.png");
    this->m_RotateHoverCursor  = QCursor(QPixmap(":/icons/rotate_hover.png").scaled(32,32,Qt::IgnoreAspectRatio,Qt::SmoothTransformation));
    this->m_RotatePressCursor = QCursor(QPixmap(":/icons/rotate_press.png").scaled(32,32,Qt::IgnoreAspectRatio,Qt::SmoothTransformation));
    ItemResize();
}

void AnchorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setRenderHint(QPainter::SmoothPixmapTransform, true);

    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::NoBrush);
    painter->drawPixmap(this->getItemRect(), this->m_ItemPixmap, this->m_ItemPixmap.rect());

    //当Item是选中状态时,绘制外框和控制点图像
    if (this->isSelected())
    {
        QRectF rect = this->getItemRect();
        const qreal penWidth = 0;
        // 边框区域颜色
        QColor color = QColor(Qt::gray);
        // 绘制虚线
        painter->setPen(QPen(color, penWidth, Qt::DashLine));
        painter->setBrush(Qt::NoBrush);

        rect.adjust(-this->m_Internal, -this->m_Internal, this->m_Internal, this->m_Internal);
        painter->drawRect(rect);

        painter->drawLine(QLineF(QPointF(rect.left() + rect.width()/2, rect.bottom()), QPointF(rect.left() + rect.width()/2,rect.bottom()+this->m_LineLen)));

        painter->setPen(Qt::NoPen);
        //top-left
        painter->drawPixmap(this->m_TopLeftRect,this->m_ScalePixmap, this->m_ScalePixmap.rect());
        //bottom-left
        painter->drawPixmap(this->m_BottomLeftRect,this->m_ScalePixmap, this->m_ScalePixmap.rect());
        //top-right
        painter->drawPixmap(this->m_TopRightRect,this->m_ScalePixmap, this->m_ScalePixmap.rect());
        //bottom-left
        painter->drawPixmap(this->m_BottomRightRect,this->m_ScalePixmap, this->m_ScalePixmap.rect());

        painter->drawPixmap(this->m_RotateRect, this->m_RotatePixmap, this->m_RotatePixmap.rect());
    }
}

void AnchorItem::ItemResize()
{
    QRectF rect = this->getItemRect();
    rect.adjust(-this->m_Internal, -this->m_Internal, this->m_Internal, this->m_Internal);
    this->m_TopLeftRect = QRectF(rect.x()-this->m_EllipseWidth/2, rect.y()-this->m_EllipseWidth/2, this->m_EllipseWidth, this->m_EllipseWidth);
    this->m_BottomLeftRect = QRectF(rect.x()-this->m_EllipseWidth/2, rect.bottomLeft().y()-this->m_EllipseWidth/2, this->m_EllipseWidth, this->m_EllipseWidth);
    this->m_TopRightRect = QRectF(rect.topRight().x()-this->m_EllipseWidth/2, rect.y()-this->m_EllipseWidth/2, this->m_EllipseWidth, this->m_EllipseWidth);
    this->m_BottomRightRect = QRectF(rect.bottomRight().x()-this->m_EllipseWidth/2, rect.bottomRight().y()-this->m_EllipseWidth/2, this->m_EllipseWidth, this->m_EllipseWidth);
    this->m_RotateRect = QRectF(rect.left()+rect.width()/2-this->m_RotateEllipseWidth/2,rect.bottom()+this->m_LineLen, this->m_RotateEllipseWidth, this->m_RotateEllipseWidth);
}

2.图元移动

图元移动主要是重写鼠标操作事件的函数,主要如下:

    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;

图元移动的功能实现的主要分三个步骤:
1.判断鼠标的位置,如果鼠标光标落在Item中心区域内,则改变光标形态为手掌;
2.当鼠标进行按下操作时,根据当前光标形态判断此时鼠标按下将要进行移动操作,并记录下鼠标按下位置对应在场景中的坐标,并改变光标形态为拳头形状;
3.根据鼠标移动,获取鼠标移动位置对应的场景坐标,通过setPos操作改变Item在场景中的位置,实现Item的移动。
主要实现的示例代码如下

QPointF m_PressScenePos;   //存放鼠标按下时,按下点在Scene对应的位置
int m_Operation;    //存放鼠标按下时的图元操作类型
enum ItemOperation{
    ITEM_NONE = 0x00,
    ITEM_MOVE = 0x01,
    ITEM_RESIZE = 0x02,
    ITEM_TOTATE = 0x03
};
void AnchorItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    this->setFocus();
    if(this->m_CursorShape == HAND_OPEN_CURSOR)
    {
        this->m_PressScenePos = event->scenePos();
        this->m_CursorShape = HAND_CLOSE_CURSOR;
        this->setCursor(Qt::ClosedHandCursor);
        this->m_Operation = ITEM_MOVE;
    }    
    QGraphicsItem::mousePressEvent(event);
}

void AnchorItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(this->m_Operation == ITEM_MOVE)
    {
        this->setPos(this->m_CurrentScenePos + event->scenePos() - this->m_PressScenePos);
        this->update();
    }
}

void AnchorItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    this->clearFocus();
    this->setCursor(Qt::ArrowCursor);
    this->m_CursorShape = ARROR_CURSOR;
    this->m_Operation = ITEM_NONE;
    this->m_CurrentScenePos = this->pos();
    QGraphicsItem::mouseReleaseEvent(event);
}

void AnchorItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
    if(!this->isSelected())
    {
        this->m_CursorShape = HAND_OPEN_CURSOR;
        this->setCursor(Qt::OpenHandCursor);
     }else{
        QPointF pos = event->pos();
        QRectF rect = this->getItemRect();
        rect.adjust(-this->m_Internal, -this->m_Internal, this->m_Internal, this->m_Internal);
        if(rect.contains(pos))
        {
            this->m_CursorShape = HAND_OPEN_CURSOR;
            this->setCursor(Qt::OpenHandCursor);
            this->setToolTip(QString("移动"));
        }
    }
}

3.图元缩放

图元的缩放同样需要重写上述鼠标事件函数。其功能实现的主要有以下步骤:
1.判断鼠标的位置,如果鼠标落在四个旋转点区域内,则改变光标形态为缩放形状;
2.当鼠标进行按下操作时,根据当前光标形态判断此时鼠标按下将要缩放操作,并根据鼠标移动的位置来计算图元缩放的方向和大小,并通过控制m_ItemSize的控制,对缩放后的图元进行重绘;

QPointF m_PressItemPos;    //记录鼠标按下时,按下点在Item的位置
void AnchorItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
    if(this->isSelected())
    {
        QPointF pos = event->pos();
        QRectF rect = this->getItemRect();
        qreal angle = qAtan2(this->transform().m12(), this->transform().m11());
        QMarginsF pad = QMarginsF(1,1,1,1);
        rect.adjust(-this->m_Internal, -this->m_Internal, this->m_Internal, this->m_Internal);
        if((this->m_TopLeftRect+pad).contains(pos))
        {
            this->m_CursorShape = RESIZE_TOPLEFT_CURSOR;
            angle = this->m_TopLeftAngle  - angle;
//            qDebug()<<qRadiansToDegrees(angle);
            this->setCursor(GetResizeCursorShape(angle));
        }
        else if((this->m_BottomRightRect+pad).contains(pos))
        {
            this->m_CursorShape = RESIZE_BOTTOMRIGHT_CURSOR;
            angle = this->m_BottomRightAngle - angle;
//            qDebug()<<qRadiansToDegrees(angle);
            this->setCursor(GetResizeCursorShape(angle));
        }else if((this->m_TopRightRect+pad).contains(pos))
        {
            this->m_CursorShape = RESIZE_TOPRIGHT_CURSOR;
            angle = this->m_TopRightAngle - angle;
//            qDebug()<<qRadiansToDegrees(angle);
            this->setCursor(GetResizeCursorShape(angle));
        }else if((this->m_BottomLeftRect+pad).contains(pos))
        {
            this->m_CursorShape = RESIZE_BOTTOMLEFT_CURSOR;
            angle = this->m_BottomLeftAngle - angle;
//            qDebug()<<qRadiansToDegrees(angle);
            this->setCursor(GetResizeCursorShape(angle));
        }
    }
    QGraphicsItem::hoverMoveEvent(event);
}
//根据角度计算旋转点位置鼠标光标的形态
Qt::CursorShape AnchorItem::GetResizeCursorShape(qreal angle)
{
    qreal sector  = M_PI_4;
    qreal value = angle + sector/2;

    qreal theta = fmod(value, M_PI);
    if(theta<0)
    {
        theta += M_PI;
    }
    int index = static_cast<int>(floor(theta/sector));
    switch (index) {
    case 0: return Qt::SizeHorCursor;
    case 1: return Qt::SizeBDiagCursor;
    case 2: return Qt::SizeVerCursor;
    case 3: return Qt::SizeFDiagCursor;
    default: return Qt::ArrowCursor;
    }
}
void AnchorItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
	if((this->m_CursorShape == RESIZE_TOPLEFT_CURSOR) | (this->m_CursorShape == RESIZE_TOPRIGHT_CURSOR) | \
            (this->m_CursorShape == RESIZE_BOTTOMLEFT_CURSOR) | (this->m_CursorShape == RESIZE_BOTTOMRIGHT_CURSOR))
    {
        this->m_PressItemPos = event->pos();
        this->m_Operation = ITEM_RESIZE;
    }
    QGraphicsItem::mousePressEvent(event);
}

void AnchorItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
	if(this->m_Operation == ITEM_RESIZE)
	{
		QPointF diff = event->pos() - this->m_PressItemPos;
        this->m_PressItemPos = event->pos();
        qreal width, height;
        //根据鼠标按下时光标的类型来判断Item的缩放操作在Item哪个角,并据此进行不同缩放尺度的计算
        if(abs(diff.x())>=abs(diff.y()))
        {
            switch (this->m_CursorShape) {
            case RESIZE_TOPLEFT_CURSOR:
            {
                width = this->m_ItemSize.width()-2*diff.x();
                height = width/this->m_ItemRatio;
            }
                break;
            case RESIZE_TOPRIGHT_CURSOR:
            {
                width = this->m_ItemSize.width()+2*diff.x();
                height = width/this->m_ItemRatio;
            }
                break;
            case RESIZE_BOTTOMLEFT_CURSOR:
            {
                width = this->m_ItemSize.width()-2*diff.x();
                height = width/this->m_ItemRatio;
            }
                break;
            case RESIZE_BOTTOMRIGHT_CURSOR:
            {
                width = this->m_ItemSize.width()+2*diff.x();
                height = width/this->m_ItemRatio;
            }
                break;
            default:
            {
                width = this->m_ItemSize.width();
                height = this->m_ItemSize.height();
            }
                break;
            }
        }else{
            switch (this->m_CursorShape) {
            case RESIZE_TOPLEFT_CURSOR:
            {
                height = this->m_ItemSize.height()-2*diff.y();
                width = height*this->m_ItemRatio;
            }
                break;
            case RESIZE_TOPRIGHT_CURSOR:
            {
                height = this->m_ItemSize.height()-2*diff.y();
                width = height*this->m_ItemRatio;
            }
                break;
            case RESIZE_BOTTOMLEFT_CURSOR:
            {
                height = this->m_ItemSize.height()+2*diff.y();
                width = height*this->m_ItemRatio;
            }
                break;
            case RESIZE_BOTTOMRIGHT_CURSOR:
            {
                height = this->m_ItemSize.height()+2*diff.y();
                width = height*this->m_ItemRatio;
            }
                break;
            default:
            {
                width = this->m_ItemSize.width();
                height = this->m_ItemSize.height();
            }
                break;
            }
        }
        if(width<50)
        {
            width = 50;
            height = width/this->m_ItemRatio;
        }else if(width>500)
        {
            width = 500;
            height = width/this->m_ItemRatio;
        }

        this->m_ItemSize = QSizeF(width, height);
        ItemResize();
        this->update();
	}
	QGraphicsItem::mouseMoveEvent(event);
}

4.图元旋转

图元的缩放同样需要重写上述鼠标事件函数。其功能实现的主要有以下步骤:
1.判断鼠标的位置是否在旋转操作点上,如果在,则改变鼠标光标形态;
2.当鼠标进行按下操作时,根据当前光标形态判断此时鼠标按下将要执行缩放操作,并记录下鼠标按下的坐标点位置和Item当前的Transform;
3.根据鼠标移动的位置来计算图元旋转的角度和方向,并通过设置图元的Transform,对图元进行旋转重绘;

void AnchorItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
    if(this->isSelected())
    {
        QMarginsF pad = QMarginsF(1,1,1,1);
        if((this->m_RotateRect+pad).contains(pos))
        {
            this->m_CursorShape = ROTATE_CURSOR;
            this->setCursor(this->m_RotateHoverCursor);
        }
    }
}

void AnchorItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    this->setFocus();
    if(this->m_CursorShape == ROTATE_CURSOR)
    {
        this->m_PressItemPos = event->pos();
        this->m_Transform = this->transform();
        this->m_Operation = ITEM_TOTATE;
        this->setCursor(this->m_RotatePressCursor);
    }
    QGraphicsItem::mousePressEvent(event);
}

void AnchorItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(this->m_Operation == ITEM_TOTATE)
    {
        QVector2D startVect = QVector2D(this->m_PressItemPos);
        startVect.normalize();
        QVector2D endVect = QVector2D(event->pos());
        endVect.normalize();

        //向量点积用来计算旋转角度
        qreal value = QVector2D::dotProduct(startVect, endVect);
        if(value>1.0)
            value=1.0;
        else if(value<-1.0)
            value=-1.0;

        qreal angle = qRadiansToDegrees(qAcos(value));
        
        //向量叉乘用来计算旋转方向
        QVector3D vect = QVector3D::crossProduct(QVector3D(startVect,1.0), QVector3D(endVect,1.0));
        if(vect.z()<0)
            angle *= -1.0;

        this->m_Transform.rotate(angle);
        this->setTransform(this->m_Transform);
        this->update();
    }
    QGraphicsItem::mouseMoveEvent(event);
}

提示

在对场景内的图像进行操作编程处理时,一定要对场景坐标系和图元坐标系有清晰的认知!