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

news2024/12/23 6:04:27

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.h

enum 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;  // 设置当前绘制形状为 selection
    setCursor(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;        
        else
            selectMode =  move;         // 否则光标就是没有碰撞,而是在形状内部,设置为移动模式

        if ( nDragHandle!= Handle_None && nDragHandle <= Left ){   // 如果是大小调整模式
            opposite_ = item->opposite(nDragHandle);   // 获取相对点,即与当前拖动的控制点相对的点。
            if( opposite_.x() == 0 )
                opposite_.setX(1);  // 防止出现零值,设置为 1
            if (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 0
        if ( 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);  // 防止零值,设置为 1
                    if (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 0
        if ( 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;    // 初始化上一次角度为0
    dashRect = 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;  // 记录当前角度为 lastAngle
                selectMode = 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 为 NULL
    m_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++;                               // 点的数量加 1
    selectMode = 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 设置为 NULL
        selectMode = none;                           // 设置选择模式为 none
        c_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));

        // 初始化宽度和高度为0
        m_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);}

    // 复制图形项,默认返回NULL
    virtual 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;
                else
                    dir = LeftTop;
                break;
            case RightBottom:
                if ( scale.x() < 0 && scale.y() < 0 )
                    dir = LeftTop;
                else if ( scale.x() > 0 && scale.y() < 0 )
                    dir = RightTop;
                else
                    dir = LeftBottom;
                break;
            case LeftBottom:
                if ( scale.x() < 0 && scale.y() < 0 )
                    dir = RightTop;
                else if ( scale.x() > 0 && scale.y() < 0 )
                    dir = LeftTop;
                else
                    dir = RightBottom;
                break;
            case LeftTop:
                if ( scale.x() < 0 && scale.y() < 0 )
                    dir = RightBottom;
                else if ( scale.x() > 0 && scale.y() < 0 )
                    dir = LeftBottom;
                else
                    dir = 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 方法
}

待续。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2113557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络(四) —— 简单Tcp网络程序

目录 一&#xff0c;服务器初始化 1.0 部分文件代码 1.1 关于Tcp协议 1.2 创建和绑定套接字 1.3 监听 二&#xff0c;服务器启动 2.1 获取连接 2.2 提供服务 2.3 客户端启动源文件 Main.cc 二&#xff0c;客户端编写 2.1 关于Tcp客户端 2.2 客户端代码 2.3 效果…

Linux第十一节课 - 进程

一个程序从磁盘以文件的形式加载到内存之后&#xff0c;已经变成了进程&#xff01; 引入管理者和被管理者 1、管理者和被管理者不需要见面&#xff01;&#xff08;例如学生和校长&#xff01;&#xff09; 2、管理者在不见被管理者的情况下&#xff0c;如何做好管理呢&…

隐私计算实训营:SplitRec:当拆分学习遇上推荐系统

拆分学习的概念 拆分学习的核心思想是拆分网络结构。每一个参与方拥有模型结构的一部分&#xff0c;所有参与方的模型合在一起形成一个完整的模型。训练过程中&#xff0c;不同参与方只对本地模型进行正向或者反向传播计算&#xff0c;并将计算结果传递给下一个参与方。多个参…

文件操作与隐写

一、文件类型的识别 1、文件头完好情况&#xff1a; &#xff08;1&#xff09;file命令 使用file命令识别&#xff1a;识别出file.doc为jpg类型 &#xff08;2&#xff09;winhex 通过winhex工具查看文件头类型&#xff0c;根据文件头部内容去判断文件的类型 eg:JPG类型 &a…

Wni11 下 WSL 安装 CentOS

Wni11 下 WSL 安装 CentOS 方法一、安装包安装下载包安装安装打开 CentOS1. 从 Windows 终端 打开2. 从 PowerShell 打开 方法二、导入 CentOS 的 tar 文件进行安装0. 查看版本&#xff08;可选&#xff09;1. 导出 Docker 容器到 tar 文件2. 将 tar 文件导入 WSL2.1. 导入 tar…

macos安装ArgoCD

本文主要介绍如何在macos上安装并访问argo 我环境上已经安装了minikube&#xff0c;所以只需要启动minikube然后通过命令行安装argocd。 minikube start kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/st…

OpenGL(二)-更详细版的三角形

在上篇blog中已经画了一个三角形了&#xff0c;这篇讲解一下一个三角形的渲染过程。 上篇blog中的glbegin搭配glend的流程&#xff0c;在OpenGL3.2中已经被弃用了&#xff0c;3.3以后推荐使用VBOEBOVAO的流程。 图形渲染管线 作用&#xff1a;将三维坐标经过一系列变换&#x…

【Day09】

目录 Mybatis-基础操作-环境准备 Mybatis-基础操作-删除 Mybatis-基础操作-删除&#xff08;预编译SQL&#xff09; Mybatis-基础操作-新增 Mybatis-基础操作-新增(主键返回) Mybatis-基础操作-更新 Mybatis-基础操作-查询&#xff08;根据ID查询&#xff09; Mybatis-基…

YOLOv8改进 | Conv篇 | YOLOv8引入DWR

1. DWR介绍 1.1 摘要:当前的许多工作直接采用多速率深度扩张卷积从一个输入特征图中同时捕获多尺度上下文信息,从而提高实时语义分割的特征提取效率。 然而,这种设计可能会因为结构和超参数的不合理而导致多尺度上下文信息的访问困难。 为了降低多尺度上下文信息的绘制难度…

【系统分析师】-2024-2010年系统分析师历年论文题目

目录 2024.5月 2023 2022 2021 2020 2019 预测2024年11月 2024.5月 信息系统工程 论基于架构的软件设计方法信息系统工程 论性能测试方法及其应用信息系统工程 论云原生应用开发数据库及应用 论多源数据集成方法及其应用 2023 信息系统工…

HTB-Pennyworth(cve查询 和 exp使用)

前言 各位师傅大家好&#xff0c;我是qmx_07,今天给大家讲解Pennyworth靶场 渗透过程 信息搜集 服务器端口开放了8080http端口 访问网站 服务器使用jenkins cms系统&#xff0c;版本是2.289.1 通过弱口令爆破&#xff0c;账户是root,密码是password 通过命令执行nday 连…

Leetcode面试经典150题-76.最小覆盖子串

解法都在代码里&#xff0c;不懂就留言或者私信 理论上提交这个就是最优解 class Solution {public String minWindow(String s, String t) {if(s.length() < t.length()) {return "";}/**转成字符数组 */char[] sArr s.toCharArray();char[] tArr t.toCharAr…

Docker编译环境的使用(ubuntu)

目录 Ubuntu安装docker 重启docker 拉取镜像 进入docker安装软件 提交docker 添加用户到docker组 进入docker 添加build用户 停止容器 保存docker镜像 load镜像 删除容器 Ubuntu安装docker sudo apt install docker.io 国内可用的源 Welcome to nginx! (tence…

git使用基础教程

(一)Git下载 git官网 - downloads 创建本地数据仓 1.创建文件夹 2.当前目录 cmd ---git init gitee.com注册登录 git创建项目 ide 项目地址本地数据库 ide项目上传云端 (一)git 1.git下载 2.新建仓库 3. 配置Configure---Version Control---Git----path to Git Get from…

某里227逆向分析

声明: 该文章为学习使用,严禁用于商业用途和非法用途,违者后果自负,由此产生的一切后果均与作者无关。 本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除! 前言 这次会简单的讲解…

【中国国际航空-注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

【LeetCode】最接近的三数之和

题目要求 解题思路 这道题解题方法和三数之和解题思路一样&#xff0c;可以参考上一篇博客 代码实现 class Solution { public:int threeSumClosest(vector<int>& nums, int target) {//排序sort(nums.begin(),nums.end());int lennums.size();//固定一个&#x…

流媒体协议RTSP(其二)

欢迎诸位来阅读在下的博文~ 在这里&#xff0c;在下会不定期发表一些浅薄的知识和经验&#xff0c;望诸位能与在下多多交流&#xff0c;共同努力 文章目录 前期博客一、RTSP简介二、请求消息结构三、应答消息结构四、RTSP交互流程 前期博客 流媒体与直播的基础理论&#xff08…

Datawhle X 李宏毅苹果书AI夏令营深度学习笔记之——卷积神经网络的前世今生

一、卷积神经网络简介 卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;是一种深度学习模型&#xff0c;尤其擅长处理图像和视频等高维度的数据。CNN 通过模仿人类视觉系统的工作方式&#xff0c;自动学习数据中的空间层次结构&#xff0c;使得它在计算…

启动第一个docker容器

1 、 docker pull ubuntu:20.04 下载镜像 2、 docker image ls 查看镜像 3、 docker run --nametest -itd 9df6d6105df2 创建并运行一个容器 4、 查看容器 docker ps -a 5、 登录容器 docker exec -it test /bin/bash 6 退出容器 exit 7 删除容器 docker rm 238514292c…