import tkinter as tk from tkinter import filedialog, ttk import os import math from tkinter import Canvas class FolderTree3D: def __init__(self, root): self.root = root self.root.title("文件夹3D树状图") self.root.geometry("1200x800") # 创建主框架 main_frame = ttk.Frame(root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 输入区域 input_frame = ttk.Frame(main_frame) input_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(input_frame, text="文件夹路径:").pack(side=tk.LEFT, padx=(0, 5)) self.path_var = tk.StringVar() self.path_entry = ttk.Entry(input_frame, textvariable=self.path_var, width=80) self.path_entry.pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True) browse_btn = ttk.Button(input_frame, text="浏览", command=self.browse_folder) browse_btn.pack(side=tk.LEFT, padx=(0, 5)) show_btn = ttk.Button(input_frame, text="显示3D树状图", command=self.show_3d_tree) show_btn.pack(side=tk.LEFT) # 缩放控制 zoom_frame = ttk.Frame(main_frame) zoom_frame.pack(fill=tk.X, pady=(0, 5)) ttk.Label(zoom_frame, text="缩放:").pack(side=tk.LEFT, padx=(0, 5)) self.zoom_var = tk.DoubleVar(value=1.0) self.zoom_scale = ttk.Scale(zoom_frame, from_=0.1, to=2.0, variable=self.zoom_var, command=self.on_zoom) self.zoom_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) self.zoom_label = ttk.Label(zoom_frame, text="100%") self.zoom_label.pack(side=tk.LEFT) # 3D显示区域 self.canvas = Canvas(main_frame, bg="white") self.canvas.pack(fill=tk.BOTH, expand=True) # 缩放和平移支持 self.scale = 1.0 self.offset_x = 0 self.offset_y = 0 self.last_x = 0 self.last_y = 0 self.dragging = False # 绑定鼠标事件 self.canvas.bind("", self.on_press) self.canvas.bind("", self.on_drag) self.canvas.bind("", self.on_release) self.canvas.bind("", self.on_mouse_wheel) # 文件夹数据 self.folder_data = {} self.max_depth = 0 self.tree_data = [] # 存储所有节点数据,用于缩放重绘 def browse_folder(self): folder_path = filedialog.askdirectory() if folder_path: self.path_var.set(folder_path) def scan_folder(self, folder_path, depth=0): if depth > self.max_depth: self.max_depth = depth try: items = os.listdir(folder_path) folders = [] files = [] for item in items: item_path = os.path.join(folder_path, item) if os.path.isdir(item_path): folders.append(item) self.scan_folder(item_path, depth + 1) else: files.append(item) self.folder_data[folder_path] = {"folders": folders, "files": files, "depth": depth} except Exception as e: print(f"扫描文件夹出错: {e}") def draw_3d_tree(self): self.canvas.delete("all") self.tree_data = [] if not self.folder_data: return # 收集树状数据 self.collect_tree_data(self.path_var.get(), 0, self.canvas.winfo_width()//2, 100) # 绘制树状图 self.draw_tree_from_data() def collect_tree_data(self, folder_path, depth=0, root_x=0, root_y=0): if folder_path not in self.folder_data: return data = self.folder_data[folder_path] folders = data["folders"] files = data["files"] # 计算当前节点位置 node = { "type": "folder", "path": folder_path, "name": os.path.basename(folder_path), "x": root_x, "y": root_y, "depth": depth, "children": [] } self.tree_data.append(node) # 收集所有层级的节点和父节点关系 all_nodes = [] self.collect_all_nodes(folder_path, depth, all_nodes) # 按层级分组 level_nodes = {} for node_data in all_nodes: level = node_data["depth"] if level not in level_nodes: level_nodes[level] = [] level_nodes[level].append(node_data) # 重新计算所有节点位置,使用扇区布局 self.calculate_sector_positions(root_x, root_y, level_nodes) def collect_all_nodes(self, folder_path, depth, all_nodes): if folder_path not in self.folder_data: return data = self.folder_data[folder_path] folders = data["folders"] files = data["files"] # 添加当前文件夹的子节点 for folder in folders: folder_path_full = os.path.join(folder_path, folder) node_data = { "type": "folder", "name": folder, "path": folder_path_full, "depth": depth + 1, "parent": folder_path } all_nodes.append(node_data) self.collect_all_nodes(folder_path_full, depth + 1, all_nodes) for file in files: node_data = { "type": "file", "name": file, "path": os.path.join(folder_path, file), "depth": depth + 1, "parent": folder_path } all_nodes.append(node_data) def calculate_sector_positions(self, root_x, root_y, level_nodes): # 清空原有数据,重新绘制 self.tree_data = [{ "type": "folder", "name": os.path.basename(self.path_var.get()), "path": self.path_var.get(), "x": root_x, "y": root_y, "depth": 0 }] # 先计算所有节点的位置 all_node_data = [] # 为每个层级计算位置 for level in sorted(level_nodes.keys()): nodes = level_nodes[level] total_nodes = len(nodes) if total_nodes == 0: continue radius = level * 100 # 按父节点分组 parent_groups = {} for node_data in nodes: parent = node_data["parent"] if parent not in parent_groups: parent_groups[parent] = [] parent_groups[parent].append(node_data) # 为每个父节点的子节点计算位置 for parent_path, group_nodes in parent_groups.items(): parent_node = next((n for n in self.tree_data if n["path"] == parent_path), None) if not parent_node: continue # 计算当前层级的扇区范围 # 父节点所在环的文件夹个数 parent_level = parent_node["depth"] parent_level_nodes = [] if parent_level in level_nodes: parent_level_nodes = [n for n in level_nodes[parent_level] if n["type"] == "folder"] else: parent_level_nodes = [parent_node] if parent_node["type"] == "folder" else [] total_parent_folders = len(parent_level_nodes) if total_parent_folders == 0: total_parent_folders = 1 # 每个文件夹分配360度/total_parent_folders的扇区 sector_angle = 2 * math.pi / total_parent_folders # 找到父节点在同级中的位置 parent_index = 0 if parent_level in level_nodes and parent_node["type"] == "folder": for i, n in enumerate(parent_level_nodes): if n["path"] == parent_path: parent_index = i break # 计算父节点的扇区 start_angle = parent_index * sector_angle end_angle = (parent_index + 1) * sector_angle sector_width = end_angle - start_angle # 在扇区内均匀分布子节点 angle_step = sector_width / len(group_nodes) for i, node_data in enumerate(group_nodes): angle = start_angle + (i + 0.5) * angle_step # 文件夹在大圆上,文件在小圆上 if node_data["type"] == "folder": current_radius = radius else: current_radius = radius - 20 x = root_x + current_radius * math.cos(angle) y = root_y + current_radius * math.sin(angle) # 保存扇区信息 node_data["sector_start"] = start_angle + i * angle_step node_data["sector_end"] = start_angle + (i + 1) * angle_step node_data["x"] = x node_data["y"] = y node_data["angle"] = angle all_node_data.append(node_data) self.tree_data.append({ "type": node_data["type"], "name": node_data["name"], "path": node_data["path"], "x": x, "y": y, "depth": level, "parent_path": node_data["parent"], "sector_start": node_data["sector_start"], "sector_end": node_data["sector_end"], "angle": angle }) # 保存扇区信息 node_data["sector_start"] = start_angle + i * angle_step node_data["sector_end"] = start_angle + (i + 1) * angle_step # 调整父节点位置,使其与子节点中间位置对齐 # 从最深层级开始向上调整 max_level = max(level_nodes.keys()) if level_nodes else 0 for level in range(max_level, 0, -1): if level not in level_nodes: continue # 找到当前层级的文件夹 current_folders = [n for n in level_nodes[level] if n["type"] == "folder"] for folder in current_folders: # 找到该文件夹的所有子节点 child_nodes = [n for n in all_node_data if n["parent"] == folder["path"]] if not child_nodes: continue # 计算子节点的平均角度 total_angle = sum(n["angle"] for n in child_nodes) avg_angle = total_angle / len(child_nodes) # 找到父节点 parent_node = next((n for n in self.tree_data if n["path"] == folder["path"]), None) if parent_node: # 调整父节点位置 radius = level * 100 x = root_x + radius * math.cos(avg_angle) y = root_y + radius * math.sin(avg_angle) parent_node["x"] = x parent_node["y"] = y parent_node["angle"] = avg_angle def draw_node(self, x, y, folder_path, depth, width, level_spacing): # 改为使用收集的数据进行绘制 pass def draw_tree_from_data(self): self.canvas.delete("all") for node in self.tree_data: # 应用缩放和平移 x = node["x"] * self.scale + self.offset_x y = node["y"] * self.scale + self.offset_y if node["type"] == "folder": node_radius = min(15, 15 * self.scale) # 缩小文件夹节点 self.canvas.create_oval(x - node_radius, y - node_radius, x + node_radius, y + node_radius, fill="#4CAF50", outline="#2E7D32") font_size = min(8, int(8 * self.scale)) # 缩小字体 self.canvas.create_text(x, y, text=node["name"][:8] + "..." if len(node["name"]) > 8 else node["name"], fill="white", font=("Arial", font_size, "bold")) else: file_width = min(20, 20 * self.scale) # 缩小文件节点 file_height = min(15, 15 * self.scale) self.canvas.create_rectangle(x - file_width//2, y - file_height//2, x + file_width//2, y + file_height//2, fill="#2196F3", outline="#1565C0") font_size = min(6, int(6 * self.scale)) # 缩小字体 self.canvas.create_text(x, y, text=node["name"][:6] + "..." if len(node["name"]) > 6 else node["name"], fill="white", font=("Arial", font_size)) # 绘制连接线 for node in self.tree_data: if "parent_path" in node: # 找到父节点 parent_node = next((n for n in self.tree_data if n["path"] == node["parent_path"]), None) if parent_node: x1 = parent_node["x"] * self.scale + self.offset_x y1 = parent_node["y"] * self.scale + self.offset_y x2 = node["x"] * self.scale + self.offset_x y2 = node["y"] * self.scale + self.offset_y self.canvas.create_line(x1, y1, x2, y2, fill="#666666", width=1) # 在文件所在圆上标记根节点与文件节点连线和圆的交点 root_node = self.tree_data[0] root_x = root_node["x"] * self.scale + self.offset_x root_y = root_node["y"] * self.scale + self.offset_y for node in self.tree_data: if node["type"] == "file": # 计算根节点与文件节点连线和文件所在圆的交点 file_x = node["x"] * self.scale + self.offset_x file_y = node["y"] * self.scale + self.offset_y # 计算文件所在圆的半径 file_depth = node["depth"] file_radius = (file_depth * 100 - 20) * self.scale # 计算交点 dx = file_x - root_x dy = file_y - root_y distance = math.sqrt(dx * dx + dy * dy) if distance == 0: continue # 计算交点坐标 intersection_x = root_x + (dx / distance) * file_radius intersection_y = root_y + (dy / distance) * file_radius # 标记交点 self.canvas.create_oval(intersection_x - 3, intersection_y - 3, intersection_x + 3, intersection_y + 3, fill="#FF0000", outline="#FF0000", width=1) # 绘制同心圆 max_level = max(node["depth"] for node in self.tree_data) # Get root node position after scale and offset root_node = self.tree_data[0] root_x = root_node["x"] * self.scale + self.offset_x root_y = root_node["y"] * self.scale + self.offset_y for level in range(1, max_level + 1): radius = level * 100 * self.scale self.canvas.create_oval(root_x - radius, root_y - radius, root_x + radius, root_y + radius, outline="#333333", width=1) # 在内侧20px处添加小同心圆 inner_radius = radius - 20 * self.scale if inner_radius > 0: self.canvas.create_oval(root_x - inner_radius, root_y - inner_radius, root_x + inner_radius, root_y + inner_radius, outline="#666666", width=1) def draw_3d_line(self, x1, y1, x2, y2): # 绘制连接线,使用更细的线条 self.canvas.create_line(x1, y1, x2, y2, fill="#757575", width=1) self.canvas.create_line(x1 + 1, y1 + 1, x2 + 1, y2 + 1, fill="#BDBDBD", width=0.5) def on_zoom(self, value): self.scale = float(value) self.zoom_label.config(text=f"{int(self.scale * 100)}%") if self.tree_data: self.draw_tree_from_data() def on_press(self, event): self.last_x = event.x self.last_y = event.y self.dragging = True def on_drag(self, event): if self.dragging: dx = event.x - self.last_x dy = event.y - self.last_y self.offset_x += dx self.offset_y += dy self.last_x = event.x self.last_y = event.y if self.tree_data: self.draw_tree_from_data() def on_release(self, event): self.dragging = False def on_mouse_wheel(self, event): # 鼠标滚轮缩放 delta = event.delta if delta > 0: self.scale = min(2.0, self.scale * 1.1) else: self.scale = max(0.1, self.scale * 0.9) self.zoom_var.set(self.scale) self.zoom_label.config(text=f"{int(self.scale * 100)}%") if self.tree_data: self.draw_tree_from_data() def show_3d_tree(self): folder_path = self.path_var.get() if not folder_path or not os.path.isdir(folder_path): tk.messagebox.showerror("错误", "请选择有效的文件夹路径") return self.folder_data = {} self.max_depth = 0 self.scan_folder(folder_path) self.draw_3d_tree() if __name__ == "__main__": root = tk.Tk() app = FolderTree3D(root) root.mainloop()