前言:为什么需要自制卸载工具
在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 icon5.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和Tkinter构建一个高颜值的软件卸载工具——PyGeek Uninstaller。通过精心设计的界面和丰富的功能,这款工具为用户提供了一个直观、易用的软件管理平台。代码实现部分涵盖了界面布局、事件处理、功能逻辑等多个方面,展示了Python和Tkinter在GUI开发中的强大能力。此外,本文还探讨了如何通过优化代码结构和提高性能来提升工具的使用体验。总之,PyGeek Uninstaller不仅是一款实用的软件卸载工具,也是学习和掌握Python GUI编程的优秀案例。
本文来源于#创客白泽,由@蜜芽 整理发布。如若内容造成侵权/违法违规/事实不符,请联系本站客服处理!
该文章观点仅代表作者本人,不代表本站立场。本站不承担相关法律责任。
如若转载,请注明出处:https://www.zhanid.com/biancheng/4154.html




















