一、效果展示
1.添加选择图元
2.拖拽移动图元
3.缩放图元
4.旋转图元
二、实现步骤
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);
}
源码下载
源程序下载地址:https://download.csdn.net/download/weixin_43493507/87766709?spm=1001.2014.3001.5503
提示
在对场景内的图像进行操作编程处理时,一定要对场景坐标系和图元坐标系有清晰的认知!