[Python] 纯文本查看 复制代码
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import ctypes
class BatchRenameApp:
def __init__(self, root):
self.root = root
self.root.title("批量重命名工具")
self.root.geometry("780x780")
self.root.resizable(True, True)
self.root.configure(bg="#f5f5f5")
self.center_window()
self.folder_path = tk.StringVar()
self.prefix = tk.StringVar(value="文件_")
self.sort_type = tk.StringVar(value="name")
self.file_type = tk.StringVar(value="全部")
self.file_list = []
self.log_file = ".rename_log.txt"
self.last_folder_mtime = 0
self.setup_style()
self.create_widgets()
self.start_folder_monitor()
def center_window(self):
self.root.update_idletasks()
w = self.root.winfo_width()
h = self.root.winfo_height()
x = (self.root.winfo_screenwidth() // 2) - (w // 2)
y = (self.root.winfo_screenheight() // 2) - (h // 2)
self.root.geometry(f"{w}x{h}+{x}+{y}")
def setup_style(self):
style = ttk.Style()
style.theme_use('clam')
style.configure('Modern.TButton', font=('微软雅黑', 10), background="#4285F4", foreground="white", padding=8)
style.map('Modern.TButton', background=[('active', '#3367D6')])
style.configure('Modern.TLabel', font=('微软雅黑', 10), background="#f5f5f5")
style.configure('Title.TLabel', font=('微软雅黑', 16, 'bold'), background="#f5f5f5", foreground="#202124")
style.configure('TRadiobutton', font=('微软雅黑', 10), background="#f5f5f5")
style.configure("Treeview.Heading", font=('微软雅黑', 10, 'bold'))
style.configure("Treeview", font=('微软雅黑', 10), rowheight=25)
def create_widgets(self):
title_label = ttk.Label(self.root, text="批量重命名工具", style="Title.TLabel")
title_label.pack(pady=20)
folder_frame = tk.Frame(self.root, bg="#f5f5f5")
folder_frame.pack(fill=tk.X, padx=30, pady=6)
ttk.Label(folder_frame, text="目标文件夹:", style="Modern.TLabel").pack(side=tk.LEFT)
self.folder_entry = ttk.Entry(folder_frame, textvariable=self.folder_path, font=('微软雅黑', 10), width=45)
self.folder_entry.pack(side=tk.LEFT, padx=10)
ttk.Button(folder_frame, text="选择文件夹", command=self.select_folder, style='Modern.TButton').pack(side=tk.LEFT)
prefix_frame = tk.Frame(self.root, bg="#f5f5f5")
prefix_frame.pack(fill=tk.X, padx=30, pady=6)
ttk.Label(prefix_frame, text="文件前缀:", style="Modern.TLabel").pack(side=tk.LEFT)
prefix_entry = ttk.Entry(prefix_frame, textvariable=self.prefix, font=('微软雅黑', 10), width=30)
prefix_entry.pack(side=tk.LEFT, padx=10)
prefix_entry.bind('<KeyRelease>', lambda e: self.update_preview())
sort_frame = tk.Frame(self.root, bg="#f5f5f5")
sort_frame.pack(fill=tk.X, padx=30, pady=6)
ttk.Label(sort_frame, text="排序方式:", style="Modern.TLabel").pack(side=tk.LEFT, padx=0)
ttk.Radiobutton(sort_frame, text="按名称", variable=self.sort_type, value="name", command=self.load_files).pack(side=tk.LEFT, padx=8)
ttk.Radiobutton(sort_frame, text="按修改时间", variable=self.sort_type, value="mtime", command=self.load_files).pack(side=tk.LEFT, padx=8)
type_frame = tk.Frame(self.root, bg="#f5f5f5")
type_frame.pack(fill=tk.X, padx=30, pady=6)
ttk.Label(type_frame, text="文件类型:", style="Modern.TLabel").pack(side=tk.LEFT, padx=0)
ttk.Radiobutton(type_frame, text="全部", variable=self.file_type, value="全部", command=self.load_files).pack(side=tk.LEFT, padx=6)
ttk.Radiobutton(type_frame, text="图片", variable=self.file_type, value="图片", command=self.load_files).pack(side=tk.LEFT, padx=6)
ttk.Radiobutton(type_frame, text="视频", variable=self.file_type, value="视频", command=self.load_files).pack(side=tk.LEFT, padx=6)
ttk.Radiobutton(type_frame, text="文档", variable=self.file_type, value="文档", command=self.load_files).pack(side=tk.LEFT, padx=6)
ttk.Radiobutton(type_frame, text="表格", variable=self.file_type, value="表格", command=self.load_files).pack(side=tk.LEFT, padx=6)
info_frame = tk.Frame(self.root, bg="#f5f5f5")
info_frame.pack(fill=tk.X, padx=30, pady=6)
self.file_count_label = ttk.Label(info_frame, text="文件数量:0 个", style="Modern.TLabel")
self.file_count_label.pack(side=tk.LEFT)
# 表格预览区
preview_frame = tk.LabelFrame(self.root, text="重命名预览", font=('微软雅黑', 11, 'bold'), bg="#f5f5f5", padx=10, pady=10)
preview_frame.pack(fill=tk.BOTH, expand=True, padx=30, pady=10)
self.preview_tree = ttk.Treeview(preview_frame, columns=("原文件名", "新文件名"), show="headings", height=12)
self.preview_tree.heading("原文件名", text="原文件名")
self.preview_tree.heading("新文件名", text="新文件名")
self.preview_tree.column("原文件名", width=380, anchor=tk.W)
self.preview_tree.column("新文件名", width=280, anchor=tk.W)
scrollbar = ttk.Scrollbar(preview_frame, orient=tk.VERTICAL, command=self.preview_tree.yview)
self.preview_tree.config(yscrollcommand=scrollbar.set)
self.preview_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
btn_frame = tk.Frame(self.root, bg="#f5f5f5")
btn_frame.pack(pady=20)
ttk.Button(btn_frame, text="开始重命名", command=self.start_rename, style='Modern.TButton').pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text="撤销重命名", command=self.undo_rename, style='Modern.TButton').pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text="清空重置", command=self.clear_all, style='Modern.TButton').pack(side=tk.LEFT, padx=10)
# 文件夹监控
def start_folder_monitor(self):
self.check_folder_change()
def check_folder_change(self):
folder = self.folder_path.get()
if os.path.isdir(folder):
current_mtime = os.path.getmtime(folder)
if current_mtime != self.last_folder_mtime:
self.last_folder_mtime = current_mtime
self.load_files()
self.root.after(2000, self.check_folder_change)
# 排除日志文件
def match_file_type(self, filename):
if filename == self.log_file:
return False
ext = os.path.splitext(filename)[1].lower()
ftype = self.file_type.get()
image = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
video = ['.mp4', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.rmvb']
doc = ['.doc', '.docx', '.txt', '.pdf', '.ppt', '.pptx']
excel = ['.xls', '.xlsx', '.csv']
if ftype == "全部":
return True
elif ftype == "图片":
return ext in image
elif ftype == "视频":
return ext in video
elif ftype == "文档":
return ext in doc
elif ftype == "表格":
return ext in excel
return False
def select_folder(self):
path = filedialog.askdirectory(title="选择文件夹")
if path:
self.folder_path.set(path)
self.last_folder_mtime = os.path.getmtime(path)
self.load_files()
def load_files(self):
folder = self.folder_path.get()
if not os.path.isdir(folder):
return
self.file_list = []
for name in os.listdir(folder):
fp = os.path.join(folder, name)
if os.path.isfile(fp) and self.match_file_type(name):
self.file_list.append(name)
if self.sort_type.get() == "name":
self.file_list.sort()
else:
self.file_list.sort(key=lambda x: os.path.getmtime(os.path.join(folder, x)))
self.file_count_label.config(text=f"文件数量:{len(self.file_list)} 个")
self.update_preview()
def get_num_format(self, count):
if count < 10:
return "{:01d}"
elif count < 100:
return "{:02d}"
elif count < 1000:
return "{:03d}"
else:
return "{:04d}"
# 表格预览
def update_preview(self):
for item in self.preview_tree.get_children():
self.preview_tree.delete(item)
if not self.file_list:
return
prefix = self.prefix.get()
count = len(self.file_list)
fmt = self.get_num_format(count)
for i, name in enumerate(self.file_list, 1):
ext = os.path.splitext(name)[1]
new_name = f"{prefix}{fmt.format(i)}{ext}"
self.preview_tree.insert("", tk.END, values=(name, new_name))
# =================== 修复权限报错的核心代码 ===================
def start_rename(self):
folder = self.folder_path.get()
if not os.path.isdir(folder):
messagebox.showerror("错误", "请先选择文件夹")
return
if not self.file_list:
messagebox.showwarning("提示", "没有可重命名的文件")
return
prefix = self.prefix.get()
count = len(self.file_list)
fmt = self.get_num_format(count)
log_path = os.path.join(folder, self.log_file)
if not messagebox.askyesno("确认", f"即将重命名 {count} 个文件,是否继续?"):
return
# 先取消隐藏,解决权限问题
try:
ctypes.windll.kernel32.SetFileAttributesW(log_path, 0)
except:
pass
rename_records = []
ok = 0
ng = 0
for i, name in enumerate(self.file_list, 1):
old_path = os.path.join(folder, name)
ext = os.path.splitext(name)[1]
new_name = f"{prefix}{fmt.format(i)}{ext}"
new_path = os.path.join(folder, new_name)
try:
if os.path.exists(new_path):
new_path = os.path.join(folder, f"temp_{fmt.format(i)}{ext}")
os.rename(old_path, new_path)
rename_records.append(f"{name}|{new_name}")
ok += 1
except:
ng += 1
# 写入日志
with open(log_path, 'w', encoding='utf-8') as f:
f.write("\n".join(rename_records))
# 重新设置隐藏
try:
ctypes.windll.kernel32.SetFileAttributesW(log_path, 2)
except:
pass
messagebox.showinfo("完成", f"重命名完成!\n成功:{ok} 个\n失败:{ng} 个")
self.load_files()
# ==============================================================
def undo_rename(self):
folder = self.folder_path.get()
log_path = os.path.join(folder, self.log_file)
if not os.path.exists(log_path):
messagebox.showerror("错误", "未找到重命名记录,无法撤销!")
return
if not messagebox.askyesno("确认", "确定要恢复所有文件的原始名称吗?"):
return
try:
with open(log_path, 'r', encoding='utf-8') as f:
lines = [l.strip() for l in f.readlines() if l.strip()]
except:
messagebox.showerror("错误", "记录文件读取失败")
return
ok = 0
ng = 0
for line in lines:
if "|" not in line:
continue
old_name, new_name = line.split("|", 1)
new_path = os.path.join(folder, new_name)
old_path = os.path.join(folder, old_name)
try:
if os.path.exists(new_path):
os.rename(new_path, old_path)
ok += 1
except:
ng += 1
messagebox.showinfo("完成", f"撤销完成!\n成功恢复:{ok} 个\n失败:{ng} 个")
self.load_files()
def clear_all(self):
self.folder_path.set("")
self.prefix.set("文件_")
self.sort_type.set("name")
self.file_type.set("全部")
self.file_list = []
self.last_folder_mtime = 0
self.file_count_label.config(text="文件数量:0 个")
for item in self.preview_tree.get_children():
self.preview_tree.delete(item)
if __name__ == "__main__":
app = tk.Tk()
window = BatchRenameApp(app)
app.mainloop()