当前位置:首页>python>Python项目97:Tkinter+NetworkX图编辑器1.0

Python项目97:Tkinter+NetworkX图编辑器1.0

  • 2026-02-11 00:41:00
Python项目97:Tkinter+NetworkX图编辑器1.0

Python,速成心法

敲代码,查资料,问Ai

练习,探索,总结,优化

博文创作不易,我的博文不需要打赏,也不需要知识付费,可以白嫖学习编程小技巧。使用代码的过程中,如有疑问的地方,欢迎大家指正留言交流。喜欢的老铁可以多多点赞+收藏分享+置顶,小红牛在此表示感谢。

------★Python练手项目源码------

Python项目94:全球疫情模拟数据可视化大屏(dash+plotly+pandas)

Python项目91:绘制红楼梦人物关系图(NetworkX+Matplotlib)

Python项目89:NetworkX最短路径规划(城市交通)

Python项目88:文件备份与压缩系统2.0(tkinter+shutil+zipfile)

Python项目86:增强版画板2.0(tk.Canvas)

Python项目81:Excel数据统计工具3.0

Python项目81:Excel工作表批量重命名工具1.0(tkinter+openpyxl)

Python项目80:Excel数据统计工具2.0

Python项目78:学生成绩分析系统(Tkinter+SQLite3)

Python项目77:模拟炒股训练系统3.0(Mplfinance+tkinter)

Python项目76:员工排班表系统1.0(tkinter+sqlite3+tkcalendar)

Python项目74:多线程数据可视化工具2.0(tkinter+matplotlib+mplcursors)

Python项目73:自动化文件备份系统1.0(tkinter)

Python项目源码71:药品管理系统1.0(tkinter+sqlite3)

Python项目源码69:Excel数据筛选器1.0(tkinter+sqlite3+pandas)

Python项目源码63:病历管理系统1.0(tkinter+sqlite3+matplotlib)

Python源码62:酒店住房管理系统1.0(tkinter+sqlite3)

Python项目源码57:数据格式转换工具1.0(csv+json+excel+sqlite3)

Python项目源码56:食堂饭卡管理系统1.0(tkinter+splite3)

Python项目源码54:员工信息管理系统2.0(tkinter+sqlite3)

Python项目源码52:模拟银行卡系统1.0(账户管理、存款、取款、转账和交易记录查询)

Python项目源码51:五子棋对战2.0(Pygame)

Python项目源码50:理发店会员管理系统1.0(tkinter+sqlite3)

Python项目源码48:正则表达式调试工具3.0(tkinter+re+requests)

Python项目源码44:图书管理系统1.0(tkinter+sqlite3)

Python项目源码42:仓库商品管理系统1.0(tkinter+sqlite3+Excel)

Python项目源码40:字符串处理工具(tkinter+入门练习)

Python项目源码39:学生积分管理系统1.0(命令行界面+Json)

Python项目源码35:音乐播放器2.0(Tkinter+mutagen)

Python项目源码33:待办事项列表应用2.0(命令行界面+Json+类)

Python项目32:订单销售额管理系统1.0(Tkinter+CSV)

Python项目源码29:学生缴费管理系统(Tkinter+CSV)

Python项目28:设计日志管理系统2.0(Tkinter+Json)

Python项目26:设计学生成绩管理系统(简易版)

安装依赖:
pip install networkx matplotlib
优化版图编辑器代码,包含以下改进:
1. 左侧面板优化:增加滚动条:左侧面板现在有垂直滚动条,可以查看所有按钮,重新布局按钮:将按钮重新组织为紧凑的网格布局。按钮标签简化:将长标签简化为更短的标签(如"K-K"代表Kamada-Kawai布局),固定宽度:左侧面板宽度固定为300px,确保所有按钮都能完整显示
2. 所有功能完整:图类型切换:有向图/无向图切换功能,节点管理:添加、删除、修改节点,边管理:添加、删除边,批量设置方向(有向图),多种布局:9种布局方式,包括分层布局对话框,图操作:清空图、生成随机图、保存/加载图数据、显示图信息,信息显示:实时显示图信息,有滚动条支持。
3. 分层布局优化
完整的对话框界面:提供三种分层方式
手动分层:可以手动为每个节点选择层,自动分层:按节点度数自动分层,层属性管理:在节点中添加了层属性字段。
4. 保存和加载功能:完整保存:保存所有图数据,包括图类型、节点属性、边属性、位置和颜色JSON格式:使用JSON格式保存,便于查看和编辑。兼容性:支持加载不同版本的图数据文件
5. 有向图支持:箭头显示:有向边用箭头表示方向,方向管理:批量设置边方向功能,度信息:显示入度和出度。

↓ 完整源码如下 ↓

# -*- coding: utf-8 -*-# @Author : 小红牛# 微信公众号:wdPythonimport tkinter as tkfrom tkinter import ttk, messagebox, simpledialog, filedialogimport networkx as nximport matplotlib.pyplot as pltfrom matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tkfrom matplotlib.figure import Figureimport randomimport jsonimport osfrom collections import OrderedDictimport numpy as np# 设置中文字体,解决中文显示问题plt.rcParams['font.sans-serif'] = ['SimHei''Microsoft YaHei''DejaVu Sans']plt.rcParams['axes.unicode_minus'] = Falseclass GraphEditorApp:    def __init__(self, root):        self.root = root        self.root.title("NetworkX 图编辑器1.0 - 支持有向图")        self.root.geometry("1300x900")  # 增加高度        # 创建 NetworkX 图对象 - 现在支持有向图        self.G = nx.DiGraph()  # 默认为有向图        # 节点属性存储        self.node_positions = {}        self.node_colors = {}        self.default_node_color = "lightblue"        self.default_edge_color = "gray"        # 当前选中的节点        self.selected_node = None        # 图类型(有向/无向)        self.graph_type = "directed"        # 布局        self.setup_ui()        # 绑定事件        self.bind_events()        # 初始化示例图        self.create_sample_graph()    def setup_ui(self):        # 创建主框架        main_frame = ttk.Frame(self.root)        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)        # 创建带滚动条的左侧控制面板        left_container = ttk.Frame(main_frame)        left_container.pack(side=tk.LEFT, fill=tk.Y, padx=(05))        # 创建Canvas和滚动条        canvas = tk.Canvas(left_container, width=300, height=800)        scrollbar = ttk.Scrollbar(left_container, orient="vertical", command=canvas.yview)        scrollable_frame = ttk.Frame(canvas)        # 配置Canvas        canvas.configure(yscrollcommand=scrollbar.set)        canvas.bind('<Configure>'lambda e: canvas.configure(scrollregion=canvas.bbox("all")))        # 将可滚动框架放入Canvas        canvas_window = canvas.create_window((00), window=scrollable_frame, anchor="nw")        # 当可滚动框架大小变化时,更新滚动区域        scrollable_frame.bind("<Configure>"lambda e: canvas.configure(scrollregion=canvas.bbox("all")))        # 当Canvas大小变化时,调整窗口宽度        canvas.bind('<Configure>'lambda e: canvas.itemconfig(canvas_window, width=e.width))        # 布局Canvas和滚动条        canvas.pack(side="left", fill="both", expand=True)        scrollbar.pack(side="right", fill="y")        # 绑定鼠标滚轮事件        def _on_mousewheel(event):            canvas.yview_scroll(int(-1*(event.delta/120)), "units")        canvas.bind_all("<MouseWheel>", _on_mousewheel)        # 左侧控制面板内容        control_frame = scrollable_frame        # 图类型选择部分        graph_type_frame = ttk.LabelFrame(control_frame, text="图类型", padding="10")        graph_type_frame.pack(fill=tk.X, pady=(010))        self.graph_type_var = tk.StringVar(value="directed")        type_btn_frame = ttk.Frame(graph_type_frame)        type_btn_frame.pack(fill=tk.X)        ttk.Radiobutton(type_btn_frame, text="有向图", variable=self.graph_type_var,                        value="directed", command=self.change_graph_type).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Radiobutton(type_btn_frame, text="无向图", variable=self.graph_type_var,                        value="undirected", command=self.change_graph_type).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 节点管理部分        node_frame = ttk.LabelFrame(control_frame, text="节点管理", padding="10")        node_frame.pack(fill=tk.X, pady=(010))        # 使用网格布局使按钮更紧凑        node_btn_frame = ttk.Frame(node_frame)        node_btn_frame.pack(fill=tk.X)        ttk.Button(node_btn_frame, text="添加节点", command=self.add_node_dialog).grid(row=0, column=0, sticky=tk.EW, padx=2, pady=2)        ttk.Button(node_btn_frame, text="删除节点", command=self.delete_node_dialog).grid(row=0, column=1, sticky=tk.EW, padx=2, pady=2)        ttk.Button(node_btn_frame, text="修改节点", command=self.edit_node_dialog).grid(row=0, column=2, sticky=tk.EW, padx=2, pady=2)        # 配置网格权重        node_btn_frame.columnconfigure(0, weight=1)        node_btn_frame.columnconfigure(1, weight=1)        node_btn_frame.columnconfigure(2, weight=1)        # 边管理部分        edge_frame = ttk.LabelFrame(control_frame, text="边管理", padding="10")        edge_frame.pack(fill=tk.X, pady=(010))        edge_btn_frame = ttk.Frame(edge_frame)        edge_btn_frame.pack(fill=tk.X)        ttk.Button(edge_btn_frame, text="添加边", command=self.add_edge_dialog).grid(row=0, column=0, sticky=tk.EW, padx=2, pady=2)        ttk.Button(edge_btn_frame, text="删除边", command=self.delete_edge_dialog).grid(row=0, column=1, sticky=tk.EW, padx=2, pady=2)        ttk.Button(edge_btn_frame, text="批量设置方向", command=self.batch_set_edge_direction).grid(row=0, column=2, sticky=tk.EW, padx=2, pady=2)        # 配置网格权重        edge_btn_frame.columnconfigure(0, weight=1)        edge_btn_frame.columnconfigure(1, weight=1)        edge_btn_frame.columnconfigure(2, weight=1)        # 布局管理部分        layout_frame = ttk.LabelFrame(control_frame, text="布局方式", padding="10")        layout_frame.pack(fill=tk.X, pady=(010))        # 第一行布局按钮        row1_frame = ttk.Frame(layout_frame)        row1_frame.pack(fill=tk.X, pady=(05))        ttk.Button(row1_frame, text="随机", command=self.random_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row1_frame, text="环形", command=self.circular_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row1_frame, text="弹簧", command=self.spring_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 第二行布局按钮        row2_frame = ttk.Frame(layout_frame)        row2_frame.pack(fill=tk.X, pady=(05))        ttk.Button(row2_frame, text="谱布局", command=self.spectral_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row2_frame, text="Shell", command=self.shell_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row2_frame, text="分层", command=self.multipartite_layout_dialog, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 第三行布局按钮        row3_frame = ttk.Frame(layout_frame)        row3_frame.pack(fill=tk.X, pady=(05))        ttk.Button(row3_frame, text="K-K", command=self.kamada_kawai_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row3_frame, text="螺旋", command=self.spiral_layout, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(row3_frame, text="更多", command=self.more_layouts_dialog, width=8).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 图操作部分 - 重新组织为两列        graph_frame = ttk.LabelFrame(control_frame, text="图操作", padding="10")        graph_frame.pack(fill=tk.X, pady=(010))        # 第一行图操作按钮        graph_row1 = ttk.Frame(graph_frame)        graph_row1.pack(fill=tk.X, pady=(05))        ttk.Button(graph_row1, text="清空图", command=self.clear_graph).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(graph_row1, text="随机图", command=self.generate_random_graph).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 第二行图操作按钮        graph_row2 = ttk.Frame(graph_frame)        graph_row2.pack(fill=tk.X, pady=(05))        ttk.Button(graph_row2, text="保存图数据", command=self.save_graph_data).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        ttk.Button(graph_row2, text="加载图数据", command=self.load_graph_data).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 第三行图操作按钮        graph_row3 = ttk.Frame(graph_frame)        graph_row3.pack(fill=tk.X)        ttk.Button(graph_row3, text="显示图信息", command=self.show_graph_info).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)        # 状态栏        self.status_var = tk.StringVar()        self.status_var.set("就绪 | 有向图 | 节点数: 0, 边数: 0")        status_bar = ttk.Label(graph_frame, textvariable=self.status_var, relief=tk.SUNKEN)        status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5)        # 信息显示部分        info_frame = ttk.LabelFrame(control_frame, text="图信息", padding="10")        info_frame.pack(fill=tk.X, pady=(010))        # 使用Text小部件显示信息,并添加滚动条        info_container = ttk.Frame(info_frame)        info_container.pack(fill=tk.BOTH, expand=True)        self.info_text = tk.Text(info_container, height=12, width=25)        info_scrollbar = ttk.Scrollbar(info_container, orient="vertical", command=self.info_text.yview)        self.info_text.configure(yscrollcommand=info_scrollbar.set)        self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)        info_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)        # 右侧图显示区域        graph_display_frame = ttk.LabelFrame(main_frame, text="图可视化", padding="5")        graph_display_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)        # 创建 Matplotlib 图形        self.fig = Figure(figsize=(102), dpi=100)        self.ax = self.fig.add_subplot(111)        self.ax.set_title("网络图可视化 (有向图)", fontsize=14)        self.ax.set_xticks([])        self.ax.set_yticks([])        # 将 Matplotlib 图形嵌入到 Tkinter 中        self.canvas = FigureCanvasTkAgg(self.fig, master=graph_display_frame)        self.canvas.draw()        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)        # 添加 Matplotlib 工具栏        toolbar = NavigationToolbar2Tk(self.canvas, graph_display_frame)        toolbar.update()        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)    def bind_events(self):        # 绑定画布点击事件        self.canvas.mpl_connect('button_press_event'self.on_click)    def change_graph_type(self):        """改变图类型(有向/无向)"""        new_type = self.graph_type_var.get()        if new_type == self.graph_type:            return        if len(self.G.nodes()) > 0:            if not messagebox.askyesno("确认切换"                                      f"切换图类型将重新创建图结构,当前图中的边方向信息可能会丢失。确定要切换到{'有向图'if new_type == 'directed'else'无向图'}吗?"):                self.graph_type_var.set(self.graph_type)  # 重置选择                return        # 保存当前节点和边信息        nodes = list(self.G.nodes())        edges = list(self.G.edges())        node_attrs = {node: dict(self.G.nodes[node]) for node in nodes}        edge_attrs = {edge: dict(self.G.edges[edge]) for edge in edges}        # 创建新图        if new_type == "directed":            self.G = nx.DiGraph()        else:            self.G = nx.Graph()        # 恢复节点        for node in nodes:            self.G.add_node(node, **node_attrs[node])        # 恢复边        for edge in edges:            self.G.add_edge(edge[0], edge[1], **edge_attrs[edge])        self.graph_type = new_type        self.selected_node = None        # 更新界面        self.ax.set_title(f"网络图可视化 ({'有向图'if new_type == 'directed'else'无向图'})", fontsize=14)        self.update_status()        self.update_graph_display()    def create_sample_graph(self):        """创建示例图"""        # 添加一些节点        nodes = ['A''B''C''D''E''F']        for node in nodes:            self.G.add_node(node, label=node, weight=random.randint(110), layer=random.randint(02))            self.node_colors[node] = self.default_node_color        # 添加一些边(有向边)        edges = [('A''B'), ('B''C'), ('C''D'), ('D''E'), ('E''F'),                  ('F''A'), ('A''C'), ('B''D'), ('C''E'), ('D''F')]        for edge in edges:            self.G.add_edge(edge[0], edge[1], weight=random.randint(15),                            color=random.choice(["gray""blue""red""green"]))        # 使用弹簧布局        self.spring_layout()        self.update_graph_display()    def update_graph_display(self):        """更新图的可视化显示"""        self.ax.clear()        if len(self.G.nodes()) == 0:            self.ax.text(0.50.5"图为空\n点击控制面板添加节点"                        ha='center', va='center', fontsize=12, transform=self.ax.transAxes)            self.ax.set_title(f"网络图可视化 ({'有向图'if self.graph_type == 'directed'else'无向图'})", fontsize=14)            self.canvas.draw()            self.update_status()            return        # 获取节点位置,如果没有则使用默认位置        if not self.node_positions:            self.spring_layout()        # 绘制边        for edge in self.G.edges():            x1, y1 = self.node_positions[edge[0]]            x2, y2 = self.node_positions[edge[1]]            # 确定边颜色            edge_color = self.G[edge[0]][edge[1]].get('color'self.default_edge_color)            # 绘制边            if self.graph_type == "directed":                # 有向边:使用箭头                dx, dy = x2 - x1, y2 - y1                # 缩短箭头长度,避免覆盖节点                shorten = 0.1                x2_adj = x1 + (1 - shorten) * dx                y2_adj = y1 + (1 - shorten) * dy                # 绘制箭头                self.ax.annotate("", xy=(x2_adj, y2_adj), xytext=(x1, y1),                               arrowprops=dict(arrowstyle="->", color=edge_color,                                               lw=2, alpha=0.8, shrinkA=10, shrinkB=10))                # 绘制边线                self.ax.plot([x1, x2], [y1, y2], color=edge_color, linewidth=1, zorder=1, alpha=0.5)            else:                # 无向边:只绘制线                self.ax.plot([x1, x2], [y1, y2], color=edge_color, linewidth=2, zorder=1)            # 显示边的权重(如果有)            if 'weight' in self.G[edge[0]][edge[1]]:                weight = self.G[edge[0]][edge[1]]['weight']                mid_x = (x1 + x2) / 2                mid_y = (y1 + y2) / 2                self.ax.text(mid_x, mid_y, str(weight),                            fontsize=9, ha='center', va='center',                           bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))        # 绘制节点        for node in self.G.nodes():            x, y = self.node_positions[node]            # 确定节点颜色            color = self.node_colors.get(node, self.default_node_color)            if node == self.selected_node:                color = "red"            # 绘制节点            self.ax.scatter(x, y, s=500, c=color, edgecolors='black', linewidth=2, zorder=2)            # 显示节点标签和权重            label = node            weight = self.G.nodes[node].get('weight''')            if weight:                label = f"{node}\n({weight})"            # 如果有层属性,显示层信息            layer = self.G.nodes[node].get('layer''')            if layer is not None and layer != '':                label = f"{node}\nL:{layer}"                if weight:                    label = f"{node}\nL:{layer} W:{weight}"            self.ax.text(x, y, label, ha='center', va='center'                       fontsize=10, fontweight='bold', zorder=3)        self.ax.set_title(f"网络图可视化 ({'有向图'if self.graph_type == 'directed'else'无向图'})", fontsize=14)        self.ax.set_xticks([])        self.ax.set_yticks([])        # 动态调整坐标轴范围        self.adjust_axes()        self.canvas.draw()        self.update_status()        self.update_info_text()    def adjust_axes(self):        """根据节点位置动态调整坐标轴范围"""        if not self.node_positions:            return        xs = [pos[0for pos in self.node_positions.values()]        ys = [pos[1for pos in self.node_positions.values()]        if xs and ys:            x_min, x_max = min(xs), max(xs)            y_min, y_max = min(ys), max(ys)            # 添加一些边距            x_margin = max(0.2, (x_max - x_min) * 0.2)            y_margin = max(0.2, (y_max - y_min) * 0.2)            self.ax.set_xlim(x_min - x_margin, x_max + x_margin)            self.ax.set_ylim(y_min - y_margin, y_max + y_margin)    def update_status(self):        """更新状态栏"""        node_count = len(self.G.nodes())        edge_count = len(self.G.edges())        graph_type_text = "有向图" if self.graph_type == "directed" else "无向图"        selected_text = f" | 选中: {self.selected_node}" if self.selected_node else ""        self.status_var.set(f"就绪 | {graph_type_text} | 节点数: {node_count}, 边数: {edge_count}{selected_text}")    def update_info_text(self):        """更新信息文本框"""        self.info_text.delete(1.0, tk.END)        # 基本信息        graph_type_text = "有向图" if self.graph_type == "directed" else "无向图"        self.info_text.insert(tk.END, f"图类型: {graph_type_text}\n")        self.info_text.insert(tk.END, f"节点数: {len(self.G.nodes())}\n")        self.info_text.insert(tk.END, f"边数: {len(self.G.edges())}\n")        # 节点列表        self.info_text.insert(tk.END, "\n节点列表:\n")        for node in sorted(self.G.nodes()):            if self.graph_type == "directed":                in_degree = self.G.in_degree(node)                out_degree = self.G.out_degree(node)                degree_info = f"入度={in_degree}, 出度={out_degree}"            else:                degree = self.G.degree(node)                degree_info = f"度={degree}"            weight = self.G.nodes[node].get('weight''N/A')            layer = self.G.nodes[node].get('layer''N/A')            self.info_text.insert(tk.END, f"  {node}{degree_info}, 权重={weight}, 层={layer}\n")        # 边列表        self.info_text.insert(tk.END, "\n边列表:\n")        for edge in sorted(self.G.edges()):            weight = self.G[edge[0]][edge[1]].get('weight''N/A')            color = self.G[edge[0]][edge[1]].get('color''gray')            direction = f"{edge[0]} → {edge[1]}" if self.graph_type == "directed" else f"{edge[0]} - {edge[1]}"            self.info_text.insert(tk.END, f"  {direction}: 权重={weight}, 颜色={color}\n")    def on_click(self, event):        """处理图上的点击事件"""        if event.xdata is None or event.ydata is None:            return        # 检查是否点击了节点        clicked_node = None        min_distance = float('inf')        for node, (x, y) in self.node_positions.items():            distance = ((x - event.xdata)**2 + (y - event.ydata)**2)**0.5            # 考虑到节点的大小            if distance < 0.1 and distance < min_distance:                min_distance = distance                clicked_node = node        # 设置选中的节点        if clicked_node:            if self.selected_node == clicked_node:                # 双击取消选中                self.selected_node = None            else:                self.selected_node = clicked_node        else:            self.selected_node = None        self.update_graph_display()    def add_node_dialog(self):        """添加节点对话框"""        dialog = tk.Toplevel(self.root)        dialog.title("添加节点")        dialog.geometry("300x350")  # 增加高度以容纳层属性        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="节点名称:").grid(row=0, column=0, sticky=tk.W, pady=(05))        name_entry = ttk.Entry(content_frame, width=25)        name_entry.grid(row=0, column=1, pady=(05))        name_entry.focus()        ttk.Label(content_frame, text="节点权重:").grid(row=1, column=0, sticky=tk.W, pady=(05))        weight_entry = ttk.Entry(content_frame, width=25)        weight_entry.grid(row=1, column=1, pady=(05))        weight_entry.insert(0"1")        ttk.Label(content_frame, text="节点层(分层布局用):").grid(row=2, column=0, sticky=tk.W, pady=(05))        layer_entry = ttk.Entry(content_frame, width=25)        layer_entry.grid(row=2, column=1, pady=(05))        layer_entry.insert(0"0")        ttk.Label(content_frame, text="节点颜色:").grid(row=3, column=0, sticky=tk.W, pady=(05))        color_var = tk.StringVar(value="lightblue")        color_combo = ttk.Combobox(content_frame, textvariable=color_var,                                    values=["lightblue""lightgreen""lightcoral"                                           "lightyellow""lightpink""lightgray"],                                    state="readonly", width=23)        color_combo.grid(row=3, column=1, pady=(05))        button_frame = ttk.Frame(content_frame)        button_frame.grid(row=4, column=0, columnspan=2, pady=(200))        def add_node():            name = name_entry.get().strip()            weight_str = weight_entry.get().strip()            layer_str = layer_entry.get().strip()            color = color_var.get()            if not name:                messagebox.showerror("错误""节点名称不能为空")                return            if name in self.G.nodes():                messagebox.showerror("错误"f"节点 '{name}' 已存在")                return            try:                weight = int(weight_str) if weight_str else 1            except ValueError:                messagebox.showerror("错误""权重必须是整数")                return            try:                layer = int(layer_str) if layer_str else 0            except ValueError:                messagebox.showerror("错误""层必须是整数")                return            # 添加节点            self.G.add_node(name, weight=weight, layer=layer)            self.node_colors[name] = color            # 为新节点设置随机位置            if len(self.node_positions) == 0:                self.node_positions[name] = (00)            else:                # 在现有节点附近添加新节点                existing_node = random.choice(list(self.node_positions.keys()))                x, y = self.node_positions[existing_node]                self.node_positions[name] = (                    x + random.uniform(-0.30.3),                    y + random.uniform(-0.30.3)                )            self.update_graph_display()            dialog.destroy()        ttk.Button(button_frame, text="添加", command=add_node).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def delete_node_dialog(self):        """删除节点对话框"""        if len(self.G.nodes()) == 0:            messagebox.showinfo("提示""图中没有节点可以删除")            return        if self.selected_node:            # 如果已选中节点,直接删除            self.delete_node(self.selected_node)            return        dialog = tk.Toplevel(self.root)        dialog.title("删除节点")        dialog.geometry("300x300")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="选择要删除的节点:").pack(pady=(2010))        node_var = tk.StringVar()        node_combo = ttk.Combobox(content_frame, textvariable=node_var,                                  values=list(self.G.nodes()), state="readonly", width=25)        node_combo.pack(pady=10)        node_combo.current(0)        # 显示节点信息        info_label = ttk.Label(content_frame, text="", wraplength=250)        info_label.pack(pady=10)        def update_info(event):            node = node_var.get()            if node:                if self.graph_type == "directed":                    in_degree = self.G.in_degree(node)                    out_degree = self.G.out_degree(node)                    degree_info = f"入度={in_degree}, 出度={out_degree}"                else:                    degree = self.G.degree(node)                    degree_info = f"度={degree}"                weight = self.G.nodes[node].get('weight''N/A')                layer = self.G.nodes[node].get('layer''N/A')                info_label.config(text=f"节点: {node}\n{degree_info}\n权重: {weight}\n层: {layer}")        node_combo.bind("<<ComboboxSelected>>", update_info)        update_info(None)  # 初始更新        button_frame = ttk.Frame(content_frame)        button_frame.pack(pady=(200))        def delete_node():            node = node_var.get()            if node:                self.delete_node(node)                dialog.destroy()        ttk.Button(button_frame, text="删除", command=delete_node).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def delete_node(self, node):        """删除指定节点"""        if node not in self.G.nodes():            messagebox.showerror("错误"f"节点 '{node}' 不存在")            return        # 确认删除        if not messagebox.askyesno("确认删除"f"确定要删除节点 '{node}' 及其所有连接边吗?"):            return        # 删除节点        self.G.remove_node(node)        # 从位置和颜色字典中移除        if node in self.node_positions:            del self.node_positions[node]        if node in self.node_colors:            del self.node_colors[node]        # 清除选中状态        if self.selected_node == node:            self.selected_node = None        self.update_graph_display()    def edit_node_dialog(self):        """修改节点对话框"""        if len(self.G.nodes()) == 0:            messagebox.showinfo("提示""图中没有节点可以修改")            return        if self.selected_node:            # 如果已选中节点,直接编辑            node = self.selected_node        else:            # 否则弹出对话框选择节点            dialog = tk.Toplevel(self.root)            dialog.title("选择节点")            dialog.geometry("300x200")            dialog.transient(self.root)            dialog.grab_set()            ttk.Label(dialog, text="选择要修改的节点:", padding="20").pack()            node_var = tk.StringVar()            node_combo = ttk.Combobox(dialog, textvariable=node_var,                                      values=list(self.G.nodes()), state="readonly", width=20)            node_combo.pack(pady=10)            node_combo.current(0)            def select_node():                nonlocal node                node = node_var.get()                dialog.destroy()                if node:                    self.show_edit_dialog(node)            ttk.Button(dialog, text="选择", command=select_node).pack(pady=10)            dialog.wait_window()            return        # 直接显示编辑对话框        self.show_edit_dialog(node)    def show_edit_dialog(self, node):        """显示编辑节点对话框"""        dialog = tk.Toplevel(self.root)        dialog.title("修改节点")        dialog.geometry("300x350")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text=f"修改节点: {node}").grid(row=0, column=0, columnspan=2, pady=(020))        ttk.Label(content_frame, text="新节点名称:").grid(row=1, column=0, sticky=tk.W, pady=(05))        name_entry = ttk.Entry(content_frame, width=25)        name_entry.grid(row=1, column=1, pady=(05))        name_entry.insert(0, node)        current_weight = self.G.nodes[node].get('weight'1)        ttk.Label(content_frame, text="节点权重:").grid(row=2, column=0, sticky=tk.W, pady=(05))        weight_entry = ttk.Entry(content_frame, width=25)        weight_entry.grid(row=2, column=1, pady=(05))        weight_entry.insert(0str(current_weight))        current_layer = self.G.nodes[node].get('layer'0)        ttk.Label(content_frame, text="节点层:").grid(row=3, column=0, sticky=tk.W, pady=(05))        layer_entry = ttk.Entry(content_frame, width=25)        layer_entry.grid(row=3, column=1, pady=(05))        layer_entry.insert(0str(current_layer))        current_color = self.node_colors.get(node, self.default_node_color)        ttk.Label(content_frame, text="节点颜色:").grid(row=4, column=0, sticky=tk.W, pady=(05))        color_var = tk.StringVar(value=current_color)        color_combo = ttk.Combobox(content_frame, textvariable=color_var,                                    values=["lightblue""lightgreen""lightcoral"                                           "lightyellow""lightpink""lightgray""red"],                                    state="readonly", width=23)        color_combo.grid(row=4, column=1, pady=(05))        button_frame = ttk.Frame(content_frame)        button_frame.grid(row=5, column=0, columnspan=2, pady=(200))        def edit_node():            new_name = name_entry.get().strip()            weight_str = weight_entry.get().strip()            layer_str = layer_entry.get().strip()            color = color_var.get()            if not new_name:                messagebox.showerror("错误""节点名称不能为空")                return            try:                weight = int(weight_str) if weight_str else 1            except ValueError:                messagebox.showerror("错误""权重必须是整数")                return            try:                layer = int(layer_str) if layer_str else 0            except ValueError:                messagebox.showerror("错误""层必须是整数")                return            # 如果节点名称改变            if new_name != node:                if new_name in self.G.nodes():                    messagebox.showerror("错误"f"节点 '{new_name}' 已存在")                    return                # 重命名节点                self.G = nx.relabel_nodes(self.G, {node: new_name})                # 更新位置和颜色字典                if node in self.node_positions:                    self.node_positions[new_name] = self.node_positions[node]                    del self.node_positions[node]                if node in self.node_colors:                    self.node_colors[new_name] = self.node_colors[node]                    del self.node_colors[node]                # 更新选中状态                if self.selected_node == node:                    self.selected_node = new_name            else:                # 只更新权重和层                self.G.nodes[node]['weight'] = weight                self.G.nodes[node]['layer'] = layer            # 更新颜色            self.node_colors[new_name if new_name != node else node] = color            self.update_graph_display()            dialog.destroy()        ttk.Button(button_frame, text="保存", command=edit_node).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def add_edge_dialog(self):        """添加边对话框"""        if len(self.G.nodes()) < 2:            messagebox.showinfo("提示""至少需要两个节点才能添加边")            return        dialog = tk.Toplevel(self.root)        dialog.title("添加边")        dialog.geometry("300x400")  # 增加高度以容纳方向选择        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="起点节点:").grid(row=0, column=0, sticky=tk.W, pady=(05))        node1_var = tk.StringVar()        node1_combo = ttk.Combobox(content_frame, textvariable=node1_var,                                   values=list(self.G.nodes()), state="readonly", width=25)        node1_combo.grid(row=0, column=1, pady=(05))        node1_combo.current(0)        ttk.Label(content_frame, text="终点节点:").grid(row=1, column=0, sticky=tk.W, pady=(05))        node2_var = tk.StringVar()        node2_combo = ttk.Combobox(content_frame, textvariable=node2_var,                                   values=list(self.G.nodes()), state="readonly", width=25)        node2_combo.grid(row=1, column=1, pady=(05))        node2_combo.current(1 if len(self.G.nodes()) > 1 else 0)        ttk.Label(content_frame, text="边权重:").grid(row=2, column=0, sticky=tk.W, pady=(05))        weight_entry = ttk.Entry(content_frame, width=25)        weight_entry.grid(row=2, column=1, pady=(05))        weight_entry.insert(0"1")        ttk.Label(content_frame, text="边颜色:").grid(row=3, column=0, sticky=tk.W, pady=(05))        color_var = tk.StringVar(value="gray")        color_combo = ttk.Combobox(content_frame, textvariable=color_var,                                    values=["gray""black""red""blue""green""orange"],                                    state="readonly", width=23)        color_combo.grid(row=3, column=1, pady=(05))        # 如果有向图,显示方向选项        if self.graph_type == "directed":            ttk.Label(content_frame, text="边方向:").grid(row=4, column=0, sticky=tk.W, pady=(05))            direction_var = tk.StringVar(value="forward")            direction_frame = ttk.Frame(content_frame)            direction_frame.grid(row=4, column=1, pady=(05), sticky=tk.W)            ttk.Radiobutton(direction_frame, text="正向", variable=direction_var,                            value="forward").pack(side=tk.LEFT)            ttk.Radiobutton(direction_frame, text="反向", variable=direction_var,                            value="reverse").pack(side=tk.LEFT, padx=(100))        else:            direction_var = None        button_frame = ttk.Frame(content_frame)        button_frame.grid(row=5, column=0, columnspan=2, pady=(200))        def add_edge():            node1 = node1_var.get()            node2 = node2_var.get()            weight_str = weight_entry.get().strip()            color = color_var.get()            if node1 == node2:                messagebox.showerror("错误""起点和终点不能相同")                return            try:                weight = int(weight_str) if weight_str else 1            except ValueError:                messagebox.showerror("错误""权重必须是整数")                return            # 检查边是否已存在            if self.G.has_edge(node1, node2):                messagebox.showerror("错误"f"边 '{node1} -> {node2}' 已存在")                return            # 如果是无向图,检查反向边是否存在            if self.graph_type == "undirected" and self.G.has_edge(node2, node1):                messagebox.showerror("错误"f"边 '{node2} - {node1}' 已存在")                return            # 确定边的方向            if direction_var and direction_var.get() == "reverse":                # 交换起点和终点                node1, node2 = node2, node1            # 添加边            self.G.add_edge(node1, node2, weight=weight, color=color)            self.update_graph_display()            dialog.destroy()        ttk.Button(button_frame, text="添加", command=add_edge).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def delete_edge_dialog(self):        """删除边对话框"""        if len(self.G.edges()) == 0:            messagebox.showinfo("提示""图中没有边可以删除")            return        dialog = tk.Toplevel(self.root)        dialog.title("删除边")        dialog.geometry("300x300")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="选择要删除的边:").pack(pady=(2010))        # 创建边列表        if self.graph_type == "directed":            edge_list = [f"{u} → {v}" for u, v in self.G.edges()]        else:            edge_list = [f"{u} - {v}" for u, v in self.G.edges()]        edge_var = tk.StringVar()        edge_combo = ttk.Combobox(content_frame, textvariable=edge_var,                                  values=edge_list, state="readonly", width=25)        edge_combo.pack(pady=10)        edge_combo.current(0)        # 显示边信息        info_label = ttk.Label(content_frame, text="", wraplength=250)        info_label.pack(pady=10)        def update_info(event):            edge_str = edge_var.get()            if edge_str:                if self.graph_type == "directed":                    node1, node2 = edge_str.split(" → ")                else:                    node1, node2 = edge_str.split(" - ")                weight = self.G[node1][node2].get('weight''N/A')                color = self.G[node1][node2].get('color''gray')                info_label.config(text=f"边: {node1} -> {node2}\n权重: {weight}\n颜色: {color}")        edge_combo.bind("<<ComboboxSelected>>", update_info)        update_info(None)  # 初始更新        button_frame = ttk.Frame(content_frame)        button_frame.pack(pady=(200))        def delete_edge():            edge_str = edge_var.get()            if not edge_str:                return            if self.graph_type == "directed":                node1, node2 = edge_str.split(" → ")            else:                node1, node2 = edge_str.split(" - ")            # 检查边是否存在            if not self.G.has_edge(node1, node2):                messagebox.showerror("错误"f"边 '{edge_str}' 不存在")                return            # 删除边            self.G.remove_edge(node1, node2)            self.update_graph_display()            dialog.destroy()        ttk.Button(button_frame, text="删除", command=delete_edge).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def batch_set_edge_direction(self):        """批量设置边方向(用于有向图)"""        if self.graph_type != "directed":            messagebox.showinfo("提示""此功能仅适用于有向图")            return        if len(self.G.edges()) == 0:            messagebox.showinfo("提示""图中没有边可以设置方向")            return        dialog = tk.Toplevel(self.root)        dialog.title("批量设置边方向")        dialog.geometry("400x500")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="选择边并设置新方向:", font=("Arial"12"bold")).pack(pady=(010))        # 边列表        edges_listbox = tk.Listbox(content_frame, selectmode=tk.MULTIPLE, height=10)        edges_listbox.pack(fill=tk.BOTH, expand=True, pady=(010))        for edge in self.G.edges():            edges_listbox.insert(tk.END, f"{edge[0]} → {edge[1]}")        # 方向选择        ttk.Label(content_frame, text="新方向:").pack(anchor=tk.W, pady=(05))        direction_var = tk.StringVar(value="keep")        direction_frame = ttk.Frame(content_frame)        direction_frame.pack(fill=tk.X, pady=(010))        ttk.Radiobutton(direction_frame, text="保持原方向", variable=direction_var,                        value="keep").pack(side=tk.LEFT, padx=5)        ttk.Radiobutton(direction_frame, text="反转方向", variable=direction_var,                        value="reverse").pack(side=tk.LEFT, padx=5)        ttk.Radiobutton(direction_frame, text="设为双向", variable=direction_var,                        value="both").pack(side=tk.LEFT, padx=5)        def apply_directions():            selected_indices = edges_listbox.curselection()            if not selected_indices:                messagebox.showwarning("警告""请选择至少一条边")                return            direction = direction_var.get()            edges_to_modify = []            # 收集要修改的边            for idx in selected_indices:                edge_str = edges_listbox.get(idx)                node1, node2 = edge_str.split(" → ")                edges_to_modify.append((node1, node2))            # 应用修改            for node1, node2 in edges_to_modify:                edge_attrs = dict(self.G[node1][node2])                # 删除原边                self.G.remove_edge(node1, node2)                if direction == "reverse":                    # 反转方向                    self.G.add_edge(node2, node1, **edge_attrs)                elif direction == "both":                    # 添加双向边                    self.G.add_edge(node1, node2, **edge_attrs)                    self.G.add_edge(node2, node1, **edge_attrs)                else:                    # 保持原方向(重新添加,确保边存在)                    self.G.add_edge(node1, node2, **edge_attrs)            messagebox.showinfo("成功"f"已修改 {len(edges_to_modify)} 条边的方向")            self.update_graph_display()            dialog.destroy()        button_frame = ttk.Frame(content_frame)        button_frame.pack(fill=tk.X, pady=(100))        ttk.Button(button_frame, text="应用", command=apply_directions).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    # ===========================    # 布局方法    # ===========================    def random_layout(self):        """随机布局 - 节点随机分布"""        for node in self.G.nodes():            self.node_positions[node] = (                random.uniform(-11),                random.uniform(-11)            )        self.update_graph_display()    def circular_layout(self):        """环形布局 - 节点均匀分布在圆环上"""        try:            pos = nx.circular_layout(self.G)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"环形布局失败: {str(e)}")    def spring_layout(self):        """弹簧布局 - 基于力导向算法"""        try:            pos = nx.spring_layout(self.G, seed=42, k=0.8)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"弹簧布局失败: {str(e)}")    def spectral_layout(self):        """谱布局 - 基于图的拉普拉斯矩阵特征向量"""        try:            pos = nx.spectral_layout(self.G)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"谱布局失败: {str(e)}")    def shell_layout(self):        """Shell布局 - 节点分布在多个同心圆上"""        try:            # 根据节点度将节点分成几个shell            nodes = list(self.G.nodes())            if len(nodes) == 0:                return            # 按节点度排序(有向图使用总度数,无向图使用度)            if self.graph_type == "directed":                nodes_sorted = sorted(nodes, key=lambda n: self.G.in_degree(n) + self.G.out_degree(n))            else:                nodes_sorted = sorted(nodes, key=lambda n: self.G.degree(n))            # 将节点分成2-3个shell            num_shells = min(3len(nodes))            shell_list = []            for i in range(num_shells):                start_idx = i * len(nodes) // num_shells                end_idx = (i + 1) * len(nodes) // num_shells                shell_list.append(nodes_sorted[start_idx:end_idx])            pos = nx.shell_layout(self.G, nlist=shell_list)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"Shell布局失败: {str(e)}")    def multipartite_layout_dialog(self):        """分层布局对话框"""        if len(self.G.nodes()) == 0:            messagebox.showinfo("提示""图中没有节点")            return        dialog = tk.Toplevel(self.root)        dialog.title("分层布局设置")        dialog.geometry("400x500")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="分层布局设置", font=("Arial"12"bold")).pack(pady=(020))        # 分层方式选择        ttk.Label(content_frame, text="分层方式:").pack(anchor=tk.W, pady=(05))        method_var = tk.StringVar(value="layer_attr")        method_frame = ttk.Frame(content_frame)        method_frame.pack(fill=tk.X, pady=(015))        ttk.Radiobutton(method_frame, text="使用节点的'layer'属性"                       variable=method_var, value="layer_attr").pack(anchor=tk.W)        ttk.Radiobutton(method_frame, text="按节点度数分层"                       variable=method_var, value="by_degree").pack(anchor=tk.W)        ttk.Radiobutton(method_frame, text="手动设置分层"                       variable=method_var, value="manual").pack(anchor=tk.W)        # 手动设置分层的层数输入        ttk.Label(content_frame, text="层数 (仅手动模式):").pack(anchor=tk.W, pady=(05))        layer_count_var = tk.StringVar(value="3")        layer_count_entry = ttk.Entry(content_frame, textvariable=layer_count_var, width=10)        layer_count_entry.pack(anchor=tk.W, pady=(015))        # 显示当前节点的层属性        layer_info_frame = ttk.LabelFrame(content_frame, text="节点层属性", padding="10")        layer_info_frame.pack(fill=tk.BOTH, expand=True, pady=(015))        layer_info_text = tk.Text(layer_info_frame, height=6, width=30)        layer_info_text.pack(fill=tk.BOTH, expand=True)        # 填充层信息        for node in self.G.nodes():            layer = self.G.nodes[node].get('layer''未设置')            if self.graph_type == "directed":                in_degree = self.G.in_degree(node)                out_degree = self.G.out_degree(node)                degree_info = f"入度={in_degree}, 出度={out_degree}"            else:                degree = self.G.degree(node)                degree_info = f"度={degree}"            layer_info_text.insert(tk.END, f"{node}: 层={layer}{degree_info}\n")        layer_info_text.config(state=tk.DISABLED)        def apply_layout():            method = method_var.get()            try:                if method == "layer_attr":                    # 使用节点的layer属性                    layers = {}                    for node in self.G.nodes():                        layer = self.G.nodes[node].get('layer'0)                        layers[node] = layer                    # 检查是否所有节点都有layer属性                    missing_layers = [node for node in self.G.nodes() if 'layer' not in self.G.nodes[node]]                    if missing_layers:                        if not messagebox.askyesno("警告"                                                  f"以下节点没有layer属性: {', '.join(missing_layers[:5])}{'...'iflen(missing_layers) > 5else''}\n是否使用默认值(0)?"):                            return                        for node in missing_layers:                            layers[node] = 0                    pos = nx.multipartite_layout(self.G, subset_key=layers)                    self.node_positions = pos                elif method == "by_degree":                    # 按节点度数分层                    layers = {}                    nodes = list(self.G.nodes())                    if self.graph_type == "directed":                        # 有向图:按总度数(入度+出度)分层                        degrees = [(node, self.G.in_degree(node) + self.G.out_degree(node))                                   for node in nodes]                    else:                        # 无向图:按度数分层                        degrees = [(node, self.G.degree(node)) for node in nodes]                    # 按度数排序                    degrees.sort(key=lambda x: x[1])                    # 分成3层                    num_layers = 3                    for i, (node, _) in enumerate(degrees):                        layers[node] = i * num_layers // len(degrees)                    pos = nx.multipartite_layout(self.G, subset_key=layers)                    self.node_positions = pos                elif method == "manual":                    # 手动设置分层                    try:                        num_layers = int(layer_count_var.get())                        if num_layers < 1:                            messagebox.showerror("错误""层数必须大于0")                            return                    except ValueError:                        messagebox.showerror("错误""层数必须是整数")                        return                    # 弹出新对话框让用户为每个节点选择层                    self.manual_layer_selection_dialog(num_layers, dialog)                    return                self.update_graph_display()                dialog.destroy()            except Exception as e:                messagebox.showerror("错误"f"分层布局失败: {str(e)}")        button_frame = ttk.Frame(content_frame)        button_frame.pack(fill=tk.X)        ttk.Button(button_frame, text="应用", command=apply_layout).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def manual_layer_selection_dialog(self, num_layers, parent_dialog):        """手动分层选择对话框"""        dialog = tk.Toplevel(self.root)        dialog.title("手动设置节点分层")        dialog.geometry("500x600")        dialog.transient(parent_dialog)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text=f"为每个节点选择层 (0-{num_layers-1}):"                 font=("Arial"12"bold")).pack(pady=(010))        # 创建滚动框架        canvas = tk.Canvas(content_frame)        scrollbar = ttk.Scrollbar(content_frame, orient="vertical", command=canvas.yview)        scrollable_frame = ttk.Frame(canvas)        scrollable_frame.bind(            "<Configure>",            lambda e: canvas.configure(scrollregion=canvas.bbox("all"))        )        canvas.create_window((00), window=scrollable_frame, anchor="nw")        canvas.configure(yscrollcommand=scrollbar.set)        # 节点层选择器        node_layer_vars = {}        for i, node in enumerate(self.G.nodes()):            frame = ttk.Frame(scrollable_frame)            frame.pack(fill=tk.X, pady=2)            ttk.Label(frame, text=node, width=10).pack(side=tk.LEFT, padx=(010))            current_layer = self.G.nodes[node].get('layer'0)            var = tk.IntVar(value=current_layer)            node_layer_vars[node] = var            for layer in range(num_layers):                ttk.Radiobutton(frame, text=str(layer), variable=var,                                value=layer).pack(side=tk.LEFT, padx=5)        # 将canvas和scrollbar放入内容框架        canvas.pack(side="left", fill="both", expand=True)        scrollbar.pack(side="right", fill="y")        def apply_layers():            # 应用层设置            layers = {}            for node, var in node_layer_vars.items():                layers[node] = var.get()                # 更新节点的layer属性                self.G.nodes[node]['layer'] = var.get()            try:                pos = nx.multipartite_layout(self.G, subset_key=layers)                self.node_positions = pos                self.update_graph_display()                dialog.destroy()                parent_dialog.destroy()            except Exception as e:                messagebox.showerror("错误"f"应用分层失败: {str(e)}")        button_frame = ttk.Frame(content_frame)        button_frame.pack(fill=tk.X, pady=(100))        ttk.Button(button_frame, text="应用", command=apply_layers).pack(side=tk.LEFT, padx=5)        ttk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=5)    def kamada_kawai_layout(self):        """Kamada-Kawai布局 - 基于能量最小化"""        try:            pos = nx.kamada_kawai_layout(self.G)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"Kamada-Kawai布局失败: {str(e)}")    def spiral_layout(self):        """螺旋布局 - 节点沿螺旋线分布"""        try:            pos = nx.spiral_layout(self.G)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"螺旋布局失败: {str(e)}")    def more_layouts_dialog(self):        """更多布局选项对话框"""        dialog = tk.Toplevel(self.root)        dialog.title("更多布局选项")        dialog.geometry("400x500")        dialog.transient(self.root)        dialog.grab_set()        # 对话框内容框架        content_frame = ttk.Frame(dialog, padding="20")        content_frame.pack(fill=tk.BOTH, expand=True)        ttk.Label(content_frame, text="选择布局方式:", font=("Arial"12"bold")).pack(pady=(020))        # 布局按钮列表        layouts = [            ("Fruchterman-Reingold布局"self.fruchterman_reingold_layout),            ("平面布局"self.planar_layout),            ("二分图布局"self.bipartite_layout),            ("布局缩放 (0.5x)"lambdaself.rescale_layout(0.5)),            ("布局缩放 (1.5x)"lambdaself.rescale_layout(1.5)),            ("布局缩放 (2.0x)"lambdaself.rescale_layout(2.0)),            ("重置布局缩放"lambdaself.rescale_layout(1.0)),        ]        for layout_name, layout_func in layouts:            ttk.Button(content_frame, text=layout_name,                       command=lambda func=layout_func: self.execute_layout(func, dialog)).pack(fill=tk.X, pady=2)        # 分隔线        ttk.Separator(content_frame, orient='horizontal').pack(fill=tk.X, pady=20)        # 布局说明        info_frame = ttk.LabelFrame(content_frame, text="布局说明", padding="10")        info_frame.pack(fill=tk.X, pady=(010))        info_text = tk.Text(info_frame, height=6, width=30, wrap=tk.WORD)        info_text.pack(fill=tk.BOTH, expand=True)        info_text.insert(tk.END,             "• 弹簧布局: 基于力导向算法,模拟弹簧系统\n"            "• 谱布局: 基于图的拉普拉斯矩阵特征向量\n"            "• Shell布局: 节点分布在多个同心圆上\n"            "• 分层布局: 节点按层排列,适用于多层网络\n"            "• Kamada-Kawai: 基于能量最小化算法\n"            "• 螺旋布局: 节点沿螺旋线分布\n"        )        info_text.config(state=tk.DISABLED)        ttk.Button(content_frame, text="关闭", command=dialog.destroy).pack(fill=tk.X, pady=(100))    def execute_layout(self, layout_func, dialog):        """执行布局函数并关闭对话框"""        try:            layout_func()            dialog.destroy()        except Exception as e:            messagebox.showerror("错误"f"执行布局时出错: {str(e)}")    def fruchterman_reingold_layout(self):        """Fruchterman-Reingold布局 - 另一种力导向布局"""        try:            # NetworkX的spring_layout实际上就是Fruchterman-Reingold算法            pos = nx.spring_layout(self.G, k=0.5, iterations=100)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"Fruchterman-Reingold布局失败: {str(e)}")    def planar_layout(self):        """平面布局 - 适用于平面图"""        try:            # 检查图是否是平面图            if not nx.check_planarity(self.G)[0]:                messagebox.showwarning("警告""该图不是平面图,可能无法生成有效的平面布局")            # 尝试生成平面布局            pos = nx.planar_layout(self.G)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"平面布局失败: {str(e)}")    def bipartite_layout(self):        """二分图布局 - 用于二分图"""        try:            # 尝试将节点分成两组            if len(self.G.nodes()) < 2:                messagebox.showinfo("提示""二分图布局需要至少2个节点")                return            # 简单地将节点按名称分为两组            nodes = list(self.G.nodes())            group1 = nodes[:len(nodes)//2]            group2 = nodes[len(nodes)//2:]            pos = nx.bipartite_layout(self.G, group1)            self.node_positions = pos            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"二分图布局失败: {str(e)}")    def rescale_layout(self, scale_factor=1.0):        """缩放布局 - 调整布局的比例"""        try:            if not self.node_positions:                messagebox.showinfo("提示""请先使用其他布局方式")                return            # 缩放所有节点的位置            scaled_positions = {}            for node, (x, y) in self.node_positions.items():                scaled_positions[node] = (x * scale_factor, y * scale_factor)            self.node_positions = scaled_positions            self.update_graph_display()        except Exception as e:            messagebox.showerror("错误"f"缩放布局失败: {str(e)}")    def clear_graph(self):        """清空整个图"""        if len(self.G.nodes()) == 0:            return        if not messagebox.askyesno("确认清空""确定要清空整个图吗?"):            return        self.G.clear()        self.node_positions.clear()        self.node_colors.clear()        self.selected_node = None        self.update_graph_display()    def generate_random_graph(self):        """生成随机图"""        if len(self.G.nodes()) > 0:            if not messagebox.askyesno("确认生成""生成随机图将替换当前图,确定吗?"):                return        # 清空当前图        self.G.clear()        self.node_positions.clear()        self.node_colors.clear()        self.selected_node = None        # 生成随机节点和边        num_nodes = random.randint(815)        nodes = [f"Node{i}" for i in range(1, num_nodes + 1)]        for node in nodes:            self.G.add_node(node, weight=random.randint(110), layer=random.randint(02))            self.node_colors[node] = random.choice(["lightblue""lightgreen""lightcoral"                                                   "lightyellow""lightpink""lightgray"])        # 随机添加边,确保图是连通的        # 首先创建一棵生成树确保连通性        for i in range(1, num_nodes):            self.G.add_edge(nodes[i-1], nodes[i], weight=random.randint(15),                           color=random.choice(["gray""blue""red""green"]))        # 再随机添加一些边        for i in range(num_nodes * 2):            node1 = random.choice(nodes)            node2 = random.choice(nodes)            if node1 != node2 and not self.G.has_edge(node1, node2):                self.G.add_edge(node1, node2, weight=random.randint(15),                               color=random.choice(["gray""blue""red""green"]))        # 使用弹簧布局        self.spring_layout()        self.update_graph_display()    def show_graph_info(self):        """显示详细的图信息"""        if len(self.G.nodes()) == 0:            messagebox.showinfo("图信息""图为空")            return        info_window = tk.Toplevel(self.root)        info_window.title("图详细信息")        info_window.geometry("600x700")        # 创建文本框和滚动条        text_frame = ttk.Frame(info_window)        text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)        text_widget = tk.Text(text_frame, wrap=tk.WORD)        scrollbar = ttk.Scrollbar(text_frame, orient="vertical", command=text_widget.yview)        text_widget.configure(yscrollcommand=scrollbar.set)        text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)        # 添加图信息        text_widget.insert(tk.END, f"========== 图信息 ==========\n\n")        # 基本信息        text_widget.insert(tk.END, f"图类型: {'有向图'if self.graph_type == 'directed'else'无向图'}\n")        text_widget.insert(tk.END, f"节点数: {len(self.G.nodes())}\n")        text_widget.insert(tk.END, f"边数: {len(self.G.edges())}\n\n")        # 节点详细信息        text_widget.insert(tk.END, f"========== 节点信息 ==========\n\n")        for node in sorted(self.G.nodes()):            text_widget.insert(tk.END, f"节点: {node}\n")            # 节点属性            for attr_name, attr_value in self.G.nodes[node].items():                text_widget.insert(tk.END, f"  {attr_name}{attr_value}\n")            # 节点度信息            if self.graph_type == "directed":                in_degree = self.G.in_degree(node)                out_degree = self.G.out_degree(node)                text_widget.insert(tk.END, f"  入度: {in_degree}, 出度: {out_degree}, 总度数: {in_degree + out_degree}\n")            else:                degree = self.G.degree(node)                text_widget.insert(tk.END, f"  度: {degree}\n")            # 连接信息            if self.graph_type == "directed":                predecessors = list(self.G.predecessors(node))                successors = list(self.G.successors(node))                text_widget.insert(tk.END, f"  前驱节点: {', '.join(predecessors) if predecessors else'无'}\n")                text_widget.insert(tk.END, f"  后继节点: {', '.join(successors) if successors else'无'}\n")            else:                neighbors = list(self.G.neighbors(node))                text_widget.insert(tk.END, f"  邻居节点: {', '.join(neighbors) if neighbors else'无'}\n")            text_widget.insert(tk.END, "\n")        # 边详细信息        text_widget.insert(tk.END, f"========== 边信息 ==========\n\n")        for edge in sorted(self.G.edges()):            text_widget.insert(tk.END, f"边: {edge[0]} -> {edge[1]}\n")            # 边属性            for attr_name, attr_value in self.G[edge[0]][edge[1]].items():                text_widget.insert(tk.END, f"  {attr_name}{attr_value}\n")            text_widget.insert(tk.END, "\n")        text_widget.config(state=tk.DISABLED)        # 关闭按钮        ttk.Button(info_window, text="关闭", command=info_window.destroy).pack(pady=10)    def save_graph_data(self):        """保存图数据到文件"""        if len(self.G.nodes()) == 0:            messagebox.showinfo("提示""图为空,无法保存")            return        # 打开文件保存对话框        file_path = filedialog.asksaveasfilename(            defaultextension=".json",            filetypes=[("JSON files""*.json"), ("所有文件""*.*")],            title="保存图数据"        )        if file_path:            try:                # 准备要保存的数据 - 使用OrderedDict保持键的顺序                graph_data = OrderedDict()                # 保存元数据                graph_data["metadata"] = {                    "format_version""1.1",                    "graph_type"self.graph_type,                    "node_count"len(self.G.nodes()),                    "edge_count"len(self.G.edges()),                    "created_with""NetworkX Graph Editor"                }                # 保存节点信息                nodes_data = []                for node in self.G.nodes():                    node_data = OrderedDict()                    node_data["id"] = str(node)  # 确保ID是字符串                    node_data["label"] = str(node)                    # 收集所有节点属性                    for attr_name, attr_value in self.G.nodes[node].items():                        node_data[attr_name] = attr_value                    # 添加位置信息                    if node in self.node_positions:                        pos = self.node_positions[node]                        node_data["x"] = float(pos[0])                        node_data["y"] = float(pos[1])                    # 添加颜色信息                    node_data["color"] = self.node_colors.get(node, self.default_node_color)                    nodes_data.append(node_data)                graph_data["nodes"] = nodes_data                # 保存边信息                edges_data = []                for edge in self.G.edges():                    edge_data = OrderedDict()                    edge_data["source"] = str(edge[0])                    edge_data["target"] = str(edge[1])                    # 收集所有边属性                    for attr_name, attr_value in self.G[edge[0]][edge[1]].items():                        edge_data[attr_name] = attr_value                    edges_data.append(edge_data)                graph_data["edges"] = edges_data                # 写入JSON文件,确保使用ensure_ascii=False支持中文                with open(file_path, 'w', encoding='utf-8'as f:                    json.dump(graph_data, f, indent=2, ensure_ascii=False, default=str)                messagebox.showinfo("成功"f"图数据已保存到:\n{file_path}\n\n图类型: {'有向图'if self.graph_type == 'directed'else'无向图'}\n节点数: {len(self.G.nodes())}\n边数: {len(self.G.edges())}")            except Exception as e:                messagebox.showerror("错误"f"保存图数据时出错:\n{str(e)}")    def load_graph_data(self):        """从文件加载图数据"""        # 打开文件选择对话框        file_path = filedialog.askopenfilename(            filetypes=[("JSON files""*.json"), ("所有文件""*.*")],            title="选择要加载的图数据文件"        )        if file_path:            try:                # 读取JSON文件                with open(file_path, 'r', encoding='utf-8'as f:                    graph_data = json.load(f)                # 检查文件格式                if "nodes" not in graph_data or "edges" not in graph_data:                    messagebox.showerror("错误""文件格式错误:缺少节点或边数据")                    return                # 确认是否覆盖当前图                if len(self.G.nodes()) > 0:                    if not messagebox.askyesno("确认加载""加载图数据将替换当前图,确定吗?"):                        return                # 获取图类型                metadata = graph_data.get("metadata", {})                graph_type = metadata.get("graph_type""directed")                # 创建相应类型的图                if graph_type == "directed":                    self.G = nx.DiGraph()                else:                    self.G = nx.Graph()                self.graph_type = graph_type                self.graph_type_var.set(graph_type)                # 清空当前数据                self.node_positions.clear()                self.node_colors.clear()                self.selected_node = None                # 加载节点                nodes_loaded = 0                for node_data in graph_data["nodes"]:                    try:                        node_id = node_data.get("id")                        if node_id is None:                            continue                        # 创建节点                        self.G.add_node(node_id)                        # 添加节点属性                        for attr_name, attr_value in node_data.items():                            if attr_name not in ["id""x""y""color"]:                                self.G.nodes[node_id][attr_name] = attr_value                        # 添加位置信息                        if "x" in node_data and "y" in node_data:                            self.node_positions[node_id] = (                                float(node_data["x"]),                                float(node_data["y"])                            )                        # 添加颜色信息                        if "color" in node_data:                            self.node_colors[node_id] = node_data["color"]                        else:                            self.node_colors[node_id] = self.default_node_color                        nodes_loaded += 1                    except Exception as e:                        print(f"加载节点时出错: {str(e)}")                # 加载边                edges_loaded = 0                for edge_data in graph_data["edges"]:                    try:                        source = edge_data.get("source")                        target = edge_data.get("target")                        if source is None or target is None:                            continue                        # 确保节点存在                        if source not in self.G.nodes():                            self.G.add_node(source)                            self.node_colors[source] = self.default_node_color                        if target not in self.G.nodes():                            self.G.add_node(target)                            self.node_colors[target] = self.default_node_color                        # 创建边                        self.G.add_edge(source, target)                        # 添加边属性                        for attr_name, attr_value in edge_data.items():                            if attr_name not in ["source""target"]:                                self.G[source][target][attr_name] = attr_value                        edges_loaded += 1                    except Exception as e:                        print(f"加载边时出错: {str(e)}")                # 如果没有位置信息,使用弹簧布局                if not self.node_positions:                    self.spring_layout()                self.ax.set_title(f"网络图可视化 ({'有向图'if self.graph_type == 'directed'else'无向图'})", fontsize=14)                messagebox.showinfo("成功"                    f"图数据已从文件加载:\n{file_path}\n\n"                    f"图类型: {'有向图'if self.graph_type == 'directed'else'无向图'}\n"                    f"加载节点数: {nodes_loaded}\n"                    f"加载边数: {edges_loaded}")                self.update_graph_display()            except json.JSONDecodeError:                messagebox.showerror("错误""文件格式错误,不是有效的JSON文件")            except KeyError as e:                messagebox.showerror("错误"f"图数据格式不完整: {str(e)}")            except Exception as e:                messagebox.showerror("错误"f"加载图数据时出错:\n{str(e)}")def main():    root = tk.Tk()    app = GraphEditorApp(root)    root.mainloop()if __name__ == "__main__":    main()

完毕!!感谢您的收看

------★历史博文集合★------

Python入门篇  进阶篇  视频教程  Py安装

py项目Python模块 Python爬虫  Json

Xpath正则表达式SeleniumEtreeCss

Gui程序开发TkinterPyqt5 列表元组字典

数据可视化 matplotlib  词云图Pyecharts

海龟画图PandasBug处理电脑小知识

自动化脚本编程工具NumPy CSVWeb

Pygame  图像处理  机器学习数据库

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-02-11 03:49:26 HTTP/2.0 GET : https://f.mffb.com.cn/a/474912.html
  2. 运行时间 : 0.234560s [ 吞吐率:4.26req/s ] 内存消耗:5,475.71kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=9f7a0ddaffd6935975cdeaffe71aca9c
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000926s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001490s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001682s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.004808s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001451s ]
  6. SELECT * FROM `set` [ RunTime:0.000600s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001545s ]
  8. SELECT * FROM `article` WHERE `id` = 474912 LIMIT 1 [ RunTime:0.005901s ]
  9. UPDATE `article` SET `lasttime` = 1770752966 WHERE `id` = 474912 [ RunTime:0.009349s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.002779s ]
  11. SELECT * FROM `article` WHERE `id` < 474912 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.005570s ]
  12. SELECT * FROM `article` WHERE `id` > 474912 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.002208s ]
  13. SELECT * FROM `article` WHERE `id` < 474912 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.010031s ]
  14. SELECT * FROM `article` WHERE `id` < 474912 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.004808s ]
  15. SELECT * FROM `article` WHERE `id` < 474912 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.004944s ]
0.238231s