吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1141|回复: 13
收起左侧

[Python 原创] 右键文件修改为指定扩展名

  [复制链接]
linjian648 发表于 2026-6-9 10:41
因为众所周知的原因,从网上下载压缩包的时候文件扩展名都不是压缩包扩展名,所以让ai写了这个。把需要的扩展名添加到系统自带的右键菜单中。
可以直接右键文件重命名为指定扩展名,当只设置一个扩展名的时候在一级菜单里面,超过一个就到二级菜单了。可选择多个文件一起改扩展名。
只在win10电脑上用过。其他版本不知道能不能正常用。
image.png
image.png
[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" &#127991;&#65039;  {clean_ext}", 
                font=ctk.CTkFont(family="Microsoft YaHei", size=13, weight="bold")
            )
            lbl.pack(side="left")
            
            del_btn = ctk.CTkButton(
                row_frame,
                text="&#128465;&#65039; 删除",
                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="&#10060; 错误:配置的扩展名列表不能为空!", 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"&#128994; 右键菜单已启用 (当前目标: {active_list})", 
                text_color="#81C995"
            )
        elif reg_status == "mismatch":
            self.status_indicator.configure(
                text="&#128993; 配置列表已修改未保存:请点击保存以重新生成右键菜单", 
                text_color="#FDD633"
            )
        else:
            self.status_indicator.configure(
                text="&#9898; 右键菜单当前未启用", 
                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()

免费评分

参与人数 3吾爱币 +7 热心值 +1 收起 理由
苏紫方璇 + 5 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
ltysvx + 1 用心讨论,共获提升!
cioceo + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| linjian648 发表于 2026-6-9 14:00
冥界3大法王 发表于 2026-6-9 12:46
那和批处理, regedit /s 静默没有本质的区别
我98年开始都是直接开启隐藏、系统、扩展名。。显示所有文 ...

你自制的文件管理器让我想起了Total Commander,我电脑其实有装这个的,但是不知道怎么回事,很少会用它去开目录。哈哈哈,感觉是这玩意没有直接替代本地的资源管理器导致的,如果说所有应用调用的打开文件夹都是第三方的文件管理器的话,利用率就高了。
冥界3大法王 发表于 2026-6-9 10:57
image.png
你这个太麻烦了。。
PixPin_2026-06-09_10-55-37.jpg
你看我自制的这个。。。和你那个感觉差不多啊。。热键打出输入完了就完活了。
再说修改指定扩展名,如果手动系统还会提醒(除非你汇编那两个DLL)
热键+静默修改=最合理
 楼主| linjian648 发表于 2026-6-9 11:02
冥界3大法王 发表于 2026-6-9 10:57
你这个太麻烦了。。

你看我自制的这个。。。和你那个感觉差不多啊。。热键打出输入完了就完活了。

快捷键好难记的,我在家用电脑的时候基本都是鼠标党,只有在公司才会用快捷键,算是很分裂的了。
 楼主| linjian648 发表于 2026-6-9 11:08
冥界3大法王 发表于 2026-6-9 10:57
你这个太麻烦了。。

你看我自制的这个。。。和你那个感觉差不多啊。。热键打出输入完了就完活了。

另外,我在自己电脑上用,是后台静默修改的,系统不会弹提示。大概原因是调用cmd改的吧
冥界3大法王 发表于 2026-6-9 11:29
linjian648 发表于 2026-6-9 11:08
另外,我在自己电脑上用,是后台静默修改的,系统不会弹提示。大概原因是调用cmd改的吧

再有这种东西上Python有点不实用
AutoHotkey这种热键按完就毕业了
计小宇 发表于 2026-6-9 11:40
我习惯改“文件夹”属性,打开隐藏后缀,后自己改!
 楼主| linjian648 发表于 2026-6-9 11:42
冥界3大法王 发表于 2026-6-9 11:29
再有这种东西上Python有点不实用
AutoHotkey这种热键按完就毕业了

哈哈哈,ai写的,他用什么写,我用什么,看着可能有点用就往网上发一发
 楼主| linjian648 发表于 2026-6-9 11:44
计小宇 发表于 2026-6-9 11:40
我习惯改“文件夹”属性,打开隐藏后缀,后自己改!

改多了就有点烦了
冥界3大法王 发表于 2026-6-9 12:46
计小宇 发表于 2026-6-9 11:40
我习惯改“文件夹”属性,打开隐藏后缀,后自己改!

那和批处理, regedit /s 静默没有本质的区别
我98年开始都是直接开启隐藏、系统、扩展名。。显示所有文件的。。标题显示完整路径、管理员权限的。。。

知道是怎么回事了。。。谁没事改来改去。。。全是骗菜鸟用的。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-9 13:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表