QT网络拓扑图绘制实验

前言

在网络通讯中,我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);

依据情况选用。

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习—李宏毅教程笔记】各式各样的 Attention

目录 一、普通 Self-Attention 的痛点 二、对 Self-Attention 的优化方式 1、Local Attention / Truncated Attention 2、Stride Attention 3、Global Attention 4、知名的 Self-Attention 的变形的应用 &#xff08;1&#xff09;Longformer &#xff08;2&#xff09…

OceanBases数据库单机社区版保姆级安装

目录 背景 简介 安装 OceanBase 下载地址 上传解压安装包 ​编辑 执行安装命令 ​编辑 应用环境配置 执行以下命令&#xff0c;快速部署 OceanBase 数据库(仅用于简单使用&#xff0c;不应用于生产)。 查看一下数据库状态 ​编辑连接数据库 用户创建 使用工具Navi…

Linux守护进程

一、相关概念 QQ邮箱关于三种协议的解释&#xff1a;SMTP/IMAP服务 1.SMTP协议 SMTP&#xff08;​​Simple Mail Transfer Protocol​​&#xff0c;简单邮件传输协议&#xff09;是一种用于发送电子邮件的互联网标准。它在TCP/IP协议族中&#xff0c;通常使用25端口进行通…

Java【网络原理】(4)HTTP协议

目录 1.前言 2.正文 2.1自定义协议 2.2HTTP协议 2.2.1抓包工具 2.2.2请求响应格式 2.2.2.1URL 2.2.2.2urlencode 2.2.3认识方法 2.2.3.1GET与POST 2.2.3.2PUT与DELETE 2.2.4请求头关键属性 3.小结 1.前言 哈喽大家好啊&#xff0c;今天来继续给大家带来Java中网络…

【版本控制】idea中使用git

大家好&#xff0c;我是jstart千语。接下来继续对git的内容进行讲解。也是在开发中最常使用&#xff0c;最重要的部分&#xff0c;在idea中操作git。目录在右侧哦。 如果需要git命令的详解&#xff1a; 【版本控制】git命令使用大全-CSDN博客 一、配置git 要先关闭项目&#xf…

【中间件】redis使用

一、redis介绍 redis是一种NoSQL类型的数据库&#xff0c;其数据存储在内存中&#xff0c;因此其数据查询效率很高&#xff0c;很快。常被用作数据缓存&#xff0c;分布式锁 等。SpringBoot集成了Redis&#xff0c;可查看开发文档Redis开发文档。Redis有自己的可视化工具Redis …

一文粗通 Celery 分布式任务队列

目录 简介什么是 CeleryCelery 的基本组成Celery 的应用场景快速开始 设置热重载开发脚本基本任务管理绑定任务本身设置任务的执行超时时间允许任务重试自定义任务名称实现任务优先级 高级任务管理任务延迟执行指定时间执行任务超时自动取消任务优先级重试任务 任务链与工作流简…

知识了解03——怎么解决使用npm包下载慢的问题?

1、为什么使用npm下载包会下载的慢 因为使用npm下载包时&#xff0c;默认使用国外服务器进行下载&#xff0c;此时的网络传输需要经过漫长的海底电缆&#xff0c;因此下载速度会变慢 2、怎么解决&#xff1f;&#xff08;切换镜像源&#xff09; &#xff08;1&#xff09;方…

系统思考与理性决策

汉诺贝克在《逆向投资心理学&#xff1a;引发市场波动的非技术因素分析》书中提到&#xff1a;“心理造就90%的行情。投资者利用别人的愚蠢所获得的利益&#xff0c;往往比靠自己得到智慧来得多。避免跟风&#xff0c;我觉得可以降低或者识别绝大部分的陷阱和风险。”这一观点深…

IP数据报

IP数据报组成 IP数据报&#xff08;IP Datagram&#xff09;是网络中传输数据的基本单位。 IP数据报头部 版本&#xff08;Version&#xff09; 4bit 告诉我们使用的是哪种IP协议。IPv4版本是“4”&#xff0c;IPv6版本是“6”。 头部长度&#xff08;IHL&#xff0c;Intern…

卡洛诗以“中式西餐”为锚点

意大利的浪漫与中国的烟火气&#xff0c;看似相隔万里&#xff0c;却在卡洛诗的餐桌上碰撞出令人惊艳的火花。 西餐曾是中国餐饮市场的“奢侈品”——复杂的工艺、高昂的定价&#xff0c;让它与普通人的日常餐桌格格不入。卡洛诗创始人敏锐地捕捉到这一矛盾&#xff1a;“西餐…

前端与传统接口的桥梁:JSONP解决方案

1.JSONP原理 1.1.动态脚本注入 说明&#xff1a;通过创建 <script> 标签绕过浏览器同源策略 1.2.回调约定 说明&#xff1a;服务端返回 函数名(JSON数据) 格式的JS代码 1.3.自动执行 说明&#xff1a;浏览器加载脚本后立即触发前端预定义的回调函数&#xff08;现代开…

STM32---外部中断EXTI

目录 一、中断向量表 二、EXTI工作原理图 三、NVIC模块 四、GPIO设置为EXTI的结构 五、C语言示例代码 在STM32中&#xff0c;中断是一个非常重要的结构&#xff0c;他能让我们在执行主函数的时候&#xff0c;由硬件检测一些外部或内部产生的中断信号&#xff0c;跳转到中断…

SQL注入 01

0x01 用户、脚本、数据库之间的关系 首先客户端发出了ID36的请求&#xff0c;脚本引擎收到后将ID36的请求先代入脚本的sql查询语句Select * from A where id 36 &#xff0c; 然后将此代入到数据库中进行查询&#xff0c;查到后将返回查询到的所有记录给脚本引擎&#xff0c;接…

Retinex系列图像/视频增强算法介绍

Retinex 系列原理基础 一、核心原理与理论 Retinex算法基于人类视觉系统特性,认为观测到的图像由光照分量(L)与反射分量( R )乘积构成,即: S ( x , y ) = L ( x , y

在 Visual Studio Code 中安装通义灵码 - 智能编码助手

高效的编码工具对于提升开发效率和代码质量至关重要。 通义灵码作为一款智能编码助手&#xff0c;为开发者提供了全方位的支持。 本文将详细介绍如何在 Visual Studio Code&#xff08;简称 VSCode&#xff09;中安装通义灵码&#xff0c;以及如何进行相关配置以开启智能编码…

Git 查看提交历史

Git提交历史深度解析&#xff1a;从代码考古到精准回退 前言 在软件开发的生命周期中&#xff0c;提交历史是团队协作的时空胶囊。Git作为分布式版本控制系统&#xff0c;其强大的历史追溯能力可帮助开发者&#xff1a; 精准定位引入Bug的提交分析代码演进趋势恢复误删的重要…

Linux操作系统学习之---进程状态

目录 明确进程的概念: Linux下的进程状态: 虚拟终端的概念: 见一见现象: 用途之一 : 结合指令来监控进程的状态: 和进程强相关的系统调用函数接口: getpid()和getppid(): fork(): fork函数创建子进程的分流逻辑: 进程之间具有独立性: 进程中存在的写时拷贝: 见一见进程状态…

vue学习笔记06

学习的课程地址&#xff1a;老杜Vue视频教程&#xff0c;Vue2&#xff0c;Vue3实战精讲&#xff0c;一套通关vue_哔哩哔哩_bilibili 1、vue程序初体验 2、vue核心技术(基础) 3、Vue组件化 前面参见&#xff1a; vue学习笔记01 vue学习笔记02 vue学习笔记03 vue学习笔记…

linux 学习 4.1 目录查询的相关命令(不涉及修改的命令)

目录 命令含义/bin(binary)存放可执行程序或脚本文件/sys(system)存放系统相关的文件/dev(device)存放设备文件/etc系统配置文件/lib(library)存放系统文件/var(variable)存放变化速度快的文件/proc(process)存放进程相关的数据/rootroot用户的家目录/home/{username}username…