基于Qt的在QGraphicsView中绘制带有可动拐点的连线的一种方法

摘要:本文详细介绍了基于Qt框架在QGraphicsView 中实现带有可动拐点连线的绘制方法。通过自定CustomItem和CustomPath类,结合QGraphicsItem的几何变化事件与QPainterPath的路径绘制功能,实现了动态连线的基本框架。进一步探讨了平行线偏移规则的设计与拐点交叉问题的解决方案,通过角平分线计算和交叉检测优化路径连接效果。最终提出了一种支持用户拖拽拐点、自动刷新连线的交互式图形方案,并展示了代码实现与效果演示,为复杂图形编辑工具的开发提供了参考。

关键词:QGraphicsViewQGraphicsItemQPainterPath、可动拐点、平行线偏移、角平分线、交叉检测、Qt图形框架

完整代码见最后。

1、QGraphicsItem和QPainterPath的基础使用

做一点准备工作,先用一个简单的案例,创建代码基本框架。

问题描述:

已知起点和终点,如何绘制过两点的线段?

要求:点图形可动,连线图形可刷新

解决思路:

1、准备工作,需要创建可动的图形类CustomItem和连线类CustomPath,以便观察各种情况。

2、使用QPainterPathmoveTo()lineTo()绘制连线。

3、在图形类CustomItemitemChange函数中刷新连线。

代码如下:

C++ 复制代码
class CustomPath;
// 图形类,描述起点和终点
class CustomItem : public QGraphicsRectItem
{
public:
    CustomItem(QGraphicsItem *parent = nullptr);
    void addPath(CustomPath *path);

protected:
    QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;

private:
    QList<CustomPath *> mPathList; // 连线列表
};

CustomItem::CustomItem(QGraphicsItem *parent) : QGraphicsRectItem(parent)
{
    // 设置形状
    setRect(-5, -5, 10, 10);
    // 设置颜色
    setBrush(Qt::black);
    // 设置可移动
    setFlag(QGraphicsItem::ItemIsMovable, true);
    // 设置可发送几何变动,可在itemChange中进行检测
    setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}
// 添加连线
void CustomItem::addPath(CustomPath *path)
{
    mPathList.append(path);
}

QVariant CustomItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
    switch (change) {
    // 当位置变动时,刷新连线
    case QGraphicsItem::ItemPositionHasChanged:
    {
        for (int i = 0, size = mPathList.size(); i < size; ++i) {
            mPathList.at(i)->updatePosition();
        }
    }

    default:
        break;
    }

    return QGraphicsItem::itemChange(change, value);
}

在这段代码中,创建了图形类CustomItem,设置图形可移动,同时在移动时刷新与图形相连的连线。

C++ 复制代码
// 连线类,描述连线
class CustomPath : public QGraphicsPathItem
{
public:
    CustomPath(QGraphicsItem *start, QGraphicsItem *end, QGraphicsItem *parent = nullptr);
    void updatePosition(); // 刷新连线

private:
    QGraphicsItem *mStartItem = nullptr;  // 起点
    QGraphicsItem *mEndItem = nullptr;    // 终点
};

CustomPath::CustomPath(QGraphicsItem *start, QGraphicsItem *end, QGraphicsItem *parent)
    : QGraphicsPathItem(parent), mStartItem(start), mEndItem(end)
{
    // 设置绘制画笔,颜色黑色,笔宽为1
    setPen(QPen(Qt::black, 1));
}
// 刷新连线
void CustomPath::updatePosition()
{
    // 获取两端的位置
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    // 绘制连线
    QPainterPath path;
    path.moveTo(start_pos);
    path.lineTo(end_pos);
    // 设置连线
    setPath(path);
}

在这段代码中,创建了连线类CustomPath,主要作用是刷新连线updatePosition函数。

C++ 复制代码
    // 创建画布
    QGraphicsScene *scene = new QGraphicsScene(this);
    ui->graphicsView->setScene(scene);

    // 创建起点
    CustomItem *item_start = new CustomItem;
    item_start->setPos(100, 100);
    scene->addItem(item_start);
    // 创建终点
    CustomItem *item_end = new CustomItem;
    item_end->setPos(200, 200);
    scene->addItem(item_end);

    // 创建连线
    CustomPath *path = new CustomPath(item_start, item_end);
    item_start->addPath(path);
    item_end->addPath(path);
    path->updatePosition();
    scene->addItem(path);

在这段代码中,创建了点A和点B,设置它们的位置,创建了连线并刷新。

效果如下:

2、如何创建平行线

现在要在同一个连线类CustomPath中绘制两条连线,引出偏移规则的确定方法。

问题描述:

现有点A和点B,分别在其周围找两点(点A1A2,点B1B2),如何绘制两条平行线?

解决思路:

只需确定偏移规则。比如这两点分别位于点的左右或者上下两侧。这里设置为左右偏移5个像素点。

代码如下:

C++ 复制代码
void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();
    // 起点左右偏移
    QPointF start_p1 = start_pos + QPointF(-5, 0);
    QPointF start_p2 = start_pos + QPointF(5, 0);
    // 终点左右偏移
    QPointF end_p1 = end_pos + QPointF(-5, 0);
    QPointF end_p2 = end_pos + QPointF(5, 0);

    // 两次连线
    QPainterPath path;
    path.moveTo(start_p1);
    path.lineTo(end_p1);
    path.moveTo(start_p2);
    path.lineTo(end_p2);

    setPath(path);
}

这段代码中,将起点和终点分别左右偏移五个像素,然后连线,使用两次moveTo()lineTo()

效果如下:

3、偏移规则的问题

问题描述:

可以发现(如图),当移动两个点位于同一水平线时,连线会发生重叠。

解决思路:

这是由于偏移规则的缺陷。无论是上下偏移还是左右偏移或者其他的偏移,都会产生这种情况。

那么,这两个偏移点必须根据情况发生变化。

确定新的偏移规则:斜向偏移,直线如果斜向右上(或者斜向左下),则偏移点为(5,5)和(-5,-5);直线如果斜向左上(或者斜向右下),则偏移点为(5,-5)和(-5,5)。

代码如下:

C++ 复制代码
// 偏移规则
QPointF CustomPath::getOffset(const QPointF &p1, const QPointF &p2)
{
    QPointF dp = p1 - p2;
    QPointF offset;
    // 根据差值判断
    if (dp.x() * dp.y() >= 0) {
        // 设置偏移量
        offset = QPointF(-5, 5);
    } else {
        offset = QPointF(5, 5);
    }
    return offset;
}

void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    QPointF offset = getOffset(start_pos, end_pos);

    // 起点和终点偏移
    QPointF start_p1 = start_pos + offset;
    QPointF start_p2 = start_pos - offset;
    QPointF end_p1 = end_pos + offset;
    QPointF end_p2 = end_pos - offset;

    QPainterPath path;
    path.moveTo(start_p1);
    path.lineTo(end_p1);
    path.moveTo(start_p2);
    path.lineTo(end_p2);

    setPath(path);
}

在这段代码中,使用了两点xy轴的差值进行判断斜向方向,同时设置了偏移量。

效果如下:

4、带有拐点的连线

问题描述:

重新从解决简单的问题开始:现在有点X,需要创建从A->X->B的单条连线,如何实现?

解决思路:

确定拐点位置,插入使用lineTo()即可。

代码如下:

C++ 复制代码
// 拐点类
class CustomPoint : public QGraphicsEllipseItem
{
public:
    CustomPoint(QGraphicsItem *parent = nullptr);
    void setPathItem(CustomPath *pathItem);

protected:
    QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;

private:
    CustomPath *mPathItem = nullptr; // 拐点所属连线
};

CustomPoint::CustomPoint(QGraphicsItem *parent)
    : QGraphicsEllipseItem(parent)
{
    // 设置图形为圆形
    setRect(-2, -2, 4, 4);
    setBrush(Qt::black);
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}

QVariant CustomPoint::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
    switch (change) {
    case QGraphicsItem::ItemPositionHasChanged:
    {
        // 当拐点位置发生变化,刷新连线
        if (mPathItem) {
            mPathItem->updatePosition();
        }
    }

    default:
        break;
    }

    return QGraphicsItem::itemChange(change, value);
}

void CustomPoint::setPathItem(CustomPath *pathItem)
{
    mPathItem = pathItem;
}

在这段代码中,创建了拐点类CustomPoint,设置它的形状、笔刷、可移动属性;当拐点位置发生变化时,刷线连线。

C++ 复制代码
// 对部分代码进行修改
class CustomPath : public QGraphicsPathItem
{
public:
    CustomPath(QGraphicsItem *start, QGraphicsItem *end, QGraphicsItem *parent = nullptr);
    void updatePosition();
    void setPoint(CustomPoint *point); // 设置拐点

private:
    QGraphicsItem *mStartItem = nullptr;
    QGraphicsItem *mEndItem = nullptr;

    CustomPoint *mPoint = nullptr;  // 拐点
};

void CustomPath::setPoint(CustomPoint *point)
{
    mPoint = point;
}

void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    QPointF point_pos = mPoint->pos();

    QPainterPath path;
    path.moveTo(start_pos);
    path.lineTo(point_pos);   // 从起点->拐点->终点
    path.lineTo(end_pos);

    setPath(path);
}


    // 修改使用代码
    QGraphicsScene *scene = new QGraphicsScene(this);
    ui->graphicsView->setScene(scene);
    // 创建起点
    CustomItem *item_start = new CustomItem;
    item_start->setPos(100, 100);
    scene->addItem(item_start);
    // 创建终点
    CustomItem *item_end = new CustomItem;
    item_end->setPos(200, 200);
    scene->addItem(item_end);
    // 创建连线
    CustomPath *path = new CustomPath(item_start, item_end);
    item_start->addPath(path);
    item_end->addPath(path);
    scene->addItem(path);
    // 添加拐点图形
    CustomPoint *point = new CustomPoint(path);
    point->setPos(100, 150);
    path->setPoint(point);  // 设置拐点
    point->setPathItem(path); // 设置连线

    path->updatePosition();

在这段代码中,对部分代码进行修改:在连线类中添加了拐点成员,在刷新连线函数中连线到拐点,在实际使用代码中添加了拐点图形,在最后刷新图形连线。

效果如下:

5、带有拐点的两条平行线

问题描述:

那么如何绘制带有拐点的两条连线呢?

解决思路:

直接将偏移规则应用到拐点位置,根据起点和拐点位置(或者拐点和终点位置)确定偏移,会如何?

代码如下:

C++ 复制代码
void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    QPointF point_pos = mPoint->pos();
    // 计算偏移
    QPointF offset_sp = getOffset(start_pos, point_pos);
    QPointF offset_pe = getOffset(point_pos, end_pos);
    // 起点偏移
    QPointF start_p1 = start_pos + offset_sp;
    QPointF start_p2 = start_pos - offset_sp;
    // 拐点对起点偏移
    QPointF point_ps1 = point_pos + offset_sp;
    QPointF point_ps2 = point_pos - offset_sp;
    // 拐点对终点偏移
//    QPointF point_pe1 = point_pos + offset_pe;
//    QPointF point_pe2 = point_pos - offset_pe;
    // 终点偏移
    QPointF end_p1 = end_pos + offset_pe;
    QPointF end_p2 = end_pos - offset_pe;

    // 使用两个
    QPainterPath path;
    path.moveTo(start_p1);
    path.lineTo(point_ps1);
//    path.lineTo(point_pe1);
    path.lineTo(end_p1);

    path.moveTo(start_p2);
    path.lineTo(point_ps2);
//    path.lineTo(point_pe2);
    path.lineTo(end_p2);

    // 使用四个
//    {
//        path.moveTo(start_p1);
//        path.lineTo(point_ps1);
//        path.moveTo(point_pe1);
//        path.lineTo(end_p1);

//        path.moveTo(start_p2);
//        path.lineTo(point_ps2);
//        path.moveTo(point_pe2);
//        path.lineTo(end_p2);
//    }
    setPath(path);
}

在调整代码的过程中,就会发现,在拐点对起点和拐点对终点应用偏移规则时,会产生四个偏移点。

只使用其中两个会发生什么情况?使用四个会发生什么情况?

效果如下:

使用两个的情况:可以看到连线产生了交错,并且某些角度情况下发生重合。

使用四个的情况:可以看到连线产生交错,并且某些角度下连接点错开。

6、拐点处的偏移问题

问题描述:

如果对拐点也应用偏移规则,使用两个点,会产生交错的情况;使用四个点,不仅会产生交错,而且会断开。

解决思路:

拐点处的偏移点应该只可以有两个;其偏移点只对一个点应用;考虑使用角平分线。

方案就是:起点-拐点-终点,形成一个角度,计算出角平分线;过起点的两个偏移点,作起点和拐点连线的两条平行线;这两条平行线和角平分线的交点,作为拐点处的偏移点;然后连接拐点处偏移点和终点偏移点,形成连线。

代码如下:

C++ 复制代码
// 计算角平分线
QLineF CustomPath::calculateAngleBisector(const QPointF &start, const QPointF &mid, const QPointF &end)
{
    // 计算向量A和B
    QPointF vectorA = start - mid;
    QPointF vectorB = end - mid;

    // 归一化向量A和B
    qreal lengthA = std::hypot(vectorA.x(), vectorA.y());
    qreal lengthB = std::hypot(vectorB.x(), vectorB.y());
    QPointF unitA = vectorA / lengthA;
    QPointF unitB = vectorB / lengthB;

    // 计算角平分线向量
    QPointF bisector = unitA + unitB;

    // 如果共线则向量为零,需要使用垂线
    if (bisector.isNull()) {
        bisector = QPointF(-unitA.y(), unitA.x());
    }

    // 归一化角平分线向量
    qreal lengthBisector = std::hypot(bisector.x(), bisector.y());
    QPointF unitBisector = bisector / lengthBisector;

    // 从中点出发,沿角平分线方向绘制一条直线
    QPointF bisectorEnd = mid + unitBisector * 100; // 100为长度,可根据需要调整
    QPointF bisectorEnd_n = mid - unitBisector * 100;
    return QLineF(bisectorEnd_n, bisectorEnd);
    //    return unitBisector;
}

// 计算过p点的l1的平行线与bisector_line的交点
QPointF CustomPath::calculateBisectorPoint(const QLineF &l1, const QLineF &bisector_line, const QPointF &p)
{
    // 起点到拐点连线的向量
    QPointF lp(l1.p2() - l1.p1());
    qreal length = std::hypot(lp.x(), lp.y());
    QPointF unit = lp / length;

    // 过偏移点的平行线
    QLineF line(p, p+unit*100);

    // 计算交点
    QPointF intersection;
    QLineF::IntersectType type = line.intersects(bisector_line, &intersection);
    return intersection;
}

void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    QPointF point_pos = mPoint->pos();
    // 计算偏移
    QPointF offset_sp = getOffset(start_pos, point_pos);
    QPointF offset_pe = getOffset(point_pos, end_pos);
    // 起点偏移
    QPointF start_p1 = start_pos + offset_sp;
    QPointF start_p2 = start_pos - offset_sp;
    // 终点偏移
    QPointF end_p1 = end_pos + offset_pe;
    QPointF end_p2 = end_pos - offset_pe;

    // 计算角平分线
    QLineF bisector_line = calculateAngleBisector(start_pos, point_pos, end_pos);
    QLineF start_line(start_pos, point_pos);
    // 计算交点
    QPointF p1_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p1);
    QPointF p2_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p2);

    // 连线
    QPainterPath path;
    path.moveTo(start_p1);
    path.lineTo(p1_bst_itst);
    path.lineTo(end_p1);

    path.moveTo(start_p2);
    path.lineTo(p2_bst_itst);
    path.lineTo(end_p2);

    setPath(path);
}

在这段代码中,计算起点-拐点-终点形成角度的角平分线,考虑三点共线情况下,使用垂线向量;然后有起点到拐点的连线,过两起点偏移点,作平行线,并得到和角平分线的交点;从交点连线到终点偏移点。

效果如图:

可见在拐点和终点的两条连线发生了交叉,继续完善。

7、后半段交叉问题

问题描述:

偏移点并不总是对应的,拐点到终点的连线发生了交叉。

解决思路:

判断后半段是否交叉,如果交叉,则互换偏移点。

代码如下:

C++ 复制代码
// 判断是否交叉
bool CustomPath::calculateLineIsIntersect(const QPointF &start1, const QPointF &end1,
                                                const QPointF &start2, const QPointF &end2)
{
    QLineF line1(start1, end1);
    QLineF line2(start2, end2);
    QPointF intersection;
    QLineF::IntersectType type = line1.intersects(line2, &intersection);
    if (type == QLineF::BoundedIntersection && ! intersection.isNull()) {
        return true;
    } else {
        return false;
    }
}

void CustomPath::updatePosition()
{
    QPointF start_pos = mStartItem->pos();
    QPointF end_pos = mEndItem->pos();

    QPointF point_pos = mPoint->pos();

    QPointF offset_sp = getOffset(start_pos, point_pos);
    QPointF offset_pe = getOffset(point_pos, end_pos);

    QPointF start_p1 = start_pos + offset_sp;
    QPointF start_p2 = start_pos - offset_sp;

    QPointF end_p1 = end_pos + offset_pe;
    QPointF end_p2 = end_pos - offset_pe;

    // 计算角平分线
    QLineF bisector_line = calculateAngleBisector(start_pos, point_pos, end_pos);
    QLineF start_line(start_pos, point_pos);
    // 计算交点
    QPointF p1_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p1);
    QPointF p2_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p2);

    QPainterPath path;

    // 前半段
    path.moveTo(start_p1);
    path.lineTo(p1_bst_itst);
    path.moveTo(start_p2);
    path.lineTo(p2_bst_itst);

    // 后半段,判断是否交叉
    if (calculateLineIsIntersect(end_p1, p1_bst_itst, end_p2, p2_bst_itst)) {
        // 如果交叉
        path.moveTo(p1_bst_itst);
        path.lineTo(end_p2);

        path.moveTo(p2_bst_itst);
        path.lineTo(end_p1);
    } else {
        path.moveTo(p1_bst_itst);
        path.lineTo(end_p1);

        path.moveTo(p2_bst_itst);
        path.lineTo(end_p2);
    }

    setPath(path);
}

在这段代码中,修改了绘制连线的顺序,先绘制前半段,再绘制后半段;如果后半段发生交叉,则互换最后的终点偏移点。

效果如下:

可见当形成的角度极小的时候,拐点处会极度尖锐,对这个问题我没有很好的办法。还好拐点是可以移动的。如果你有想法,欢迎共同讨论。

总结:

本文系统性地解决了在Qt图形视图中绘制动态连线的技术难点。首先,通过继承QGraphicsItem实现可拖拽的图形项CustomItem,利用itemChange事件触发连线刷新,确保了图形与路径的实时联动。其次,引入CustomPath类管理路径绘制,通过QPainterPath灵活构建线段与拐点连接逻辑。针对平行线偏移问题,提出基于斜向偏移与角平分线的动态调整策略,有效避免了路径重叠与错位。然而,在极端角度下拐点处仍可能因偏移计算产生尖锐连接,需进一步优化算法或引入平滑曲线处理。

完整代码:

  • mainwindow.h

点击折叠或展开代码

C++ 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtWidgets>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE


class CustomPath;
class CustomPoint;
// 图形类,描述起点和终点
class CustomItem : public QGraphicsRectItem
{
public:
   CustomItem(QGraphicsItem *parent = nullptr);
   void addPath(CustomPath *path);

protected:
   QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;

private:
   QList<CustomPath *> mPathList; // 连线列表
};
// 连线类,描述连线
class CustomPath : public QGraphicsPathItem
{
public:
   CustomPath(QGraphicsItem *start, QGraphicsItem *end, QGraphicsItem *parent = nullptr);
   void updatePosition();   // 刷新连线

   void setPoint(CustomPoint *point);   // 设置拐点

private:
   QGraphicsItem *mStartItem = nullptr;  // 起点
   QGraphicsItem *mEndItem = nullptr;    // 终点

   CustomPoint *mPoint = nullptr;     // 拐点

   QPointF getOffset(const QPointF &p1, const QPointF &p2);
   QLineF calculateAngleBisector(const QPointF& start, const QPointF& mid, const QPointF& end);
   QPointF calculateBisectorPoint(const QLineF &l1, const QLineF &bisector_line, const QPointF &p);
   bool calculateLineIsIntersect(const QPointF &start1, const QPointF &end1, const QPointF &start2, const QPointF &end2);

};
// 拐点类
class CustomPoint : public QGraphicsEllipseItem
{
public:
   CustomPoint(QGraphicsItem *parent = nullptr);
   void setPathItem(CustomPath *pathItem);

protected:
   QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override;

private:
   CustomPath *mPathItem = nullptr;   // 拐点所属连线
};

class MainWindow : public QMainWindow
{
   Q_OBJECT

public:
   MainWindow(QWidget *parent = nullptr);
   ~MainWindow();

private:
   Ui::MainWindow *ui;

   void initGraphics();
};
#endif // MAINWINDOW_H
  • mainwindow.cpp

点击折叠或展开代码

C++ 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"



MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
   , ui(new Ui::MainWindow)
{
   ui->setupUi(this);

   initGraphics();
}

MainWindow::~MainWindow()
{
   delete ui;
}

// 问题1、2、3
//void MainWindow::initGraphics()
//{
//    // 创建画布
//    QGraphicsScene *scene = new QGraphicsScene(this);
//    ui->graphicsView->setScene(scene);
//    // 创建起点
//    CustomItem *item_start = new CustomItem;
//    item_start->setPos(100, 100);
//    scene->addItem(item_start);
//    // 创建终点
//    CustomItem *item_end = new CustomItem;
//    item_end->setPos(200, 200);
//    scene->addItem(item_end);
//    // 创建连线
//    CustomPath *path = new CustomPath(item_start, item_end);
//    item_start->addPath(path);
//    item_end->addPath(path);
//    scene->addItem(path);

//    path->updatePosition();
//}
// 问题4、5
void MainWindow::initGraphics()
{
   QGraphicsScene *scene = new QGraphicsScene(this);
   ui->graphicsView->setScene(scene);

   CustomItem *item_start = new CustomItem;
   item_start->setPos(100, 100);
   scene->addItem(item_start);

   CustomItem *item_end = new CustomItem;
   item_end->setPos(200, 200);
   scene->addItem(item_end);

   CustomPath *path = new CustomPath(item_start, item_end);
   item_start->addPath(path);
   item_end->addPath(path);
   scene->addItem(path);
   // 添加拐点图形
   CustomPoint *point = new CustomPoint(path);
   point->setPos(100, 150);
   path->setPoint(point);
   point->setPathItem(path);

   path->updatePosition();

}


CustomItem::CustomItem(QGraphicsItem *parent) : QGraphicsRectItem(parent)
{
   // 设置形状
   setRect(-5, -5, 10, 10);
   // 设置颜色
   setBrush(Qt::black);
   // 设置可移动
   setFlag(QGraphicsItem::ItemIsMovable, true);
   // 设置可发送几何变动,可在itemChange中进行检测
   setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}
// 添加连线
void CustomItem::addPath(CustomPath *path)
{
   mPathList.append(path);
}

QVariant CustomItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
   switch (change) {
   // 当位置变动时,刷新连线
   case QGraphicsItem::ItemPositionHasChanged:
   {
       for (int i = 0, size = mPathList.size(); i < size; ++i) {
           mPathList.at(i)->updatePosition();
       }
   }

   default:
       break;
   }

   return QGraphicsItem::itemChange(change, value);
}

CustomPath::CustomPath(QGraphicsItem *start, QGraphicsItem *end, QGraphicsItem *parent)
   : QGraphicsPathItem(parent), mStartItem(start), mEndItem(end)
{
   // 设置绘制画笔,颜色黑色,笔宽为1
   setPen(QPen(Qt::black, 1));
}
// 问题1
//void CustomPath::updatePosition()
//{
//    // 获取两端的位置
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();

//    // 绘制连线
//    QPainterPath path;
//    path.moveTo(start_pos);
//    path.lineTo(end_pos);
//    // 设置连线
//    setPath(path);
//}
// 问题2
//void CustomPath::updatePosition()
//{
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();
//    // 起点左右偏移
//    QPointF start_p1 = start_pos + QPointF(-5, 0);
//    QPointF start_p2 = start_pos + QPointF(5, 0);
//    // 终点左右偏移
//    QPointF end_p1 = end_pos + QPointF(-5, 0);
//    QPointF end_p2 = end_pos + QPointF(5, 0);

//    // 两次连线
//    QPainterPath path;
//    path.moveTo(start_p1);
//    path.lineTo(end_p1);
//    path.moveTo(start_p2);
//    path.lineTo(end_p2);

//    setPath(path);
//}

QPointF CustomPath::getOffset(const QPointF &p1, const QPointF &p2)
{
   QPointF dp = p1 - p2;
   QPointF offset;
   // 根据差值判断
   if (dp.x() * dp.y() >= 0) {
       // 设置偏移量
       offset = QPointF(-5, 5);
   } else {
       offset = QPointF(5, 5);
   }
   return offset;
}
// 问题3
//void CustomPath::updatePosition()
//{
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();

//    QPointF offset = getOffset(start_pos, end_pos);

//    // 起点和终点偏移
//    QPointF start_p1 = start_pos + offset;
//    QPointF start_p2 = start_pos - offset;
//    QPointF end_p1 = end_pos + offset;
//    QPointF end_p2 = end_pos - offset;

//    QPainterPath path;
//    path.moveTo(start_p1);
//    path.lineTo(end_p1);
//    path.moveTo(start_p2);
//    path.lineTo(end_p2);

//    setPath(path);
//}
// 问题4
//void CustomPath::updatePosition()
//{
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();

//    QPointF point_pos = mPoint->pos();

//    QPainterPath path;
//    path.moveTo(start_pos);
//    path.lineTo(point_pos);   // 从起点->拐点->终点
//    path.lineTo(end_pos);

//    setPath(path);
//}

// 问题5
//void CustomPath::updatePosition()
//{
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();

//    QPointF point_pos = mPoint->pos();
//    // 计算偏移
//    QPointF offset_sp = getOffset(start_pos, point_pos);
//    QPointF offset_pe = getOffset(point_pos, end_pos);
//    // 起点偏移
//    QPointF start_p1 = start_pos + offset_sp;
//    QPointF start_p2 = start_pos - offset_sp;
//    // 拐点对起点偏移
//    QPointF point_ps1 = point_pos + offset_sp;
//    QPointF point_ps2 = point_pos - offset_sp;
//    // 拐点对终点偏移
//    QPointF point_pe1 = point_pos + offset_pe;
//    QPointF point_pe2 = point_pos - offset_pe;
//    // 终点偏移
//    QPointF end_p1 = end_pos + offset_pe;
//    QPointF end_p2 = end_pos - offset_pe;

//    // 使用两个
//    QPainterPath path;
//    path.moveTo(start_p1);
//    path.lineTo(point_ps1);
////    path.lineTo(point_pe1);
//    path.lineTo(end_p1);

//    path.moveTo(start_p2);
//    path.lineTo(point_ps2);
////    path.lineTo(point_pe2);
//    path.lineTo(end_p2);
//      // 使用四个
////    {
////        path.moveTo(start_p1);
////        path.lineTo(point_ps1);
////        path.moveTo(point_pe1);
////        path.lineTo(end_p1);

////        path.moveTo(start_p2);
////        path.lineTo(point_ps2);
////        path.moveTo(point_pe2);
////        path.lineTo(end_p2);
////    }

//    setPath(path);
//}
// 问题6
//void CustomPath::updatePosition()
//{
//    QPointF start_pos = mStartItem->pos();
//    QPointF end_pos = mEndItem->pos();

//    QPointF point_pos = mPoint->pos();

//    QPointF offset_sp = getOffset(start_pos, point_pos);
//    QPointF offset_pe = getOffset(point_pos, end_pos);

//    QPointF start_p1 = start_pos + offset_sp;
//    QPointF start_p2 = start_pos - offset_sp;

//    QPointF end_p1 = end_pos + offset_pe;
//    QPointF end_p2 = end_pos - offset_pe;

//    // 计算角平分线
//    QLineF bisector_line = calculateAngleBisector(start_pos, point_pos, end_pos);
//    QLineF start_line(start_pos, point_pos);
//    // 计算交点
//    QPointF p1_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p1);
//    QPointF p2_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p2);


//    QPainterPath path;
//    path.moveTo(start_p1);
//    path.lineTo(p1_bst_itst);
//    path.lineTo(end_p1);

//    path.moveTo(start_p2);
//    path.lineTo(p2_bst_itst);
//    path.lineTo(end_p2);

//    setPath(path);
//}
// 问题7
void CustomPath::updatePosition()
{
   QPointF start_pos = mStartItem->pos();
   QPointF end_pos = mEndItem->pos();

   QPointF point_pos = mPoint->pos();

   QPointF offset_sp = getOffset(start_pos, point_pos);
   QPointF offset_pe = getOffset(point_pos, end_pos);

   QPointF start_p1 = start_pos + offset_sp;
   QPointF start_p2 = start_pos - offset_sp;

   QPointF end_p1 = end_pos + offset_pe;
   QPointF end_p2 = end_pos - offset_pe;

   // 计算角平分线
   QLineF bisector_line = calculateAngleBisector(start_pos, point_pos, end_pos);
   QLineF start_line(start_pos, point_pos);
   // 计算交点
   QPointF p1_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p1);
   QPointF p2_bst_itst = calculateBisectorPoint(start_line, bisector_line, start_p2);

   QPainterPath path;

   // 前半段
   path.moveTo(start_p1);
   path.lineTo(p1_bst_itst);
   path.moveTo(start_p2);
   path.lineTo(p2_bst_itst);


   // 后半段,判断是否交叉
   if (calculateLineIsIntersect(end_p1, p1_bst_itst, end_p2, p2_bst_itst)) {
       // 如果交叉
       path.moveTo(p1_bst_itst);
       path.lineTo(end_p2);

       path.moveTo(p2_bst_itst);
       path.lineTo(end_p1);
   } else {
       path.moveTo(p1_bst_itst);
       path.lineTo(end_p1);

       path.moveTo(p2_bst_itst);
       path.lineTo(end_p2);
   }

   setPath(path);
}
// 计算角平分线
QLineF CustomPath::calculateAngleBisector(const QPointF &start, const QPointF &mid, const QPointF &end)
{
   // 计算向量A和B
   QPointF vectorA = start - mid;
   QPointF vectorB = end - mid;

   // 归一化向量A和B
   qreal lengthA = std::hypot(vectorA.x(), vectorA.y());
   qreal lengthB = std::hypot(vectorB.x(), vectorB.y());
   QPointF unitA = vectorA / lengthA;
   QPointF unitB = vectorB / lengthB;

   // 计算角平分线向量
   QPointF bisector = unitA + unitB;

   // 如果共线则向量为零,需要使用垂线
   if (bisector.isNull()) {
       bisector = QPointF(-unitA.y(), unitA.x());
   }

   // 归一化角平分线向量
   qreal lengthBisector = std::hypot(bisector.x(), bisector.y());
   QPointF unitBisector = bisector / lengthBisector;

   // 从中点出发,沿角平分线方向绘制一条直线
   QPointF bisectorEnd = mid + unitBisector * 100; // 100为长度,可根据需要调整
   QPointF bisectorEnd_n = mid - unitBisector * 100;
   return QLineF(bisectorEnd_n, bisectorEnd);
   //    return unitBisector;
}
// 计算过p点的l1的平行线与bisector_line的交点
QPointF CustomPath::calculateBisectorPoint(const QLineF &l1, const QLineF &bisector_line, const QPointF &p)
{
   // 起点到拐点连线的向量
   QPointF lp(l1.p2() - l1.p1());
   qreal length = std::hypot(lp.x(), lp.y());
   QPointF unit = lp / length;

   // 过偏移点的平行线
   QLineF line(p, p+unit*100);

   // 计算交点
   QPointF intersection;
   QLineF::IntersectType type = line.intersects(bisector_line, &intersection);
   return intersection;
}
// 判断是否交叉
bool CustomPath::calculateLineIsIntersect(const QPointF &start1, const QPointF &end1,
                                               const QPointF &start2, const QPointF &end2)
{
   QLineF line1(start1, end1);
   QLineF line2(start2, end2);
   QPointF intersection;
   QLineF::IntersectType type = line1.intersects(line2, &intersection);
   if (type == QLineF::BoundedIntersection && ! intersection.isNull()) {
       return true;
   } else {
       return false;
   }
}

void CustomPath::setPoint(CustomPoint *point)
{
   mPoint = point;
}

CustomPoint::CustomPoint(QGraphicsItem *parent)
   : QGraphicsEllipseItem(parent)
{
   // 设置图形为圆形
   setRect(-2, -2, 4, 4);
   setBrush(Qt::black);
   setFlag(QGraphicsItem::ItemIsMovable, true);
   setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);
}

QVariant CustomPoint::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
   switch (change) {
   case QGraphicsItem::ItemPositionHasChanged:
   {
       // 当拐点位置发生变化,刷新连线
       if (mPathItem) {
           mPathItem->updatePosition();
       }
   }

   default:
       break;
   }

   return QGraphicsItem::itemChange(change, value);
}

void CustomPoint::setPathItem(CustomPath *pathItem)
{
   mPathItem = pathItem;
}