[Python] 纯文本查看 复制代码
import os
import sys
import json
import winreg
import ctypes
import customtkinter as ctk
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
DEFAULT_CONFIG = {
"extensions": ["pdf", "zip", "png", "txt"],
"show_notification": True
}
def show_message(title, message, icon_type=0x40):
ctypes.windll.user32.MessageBoxW(0, message, title, icon_type | 0x00000000)
def get_config_path():
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(base_dir, "config.json")
try:
test_file = os.path.join(base_dir, ".write_test")
with open(test_file, "w") as f:
pass
os.remove(test_file)
return config_file
except (OSError, PermissionError):
appdata_dir = os.path.join(
os.environ.get("LOCALAPPDATA", os.environ.get("APPDATA", "")),
"QuickRename"
)
os.makedirs(appdata_dir, exist_ok=True)
return os.path.join(appdata_dir, "config.json")
def load_config():
config_path = get_config_path()
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
for key, val in DEFAULT_CONFIG.items():
if key not in config:
config[key] = val
return config
except Exception:
return DEFAULT_CONFIG.copy()
return DEFAULT_CONFIG.copy()
def save_config(config):
config_path = get_config_path()
try:
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=4, ensure_ascii=False)
return True
except Exception as e:
show_message("配置保存失败", f"无法写入配置文件,原因:{str(e)}", 0x10)
return False
def get_exe_path():
if getattr(sys, 'frozen', False):
return sys.executable
else:
python_path = sys.executable
script_path = os.path.abspath(sys.argv[0])
return f'"{python_path}" "{script_path}"'
def delete_reg_key_recursive(root_key, subkey_path):
try:
hkey = winreg.OpenKey(root_key, subkey_path, 0, winreg.KEY_ALL_ACCESS)
except FileNotFoundError:
return
try:
while True:
child_key = winreg.EnumKey(hkey, 0)
delete_reg_key_recursive(root_key, f"{subkey_path}\\{child_key}")
except OSError:
pass
winreg.CloseKey(hkey)
winreg.DeleteKey(root_key, subkey_path)
def normalize_ext(suffix):
clean_ext = suffix.strip()
if clean_ext and not clean_ext.startswith('.'):
clean_ext = '.' + clean_ext
return clean_ext
def generate_bat_and_vbs(show_notification):
show_notif_val = "1" if show_notification else "0"
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
bat_path = os.path.join(base_dir, "rename.bat")
vbs_path = os.path.join(base_dir, "rename.vbs")
bat_content = f"""@echo off
chcp 65001 >nul
set "FILE_PATH=%~1"
set "TARGET_EXT=%~2"
set "SHOW_NOTIFY={show_notif_val}"
if not exist "%FILE_PATH%" exit
if "%TARGET_EXT%"=="" exit
set "FILE_DIR=%~dp1"
set "FILE_NAME=%~n1"
if exist "%FILE_PATH%\\" (
goto is_dir
) else (
goto is_file
)
:is_file
set "NEW_NAME=%FILE_NAME%%TARGET_EXT%"
set "COUNTER=1"
:file_loop
if exist "%FILE_DIR%%NEW_NAME%" (
set "NEW_NAME=%FILE_NAME% (%COUNTER%)%TARGET_EXT%"
set /a COUNTER+=1
goto file_loop
)
ren "%FILE_PATH%" "%NEW_NAME%"
goto success
:is_dir
for %%i in ("%FILE_PATH%") do set "DIR_NAME=%%~nxi"
set "NEW_NAME=%DIR_NAME%%TARGET_EXT%"
set "COUNTER=1"
:dir_loop
if exist "%FILE_DIR%%NEW_NAME%" (
set "NEW_NAME=%DIR_NAME% (%COUNTER%)%TARGET_EXT%"
set /a COUNTER+=1
goto dir_loop
)
ren "%FILE_PATH%" "%NEW_NAME%"
goto success
:success
if "%SHOW_NOTIFY%"=="1" (
echo MsgBox "原名称: %~nx1" ^& vbCrLf ^& "新名称: %NEW_NAME%", 64, "修改扩展名成功" > "%temp%\\rename_ok.vbs"
wscript "%temp%\\rename_ok.vbs"
del "%temp%\\rename_ok.vbs"
)
exit
"""
vbs_content = f"""Set shell = CreateObject("WScript.Shell")
shell.Run \"\"\"{bat_path}\"\" \"\"\" & WScript.Arguments(0) & \"\"\" \"\"\" & WScript.Arguments(1) & \"\"\"\", 0, False
"""
try:
with open(bat_path, "w", encoding="gbk", errors="ignore") as f:
f.write(bat_content)
with open(vbs_path, "w", encoding="gbk", errors="ignore") as f:
f.write(vbs_content)
return bat_path, vbs_path
except Exception as e:
show_message("脚本生成失败", f"无法写入通用批处理处理器,原因:{str(e)}", 0x10)
return None, None
def register_context_menu(extensions, show_notification):
res = generate_bat_and_vbs(show_notification)
if not res:
return False
_, vbs_path = res
valid_exts = [e.strip() for e in extensions if e.strip()]
reg_paths = [
r"Software\Classes\*\shell\QuickRename",
r"Software\Classes\Directory\shell\QuickRename"
]
try:
exe_path = sys.executable if getattr(sys, 'frozen', False) else None
for reg_path in reg_paths:
try:
delete_reg_key_recursive(winreg.HKEY_CURRENT_USER, reg_path)
except Exception:
pass
if len(valid_exts) == 1:
clean_ext = normalize_ext(valid_exts[0])
menu_name = f"修改扩展名为 {clean_ext}"
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE) as hkey:
winreg.SetValueEx(hkey, "", 0, winreg.REG_SZ, menu_name)
if exe_path:
winreg.SetValueEx(hkey, "Icon", 0, winreg.REG_SZ, exe_path)
cmd_path = f"{reg_path}\\command"
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, cmd_path, 0, winreg.KEY_WRITE) as hkey_cmd:
cmd_value = f'wscript.exe "{vbs_path}" "%1" "{clean_ext}"'
winreg.SetValueEx(hkey_cmd, "", 0, winreg.REG_SZ, cmd_value)
else:
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE) as hkey:
winreg.SetValueEx(hkey, "MUIVerb", 0, winreg.REG_SZ, "快速修改扩展名")
winreg.SetValueEx(hkey, "SubCommands", 0, winreg.REG_SZ, "")
if exe_path:
winreg.SetValueEx(hkey, "Icon", 0, winreg.REG_SZ, exe_path)
shell_path = f"{reg_path}\\shell"
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, shell_path, 0, winreg.KEY_WRITE) as hkey_shell:
for ext in valid_exts:
clean_ext = normalize_ext(ext)
ext_key_name = ext.replace('.', '')
if not ext_key_name:
continue
item_path = f"{shell_path}\\{ext_key_name}"
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, item_path, 0, winreg.KEY_WRITE) as hkey_item:
winreg.SetValueEx(hkey_item, "", 0, winreg.REG_SZ, f"转换为 {clean_ext}")
cmd_path = f"{item_path}\\command"
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, cmd_path, 0, winreg.KEY_WRITE) as hkey_cmd:
cmd_value = f'wscript.exe "{vbs_path}" "%1" "{clean_ext}"'
winreg.SetValueEx(hkey_cmd, "", 0, winreg.REG_SZ, cmd_value)
return True
except Exception as e:
show_message("注册表写入失败", f"自适应菜单集成失败,原因:{str(e)}", 0x10)
return False
def unregister_context_menu():
reg_paths = [
r"Software\Classes\*\shell\QuickRename",
r"Software\Classes\Directory\shell\QuickRename"
]
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
bat_path = os.path.join(base_dir, "rename.bat")
vbs_path = os.path.join(base_dir, "rename.vbs")
try:
for reg_path in reg_paths:
delete_reg_key_recursive(winreg.HKEY_CURRENT_USER, reg_path)
if os.path.exists(bat_path):
os.remove(bat_path)
if os.path.exists(vbs_path):
os.remove(vbs_path)
return True
except Exception as e:
show_message("注册表清理失败", f"移除右键菜单失败,原因:{str(e)}", 0x10)
return False
def check_registry_status(extensions):
valid_exts = [e.strip() for e in extensions if e.strip()]
if not valid_exts:
return "inactive"
try:
key_path = r"Software\Classes\*\shell\QuickRename"
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_READ) as hkey:
if len(valid_exts) == 1:
# 检查一级菜单形式
try:
val, _ = winreg.QueryValueEx(hkey, "")
clean_ext = normalize_ext(valid_exts[0])
expected_val = f"修改扩展名为 {clean_ext}"
try:
winreg.QueryValueEx(hkey, "SubCommands")
return "mismatch"
except FileNotFoundError:
pass
if val == expected_val:
return "active"
else:
return "mismatch"
except FileNotFoundError:
return "mismatch"
else:
# 检查级联二级菜单形式
try:
winreg.QueryValueEx(hkey, "SubCommands")
except FileNotFoundError:
return "mismatch"
shell_path = r"Software\Classes\*\shell\QuickRename\shell"
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, shell_path, 0, winreg.KEY_READ) as hkey_shell:
reg_items = []
try:
i = 0
while True:
subkey = winreg.EnumKey(hkey_shell, i)
reg_items.append(subkey)
i += 1
except OSError:
pass
expected_items = [e.replace('.', '') for e in valid_exts if e.replace('.', '')]
if set(reg_items) == set(expected_items):
return "active"
else:
return "mismatch"
except FileNotFoundError:
return "mismatch"
except FileNotFoundError:
return "inactive"
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("快捷修改扩展名配置工具")
self.geometry("480x520")
self.resizable(False, False)
self.center_window()
self.config = load_config()
self.create_widgets()
self.render_list()
self.update_status()
def center_window(self):
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f'+{x}+{y}')
def create_widgets(self):
self.title_frame = ctk.CTkFrame(self, fg_color="transparent")
self.title_frame.pack(fill="x", padx=30, pady=(15, 8))
self.main_title = ctk.CTkLabel(
self.title_frame,
text="⚡ 快捷修改扩展名配置",
font=ctk.CTkFont(family="Microsoft YaHei", size=18, weight="bold")
)
self.main_title.pack(anchor="w")
self.sub_title = ctk.CTkLabel(
self.title_frame,
text="设置常用扩展名列表,并集成到 Windows 右键二级菜单中",
font=ctk.CTkFont(family="Microsoft YaHei", size=11),
text_color="#8AB4F8"
)
self.sub_title.pack(anchor="w", pady=(1, 0))
self.card_frame = ctk.CTkFrame(self, border_width=1, border_color="#3c4043")
self.card_frame.pack(fill="x", padx=30, pady=3)
self.add_frame = ctk.CTkFrame(self.card_frame, fg_color="transparent")
self.add_frame.pack(fill="x", padx=15, pady=(10, 8))
self.add_entry = ctk.CTkEntry(
self.add_frame,
width=230,
placeholder_text="输入新增后缀,例如: png, zip, pdf",
font=ctk.CTkFont(family="Microsoft YaHei", size=12)
)
self.add_entry.pack(side="left", padx=(0, 10))
self.add_entry.bind("<Return>", lambda e: self.on_add_click())
self.add_btn = ctk.CTkButton(
self.add_frame,
text="添加项目",
fg_color="#1a73e8",
hover_color="#1557b0",
width=90,
font=ctk.CTkFont(family="Microsoft YaHei", size=12, weight="bold"),
command=self.on_add_click
)
self.add_btn.pack(side="right", fill="x", expand=True)
self.scroll_frame = ctk.CTkScrollableFrame(
self.card_frame,
label_text="已保存的常用扩展名列表",
label_font=ctk.CTkFont(family="Microsoft YaHei", size=12, weight="bold"),
border_width=1,
border_color="#3c4043",
height=100
)
self.scroll_frame.pack(fill="x", padx=15, pady=(0, 8))
self.switch_frame = ctk.CTkFrame(self.card_frame, fg_color="transparent")
self.switch_frame.pack(fill="x", padx=15, pady=(0, 8))
self.notif_switch = ctk.CTkSwitch(
self.switch_frame,
text="在修改完成后显示气泡提示框",
font=ctk.CTkFont(family="Microsoft YaHei", size=12)
)
if self.config.get("show_notification", True):
self.notif_switch.select()
self.notif_switch.pack(anchor="w")
self.status_frame = ctk.CTkFrame(self, fg_color="transparent")
self.status_frame.pack(fill="x", padx=30, pady=(8, 5))
self.status_indicator = ctk.CTkLabel(
self.status_frame,
text="状态读取中...",
font=ctk.CTkFont(family="Microsoft YaHei", size=12, weight="bold")
)
self.status_indicator.pack(anchor="w")
self.btn_frame = ctk.CTkFrame(self, fg_color="transparent")
self.btn_frame.pack(fill="x", padx=30, pady=(5, 10))
self.uninstall_btn = ctk.CTkButton(
self.btn_frame,
text="卸载右键菜单",
fg_color="#3c4043",
hover_color="#5f6368",
width=150,
height=36,
font=ctk.CTkFont(family="Microsoft YaHei", size=13, weight="bold"),
command=self.on_uninstall
)
self.uninstall_btn.pack(side="left")
self.save_btn = ctk.CTkButton(
self.btn_frame,
text="保存并启用右键菜单",
fg_color="#1a73e8",
hover_color="#1557b0",
width=250,
height=36,
font=ctk.CTkFont(family="Microsoft YaHei", size=13, weight="bold"),
command=self.on_save
)
self.save_btn.pack(side="right")
def render_list(self):
for widget in self.scroll_frame.winfo_children():
widget.destroy()
extensions = self.config.get("extensions", [])
for ext in extensions:
clean_ext = normalize_ext(ext)
row_frame = ctk.CTkFrame(self.scroll_frame, fg_color="transparent")
row_frame.pack(fill="x", padx=5, pady=3)
lbl = ctk.CTkLabel(
row_frame,
text=f" 🏷️ {clean_ext}",
font=ctk.CTkFont(family="Microsoft YaHei", size=13, weight="bold")
)
lbl.pack(side="left")
del_btn = ctk.CTkButton(
row_frame,
text="🗑️ 删除",
width=65,
height=22,
fg_color="#3c4043",
hover_color="#c5221f",
font=ctk.CTkFont(family="Microsoft YaHei", size=11),
command=lambda e=ext: self.on_delete_click(e)
)
del_btn.pack(side="right")
def update_status(self):
extensions = self.config.get("extensions", [])
if not extensions:
self.status_indicator.configure(text="❌ 错误:配置的扩展名列表不能为空!", text_color="#F28B82")
self.save_btn.configure(state="disabled")
return
self.save_btn.configure(state="normal")
reg_status = check_registry_status(extensions)
if reg_status == "active":
active_list = ", ".join([normalize_ext(e) for e in extensions])
self.status_indicator.configure(
text=f"🟢 右键菜单已启用 (当前目标: {active_list})",
text_color="#81C995"
)
elif reg_status == "mismatch":
self.status_indicator.configure(
text="🟡 配置列表已修改未保存:请点击保存以重新生成右键菜单",
text_color="#FDD633"
)
else:
self.status_indicator.configure(
text="⚪ 右键菜单当前未启用",
text_color="#9AA0A6"
)
def on_add_click(self):
new_item = self.add_entry.get().strip().lower()
if new_item.startswith('.'):
new_item = new_item[1:]
if not new_item:
return
extensions = self.config.get("extensions", [])
if new_item in extensions:
show_message("提示", f"扩展名 '{normalize_ext(new_item)}' 已经存在于列表中!", 0x30)
return
extensions.append(new_item)
self.config["extensions"] = extensions
self.add_entry.delete(0, "end")
self.render_list()
self.update_status()
def on_delete_click(self, ext):
extensions = self.config.get("extensions", [])
if ext in extensions:
extensions.remove(ext)
self.config["extensions"] = extensions
self.render_list()
self.update_status()
def on_save(self):
extensions = self.config.get("extensions", [])
if not extensions:
show_message("提示", "扩展名列表不能为空!", 0x30)
return
show_notification = bool(self.notif_switch.get())
self.config["show_notification"] = show_notification
if save_config(self.config):
if register_context_menu(extensions, show_notification):
show_message("成功", "配置已成功保存!\n右键菜单选项已集成刷新完成。", 0x40)
self.update_status()
def on_uninstall(self):
if unregister_context_menu():
show_message("已移除", "右键菜单已成功从系统中清理。", 0x40)
self.update_status()
if __name__ == "__main__":
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
try:
ctypes.windll.user32.SetProcessDPIAware()
except Exception:
pass
app = App()
app.mainloop()