import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, simpledialog
import traceback
import sys
import os
# 添加当前目录到系统路径,确保导入成功
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, current_dir)
try:
from fat32_reader import FAT32Writer, FAT32Analyzer
except ImportError as e:
print(f"导入错误: {e}")
print(f"当前目录: {current_dir}")
print(f"Python 路径: {sys.path}")
# 如果导入失败,显示错误信息并退出
root = tk.Tk()
root.withdraw()
messagebox.showerror("导入错误", f"无法导入 FAT32 模块: {e}\n请确保 fat32_reader.py 文件存在")
sys.exit(1)
class FAT32GUI:
"""FAT32文件系统图形界面"""
def __init__(self, fat32_reader):
self.reader = fat32_reader
self.analyzer = FAT32Analyzer(self.reader)
# 创建主窗口
self.root = tk.Tk()
self.root.title(f"FAT32 文件系统可视化 - {self.reader.get_volume_label()}")
self.root.geometry("1400x900")
# 创建主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建顶部控制面板
control_frame = tk.Frame(main_frame)
control_frame.pack(fill=tk.X, pady=(0, 10))
# 创建按钮
self.create_btn = tk.Button(
control_frame,
text="新建文件",
command=self.create_file_dialog,
width=15
)
self.create_btn.pack(side=tk.LEFT, padx=5)
self.refresh_btn = tk.Button(
control_frame,
text="刷新视图",
command=self.refresh_view,
width=15
)
self.refresh_btn.pack(side=tk.LEFT, padx=5)
# 创建过滤选项
filter_frame = tk.LabelFrame(control_frame, text="显示选项")
filter_frame.pack(side=tk.LEFT, padx=20)
self.show_hidden_var = tk.BooleanVar(value=False)
self.show_system_var = tk.BooleanVar(value=False)
self.show_special_var = tk.BooleanVar(value=False)
tk.Checkbutton(
filter_frame,
text="显示隐藏文件",
variable=self.show_hidden_var,
command=self.refresh_view
).pack(side=tk.LEFT, padx=5)
tk.Checkbutton(
filter_frame,
text="显示系统文件",
variable=self.show_system_var,
command=self.refresh_view
).pack(side=tk.LEFT, padx=5)
tk.Checkbutton(
filter_frame,
text="显示系统目录",
variable=self.show_special_var,
command=self.refresh_view
).pack(side=tk.LEFT, padx=5)
# 创建主分割面板
main_paned = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=4)
main_paned.pack(fill=tk.BOTH, expand=True)
# ========== 左侧目录树 ==========
left_frame = tk.Frame(main_paned)
main_paned.add(left_frame, width=400)
tree_frame = tk.LabelFrame(left_frame, text="目录结构")
tree_frame.pack(fill=tk.BOTH, expand=True)
# 创建树视图
tree_scroll = ttk.Scrollbar(tree_frame, orient="vertical")
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.tree = ttk.Treeview(
tree_frame,
columns=('Name', 'Size', 'Cluster', 'Created', 'Modified'),
show='tree headings',
yscrollcommand=tree_scroll.set
)
tree_scroll.config(command=self.tree.yview)
# 配置列
self.tree.heading('#0', text='类型', anchor=tk.W)
self.tree.heading('Name', text='文件名', anchor=tk.W)
self.tree.heading('Size', text='大小', anchor=tk.W)
self.tree.heading('Cluster', text='起始簇', anchor=tk.W)
self.tree.heading('Created', text='创建时间', anchor=tk.W)
self.tree.heading('Modified', text='修改时间', anchor=tk.W)
self.tree.column('#0', width=60, minwidth=60)
self.tree.column('Name', width=250, minwidth=150)
self.tree.column('Size', width=100, minwidth=80)
self.tree.column('Cluster', width=80, minwidth=60)
self.tree.column('Created', width=180, minwidth=120)
self.tree.column('Modified', width=180, minwidth=120)
self.tree.pack(fill=tk.BOTH, expand=True)
# ========== 右侧内容区域 ==========
right_frame = tk.Frame(main_paned)
main_paned.add(right_frame)
# 创建垂直分割面板
right_paned = tk.PanedWindow(right_frame, orient=tk.VERTICAL, sashrelief=tk.RAISED, sashwidth=4)
right_paned.pack(fill=tk.BOTH, expand=True)
# 磁盘布局可视化
cluster_frame = tk.LabelFrame(right_paned, text="磁盘布局可视化")
right_paned.add(cluster_frame, height=450)
# 创建画布和滚动条
canvas_frame = tk.Frame(cluster_frame)
canvas_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
h_scroll = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL)
v_scroll = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL)
self.canvas = tk.Canvas(
canvas_frame,
bg='white',
xscrollcommand=h_scroll.set,
yscrollcommand=v_scroll.set
)
h_scroll.pack(side=tk.BOTTOM, fill=tk.X)
v_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
h_scroll.config(command=self.canvas.xview)
v_scroll.config(command=self.canvas.yview)
# 文件内容/详细信息
content_frame = tk.LabelFrame(right_paned, text="文件内容/详细信息")
right_paned.add(content_frame)
# 创建标签页
notebook_frame = tk.Frame(content_frame)
notebook_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.notebook = ttk.Notebook(notebook_frame)
self.notebook.pack(fill=tk.BOTH, expand=True)
# 文件内容标签页
self.content_tab = ttk.Frame(self.notebook)
self.notebook.add(self.content_tab, text="文件内容")
text_frame = tk.Frame(self.content_tab)
text_frame.pack(fill=tk.BOTH, expand=True)
text_scroll = ttk.Scrollbar(text_frame)
text_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.text = tk.Text(
text_frame,
wrap=tk.WORD,
yscrollcommand=text_scroll.set,
font=('Consolas', 10)
)
self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
text_scroll.config(command=self.text.yview)
# 磁盘信息标签页
self.info_tab = ttk.Frame(self.notebook)
self.notebook.add(self.info_tab, text="磁盘信息")
info_frame = tk.Frame(self.info_tab)
info_frame.pack(fill=tk.BOTH, expand=True)
info_scroll = ttk.Scrollbar(info_frame)
info_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.info_text = tk.Text(
info_frame,
wrap=tk.WORD,
yscrollcommand=info_scroll.set,
state='disabled',
font=('Consolas', 10)
)
self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
info_scroll.config(command=self.info_text.yview)
# 属性信息标签页
self.attr_tab = ttk.Frame(self.notebook)
self.notebook.add(self.attr_tab, text="属性信息")
attr_frame = tk.Frame(self.attr_tab)
attr_frame.pack(fill=tk.BOTH, expand=True)
attr_scroll = ttk.Scrollbar(attr_frame)
attr_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.attr_text = tk.Text(
attr_frame,
wrap=tk.WORD,
yscrollcommand=attr_scroll.set,
font=('Consolas', 10)
)
self.attr_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
attr_scroll.config(command=self.attr_text.yview)
# 创建状态栏
status_frame = tk.Frame(self.root, height=25)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
self.status_var = tk.StringVar()
self.status_var.set("就绪")
status_bar = tk.Label(
status_frame,
textvariable=self.status_var,
bd=1,
relief=tk.SUNKEN,
anchor=tk.W,
padx=10
)
status_bar.pack(fill=tk.X)
# 初始化数据
self.refresh_data()
# 绑定事件
self.tree.bind("<<TreeviewSelect>>", self._on_tree_select)
self.selected_clusters = []
# 添加右键菜单
self.context_menu = tk.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="查看文件内容", command=self.view_file_content)
self.context_menu.add_command(label="查看簇链", command=self.view_cluster_chain)
self.context_menu.add_command(label="查看属性", command=self.view_attributes)
self.tree.bind("<Button-3>", self.show_context_menu)
def refresh_data(self):
"""刷新数据"""
try:
self.status_var.set("正在读取FAT表...")
self.root.update_idletasks()
# 读取FAT表
self.fat = self.reader.read_fat_table()
self.total_clusters = len(self.fat) // 4
# 获取簇状态
self.cluster_status = self.analyzer.get_cluster_status()
# 填充目录树
self._populate_tree()
# 绘制簇图
self._draw_clusters()
# 更新磁盘信息
self._update_disk_info()
self.status_var.set("就绪")
except Exception as e:
self.status_var.set(f"错误: {str(e)}")
messagebox.showerror("初始化错误", f"无法读取FAT表: {str(e)}")
traceback.print_exc()
def refresh_view(self):
"""刷新视图"""
try:
self.status_var.set("刷新中...")
self.root.update_idletasks()
# 重新读取FAT表
self.fat = self.reader.read_fat_table()
self.total_clusters = len(self.fat) // 4
self.cluster_status = self.analyzer.get_cluster_status()
# 重新填充目录树
self._populate_tree()
# 重新绘制簇图
self._draw_clusters()
# 更新磁盘信息
self._update_disk_info()
# 清空文件内容显示
self.text.delete('1.0', tk.END)
self.attr_text.delete('1.0', tk.END)
self.status_var.set("刷新完成")
except Exception as e:
self.status_var.set(f"刷新失败: {str(e)}")
messagebox.showerror("刷新错误", str(e))
traceback.print_exc()
def _update_disk_info(self):
"""更新磁盘信息标签页内容"""
info_text = f"""FAT32 文件系统信息
卷标: {self.reader.get_volume_label()}
每扇区字节数: {self.reader.bytes_per_sector}
每簇扇区数: {self.reader.sectors_per_cluster}
簇大小: {self.reader.sectors_per_cluster * self.reader.bytes_per_sector} 字节
保留扇区数: {self.reader.reserved_sector_count}
FAT表数量: {self.reader.num_fats}
每FAT表扇区数: {self.reader.sectors_per_fat}
根目录起始簇号: {self.reader.root_cluster}
总扇区数: {self.reader.total_sectors_32}
总簇数: {self.analyzer.total_clusters}
FAT表偏移: 0x{self.reader.fat_offset:X}
数据区偏移: 0x{self.reader.data_offset:X}
空闲簇数: {self.cluster_status.count(0)}
已分配簇数: {self.cluster_status.count(1)}
保留簇数: {self.cluster_status.count(3)}
"""
self.info_text.config(state='normal')
self.info_text.delete('1.0', tk.END)
self.info_text.insert(tk.END, info_text)
self.info_text.config(state='disabled')
def _populate_tree(self):
"""填充目录树"""
self.tree.delete(*self.tree.get_children())
self._add_dir(self.reader.root_cluster, '', "根目录", visited=set())
def _add_dir(self, cluster, parent, name, visited=None):
"""递归添加目录和文件到树中"""
if visited is None:
visited = set()
if cluster in visited:
return
visited.add(cluster)
# 添加目录节点
node = self.tree.insert(
parent,
'end',
text="📁",
open=True,
values=("目录", name, "", cluster, "", "")
)
# 读取目录内容
try:
# 根据用户设置决定是否包含特殊目录
include_special = self.show_special_var.get()
entries = self.analyzer.read_directory(cluster, include_special=include_special)
for entry in entries:
# 应用过滤设置
if not self.show_hidden_var.get() and entry.get('is_hidden', False):
continue
if not self.show_system_var.get() and entry.get('is_system', False):
continue
if entry['is_dir']:
# 添加子目录
self._add_dir(entry['start_cluster'], node, entry['name'], visited)
else:
# 添加文件
size_str = self._format_size(entry['size'])
self.tree.insert(
node,
'end',
text="📄",
values=(
"文件",
entry['name'],
size_str,
entry['start_cluster'],
entry['created'],
entry['modified']
)
)
except Exception as e:
print(f"读取目录错误: {str(e)}")
self.tree.insert(
node,
'end',
text="⚠️",
values=("错误", f"读取错误: {str(e)}", "", "", "", "")
)
def _format_size(self, size):
"""格式化文件大小 - 确保单位正确"""
if size < 1024:
return f"{size} B"
elif size < 1024 * 1024:
return f"{size/1024:.2f} KB"
else:
return f"{size/(1024*1024):.2f} MB"
def _draw_clusters(self, highlight_clusters=None):
"""绘制簇状态可视化 - 优化布局"""
self.canvas.delete('all')
cols = 80
size = 8
pad = 1
max_show = min(len(self.cluster_status), 8000) # 最多显示8000个簇
# 添加标题
self.canvas.create_text(
10, 10,
anchor='nw',
text=f"簇状态可视化 (显示前 {max_show} 个簇)",
font=('Arial', 10, 'bold'),
fill="#333"
)
# 计算最大行数
max_rows = (max_show + cols - 1) // cols
# 绘制簇
for i in range(max_show):
row = i // cols
col = i % cols
x0 = col * (size + pad) + 10
y0 = row * (size + pad) + 40
status = self.cluster_status[i]
if status == 0:
color = '#4CAF50' # 空闲 - 绿色
elif status == 1:
color = '#F44336' # 已分配 - 红色
elif status == 2:
color = '#FF9800' # 结束簇 - 橙色
else:
color = '#9E9E9E' # 保留簇 - 灰色
if highlight_clusters and i in highlight_clusters:
color = '#2196F3' # 选中文件簇链 - 蓝色
self.canvas.create_rectangle(x0, y0, x0+size, y0+size, fill=color, outline=color)
# 将图例放在簇图下方
legend_y = max_rows * (size + pad) + 60
# 添加图例标题
self.canvas.create_text(
10, legend_y - 20,
anchor='nw',
text="图例:",
font=('Arial', 9, 'bold'),
fill="#333"
)
# 添加图例项
legend_items = [
('空闲簇', '#4CAF50'),
('已分配', '#F44336'),
('结束簇', '#FF9800'),
('保留簇', '#9E9E9E'),
('选中文件', '#2196F3')
]
for idx, (text, color) in enumerate(legend_items):
x_pos = 10 + idx * 120
self.canvas.create_rectangle(
x_pos, legend_y,
x_pos + 15, legend_y + 15,
fill=color, outline='#333'
)
self.canvas.create_text(
x_pos + 20, legend_y + 7,
anchor='w',
text=text,
font=('Arial', 9),
fill="#333"
)
# 添加统计信息
stats_text = f"总簇数: {len(self.cluster_status)} | " \
f"空闲簇: {self.cluster_status.count(0)} | " \
f"已分配簇: {self.cluster_status.count(1)} | " \
f"保留簇: {self.cluster_status.count(3)}"
self.canvas.create_text(
10, legend_y + 30,
anchor='nw',
text=stats_text,
font=('Arial', 9),
fill="#333"
)
# 设置画布滚动区域
self.canvas.config(scrollregion=(0, 0, 700, legend_y + 60))
def _on_tree_select(self, event):
"""处理目录树选择事件"""
item = self.tree.selection()
if not item:
return
node = item[0]
values = self.tree.item(node, 'values')
# 如果没有值,跳过
if not values:
return
# 如果是文件节点
if values[0] == "文件":
file_name = values[1]
size_str = values[2]
start_cluster = int(values[3])
size = self._parse_size(size_str)
# 获取文件簇链
clusters = self.analyzer.get_file_clusters(start_cluster)
self.selected_clusters = clusters
# 高亮显示文件簇链
highlight_clusters = [c for c in clusters if c < 8000] # 只高亮前8000个簇
self._draw_clusters(highlight_clusters=highlight_clusters)
# 显示文件信息
self.text.config(state='normal')
self.text.delete('1.0', tk.END)
self.text.insert(tk.END, f"文件信息: {file_name}\n")
self.text.insert(tk.END, "-" * 80 + "\n")
self.text.insert(tk.END, f"大小: {size_str}\n")
self.text.insert(tk.END, f"起始簇: {start_cluster}\n")
self.text.insert(tk.END, f"簇链: {clusters}\n")
self.text.insert(tk.END, f"创建时间: {values[4]}\n")
self.text.insert(tk.END, f"修改时间: {values[5]}\n")
self.text.insert(tk.END, "-" * 80 + "\n\n")
# 读取文件内容
try:
data = self._read_file_content(clusters, size)
# 尝试解码为文本
try:
text_content = data.decode('utf-8', errors='replace')
self.text.insert(tk.END, "文件内容:\n")
self.text.insert(tk.END, "-" * 80 + "\n")
self.text.insert(tk.END, text_content)
except UnicodeDecodeError:
# 显示十六进制预览
self.text.insert(tk.END, "十六进制预览 (仅显示前1KB):\n")
self.text.insert(tk.END, "-" * 80 + "\n")
hex_view = ""
for i in range(0, min(len(data), 1024), 16):
chunk = data[i:i+16]
hexstr = ' '.join(f'{b:02X}' for b in chunk)
asciistr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
hex_view += f"{i:04X}: {hexstr.ljust(47)} {asciistr}\n"
self.text.insert(tk.END, hex_view)
except Exception as e:
self.text.insert(tk.END, f"读取文件内容失败: {str(e)}")
# 切换到文件内容标签页
self.notebook.select(self.content_tab)
else:
self.selected_clusters = []
self._draw_clusters()
self.text.config(state='normal')
self.text.delete('1.0', tk.END)
self.text.insert(tk.END, f"目录信息: {values[1]}\n")
self.text.insert(tk.END, f"起始簇: {values[3] if values else '未知'}\n")
self.text.config(state='disabled')
def _parse_size(self, size_str):
"""解析文件大小字符串为字节数"""
if 'KB' in size_str:
return int(float(size_str.replace(' KB', '')) * 1024)
elif 'MB' in size_str:
return int(float(size_str.replace(' MB', '')) * 1024 * 1024)
elif 'B' in size_str:
return int(size_str.replace(' B', ''))
return 0
def _read_file_content(self, clusters, size):
"""读取文件内容"""
data = b''
for c in clusters:
data += self.reader.read_cluster(c)
if len(data) >= size:
break
return data[:size]
def show_context_menu(self, event):
"""显示右键上下文菜单"""
item = self.tree.identify_row(event.y)
if item:
self.tree.selection_set(item)
self.context_menu.post(event.x_root, event.y_root)
def view_file_content(self):
"""查看文件内容(已通过树选择实现)"""
pass
def view_cluster_chain(self):
"""查看簇链(已通过树选择实现)"""
pass
def view_attributes(self):
"""查看文件属性"""
item = self.tree.selection()
if not item:
return
node = item[0]
values = self.tree.item(node, 'values')
if not values or values[0] != "文件":
return
self.attr_text.delete('1.0', tk.END)
self.attr_text.insert(tk.END, f"文件属性: {values[1]}\n\n")
# 显示文件在树中的详细信息
self.attr_text.insert(tk.END, "基本信息:\n")
self.attr_text.insert(tk.END, f"文件名: {values[1]}\n")
self.attr_text.insert(tk.END, f"大小: {values[2]}\n")
self.attr_text.insert(tk.END, f"起始簇: {values[3]}\n")
self.attr_text.insert(tk.END, f"创建时间: {values[4]}\n")
self.attr_text.insert(tk.END, f"修改时间: {values[5]}\n\n")
# 获取文件簇链
clusters = self.analyzer.get_file_clusters(int(values[3]))
self.attr_text.insert(tk.END, f"簇链 ({len(clusters)} 簇):\n")
self.attr_text.insert(tk.END, f"{clusters}\n")
# 切换到属性标签页
self.notebook.select(self.attr_tab)
def create_file_dialog(self):
"""创建新文件对话框"""
filename = simpledialog.askstring("新建文件", "输入文件名(支持长文件名):")
if not filename:
return
# 检查文件名是否包含非法字符
invalid_chars = ['\\', '/', ':', '*', '?', '"', '<', '>', '|']
if any(char in filename for char in invalid_chars):
messagebox.showerror("错误", f"文件名包含非法字符: {''.join(invalid_chars)}")
return
content = simpledialog.askstring("新建文件", "输入文件内容:")
if content is None:
return
try:
self.status_var.set(f"正在写入文件 {filename}...")
self.root.update_idletasks()
# 写入文件
clusters = self.reader.write_file_to_root(filename, content.encode('utf-8'))
self.status_var.set(f"文件 {filename} 写入成功,刷新中...")
self.root.update_idletasks()
# 刷新视图
self.refresh_data()
messagebox.showinfo("成功", f"文件 {filename} 已成功写入根目录!")
self.status_var.set("就绪")
# 高亮显示新文件的簇链
highlight_clusters = [c for c in clusters if c < 8000]
self._draw_clusters(highlight_clusters=highlight_clusters)
except Exception as e:
self.status_var.set(f"写入失败: {str(e)}")
messagebox.showerror("写入失败", str(e))
traceback.print_exc()
def run(self):
"""启动GUI主循环"""
self.root.mainloop()
if __name__ == "__main__":
# 获取设备路径
device_path = input("请输入U盘设备路径(例如: /dev/sdb1 或 \\\\.\\PhysicalDrive1): ")
try:
# 创建FAT32读写器
writer = FAT32Writer(device_path)
# 创建并运行GUI
gui = FAT32GUI(writer)
gui.run()
# 关闭资源
writer.close()
except Exception as e:
print(f"发生错误: {str(e)}")
traceback.print_exc()
input("按回车键退出..."输出界面数据填充有误
最新发布