拓扑排序软件设计——ToplogicalSort_app(含有源码、需求分析、可行性分析、概要设计、用户使用手册)

发布时间 2023-12-14 11:24:04作者: hiddenSharp429

@

前言

这篇博客可能会有点长,因为是一个课程的大作业,包含的内容比较多,这个项目的开发的时间在两周左右,所以这个软件指是一个简单又比较简陋的小桌面应用。


1. 需求分析

  1. 导入文件:
    用户能够通过界面导入描述课程依赖关系的文本文件(.txt),文件格式为每行表示一个有向边的关系。

  2. 绘制拓扑排序图:
    根据导入的课程依赖关系,能够绘制出对应的拓扑排序图,使用直观的图形方式展示课程间的依赖关系。

  3. 导出图像:
    用户可以将绘制好的拓扑排序图导出为图片(.png格式),以便于保存和分享。

  4. 导出拓扑排序结果:
    用户可以将拓扑排序的结果导出为文本文件(.txt格式),用于后续分析和处理。

  5. 展示C++程序输出:
    显示调用外部C++程序计算拓扑排序后的输出结果,以便用户查看拓扑排序的详细信息。

  6. 主界面:
    提供导入文件、绘制图像、导出图像、导出拓扑排序结果的按钮,以及显示绘制好的图像和C++程序输出的区域。

  7. 用户操作反馈:
    显示错误、警告等反馈信息,确保用户能够清晰了解操作结果。

  8. 性能需求
    若所有拓扑排序的结果非常多则需要很快速的返回正确并且结果个数正确的结果,避免用户等待过长的时间。同时绘制拓扑排序图应在合理的时间范围内完成,避免用户等待时间过长。

  9. 其他需求
    ①软件应该通过所给样例的测试;
    ②软件应该支持在Windows操作系统上运行


2. 可行性分析

2.1 简介

该文档对拓扑排序图绘制工具项目——TopologicalSort_app软件进行可行性分析,主要包括技术可行性和操作可行性的分析,以确保项目的实施和开发是合理、可行的。

2.2 技术可行性分析

2.2.1 技术实现方案

①使用PySide2库实现图形用户界面,提供友好的交互。
②使用networkx和matplotlib库绘制拓扑排序图,能够高效、准确地展示图形。
③利用Python内置的文件处理功能实现文件导入、导出功能。
④使用subprocess库调用外部C++程序进行拓扑排序,实现图的计算。
⑤使用tempfile库创建临时文件,以保存绘制好的图像。
⑥通过在Python项目中调用C++程序,实现了对拓扑排序的计算。
⑦使用Python的subprocess库调用外部C++程序,并获取其输出。
⑧这种跨语言调用对实现拓扑排序算法具有良好的技术可行性,确保了项目核心功能的实现。

2.2.2 开发人员技能要求

①开发人员需熟悉Python编程语言及其相关库,如PySide2、networkx、matplotlib、subprocess等。
②需要了解图论中的拓扑排序算法以及相关概念。

2.2.3 可行性

①技术方案基于成熟的Python库实现,具有较高的技术可行性。
②Python具有丰富的第三方库和开发资源,能够快速实现项目需求。

2.3 操作可行性分析

①项目设计简单明了,操作界面直观友好,用户容易上手。
②提供了导入、导出、绘制图像等功能按钮,用户操作便捷,符合用户使用习惯。
③C++程序的调用对用户是透明的,用户只需使用界面提供的功能,不需要关心底层实现语言。
④用户操作界面简单明了,易于上手,提供了直观的导入、导出、绘制图像等功能按钮,满足用户操作习惯,操作可行性较高。

2.4 结论

①该项目具有较高的技术可行性,开发成本较低,运维成本也较低。操作界面简单明了,用户操作便捷。
②调用C++程序作为拓扑排序的计算引擎是技术上可行的,不会对整体的可行性产生负面影响。
③用户无需关心C++程序调用细节,操作界面简单易用,用户操作的可行性较高,确保了项目的实施和开发是合理、可行的。


3. 项目报告

3.1 修订历史记录

日期 版本 说明 作者
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

3.2 软硬件环境

  • 操作系统:Windows
  • 硬件要求:暂无特定硬件需求
  • 开发工具:主要的IDE为PyCharm + Visual Stadio 2010;使用到的编程语言为Python + C++;文本编辑器使用的为Notepad++(不是硬性要求);编译器为Python3.6
  • 第三方库和依赖项:PySide2 v5.15.2.1、networkx v2.5.1

3.3 需求分析

TopologicalSort_app的主要功能是执行拓扑排序算法,用户可以导入图的节点和边,一条边的格式应该为 <arc_start,arc_end> 然后调用C++算法进行排序,随后返回结果并显示在软件屏幕上。上面已经详细说明了需求分析,故在此不再赘述。

3.4 详细设计

3.4.1 类设计

  • MainWindow类
    1. 在_init_函数中创建主窗口页面、存储主窗口信息、初始化控制器、连接槽函数。
    2. 在clear_topology_graph函数中清空拓扑图
    3. 在run_cpp函数中调用编译后的.cpp文件
    4. 在display_cpp_output函数中显示C++程序的输出到指定位置
    5. 在export_image函数中设置导出拓扑排序图片的操作
    6. 在export_topology函数中设置导出拓扑排序结果的操作
  • FileManager类
    1. 在_init_函数中载入MainWindow实例和InputManager实例
    2. 在import_file函数中设置导入文件的操作
    3. 在export函数中设置导出的操作
  • InputManager类
    1. 在__init__函数中载入MainWindow实例
    2. 在fill_input_boxes函数中填写相关数据到输入框中
    3. 在add_input_field函数中设置添加输入框的操作
    4. 在del_input_field函数中设置删除输入框的操作
  • TopologyManager类
    1. 在__init__函数中载入MainWindow实例
    2. 在generate_draw函数中通过调用draw_diagram文件的相关函数来完成将图片显示在相关位置上。

注:并没有draw_diagram类,只是一个py文件,里面写了一个draw_directed_graph函数,该函数通过调用networkx和matplotlib来完成图片的生成。

3.4.2 核心流程描述

定义一个课程结构体,声明二维向量,利用dfs函数递归进行深度优先搜索,生成所有可能的结果,判断是否存在循环依赖关系,用户可导出排序结果

3.4.3 核心算法设计

  1. dfs 函数接收两个参数:课程向量 courses 和拓扑排序的结果向量 result。
  2. 在函数开始定义了一个递归停止条件判断:如果当前拓扑排序结果的大小等于课程向量 courses 的大小,即 result 的大小等于 courses 的大小,说明已经生成了一个完整的拓扑排序结果。
  3. 遍历课程向量 courses。

(1)对于每一个课程,判断当前课程是否满足拓扑排序的条件,即入度为0且未被访问过;
(2)如果满足条件,将其添加到 result 中并将当前课程标记为已访问;
(3)对于每一个课程,判断当前课程是否满足拓扑排序的条件,即入度为0且未被访问过;
(4)如果满足条件,将其添加到 result 中并将当前课程标记为已访问;
(5)遍历当前课程的后继课程;将所有当前课程对应的后继课程入度减1;
(6)递归调用 dfs 函数处理下一个课程;
(7)回溯,将当前课程标记为未访问,回复后继课程的入度,从 result 中移除最后一门课程,得出其他分支结果;
(8)继续遍历下一个课程(更换拓扑排序的起始课程),重复上述步骤(在循环里);
(9)当所有的课程都被遍历完后,dfs 函数执行结束;将当前的拓扑排序结果 result 添加到 allTopologicalSorts 向量中。

3.5 运行结果截图

3.5.1 样例1

在这里插入图片描述

3.5.2 样例2

在这里插入图片描述

3.5.3 样例3

在这里插入图片描述

3.5.4 样例4

在这里插入图片描述

3.5.5 样例5

在这里插入图片描述

3.6. 测试

3.6.1 测试样例1

  • Filename:data_1.txt
  • Content
    <a,b>
    <b,c>
    <c,d>
    <d,f>
    <f,g>
  • Result
    sort ruselt_1:a b c d f g
  • photo
    在这里插入图片描述

3.6.2 测试样例2

  • Filename:data_2.txt
  • Content
    <CS 100,CS 200>
    <CS 200,CS 250>
    <CS 200,CS 300>
    <CS 250,CS 350>
  • Result
    sort ruselt_1:CS 100 CS 200 CS 250 CS 300 CS 350
    ---------------------
    sort ruselt_2:CS 100 CS 200 CS 250 CS 350 CS 300
    ---------------------
    sort ruselt_3:CS 100 CS 200 CS 300 CS 250 CS 350
  • photo
    在这里插入图片描述

3.6.3 测试样例3

  • Filename:data_3.txt
  • Content
    <CS 350,CS 250>
    <MA 140,MA 141>
    <MA 141,CS 150>
    <CS 250,CS 225>
    <MA 141,CS 225>
    <CS 225,CS 155>
    <CS 150,CS 155>
    <CS 155,CS 200>
    <CS 225,CS 230>
    <CS 225,CS 300>
    <CS 300,CS 301>
    <CS 300,CS 340>
    <CS 340,CS 345>
    <CS 340,CS 360>
    <CS 250,CS 360>
    <CS 360,CS 390>
  • Result
    sort ruselt_1:CS 350 CS 250 MA 140 MA 141 CS 150 CS 225 CS 155 CS 200 CS 230 CS 300 CS 301 CS 340 CS 345 CS 360 CS 390
    ---------------------
    sort ruselt_2:CS 350 CS 250 MA 140 MA 141 CS 150 CS 225 CS 155 CS 200 CS 230 CS 300 CS 301 CS 340 CS 360 CS 345 CS 390
    ……
    ……
    ……
    sort ruselt_113399:MA 140 MA 141 CS 150 CS 350 CS 250 CS 225 CS 300 CS 340 CS 360 CS 390 CS 345 CS 301 CS 155 CS 230 CS 200
    ---------------------
    sort ruselt_113400:MA 140 MA 141 CS 150 CS 350 CS 250 CS 225 CS 300 CS 340 CS 360 CS 390 CS 345 CS 301 CS 230 CS 155 CS 200

3.6.4 测试样例4

  • 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

  • Photo
    在这里插入图片描述

3.6.5 测试样例5

  • Filename:data_5.txt
  • Content
    <0,1>
    <1,2>
    <2,0>
  • Result
    存在循环依赖关系
  • Photo
    在这里插入图片描述

6. 系统特色以及可扩展点

6.1系统特色:

TopologicalSort_app 是一个Python项目,它基于PySide2库实现的图形用户界面(GUI)应用程序,用于创建、导入、导出拓扑排序图。下面将介绍它的一些系统特色:

  1. 图形用户界面(GUI):
    使用PySide2的QtDesigner创建了一个图形用户界面名为main.ui存放在statics文件夹下面,提供了文件导入、图形绘制、导出等功能接口。
  2. 拓扑排序图绘制:
    通过调用Draw_diagram.py里面的draw_directed_graph函数返回给主界面一个图片的临时地址,生成后若不保存则自动释放资源,不占用计算机内部关键资源。在python文件调用了networkx和matplotlib库实现了拓扑排序图的绘制功能。使用有向图表示节点和边的关系,并在GUI中名为photoLable的QLabel显示该图。
  3. 文件导入和导出:
    允许用户导入文本文件(.txt)以填充输入框并绘制拓扑排序图,同时也支持将绘制好的图像导出为图片(.png)或拓扑排序结果导出为文本文件。
  4. C++程序调用:
    由于C++运行速度比python快,故复杂的计算任务交给C++来完成,项目能够调用外部的C++程序(通过使用g++来编译.cpp文件,从而生成.exe文件)python运行该.exe并传入相关参数给.exe文件,最后将输出c++文件返回的结果显示在GUI中。此外,C++程序的输出可以导出为文本文件。
  5. 动态UI加载:
    使用PySide2的QUiLoader动态加载UI文件,使得UI可以通过简单的编辑UI文件而不需要修改代码。以此来到模块化编程,更加灵活多变。
  6. 用户操作反馈:
    使用QMessageBox提供信息、警告和错误提示,以向用户提供反馈。
  7. 临时文件处理:
    使用tempfile库创建临时文件以保存绘制的图像,以便导出功能可以使用这些临时文件。
  8. 异常处理:
    项目中实现了异常处理机制,能够捕获并显示错误信息,提高应用程序的健壮性。

6.2可扩展点:

  1. 将鼠标放置在按钮上会显示出相应信息
  2. 结果图的大小可以自行调节

4. 源代码部分

4.1 项目层级

TopologicalSort_app

├─.idea

├─build

├─core (核心算法)

├─data (读入的文件信息)

├─dist (发布软件的各个版本)

├─docs (所有文档信息)

├─lib

├─log

├─statics (静态资源)

├─test  (测试)

├─venv

└─__pycache__

4.2 运行环境

  • IDE:pycharm community v2023+

  • 解释器:python v3.6.8

  • 外部库: PySide2 v5.15.2.1、networkx v2.5.1、matplotlib v3.3.4

  • GUI工具:QtDesigner

4.3 项目核心代码

4.3.1 拓扑排序算法TopologicalSort.cpp

写在前面:这个c++程序在整个项目中是比较核心的一个部分,它利用c++运行速度更快来作为核心程序,让整个python项目调用,以达到核心业务和整个项目解耦的目的。这个.cpp文件不会直接调用,项目只会调用编译过后的TopologicalSort.exe,而这个文件会存放在statics文件夹下面。

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>

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] << " <filename>" << 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;
}

4.3.2 draw_diagram.py

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)}")

4.3.3 file_manager.py

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, "警告", "不支持的导出格式")


4.3.4 input_manager.py

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('<start,end>')
        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('<start,end>')

    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]

4.3.5 main_window.py

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_())

4.3.6 topology_manager.py

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))

4.4 GitHub仓库

有这些核心源代码可能远远不够,因为还有些不是代码的核心文件,如:使用QtDesigner设计的页面UI——main.ui文件,因此在下面我会放上这个项目的GitHub仓库地址,如果有需要可以自取哦~

https://github.com/hiddenSharp429/ToplogicalSort_app


5. 用户使用手册

5.1 运行软件APP

①打开TopologicalSort_app文件夹,双击dist文件夹进入所有发布程序选择页面
在这里插入图片描述

②选择需要运行的软件版本
在这里插入图片描述

各个版本的区别请看\ToplogicalSort_app\docs\ToplogicalSort_app更新日志.md

③进入某一个版本的文件夹后下拉找到main.exe文件
在这里插入图片描述

④双击后启动软件

5.2 操作app

①点击左上角菜单 ico
在这里插入图片描述

②选择导入选项卡
在这里插入图片描述
③选择需要导入的文件
在这里插入图片描述

一般测试样例都会放在\ToplogicalSort_app\data\里面
④导入后左边的输入框将会生成,文件中的文本将会被匹配填充,并且在下面的输入框中输出了所有拓扑排序的结果

在这里插入图片描述

测试样例的格式如下
在这里插入图片描述

⑤随后点击Generate按钮生成拓扑排序图片

在这里插入图片描述
在这里插入图片描述

⑥至此完成基本的功能,可以点击菜单选择导出选项卡
在这里插入图片描述

⑦可选择导出.txt类型文件还是.png文件,若为.txt文件则导出所有拓扑排序的结果,若为.png文件则导出拓扑图
在这里插入图片描述

运行结果:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


6. 结束语

如果有疑问欢迎大家留言讨论,你如果觉得这篇文章对你有帮助可以给我一个免费的赞吗?我们之间的交流是我最大的动力!