前言:为什么需要自制卸载工具
在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']} 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:\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软件卸载的资料请关注代码网其它相关文章!
发表评论