当前位置: 代码网 > it编程>前端脚本>Python > 基于Python打造高颜值软件卸载工具

基于Python打造高颜值软件卸载工具

2025年05月08日 Python 我要评论
前言:为什么需要自制卸载工具在windows系统中,自带的"添加/删除程序"功能一直饱受诟病:加载慢、功能弱、残留多。第三方卸载工具如geekuninstaller虽然好用,但毕竟

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

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

  • 现代化ui界面(暗黑主题+高亮配色)
  • 精准程序扫描(三路注册表探测)
  • 强力卸载模式(支持msi静默卸载)
  • 智能残留清理(全盘扫描关联文件)
  • 原生图标提取(exe文件图标解析)

一、核心功能架构设计

1.1 技术栈选型

技术组件作用说明替代方案
tkintergui界面开发pyqt/pyside
winregwindows注册表访问_winreg
pillow图标图像处理opencv
pywin32windows 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 强力卸载模式

卸载类型处理方式示例命令
标准卸载程序直接执行uninstallstringc:\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软件卸载的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com