前言
在網絡通訊中,我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);
依據情況選用。
這樣之前想到的問題就解決了,各位如果有什么新的想法或者建議歡迎告訴我本人作品永久開源,希望志同道合的網友一起學習建設。如果覺得寫的可以記得一件三連哦。