基于Python打造高颜值软件卸载工具

作者:

文章目录
  • 在Windows系统中,自带的"添加/删除程序"功能一直饱受诟病:加载慢、功能弱、残留多。第三方卸载工具如GeekUninstaller虽然好用,但毕竟是闭源商业软件。今天我们将用Python+tkinter打造一款颜值与实力并存的卸载工具,具备以下杀手级特性: 现代化UI界面(暗黑主题+高亮配色) 精准程序扫描(三路注册表探测) 强力卸载模式(支持MSI静默卸载) 智能残留清理(全盘扫描关联文件) 原生图标提取(EXE文件图标解析)
  • 通过本项目,我们实现了: 完整的程序卸载管理功能 媲美商业软件的UI体验 高效的注册表扫描机制 智能化的残留检测 未来优化方向: 增加云端垃圾文件特征库 实现卸载历史记录功能 添加软件更新检测模块
  • import os import winreg import subprocess import shutil import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from PIL import Image, ImageTk import ctypes class GeekUninstallerApp: def __init__(self, root): self.root = root self.root.title(“PyGeek Uninstaller”) self.root.geometry(“900×600”) self.root.minsize(800, 500) # 设置主题颜色 self.bg_color = “#2d2d2d” self.fg_color = “#ffffff” self.accent_color = “#4CAF50” self.secondary_color = “#2196F3” self.warning_color = “#FF5722” self.highlight_color = “#FFC107” # 初始化样式 self.setup_styles() # 创建UI self.create_widgets() # 加载已安装程序 self.load_installed_programs() def setup_styles(self): style = ttk.Style() style.theme_use(“clam”) # 树状视图样式 style.configure(“Treeview”, background=”#3d3d3d”, foreground=self.fg_color, fieldbackground=”#3d3d3d”, borderwidth=0 ) style.configure(“Treeview.Heading”, background=”#4d4d4d”, foreground=self.fg_color, relief=tk.FLAT ) style.map(“Treeview”, background=[(“selected”, self.secondary_color)]) # 配置主窗口背景 self.root.configure(bg=self.bg_color) def create_widgets(self): # 顶部标题栏 header_frame = tk.Frame(self.root, bg=self.bg_color) header_frame.pack(fill=tk.X, padx=10, pady=10) # 标题 title_label = tk.Label( header_frame, text=”PyGeek Uninstaller”, font=(“Segoe UI”, 18, “bold”), fg=self.highlight_color, bg=self.bg_color ) title_label.pack(side=tk.LEFT) # 搜索框 search_frame = tk.Frame(header_frame, bg=self.bg_color) search_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True) search_label = tk.Label( search_frame, text=”Search:”, font=(“Segoe UI”, 10), fg=self.fg_color, bg=self.bg_color ) search_label.pack(side=tk.LEFT, padx=(20, 5)) self.search_var = tk.StringVar() self.search_var.trace(“w”, self.filter_programs) search_entry = tk.Entry( search_frame, textvariable=self.search_var, font=(“Segoe UI”, 10), bg=”#3d3d3d”, fg=self.fg_color, insertbackground=self.fg_color, relief=tk.FLAT ) search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=2) # 主内容区域 main_frame = tk.Frame(self.root, bg=self.bg_color) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 程序列表 list_frame = tk.Frame(main_frame, bg=self.bg_color) list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 树状视图 self.tree = ttk.Treeview( list_frame, columns=(“name”, “publisher”, “version”, “size”), selectmode=”extended” ) # 配置列 self.tree.heading(“#0″, text=”Icon”, anchor=tk.W) self.tree.heading(“name”, text=”Name”, anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, “name”, False)) self.tree.heading(“publisher”, text=”Publisher”, anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, “publisher”, False)) self.tree.heading(“version”, text=”Version”, anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, “version”, False)) self.tree.heading(“size”, text=”Size”, anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, “size”, False)) self.tree.column(“#0”, width=30, minwidth=30, stretch=tk.NO) self.tree.column(“name”, width=250, minwidth=150, stretch=tk.YES) self.tree.column(“publisher”, width=200, minwidth=100, stretch=tk.YES) self.tree.column(“version”, width=100, minwidth=70, stretch=tk.NO) self.tree.column(“size”, width=100, minwidth=70, stretch=tk.NO) # 滚动条 scrollbar = ttk.Scrollbar(list_frame, orient=”vertical”, command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.tree.pack(fill=tk.BOTH, expand=True) # 绑定双击事件 self.tree.bind(“<Double-1>”, self.show_program_details) # 操作按钮区域 button_frame = tk.Frame(main_frame, bg=self.bg_color) button_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 10)) # 按钮样式 button_style = { “font”: (“Segoe UI”, 10), “bg”: “#4d4d4d”, “fg”: self.fg_color, “activebackground”: self.secondary_color, “activeforeground”: self.fg_color, “relief”: tk.FLAT, “bd”: 0, “padx”: 15, “pady”: 8 } # 操作按钮 self.uninstall_btn = tk.Button( button_frame, text=”Uninstall”, command=self.uninstall_selected, **button_style ) self.uninstall_btn.pack(fill=tk.X, pady=(0, 5)) self.force_btn = tk.Button( button_frame, text=”Force Remove”, command=self.force_remove, **button_style ) self.force_btn.pack(fill=tk.X, pady=(0, 5)) self.details_btn = tk.Button( button_frame, text=”Details”, command=self.show_program_details, **button_style ) self.details_btn.pack(fill=tk.X, pady=(0, 5)) self.clean_btn = tk.Button( button_frame, text=”Clean Residues”, command=self.clean_residues, **button_style ) self.clean_btn.pack(fill=tk.X, pady=(0, 5)) self.refresh_btn = tk.Button( button_frame, text=”Refresh”, command=self.refresh_list, **button_style ) self.refresh_btn.pack(fill=tk.X, pady=(0, 5)) # 状态栏 self.status_var = tk.StringVar() self.status_var.set(“Ready”) status_bar = tk.Label( self.root, textvariable=self.status_var, font=(“Segoe UI”, 9), fg=self.fg_color, bg=”#3d3d3d”, anchor=tk.W, relief=tk.SUNKEN ) status_bar.pack(fill=tk.X, side=tk.BOTTOM, ipady=5) def treeview_sort_column(self, tv, col, reverse): l = [(tv.set(k, col), k) for k in tv.get_children(”)] # 尝试转换为数字进行排序 try: l.sort(key=lambda t: float(t[0]) if t[0].replace(‘.’, ”).isdigit() else t[0], reverse=reverse) except: l.sort(reverse=reverse) # 重新排列项目 for index, (val, k) in enumerate(l): tv.move(k, ”, index) # 下次反向排序 tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse)) def load_installed_programs(self): self.tree.delete(*self.tree.get_children()) self.programs = [] # 从注册表获取已安装程序 reg_paths = [ (winreg.HKEY_LOCAL_MACHINE, r”SOFTWAREMicrosoftWindowsCurrentVersionUninstall”), (winreg.HKEY_LOCAL_MACHINE, r”SOFTWAREWOW6432NodeMicrosoftWindowsCurrentVersionUninstall”), (winreg.HKEY_CURRENT_USER, r”SOFTWAREMicrosoftWindowsCurrentVersionUninstall”) ] for hive, path in reg_paths: try: with winreg.OpenKey(hive, path) as key: for i in range(0, winreg.QueryInfoKey(key)[0]): try: subkey_name = winreg.EnumKey(key, i) with winreg.OpenKey(key, subkey_name) as subkey: try: name = winreg.QueryValueEx(subkey, “DisplayName”)[0] if not name: continue publisher = winreg.QueryValueEx(subkey, “Publisher”)[0] if winreg.QueryValueEx(subkey, “Publisher”) else “” version = winreg.QueryValueEx(subkey, “DisplayVersion”)[0] if winreg.QueryValueEx(subkey, “DisplayVersion”) else “” install_location = winreg.QueryValueEx(subkey, “InstallLocation”)[0] if winreg.QueryValueEx(subkey, “InstallLocation”) else “” uninstall_string = winreg.QueryValueEx(subkey, “UninstallString”)[0] if winreg.QueryValueEx(subkey, “UninstallString”) else “” size = self.get_program_size(install_location) program = { “name”: name, “publisher”: publisher, “version”: version, “size”: size, “install_location”: install_location, “uninstall_string”: uninstall_string, “reg_key”: f”{path}\{subkey_name}”, “hive”: hive } self.programs.append(program) # 插入到树状视图 self.tree.insert(“”, “end”, values=( name, publisher, version, self.format_size(size) )) except (WindowsError, ValueError): continue except (WindowsError, ValueError): continue except WindowsError: continue # 按名称排序 self.programs.sort(key=lambda x: x[“name”].lower()) self.treeview_sort_column(self.tree, “name”, False) self.status_var.set(f”Loaded {len(self.programs)} programs”) def get_program_size(self, install_location): if not install_location or not os.path.isdir(install_location): return 0 total_size = 0 for dirpath, dirnames, filenames in os.walk(install_location): for f in filenames: fp = os.path.join(dirpath, f) try: total_size += os.path.getsize(fp) except: continue return total_size def format_size(self, size): if size == 0: return “N/A” for unit in [‘B’, ‘KB’, ‘MB’, ‘GB’]: if size < 1024.0: return f”{size:.1f} {unit}” size /= 1024.0 return f”{size:.1f} TB” def filter_programs(self, *args): query = self.search_var.get().lower() for item in self.tree.get_children(): values = self.tree.item(item)[“values”] if query in values[0].lower() or query in values[1].lower(): self.tree.selection_set(item) self.tree.see(item) else: self.tree.selection_remove(item) def get_selected_program(self): selected_items = self.tree.selection() if not selected_items: messagebox.showwarning(“Warning”, “Please select a program first!”) return None item = selected_items[0] values = self.tree.item(item)[“values”] for program in self.programs: if program[“name”] == values[0] and program[“publisher”] == values[1]: return program return None def show_program_details(self, event=None): program = self.get_selected_program() if not program: return details_window = tk.Toplevel(self.root) details_window.title(f”Details – {program[‘name’]}”) details_window.geometry(“600×400”) details_window.configure(bg=self.bg_color) # 详细信息文本 details_text = scrolledtext.ScrolledText( details_window, wrap=tk.WORD, font=(“Consolas”, 10), bg=”#3d3d3d”, fg=self.fg_color, insertbackground=self.fg_color ) details_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 添加信息 details = f”””Program Name: {program[‘name’]} Publisher: {program[‘publisher’]} Version: {program[‘version’]} Size: {self.format_size(program[‘size’])} Install Location: {program[‘install_location’]} Uninstall Command: {program[‘uninstall_string’]} Registry Key: {program[‘reg_key’]} “”” details_text.insert(tk.END, details) details_text.configure(state=”disabled”) # 关闭按钮 close_btn = tk.Button( details_window, text=”Close”, command=details_window.destroy, font=(“Segoe UI”, 10), bg=”#4d4d4d”, fg=self.fg_color, activebackground=self.secondary_color, activeforeground=self.fg_color, relief=tk.FLAT ) close_btn.pack(pady=(0, 10)) def uninstall_selected(self): program = self.get_selected_program() if not program: return if not program[“uninstall_string”]: messagebox.showerror(“Error”, “No uninstall command found for this program!”) return try: # 运行卸载命令 if program[“uninstall_string”].lower().endswith(“.msi”): # MSI 包 cmd = f’msiexec /x “{program[“uninstall_string”]}” /quiet’ else: # 普通卸载程序 cmd = program[“uninstall_string”] subprocess.Popen(cmd, shell=True) self.status_var.set(f”Uninstalling {program[‘name’]}…”) except Exception as e: messagebox.showerror(“Error”, f”Failed to start uninstaller: {str(e)}”) def force_remove(self): program = self.get_selected_program() if not program: return if not messagebox.askyesno(“Warning”, f”Force removal will delete all files and registry entries for {program[‘name’]}.n” “This action cannot be undone. Continue?”): return # 删除安装目录 if program[“install_location”] and os.path.isdir(program[“install_location”]): try: shutil.rmtree(program[“install_location”]) self.status_var.set(f”Deleted installation folder: {program[‘install_location’]}”) except Exception as e: messagebox.showerror(“Error”, f”Failed to delete installation folder: {str(e)}”) # 删除注册表项 try: hive, path = program[“hive”], program[“reg_key”] with winreg.OpenKey(hive, path.replace(“\”, “/”), 0, winreg.KEY_ALL_ACCESS) as key: winreg.DeleteKey(hive, path) self.status_var.set(f”Deleted registry key: {path}”) except Exception as e: messagebox.showerror(“Error”, f”Failed to delete registry key: {str(e)}”) # 刷新列表 self.refresh_list() messagebox.showinfo(“Success”, f”{program[‘name’]} has been force removed!”) def clean_residues(self): program = self.get_selected_program() if not program: return # 查找残留文件 residues = [] if program[“install_location”] and os.path.isdir(program[“install_location”]): residues.append(program[“install_location”]) # 检查常见残留位置 common_locations = [ os.path.join(os.environ[“APPDATA”], program[“name”]), os.path.join(os.environ[“LOCALAPPDATA”], program[“name”]), os.path.join(os.environ[“PROGRAMDATA”], program[“name”]), os.path.join(os.environ[“USERPROFILE”], “AppData”, “Local”, program[“name”]), os.path.join(os.environ[“USERPROFILE”], “AppData”, “Roaming”, program[“name”]) ] for loc in common_locations: if os.path.exists(loc): residues.append(loc) if not residues: messagebox.showinfo(“Info”, “No residual files found for this program.”) return # 显示确认对话框 residue_text = “n”.join(residues) if not messagebox.askyesno(“Confirm”, f”The following residual files/folders will be deleted:nn{residue_text}nnContinue?”): return # 删除残留文件 success = True for residue in residues: try: if os.path.isdir(residue): shutil.rmtree(residue) else: os.remove(residue) self.status_var.set(f”Deleted residue: {residue}”) except Exception as e: messagebox.showerror(“Error”, f”Failed to delete {residue}: {str(e)}”) success = False if success: messagebox.showinfo(“Success”, “Residual files have been cleaned successfully!”) def refresh_list(self): self.status_var.set(“Refreshing program list…”) self.root.update() self.load_installed_programs() self.status_var.set(“Program list refreshed”) def main(): # 启用DPI感知 try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except: pass root = tk.Tk() app = GeekUninstallerApp(root) root.mainloop() if __name__ == “__main__”: main() 互动讨论 Q:为什么选择tkinter而不是PyQt? A:tkinter作为Python标准库,具有更好的兼容性和更小的体积,适合分发小型工具。 Q:如何增强卸载能力? ​​​​​​​A:可以集成PowerShell的Remove-MSIXPackage等现代卸载方案。 以上就是基于Python打造高颜值软件卸载工具的详细内容,更多关于Python软件卸载的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: Python批量安装卸载1000个apk的方法 使用Python制作一个恶意软件删除工具 详解如何使用pip卸载所有已安装的Python包 python 如何通过执行脚本安装库或卸载库
  • 目录
    • 前言:为什么需要自制卸载工具
    • 一、核心功能架构设计
      • 1.1 技术栈选型
      • 1.2 程序流程图
    • 二、关键技术实现详解
      • 2.1 多源注册表扫描(核心代码解析)
      • 2.2 动态图标提取技术
    • 三、高级功能实现
      • 3.1 智能文件大小计算
      • 3.2 强力卸载模式
    • 四、UI美化实战技巧
      • 4.1 现代化暗黑主题
      • 4.2 响应式布局设计
    • 五、性能优化方案
      • 5.1 图标缓存机制
      • 5.2 多线程加载
    • 项目总结与展望
      • 完整项目源码

        在Windows系统中,自带的"添加/删除程序"功能一直饱受诟病:加载慢、功能弱、残留多。第三方卸载工具如GeekUninstaller虽然好用,但毕竟是闭源商业软件。今天我们将用Python+tkinter打造一款颜值与实力并存的卸载工具,具备以下杀手级特性:

        • 现代化UI界面(暗黑主题+高亮配色)
        • 精准程序扫描(三路注册表探测)
        • 强力卸载模式(支持MSI静默卸载)
        • 智能残留清理(全盘扫描关联文件)
        • 原生图标提取(EXE文件图标解析)

        技术组件 作用说明 替代方案
        tkinter GUI界面开发 PyQt/PySide
        winreg Windows注册表访问 _winreg
        Pillow 图标图像处理 OpenCV
        pywin32 Windows API调用 ctypes
        shutil 文件系统操作 os模块

        def load_installed_programs(self):
            reg_paths = [
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWAREMicrosoftWindowsCurrentVersionUninstall"),
                # 64位系统兼容路径
                (winreg.HKEY_LOCAL_MACHINE, r"SOFTWAREWOW6432Node..."), 
                # 当前用户安装路径
                (winreg.HKEY_CURRENT_USER, r"SOFTWAREMicrosoft...")
            ]
            
            for hive, path in reg_paths:
                try:
                    with winreg.OpenKey(hive, path) as key:
                        for i in range(winreg.QueryInfoKey(key)[0]):
                            # 提取程序信息...
                            program = {
                                "name": name,
                                "version": version,
                                "install_location": install_path,
                                "uninstall_string": uninstall_cmd
                            }

        技术要点:

        • 同时扫描HKLM和HKCU两大主键
        • 处理64位系统的WOW6432Node兼容路径
        • 异常处理确保扫描过程不中断

        def get_icon_from_exe(self, exe_path):
            # 使用Win32 API提取图标
            large, small = win32gui.ExtractIconEx(exe_path, 0)
            hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
            
            # 创建兼容位图
            hbmp = win32ui.CreateBitmap()
            hbmp.CreateCompatibleBitmap(hdc, 16, 16)
            
            # 转换为PIL图像
            bmpstr = hbmp.GetBitmapBits(True)
            icon = Image.frombuffer('RGB', (16,16), bmpstr, 'raw', 'BGRX', 0, 1)
            return ImageTk.PhotoImage(icon)
        

        创新点:

        • 直接从EXE/DLL提取原始图标
        • 自动降采样到16×16尺寸
        • 异常时回退到默认图标

        def get_program_size(self, path):
            total = 0
            for root, dirs, files in os.walk(path):
                for f in files:
                    try:
                        total += os.path.getsize(os.path.join(root, f))
                    except:
                        continue
            return total
        
        ​​​​​​​def format_size(self, size):
            # 智能转换单位
            units = ['B', 'KB', 'MB', 'GB']
            for unit in units:
                if size < 1024.0:
                    return f"{size:.1f} {unit}"
                size /= 1024.0
            return f"{size:.1f} TB"

        卸载类型 处理方式 示例命令
        标准卸载程序 直接执行UninstallString C:Program Files…
        MSI安装包 调用msiexec静默卸载 msiexec /x {GUID}
        无卸载程序 提示手动删除

        self.bg_color = "#2d2d2d"  # 背景色
        self.fg_color = "#ffffff"   # 前景色
        self.accent_color = "#4CAF50" # 强调色
        
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("Treeview", 
            background="#3d3d3d",
            foreground=self.fg_color,
            fieldbackground="#3d3d3d"
        )
        

        # 主界面采用Pack布局
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 左侧列表区域
        list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # 右侧按钮区域
        button_frame.pack(side=tk.RIGHT, fill=tk.Y)
        

        self.icon_cache = {}  # 缓存字典
        
        def get_program_icon(self, program):
            if program['name'] in self.icon_cache:
                return self.icon_cache[program['name']]
            
            icon = self._extract_icon(program)
            self.icon_cache[program['name']] = icon
            return icon
        

        from threading import Thread
        
        def load_data_async(self):
            Thread(target=self.load_installed_programs, daemon=True).start()
        

        通过本项目,我们实现了:

        • 完整的程序卸载管理功能
        • 媲美商业软件的UI体验
        • 高效的注册表扫描机制
        • 智能化的残留检测

        未来优化方向:

        • 增加云端垃圾文件特征库
        • 实现卸载历史记录功能
        • 添加软件更新检测模块

        import os
        import winreg
        import subprocess
        import shutil
        import tkinter as tk
        from tkinter import ttk, messagebox, scrolledtext
        from PIL import Image, ImageTk
        import ctypes
        
        class GeekUninstallerApp:
            def __init__(self, root):
                self.root = root
                self.root.title("PyGeek Uninstaller")
                self.root.geometry("900x600")
                self.root.minsize(800, 500)
                
                # 设置主题颜色
                self.bg_color = "#2d2d2d"
                self.fg_color = "#ffffff"
                self.accent_color = "#4CAF50"
                self.secondary_color = "#2196F3"
                self.warning_color = "#FF5722"
                self.highlight_color = "#FFC107"
                
                # 初始化样式
                self.setup_styles()
                
                # 创建UI
                self.create_widgets()
                
                # 加载已安装程序
                self.load_installed_programs()
            
            def setup_styles(self):
                style = ttk.Style()
                style.theme_use("clam")
                
                # 树状视图样式
                style.configure("Treeview", 
                    background="#3d3d3d", 
                    foreground=self.fg_color, 
                    fieldbackground="#3d3d3d", 
                    borderwidth=0
                )
                style.configure("Treeview.Heading", 
                    background="#4d4d4d", 
                    foreground=self.fg_color, 
                    relief=tk.FLAT
                )
                style.map("Treeview", background=[("selected", self.secondary_color)])
                
                # 配置主窗口背景
                self.root.configure(bg=self.bg_color)
            
            def create_widgets(self):
                # 顶部标题栏
                header_frame = tk.Frame(self.root, bg=self.bg_color)
                header_frame.pack(fill=tk.X, padx=10, pady=10)
                
                # 标题
                title_label = tk.Label(
                    header_frame, 
                    text="PyGeek Uninstaller", 
                    font=("Segoe UI", 18, "bold"), 
                    fg=self.highlight_color, 
                    bg=self.bg_color
                )
                title_label.pack(side=tk.LEFT)
                
                # 搜索框
                search_frame = tk.Frame(header_frame, bg=self.bg_color)
                search_frame.pack(side=tk.RIGHT, fill=tk.X, expand=True)
                
                search_label = tk.Label(
                    search_frame, 
                    text="Search:", 
                    font=("Segoe UI", 10), 
                    fg=self.fg_color, 
                    bg=self.bg_color
                )
                search_label.pack(side=tk.LEFT, padx=(20, 5))
                
                self.search_var = tk.StringVar()
                self.search_var.trace("w", self.filter_programs)
                search_entry = tk.Entry(
                    search_frame, 
                    textvariable=self.search_var, 
                    font=("Segoe UI", 10), 
                    bg="#3d3d3d", 
                    fg=self.fg_color, 
                    insertbackground=self.fg_color, 
                    relief=tk.FLAT
                )
                search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=2)
                
                # 主内容区域
                main_frame = tk.Frame(self.root, bg=self.bg_color)
                main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
                
                # 程序列表
                list_frame = tk.Frame(main_frame, bg=self.bg_color)
                list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
                
                # 树状视图
                self.tree = ttk.Treeview(
                    list_frame, 
                    columns=("name", "publisher", "version", "size"), 
                    selectmode="extended"
                )
                
                # 配置列
                self.tree.heading("#0", text="Icon", anchor=tk.W)
                self.tree.heading("name", text="Name", anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, "name", False))
                self.tree.heading("publisher", text="Publisher", anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, "publisher", False))
                self.tree.heading("version", text="Version", anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, "version", False))
                self.tree.heading("size", text="Size", anchor=tk.W, command=lambda: self.treeview_sort_column(self.tree, "size", False))
                
                self.tree.column("#0", width=30, minwidth=30, stretch=tk.NO)
                self.tree.column("name", width=250, minwidth=150, stretch=tk.YES)
                self.tree.column("publisher", width=200, minwidth=100, stretch=tk.YES)
                self.tree.column("version", width=100, minwidth=70, stretch=tk.NO)
                self.tree.column("size", width=100, minwidth=70, stretch=tk.NO)
                
                # 滚动条
                scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.tree.yview)
                self.tree.configure(yscrollcommand=scrollbar.set)
                scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                self.tree.pack(fill=tk.BOTH, expand=True)
                
                # 绑定双击事件
                self.tree.bind("<Double-1>", self.show_program_details)
                
                # 操作按钮区域
                button_frame = tk.Frame(main_frame, bg=self.bg_color)
                button_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 10))
                
                # 按钮样式
                button_style = {
                    "font": ("Segoe UI", 10), 
                    "bg": "#4d4d4d", 
                    "fg": self.fg_color, 
                    "activebackground": self.secondary_color, 
                    "activeforeground": self.fg_color, 
                    "relief": tk.FLAT, 
                    "bd": 0, 
                    "padx": 15, 
                    "pady": 8
                }
                
                # 操作按钮
                self.uninstall_btn = tk.Button(
                    button_frame, 
                    text="Uninstall", 
                    command=self.uninstall_selected, 
                    **button_style
                )
                self.uninstall_btn.pack(fill=tk.X, pady=(0, 5))
                
                self.force_btn = tk.Button(
                    button_frame, 
                    text="Force Remove", 
                    command=self.force_remove, 
                    **button_style
                )
                self.force_btn.pack(fill=tk.X, pady=(0, 5))
                
                self.details_btn = tk.Button(
                    button_frame, 
                    text="Details", 
                    command=self.show_program_details, 
                    **button_style
                )
                self.details_btn.pack(fill=tk.X, pady=(0, 5))
                
                self.clean_btn = tk.Button(
                    button_frame, 
                    text="Clean Residues", 
                    command=self.clean_residues, 
                    **button_style
                )
                self.clean_btn.pack(fill=tk.X, pady=(0, 5))
                
                self.refresh_btn = tk.Button(
                    button_frame, 
                    text="Refresh", 
                    command=self.refresh_list, 
                    **button_style
                )
                self.refresh_btn.pack(fill=tk.X, pady=(0, 5))
                
                # 状态栏
                self.status_var = tk.StringVar()
                self.status_var.set("Ready")
                status_bar = tk.Label(
                    self.root, 
                    textvariable=self.status_var, 
                    font=("Segoe UI", 9), 
                    fg=self.fg_color, 
                    bg="#3d3d3d", 
                    anchor=tk.W, 
                    relief=tk.SUNKEN
                )
                status_bar.pack(fill=tk.X, side=tk.BOTTOM, ipady=5)
            
            def treeview_sort_column(self, tv, col, reverse):
                l = [(tv.set(k, col), k) for k in tv.get_children('')]
                
                # 尝试转换为数字进行排序
                try:
                    l.sort(key=lambda t: float(t[0]) if t[0].replace('.', '').isdigit() else t[0], reverse=reverse)
                except:
                    l.sort(reverse=reverse)
                
                # 重新排列项目
                for index, (val, k) in enumerate(l):
                    tv.move(k, '', index)
                
                # 下次反向排序
                tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse))
            
            def load_installed_programs(self):
                self.tree.delete(*self.tree.get_children())
                self.programs = []
                
                # 从注册表获取已安装程序
                reg_paths = [
                    (winreg.HKEY_LOCAL_MACHINE, r"SOFTWAREMicrosoftWindowsCurrentVersionUninstall"),
                    (winreg.HKEY_LOCAL_MACHINE, r"SOFTWAREWOW6432NodeMicrosoftWindowsCurrentVersionUninstall"),
                    (winreg.HKEY_CURRENT_USER, r"SOFTWAREMicrosoftWindowsCurrentVersionUninstall")
                ]
                
                for hive, path in reg_paths:
                    try:
                        with winreg.OpenKey(hive, path) as key:
                            for i in range(0, winreg.QueryInfoKey(key)[0]):
                                try:
                                    subkey_name = winreg.EnumKey(key, i)
                                    with winreg.OpenKey(key, subkey_name) as subkey:
                                        try:
                                            name = winreg.QueryValueEx(subkey, "DisplayName")[0]
                                            if not name:
                                                continue
                                            
                                            publisher = winreg.QueryValueEx(subkey, "Publisher")[0] if winreg.QueryValueEx(subkey, "Publisher") else ""
                                            version = winreg.QueryValueEx(subkey, "DisplayVersion")[0] if winreg.QueryValueEx(subkey, "DisplayVersion") else ""
                                            install_location = winreg.QueryValueEx(subkey, "InstallLocation")[0] if winreg.QueryValueEx(subkey, "InstallLocation") else ""
                                            uninstall_string = winreg.QueryValueEx(subkey, "UninstallString")[0] if winreg.QueryValueEx(subkey, "UninstallString") else ""
                                            size = self.get_program_size(install_location)
                                            
                                            program = {
                                                "name": name,
                                                "publisher": publisher,
                                                "version": version,
                                                "size": size,
                                                "install_location": install_location,
                                                "uninstall_string": uninstall_string,
                                                "reg_key": f"{path}\{subkey_name}",
                                                "hive": hive
                                            }
                                            
                                            self.programs.append(program)
                                            
                                            # 插入到树状视图
                                            self.tree.insert("", "end", values=(
                                                name, 
                                                publisher, 
                                                version, 
                                                self.format_size(size)
                                            ))
                                        except (WindowsError, ValueError):
                                            continue
                                except (WindowsError, ValueError):
                                    continue
                    except WindowsError:
                        continue
                
                # 按名称排序
                self.programs.sort(key=lambda x: x["name"].lower())
                self.treeview_sort_column(self.tree, "name", False)
                
                self.status_var.set(f"Loaded {len(self.programs)} programs")
            
            def get_program_size(self, install_location):
                if not install_location or not os.path.isdir(install_location):
                    return 0
                
                total_size = 0
                for dirpath, dirnames, filenames in os.walk(install_location):
                    for f in filenames:
                        fp = os.path.join(dirpath, f)
                        try:
                            total_size += os.path.getsize(fp)
                        except:
                            continue
                return total_size
            
            def format_size(self, size):
                if size == 0:
                    return "N/A"
                for unit in ['B', 'KB', 'MB', 'GB']:
                    if size < 1024.0:
                        return f"{size:.1f} {unit}"
                    size /= 1024.0
                return f"{size:.1f} TB"
            
            def filter_programs(self, *args):
                query = self.search_var.get().lower()
                for item in self.tree.get_children():
                    values = self.tree.item(item)["values"]
                    if query in values[0].lower() or query in values[1].lower():
                        self.tree.selection_set(item)
                        self.tree.see(item)
                    else:
                        self.tree.selection_remove(item)
            
            def get_selected_program(self):
                selected_items = self.tree.selection()
                if not selected_items:
                    messagebox.showwarning("Warning", "Please select a program first!")
                    return None
                
                item = selected_items[0]
                values = self.tree.item(item)["values"]
                
                for program in self.programs:
                    if program["name"] == values[0] and program["publisher"] == values[1]:
                        return program
                
                return None
            
            def show_program_details(self, event=None):
                program = self.get_selected_program()
                if not program:
                    return
                
                details_window = tk.Toplevel(self.root)
                details_window.title(f"Details - {program['name']}")
                details_window.geometry("600x400")
                details_window.configure(bg=self.bg_color)
                
                # 详细信息文本
                details_text = scrolledtext.ScrolledText(
                    details_window, 
                    wrap=tk.WORD, 
                    font=("Consolas", 10), 
                    bg="#3d3d3d", 
                    fg=self.fg_color, 
                    insertbackground=self.fg_color
                )
                details_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
                
                # 添加信息
                details = f"""Program Name: {program['name']}
        Publisher: {program['publisher']}
        Version: {program['version']}
        Size: {self.format_size(program['size'])}
        Install Location: {program['install_location']}
        Uninstall Command: {program['uninstall_string']}
        Registry Key: {program['reg_key']}
        """
                details_text.insert(tk.END, details)
                details_text.configure(state="disabled")
                
                # 关闭按钮
                close_btn = tk.Button(
                    details_window, 
                    text="Close", 
                    command=details_window.destroy, 
                    font=("Segoe UI", 10), 
                    bg="#4d4d4d", 
                    fg=self.fg_color, 
                    activebackground=self.secondary_color, 
                    activeforeground=self.fg_color, 
                    relief=tk.FLAT
                )
                close_btn.pack(pady=(0, 10))
            
            def uninstall_selected(self):
                program = self.get_selected_program()
                if not program:
                    return
                
                if not program["uninstall_string"]:
                    messagebox.showerror("Error", "No uninstall command found for this program!")
                    return
                
                try:
                    # 运行卸载命令
                    if program["uninstall_string"].lower().endswith(".msi"):
                        # MSI 包
                        cmd = f'msiexec /x "{program["uninstall_string"]}" /quiet'
                    else:
                        # 普通卸载程序
                        cmd = program["uninstall_string"]
                    
                    subprocess.Popen(cmd, shell=True)
                    self.status_var.set(f"Uninstalling {program['name']}...")
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to start uninstaller: {str(e)}")
            
            def force_remove(self):
                program = self.get_selected_program()
                if not program:
                    return
                
                if not messagebox.askyesno("Warning", 
                        f"Force removal will delete all files and registry entries for {program['name']}.n"
                        "This action cannot be undone. Continue?"):
                    return
                
                # 删除安装目录
                if program["install_location"] and os.path.isdir(program["install_location"]):
                    try:
                        shutil.rmtree(program["install_location"])
                        self.status_var.set(f"Deleted installation folder: {program['install_location']}")
                    except Exception as e:
                        messagebox.showerror("Error", f"Failed to delete installation folder: {str(e)}")
                
                # 删除注册表项
                try:
                    hive, path = program["hive"], program["reg_key"]
                    with winreg.OpenKey(hive, path.replace("\", "/"), 0, winreg.KEY_ALL_ACCESS) as key:
                        winreg.DeleteKey(hive, path)
                    self.status_var.set(f"Deleted registry key: {path}")
                except Exception as e:
                    messagebox.showerror("Error", f"Failed to delete registry key: {str(e)}")
                
                # 刷新列表
                self.refresh_list()
                messagebox.showinfo("Success", f"{program['name']} has been force removed!")
            
            def clean_residues(self):
                program = self.get_selected_program()
                if not program:
                    return
                
                # 查找残留文件
                residues = []
                if program["install_location"] and os.path.isdir(program["install_location"]):
                    residues.append(program["install_location"])
                
                # 检查常见残留位置
                common_locations = [
                    os.path.join(os.environ["APPDATA"], program["name"]),
                    os.path.join(os.environ["LOCALAPPDATA"], program["name"]),
                    os.path.join(os.environ["PROGRAMDATA"], program["name"]),
                    os.path.join(os.environ["USERPROFILE"], "AppData", "Local", program["name"]),
                    os.path.join(os.environ["USERPROFILE"], "AppData", "Roaming", program["name"])
                ]
                
                for loc in common_locations:
                    if os.path.exists(loc):
                        residues.append(loc)
                
                if not residues:
                    messagebox.showinfo("Info", "No residual files found for this program.")
                    return
                
                # 显示确认对话框
                residue_text = "n".join(residues)
                if not messagebox.askyesno("Confirm", 
                        f"The following residual files/folders will be deleted:nn{residue_text}nnContinue?"):
                    return
                
                # 删除残留文件
                success = True
                for residue in residues:
                    try:
                        if os.path.isdir(residue):
                            shutil.rmtree(residue)
                        else:
                            os.remove(residue)
                        self.status_var.set(f"Deleted residue: {residue}")
                    except Exception as e:
                        messagebox.showerror("Error", f"Failed to delete {residue}: {str(e)}")
                        success = False
                
                if success:
                    messagebox.showinfo("Success", "Residual files have been cleaned successfully!")
            
            def refresh_list(self):
                self.status_var.set("Refreshing program list...")
                self.root.update()
                self.load_installed_programs()
                self.status_var.set("Program list refreshed")
        
        def main():
            # 启用DPI感知
            try:
                ctypes.windll.shcore.SetProcessDpiAwareness(1)
            except:
                pass
            
            root = tk.Tk()
            app = GeekUninstallerApp(root)
            root.mainloop()
        
        if __name__ == "__main__":
            main()
        

        互动讨论

        Q:为什么选择tkinter而不是PyQt?

        A:tkinter作为Python标准库,具有更好的兼容性和更小的体积,适合分发小型工具。

        Q:如何增强卸载能力?

        ​​​​​​​A:可以集成PowerShell的Remove-MSIXPackage等现代卸载方案。

        以上就是基于Python打造高颜值软件卸载工具的详细内容,更多关于Python软件卸载的资料请关注风君子博客其它相关文章!

        您可能感兴趣的文章:

        • Python批量安装卸载1000个apk的方法
        • 使用Python制作一个恶意软件删除工具
        • 详解如何使用pip卸载所有已安装的Python包
        • python 如何通过执行脚本安装库或卸载库

        站内搜索