目录
- 前言:为什么需要自制卸载工具
- 一、核心功能架构设计
- 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文件图标解析)
一、核心功能架构设计
1.1 技术栈选型
技术组件 | 作用说明 | 替代方案 |
---|---|---|
tkinter | GUI界面开发 | PyQt/PySide |
winreg | Windows注册表访问 | _winreg |
Pillow | 图标图像处理 | OpenCV |
pywin32 | Windows API调用 | ctypes |
shutil | 文件系统操作 | os模块 |
1.2 程序流程图
二、关键技术实现详解
2.1 多源注册表扫描(核心代码解析)
def load_installed_programs(self): reg_paths = [ (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), # 64位系统兼容路径 (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\..."), # 当前用户安装路径 (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\...") ] 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兼容路径
- 异常处理确保扫描过程不中断
2.2 动态图标提取技术
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提取原始图标
- 自动降采样到16x16尺寸
- 异常时回退到默认图标
三、高级功能实现
3.1 智能文件大小计算
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"
3.2 强力卸载模式
卸载类型 | 处理方式 | 示例命令 |
---|---|---|
标准卸载程序 | 直接执行UninstallString | C:\Program Files\... |
MSI安装包 | 调用msiexec静默卸载 | msiexec /x {GUID} |
无卸载程序 | 提示手动删除 | - |
四、UI美化实战技巧
4.1 现代化暗黑主题
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" )
4.2 响应式布局设计
# 主编程界面采用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)
五、性能优化方案
5.1 图标缓存机制
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
5.2 多线程加载
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"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), (winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall") ] 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']} Publ编程客栈isher: {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",http://www.devze.com 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("Erro编程客栈r", 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:\n\n{residue_text}\n\nContinue?"): 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软件卸载的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论