import os
import glob
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from datetime import datetime, timezone
import requests
import json
import threading
import shutil
class BatchRenamerPro:
def __init__(self, root):
self.root = root
self.root.title("Batch File Renamer Pro - 三松集团专业批量文件重命名带撤销功能(强哥出品)")
self.root.geometry("650x800")
self.root.resizable(False, False) # 固定窗口大小,不可调整
# 美化:设置主题和样式
style = ttk.Style()
style.theme_use('clam') # 使用clam主题,美观现代
style.configure('Title.TLabel', font=('Arial', 14, 'bold'), background='white')
style.configure('Header.TLabel', font=('Arial', 12, 'bold'), foreground='#2E5C8A')
style.configure('TButton', font=('Arial', 10), padding=10)
style.configure('Accent.TButton', font=('Arial', 10, 'bold'), foreground='white', background='#4CAF50')
style.configure('TCheckbutton', font=('Arial', 10)) # 在style中配置字体
# 背景色
self.root.configure(bg='white')
# 截止日期:2025-12-31
self.expiry_date = datetime(2025, 12, 31, 23, 59, 59, tzinfo=timezone.utc)
# 重命名历史,用于撤销
self.rename_history = []
self.backup_folder = None
# 检查许可证
if not self.check_license():
self.root.destroy()
return True
self.setup_ui()
def check_license(self):
"""检查许可证:尝试从互联网获取当前UTC时间比对截止日期,若失败则使用本地日期"""
try:
# 尝试从网络获取时间
response = requests.get('http://worldtimeapi.org/api/timezone/Etc/UTC', timeout=5)
if response.status_code == 200:
data = response.json()
current_utc = datetime.fromisoformat(data['datetime'].replace('Z', '+00:00'))
else:
raise ValueError("无法从互联网获取时间")
except (requests.RequestException, json.JSONDecodeError, ValueError):
# 如果网络获取失败,使用本地时间
current_utc = datetime.now(timezone.utc)
# 比较当前时间和过期日期
if current_utc > self.expiry_date:
messagebox.showerror("软件内部错误",
"软件内部错误,请联系管理员获取更新版本。\nEmail: lyt@singsong.com.cn")
return False
return True
def setup_ui(self):
"""设置美观的GUI界面"""
# 标题
title_frame = tk.Frame(self.root, bg='white', height=50)
title_frame.pack(fill=tk.X, pady=(0, 10))
title_frame.pack_propagate(False)
ttk.Label(title_frame, text="Batch File Renamer Pro", style='Title.TLabel').pack(expand=True)
ttk.Label(title_frame, text="专业批量文件重命名工具 | v1.0 | © 2025 SINGSONG Studio", font=('Arial', 8),
foreground='#888888').pack()
# 主容器,使用Notebook分tab?不,为简单用frame分组
main_container = ttk.Frame(self.root, padding="20", relief='raised', borderwidth=1)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# 输入组
input_frame = ttk.LabelFrame(main_container, text="输入设置", padding="10")
input_frame.pack(fill=tk.X, pady=(0, 10))
# 文件夹选择
ttk.Label(input_frame, text="选择文件夹:", font=('Arial', 10)).grid(row=0, column=0, sticky=tk.W, pady=5)
self.folder_var = tk.StringVar(value=os.getcwd()) # 默认显示当前工作目录
folder_entry = ttk.Entry(input_frame, textvariable=self.folder_var, width=50, font=('Arial', 9))
folder_entry.grid(row=0, column=1, padx=(10, 5), pady=5)
ttk.Button(input_frame, text="浏览", command=self.browse_folder, style='TButton').grid(row=0, column=2, pady=5)
# 添加文本
ttk.Label(input_frame, text="添加/替换文本:", font=('Arial', 10)).grid(row=1, column=0, sticky=tk.W, pady=5)
self.add_text_var = tk.StringVar(value="-Glitter")
ttk.Entry(input_frame, textvariable=self.add_text_var, width=50, font=('Arial', 9)).grid(row=1, column=1,
padx=(10, 5), pady=5)
# 添加位置
ttk.Label(input_frame, text="操作位置:", font=('Arial', 10)).grid(row=2, column=0, sticky=tk.W, pady=5)
self.position_var = tk.StringVar(value="后缀")
position_combo = ttk.Combobox(input_frame, textvariable=self.position_var, values=["前缀", "后缀", "替换"],
state="readonly", width=47, font=('Arial', 9))
position_combo.grid(row=2, column=1, padx=(10, 5), pady=5)
# 文件类型过滤
ttk.Label(input_frame, text="文件类型过滤:", font=('Arial', 10)).grid(row=3, column=0, sticky=tk.W, pady=5)
self.file_type_var = tk.StringVar(value="*.jpg")
ttk.Entry(input_frame, textvariable=self.file_type_var, width=50, font=('Arial', 9)).grid(row=3, column=1,
padx=(10, 5), pady=5)
ttk.Label(input_frame, text="(支持通配符,如 *.jpg;*.png)", font=('Arial', 8), foreground='#666666').grid(row=4,
column=1,
sticky=tk.W,
pady=(0, 5))
# 选项组
options_frame = ttk.LabelFrame(main_container, text="高级选项", padding="10")
options_frame.pack(fill=tk.X, pady=(0, 10))
self.recursive_var = tk.BooleanVar(value=True) # 默认不选中
ttk.Checkbutton(options_frame, text="递归处理子文件夹", variable=self.recursive_var, style='TCheckbutton').grid(
row=0, column=0, sticky=tk.W, pady=5)
self.preview_var = tk.BooleanVar(value=False) # 默认不选中
ttk.Checkbutton(options_frame, text="预览模式(不实际执行)", variable=self.preview_var,
style='TCheckbutton').grid(
row=1, column=0, sticky=tk.W, pady=5)
self.backup_var = tk.BooleanVar(value=False) # 默认不选中
ttk.Checkbutton(options_frame, text="启用备份(支持撤销)", variable=self.backup_var, style='TCheckbutton').grid(
row=2, column=0, sticky=tk.W, pady=5)
# 控制按钮组
control_frame = ttk.Frame(main_container)
control_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(control_frame, text="执行重命名", command=self.run_rename, style='Accent.TButton').pack(side=tk.LEFT,
padx=(0, 10),
pady=10)
# 移除撤销最后操作按钮
self.undo_all_button = ttk.Button(control_frame, text="撤销所有", command=self.undo_all, state='disabled',
style='TButton')
self.undo_all_button.pack(side=tk.LEFT, padx=5, pady=10)
# 日志输出组
log_frame = ttk.LabelFrame(main_container, text="操作日志", padding="5")
log_frame.pack(fill=tk.BOTH, expand=True)
self.log_text = scrolledtext.ScrolledText(log_frame, width=70, height=20, font=('Consolas', 9), bg='#f9f9f9',
fg='#333333') # 增加日志窗口的高度
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 菜单栏(保持)
menubar = tk.Menu(self.root, bg='white', fg='black', font=('Arial', 9))
self.root.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0, bg='white', fg='black')
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="退出", command=self.root.quit)
help_menu = tk.Menu(menubar, tearoff=0, bg='white', fg='black')
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="关于", command=self.show_about)
help_menu.add_command(label="帮助", command=self.show_help)
def browse_folder(self):
"""浏览选择文件夹"""
folder = filedialog.askdirectory()
if folder:
self.folder_var.set(folder)
def log(self, message):
"""添加日志消息"""
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
self.log_text.see(tk.END) # 自动滚动到最底部
self.root.update_idletasks()
def show_about(self):
"""关于对话框"""
messagebox.showinfo("关于 Batch File Renamer Pro",
"Batch File Renamer Pro v1.0\n\n专业批量文件重命名工具\n\n开发者: 三松集团强哥出品\n支持多平台批量操作,提升生产力!")
def show_help(self):
"""帮助对话框"""
help_text = """
使用指南:
1. 点击'浏览'选择目标文件夹。
2. 输入要添加的文本(如 'Glitter')。
3. 选择添加位置:前缀(开头)、后缀(结尾)、替换(替换原名)。
4. 指定文件类型(如 *.jpg)。
5. 勾选选项:递归子文件夹、预览模式、备份。
6. 点击'执行重命名'开始。
7. 执行后,可使用'撤销所有'恢复。
注意:
- 预览模式下仅显示变化,不实际修改文件。
- 备份将创建 'backup_[日期]' 文件夹,支持撤销。
"""
messagebox.showinfo("帮助", help_text)
def run_rename(self):
"""执行重命名(在子线程中)"""
if not self.folder_var.get().strip():
messagebox.showwarning("警告", "请选择文件夹!")
return
# 清除日志和历史
self.log_text.delete(1.0, tk.END)
self.rename_history = []
self.undo_all_button.config(state='disabled') # 默认禁用撤销所有按钮
# 创建备份文件夹如果启用
if self.backup_var.get():
self.backup_folder = os.path.join(self.folder_var.get(),
f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}")
os.makedirs(self.backup_folder, exist_ok=True)
self.log(f"创建备份文件夹: {self.backup_folder}")
# 在子线程运行以避免UI冻结
thread = threading.Thread(target=self._perform_rename)
thread.daemon = True
thread.start()
def _perform_rename(self):
"""实际执行重命名逻辑"""
folder = self.folder_var.get()
add_text = self.add_text_var.get().strip()
position = self.position_var.get()
file_types = [ft.strip() for ft in self.file_type_var.get().strip().split(';') if ft.strip()]
recursive = self.recursive_var.get()
preview = self.preview_var.get()
if not add_text or not file_types:
self.log("错误: 添加文本或文件类型不能为空!")
return
processed = 0
skipped = 0
try:
if recursive:
for root, dirs, files in os.walk(folder):
for file_type in file_types:
pattern = os.path.join(root, file_type)
matches = glob.glob(pattern, recursive=True)
for file_path in matches:
self._rename_single(file_path, add_text, position, preview, processed, skipped)
processed += 1
else:
for file_type in file_types:
matches = glob.glob(os.path.join(folder, file_type))
for file_path in matches:
self._rename_single(file_path, add_text, position, preview, processed, skipped)
processed += 1
self.log(f"批量重命名完成!处理: {processed}, 跳过: {skipped}")
if self.rename_history:
self.undo_all_button.config(state='normal') # 启用撤销所有按钮
if preview:
messagebox.showinfo("预览完成", "预览模式:文件未实际修改。取消预览以执行真实重命名。")
except Exception as e:
self.log(f"错误: {str(e)}")
messagebox.showerror("执行错误", str(e))
def _rename_single(self, file_path, add_text, position, preview, processed, skipped):
"""处理单个文件重命名"""
file_name = os.path.basename(file_path)
base_name, ext = os.path.splitext(file_name)
if position == "前缀":
new_name = add_text + base_name + ext
elif position == "替换":
new_name = add_text + ext
else: # 后缀
new_name = base_name + add_text + ext
dir_name = os.path.dirname(file_path)
new_path = os.path.join(dir_name, new_name)
if os.path.exists(new_path):
self.log(f"跳过 {file_name}:新文件名 {new_name} 已存在")
skipped += 1
else:
if preview:
self.log(f"预览: {file_name} → {new_name}")
else:
old_path = file_path
# 备份如果启用
if self.backup_var.get() and self.backup_folder:
backup_path = os.path.join(self.backup_folder, file_name)
shutil.copy2(old_path, backup_path)
self.log(f"备份: {file_name} 到 {self.backup_folder}")
os.rename(old_path, new_path)
self.rename_history.append((old_path, new_path)) # 记录用于撤销
self.log(f"重命名: {file_name} → {new_name}")
def undo_all(self):
"""撤销所有重命名"""
if not self.rename_history:
messagebox.showwarning("警告", "无操作可撤销!")
return
if messagebox.askyesno("确认撤销", "撤销所有重命名操作?这将恢复所有文件。"):
while self.rename_history:
old_path, new_path = self.rename_history.pop()
try:
os.rename(new_path, old_path)
self.log(f"撤销: {os.path.basename(new_path)} → {os.path.basename(old_path)}")
except Exception as e:
self.log(f"撤销失败: {str(e)}")
messagebox.showerror("撤销错误", str(e))
self.log("所有操作已撤销!")
self.undo_all_button.config(state='disabled') # 撤销所有后禁用撤销按钮
def main():
root = tk.Tk()
app = BatchRenamerPro(root)
root.mainloop()
if __name__ == "__main__":
main()