Python实例题
题目
要求:
解题思路:
代码实现:
基于 Python 的简单文件管理器
tkinter
构建图形用户界面。os
和 shutil
模块进行文件操作。tkinter
构建用户友好的界面。import os
import shutil
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import datetime
import threading
import time
from pathlib import Path
class FileManager:
def __init__(self, root):
self.root = root
self.root.title("文件管理器")
self.root.geometry("1000x600")
# 设置字体以支持中文显示
self.default_font = ('SimHei', 10)
self.root.option_add("*Font", self.default_font)
# 当前路径
self.current_path = os.getcwd()
# 创建主界面
self.create_widgets()
# 加载初始目录
self.load_directory(self.current_path)
def create_widgets(self):
"""创建界面组件"""
# 创建主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建顶部导航栏
nav_frame = ttk.Frame(main_frame)
nav_frame.pack(fill=tk.X, padx=5, pady=5)
# 返回上级目录按钮
ttk.Button(nav_frame, text="上级目录", command=self.go_up).pack(side=tk.LEFT, padx=5)
# 刷新按钮
ttk.Button(nav_frame, text="刷新", command=lambda: self.load_directory(self.current_path)).pack(side=tk.LEFT, padx=5)
# 地址栏
self.path_var = tk.StringVar()
self.path_entry = ttk.Entry(nav_frame, textvariable=self.path_var, width=80)
self.path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.path_entry.bind("", lambda event: self.navigate_to_path())
# 转到按钮
ttk.Button(nav_frame, text="转到", command=self.navigate_to_path).pack(side=tk.LEFT, padx=5)
# 创建左右分割的主区域
paned_window = ttk.PanedWindow(main_frame, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True, pady=5)
# 左侧树形视图(目录结构)
self.tree_frame = ttk.Frame(paned_window)
paned_window.add(self.tree_frame, weight=1)
# 右侧文件列表视图
self.list_frame = ttk.Frame(paned_window)
paned_window.add(self.list_frame, weight=3)
# 创建树形视图
self.create_tree_view()
# 创建文件列表视图
self.create_file_list_view()
# 创建底部状态栏
self.status_var = tk.StringVar()
self.status_var.set(f"当前路径: {self.current_path}")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def create_tree_view(self):
"""创建目录树视图"""
# 创建滚动条
tree_scroll = ttk.Scrollbar(self.tree_frame)
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 创建树形视图
self.tree = ttk.Treeview(self.tree_frame, yscrollcommand=tree_scroll.set)
self.tree.pack(fill=tk.BOTH, expand=True)
tree_scroll.config(command=self.tree.yview)
# 设置列
self.tree["columns"] = ("path")
self.tree.column("#0", width=200, minwidth=200)
self.tree.column("path", width=0, stretch=tk.NO)
# 设置标题
self.tree.heading("#0", text="目录结构", anchor=tk.W)
# 绑定事件
self.tree.bind("", self.on_tree_select)
# 加载根目录
self.load_tree()
def load_tree(self):
"""加载目录树"""
# 清空树
for item in self.tree.get_children():
self.tree.delete(item)
# 获取系统根目录
if os.name == 'nt': # Windows系统
roots = [f"{drive}:\\" for drive in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if os.path.exists(f"{drive}:\\")]
else: # Linux/Mac系统
roots = ["/"]
# 添加根目录到树
for root in roots:
self.tree.insert("", tk.END, text=root, values=(root,))
self.load_tree_children(root, "")
def load_tree_children(self, path, parent_id):
"""递归加载目录树的子目录"""
try:
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isdir(item_path):
item_id = self.tree.insert(parent_id, tk.END, text=item, values=(item_path,))
# 添加子目录标记
self.tree.insert(item_id, tk.END)
except PermissionError:
pass
def on_tree_select(self, event):
"""处理树节点选择事件"""
selected_item = self.tree.selection()
if not selected_item:
return
item = selected_item[0]
path = self.tree.item(item, "values")[0]
# 如果该节点有子目录且未加载,则加载子目录
if self.tree.get_children(item) and self.tree.item(self.tree.get_children(item)[0], "text") == '':
self.tree.delete(self.tree.get_children(item))
self.load_tree_children(path, item)
# 加载选中的目录
self.load_directory(path)
def create_file_list_view(self):
"""创建文件列表视图"""
# 创建顶部工具栏
toolbar = ttk.Frame(self.list_frame)
toolbar.pack(fill=tk.X, padx=5, pady=5)
# 新建文件夹按钮
ttk.Button(toolbar, text="新建文件夹", command=self.create_directory).pack(side=tk.LEFT, padx=5)
# 删除按钮
ttk.Button(toolbar, text="删除", command=self.delete_selected).pack(side=tk.LEFT, padx=5)
# 重命名按钮
ttk.Button(toolbar, text="重命名", command=self.rename_selected).pack(side=tk.LEFT, padx=5)
# 复制按钮
ttk.Button(toolbar, text="复制", command=self.copy_selected).pack(side=tk.LEFT, padx=5)
# 移动按钮
ttk.Button(toolbar, text="移动", command=self.move_selected).pack(side=tk.LEFT, padx=5)
# 搜索框
search_frame = ttk.Frame(toolbar)
search_frame.pack(side=tk.RIGHT, padx=5)
ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT, padx=5)
self.search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=20)
search_entry.pack(side=tk.LEFT, padx=5)
search_entry.bind("", lambda event: self.search_files())
ttk.Button(search_frame, text="搜索", command=self.search_files).pack(side=tk.LEFT, padx=5)
# 创建滚动条
list_scroll = ttk.Scrollbar(self.list_frame)
list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 创建文件列表
columns = ("name", "type", "size", "modified", "path")
self.file_list = ttk.Treeview(self.list_frame, columns=columns, show="headings", yscrollcommand=list_scroll.set)
self.file_list.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
list_scroll.config(command=self.file_list.yview)
# 设置列宽和标题
self.file_list.column("name", width=250, anchor=tk.W)
self.file_list.column("type", width=100, anchor=tk.CENTER)
self.file_list.column("size", width=100, anchor=tk.RIGHT)
self.file_list.column("modified", width=150, anchor=tk.CENTER)
self.file_list.column("path", width=0, stretch=tk.NO) # 隐藏路径列
self.file_list.heading("name", text="名称")
self.file_list.heading("type", text="类型")
self.file_list.heading("size", text="大小")
self.file_list.heading("modified", text="修改日期")
# 绑定事件
self.file_list.bind("", self.on_double_click)
self.file_list.bind("", self.on_right_click)
# 创建右键菜单
self.create_context_menu()
def create_context_menu(self):
"""创建右键菜单"""
self.context_menu = tk.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="打开", command=self.open_selected)
self.context_menu.add_separator()
self.context_menu.add_command(label="新建文件夹", command=self.create_directory)
self.context_menu.add_separator()
self.context_menu.add_command(label="复制", command=self.copy_selected)
self.context_menu.add_command(label="移动", command=self.move_selected)
self.context_menu.add_separator()
self.context_menu.add_command(label="重命名", command=self.rename_selected)
self.context_menu.add_command(label="删除", command=self.delete_selected)
self.context_menu.add_separator()
self.context_menu.add_command(label="查看属性", command=self.show_properties)
def on_right_click(self, event):
"""处理右键点击事件"""
# 选择点击的项
item = self.file_list.identify_row(event.y)
if item:
self.file_list.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def load_directory(self, path):
"""加载目录内容"""
try:
# 更新当前路径
self.current_path = path
self.path_var.set(path)
# 清空文件列表
for item in self.file_list.get_children():
self.file_list.delete(item)
# 获取目录内容
items = os.listdir(path)
# 先添加目录,再添加文件(按名称排序)
directories = []
files = []
for item in items:
item_path = os.path.join(path, item)
if os.path.isdir(item_path):
directories.append(item)
else:
files.append(item)
# 排序
directories.sort(key=str.lower)
files.sort(key=str.lower)
# 添加到列表
for item in directories + files:
item_path = os.path.join(path, item)
try:
# 获取文件信息
stat = os.stat(item_path)
modified_time = datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
if os.path.isdir(item_path):
item_type = "文件夹"
size = "-"
else:
item_type = "文件"
size = self.format_size(stat.st_size)
# 添加到列表
self.file_list.insert("", tk.END, values=(item, item_type, size, modified_time, item_path))
except (PermissionError, OSError):
continue
# 更新状态栏
self.status_var.set(f"当前路径: {path} | 文件夹: {len(directories)} | 文件: {len(files)}")
# 在树形视图中高亮显示当前目录
self.highlight_current_directory_in_tree()
except (PermissionError, FileNotFoundError) as e:
messagebox.showerror("错误", f"无法访问该目录: {e}")
# 尝试返回上级目录
if os.path.dirname(path) != path:
self.load_directory(os.path.dirname(path))
def highlight_current_directory_in_tree(self):
"""在树形视图中高亮显示当前目录"""
# 遍历树形视图,查找当前路径
def find_item(parent, path):
for item in self.tree.get_children(parent):
item_path = self.tree.item(item, "values")[0]
if item_path == path:
return item
if path.startswith(item_path) and os.path.isdir(item_path):
# 如果该节点有子目录且未加载,则加载子目录
if self.tree.get_children(item) and self.tree.item(self.tree.get_children(item)[0], "text") == '':
self.tree.delete(self.tree.get_children(item))
self.load_tree_children(item_path, item)
# 递归查找子节点
result = find_item(item, path)
if result:
return result
return None
# 查找并选中当前路径
item = find_item("", self.current_path)
if item:
self.tree.selection_set(item)
self.tree.see(item)
def format_size(self, size_bytes):
"""格式化文件大小"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes/1024:.2f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes/(1024*1024):.2f} MB"
else:
return f"{size_bytes/(1024*1024*1024):.2f} GB"
def go_up(self):
"""返回上级目录"""
parent_path = os.path.dirname(self.current_path)
if parent_path != self.current_path: # 避免在根目录时出错
self.load_directory(parent_path)
def navigate_to_path(self):
"""导航到地址栏中的路径"""
path = self.path_var.get()
if os.path.exists(path) and os.path.isdir(path):
self.load_directory(path)
else:
messagebox.showerror("错误", f"路径不存在: {path}")
def on_double_click(self, event):
"""处理双击事件"""
selected_items = self.file_list.selection()
if not selected_items:
return
item = selected_items[0]
item_path = self.file_list.item(item, "values")[4]
if os.path.isdir(item_path):
# 如果是目录,则打开
self.load_directory(item_path)
else:
# 如果是文件,则尝试打开
self.open_selected()
def open_selected(self):
"""打开选中的文件或目录"""
selected_items = self.file_list.selection()
if not selected_items:
return
item = selected_items[0]
item_path = self.file_list.item(item, "values")[4]
try:
if os.path.isdir(item_path):
self.load_directory(item_path)
else:
# 尝试打开文件
if os.name == 'nt': # Windows
os.startfile(item_path)
else: # Linux/Mac
os.system(f'xdg-open "{item_path}"')
except Exception as e:
messagebox.showerror("错误", f"无法打开: {e}")
def create_directory(self):
"""创建新目录"""
dir_name = simpledialog.askstring("新建文件夹", "输入文件夹名称:", parent=self.root)
if not dir_name:
return
new_dir_path = os.path.join(self.current_path, dir_name)
try:
if not os.path.exists(new_dir_path):
os.makedirs(new_dir_path)
self.load_directory(self.current_path)
messagebox.showinfo("成功", f"文件夹 '{dir_name}' 已创建")
else:
messagebox.showerror("错误", f"文件夹 '{dir_name}' 已存在")
except Exception as e:
messagebox.showerror("错误", f"无法创建文件夹: {e}")
def delete_selected(self):
"""删除选中的文件或目录"""
selected_items = self.file_list.selection()
if not selected_items:
return
# 确认删除
count = len(selected_items)
message = f"确定要删除选中的 {count} 个项目吗?此操作不可撤销!"
if messagebox.askyesno("确认删除", message):
for item in selected_items:
item_path = self.file_list.item(item, "values")[4]
try:
if os.path.isdir(item_path):
# 删除目录(递归)
shutil.rmtree(item_path)
else:
# 删除文件
os.remove(item_path)
except Exception as e:
messagebox.showerror("错误", f"无法删除 '{os.path.basename(item_path)}': {e}")
# 刷新目录
self.load_directory(self.current_path)
def rename_selected(self):
"""重命名选中的文件或目录"""
selected_items = self.file_list.selection()
if not selected_items or len(selected_items) > 1:
return
item = selected_items[0]
old_name = self.file_list.item(item, "values")[0]
old_path = self.file_list.item(item, "values")[4]
new_name = simpledialog.askstring("重命名", "输入新名称:", initialvalue=old_name, parent=self.root)
if not new_name or new_name == old_name:
return
new_path = os.path.join(self.current_path, new_name)
try:
if not os.path.exists(new_path):
os.rename(old_path, new_path)
self.load_directory(self.current_path)
messagebox.showinfo("成功", f"'{old_name}' 已重命名为 '{new_name}'")
else:
messagebox.showerror("错误", f"'{new_name}' 已存在")
except Exception as e:
messagebox.showerror("错误", f"无法重命名: {e}")
def copy_selected(self):
"""复制选中的文件或目录"""
selected_items = self.file_list.selection()
if not selected_items:
return
# 记录选中的项目路径
self.copied_items = [self.file_list.item(item, "values")[4] for item in selected_items]
self.copy_operation = "copy"
messagebox.showinfo("复制", f"已复制 {len(selected_items)} 个项目")
def move_selected(self):
"""移动选中的文件或目录"""
selected_items = self.file_list.selection()
if not selected_items:
return
# 记录选中的项目路径
self.copied_items = [self.file_list.item(item, "values")[4] for item in selected_items]
self.copy_operation = "move"
messagebox.showinfo("移动", f"已剪切 {len(selected_items)} 个项目")
def paste(self):
"""粘贴复制或移动的项目"""
if not hasattr(self, 'copied_items') or not self.copied_items:
return
for item_path in self.copied_items:
item_name = os.path.basename(item_path)
dest_path = os.path.join(self.current_path, item_name)
# 检查目标是否已存在
if os.path.exists(dest_path):
# 如果已存在,添加数字后缀
base_name, ext = os.path.splitext(item_name)
count = 1
while os.path.exists(dest_path):
new_name = f"{base_name} ({count}){ext}"
dest_path = os.path.join(self.current_path, new_name)
count += 1
try:
if self.copy_operation == "copy":
if os.path.isdir(item_path):
# 复制目录
shutil.copytree(item_path, dest_path)
else:
# 复制文件
shutil.copy2(item_path, dest_path)
else:
# 移动
shutil.move(item_path, dest_path)
except Exception as e:
messagebox.showerror("错误", f"无法{self.copy_operation}: {e}")
# 刷新目录
self.load_directory(self.current_path)
# 清除复制/移动的项目
if self.copy_operation == "move":
self.copied_items = []
def show_properties(self):
"""显示选中项目的属性"""
selected_items = self.file_list.selection()
if not selected_items or len(selected_items) > 1:
return
item = selected_items[0]
item_path = self.file_list.item(item, "values")[4]
try:
stat = os.stat(item_path)
# 创建属性窗口
properties_window = tk.Toplevel(self.root)
properties_window.title(f"{os.path.basename(item_path)} 的属性")
properties_window.geometry("400x300")
properties_window.resizable(False, False)
# 创建属性信息
frame = ttk.Frame(properties_window, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
# 基本信息
ttk.Label(frame, text=f"名称: {os.path.basename(item_path)}").grid(row=0, column=0, sticky=tk.W, pady=5)
ttk.Label(frame, text=f"类型: {'文件夹' if os.path.isdir(item_path) else '文件'}").grid(row=1, column=0, sticky=tk.W, pady=5)
ttk.Label(frame, text=f"位置: {os.path.dirname(item_path)}").grid(row=2, column=0, sticky=tk.W, pady=5)
if not os.path.isdir(item_path):
ttk.Label(frame, text=f"大小: {self.format_size(stat.st_size)}").grid(row=3, column=0, sticky=tk.W, pady=5)
ttk.Label(frame, text=f"创建时间: {datetime.datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S')}").grid(row=4, column=0, sticky=tk.W, pady=5)
ttk.Label(frame, text=f"修改时间: {datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')}").grid(row=5, column=0, sticky=tk.W, pady=5)
ttk.Label(frame, text=f"访问时间: {datetime.datetime.fromtimestamp(stat.st_atime).strftime('%Y-%m-%d %H:%M:%S')}").grid(row=6, column=0, sticky=tk.W, pady=5)
# 权限信息
ttk.Label(frame, text="").grid(row=7, column=0, pady=10)
ttk.Label(frame, text="权限:").grid(row=8, column=0, sticky=tk.W)
perms = []
if os.access(item_path, os.R_OK):
perms.append("读取")
if os.access(item_path, os.W_OK):
perms.append("写入")
if os.access(item_path, os.X_OK):
perms.append("执行")
ttk.Label(frame, text=", ".join(perms) if perms else "无权限").grid(row=9, column=0, sticky=tk.W, pady=5)
# 关闭按钮
ttk.Button(frame, text="确定", command=properties_window.destroy).grid(row=10, column=0, pady=20)
except Exception as e:
messagebox.showerror("错误", f"无法获取属性: {e}")
def search_files(self):
"""搜索文件和目录"""
search_text = self.search_var.get().strip().lower()
if not search_text:
# 如果搜索文本为空,则重新加载当前目录
self.load_directory(self.current_path)
return
# 创建搜索结果窗口
search_window = tk.Toplevel(self.root)
search_window.title(f"搜索: {search_text}")
search_window.geometry("800x500")
# 创建搜索结果列表
columns = ("name", "type", "size", "modified", "path")
search_results = ttk.Treeview(search_window, columns=columns, show="headings")
search_results.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 设置列宽和标题
search_results.column("name", width=250, anchor=tk.W)
search_results.column("type", width=100, anchor=tk.CENTER)
search_results.column("size", width=100, anchor=tk.RIGHT)
search_results.column("modified", width=150, anchor=tk.CENTER)
search_results.column("path", width=0, stretch=tk.NO) # 隐藏路径列
search_results.heading("name", text="名称")
search_results.heading("type", text="类型")
search_results.heading("size", text="大小")
search_results.heading("modified", text="修改日期")
# 绑定双击事件
search_results.bind("", lambda event: self.on_search_result_double_click(event, search_results))
# 创建状态栏
status_var = tk.StringVar()
status_var.set("正在搜索...")
status_bar = ttk.Label(search_window, textvariable=status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 在单独的线程中执行搜索,避免UI卡顿
def perform_search():
results_count = 0
start_time = time.time()
try:
for root, dirs, files in os.walk(self.current_path):
# 搜索目录
for dir_name in dirs:
if search_text in dir_name.lower():
dir_path = os.path.join(root, dir_name)
try:
stat = os.stat(dir_path)
modified_time = datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
search_results.insert("", tk.END, values=(
dir_name, "文件夹", "-", modified_time, dir_path
))
results_count += 1
except (PermissionError, OSError):
continue
# 搜索文件
for file_name in files:
if search_text in file_name.lower():
file_path = os.path.join(root, file_name)
try:
stat = os.stat(file_path)
modified_time = datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M')
size = self.format_size(stat.st_size)
search_results.insert("", tk.END, values=(
file_name, "文件", size, modified_time, file_path
))
results_count += 1
except (PermissionError, OSError):
continue
except Exception as e:
status_var.set(f"搜索出错: {e}")
end_time = time.time()
status_var.set(f"搜索完成,找到 {results_count} 个结果,耗时 {end_time - start_time:.2f} 秒")
# 启动搜索线程
search_thread = threading.Thread(target=perform_search)
search_thread.daemon = True
search_thread.start()
def on_search_result_double_click(self, event, treeview):
"""处理搜索结果双击事件"""
selected_items = treeview.selection()
if not selected_items:
return
item = selected_items[0]
item_path = treeview.item(item, "values")[4]
if os.path.isdir(item_path):
# 如果是目录,则关闭搜索窗口并打开该目录
event.widget.master.destroy()
self.load_directory(item_path)
else:
# 如果是文件,则尝试打开
try:
if os.name == 'nt': # Windows
os.startfile(item_path)
else: # Linux/Mac
os.system(f'xdg-open "{item_path}"')
except Exception as e:
messagebox.showerror("错误", f"无法打开: {e}")
if __name__ == "__main__":
root = tk.Tk()
app = FileManager(root)
root.mainloop()