基于Python+tkinter打造高颜值软件卸载工具代码详解

创客白泽 2025-05-09 09:58:58编程技术
814

前言:为什么需要自制卸载工具

在Windows系统中,自带的"添加/删除程序"功能一直饱受诟病:加载慢、功能弱、残留多。第三方卸载工具如GeekUninstaller虽然好用,但毕竟是闭源商业软件。今天我们将用Python+tkinter打造一款颜值与实力并存的卸载工具,具备以下杀手级特性:

  • 现代化UI界面(暗黑主题+高亮配色)

  • 精准程序扫描(三路注册表探测)

  • 强力卸载模式(支持MSI静默卸载)

  • 智能残留清理(全盘扫描关联文件)

  • 原生图标提取(EXE文件图标解析)

基于Python+tkinter打造高颜值软件卸载工具代码详解

一、核心功能架构设计

1.1 技术栈选型

技术组件 作用说明 替代方案
tkinter GUI界面开发 PyQt/PySide
winreg Windows注册表访问 _winreg
Pillow 图标图像处理 OpenCV
pywin32 Windows API调用 ctypes
shutil 文件系统操作 os模块

1.2 程序流程图

基于Python+tkinter打造高颜值软件卸载工具代码详解

二、关键技术实现详解

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和Tkinter构建一个高颜值的软件卸载工具——PyGeek Uninstaller。通过精心设计的界面和丰富的功能,这款工具为用户提供了一个直观、易用的软件管理平台。代码实现部分涵盖了界面布局、事件处理、功能逻辑等多个方面,展示了Python和Tkinter在GUI开发中的强大能力。此外,本文还探讨了如何通过优化代码结构和提高性能来提升工具的使用体验。总之,PyGeek Uninstaller不仅是一款实用的软件卸载工具,也是学习和掌握Python GUI编程的优秀案例。

Python tkinter 软件卸载工具
THE END
蜜芽
故事不长,也不难讲,四字概括,毫无意义。

相关推荐

Python yield 用法大全:轻松掌握生成器与迭代器设计
在Python中,yield关键字是构建生成器的核心工具,它通过状态保存机制实现了高效的内存管理和惰性计算。与传统的迭代器实现相比,yield能将迭代器设计从复杂的类定义简化为直...
2025-09-15 编程技术
1227

基于Python的旅游数据分析可视化系统【2026最新】
本研究成功开发了基于Python+Django+Vue+MySQL的旅游数据分析可视化系统,实现了从数据采集到可视化展示的全流程管理。系统采用前后端分离架构,前端通过Vue框架构建响应式界...
2025-09-13 编程技术
1173

手把手教你用Python读取txt文件:从基础到实战的完整教程
Python作为数据处理的利器,文件读写是其基础核心功能。掌握txt文件读取不仅能处理日志、配置文件等常见场景,更是理解Python文件I/O的基石。本文ZHANID工具网将从基础语法到...
2025-09-12 编程技术
1086

Python Flask 入门指南:从零开始搭建你的第一个 Web 应用
Flask作为 Python 中最轻量级且灵活的 Web 框架之一,特别适合初学者快速上手 Web 应用开发。本文将带你一步步了解如何在本地环境中安装 Flask、创建一个简单的 Web 应用,并...
2025-09-11 编程技术
1033

Python 如何调用 MediaPipe?详细安装与使用指南
MediaPipe 是 Google 开发的跨平台机器学习框架,支持实时处理视觉、音频和文本数据。本文脚本之家将系统讲解 Python 环境下 MediaPipe 的安装、配置及核心功能调用方法,涵盖...
2025-09-10 编程技术
1101

基于Python开发一个利率计算器的思路及示例代码
利率计算是金融领域的基础需求,涵盖贷款利息、存款收益、投资回报等场景。传统计算依赖手工公式或Excel表格,存在效率低、易出错等问题。Python凭借其简洁的语法和强大的数学...
2025-09-09 编程技术
1119