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中使用QGraphicsScene
和QGraphicsView
绘制节点和边:(这里要注意一个问题,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 过滤节点)
新的问题
到上面图就基本绘制完成了,但是我遇到了一个新的问题,如果链接的节点太多了,场景视图装不下怎么办
- 解决思路:
- 计算当前布局的坐标范围(找到所有节点的最小/最大坐标)。
- 将原始坐标归一化(缩放到
[0, 1]
区间)。 - 按目标尺寸缩放并平移,使布局适配到指定区域(如
800x600
的 Qt 场景)。
具体步骤:
1.获取布局的边界范围
minX
:所有节点中,最小的 x 坐标值**(最左侧节点的位置)。- **
maxX
:所有节点中,最大的 x 坐标值**(最右侧节点的位置)。 - **
minY
:所有节点中,最小的 y 坐标值**(最下方节点的位置)。 - **
maxY
:所有节点中,最大的 y 坐标值**(最上方节点的位置)。 - 假设节点坐标分布在
x ∈ [50, 950]
,y ∈ [30, 570]
。 - 则
minX=50
,maxX=950
,minY=30
,maxY=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);
依据情况选用。
这样之前想到的问题就解决了,各位如果有什么新的想法或者建议欢迎告诉我本人作品永久开源,希望志同道合的网友一起学习建设。如果觉得写的可以记得一件三连哦。