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