开发者

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

目录
  • 前言:为什么需要自制卸载工具
  • 一、核心功能架构设计
    • 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文件图标解析)

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

      一、核心功能架构设计

      1.1 技术栈选型

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

      1.2 程序流程图

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

      二、关键技术实现详解

      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']}
      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)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜