手把手教你制作【带吸附效果的线段绘制】(QT)

手把手教你制作【带吸附效果的线段绘制】(QT)

一、功能描述

需要完成带吸附效果的线段就需要完成一下功能点。

  1. 线段绘制。
  2. 当鼠标接近绘制好的线段时线段变粗。
  3. 当线段在线段两段时,按下鼠标选择定点移动。
  4. 当选择线段中间时,线段平移。

二、代码分析

实现以上功能,需要完成关键的三个鼠标响应事件。

// 鼠标按下事件
void mousePressEvent(QMouseEvent *event);
// 鼠标释放事件
void mouseReleaseEvent(QMouseEvent *event);
// 鼠标要移动事件
void mouseMoveEvent(QMouseEvent *event);

关键的绘图事件

// 界面的绘制在这里
void paintEvent(QPaintEvent *event);

关键数据结构


enum SelStatus {
    outLine,       // 在线段外
    onStartPoint,  // 在起始点
    onEndPoint,    // 在结束点
    onLine         // 在线段上
};
struct LineSegment {
    QPointF startPoint;    // 线段起点
    QPointF endPoint;      // 点段终点
    LineSegment(QPointF a, QPointF b)
    {
        startPoint = a;
        endPoint = b;
    }
    LineSegment() {}
};
struct LineInfo {
    bool bDraw;  //是否绘制
    SelStatus selStatus;    // 线段的选中状态

    LineSegment* seg;
    LineInfo()
    {
        selStatus = outLine;
        seg = new LineSegment;
    }
    ~LineInfo() { delete seg; }
};

鼠标按下

  • 记录按下的点以及线段绘制的起始点。
  • 如果线段被选中,记录选中的线段的起始点和结束点

void QDrawingPaperView::mousePressEvent(QMouseEvent *event)
{
    switch (event->button()) {
        case Qt::LeftButton:
            m_bLBtnDown = true;  // 记录左键鼠标按下
            m_currectSelectedLine = nullptr;
            m_currectSelectedLine = getSeleledLine();
            if (nullptr == m_currectSelectedLine) {  // 未选中

                m_startPoint = event->pos();

                m_endPoint = m_startPoint;
                // 记录绘画起始点
                m_tempLine = new LineInfo;
                m_tempLine->seg->startPoint.setX(event->x());
                m_tempLine->seg->startPoint.setY(event->y());
            } else {
                m_tmpPoint = event->pos();
                // 记录被选中的开始点和结束点,当鼠标移动时需要通过他们来计算移动的偏移量。
                m_startPoint = m_currectSelectedLine->seg->startPoint;
                m_endPoint = m_currectSelectedLine->seg->endPoint;
            }
            break;
        default:
            break;
    }
}

鼠标释放

  • 记录按下的点以及线段绘制的结束点。
  • 如果线段被选中不做处理。

void QDrawingPaperView::mouseReleaseEvent(QMouseEvent *event)
{
    switch (event->button()) {
        case Qt::LeftButton:
            m_bLBtnDown = false;  // 记录左键鼠标按下
            if (nullptr == m_currectSelectedLine) {
                // 记录绘画结束点
                m_tempLine->seg->endPoint.setX(event->pos().x());
                m_tempLine->seg->endPoint.setY(event->pos().y());
                m_tempLine->bDraw = true;
                // 将线段信息保存在列表中,开始位置不能与起始位置重复.
                if (m_tempLine->seg->startPoint != m_tempLine->seg->endPoint)
                    m_listLineInfos.push_back(m_tempLine);
            } else {
                m_currectSelectedLine = nullptr;
            }
            break;
        default:
            break;
    }
    update();
}

鼠标移动

关键的算法都在这里。

  • 需要判断是否选中线段
  • 需要判断选择线段的位置
  • 如果选中需要判断是否已经按下。
  • 如果按下更具选中的状态修改线段坐标点

void QDrawingPaperView::mouseMoveEvent(QMouseEvent *event)
{
    QPointF movePt = event->pos();

    if (!m_bLBtnDown) {
        selSeg(movePt);
    }
    m_currectSelectedLine = getSeleledLine();
    if (nullptr == m_currectSelectedLine) {
        if (m_bLBtnDown) {
            m_endPoint = movePt;
        }
    } else {
        if (m_bLBtnDown) {
            if (m_currectSelectedLine->selStatus == onLine) {
                qInfo() << (movePt);
                qInfo() << "m_tmpPoint" << m_tmpPoint;
                qInfo() << "偏移:" << movePt - m_tmpPoint;
                qInfo() << m_currectSelectedLine->seg->startPoint + movePt -
                               m_tmpPoint;
                m_currectSelectedLine->seg->startPoint =
                    m_startPoint + movePt - m_tmpPoint;

                m_currectSelectedLine->seg->endPoint =
                    m_endPoint + movePt - m_tmpPoint;
                qInfo() << "endPoint" << (m_currectSelectedLine->seg->endPoint);
            } else if (m_currectSelectedLine->selStatus == onStartPoint) {
                m_currectSelectedLine->seg->startPoint =
                    m_startPoint + movePt - m_tmpPoint;
            } else if (m_currectSelectedLine->selStatus == onEndPoint) {
                m_currectSelectedLine->seg->endPoint =
                    m_endPoint + movePt - m_tmpPoint;
            }
        }
    }
    update();
}

线段绘制

线段绘制的入门基础可以查看文章QT线段画板实战_啊渊的专栏-CSDN博客

  • 根据线段的顶点绘制
  • 根据线段的选中状态绘制

void QDrawingPaperView::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    // 设置画笔的样式

    // 绘制临时图像
    if (m_bLBtnDown == true && nullptr == m_currectSelectedLine) {
        p.setPen(QPen(Qt::red, 1));
        p.drawLine(m_startPoint, m_endPoint);
    }
    // 绘制最终的数据列表
    foreach (auto item, m_listLineInfos) {
        if (true == item->bDraw) {
            if (item->selStatus != outLine) {
                p.setPen(QPen(Qt::red, 2));
                if (item->selStatus == onStartPoint) {
                    p.setPen(QPen(Qt::red, 2));
                    p.drawEllipse(item->seg->startPoint, 3, 3);
                } else if (item->selStatus == onEndPoint) {
                    p.setPen(QPen(Qt::red, 2));
                    p.drawEllipse(item->seg->endPoint, 3, 3);
                }
            } else {
                p.setPen(QPen(Qt::red, 1));
            }
            p.drawLine(item->seg->startPoint, item->seg->endPoint);
        }
    }
}

关键算法计算垂点

详细分析可以查看文章求点到线段的最短距离(QT)_啊渊的专栏-CSDN博客

QPointF QDrawingPaperView::getPointToLineVerticalpoint(QPointF pt,
                                                       LineSegment seg)
{
    QPointF np;
    double x_se = seg.startPoint.x() - seg.endPoint.x();
    double y_se = seg.startPoint.y() - seg.endPoint.y();
    double x_se_2 = x_se * x_se;
    double y_se_2 = y_se * y_se;
    double x = (x_se_2 * pt.x() + (pt.y() - seg.startPoint.y()) * y_se * x_se +
                seg.startPoint.x() * y_se_2) /
               (x_se_2 + y_se_2);
    double y = pt.y() + x_se * (pt.x() - x) / y_se;
    np.setX(x);
    np.setY(y);
    return np;
}

源码

git传送门

mkdir build
qmake ..
make

CustomLine3 · master · 啊渊 / QT博客案例 · GIT CODE

上一篇:PHP-elasticsearch/elasticsearch包分词功能实现及源码解析


下一篇:Springboot-actuator的常用endpoint的使用说明