QT网络拓扑图绘制实验

发布于:2025-04-21 ⋅ 阅读:(23) ⋅ 点赞:(0)

前言

在网络通讯中,我qt常用的是TCP或者UDP协议,就比方说TCP吧,一台服务器有时可能会和多台客户端相连接,我之前都是处理单链接情况,最近研究图结构的时候,突然就想到了这个问题。那么如何解决这个问题呢,我是想将图显示在view中,并且可以动态交互。

图的绘制API支持

首先就是图的绘制了,c++的stl和qt封装的库对图结构,都没有直接的支持,无非是容器接适配器模拟邻接表什么的实现,对我来说感觉好麻烦,我就想偷懒,上网搜了下,了解到了有两个库支持图结构的绘制,一个是BOOST库,这个不用介绍了,c++的一些新特性比如智能指针就是从这来的。再一个就是OGDF。

  • 图结构与算法支持
    OGDF支持多种图结构(如无向图、有向图、带权图等),并提供丰富的算法库,包括:

    • 布局算法:如分层布局(Sugiyama Layout)、力导向布局(Force-Directed Layout)、树状布局(Tree Layout)等,用于优化节点和边的空间排列。

    • 图操作:支持图的复制、子图提取(如连通分量分离)、节点与边的动态增删等4。

    • 属性管理:通过GraphAttributes类管理节点和边的可视化属性(如颜色、大小、标签),需注意属性与图结构的同步问题。

  • 跨平台与扩展性
    OGDF兼容Windows、Linux和macOS,支持与Qt等GUI框架集成,便于开发交互式图形界面应用。

  • 高性能与模块化设计
    其代码高度优化,适用于大规模图数据处理。用户可通过继承类或重载函数扩展功能,例如自定义布局算法或调整节点渲染逻辑。

与其他工具的对比

  • Boost Graph Library (BGL):BGL侧重通用图算法,而OGDF更专注于可视化与布局优化。

  • Graphviz:Graphviz适合快速生成静态图,OGDF则提供更灵活的API和动态交互支持,适合集成到C++应用中4。

图的绘制 

采用力向布局绘制,即有链接的两个节点会相互靠近。

首先引入库函数
#include <ogdf/basic/Graph.h>
#include <ogdf/basic/GraphAttributes.h>

用Graph创建一个图,通过newnode()创建节点newedge()创建边,只包含图的逻辑结构,不包含可视化的属性。

用graphattributes创建节点属性对象,用来存储图可视化或布局属性
// 创建图
Graph graph;
GraphAttributes ga(graph, GraphAttributes::nodeGraphics | GraphAttributes::edgeGraphics);
 添加节点

接下来开始在图中加入需要的节点(服务器节点/客户端节点)

// 添加服务器节点
node serverNode = graph.newNode();
ga.x(serverNode) = 0;  // 初始坐标
ga.y(serverNode) = 0;

// 添加客户端节点(示例:3个客户端)
std::vector<node> clientNodes;
for (int i = 0; i < 3; ++i) {
    node client = graph.newNode();
    ga.x(client) = i * 50; // 临时坐标,布局算法会覆盖
    ga.y(client) = i * 50;
    clientNodes.push_back(client);
    graph.newEdge(serverNode, client); // 连接服务器与客户端
}
选择力导向布局,使服务器居中,客户端均匀分布
#include <ogdf/energybased/FMMMLayout.h>

FMMMLayout fmmm;
fmmm.useHighLevelOptions(true);// 启用高级配置
fmmm.unitEdgeLength(100); // 控制节点间距
fmmm.newInitialPlacement(true);// 强制重新计算初始位置
fmmm.call(ga); // 应用布局算法,更新节点坐标
这样图的布局部分就完成了,接下来我们需要将绘制好的图映射到view上。在qt中使用QGraphicsSceneQGraphicsView绘制节点和边:(这里要注意一个问题,ogdf采用的是原始坐标系,即x轴从左往右,y轴从下往上递增,而场景视图的不同,他的y轴是从上往下递增的,x轴一样,所以在映射的过程中是需要翻转Y轴坐标)
// 在Qt中创建场景和视图
QGraphicsScene *scene = new QGraphicsScene;
QGraphicsView *view = new QGraphicsView(scene);

// 绘制服务器节点(红色圆形)
QGraphicsEllipseItem *serverItem = scene->addEllipse(
    ga.x(serverNode) - 20, ga.y(serverNode) - 20, 40, 40,
    QPen(Qt::black), QBrush(Qt::red)
);

// 绘制客户端节点(蓝色圆形)和边
for (node client : clientNodes) {
    // 客户端节点
    QGraphicsEllipseItem *clientItem = scene->addEllipse(
        ga.x(v) - 20,     // 椭圆左上角的 X 坐标(中心点 X 减半径)
        ga.y(v) - 20,     // 椭圆左上角的 Y 坐标(中心点 Y 减半径)
        40,               // 椭圆的宽度(直径)
        40,               // 椭圆的高度(直径)
        QPen(Qt::black),  // 边框画笔(黑色,默认宽度 1)
        QBrush(Qt::blue)  // 填充画刷(蓝色)
    );
    
    // 边(服务器到客户端)
    QLineF line(ga.x(serverNode), ga.y(serverNode), ga.x(client), ga.y(client));
    scene->addLine(line, QPen(Qt::gray, 2));
}

view->show();
当客户端连接或断开时,更新OGDF图并刷新布局,实现实时交互
// 添加新客户端
void addClient() {
    node newClient = graph.newNode();
    graph.newEdge(serverNode, newClient);
    clientNodes.push_back(newClient);
    
    // 重新应用布局算法
    FMMMLayout fmmm;
    fmmm.call(ga);
    
    // 更新Qt场景
    updateQtScene();
}

// 删除客户端
void removeClient(node client) {
    graph.delNode(client);
    auto it = std::find(clientNodes.begin(), clientNodes.end(), client);
    if (it != clientNodes.end()) clientNodes.erase(it);
    
    // 重新布局并刷新界面
    FMMMLayout fmmm;
    fmmm.call(ga);
    updateQtScene();
}

// 刷新Qt图形项
void updateQtScene() {
    scene->clear();
    // 重新绘制所有节点和边(参考步骤4)
}

扩展应用:自定义交互

若需实现拖拽节点后更新布局,可结合 Qt 事件和 OGDF:

// 1. Qt 中捕获节点拖拽事件
void MyGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
    // 更新 OGDF 中的坐标
    ga.x(myNode) = event->pos().x();
    ga.y(myNode) = event->pos().y();
}

// 2. 部分重新布局(需自定义算法)
void updateLayout() {
    // 固定已拖拽的节点,仅调整其他节点
    FMMMLayout fmmm;
    fmmm.fixSomeNodes({myNode});  // 假设支持固定节点
    fmmm.call(ga);
}

 将自定义节点属性如IP地址与对应节点相绑定

有三种办法:

方案一:使用外部映射表(推荐)

在 Qt 应用层 维护一个 std::map 或 QHash,将 OGDF 的节点对象映射到业务属性:

// 定义节点业务数据类
struct NodeInfo {
    QString ip;
    QString name;
    // 其他业务字段...
};

// 全局或类成员变量
std::map<ogdf::node, NodeInfo> nodeInfoMap;

// 添加节点时绑定数据
ogdf::node clientNode = graph.newNode();
nodeInfoMap[clientNode] = NodeInfo{"192.168.1.2", "ClientA"};

// 通过节点获取数据(如在Qt点击事件中)
void onNodeClicked(ogdf::node clickedNode) {
    if (nodeInfoMap.contains(clickedNode)) {
        qDebug() << "IP:" << nodeInfoMap[clickedNode].ip;
    }
}

优点

  • 数据与图结构解耦,OGDF 更新(如删除节点)时无需同步业务数据

  • 适用于业务属性复杂或需频繁增删的场景


方案二:扩展 GraphAttributes(高级用法)

通过继承 GraphAttributes 添加自定义属性字段,但需修改 OGDF 源码或自定义包装类:

class CustomGraphAttributes : public ogdf::GraphAttributes {
public:
    // 添加自定义属性
    QString& ip(ogdf::node v) { 
        return m_nodeIP[v]; 
    }
    
private:
    // 使用 OGDF 的扩展机制存储数据
    ogdf::NodeMap<QString> m_nodeIP;
};

// 初始化时使用自定义类
CustomGraphAttributes ga(graph, GraphAttributes::nodeGraphics);
ga.ip(serverNode) = "192.168.1.1";
缺点
  • 需要深入理解 OGDF 内部机制,对新手不友好

  • 修改 OGDF 源码可能导致版本升级冲突


方案三:Qt 图形项存储(简单场景)

将业务数据直接附加到 QGraphicsItem 的自定义数据中:

// 创建节点图形项时存储数据
QGraphicsEllipseItem* clientItem = scene->addEllipse(...);
clientItem->setData(Qt::UserRole, QVariant::fromValue(NodeInfo{"192.168.1.2", "ClientA"}));

// 点击时获取数据
void mousePressEvent(QGraphicsSceneMouseEvent* event) {
    QGraphicsItem* item = scene->itemAt(event->scenePos(), QTransform());
    if (item) {
        NodeInfo info = item->data(Qt::UserRole).value<NodeInfo>();
        qDebug() << "IP:" << info.ip;
    }
}

缺点

  • 数据与图形项绑定,若 OGDF 节点被删除但 Qt 项未及时清理,会导致数据残留

  • 不适合需要基于业务属性进行图算法计算的场景(如按 IP 过滤节点)

 新的问题

到上面图就基本绘制完成了,但是我遇到了一个新的问题,如果链接的节点太多了,场景视图装不下怎么办

  • 解决思路
    1. 计算当前布局的坐标范围​(找到所有节点的最小/最大坐标)。
    2. 将原始坐标归一化​(缩放到 [0, 1] 区间)。
    3. 按目标尺寸缩放并平移,使布局适配到指定区域(如 800x600 的 Qt 场景)。

具体步骤:

1.获取布局的边界范围

  • minX:所有节点中,​最小的 x 坐标值**​(最左侧节点的位置)。
  • ​**maxX:所有节点中,​最大的 x 坐标值**​(最右侧节点的位置)。
  • ​**minY:所有节点中,​最小的 y 坐标值**​(最下方节点的位置)。
  • ​**maxY:所有节点中,​最大的 y 坐标值**​(最上方节点的位置)。
  • 假设节点坐标分布在 x ∈ [50, 950]y ∈ [30, 570]
  • 则 minX=50maxX=950minY=30maxY=570
double minX = std::numeric_limits<double>::max();
double maxX = -minX;
double minY = minX, maxY = maxX;

for (node v : graph.nodes) {
    minX = std::min(minX, ga.x(v));
    maxX = std::max(maxX, ga.x(v));
    minY = std::min(minY, ga.y(v));
    maxY = std::max(maxY, ga.y(v));
}

 先初始化极端值,再把绘制好的节点数据依次遍历比较,比如ga(x,y)节点,min(minX,x),把极值与节点的x比较,取最小的作为新的最小x值,其他同理。

把minx初始化为极小负数,maxx初始化为极大正数,与加入的节点坐标相比对,第一次加入的节点的x初始化minx,后面加入的节点x与minX,maxX比较,比minx小,更新minX,比maxX大,更新MaxX。

 2.计算缩放比例和目标区域

qt界面上的布局如上,view是我们显示的区域,他的x范围是场景的x范围减去两边的margin得到,

maxX-maxY得到绘制的范围,用目标的范围除以绘制的范围就可以得到缩放比例,取x的比例和y的比例最小,保证x,y都唔那个缩小进目标。实现代码如下:

double targetWidth = 800.0;
double targetHeight = 600.0;
double scaleX = (targetWidth - 2 * margin) / (maxX - minX);
double scaleY = (targetHeight - 2 * margin) / (maxY - minY);
double scale = std::min(scaleX, scaleY); // 保持宽高比

 3.进行缩放和偏移

我们计算好了缩放比例,下一步开始缩放并放到view中,注意要加个margin,有个边框的

for (node v : graph.nodes) {
    ga.x(v) = (ga.x(v) - minX) * scale + margin;
    ga.y(v) = (ga.y(v) - minY) * scale + margin;
}

这样就解决了边界溢出的问题,这是其中一种方法,网上面还有动态调整场景范围,QT自动适配fitInView,OGDF封装的布局包装类LayoutPlanarizationGrid

// 计算所有图元的边界矩形
QRectF itemsBoundingRect = scene.itemsBoundingRect();

// 调整视图,使所有内容可见
view.fitInView(itemsBoundingRect, Qt::KeepAspectRatio);

 或

PlanarizationGridLayout pgl;
pgl.setPageRatio(1.0);       // 设置宽高比
pgl.setMinimalNodeDistance(20);
pgl.call(ga);

依据情况选用。

这样之前想到的问题就解决了,各位如果有什么新的想法或者建议欢迎告诉我本人作品永久开源,希望志同道合的网友一起学习建设。如果觉得写的可以记得一件三连哦。


网站公告

今日签到

点亮在社区的每一天
去签到