这篇博客可能会有点长,因为是一个课程的大作业,包含的内容比较多,这个项目的开发的时间在两周左右,所以这个软件指是一个简单又比较简陋的小桌面应用。
导入文件:
用户能够通过界面导入描述课程依赖关系的文本文件(.txt),文件格式为每行表示一个有向边的关系。
绘制拓扑排序图:
根据导入的课程依赖关系,能够绘制出对应的拓扑排序图,使用直观的图形方式展示课程间的依赖关系。
导出图像:
用户可以将绘制好的拓扑排序图导出为图片(.png格式),以便于保存和分享。
导出拓扑排序结果:
用户可以将拓扑排序的结果导出为文本文件(.txt格式),用于后续分析和处理。
展示C++程序输出:
显示调用外部C++程序计算拓扑排序后的输出结果,以便用户查看拓扑排序的详细信息。
主界面:
提供导入文件、绘制图像、导出图像、导出拓扑排序结果的按钮,以及显示绘制好的图像和C++程序输出的区域。
用户操作反馈:
显示错误、警告等反馈信息,确保用户能够清晰了解操作结果。
性能需求
若所有拓扑排序的结果非常多则需要很快速的返回正确并且结果个数正确的结果,避免用户等待过长的时间。同时绘制拓扑排序图应在合理的时间范围内完成,避免用户等待时间过长。
其他需求
①软件应该通过所给样例的测试;
②软件应该支持在Windows操作系统上运行
该文档对拓扑排序图绘制工具项目——TopologicalSort_app软件进行可行性分析,主要包括技术可行性和操作可行性的分析,以确保项目的实施和开发是合理、可行的。
①使用PySide2库实现图形用户界面,提供友好的交互。
②使用networkx和matplotlib库绘制拓扑排序图,能够高效、准确地展示图形。
③利用Python内置的文件处理功能实现文件导入、导出功能。
④使用subprocess库调用外部C++程序进行拓扑排序,实现图的计算。
⑤使用tempfile库创建临时文件,以保存绘制好的图像。
⑥通过在Python项目中调用C++程序,实现了对拓扑排序的计算。
⑦使用Python的subprocess库调用外部C++程序,并获取其输出。
⑧这种跨语言调用对实现拓扑排序算法具有良好的技术可行性,确保了项目核心功能的实现。
①开发人员需熟悉Python编程语言及其相关库,如PySide2、networkx、matplotlib、subprocess等。
②需要了解图论中的拓扑排序算法以及相关概念。
①技术方案基于成熟的Python库实现,具有较高的技术可行性。
②Python具有丰富的第三方库和开发资源,能够快速实现项目需求。
①项目设计简单明了,操作界面直观友好,用户容易上手。
②提供了导入、导出、绘制图像等功能按钮,用户操作便捷,符合用户使用习惯。
③C++程序的调用对用户是透明的,用户只需使用界面提供的功能,不需要关心底层实现语言。
④用户操作界面简单明了,易于上手,提供了直观的导入、导出、绘制图像等功能按钮,满足用户操作习惯,操作可行性较高。
①该项目具有较高的技术可行性,开发成本较低,运维成本也较低。操作界面简单明了,用户操作便捷。
②调用C++程序作为拓扑排序的计算引擎是技术上可行的,不会对整体的可行性产生负面影响。
③用户无需关心C++程序调用细节,操作界面简单易用,用户操作的可行性较高,确保了项目的实施和开发是合理、可行的。
日期 | 版本 | 说明 | 作者 |
---|---|---|---|
2023.9.10 | 1.0.0 | 创建好初步的页面 | hiddenSharp |
2023.9.11 | 1.0.1 | 完善了页面的排版 | hiddenSharp |
2023.9.14 | 1.1.0 | 1. 为生成的拓扑排序图片添加了放大和缩小按钮 2. 为生成的拓扑图片添加了背景颜色 |
hiddenSharp |
2023.9.15 | 1.2.0 | 1. 删除了放大和缩小按钮 2. 优化了图片大小格式以及清晰度 3. 新增导入文件后可以之间生成该图的所有拓扑排序结果 |
hiddenSharp |
2023.9.16 | 1.2.1 | 1. 重新导入文件后将清空之前生成的图片并且进度条置零 2. 初始化进度条值为0,导入文件后增加50,生成图片后再加50 3. 删除了自动导出结果的功能,修改为用户手动点击Export进行结果的导出 4. 新增用户进行导出操作后,可以下拉选择导出的文件类型(.txt为所有的拓扑排序结果,.png为拓扑图) |
hiddenSharp |
2023.9.17 | 1.2.2 | 1. 固定了软件的窗口大小,不可调整窗口大小以及最大化 2. 调整了进度条的逻辑,取消了50的值,只有0与100 3. 完善了ADD 和 DEL按钮后生成图片以及拓扑排序结果的逻辑 4. DEL 按钮和 ADD按钮异常BUG |
hiddenSharp |
2023.9.23 | 1.3.0 | 进行了项目结构的重构,更加具有面向对象的思想,将各模块分离出来了。新增FileManager类、InputManager类、TopologyManager类,将MainWindow类进行解耦和重构。 | hiddenSharp |
TopologicalSort_app的主要功能是执行拓扑排序算法,用户可以导入图的节点和边,一条边的格式应该为
然后调用C++算法进行排序,随后返回结果并显示在软件屏幕上。上面已经详细说明了需求分析,故在此不再赘述。
注:并没有draw_diagram类,只是一个py文件,里面写了一个draw_directed_graph函数,该函数通过调用networkx和matplotlib来完成图片的生成。
定义一个课程结构体,声明二维向量,利用dfs函数递归进行深度优先搜索,生成所有可能的结果,判断是否存在循环依赖关系,用户可导出排序结果
(1)对于每一个课程,判断当前课程是否满足拓扑排序的条件,即入度为0且未被访问过;
(2)如果满足条件,将其添加到 result 中并将当前课程标记为已访问;
(3)对于每一个课程,判断当前课程是否满足拓扑排序的条件,即入度为0且未被访问过;
(4)如果满足条件,将其添加到 result 中并将当前课程标记为已访问;
(5)遍历当前课程的后继课程;将所有当前课程对应的后继课程入度减1;
(6)递归调用 dfs 函数处理下一个课程;
(7)回溯,将当前课程标记为未访问,回复后继课程的入度,从 result 中移除最后一门课程,得出其他分支结果;
(8)继续遍历下一个课程(更换拓扑排序的起始课程),重复上述步骤(在循环里);
(9)当所有的课程都被遍历完后,dfs 函数执行结束;将当前的拓扑排序结果 result 添加到 allTopologicalSorts 向量中。
Filename:data_4.txt
Content:
< 0,1>
< 1,3>
< 0,2>
< 2,4>
< 4,5>
< 3,5>
Result:
sort ruselt_1:0 1 3 2 4 5
---------------------
sort ruselt_2:0 1 2 3 4 5
---------------------
sort ruselt_3:0 1 2 4 3 5
---------------------
sort ruselt_4:0 2 1 3 4 5
---------------------
sort ruselt_5:0 2 1 4 3 5
---------------------
sort ruselt_6:0 2 4 1 3 5
TopologicalSort_app 是一个Python项目,它基于PySide2库实现的图形用户界面(GUI)应用程序,用于创建、导入、导出拓扑排序图。下面将介绍它的一些系统特色:
TopologicalSort_app
├─.idea
├─build
├─core (核心算法)
├─data (读入的文件信息)
├─dist (发布软件的各个版本)
├─docs (所有文档信息)
├─lib
├─log
├─statics (静态资源)
├─test (测试)
├─venv
└─__pycache__
IDE:pycharm community v2023+
解释器:python v3.6.8
外部库: PySide2 v5.15.2.1、networkx v2.5.1、matplotlib v3.3.4
GUI工具:QtDesigner
写在前面:这个c++程序在整个项目中是比较核心的一个部分,它利用c++运行速度更快来作为核心程序,让整个python项目调用,以达到核心业务和整个项目解耦的目的。这个.cpp文件不会直接调用,项目只会调用编译过后的TopologicalSort.exe,而这个文件会存放在statics文件夹下面。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
struct Course {
string name; // 课程名
vector<Course*> prerequisites; // 对应的先修课程的指针向量
int indegree; // 入度(有多少个先修条件)
bool visited; // 判断课程是否被访问过
Course(const string& n) : name(n), indegree(0), visited(false) {} // 构造函数
};
vector<vector<string>> allTopologicalSorts;
void dfs(vector<Course*>& courses, vector<string>& result) {
if (result.size() == courses.size()) {
allTopologicalSorts.push_back(result);
return; // 递归终止条件:完成了一次拓扑排序
}
for (size_t i = 0; i < courses.size(); ++i) { // 遍历每个课程
Course* course = courses[i]; // 当前课程
if (course->indegree == 0 && !course->visited) { // 如果当前课程的入度为0且未被访问过
course->visited = true; // 标记当前课程已访问
result.push_back(course->name); // 将当前课程添加到当前排列中
for (size_t j = 0; j < course->prerequisites.size(); ++j) { // 减少当前课程的邻接课程的入度
Course* prerequisite = course->prerequisites[j];
prerequisite->indegree--;
}
dfs(courses, result); //递归
course->visited = false; // 标记当前课程为未访问状态
for (size_t j = 0; j < course->prerequisites.size(); ++j) { // 回溯:撤销之前的修改
Course* prerequisite = course->prerequisites[j];
prerequisite->indegree++; // 恢复后续邻接课程的入度
}
result.pop_back(); // 移除当前排列中的最后一门课程
}
}
}
bool printAllTopologicalSorts(vector<Course*>& courses) {
vector<string> result;
dfs(courses, result);
return !allTopologicalSorts.empty();
}
void shuchu(vector<Course*>& courses) {
int count = 0;
for (size_t i = 0; i < allTopologicalSorts.size(); ++i) {
cout << "sort ruselt_" << ++count << ':';
for (size_t j = 0; j < allTopologicalSorts[i].size(); ++j) {
cout << allTopologicalSorts[i][j] << " ";
}
cout << endl;
if (i != allTopologicalSorts.size() - 1) {
cout << "---------------------" << endl;
}
}
}
int main(int argc, char* argv[]) {
if (argc < 2) {
cout << "Usage: " << argv[0] << " " << endl;
return 0;
}
string filename = argv[1];
vector<Course*> courses;
unordered_map<string, Course*> courseMap;
ifstream file(filename);
if (file.is_open()) {
string line;
while (getline(file, line)) {
if (line.empty()) {
continue;
}
line = line.substr(1, line.size() - 2);
stringstream ss(line);
string courseName, prereqName;
getline(ss, courseName, ',');
getline(ss, prereqName);
Course* course = courseMap[courseName];
if (!course) {
course = new Course(courseName);
courses.push_back(course);
courseMap[courseName] = course;
}
Course* prereq = courseMap[prereqName];
if (!prereq) {
prereq = new Course(prereqName);
courses.push_back(prereq);
courseMap[prereqName] = prereq;
}
course->prerequisites.push_back(prereq);
prereq->indegree++;
}
file.close();
} else {
cout << "无法打开文件" << endl;
return 0;
}
if (printAllTopologicalSorts(courses)) {
shuchu(courses);
} else {
cout << "存在循环依赖关系" << endl;
}
for (size_t i = 0; i < courses.size(); i++) {
delete courses[i];
}
courses.clear();
return 0;
}
import networkx as nx
import matplotlib.pyplot as plt
import tempfile
def draw_directed_graph(edges, figsize=(3, 3)):
try:
# 创建一个有向图对象
G = nx.DiGraph()
# 添加有向边
for data in edges:
data = data.strip('<>')
source, target = data.split(',')
G.add_edge(source, target)
# 设置图片的大小
plt.figure(figsize=figsize)
# 绘制有向图
pos = nx.spring_layout(G)
# Adjust node positions for labels to be around the nodes
pos_labels = {node: (x, y + 0.01) for node, (x, y) in pos.items()}
nx.draw(G, pos, with_labels=False, node_color='g', node_size=200, arrows=True)
# Draw labels separately with adjusted positions
nx.draw_networkx_labels(G, pos_labels, font_size=10)
# 保存图形到临时文件
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
plt.savefig(tmpfile, format="png", bbox_inches="tight")
# 返回临时文件的路径
return tmpfile.name
except Exception as e:
print(f"生成图时出现错误:{str(e)}")
from PySide2.QtWidgets import QFileDialog, QMessageBox
class FileManager:
def __init__(self, main_window, input_manager):
self.main_window = main_window
self.input_manager = input_manager
def import_file(self):
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(self.main_window.ui, "选择要导入的文件", "", "文本文件 (*.txt);;所有文件 (*)", options=options)
if file_path:
try:
self.main_window.clear_topology_graph() # 清空拓扑排序图
self.main_window.file_path = file_path
with open(file_path, 'r', encoding='utf-8') as file:
file_content = file.read()
# 调用C++文件并获取结果
cpp_output = self.main_window.run_cpp(file_path)
# 将结果显示在plainTextEdit上
self.main_window.display_cpp_output(cpp_output)
self.input_manager.fill_input_boxes(file_content)
except Exception as e:
QMessageBox.critical(self.main_window.ui, "错误", f"导入文件时出现错误:{str(e)}")
else:
QMessageBox.warning(self.main_window.ui, "警告", "未选择任何文件")
def export(self):
options = QFileDialog.Options()
export_option, _ = QFileDialog.getSaveFileName(self.main_window.ui, "选择导出路径", "",
"Images (*.png);;Text Files (*.txt)", options=options)
if export_option:
if export_option.endswith(".png"):
# 导出图片
self.main_window.export_image(export_option)
elif export_option.endswith(".txt"):
# 导出拓扑排序结果
self.main_window.export_topology(export_option)
else:
QMessageBox.warning(self.main_window.ui, "警告", "不支持的导出格式")
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QListWidgetItem, QLineEdit, QAbstractItemView
class InputManager:
def __init__(self, main_window):
self.main_window = main_window
def fill_input_boxes(self, file_content):
# 清空现有输入框的内容
self.main_window.ui.inputList.clear()
data_list = file_content.split('\n') # 按换行符分割数据
for data in data_list:
data = data.strip()
if data:
# 提取源节点和目标节点
source, target = data.split(',')
source = source.strip()
target = target.strip()
# 创建适当的输入格式
input_item = QListWidgetItem()
input_line_edit = QLineEdit(f"{source},{target}")
input_line_edit.setAlignment(Qt.AlignCenter)
self.main_window.ui.inputList.addItem(input_item)
self.main_window.ui.inputList.setItemWidget(input_item, input_line_edit)
def add_input_field(self):
input_item = QListWidgetItem()
input_line_edit = QLineEdit('' )
input_line_edit.setAlignment(Qt.AlignCenter) # 设置文本居中对齐
self.main_window.ui.inputList.addItem(input_item)
self.main_window.ui.inputList.setItemWidget(input_item, input_line_edit)
# 将边信息添加到列表中
self.main_window.edge_info.append('' )
def del_input_field(self):
selected_items = self.main_window.ui.inputList.selectedItems()
for item in selected_items:
index = self.main_window.ui.inputList.row(item)
self.main_window.ui.inputList.takeItem(index)
# 从列表中删除对应的边信息
if index < len(self.main_window.edge_info):
del self.main_window.edge_info[index]
import os
import subprocess
from PySide2.QtWidgets import (
QFileDialog, QMessageBox, QListWidgetItem,
QLineEdit, QAbstractItemView, QWidget,
QVBoxLayout, QLabel
)
from PySide2.QtUiTools import QUiLoader
from PySide2.QtCore import Qt
from .file_manager import FileManager
from .input_manager import InputManager
from .topology_manager import TopologyManager
class MainWindow:
def __init__(self):
# 动态加载.ui文件
self.ui = QUiLoader().load('statics/main.ui')
# 禁止调整窗口大小
self.ui.setFixedSize(self.ui.size())
# 设置窗口属性,禁止最大化
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowMaximizeButtonHint)
# 用于存储边信息的列表
self.edge_info = []
# 存储文件路径
self.file_path = None
# 初始化控制器
self.input_manager = InputManager(self)
self.file_manager = FileManager(self, self.input_manager)
self.topology_manager = TopologyManager(self)
# 连接相关操作的槽函数
self.ui.actionImport.triggered.connect(self.file_manager.import_file)
self.ui.actionExport.triggered.connect(self.file_manager.export)
self.ui.addButton.clicked.connect(self.input_manager.add_input_field)
self.ui.delButton.clicked.connect(self.input_manager.del_input_field)
self.ui.generateButton.clicked.connect(self.topology_manager.generate_draw)
# 设置输入框为单选模式
self.ui.inputList.setSelectionMode(QAbstractItemView.SingleSelection)
# 设置按钮的提示文本
self.ui.addButton.setToolTip("添加输入框 (Alt + A)")
self.ui.delButton.setToolTip("删除输入框 (Alt + D)")
self.ui.generateButton.setToolTip("生成拓扑排序结果 (Alt + Enter)")
# 创建一个 QWidget 作为容器
self.photo_container = QWidget()
self.ui.photoLabel.layout().addWidget(self.photo_container)
self.ui.photoLabel.setStyleSheet("background-color: white;")
# 在容器上设置布局
container_layout = QVBoxLayout()
self.photo_container.setLayout(container_layout)
# 将 QLabel 添加到容器中
self.photo_label = QLabel()
container_layout.addWidget(self.photo_label)
def clear_topology_graph(self):
self.photo_label.clear() # 清空 QLabel 上的图像
self.ui.progressBar.setValue(0) # 重置进度条的值
def run_cpp(self, file_path):
try:
# 获取当前脚本所在目录
script_directory = os.path.dirname(os.path.abspath(__file__))
# 构建调用命令
cpp_executable_path = os.path.join(script_directory, 'TopologicalSort.exe')
command = f'"{cpp_executable_path}" "{file_path}"'
# 调用外部程序
result = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
# 返回结果
return result.stdout
except Exception as e:
print("Error during running C++ program:", str(e))
return "Error: Unable to run C++ program"
# 在plainTextEdit中显示C++程序的输出
def display_cpp_output(self, cpp_output):
try:
# 将字节串解码为字符串
cpp_output_str = cpp_output.decode('utf-8')
# 在plainTextEdit中显示C++程序的输出
self.ui.plainTextEdit.setPlainText(cpp_output_str)
except Exception as e:
print("Error: Unable to display C++ output:", str(e))
self.ui.plainTextEdit.setPlainText("Error: Unable to display C++ output")
def export_image(self, export_path):
pixmap = self.photo_label.pixmap()
if pixmap:
pixmap.save(export_path, "PNG")
QMessageBox.information(self.ui, "导出成功", f"图像已成功导出到:{export_path}")
else:
QMessageBox.warning(self.ui, "警告", "没有图像可导出")
def export_topology(self, export_path):
try:
# 获取C++程序的输出
cpp_output = self.ui.plainTextEdit.toPlainText().encode('utf-8')
# 写入文件
with open(export_path, 'wb') as file:
file.write(cpp_output)
QMessageBox.information(self.ui, "导出成功", f"拓扑排序结果已成功导出到:{export_path}")
except Exception as e:
QMessageBox.critical(self.ui, "错误", f"导出拓扑排序结果时出现错误:{str(e)}")
if __name__ == "__main__":
# Create the application instance
import sys
from PySide2.QtWidgets import QApplication
app = QApplication(sys.argv)
# Create and show the main window
mainWindow = MainWindow()
mainWindow.ui.show()
# Start the event loop
sys.exit(app.exec_())
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QMessageBox
from .draw_diagram import draw_directed_graph
class TopologyManager:
def __init__(self, main_window):
self.main_window = main_window
def generate_draw(self):
try:
if not self.main_window.file_path:
QMessageBox.warning(self.main_window.ui, "警告", "未选择任何文件")
return
# 获取所有输入框的值
input_items = [self.main_window.ui.inputList.itemWidget(self.main_window.ui.inputList.item(i)).text()
for i in range(self.main_window.ui.inputList.count())]
# 将边信息更新为当前输入框中的值
self.main_window.edge_info = input_items
# 调用绘图函数并获取图形文件路径
graph_image_path = draw_directed_graph(input_items)
if graph_image_path:
# 将图形文件设置为QLabel的图像
pixmap = QPixmap(graph_image_path)
self.main_window.photo_label.setPixmap(pixmap)
self.main_window.ui.progressBar.setValue(100)
# 更新文件中的边信息(覆盖原文件)
with open(self.main_window.file_path, 'w', encoding='utf-8') as file:
file.write('\n'.join(input_items))
# 重新调用C++程序并更新输出
cpp_output = self.main_window.run_cpp(self.main_window.file_path)
self.main_window.display_cpp_output(cpp_output)
except Exception as e:
print("Error during generate_draw:", str(e))
有这些核心源代码可能远远不够,因为还有些不是代码的核心文件,如:使用QtDesigner设计的页面UI——main.ui文件,因此在下面我会放上这个项目的GitHub仓库地址,如果有需要可以自取哦~
https://github.com/hiddenSharp429/ToplogicalSort_app
①打开TopologicalSort_app文件夹,双击dist文件夹进入所有发布程序选择页面
各个版本的区别请看\ToplogicalSort_app\docs\ToplogicalSort_app更新日志.md
④双击后启动软件
一般测试样例都会放在\ToplogicalSort_app\data\里面
④导入后左边的输入框将会生成,文件中的文本将会被匹配填充,并且在下面的输入框中输出了所有拓扑排序的结果
⑤随后点击Generate按钮生成拓扑排序图片
⑦可选择导出.txt类型文件还是.png文件,若为.txt文件则导出所有拓扑排序的结果,若为.png文件则导出拓扑图
如果有疑问欢迎大家留言讨论,你如果觉得这篇文章对你有帮助可以给我一个免费的赞吗?我们之间的交流是我最大的动力!