当前位置: 首页 > news >正文

qtdraw-使用qt绘图之开源源码学习

1. 资源介绍

功能:使用qt在画板上绘制各种形状,并保持绘制内容到xml文件中。

项目源码:https://github.com/egan2015/qdraw

软件界面

1.1 支持shape

6种

1.2 支持的功能

6种,分别是对绘制的图形进行撤销undo,重做redo,裁剪,复制,粘贴,删除功能。

2. 总体类图关系

总体分割3个独立块。

2.1 绘制类

DrawTool 基类定义了基本的绘图和交互行为,而各子类分别实现了不同的编辑和绘制功能。这种设计方式具有良好的扩展性,可以轻松添加新的工具类以支持更多类型的图形操作。但是具体的绘制操作是放在各自的GraphicsItem,后面会介绍。

很奇怪,为何父类和子类都没有继承QObject,难道是不使用qt的信号槽机制?

注解:懂得可以不看,跳过。

这里以void PolygonTool::mouseReleaseEvent函数为例,其内部有一句

emit scene->itemAdded( item );  

在 C++ 的 Qt 框架中,要使用信号和槽机制的确需要类继承自 QObject 并使用 Q_OBJECT 宏。然而,PolygonTool 类本身并没有使用 Q_OBJECT 或继承自 QObject,却调用了 emit 关键字发送信号。这可能导致一些困惑。

信号定义在其他类中: itemAdded 是在 scene 对象中定义的信号。PolygonTool 没有继承自 QObject,它仍然可以通过持有的 scene 对象(必须是 QObject 的子类)来发射信号。这是 Qt 框架设计的一部分,允许灵活的信号和槽连接,即使在复杂的类继承结构中。

2.2 GraphicsItem类

3. 绘制类成员介绍

3.1 DrawTool基类

其他四个类SelectTool, RotationTool, RectTool, PolygonTool都是继承此基类。 DrawTool 是一个基类,用于为不同的绘图工具提供公共功能和接口。它的主要作用是定义和实现一些基础的绘图操作,以及管理和查找不同的绘图工具实例。

//drawtool.henum DrawShape
{selection,   // 选择模式:用于选择和操作现有的图形项。rotation,    // 旋转模式:用于对图形项进行旋转操作。line,        // 线段:用于绘制直线。rectangle,   // 矩形:用于绘制矩形。roundrect,   // 圆角矩形:用于绘制带有圆角的矩形。ellipse,     // 椭圆:用于绘制椭圆形状。bezier,      // 贝塞尔曲线:用于绘制贝塞尔曲线,用于平滑和复杂的曲线形状。polygon,     // 多边形:用于绘制闭合的多边形。polyline,    // 折线:用于绘制由多个线段组成的折线。
};// 基类
class DrawTool
{
public:DrawTool( DrawShape shape ); // 构造函数,初始化绘制工具,参数为绘制形状virtual void mousePressEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;  // 鼠标按下事件,虚函数virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene );virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent * event ,DrawScene *scene );DrawShape m_drawShape;  // 当前绘制工具的形状bool m_hoverSizer; // 是否悬停在调整大小的手柄上static DrawTool * findTool( DrawShape drawShape ); // 根据形状查找相应的绘制工具static QList<DrawTool*> c_tools; // 静态成员,保存所有绘制工具的列表static QPointF c_down; // 静态成员,保存鼠标按下时的位置static quint32 c_nDownFlags; // 静态成员,保存按下时的标志static QPointF c_last;  // 静态成员,保存最后一次鼠标事件的位置static DrawShape c_drawShape;  // 静态成员,当前选中的绘制形状
};// drawtool.cpp// 初始化全部的静态成员变量,为啥不在构造函数内部初始化:
// 语法规则, 静态成员变量的初始化必须在类定义的外部进行
// 因为是静态的,静态成员属于类而不是对象,避免重复初始化,静态成员变量在程序加载时就会被分配内存并初始化,而构造函数是在对象被创建时才会调用。
QList<DrawTool*> DrawTool::c_tools;  // 初始化静态成员,绘制工具列表
QPointF DrawTool::c_down; // 初始化静态成员,鼠标按下位置
QPointF DrawTool::c_last;  // 初始化静态成员,最后一次鼠标位置
quint32 DrawTool::c_nDownFlags;   // 初始化静态成员,鼠标按下标志
DrawShape DrawTool::c_drawShape = selection;  // 初始化静态成员,当前绘制形状为 selection// DrawTool 构造函数,初始化绘制形状并将工具加入工具列表
DrawTool::DrawTool(DrawShape shape)
{m_drawShape = shape ;     // 设置绘制形状m_hoverSizer = false;     // 初始化悬停调整大小状态c_tools.push_back(this);  // 将当前工具加入工具列表
}// 鼠标按下事件处理
void DrawTool::mousePressEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{c_down = event->scenePos();  // 保存鼠标 按下时的位置c_last = event->scenePos();  // 初始化最后位置为 按下时位置
}// 鼠标移动事件处理
void DrawTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{c_last = event->scenePos();  // 更新最后一次鼠标事件的位置
}// 鼠标释放事件处理
void DrawTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{if (event->scenePos() == c_down )  // 如果鼠标释放位置和按下位置相同c_drawShape = selection;  // 设置当前绘制形状为 selectionsetCursor(scene,Qt::ArrowCursor);  // 重置光标为箭头样式
}// 鼠标双击事件处理,当前为空实现
void DrawTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{}// 根据形状查找相应的绘制工具
DrawTool *DrawTool::findTool(DrawShape drawShape)
{QList<DrawTool*>::const_iterator iter = c_tools.constBegin();  // 获取工具列表的常量迭代器for ( ; iter != c_tools.constEnd() ; ++iter ){if ((*iter)->m_drawShape == drawShape )  // 如果找到形状匹配的工具return (*iter);  // 返回该工具}return 0;  // 如果没有匹配的工具,返回 nullptr
}

3.2 SelectTool选中类

SelectTool 类专注于处理绘图场景中的选择操作,包括选择、移动、缩放和编辑图形项。它通过重写基类 DrawTool 的虚拟函数,提供了具体的选择操作实现。SelectTool 管理图形项的选择状态、虚线框的显示以及选择模式的切换,为用户提供了直观的操作反馈和功能。


class SelectTool : public DrawTool
{
public:SelectTool();virtual void mousePressEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene );QPointF initialPositions;       // 记录初始位置QPointF opposite_;              // 对应点,用于大小调整操作QGraphicsPathItem * dashRect;   // 虚线框,用于选择时的视觉反馈GraphicsItemGroup * selLayer;   // 选择的图层
};// 构造函数,初始化成员变量
SelectTool::SelectTool():DrawTool(selection)    // 初始化基类 DrawTool,模式为 selection (选择),这样继承过来的变量得到了初始化。
{dashRect = 0;           // 初始化虚线框指针为空selLayer = 0;           // 初始化选择层指针为空opposite_ = QPointF();  // 初始化对点为空,即与当前拖动的控制点相对的点。
}// 选中时会有三种操作:size(调整大小),editor(编辑),move(移动)
void SelectTool::mousePressEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{DrawTool::mousePressEvent(event,scene);  // 调用基类的鼠标按下事件处理if ( event->button() != Qt::LeftButton ) return;    // 如果不是左键按下,则不处理if (!m_hoverSizer)scene->mouseEvent(event);  // 如果没有悬停在调整器上,则将事件传递给场景nDragHandle = Handle_None;    // 初始化拖拽句柄为无selectMode = none;            // 初始化选择模式为无QList<QGraphicsItem *> items = scene->selectedItems();  // 获取当前选中的图形项,是通过鼠标点击、框选选中的。AbstractShape *item = 0;     // 初始化选中的形状指针为空if ( items.count() == 1 )    // 如果只选中了一个图形项item = qgraphicsitem_cast<AbstractShape*>(items.first());  // 尝试将其转换为 AbstractShape 类型if ( item != 0 ){  // 如果选中的图形项不为空nDragHandle = item->collidesWithHandle(event->scenePos());  // 检查是否与句柄碰撞(鼠标在形状的边缘位置),从而判断当前模式。if ( nDragHandle != Handle_None && nDragHandle <= Left )    // 如果拖拽句柄<=Left,在左边就说明有碰撞selectMode = size;         // 设置选择模式为大小调整,其中size是枚举SelectMode成员else if ( nDragHandle > Left )  // 如果拖拽句柄>Left, 设置选择模式为编辑selectMode = editor;        elseselectMode =  move;         // 否则光标就是没有碰撞,而是在形状内部,设置为移动模式if ( nDragHandle!= Handle_None && nDragHandle <= Left ){   // 如果是大小调整模式opposite_ = item->opposite(nDragHandle);   // 获取相对点,即与当前拖动的控制点相对的点。if( opposite_.x() == 0 )opposite_.setX(1);  // 防止出现零值,设置为 1if (opposite_.y() == 0 )opposite_.setY(1);}setCursor(scene,Qt::ClosedHandCursor);  // 设置光标为闭合手形状}else if ( items.count() > 1 )selectMode =  move;   // 如果选中了多个图形项,则只能是移动模式if( selectMode == none ){    // 如果没有设置模式selectMode = netSelect;  // 设置为网络选择模式if ( scene->view() ){QGraphicsView * view = scene->view();view->setDragMode(QGraphicsView::RubberBandDrag);  // 设置视图为橡皮筋拖动模式}
#if 0if ( selLayer ){ // 取消注释以清除选择图层scene->destroyGroup(selLayer);selLayer = 0;}
#endif}if ( selectMode == move && items.count() == 1 ){  // 如果是移动模式且只有一个选中项if (dashRect ){scene->removeItem(dashRect);  // 如果已有虚线框,移除delete dashRect;              // 删除虚线框dashRect = 0;                 // 重置虚线框指针}dashRect = new QGraphicsPathItem(item->shape());   // 创建一个新的虚线框dashRect->setPen(Qt::DashLine);   // 设置为虚线dashRect->setPos(item->pos());    // 设置位置dashRect->setTransformOriginPoint(item->transformOriginPoint());   // 设置变换原点dashRect->setTransform(item->transform());         // 应用图形项的变换dashRect->setRotation(item->rotation());           // 设置旋转dashRect->setScale(item->scale());                 // 设置缩放dashRect->setZValue(item->zValue());               // 设置 Z 值scene->addItem(dashRect);// 将虚线框添加到场景initialPositions = item->pos();  // 记录初始位置}
}// 处理鼠标移动事件
void SelectTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{DrawTool::mouseMoveEvent(event,scene);  // 调用基类的鼠标移动事件处理QList<QGraphicsItem *> items = scene->selectedItems();  // 获取当前选中的图形项AbstractShape * item = 0;  // 初始化选中的形状指针为空if ( items.count() == 1 ){  // 如果只选中了一个图形项item = qgraphicsitem_cast<AbstractShape*>(items.first());  // 尝试将其转换为 AbstractShape 类型if ( item != 0 ){  // 如果选中的图形项不为空if ( nDragHandle != Handle_None && selectMode == size ){  // 如果是大小调整模式if (opposite_.isNull()){  // 如果对点为空opposite_ = item->opposite(nDragHandle);   // 获取对点,即与当前拖动的控制点相对的点。if( opposite_.x() == 0 )opposite_.setX(1);  // 防止零值,设置为 1if (opposite_.y() == 0 )opposite_.setY(1);}/*mapFromScene 的作用是将场景坐标(Scene Coordinates)转换为图形项自身的局部坐标系(Item Coordinates)。将全局的场景坐标 c_last 转换为 item 自身的局部坐标系中的一个点。这是因为 c_last 是在场景中的一个点,但我们在操作图形项时,通常需要知道该点在图形项自身坐标系中的位置。按下c_down之后,移动c_last鼠标就是拉伸按下的坐标是initial_delta,移动后的坐标是new_delta在图形项的缩放过程中,我们通常是以某个固定点(对点 opposite_)为基准,然后根据鼠标的位置(c_last 和 c_down)计算拉伸或缩放的比例。这样就能准确地控制图形的大小变化,使得缩放操作相对直观且符合用户的期望。*/QPointF new_delta = item->mapFromScene(c_last) - opposite_;  // 计算新的相对距离: c_last鼠标移动的坐标 - 边界QPointF initial_delta = item->mapFromScene(c_down) - opposite_;  // 计算初始相对距离: c_down鼠标按下的坐标 - 边界double sx = new_delta.x() / initial_delta.x();  // 计算 X 方向的缩放比例double sy = new_delta.y() / initial_delta.y();  // 计算 Y 方向的缩放比例/*nDragHandle: 一个图形项会有多个控制手柄(如顶点、边缘中点等),每个手柄对应不同的缩放或调整操作。sx sy: 表示 X,Y 方向上的缩放比例。opposite_: 表示固定不动的对点坐标(相对于拖动手柄)。在缩放或拉伸过程中,这个点保持不变,是所有变换操作的基准点。*/item->stretch(nDragHandle, sx , sy , opposite_);  // 执行图形项的拉伸操作emit scene->itemResize(item,nDragHandle,QPointF(sx,sy)); // 发送图形项大小调整的信号//  qDebug()<<"scale:"<<nDragHandle<< item->mapToScene(opposite_)<< sx << " 锛? << sy//         << new_delta << item->mapFromScene(c_last)//         << initial_delta << item->mapFromScene(c_down) << item->boundingRect();} else if ( nDragHandle > Left  && selectMode == editor ){  // 如果是编辑模式// 确定控制点:通过 nDragHandle 确定用户正在操作的控制点。// 更新位置:根据当前鼠标的位置 c_last,将控制点移动到新的位置。// 修改图形形状:调整图形项的形状以反映控制点的移动。这可能包括更新曲线的曲率、多边形的形状等。item->control(nDragHandle,c_last);   // 更新图形项的控制点位置。控制点是用于定义图形形状的关键点emit scene->itemControl(item,nDragHandle,c_last,c_down);  // 发送图形项控制的信号}else if(nDragHandle == Handle_None ){  // 如果没有拖拽句柄int handle = item->collidesWithHandle(event->scenePos());  // 检查是否与句柄碰撞if ( handle != Handle_None){setCursor(scene,Qt::OpenHandCursor);  // 如果碰撞,设置光标为打开的手形状m_hoverSizer = true;  // 设置悬停调整标志}else{setCursor(scene,Qt::ArrowCursor);  // 否则设置光标为箭头形状m_hoverSizer = false;   // 取消悬停调整标志}}}}if ( selectMode == move ){  // 如果是移动模式setCursor(scene,Qt::ClosedHandCursor);  // 设置光标为闭合的手形状if ( dashRect ){dashRect->setPos(initialPositions + c_last - c_down);  // 更新虚线框的位置}}if ( selectMode != size  && items.count() > 1)  // 如果不是大小调整模式且选中多个项{scene->mouseEvent(event);   // 将事件传递给场景}}void SelectTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{DrawTool::mouseReleaseEvent(event,scene);if ( event->button() != Qt::LeftButton ) return;// 获取当前场景中所有被选中的图形项。QList<QGraphicsItem *> items = scene->selectedItems();// 如果只选中了一个图形项,进行以下处理。if ( items.count() == 1 ){AbstractShape * item = qgraphicsitem_cast<AbstractShape*>(items.first());// 如果转换成功且当前选择模式为移动模式,并且鼠标释放的位置与按下的位置不同。if ( item != 0  && selectMode == move && c_last != c_down ){// 将图形项的位置设置为初始位置加上鼠标移动的偏移量。item->setPos(initialPositions + c_last - c_down);// 触发场景的 itemMoved 信号,通知图形项已被移动,并传递移动的偏移量。emit scene->itemMoved(item , c_last - c_down );}// 如果图形项存在,并且选择模式为尺寸调整或编辑模式,并且鼠标释放的位置与按下的位置不同。else if ( item !=0 && (selectMode == size || selectMode ==editor) && c_last != c_down ){// 更新图形项的坐标,通常是为了确保调整后的形状参数被正确应用。item->updateCoordinate();}}// 如果选中了多个图形项,且选择模式为移动模式,并且鼠标释放的位置与按下的位置不同。else if ( items.count() > 1 && selectMode == move && c_last != c_down ){// 触发场景的 itemMoved 信号,传递 NULL 代表多个图形项的移动操作,并传递移动的偏移量。emit scene->itemMoved(NULL , c_last - c_down );}// 如果当前选择模式为网格选择模式(框选),进行以下处理。if (selectMode == netSelect ){// 检查场景是否有视图,如果有视图,则设置视图的拖拽模式为无拖拽。if ( scene->view() ){QGraphicsView * view = scene->view();view->setDragMode(QGraphicsView::NoDrag);}// 如果代码块开启,检查选中的图形项数量是否大于 1,并将其创建为一个组。
#if 0if ( scene->selectedItems().count() > 1 ){selLayer = scene->createGroup(scene->selectedItems());selLayer->setSelected(true);}
#endif}// 后面是清空成员变量。if (dashRect ){// 从场景中移除虚线矩形,并删除该对象释放内存。scene->removeItem(dashRect);delete dashRect;dashRect = 0;}// 重置选择模式为无,表示没有正在进行的选择或编辑操作。selectMode = none;// 重置拖拽句柄为无,表示没有正在进行的拖拽操作。nDragHandle = Handle_None;// 重置悬停尺寸标志为 false,表示当前鼠标没有悬停在可调整大小的区域。m_hoverSizer = false;// 重置对点坐标为 (0, 0),用于下一次的尺寸调整操作。opposite_ = QPointF();// 将鼠标事件传递给场景进行处理,确保场景接收到完整的鼠标事件流程。scene->mouseEvent(event);
}

3.3 RotationTool旋转类

RotationTool 类专注于处理图形项的旋转操作。它通过重写基类 DrawTool 的虚拟函数,提供了具体的旋转实现。RotationTool 管理图形项的旋转状态、旋转角度的计算以及旋转视觉反馈的显示,为用户提供了直观的旋转操作体验。通过管理 lastAngledashRectRotationTool 能够准确地计算和显示旋转效果,并在鼠标操作时给予用户实时反馈。

class  RotationTool : public DrawTool
{
public:RotationTool();virtual void mousePressEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene );qreal lastAngle;  // 记录上一次计算的旋转角度QGraphicsPathItem * dashRect;  // 用于显示虚线矩形的指针
};// 构造函数,初始化旋转工具
RotationTool::RotationTool():DrawTool(rotation) // 实例化基类的成员变量
{lastAngle = 0;    // 初始化上一次角度为0dashRect = 0;     // 初始化虚线矩形指针为空
}void RotationTool::mousePressEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类 DrawTool 的 mousePressEvent 方法DrawTool::mousePressEvent(event,scene);if ( event->button() != Qt::LeftButton ) return;// 如果没有在调整大小的状态,则将事件传递给场景if (!m_hoverSizer)scene->mouseEvent(event);// 获取当前场景中所有被选中的图形项QList<QGraphicsItem *> items = scene->selectedItems();// 如果选中了一个图形项if ( items.count() == 1 ){AbstractShape * item = qgraphicsitem_cast<AbstractShape*>(items.first());if ( item != 0 ){// 检测鼠标点击的位置是否在图形项的控制句柄上,即边界顶点等位置nDragHandle = item->collidesWithHandle(event->scenePos());// 如果点击的是控制句柄if ( nDragHandle !=Handle_None){// 获取图形项的中心点相对于场景的坐标QPointF origin = item->mapToScene(item->boundingRect().center());// 计算鼠标当前位置相对于中心点的 Y 和 X 方向的长度qreal len_y = c_last.y() - origin.y();qreal len_x = c_last.x() - origin.x();// 使用反正切函数计算鼠标位置与中心点连线的角度,并将其转换为度数qreal angle = atan2(len_y,len_x)*180/PI;// 当前点和item中心点坐标(都是场景坐标)形成的角度lastAngle = angle;  // 记录当前角度为 lastAngleselectMode = rotate;  // 设置选择模式为旋转// 如果 dashRect 已存在,移除并删除它if (dashRect ){scene->removeItem(dashRect);delete dashRect;dashRect = 0;}// 创建一个新的虚线矩形项,以形状项的轮廓作为路径dashRect = new QGraphicsPathItem(item->shape());dashRect->setPen(Qt::DashLine);  // 设置虚线笔刷dashRect->setPos(item->pos());dashRect->setTransformOriginPoint(item->transformOriginPoint());  // 设置变换的原点dashRect->setTransform(item->transform());  // 设置变换矩阵dashRect->setRotation(item->rotation());  // 设置旋转角度dashRect->setScale(item->scale());  // 设置缩放比例dashRect->setZValue(item->zValue());  // 设置 Z 轴位置scene->addItem(dashRect);  // 将虚线矩形添加到场景中setCursor(scene,QCursor((QPixmap(":/icons/rotate.png"))));  // 设置旋转光标}else{// 如果没有点击控制句柄,则切换到选择工具,并处理按下事件c_drawShape = selection;selectTool.mousePressEvent(event,scene);}}}
}void RotationTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类 DrawTool 的 mouseMoveEvent 方法DrawTool::mouseMoveEvent(event,scene);// 获取当前场景中所有被选中的图形项QList<QGraphicsItem *> items = scene->selectedItems();// 如果选中了一个图形项if ( items.count() == 1 ){AbstractShape * item = qgraphicsitem_cast<AbstractShape*>(items.first());// 如果转换成功且在拖拽句柄且选择模式为旋转模式if ( item != 0  && nDragHandle !=Handle_None && selectMode == rotate ){// 获取图形项的中心点相对于场景的坐标QPointF origin = item->mapToScene(item->boundingRect().center());// 计算鼠标当前位置相对于中心点的 Y 和 X 方向的长度qreal len_y = c_last.y() - origin.y();qreal len_x = c_last.x() - origin.x();// 使用反正切函数计算鼠标位置与中心点连线的角度,并将其转换为度数qreal angle = atan2(len_y,len_x)*180/PI;// 计算新的旋转角度// angle - lastAngle: 计算自上次记录角度以来的角度变化量。这表示用户在旋转操作期间,鼠标移动所导致的角度变化。// item->rotation(): 获取图形项当前的旋转角度,自带的角度。angle = item->rotation() + int(angle - lastAngle) ;  // 将角度限制在 -360 到 360 度之间if ( angle > 360 )angle -= 360;if ( angle < -360 )angle+=360;// 如果虚线矩形存在,更新其旋转角度if ( dashRect ){//dashRect->setTransform(QTransform::fromTranslate(15,15),true);//dashRect->setTransform(QTransform().rotate(angle));//dashRect->setTransform(QTransform::fromTranslate(-15,-15),true);dashRect->setRotation( angle );}// 设置旋转光标setCursor(scene,QCursor((QPixmap(":/icons/rotate.png"))));}else if ( item ){// 检查鼠标当前位置是否在控制句柄上int handle = item->collidesWithHandle(event->scenePos());// 如果在控制句柄上,设置旋转光标,并标记悬停状态if ( handle != Handle_None){setCursor(scene,QCursor((QPixmap(":/icons/rotate.png"))));m_hoverSizer = true;}else{// 如果不在控制句柄上,设置为普通箭头光标,并重置悬停状态setCursor(scene,Qt::ArrowCursor);m_hoverSizer = false;}}}// 将事件传递给场景scene->mouseEvent(event);
}void RotationTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类 DrawTool 的 mouseReleaseEvent 方法DrawTool::mouseReleaseEvent(event,scene);// 如果释放的不是左键,直接返回if ( event->button() != Qt::LeftButton ) return;QList<QGraphicsItem *> items = scene->selectedItems();if ( items.count() == 1 ){AbstractShape * item = qgraphicsitem_cast<AbstractShape*>(items.first());// 如果转换成功且在拖拽句柄且选择模式为旋转模式if ( item != 0  && nDragHandle !=Handle_None && selectMode == rotate ){// 获取图形项的中心点相对于场景的坐标QPointF origin = item->mapToScene(item->boundingRect().center());// 计算鼠标当前位置相对于中心点的偏移量QPointF delta = c_last - origin ;// 计算鼠标当前位置相对于中心点的 Y 和 X 方向的长度qreal len_y = c_last.y() - origin.y();qreal len_x = c_last.x() - origin.x();// 使用反正切函数计算鼠标位置与中心点连线的角度,并将其转换为度数qreal angle = atan2(len_y,len_x)*180/PI,oldAngle = item->rotation();// 计算最终的旋转角度angle = item->rotation() + int(angle - lastAngle) ;// 将角度限制在 -360 到 360 度之间if ( angle > 360 )angle -= 360;if ( angle < -360 )angle+=360;// 设置图形项的旋转角度item->setRotation( angle );// 触发场景的 itemRotate 信号,通知图形项已被旋转,并传递旧的角度emit scene->itemRotate(item , oldAngle);qDebug()<<"rotate:"<<angle<<item->boundingRect();}}setCursor(scene,Qt::ArrowCursor);// 清空成员变量。selectMode = none;nDragHandle = Handle_None;lastAngle = 0;m_hoverSizer = false;if (dashRect ){scene->removeItem(dashRect);delete dashRect;dashRect = 0;}// 将事件传递给场景scene->mouseEvent(event);
}

3.4 RectTool

这里RectTool类支持绘制rectangle矩形、roundrect圆角矩形、ellipse椭圆,但是具体的绘制操作是由其成员变量item自己负责。

class RectTool : public DrawTool
{
public:RectTool(DrawShape drawShape);  // 构造函数,用于初始化工具的形状类型virtual void mousePressEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene );GraphicsItem * item;   // 当前创建的图形项
};RectTool::RectTool(DrawShape drawShape):DrawTool(drawShape)  // 调用基类构造函数初始化工具类型
{item = 0; // 初始化图形项为nullptr
}void RectTool::mousePressEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{if ( event->button() != Qt::LeftButton ) return;// 清除场景中所有的选中项scene->clearSelection();// 调用基类的鼠标按下事件处理DrawTool::mousePressEvent(event,scene);// 根据工具类型创建不同的图形项switch ( c_drawShape ){case rectangle: // 矩形item = new GraphicsRectItem(QRect(1,1,1,1));        // 创建一个初始大小为1x1的矩形项break;case roundrect: // 圆角矩形item = new GraphicsRectItem(QRect(1,1,1,1),true);   // 创建一个初始大小为1x1的圆角矩形项break;case ellipse:   // 椭圆item = new GraphicsEllipseItem(QRect(1,1,1,1));     // 创建一个初始大小为1x1的椭圆项break;}if ( item == 0) return;  // 如果图形项创建失败,则返回// 将点击起始点位置向右下偏移2像素:// 在图形项的初始创建时(例如,矩形或圆角矩形),其尺寸通常从一个很小的区域开始。偏移量确保图形项在可视区域内有足够的空间来展示c_down+=QPoint(2,2);               item->setPos(event->scenePos());   // 设置图形项的位置为鼠标点击的位置scene->addItem(item);              // 将图形项添加到场景中item->setSelected(true);           // 选中图形项selectMode = size;                 // 设置选择模式为调整大小nDragHandle = RightBottom;         // 设置拖拽手柄为右下角}void RectTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{setCursor(scene,Qt::CrossCursor);       // 设置光标为十字形,以指示绘图模式selectTool.mouseMoveEvent(event,scene); // 调用选择工具的鼠标移动事件处理
}void RectTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{selectTool.mouseReleaseEvent(event,scene);    // 调用选择工具的鼠标释放事件处理// 如果鼠标释放位置与初始点击位置相同,则删除创建的图形项if ( event->scenePos() == (c_down-QPoint(2,2))){  // 前面c_down鼠标点击的位置已经偏移2像素,所以这里要补偿回来。if ( item != 0){item->setSelected(false);  // 取消选中图形项scene->removeItem(item);   // 从场景中移除图形项delete item ;              // 删除图形项对象item = 0;                  // 将图形项指针设置为nullptr}qDebug()<<"RectTool removeItem:";}// 发射信号,表示图形项已添加到场景中else if( item ){emit scene->itemAdded( item );}// 将工具形状重置为选择模式c_drawShape = selection;
}

3.5 PolygonTool

 PolygonTool 是一个用于绘制多边形折线线段贝塞尔曲线的工具类,如下所示,继承自 DrawTool。它提供了在场景中通过鼠标交互来创建和编辑这些图形的功能。

class PolygonTool : public DrawTool
{
public:PolygonTool(DrawShape shape );virtual void mousePressEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseMoveEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene ) ;virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent * event , DrawScene * scene );virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent * event ,DrawScene *scene );GraphicsPolygonItem * item;  // 当前绘制的图形项,可能是多边形、贝塞尔曲线、多线段或直线int m_nPoints;               // 当前图形的点的数量QPointF initialPositions;    // 初始位置,用于记录第一个点的位置};PolygonTool::PolygonTool(DrawShape shape):DrawTool(shape)
{item = NULL;      // 初始化 item 为 NULLm_nPoints = 0;    // 初始化点的数量为 0
}// 当首次按下时,会被添加两次,一次是item->addPoint(c_down);  item->addPoint(c_down+QPoint(1,0));
// 为了初始化一个起始线段或者提供初步的形状轮廓,使得在接下来的鼠标移动事件中能够立即看到一个图形的初步形态。
void PolygonTool::mousePressEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类的鼠标按下事件处理DrawTool::mousePressEvent(event,scene);if ( event->button() != Qt::LeftButton ) return;// 如果当前没有正在绘制的图形项,即刚开始绘制if ( item == NULL ){// 根据绘制的形状类型创建相应的图形项if ( c_drawShape == polygon ){item = new GraphicsPolygonItem(NULL);      // 创建一个多边形项}else if (c_drawShape == bezier ){            item = new GraphicsBezier();               // 创建一个贝塞尔曲线项}else if ( c_drawShape == polyline ){          item = new GraphicsBezier(false);          // 创建一个折线}else if ( c_drawShape == line ){              item = new GraphicsLineItem(0);            // 创建一个直线项}item->setPos(event->scenePos());  // 设置图形项的位置为鼠标按下的位置              scene->addItem(item);             // 将图形项添加到场景中initialPositions = c_down;        // 记录起始位置item->addPoint(c_down);           // 添加第一个点,因为是刚开始绘制item->setSelected(true);          // 设置图形项为选中状态m_nPoints++;}else if ( c_down == c_last ){        // 如果按下的点与上一个点重合/*if ( item != NULL ){scene->removeItem(item);      // 从场景中移除当前图形项delete item;item = NULL ;c_drawShape = selection;selectMode = none;return ;}*/}item->addPoint(c_down+QPoint(1,0));        // 添加一个点,位置偏移一点以避免重合m_nPoints++;                               // 点的数量加 1selectMode = size ;                        // 设置选择模式为 size,用于调整大小nDragHandle = item->handleCount();         // 设置拖动手柄为当前图形项的手柄数量// 意味着将拖动手柄的索引设置为最后一个添加的手柄,即当前鼠标点击的位置。这有助于后续在鼠标移动事件中准确控制拖动的手柄。
}void PolygonTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{DrawTool::mouseMoveEvent(event,scene);  // 调用基类的鼠标移动事件处理setCursor(scene,Qt::CrossCursor);       // 设置光标为十字形//    selectTool.mouseMoveEvent(event,scene);  // 选择工具移动事件处理if ( item != 0 ){  // 如果图形项存在// 如果有有效的手柄并且选择模式是调整大小if ( nDragHandle != Handle_None && selectMode == size ){// 控制图形项的控制点位置item->control(nDragHandle,c_last);}}}// 直线是点击开始坐标,和最后单击一个点坐标即可。其他形状需要双击才结束,所以放到另一个函数处理。
void PolygonTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类的鼠标释放事件处理DrawTool::mousePressEvent(event,scene);// 绘制的形状类型如果只直线,需要特殊处理下if ( c_drawShape == line ){                     item->endPoint(event->scenePos());           // 设置直线的结束点item->updateCoordinate();                    // 更新图形项的坐标,确之前的变化得到应用emit scene->itemAdded( item );               // 触发图形项添加信号// 清空成员变量item = NULL;                                 // 将 item 设置为 NULLselectMode = none;                           // 设置选择模式为 nonec_drawShape = selection;                     // 设置绘制模式为选择模式m_nPoints = 0;                               // 重置点的数量}
}void PolygonTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event, DrawScene *scene)
{// 调用基类的鼠标双击事件处理DrawTool::mouseDoubleClickEvent(event,scene);// 设置图形项的结束点item->endPoint(event->scenePos());// 更新图形项的坐标item->updateCoordinate();// 触发图形项添加信号emit scene->itemAdded( item );// 清空成员变量item = NULL;selectMode = none;c_drawShape = selection;m_nPoints = 0;
}

4. GraphicsItem类

AbstractShapeType是模板基类,提供基本的绘制属性和方法。 如果AbstractShapeType模板,拥有两个直接子类GraphicsItemGroup和GraphicsItem。

4.1 AbstractShapeType类

AbstractShapeType 是一个模板类,提供了一些绘图的基本属性和方法。它通过继承自 BaseType(默认为 QGraphicsItem)来扩展图形项的功能,主要用于定义形状的通用行为和属性。

// AbstractShapeType模板类:定义了一些绘图形状的基本属性和方法。
template < typename BaseType = QGraphicsItem >
class AbstractShapeType : public BaseType  // 这么写,继承也是一个动态的,通过传参指定。
{
public:// 构造函数,初始化基类,并设置笔刷和笔的初始状态explicit AbstractShapeType(QGraphicsItem * parent = 0 ):BaseType(parent){// 设置画笔为无笔触(透明)m_pen=QPen(Qt::NoPen);// 设置画刷为随机颜色的画刷m_brush= QBrush(QColor(rand() % 32 * 8, rand() % 32 * 8, rand() % 32 * 8));// 初始化宽度和高度为0m_width = m_height = 0;}virtual ~AbstractShapeType(){}// 返回图形的显示名称virtual QString displayName () const { return QString("AbstractType");}// 处理控制点的操作,默认实现为空virtual void control(int dir, const QPointF & delta ){ Q_UNUSED(dir);Q_UNUSED(delta);}// 处理图形的拉伸操作,默认实现为空virtual void stretch( int  , double  , double  , const QPointF & ) {}// 返回图形的边界矩形virtual QRectF  rect() const { return m_localRect; }// 更新图形的坐标系,默认实现为空virtual void updateCoordinate () {}// 移动图形,默认实现为空virtual void move( const QPointF & point ){Q_UNUSED(point);}// 复制图形项,默认返回NULLvirtual QGraphicsItem * duplicate() const { return NULL;}// 返回控制点的数量virtual int handleCount() const { return m_handles.size();}// 从XML加载图形,必须由派生类实现virtual bool loadFromXml(QXmlStreamReader * xml ) = 0;// 保存图形到XML,必须由派生类实现virtual bool saveToXml( QXmlStreamWriter * xml ) = 0 ;// 检查指定点是否与控制点碰撞,返回碰撞的控制点方向int collidesWithHandle( const QPointF & point ) const{// 获取处理手柄的逆向迭代器的结束位置。m_handles:是一个存储图形项控制手柄的容器const Handles::const_reverse_iterator hend =  m_handles.rend();// 从末尾开始遍历所有处理手柄for (Handles::const_reverse_iterator it = m_handles.rbegin(); it != hend; ++it){// 将点从场景坐标系转换为处理手柄的局部坐标系QPointF pt = (*it)->mapFromScene(point);// 检查点是否在处理手柄的区域内if ((*it)->contains(pt) ){return (*it)->dir();  // 返回处理手柄的方向}}return Handle_None;  // 未碰撞,返回无控制点}// 返回指定控制点的位置virtual QPointF handlePos( int handle ) const{// 从末尾开始遍历所有处理手柄const Handles::const_reverse_iterator hend =  m_handles.rend();for (Handles::const_reverse_iterator it = m_handles.rbegin(); it != hend; ++it){if ((*it)->dir() == handle ){return (*it)->pos();}}// 如果找不到处理手柄,返回默认的 QPointF()return QPointF();}// 根据缩放方向交换控制点int swapHandle(int handle, const QPointF& scale ) const{int dir = Handle_None;if ( scale.x() < 0 || scale.y() < 0 ){// 根据当前处理手柄和比例因子的方向,计算交换后的处理手柄方向switch (handle) {case RightTop:if ( scale.x() < 0 && scale.y() < 0 )dir = LeftBottom;else if ( scale.x() > 0 && scale.y() < 0 )dir = RightBottom;elsedir = LeftTop;break;case RightBottom:if ( scale.x() < 0 && scale.y() < 0 )dir = LeftTop;else if ( scale.x() > 0 && scale.y() < 0 )dir = RightTop;elsedir = LeftBottom;break;case LeftBottom:if ( scale.x() < 0 && scale.y() < 0 )dir = RightTop;else if ( scale.x() > 0 && scale.y() < 0 )dir = LeftTop;elsedir = RightBottom;break;case LeftTop:if ( scale.x() < 0 && scale.y() < 0 )dir = RightBottom;else if ( scale.x() > 0 && scale.y() < 0 )dir = LeftBottom;elsedir = RightTop;break;case Right:if (scale.x() < 0 )dir = Left;break;case Left:if (scale.x() < 0 )dir = Right;break;case Top:if (scale.y()<0)dir = Bottom;break;case Bottom:if (scale.y()<0)dir = Top;break;}}return dir;}// 返回指定控制点的相对位置virtual QPointF opposite( int handle ) {QPointF pt;switch (handle) {case Right:pt = m_handles.at(Left-1)->pos();break;case RightTop:pt = m_handles[LeftBottom-1]->pos();break;case RightBottom:pt = m_handles[LeftTop-1]->pos();break;case LeftBottom:pt = m_handles[RightTop-1]->pos();break;case Bottom:pt = m_handles[Top-1]->pos();break;case LeftTop:pt = m_handles[RightBottom-1]->pos();break;case Left:pt = m_handles[Right-1]->pos();break;case Top:pt = m_handles[Bottom-1]->pos();break;}return pt;}// 返回笔刷颜色QColor brushColor() const {return m_brush.color();}// 获取画刷QBrush brush() const {return m_brush;}// 获取画笔QPen   pen() const {return m_pen;}// 获取画笔的颜色QColor penColor() const {return m_pen.color();}// 设置画笔void setPen(const QPen & pen ) { m_pen = pen;}// 设置画刷void setBrush( const QBrush & brush ) { m_brush = brush ; }// 设置画刷的颜色void setBrushColor( const QColor & color ) { m_brush.setColor(color);}// 获取宽度qreal width() const { return m_width ; }// 设置宽度,并更新坐标void   setWidth( qreal width ){m_width = width ;updateCoordinate();}// 获取高度qreal  height() const {return m_height;}// 设置高度,并更新坐标void   setHeight ( qreal height ){m_height = height ;updateCoordinate();}protected:// 更新处理手柄(虚函数,子类可以重写)virtual void updatehandles(){}            // 设置处理手柄的状态void setState(SelectionHandleState st){// 遍历所有处理手柄并设置其状态const Handles::iterator hend =  m_handles.end();for (Handles::iterator it = m_handles.begin(); it != hend; ++it)(*it)->setState(st);}// 画刷(用于填充形状)QBrush m_brush;// 画笔(用于边框)QPen   m_pen ;// 处理手柄的容器typedef std::vector<SizeHandleRect*> Handles;Handles m_handles;// 本地矩形(形状的边界)QRectF m_localRect;// 形状的宽度和高度qreal m_width;qreal m_height;
};typedef  AbstractShapeType< QGraphicsItem > AbstractShape;

 4.2 GraphicsItem类

可以看到GrapicsItems继承了AbstractShapeType<QGraphicsItem>,而AbstractShapeType是继承了GrapicsItems。这里设计有些问题,搞得复杂了,建议稍微看看就得了。

注意:这里好像是相互继承的结构,c++不支持相互继承。举个简单例子:

// 不支持直接相互继承
class A : public B{}class B :  public A> {}// 支持间接的相互继承,但不建议这么做
template < typename BaseType = QGraphicsItem >
class A : public BaseType {}class B :  public A<B> {}

相互继承:B继承了A<B>,而A<B>又继承了B,C++ 不支持真正的“相互继承”,即两个类互相直接继承彼此。模板类继承的情况虽然复杂,但并不等于相互继承。

建议在设计时避免这种复杂的继承结构,以减少潜在的问题和困惑。

class GraphicsItem : public QObject,public AbstractShapeType<QGraphicsItem>
{Q_OBJECT// Q_PROPERTY 自动生成对属性的访问函数,使得属性可以方便地通过 Qt 的信号槽机制、Qt Designer 或其他方式进行访问。Q_PROPERTY(QColor pen READ penColor WRITE setPen )        // 声明pen和setPen方法,是读还是写操作。Q_PROPERTY(QColor brush READ brushColor WRITE setBrushColor )Q_PROPERTY(qreal  width READ width WRITE setWidth )Q_PROPERTY(qreal  height READ height WRITE setHeight )Q_PROPERTY(QPointF  position READ pos WRITE setPos )public:GraphicsItem(QGraphicsItem * parent );enum {Type = UserType+1};   // 定义一个唯一的类型标识符        int  type() const { return Type; } // 返回类型标识符virtual QPixmap image() ;  // 声明一个虚拟函数用于获取图像
signals:void selectedChange(QGraphicsItem *item);  // 声明一个信号,当选择状态改变时发射protected:QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value); // 处理图形项的变化void updatehandles(); // 更新控制柄的位置void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);  // 处理上下文菜单事件bool readBaseAttributes(QXmlStreamReader * xml );     // 从 XML 读取基本属性bool writeBaseAttributes( QXmlStreamWriter * xml );   // 将基本属性写入 XML};GraphicsItem::GraphicsItem(QGraphicsItem *parent):AbstractShapeType<QGraphicsItem>(parent)
{/*  // 设置图形项的效果QGraphicsDropShadowEffect *effect = new QGraphicsDropShadowEffect;effect->setBlurRadius(4);setGraphicsEffect(effect);*/// 初始化控制柄m_handles.reserve(Left);  // 预留 m_handles 容器的空间,以提高性能并避免不必要的内存重新分配。for (int i = LeftTop; i <= Left; ++i) {  // 遍历每个控制柄方向SizeHandleRect *shr = new SizeHandleRect(this,i);  // 创建一个控制柄,都是矩形的边界框m_handles.push_back(shr);}// 设置图形项的标志setFlag(QGraphicsItem::ItemIsMovable, true);  // 允许移动setFlag(QGraphicsItem::ItemIsSelectable, true); // 允许选择setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);  // 当几何形状发生变化时发送通知this->setAcceptHoverEvents(true);  // 接受悬停事件
}QPixmap GraphicsItem::image() {QPixmap pixmap(64, 64);   // 创建一个 64x64 的图像pixmap.fill(Qt::transparent);  // 填充透明背景//设置绘制属性QPainter painter(&pixmap);     // 创建绘图对象setPen(QPen(Qt::black));       // 设置画笔为黑色setBrush(Qt::white);           // 设置画刷为白色QStyleOptionGraphicsItem *styleOption = new QStyleOptionGraphicsItem;  // 创建绘图选项//painter.translate(m_localRect.center().x(),m_localRect.center().y());  // 移动绘图位置(被注释掉了)// 绘制图形: 实现不在代码中提供,因此图像的具体样式取决于 paint 函数如何绘制图形。paint(&painter,styleOption);  delete styleOption;  // 删除绘图选项return pixmap;  // 返回图像
}void GraphicsItem::updatehandles()
{const QRectF &geom = this->boundingRect();  // 获取图形项的边界矩形// 获取控制柄列表的末尾const Handles::iterator hend =  m_handles.end();  for (Handles::iterator it = m_handles.begin(); it != hend; ++it) {SizeHandleRect *hndl = *it;// 根据控制柄的方向更新位置switch (hndl->dir()) {  // hndl是哪个方向case LeftTop: // 左上角hndl->move(geom.x() , geom.y() );  // move 方法将手柄的左上角移动到 (x, y) 坐标。break;case Top:     // 是上边hndl->move(geom.x() + geom.width() / 2 , geom.y() );  // 将手柄移动到图形项的上边中点位置。break;case RightTop:hndl->move(geom.x() + geom.width() , geom.y() );break;case Right:hndl->move(geom.x() + geom.width() , geom.y() + geom.height() / 2 );break;case RightBottom:hndl->move(geom.x() + geom.width() , geom.y() + geom.height() );break;case Bottom:hndl->move(geom.x() + geom.width() / 2 , geom.y() + geom.height() );break;case LeftBottom:hndl->move(geom.x(), geom.y() + geom.height());break;case Left:hndl->move(geom.x(), geom.y() + geom.height() / 2);break;default:break;}}
}void GraphicsItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{Q_UNUSED(event);  // 忽略事件
}// 从xml读取属性值,赋值给对象实例
bool GraphicsItem::readBaseAttributes(QXmlStreamReader *xml)
{qreal x = xml->attributes().value(tr("x")).toDouble();qreal y = xml->attributes().value(tr("y")).toDouble();m_width = xml->attributes().value("width").toDouble();m_height = xml->attributes().value("height").toDouble();setZValue(xml->attributes().value("z").toDouble());setRotation(xml->attributes().value("rotate").toDouble());setPos(x,y);return true;
}// 从对象实例中,写入属性值到xml文件中
bool GraphicsItem::writeBaseAttributes(QXmlStreamWriter *xml)
{xml->writeAttribute(tr("rotate"),QString("%1").arg(rotation()));  // "rotate",表示 XML 属性的名称。%1 是一个占位符xml->writeAttribute(tr("x"),QString("%1").arg(pos().x()));xml->writeAttribute(tr("y"),QString("%1").arg(pos().y()));xml->writeAttribute(tr("z"),QString("%1").arg(zValue()));xml->writeAttribute(tr("width"),QString("%1").arg(m_width));xml->writeAttribute(tr("height"),QString("%1").arg(m_height));return true;
}QVariant GraphicsItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{// 如果选择状态发生改变if ( change == QGraphicsItem::ItemSelectedHasChanged ) { // 如果不是一个组QGraphicsItemGroup *g = dynamic_cast<QGraphicsItemGroup*>(parentItem());if (!g)// 设置控制柄的状态:SelectionHandleOff, SelectionHandleInactive, SelectionHandleActive setState(value.toBool() ? SelectionHandleActive : SelectionHandleOff);  // 如果是组,取消选择状态else{setSelected(false);return QVariant::fromValue<bool>(false);}}/*else if (change == ItemPositionChange && scene()) {// value is the new position.QPointF newPos = value.toPointF();QRectF rect = scene()->sceneRect();if (!rect.contains(newPos)) {// Keep the item inside the scene rect.newPos.setX(qMin(rect.right()-boundingRect().width()/2, qMax(newPos.x(), rect.left()+boundingRect().width()/2)));newPos.setY(qMin(rect.bottom()-boundingRect().height()/2, qMax(newPos.y(), rect.top()+boundingRect().height()/2)));return newPos;}}*/return QGraphicsItem::itemChange(change, value);  // 调用基类的 itemChange 方法
}

4.3 GraphicsRectItem类

 GraphicsRectItem 类的作用主要是为图形场景中的矩形、圆角矩形元素提供一个基础的图形项,并为其提供各种功能和操作接口。

class GraphicsRectItem : public GraphicsItem
{
public:// 构造函数,初始化矩形对象,rect为矩形区域,isRound表示是否是圆角矩形GraphicsRectItem(const QRect & rect, bool isRound = false, QGraphicsItem * parent = 0 );// 返回图形项的边界矩形QRectF boundingRect() const;// 返回图形项的形状,通常用于碰撞检测QPainterPath shape() const;// 控制柄操作,用于调整图形项void control(int dir, const QPointF & delta);// 拉伸操作,调整图形项的大小void stretch(int handle , double sx , double sy , const QPointF & origin);// 返回当前矩形区域QRectF  rect() const {  return m_localRect;}// 更新图形项的坐标void updateCoordinate();// 移动图形项void move( const QPointF & point );// 复制当前图形项QGraphicsItem *duplicate () const ;// 返回图形项的显示名称QString displayName() const { return tr("rectangle"); }// 从XML加载图形项virtual bool loadFromXml(QXmlStreamReader * xml );// 保存图形项到XMLvirtual bool saveToXml( QXmlStreamWriter * xml );protected:// 更新控制柄位置void updatehandles();// 绘制图形项void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);bool m_isRound;           // 是否为圆角矩形qreal m_fRatioY;          // 控制圆角矩形的水平和垂直圆角半径比例qreal m_fRatioX;          // QRectF m_initialRect;     // m_initialRect、m_localRect: 初始矩形和当前矩形的位置和尺寸。QPointF opposite_;        // 参考点,矩形的中心点QPointF m_originPoint;    // 原点。m_originPoint、opposite_: 用于几何变换和定位的辅助点。
};// 构造函数实现
GraphicsRectItem::GraphicsRectItem(const QRect & rect , bool isRound , QGraphicsItem *parent):GraphicsItem(parent)    // 调用基类构造函数,m_isRound(isRound)      // 初始化是否圆角,m_fRatioX(1/10.0)       // 初始化宽度圆角比例,m_fRatioY(1/3.0)        // 初始化高度圆角比例
{m_width = rect.width();       // 设置矩形宽度m_height = rect.height();     // 设置矩形高度m_initialRect = rect;         // 初始化矩形区域m_localRect = rect;           // 设置本地矩形区域m_originPoint = QPointF(0,0); // 设置原点// 如果是圆角矩形,添加控制柄用于调整圆角if( m_isRound ){SizeHandleRect *shr = new SizeHandleRect(this, 9 , true);m_handles.push_back(shr);shr = new SizeHandleRect(this, 10 , true);m_handles.push_back(shr);//shr = new SizeHandleRect(this, 11 , true);//m_handles.push_back(shr);}updatehandles();  // 更新控制柄位置
}// 返回图形项的边界矩形
QRectF GraphicsRectItem::boundingRect() const
{return m_localRect;  // 使用当前的本地矩形作为边界
}// 返回图形项的形状路径
QPainterPath GraphicsRectItem::shape() const
{QPainterPath path;  // 创建绘制路径对象double rx,ry;       // 圆角的半径if(m_fRatioX<=0)rx=0;            // 如果宽度比例小于等于0,则圆角半径为0else {rx = m_width * m_fRatioX + 0.5;  // 否则计算圆角半径}if ( m_fRatioY <=0 )ry = 0;        elsery = m_height * m_fRatioY + 0.5;// 如果是圆角矩形if ( m_isRound )path.addRoundedRect(rect(),rx,ry);  // 添加圆角矩形到路径elsepath.addRect(rect());               // 否则添加普通矩形到路径return path;
}// 控制矩形的形状,通过手柄调整圆角的半径。
void GraphicsRectItem::control(int dir, const QPointF & delta)
{// 从父项的坐标系转换到当前图形项(GraphicsRectItem)的本地坐标系中。// 该坐标是10个关键点之一。QPointF local_pt = mapFromParent(delta);  // 根据不同的控制方向调整属性switch (dir) {// 如果是第9个关键点// 更新垂直方向的圆角半径比例。// 保证 y 不会超过矩形中心位置,也不会低于矩形顶部。case 9:{QRectF rect_roi = rect();                  // 获取当前矩形区域int local_y = local_pt.y();                // 获取本地的y坐标// 控制 local_y 位置在合理范围内,从而有效限制圆角的最大和最小半径。if(local_y > rect_roi.center().y() )  local_y = rect_roi.center().y(); // 限制不超过矩形中心           if(local_y < rect_roi.top())  local_y = rect_roi.top();   // 限制不超过顶部// 计算 m_fRatioY: 当前手柄位置 y 到矩形顶部的距离占矩形高度的比例。int H= rect_roi.height();     // 获取矩形高度if(H==0)  H = 1;    // 避免除零m_fRatioY = std::abs(((float)(rect_roi.top()- local_y)))/H;     // 计算并设置高度比例}break; // 水平调整圆角// 保证 x 不会低于中心位置,也不会超过矩形右侧。case 10:   {QRectF delta1 = rect();int x = local_pt.x();// 保证 x 不会低于中心位置,也不会超过矩形右侧。if(x < delta1.center().x() ) x = delta1.center().x();if(x>delta1.right()) x = delta1.right();int W= delta1.width();if(W==0) W=1;m_fRatioX = std::abs(((float)(delta1.right()-x)))/W;break;}// 用于调整原点位置case 11:{
//        setTransform(transform().translate(-local.x(),-local.y()));
//        setTransformOriginPoint(local.x(),local.y());
//        setTransform(transform().translate(local.x(),local.y()));m_originPoint = local_pt;  // 设置原点为当前本地坐标}break;default:break;}prepareGeometryChange();   // 准备几何图形的变化,通知Qt重新绘制updatehandles();           // 更新控制柄位置
}//  对矩形进行拉伸变换,根据给定的比例和参考点调整矩形的尺寸。
void GraphicsRectItem::stretch(int handle , double sx, double sy, const QPointF & origin)
{QTransform trans;  // 创建一个变换对象switch (handle) {  // 根据控制柄的不同类型调整拉伸比例case Right:case Left:sy = 1;        // 如果是左右控制柄,则只拉伸宽度,不改变高度break;case Top:case Bottom:       // 如果是上下控制柄,则只拉伸高度,不改变宽度sx = 1;break;default:break;}opposite_ = origin;  // 设置参考点,这里是矩形的中心点// 这样做的好处是能够以特定点为中心进行变换(如缩放),而不是默认的图形左上角或中心。trans.translate(origin.x(), origin.y());   // 将坐标系原点移动到指定位置trans.scale(sx,sy);                        // 这样缩放是以新的原点(origin)为中心进行的。trans.translate(-origin.x(), -origin.y()); // 再将坐标系原点移回去prepareGeometryChange();                        // 准备几何图形变化,通知Qt重新绘制m_localRect = trans.mapRect(m_initialRect);     // 使用变换对象映射初始矩形到新的大小m_width = m_localRect.width();                  // 更新宽度m_height = m_localRect.height();                // 更新高度updatehandles();                                // 更新控制柄位置}// 更新图形项的坐标和变换,以适应其场景中的位置变化。
void GraphicsRectItem::updateCoordinate()
{// 计算原点与矩形中心的偏移量QPointF pt1,pt2,delta;  // 声明坐标点和差值pt1 = mapToScene(transformOriginPoint());  // 获取变换原点的场景坐标pt2 = mapToScene(m_localRect.center());    // 获取矩形中心的场景坐标delta = pt1 - pt2;                         // 计算原点与中心点的差值// 如果没有父项,其坐标变换相对的是场景坐标系,而不是其他父项的坐标系。if (!parentItem() ){prepareGeometryChange();    // 准备几何图形变化// 移动m_localRect,将m_localRect的中心点移动到其原点位置。其中m_width是矩形的宽度// 通过将原点设为中心,任何缩放、旋转等变换都会以图形的中心为参考点m_localRect = QRectF(-m_width/2,-m_height/2,m_width,m_height);// 更新宽度 高度m_width = m_localRect.width();m_height = m_localRect.height();// 上面只是求变换参数,这里开始应用变换// 首先将图形移动到坐标系原点,以对齐新的坐标系中心。setTransform(transform().translate(delta.x(),delta.y()));  // 设置变换原点为坐标系原点,也就是m_localRect中心点。setTransformOriginPoint(m_localRect.center());// 它会调整图形项的场景坐标位置,但不会影响项的局部坐标系,也不会影响项的变换矩阵// 操作的是图形项的场景坐标,属于一种外部移动。moveBy(-delta.x(),-delta.y());  // 它会将变换矩阵中添加一个平移变换,改变图形项在其局部坐标系中的位置。// 改变的是图形项内部的变换矩阵,属于一种内部变换。setTransform(transform().translate(-delta.x(),-delta.y()));// 重置相对点opposite_ = QPointF(0,0);// 更新控制柄位置updatehandles();}m_initialRect = m_localRect;  // 更新初始矩形为当前矩形
}// 移动图形项到指定点。
void GraphicsRectItem::move(const QPointF &point)
{moveBy(point.x(),point.y());
}// 创建一个当前图形项的副本,复制其位置、变换、状态等属性。
QGraphicsItem *GraphicsRectItem::duplicate() const
{// 创建新的矩形项,复制当前矩形和是否圆角属性GraphicsRectItem * item = new GraphicsRectItem( rect().toRect(),m_isRound);// 设置其属性item->m_width = width();             // item->m_height = height();           // item->setPos(pos().x(),pos().y());   // 复制位置item->setPen(pen());                 // 复制画笔item->setBrush(brush());             // 复制画刷item->setTransform(transform());     // 复制变换item->setTransformOriginPoint(transformOriginPoint());   // 复制变换原点item->setRotation(rotation());       // 复制旋转角度item->setScale(scale());             // 复制缩放比例item->setZValue(zValue()+0.1);       // 复制Z值并稍作提升item->m_fRatioY = m_fRatioY;         //item->m_fRatioX = m_fRatioX;         //item->updateCoordinate();            // 更新复制项的坐标return item;
}// 从 XML 中加载图形项的状态。
bool GraphicsRectItem::loadFromXml(QXmlStreamReader * xml )
{m_isRound = (xml->name() == tr("roundrect"));  // 判断是否为圆角矩形if ( m_isRound ){m_fRatioX = xml->attributes().value(tr("rx")).toDouble();m_fRatioY = xml->attributes().value(tr("ry")).toDouble();}readBaseAttributes(xml);   // 读取基础属性updateCoordinate();xml->skipCurrentElement(); // 跳过当前元素return true;
}// 将图形项的状态保存到 XML 中。
bool GraphicsRectItem::saveToXml(QXmlStreamWriter * xml)
{if ( m_isRound ){xml->writeStartElement(tr("roundrect")); // 写入开始标签xml->writeAttribute(tr("rx"),QString("%1").arg(m_fRatioX));xml->writeAttribute(tr("ry"),QString("%1").arg(m_fRatioY));}elsexml->writeStartElement(tr("rect"));writeBaseAttributes(xml); // 写入基础属性xml->writeEndElement();   // 写入结束标签return true;
}// 更新手柄的位置,用于控制矩形的圆角和变换。
void GraphicsRectItem::updatehandles()
{const QRectF &geom = this->boundingRect();   // 获取当前边界矩形GraphicsItem::updatehandles();               // 调用基类的更新控制柄方法// 如果是圆角矩形if ( m_isRound ){// 更新第8号控制柄位置,即控制圆角y方向半径比例m_handles[8]->move( geom.right() , geom.top() + geom.height() * m_fRatioY );// // 更新第9号控制柄位置,即控制圆角x方向半径比例m_handles[9]->move( geom.right() - geom.width() * m_fRatioX , geom.top());//m_handles[10]->move(m_originPoint.x(),m_originPoint.y());}
}// 静态函数,重新计算点集的边界矩形
static
QRectF RecalcBounds(const QPolygonF&  pts)
{// 以第一个点为初始边界,后面不断地更新,最终找到真正的边界点。QRectF bounds(pts[0], QSize(0, 0));// 遍历所有点for (int i = 1; i < pts.count(); ++i){if (pts[i].x() < bounds.left())    // 更新左边界bounds.setLeft(pts[i].x());if (pts[i].x() > bounds.right())   // 更新右边界bounds.setRight(pts[i].x());if (pts[i].y() < bounds.top())     // 更新上边界bounds.setTop(pts[i].y());if (pts[i].y() > bounds.bottom())  // 更新下边界bounds.setBottom (pts[i].y());}// 确保矩形的表示形式统一,即总是左上角的坐标最小,宽度和高度为正值。bounds = bounds.normalized();  // 将边界标准化return bounds;
}// 绘制图形项
void GraphicsRectItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{painter->setPen(pen());      // 设置画笔painter->setBrush(brush());  // 设置画刷// 圆角的半径double rx,ry;if(m_fRatioX<=0) rx=0;                   // 如果宽度比例小于等于0,则圆角半径为0else rx = m_width * m_fRatioX + 0.5;     // 否则计算宽度圆角半径,半径+0.5,防止为0if ( m_fRatioY <=0 ) ry = 0;else ry = m_height * m_fRatioY + 0.5;// 绘制圆角矩形if ( m_isRound ) painter->drawRoundedRect(rect(),rx,ry);// 否则绘制普通矩形else painter->drawRect(rect().toRect());painter->setPen(Qt::blue);// 在矩形的中心点位置,绘制一个十字painter->drawLine(QLine(QPoint(opposite_.x()-6,opposite_.y()),QPoint(opposite_.x()+6,opposite_.y())));  // 绘制水平线painter->drawLine(QLine(QPoint(opposite_.x(),opposite_.y()-6),QPoint(opposite_.x(),opposite_.y()+6)));  // 绘制垂直线// 如果图形项被选中if (option->state & QStyle::State_Selected)qt_graphicsItem_highlightSelected(this, painter, option);  // 调用Qt函数高亮显示图形项
}

4.4 GraphicsEllipseItem类

GraphicsEllipseItem 类是一个用于管理和操作椭圆、扇形图形项的类,它继承自 GraphicsRectItem,扩展了矩形图形项的功能,以支持椭圆形状的绘制和交互。 

class GraphicsEllipseItem :public GraphicsRectItem
{
public:// 构造函数,初始化椭圆项GraphicsEllipseItem(const QRect & rect , QGraphicsItem * parent = 0);// 返回椭圆的绘制路径QPainterPath shape() const;// 根据控制方向和偏移量调整椭圆的属性void control(int dir, const QPointF & delta );// 返回椭圆的边界矩形QRectF boundingRect() const ;// 复制当前椭圆项并返回新的实例QGraphicsItem *duplicate() const;// 返回椭圆的显示名称QString displayName() const { return tr("ellipse"); }// 从 XML 文件加载椭圆的属性virtual bool loadFromXml(QXmlStreamReader * xml );// 保存椭圆的属性到 XML 文件virtual bool saveToXml( QXmlStreamWriter * xml );
protected:// 更新控制柄的位置void updatehandles();// 绘制椭圆void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);int   m_startAngle;  // 起始角度int   m_spanAngle;   // 跨度角度
};GraphicsEllipseItem::GraphicsEllipseItem(const QRect & rect ,QGraphicsItem *parent):GraphicsRectItem(rect,parent)
{m_startAngle = 40;   // 初始化起始角度m_spanAngle  = 400;  // 初始化跨度角度// 创建用于控制起始角度的控制柄 (编号9)SizeHandleRect *shr = new SizeHandleRect(this, 9 , true);  // 9特指是圆心点m_handles.push_back(shr);// 创建用于控制终止角度的控制柄 (编号10)shr = new SizeHandleRect(this, 10 , true);                 // 10特指是圆上点m_handles.push_back(shr);// 更新所有控制柄的位置updatehandles();
}// 返回椭圆的绘制路径
QPainterPath GraphicsEllipseItem::shape() const
{QPainterPath path;// 使用较大的作为起始角度。int startAngle = m_startAngle <= m_spanAngle ? m_startAngle : m_spanAngle;// 使用较小的作为结束角度。int endAngle = m_startAngle >= m_spanAngle ? m_startAngle : m_spanAngle;// 确保跨度不超过360度if(endAngle - startAngle > 360) endAngle = startAngle + 360;// 如果 m_localRect 为空矩形,返回空路径if (m_localRect.isNull()) return path;// 如果跨度不是完整的360度。说明不是完成的椭圆,是扇形结构if ((endAngle - startAngle) % 360 != 0 ) {path.moveTo(m_localRect.center());     // 移动到矩形中心     path.arcTo(m_localRect, startAngle, endAngle - startAngle);  // 绘制扇形} // 说明是完整的椭圆else {path.addEllipse(m_localRect);}path.closeSubpath();  // 关闭子路径return path;
}// 根据控制方向和偏移量调整椭圆的属性
void GraphicsEllipseItem::control(int dir, const QPointF & delta)
{QPointF local = mapFromScene(delta);  // 将控制点的场景坐标转换为本地坐标switch (dir) {  // 根据不同的控制方向调整属性// 起始点角度:操作点和中心点连线的角度case 9:{qreal len_y = local.y() - m_localRect.center().y();qreal len_x = local.x() - m_localRect.center().x();m_startAngle = -atan2(len_y,len_x)*180/M_PI;}break;// 结束点:操作点和中心点连线的角度case 10:{qreal len_y = local.y() - m_localRect.center().y();qreal len_x = local.x() - m_localRect.center().x();m_spanAngle = -atan2(len_y,len_x)*180/M_PI;break;}default:break;}prepareGeometryChange(); // 准备几何形状变化// 确保起始角度和终止角度的关系正确if ( m_startAngle > m_spanAngle )m_startAngle-=360;if ( m_spanAngle < m_startAngle ){qreal tmp = m_spanAngle;m_spanAngle = m_startAngle;m_startAngle = tmp;}// 确保跨度不超过360度if ( qAbs(m_spanAngle-m_startAngle) > 360 ){m_startAngle = 40;  // 重置为默认起始角度m_spanAngle = 400;  // 重置为默认跨度角度}// 更新控制柄位置updatehandles();
}// 返回椭圆的边界矩形
QRectF GraphicsEllipseItem::boundingRect() const
{return shape().controlPointRect();  // 使用形状的控制点矩形作为边界矩形
}// 复制当前椭圆项并返回新的实例
QGraphicsItem *GraphicsEllipseItem::duplicate() const
{// 创建新的椭圆项GraphicsEllipseItem * item = new GraphicsEllipseItem( m_localRect.toRect() );// 复制属性item->m_width = width();item->m_height = height();item->m_startAngle = m_startAngle;item->m_spanAngle   = m_spanAngle;   // 复制跨度角度m_startAngle - m_endAngle// 复制其他属性item->setPos(pos().x(),pos().y());item->setPen(pen());item->setBrush(brush());item->setTransform(transform());item->setTransformOriginPoint(transformOriginPoint());item->setRotation(rotation());item->setScale(scale());item->setZValue(zValue()+0.1);  // 提升 z 值,使之稍微位于上层item->updateCoordinate();return item;
}// 从 XML 文件加载椭圆的属性
bool GraphicsEllipseItem::loadFromXml(QXmlStreamReader *xml)
{m_startAngle = xml->attributes().value("startAngle").toInt();m_spanAngle  = xml->attributes().value("spanAngle").toInt();readBaseAttributes(xml);xml->skipCurrentElement();updateCoordinate();return true;
}// 保存椭圆的属性到 XML 文件
bool GraphicsEllipseItem::saveToXml(QXmlStreamWriter * xml)
{xml->writeStartElement(tr("ellipse"));  // 写入元素开始标签xml->writeAttribute("startAngle",QString("%1").arg(m_startAngle));xml->writeAttribute("spanAngle",QString("%1").arg(m_spanAngle));writeBaseAttributes(xml);xml->writeEndElement();return true;
}// 更新控制柄的位置,在调用此函数前,m_localRect的中心点移动到其原点位置
void GraphicsEllipseItem::updatehandles()
{// 调用基类的更新控制柄方法GraphicsItem::updatehandles();// 计算本地矩形,将m_localRect的中心点移动到其原点位置QRectF local = QRectF(-m_width/2,-m_height/2,m_width,m_height);// 计算矩形中心点的偏移量QPointF delta = local.center() - m_localRect.center();//qDebug() << delta;  // 一直是(0,0),可能是确认确保两者没有偏移。// 计算起始角度控制柄的位置qreal x = (m_width/2) * cos( -m_startAngle * M_PI / 180 );qreal y = (m_height/2) * sin( -m_startAngle * M_PI / 180);m_handles.at(8)->move(x-delta.x(),y-delta.y());  // 移动控制柄到新的位置// 计算终止角度控制柄的位置x = (m_width/2) * cos( -m_spanAngle * M_PI / 180);y = (m_height/2) * sin(-m_spanAngle * M_PI / 180);m_handles.at(9)->move(x-delta.x(),y-delta.y());  // 移动控制柄到新的位置
}// 绘制椭圆
void GraphicsEllipseItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{Q_UNUSED(option);  // 忽略未使用的参数Q_UNUSED(widget);  // 忽略未使用的参数QColor c = brushColor();  // 获取画刷颜色QRectF rc = m_localRect;  // 获取本地矩形// 设置画笔painter->setPen(pen());   QBrush b(c);              painter->setBrush(b);   // 使用较大的作为起始角度int startAngle = m_startAngle <= m_spanAngle ? m_startAngle : m_spanAngle;// 使用较小的作为结束角度int endAngle = m_startAngle >= m_spanAngle ? m_startAngle : m_spanAngle;// 确保跨度不超过360度if(endAngle - startAngle > 360) endAngle = startAngle + 360;// 如果是完整的椭圆if (qAbs(endAngle-startAngle) % (360) == 0) painter->drawEllipse(m_localRect);// 否则绘制扇形else painter->drawPie(m_localRect, startAngle * 16 , (endAngle-startAngle) * 16);// 如果项处于选中状态,绘制选中效果if (option->state & QStyle::State_Selected)qt_graphicsItem_highlightSelected(this, painter, option);
}

4.5 GraphicsPolygonItem类

 GraphicsPolygonItem 类是一个自定义的 QGraphicsItem,用于在 Qt 图形视图框架中绘制和操作多边形图形。它继承自 GraphicsItem,提供了对多边形形状的灵活控制和操作。

class GraphicsPolygonItem : public GraphicsItem
{
public:// 构造函数,初始化多边形项GraphicsPolygonItem(QGraphicsItem * parent = 0);// 返回多边形的边界矩形QRectF boundingRect() const ;// 返回多边形的绘制路径QPainterPath shape() const;// 向多边形中添加一个点virtual void addPoint( const QPointF & point ) ;// 结束当前多边形点的添加virtual void endPoint(const QPointF & point );// 根据控制方向和偏移量调整多边形的属性void control(int dir, const QPointF & delta);// 根据控制柄和缩放比例拉伸多边形void stretch( int handle , double sx , double sy , const QPointF & origin );// 更新多边形的坐标void updateCoordinate ();// 从 XML 文件加载多边形的属性virtual bool loadFromXml(QXmlStreamReader * xml );// 保存多边形的属性到 XML 文件virtual bool saveToXml( QXmlStreamWriter * xml );// 返回多边形的显示名称QString displayName() const { return tr("polygon"); }// 复制当前多边形项并返回新的实例QGraphicsItem *duplicate() const;protected:// 更新控制柄的位置void updatehandles();// 绘制多边形void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);QPolygonF m_points;           // 存储多边形的点QPolygonF m_initialPoints;    // 存储多边形的初始点
};GraphicsPolygonItem::GraphicsPolygonItem(QGraphicsItem *parent):GraphicsItem(parent)
{// handlesm_points.clear();          // 初始化点集合为空m_pen = QPen(Qt::black);   // 设置画笔为黑色
}// 返回多边形的边界矩形
QRectF GraphicsPolygonItem::boundingRect() const
{return shape().controlPointRect();
}// 返回多边形的绘制路径
QPainterPath GraphicsPolygonItem::shape() const
{QPainterPath path;// 将多边形的点添加到绘制路径path.addPolygon(m_points);// 关闭子路径,使路径形成封闭的多边形path.closeSubpath();// 根据当前的画笔宽度调整路径,用于检测碰撞。// 这个函数会根据画笔的宽度来扩展或缩小路径,从而创建一个包含画笔影响的路径。return qt_graphicsItem_shapeFromPath(path,pen());
}void GraphicsPolygonItem::addPoint(const QPointF &point)
{// 将场景坐标的点转换为本地坐标并添加到点集合中m_points.append(mapFromScene(point));// 获取当前点的方向编号:点的个数就是编号,从1+8=9开始,m_points初始化有一个(0,0)坐标。int dir = m_points.count();// 创建一个新的控制柄用于调整点的位置SizeHandleRect *shr = new SizeHandleRect(this, dir+Left, true);// 设置控制柄为活动状态shr->setState(SelectionHandleActive);// 将控制柄添加到控制柄集合中m_handles.push_back(shr);
}void GraphicsPolygonItem::control(int dir, const QPointF &delta)
{// 将控制点的场景坐标转换为本地坐标QPointF pt = mapFromScene(delta);// 检查控制方向是否有效,小于等于Left,说明一个点都没有。if ( dir <= Left ) return ;// 更新指定控制点的位置m_points[dir - Left -1] = pt;// 准备更新几何图形prepareGeometryChange();// 更新多边形的边界矩形m_localRect = m_points.boundingRect();// 更新多边形的宽度和高度m_width = m_localRect.width();          m_height = m_localRect.height();// 保存更新后的点集合m_initialPoints = m_points;// 更新控制柄的位置updatehandles();
}void GraphicsPolygonItem::stretch(int handle, double sx, double sy, const QPointF &origin)
{QTransform trans;// 根据控制柄方向调整缩放比例switch (handle) {case Right:case Left:sy = 1;          // 水平方向的控制柄只影响水平缩放break;case Top:case Bottom:sx = 1;          // 垂直方向的控制柄只影响垂直缩放break;default:break;}trans.translate(origin.x(),origin.y());   // 将原点平移到目标点trans.scale(sx,sy);    // 执行缩放trans.translate(-origin.x(),-origin.y()); // 将原点平移到目标点// 准备更新几何图形prepareGeometryChange();// 将初始点集合按变换矩阵变换,更新多边形点集合m_points = trans.map(m_initialPoints);// 更新边界矩形m_localRect = m_points.boundingRect();m_width = m_localRect.width();m_height = m_localRect.height();// 更新控制柄的位置updatehandles();
}void GraphicsPolygonItem::updateCoordinate()
{// 将局部顶点坐标转换成场景坐标。QPolygonF pts = mapToScene(m_points); QPointF pt1, pt2, delta;if (parentItem()==NULL){// 当前变换原点的场景坐标pt1 = mapToScene(transformOriginPoint());// 边界中心的场景坐标pt2 = mapToScene(boundingRect().center());delta = pt1 - pt2;//qDebug() << delta << pt1 << pt2; //存在偏移。// 更新所有点的坐标以考虑偏移量for (int i = 0; i < pts.count() ; ++i )pts[i]+=delta;prepareGeometryChange();// 更新多边形的本地点集合m_points = mapFromScene(pts);// 更新边界矩形m_localRect = m_points.boundingRect();m_width = m_localRect.width();m_height = m_localRect.height();// 更新图形的变换以适应新的原点setTransform(transform().translate(delta.x(),delta.y()));//setTransformOriginPoint(boundingRect().center());moveBy(-delta.x(),-delta.y());setTransform(transform().translate(-delta.x(),-delta.y()));// 更新控制柄的位置updatehandles();}// 保存更新后的点集合m_initialPoints = m_points;}bool GraphicsPolygonItem::loadFromXml(QXmlStreamReader *xml)
{readBaseAttributes(xml);// 逐个读取多边形的点while(xml->readNextStartElement()){if (xml->name()=="point"){// 获取点的x和y坐标qreal x = xml->attributes().value("x").toDouble();qreal y = xml->attributes().value("y").toDouble();m_points.append(QPointF(x,y));// 创建控制柄int dir = m_points.count();SizeHandleRect *shr = new SizeHandleRect(this, dir+Left, true);m_handles.push_back(shr);// 跳过当前元素xml->skipCurrentElement();}else // 跳过非点元素xml->skipCurrentElement();}// 更新多边形的坐标updateCoordinate();return true;
}bool GraphicsPolygonItem::saveToXml(QXmlStreamWriter *xml)
{// 开始写入多边形元素xml->writeStartElement("polygon");writeBaseAttributes(xml);// 保存每个点的坐标for ( int i = 0 ; i < m_points.count();++i){xml->writeStartElement("point");xml->writeAttribute("x",QString("%1").arg(m_points[i].x()));xml->writeAttribute("y",QString("%1").arg(m_points[i].y()));xml->writeEndElement();}xml->writeEndElement();return true;
}void GraphicsPolygonItem::endPoint(const QPointF & point)
{Q_UNUSED(point);int nPoints = m_points.count();// 检查多边形最后两个点是否相同或水平对齐if( nPoints > 2 && (m_points[nPoints-1] == m_points[nPoints-2]/*相同*/ ||  /*水平对齐,只相差一个像素*/m_points[nPoints-1].x() - 1 == m_points[nPoints-2].x() &&m_points[nPoints-1].y() == m_points[nPoints-2].y())){// 删除重复的最后一个控制柄delete m_handles[Left + nPoints-1];// 移除重复点m_points.remove(nPoints-1);// 调整控制柄集合的大小m_handles.resize(Left + nPoints-1);}// 更新初始点集合m_initialPoints = m_points;
}QGraphicsItem *GraphicsPolygonItem::duplicate() const
{// 创建一个新的多边形项并复制当前项的属性GraphicsPolygonItem * item = new GraphicsPolygonItem( );item->m_width = width();item->m_height = height();item->m_points = m_points;// 为每个点创建控制柄for ( int i = 0 ; i < m_points.size() ; ++i ){item->m_handles.push_back(new SizeHandleRect(item,Left+i+1,true));}item->setPos(pos().x(),pos().y());item->setPen(pen());item->setBrush(brush());item->setTransform(transform());item->setTransformOriginPoint(transformOriginPoint());item->setRotation(rotation());item->setScale(scale());item->setZValue(zValue()+0.1);  // 提升复制项的层级以避免重叠item->updateCoordinate();return item;
}void GraphicsPolygonItem::updatehandles()
{// 调用基类的方法更新控制柄GraphicsItem::updatehandles();// 根据多边形的点更新控制柄的位置for ( int i = 0 ; i < m_points.size(); ++i ){m_handles[Left+i]->move(m_points[i].x() ,m_points[i].y() );}
}void GraphicsPolygonItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{Q_UNUSED(option);Q_UNUSED(widget);// 创建一个线性渐变的笔刷QColor c = brushColor();QLinearGradient result(boundingRect().topLeft(), boundingRect().topRight());result.setColorAt(0, c.dark(150));result.setColorAt(0.5, c.light(200));result.setColorAt(1, c.dark(150));painter->setBrush(result);// 设置画笔并绘制多边形painter->setPen(pen());painter->drawPolygon(m_points);// 如果项被选中,绘制选中高亮效果if (option->state & QStyle::State_Selected)qt_graphicsItem_highlightSelected(this, painter, option);
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 举债豪赌,光正眼科深陷“资本迷局”
  • zookeeper是啥?在kafka中有什么作用
  • OpenGL函数之wglCreateContext
  • 在 csv 上增加计算列
  • 牛客小白月赛100(下)
  • Linux 8250串口控制器
  • 每日一练 | 根据优先级信息划分业务类型
  • IP学习——Fiveday
  • 十二、新版UI
  • 斯坦福大学论文润色chat-gpt指令
  • 前端框架大观:探索现代Web开发的基石
  • 走近张大鹏教授:哈工大走出的中国第一位人工智能博士
  • java八股!1
  • 非网站业务怎么接入高防IP抗DDoS
  • 《数字信号处理》学习06-因果系统与稳定系统
  • css系列之关于字体的事
  • JAVA SE 6 GC调优笔记
  • JavaScript 一些 DOM 的知识点
  • javascript从右向左截取指定位数字符的3种方法
  • Java编程基础24——递归练习
  • Java反射-动态类加载和重新加载
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • PHP 7 修改了什么呢 -- 2
  • PHP变量
  • Protobuf3语言指南
  • Python_OOP
  • Python中eval与exec的使用及区别
  • 从地狱到天堂,Node 回调向 async/await 转变
  • 二维平面内的碰撞检测【一】
  • 三分钟教你同步 Visual Studio Code 设置
  • 使用 5W1H 写出高可读的 Git Commit Message
  • 摩拜创始人胡玮炜也彻底离开了,共享单车行业还有未来吗? ...
  • ​ubuntu下安装kvm虚拟机
  • ‌移动管家手机智能控制汽车系统
  • # Redis 入门到精通(八)-- 服务器配置-redis.conf配置与高级数据类型
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • (+3)1.3敏捷宣言与敏捷过程的特点
  • (C语言)fgets与fputs函数详解
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (Ruby)Ubuntu12.04安装Rails环境
  • (二)原生js案例之数码时钟计时
  • (过滤器)Filter和(监听器)listener
  • (七)glDrawArry绘制
  • (三) prometheus + grafana + alertmanager 配置Redis监控
  • (深度全面解析)ChatGPT的重大更新给创业者带来了哪些红利机会
  • (算法)Travel Information Center
  • (转)memcache、redis缓存
  • (自适应手机端)行业协会机构网站模板
  • .pyc文件还原.py文件_Python什么情况下会生成pyc文件?
  • ??在JSP中,java和JavaScript如何交互?
  • @FeignClient注解,fallback和fallbackFactory
  • @for /l %i in (1,1,10) do md %i 批处理自动建立目录