Qt实战15.构建网络拓扑图

1 需求描述

基于Qt图形视图框架开发一个网络拓扑模块,用于可视化展示、控制HUB(类似于交换机)与NODE(类似于连接到交换机上的设备)的关系网路。

2 设计思路

先来看个图:

这里将图形项分为了以下几种类型:

  1. 连接点类型(TopologyPointItem):作为连接线的起点和终点,仅此而已;
  2. 节点类型(TopologyNodeItem):节点包含1个连接点,连接点作为节点的子项;
  3. 集线器类型(TopologyHubItem):集线器包含16个连接点,连接点作为集线器的子项;
  4. 连接线类型(TopologyArrowItem):连接线负责连接不同的连接点,会记录起始连接点和结束连接点,当节点或集线器图形项移动后,连接线自动实时刷新。

这里部分功能也是参考了Qt的示例程序Diagram Scene Example,有兴趣的朋友可以看下。

3 代码实现

3.1 TopologyBaseItem

节点类型和集线器类型除了连接点个数不同,其他都差不多,这里抽象一个基类出来,实现一些共性。

#ifndef TOPOLOGYBASEITEM_H
#define TOPOLOGYBASEITEM_H

#include <QGraphicsObject>

class TopologyArrowItem;
class TopologyBaseItem : public QGraphicsObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

public:
    explicit TopologyBaseItem(QGraphicsItem *parent = nullptr);

    QString name() const
    {
        return m_name;
    }

    /**
     * @brief addArrow 添加关联的连接线
     * @param arrow 连接线
     */
    void addArrow(TopologyArrowItem *arrow);
    QRectF boundingRect() const override;

public slots:
    /**
     * @brief setName 设置显示名称
     * @param name 名称
     */
    void setName(QString name);

protected:
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

signals:
    void nameChanged(QString name);

private:
    QList<TopologyArrowItem *> m_arrows;
    QString m_name;
};
#endif // TOPOLOGYBASEITEM_H
#include "topologybaseitem.h"

#include "topologyarrowitem.h"

#include <QPainter>
#include <QPen>

TopologyBaseItem::TopologyBaseItem(QGraphicsItem *parent) : QGraphicsObject(parent)
{
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true);
}

void TopologyBaseItem::setName(QString name)
{
    if (m_name == name)
    {
        return;
    }

    m_name = name;
    emit nameChanged(m_name);
}

void TopologyBaseItem::addArrow(TopologyArrowItem *arrow)
{
    m_arrows.append(arrow);
}

QVariant TopologyBaseItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
    if (change == QGraphicsItem::ItemPositionChange)
    {
        foreach (TopologyArrowItem *arrow, m_arrows)
        {
            arrow->updatePosition();    //节点移动后,自动刷新连接线
        }
    }

    return value;
}

QRectF TopologyBaseItem::boundingRect() const
{
    qreal penWidth = 1;
    qreal radius = 30;
    qreal diameter = 60;

    return QRectF(-radius - penWidth / 2, -radius - penWidth / 2,
                  diameter + penWidth, diameter + penWidth);
}

void TopologyBaseItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    //绘制名称
    QPen pen;
    pen.setWidth(2);
    pen.setJoinStyle(Qt::MiterJoin);
    painter->setPen(pen);

    QFont font;
    font.setFamily("System");
    font.setPixelSize(9);
    painter->setFont(font);

    QRectF boundingRect = this->boundingRect();

    painter->drawRect(boundingRect);
    painter->drawText(boundingRect, Qt::AlignCenter, m_name);
}

void TopologyBaseItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    foreach (TopologyArrowItem *arrow, m_arrows)
    {
        arrow->updatePosition();
    }

    QGraphicsObject::mouseReleaseEvent(event);
}

3.2 TopologyNodeItem

节点类型需要添加一个连接点作为子项,实现如下:

#ifndef TOPOLOGYNODEITEM_H
#define TOPOLOGYNODEITEM_H

#include "topologybaseitem.h"

class TopologyArrowItem;
class TopologyNodeItem : public TopologyBaseItem
{
    Q_OBJECT
public:
    enum { Type = UserType + 2 };
    explicit TopologyNodeItem(QGraphicsItem *parent = nullptr);
    int type() const override { return Type; }

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) override;
};

#endif // TOPOLOGYNODEITEM_H
#include "topologynodeitem.h"

#include "topologypointitem.h"
#include "topologyarrowitem.h"

#include <QPainter>
#include <QPen>
#include <QFont>
#include <QDebug>

#define CONNECTOR_LENGTH (5)

TopologyNodeItem::TopologyNodeItem(QGraphicsItem *parent) : TopologyBaseItem(parent)
{
    TopologyPointItem *item = new TopologyPointItem(this);
    item->setPos(0, boundingRect().height()/2 + CONNECTOR_LENGTH + item->boundingRect().height()/2);
    item->setPointNum(-1);
}

void TopologyNodeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    TopologyBaseItem::paint(painter, option, widget);

    QRectF boundingRect = this->boundingRect();

    //画连接点中间线
    painter->save();
    painter->translate(boundingRect.bottomRight());
    painter->rotate(180);
    painter->drawLine(QPointF(boundingRect.width()/2, 0), QPointF(boundingRect.width()/2, -CONNECTOR_LENGTH));
    painter->restore();
}

3.3 TopologyHubItem

集线器类型需要16个连接点作为子项,实现如下:

#ifndef TOPOLOGYHUBITEM_H
#define TOPOLOGYHUBITEM_H

#include "topologybaseitem.h"

class TopologyHubItem : public TopologyBaseItem
{
    Q_OBJECT
public:
    enum { Type = UserType + 1 };
    explicit TopologyHubItem(QGraphicsItem *parent = nullptr);
    int type() const override { return Type; }

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0) override;
};
#endif // TOPOLOGYHUBITEM_H
#include "topologyhubitem.h"
#include "topologypointitem.h"

#include <QPainter>
#include <QPen>
#include <QDebug>

#define CONNECTOR_SPACING (5)

TopologyHubItem::TopologyHubItem(QGraphicsItem *parent) : TopologyBaseItem(parent)
{    
    int pointNum = 1;   //连接点编号(1-16)

    QRectF boundingRect = this->boundingRect();
    for (int i = 0; i < 4; ++i)
    {
        TopologyPointItem *item = new TopologyPointItem(this);
        item->setPointNum(pointNum++);
        item->setPos(boundingRect.topLeft());
        item->moveBy(boundingRect.height()/5 * (i + 1), -CONNECTOR_SPACING - item->boundingRect().height()/2);
    }

    for (int i = 0; i < 4; ++i)
    {
        TopologyPointItem *item = new TopologyPointItem(this);
        item->setPointNum(pointNum++);
        item->setPos(boundingRect.topRight());
        item->moveBy(CONNECTOR_SPACING + item->boundingRect().height()/2, boundingRect.height()/5 * (i + 1));
    }

    for (int i = 0; i < 4; ++i)
    {
        TopologyPointItem *item = new TopologyPointItem(this);
        item->setPointNum(pointNum++);
        item->setPos(boundingRect.bottomRight());
        item->moveBy(-boundingRect.height()/5 * (i + 1), CONNECTOR_SPACING + item->boundingRect().height()/2);
    }

    for (int i = 0; i < 4; ++i)
    {
        TopologyPointItem *item = new TopologyPointItem(this);
        item->setPointNum(pointNum++);
        item->setPos(boundingRect.bottomLeft());
        item->moveBy(-CONNECTOR_SPACING - item->boundingRect().height()/2, -boundingRect.height()/5 * (i + 1));
    }
}

void TopologyHubItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    TopologyBaseItem::paint(painter, option, widget);

    QRectF boundingRect = this->boundingRect();

    //绘制连接点中间线
    painter->save();
    painter->translate(boundingRect.topLeft());
    for (int i = 0; i < 4; ++i)
    {
        painter->drawLine(QPointF(boundingRect.width()/5 * (i + 1), 0), QPointF(boundingRect.width()/5 * (i + 1), -CONNECTOR_SPACING));
    }
    painter->restore();

    painter->save();
    painter->translate(boundingRect.topRight());
    painter->rotate(90);
    for (int i = 0; i < 4; ++i)
    {
        painter->drawLine(QPointF(boundingRect.width()/5 * (i + 1), 0), QPointF(boundingRect.width()/5 * (i + 1), -CONNECTOR_SPACING));
    }
    painter->restore();

    painter->save();
    painter->translate(boundingRect.bottomRight());
    painter->rotate(180);
    for (int i = 0; i < 4; ++i)
    {
        painter->drawLine(QPointF(boundingRect.width()/5 * (i + 1), 0), QPointF(boundingRect.width()/5 * (i + 1), -CONNECTOR_SPACING));
    }
    painter->restore();

    painter->save();
    painter->translate(boundingRect.bottomLeft());
    painter->rotate(270);
    for (int i = 0; i < 4; ++i)
    {
        painter->drawLine(QPointF(boundingRect.width()/5 * (i + 1), 0), QPointF(boundingRect.width()/5 * (i + 1), -CONNECTOR_SPACING));
    }
    painter->restore();
}

3.4 TopologyArrowItem

最后就是连接线了,连接线会根据起始连接点和结束连接点进行绘制,代码如下:

#ifndef TOPOLOGYARROWITEM_H
#define TOPOLOGYARROWITEM_H

#include <QGraphicsPathItem>

class TopologyPointItem;
class TopologyArrowItem : public QGraphicsPathItem
{
public:
    enum { Type = UserType + 4 };
    explicit TopologyArrowItem(TopologyPointItem *startItem,
                               TopologyPointItem *endItem,
                               QGraphicsItem *parent = nullptr);

    int type() const override { return Type; }
    QPainterPath shape() const override;
    void updatePosition();

protected:
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;

private:
    TopologyPointItem *m_pStartItem = nullptr;
    TopologyPointItem *m_pEndItem = nullptr;
};
#endif // TOPOLOGYARROWITEM_H
#include "topologyarrowitem.h"
#include "topologypointitem.h"

#include <QPainterPath>
#include <QPainterPathStroker>
#include <QPen>
#include <QDebug>
#include <QCursor>
#include <QAction>

TopologyArrowItem::TopologyArrowItem(TopologyPointItem *startItem,
                                     TopologyPointItem *endItem,
                                     QGraphicsItem *parent) : QGraphicsPathItem(parent)
{
    m_pStartItem = startItem;
    m_pEndItem = endItem;

    setCursor(QCursor(Qt::ArrowCursor));
//    setAcceptHoverEvents(true);
    setPen(QPen(QColor("black"), 2));
}

QPainterPath TopologyArrowItem::shape() const
{
    QPainterPathStroker pps;
    pps.setWidth(2);
    return pps.createStroke(this->path());
}

void TopologyArrowItem::updatePosition()
{
    QPointF startPoint = mapFromItem(m_pStartItem, 0, 0);
    QPointF endPoint = mapFromItem(m_pEndItem, 0, 0);

    QPainterPath path(startPoint);

    QPointF breakPointFirst(startPoint + m_pStartItem->recommendedBreakPoint());
    QPointF breakPointLast(endPoint + m_pEndItem->recommendedBreakPoint());
    path.lineTo(breakPointFirst);

    //NODE连接到HUB上方
    if (m_pStartItem->recommendedBreakPoint().y() > 0 && m_pEndItem->recommendedBreakPoint().y() < 0)
    {
        if (breakPointFirst.y() >= breakPointLast.y())
        {
            QPointF breakPoint1(breakPointFirst.x() - (breakPointFirst.x() - breakPointLast.x())/2, breakPointFirst.y());
            QPointF breakPoint2(breakPointLast.x() + (breakPointFirst.x() - breakPointLast.x())/2, breakPointLast.y());

            path.lineTo(breakPoint1);
            path.lineTo(breakPoint2);
        }
        else
        {
            QPointF breakPoint1(breakPointFirst.x(), breakPointFirst.y() + (breakPointLast.y() - breakPointFirst.y())/2);
            QPointF breakPoint2(breakPointLast.x(), breakPoint1.y());

            path.lineTo(breakPoint1);
            path.lineTo(breakPoint2);
        }
    }

    //NODE连接到HUB右方
    if (m_pStartItem->recommendedBreakPoint().y() > 0 && m_pEndItem->recommendedBreakPoint().x() > 0)
    {
        if (breakPointFirst.y() >= breakPointLast.y())
        {
            if (breakPointFirst.x() >= breakPointLast.x())
            {
                QPointF breakPoint1(breakPointFirst.x() - (breakPointFirst.x() - breakPointLast.x())/2, breakPointFirst.y());
                QPointF breakPoint2(breakPoint1.x(), breakPointLast.y());

                path.lineTo(breakPoint1);
                path.lineTo(breakPoint2);
            }
            else
            {
                //todo
            }
        }
        else
        {
            QPointF breakPoint1(breakPointFirst.x(), breakPointLast.y());
            path.lineTo(breakPoint1);
        }
    }

    path.lineTo(breakPointLast);
    path.lineTo(endPoint);
    setPath(path);
}

void TopologyArrowItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
    setPen(QPen(QColor("black"), 2));
    setZValue(0);

    QGraphicsPathItem::hoverEnterEvent(event);
}

void TopologyArrowItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
    setPen(QPen(QColor("darkgrey"), 2));
    setZValue(-1);

    QGraphicsPathItem::hoverLeaveEvent(event);
}

4 总结

使用图形视图框架实现网络拓扑可谓事半功倍,其实这里所做的只是UI层面的东西,完全可以和一些业务逻辑进行绑定,只需要稍加扩展即可。
上面的实现过程也并没有用到什么高级的设计技巧,都是一些常规思路,只需要一点点的面向对象编程思想就行了,把类设计好,然后无非就是重写一些事件函数以及一些相关的虚函数,这些函数什么时候调用,Qt框架已经处理好啦,我们只需实现,仔细想想,是不是这样?这个就是框架的魔力。

5 下载

这个demo很多细节还未处理好,就不发了,如果确实需要参考,可以评论留下邮箱,我看到了会通过邮件的形式发送。

posted @ 2022-09-27 22:02  Qt小罗  阅读(3125)  评论(35编辑  收藏  举报