目录
- 前言:为什么需要自制卸载工具
- 一、核心功能架构设计
- 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)其它相关文章!
加载中,请稍侯......
精彩评论