Python与Tkinter实战集成工具箱开发指南

打造高效集成工具箱:基于Python与Tkinter的实战开发教程

1.概述

在日常的开发和使用中,我们经常需要借助各种小工具来提高工作效率,例如快速启动常用的应用程序、管理文件等。一个简单但功能强大的集成工具箱可以帮助用户快速访问、启动并管理程序。今天,我们将以Python为基础,结合Tkinter和Win32API,开发一个类似Windows快捷方式的工具箱应用,能够让你轻松集成各种常用程序并一键启动。

本文将深入讲解如何使用Tkinter构建图形化界面,如何通过Win32API提取程序图标,以及如何利用Python的强大功能来实现一个简单高效的集成工具箱。通过这个教程,你不仅能了解如何设计和开发图形化应用,还能学习如何高效地处理文件和图标,提升开发技巧。

2.功能解析

2.1. 自动获取当前程序路径

首先,代码中定义了一个get_current_path函数,用来自动获取当前运行程序的路径。无论是在开发环境中运行脚本,还是将程序打包成exe文件后运行,get_current_path都能确保正确获取到当前程序的目录路径。

def get_current_path():
    """自动获取当前程序路径的通用方法"""
    try:
        # 打包后的情况
        if getattr(sys, 'frozen', False):
            return os.path.dirname(sys.executable)  # 返回EXE所在目录
        # 开发环境
        return os.path.dirname(os.path.abspath(__file__))  # 返回脚本所在目录
    except Exception as e:
        print(f"路径获取失败: {e}")
        return os.getcwd()  # 退回当前工作目录

2.2. 从程序文件提取图标

get_exe_icon函数是本程序的一个亮点,它能够提取指定可执行文件的图标,并将其转换为可以在Tkinter界面上显示的格式。通过win32gui和win32ui模块,我们可以从exe文件中提取图标并通过PIL库进行处理。

def get_exe_icon(exe_path):
    """从exe文件中提取图标"""
    try:
        large, small = win32gui.ExtractIconEx(exe_path, 0)
        win32gui.DestroyIcon(small[0])
        
        hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
        hbmp = win32ui.CreateBitmap()
        hbmp.CreateCompatibleBitmap(hdc, 32, 32)
        
        hdc = hdc.CreateCompatibleDC()
        hdc.SelectObject(hbmp)
        hdc.DrawIcon((0, 0), large[0])
        
        bmpinfo = hbmp.GetInfo()
        bmpstr = hbmp.GetBitmapBits(True)
        
        img = Image.frombuffer('RGBA', 
                               (bmpinfo['bmWidth'], bmpinfo['bmHeight']), 
                               bmpstr, 'raw', 'BGRA', 0, 1)
        return img
    except Exception as e:
        print(f"提取图标失败: {e}")
        return None

2.3. 程序分类与展示

为了方便管理程序,我们通过分类将程序分组,并在界面上以按钮的形式展示。每个程序按钮都包含一个图标和程序名称,点击按钮即可启动对应程序。程序面板和分类面板分别在左侧和右侧布局,通过ttk.Button实现按钮的创建,并使用UniformButton自定义按钮样式,以保证按钮的统一尺寸和良好的用户体验。

def create_category_panel(self):
    """分类面板(宽度99)"""
    self.category_frame = ttk.Frame(self.main_frame, width=99)
    self.category_frame.pack(side=tk.LEFT, fill=tk.Y, padx=3)
    
    self.category_canvas = tk.Canvas(self.category_frame, 
                                     width=99, 
                                     highlightthickness=0)
    scrollbar = ttk.Scrollbar(self.category_frame, 
                              orient="vertical", 
                              command=self.category_canvas.yview)
    self.category_container = ttk.Frame(self.category_canvas)
    

2.4. 程序扫描与动态加载

scan_directory方法用于扫描当前目录下的所有程序文件,并将其按照类别分组。程序支持多种文件类型,如.exe、.bat、.cmd和.lnk。在扫描完成后,程序将动态加载到界面上,用户可以通过点击不同的类别查看对应的程序。

def scan_directory(self):
    """扫描目录"""
    self.categories = {"所有程序": []}
    executable_ext = ['.exe', '.bat', '.cmd', '.lnk']
    
    for item in os.listdir(self.current_dir):
        path = os.path.join(self.current_dir, item)
        
        if os.path.isdir(path):
            self.categories[item] = []
            for root, _, files in os.walk(path):
                for file in files:
                    if os.path.splitext(file)[1].lower() in executable_ext:
                        full_path = os.path.join(root, file)
                        self.categories[item].append({
                            "name": os.path.splitext(file)[0],
                            "path": full_path
                        })
                        self.categories["所有程序"].append({
                            "name": os.path.splitext(file)[0],
                            "path": full_path
                        })
        else:
            if os.path.splitext(item)[1].lower() in executable_ext:
                self.categories["所有程序"].append({
                    "name": os.path.splitext(item)[0],
                    "path": path
                })

2.5. 程序启动功能

通过launch_program方法,我们可以启动用户点击的程序。该方法使用subprocess.Popen来调用外部程序。对于文件路径中的空格,代码使用了引号确保路径能够正确解析。

def launch_program(self, path):
    """启动程序"""
    try:
        subprocess.Popen(f'"{path}"', shell=True)
    except Exception as _e:
        print(f"启动失败: {_e}")

2.6. 用户界面设计

程序的UI界面设计简洁而直观,使用Tkinter创建了一个分为左右两部分的布局。左侧为程序分类面板,右侧为程序展示面板。用户可以通过点击左侧的按钮切换不同的程序类别。

此外,每个程序按钮都拥有统一的尺寸、图标和文本显示,点击按钮后会高亮显示。按钮的样式通过自定义UniformButton类来实现,使得每个按钮都保持一致的外观和操作响应。

class UniformButton(tk.Frame):
    """统一尺寸的程序按钮组件"""
    def __init__(self, parent, text, image, command):
        super().__init__(parent, bg="white", highlightthickness=0)
        self.button_width = 70
        self.button_height = 90
        self.max_chars = 10
        self.max_lines = 2

3. 效果展示:

4. 相关源码:

import os
import sys
import tkinter as tk
from tkinter import ttk
import win32ui
import win32gui
from PIL import Image, ImageTk
import subprocess
from win32com.client import Dispatch
 
def get_current_path():
    """自动获取当前程序路径的通用方法"""
    try:
        # 打包后的情况
        if getattr(sys, 'frozen', False):
            return os.path.dirname(sys.executable)  # 返回EXE所在目录
        # 开发环境
        return os.path.dirname(os.path.abspath(__file__))  # 返回脚本所在目录
    except Exception as e:
        print(f"路径获取失败: {e}")
        return os.getcwd()  # 退回当前工作目录
 
def get_exe_icon(exe_path):
    """从exe文件中提取图标"""
    try:
        # 提取图标
        large, small = win32gui.ExtractIconEx(exe_path, 0)
        win32gui.DestroyIcon(small[0])
         
        # 创建设备上下文
        hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
        hbmp = win32ui.CreateBitmap()
        hbmp.CreateCompatibleBitmap(hdc, 32, 32)
         
        # 在设备上下文中绘制图标
        hdc = hdc.CreateCompatibleDC()
        hdc.SelectObject(hbmp)
        hdc.DrawIcon((0, 0), large[0])
         
        # 获取位图信息和位图数据
        bmpinfo = hbmp.GetInfo()
        bmpstr = hbmp.GetBitmapBits(True)
         
        # 创建PIL图像对象
        img = Image.frombuffer(
            'RGBA',
            (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
            bmpstr, 'raw', 'BGRA', 0, 1
        )
         
        return img
    except Exception as e:
        print(f"提取图标失败: {e}")
        return None
 
class UniformButton(tk.Frame):
    """统一尺寸的程序按钮组件"""
    def __init__(self, parent, text, image, command):
        super().__init__(parent, bg="white", highlightthickness=0)
         
        # 固定按钮尺寸
        self.button_width = 70
        self.button_height = 90
        self.max_chars = 10
        self.max_lines = 2
         
        self.config(width=self.button_width, height=self.button_height)
        self.pack_propagate(False)
         
        # 主容器
        self.content_frame = tk.Frame(self, bg="white")
        self.content_frame.pack(fill=tk.BOTH, expand=True)
 
        # 图标区域
        self.icon_label = tk.Label(self.content_frame, 
                                 image=image, 
                                 bg="white",
                                 borderwidth=0)
        self.icon_label.image = image
        self.icon_label.pack(pady=(5, 2), expand=True)
 
        # 文本区域
        processed_text = self.process_text(text)
        self.text_label = tk.Label(self.content_frame, 
                                 text=processed_text,
                                 wraplength=self.button_width-15,
                                 justify="center",
                                 font=("微软雅黑", 8),
                                 bg="white",
                                 borderwidth=0)
        self.text_label.pack(pady=(0, 5))
 
        # 事件绑定
        self.bind_all_children("<Button-1>", lambda e: command())
        self.bind("<Enter>", self.on_enter)
        self.bind("<Leave>", self.on_leave)
 
    def bind_all_children(self, event, handler):
        for child in self.winfo_children():
            child.bind(event, handler)
            if isinstance(child, tk.Frame):
                for subchild in child.winfo_children():
                    subchild.bind(event, handler)
 
    def process_text(self, text):
        text = text[:20]
        words = text.split()
        lines = []
        current_line = []
         
        for word in words:
            if len(word) > self.max_chars:
                chunks = [word[i:i+self.max_chars] for i in range(0, len(word), self.max_chars)]
                current_line.extend(chunks)
            else:
                current_line.append(word)
             
            if sum(len(w) for w in current_line) + (len(current_line)-1) > self.max_chars:
                lines.append(" ".join(current_line[:-1]))
                current_line = [current_line[-1]]
             
            if len(lines) >= self.max_lines:
                break
         
        if current_line and len(lines) < self.max_lines:
            lines.append(" ".join(current_line))
         
        return "\n".join(lines[:self.max_lines]) + ("..." if len(text) > 20 else "")
 
    def on_enter(self, event):  
        # pylint: disable=unused-argument
        self.config(bg="#e0e0e0")
        self.content_frame.config(bg="#e0e0e0")
        for child in self.content_frame.winfo_children():
            child.config(bg="#e0e0e0")
 
    def on_leave(self, event):  
        # pylint: disable=unused-argument
        self.config(bg="white")
        self.content_frame.config(bg="white")
        for child in self.content_frame.winfo_children():
            child.config(bg="white")
 
 
class AppLauncher:
    def __init__(self, root):
        self.root = root
        self.root.title("程序启动器")
        self.root.geometry("530x400")
        self.root.resizable(False, False)
        self.icon_size = 32
         
        # 设置窗口图标
        self.set_window_icon()
         
        # 主界面
        self.main_frame = ttk.Frame(root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
         
        # 初始化默认图标
        self.default_icon = self.get_default_icon()
         
        # 组件初始化
        self.create_category_panel()
        self.create_program_panel()
         
        # 数据初始化
        self.current_dir = get_current_path()  # 使用通用路径获取方法
        self.scan_directory()
        self.populate_categories()
        self.show_category("所有程序")
 
    def set_window_icon(self):
        """设置窗口图标"""
        try:
            # 使用与编译后的exe相同的图标
            exe_path = sys.executable
            icon_img = get_exe_icon(exe_path)
            if icon_img:
                icon_img.save('temp_icon.ico', 'ICO')
                self.root.iconbitmap('temp_icon.ico')
                os.remove('temp_icon.ico')
            else:
                print("无法提取exe图标,使用默认图标")
                self.root.iconbitmap('icon/app.ico')
        except Exception as e:
            print(f"设置窗口图标失败: {e}")
 
    def resolve_shortcut(self, path):
        """解析快捷方式"""
        try:
            shell = Dispatch('WScript.Shell')
            shortcut = shell.CreateShortCut(path)
            return shortcut.TargetPath
        except Exception as _e:
            print(f"快捷方式解析失败: {path}")
            return None
 
    def get_program_icon(self, path):
        """增强图标获取"""
        try:
            # 处理快捷方式
            if path.lower().endswith('.lnk'):
                target = self.resolve_shortcut(path)
                if target and os.path.exists(target):
                    path = target
             
            # 获取图标
            large, _ = win32gui.ExtractIconEx(path, 0)
            if not large:
                raise Exception("No icons found")
                 
            hicon = large[0]
            hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
            hbmp = win32ui.CreateBitmap()
            hbmp.CreateCompatibleBitmap(hdc, self.icon_size, self.icon_size)
            hdc = hdc.CreateCompatibleDC()
            hdc.SelectObject(hbmp)
            hdc.DrawIcon((0, 0), hicon)
             
            bmpstr = hbmp.GetBitmapBits(True)
            img = Image.frombuffer('RGBA', (self.icon_size, self.icon_size), 
                                 bmpstr, 'raw', 'BGRA', 0, 1)
            return ImageTk.PhotoImage(img)
        except Exception as _e:  
            print(f"图标加载失败: {os.path.basename(path)}")
            return self.default_icon
 
    def get_default_icon(self):
        """系统默认图标"""
        try:
            # 使用文件夹图标
            shell32 = "C:\\Windows\\System32\\shell32.dll"
            large, _ = win32gui.ExtractIconEx(shell32, 3)
            hicon = large[0]
             
            hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
            hbmp = win32ui.CreateBitmap()
            hbmp.CreateCompatibleBitmap(hdc, self.icon_size, self.icon_size)
            hdc = hdc.CreateCompatibleDC()
            hdc.SelectObject(hbmp)
            hdc.DrawIcon((0, 0), hicon)
             
            bmpstr = hbmp.GetBitmapBits(True)
            img = Image.frombuffer('RGBA', (self.icon_size, self.icon_size),
                                 bmpstr, 'raw', 'BGRA', 0, 1)
            return ImageTk.PhotoImage(img)
        except Exception as _e:  
            print(f"默认图标加载失败: {_e}")
            return ImageTk.PhotoImage(Image.new('RGBA', (self.icon_size, self.icon_size), (240, 240, 240)))
 
    def create_category_panel(self):
        """分类面板(宽度99)"""
        self.category_frame = ttk.Frame(self.main_frame, width=99)
        self.category_frame.pack(side=tk.LEFT, fill=tk.Y, padx=3)
         
        self.category_canvas = tk.Canvas(self.category_frame, 
                                       width=99,
                                       highlightthickness=0)
        scrollbar = ttk.Scrollbar(self.category_frame, 
                                orient="vertical", 
                                command=self.category_canvas.yview)
        self.category_container = ttk.Frame(self.category_canvas)
         
        self.category_container.bind("<Configure>", 
            lambda e: self.category_canvas.configure(
                scrollregion=self.category_canvas.bbox("all"),
                width=99
            ))
         
        self.category_canvas.create_window((0,0), 
                                        window=self.category_container, 
                                        anchor="nw",
                                        width=99)
        self.category_canvas.configure(yscrollcommand=scrollbar.set)
         
        self.category_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
         
        ttk.Label(self.category_container, 
                text="应用分类", 
                font=("微软雅黑", 9),
                padding=3).pack(pady=5)
 
    def create_program_panel(self):
        """程序显示面板"""
        self.program_frame = ttk.Frame(self.main_frame)
        self.program_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
         
        self.program_canvas = tk.Canvas(self.program_frame, 
                                     highlightthickness=0)
        scrollbar = ttk.Scrollbar(self.program_frame, 
                                orient="vertical", 
                                command=self.program_canvas.yview)
        self.program_container = ttk.Frame(self.program_canvas)
         
        self.program_container.bind("<Configure>", 
            lambda e: self.program_canvas.configure(
                scrollregion=self.program_canvas.bbox("all")
            ))
         
        self.program_canvas.create_window((0,0), 
                                       window=self.program_container, 
                                       anchor="nw")
        self.program_canvas.configure(yscrollcommand=scrollbar.set)
         
        self.program_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
 
    def scan_directory(self):
        """扫描目录"""
        self.categories = {"所有程序": []}
        executable_ext = ['.exe', '.bat', '.cmd', '.lnk']
         
        for item in os.listdir(self.current_dir):
            path = os.path.join(self.current_dir, item)
             
            if os.path.isdir(path):
                self.categories[item] = []
                for root, _, files in os.walk(path):
                    for file in files:
                        if os.path.splitext(file)[1].lower() in executable_ext:
                            full_path = os.path.join(root, file)
                            self.categories[item].append({
                                "name": os.path.splitext(file)[0],
                                "path": full_path
                            })
                            self.categories["所有程序"].append({
                                "name": os.path.splitext(file)[0],
                                "path": full_path
                            })
            else:
                if os.path.splitext(item)[1].lower() in executable_ext:
                    self.categories["所有程序"].append({
                        "name": os.path.splitext(item)[0],
                        "path": path
                    })
 
    def populate_categories(self):
        """填充分类"""
        for category in self.categories.keys():
            btn = ttk.Button(
                self.category_container,
                text=category,
                command=lambda c=category: self.show_category(c),
                width=12
            )
            btn.pack(fill=tk.X, padx=2, pady=2)
 
    def show_category(self, category):
        """显示分类"""
        for widget in self.program_container.winfo_children():
            widget.destroy()
             
        programs = self.categories.get(category, [])
         
        if not programs:
            ttk.Label(self.program_container, text="该分类暂无应用").pack(pady=50)
            return
         
        max_columns = 5
        row = col = 0
         
        for idx, program in enumerate(programs):
            col = idx % max_columns
            row = idx // max_columns
             
            button = UniformButton(
                parent=self.program_container,
                text=program["name"],
                image=self.get_program_icon(program["path"]),
                command=lambda p=program["path"]: self.launch_program(p)
            )
            button.grid(row=row, column=col, padx=3, pady=3, sticky="nsew")
             
            self.program_container.grid_columnconfigure(col, weight=1)
            self.program_container.grid_rowconfigure(row, weight=1)
 
    def launch_program(self, path):
        """启动程序"""
        try:
            subprocess.Popen(f'"{path}"', shell=True)
        except Exception as _e:  
            print(f"启动失败: {_e}")
 
 
if __name__ == "__main__":
    root_window = tk.Tk()  
    style = ttk.Style()
    style.configure("TButton", 
                  padding=3, 
                  font=("微软雅黑", 8),
                  width=12)
    style.map("TButton",
            background=[('active', '#f0f0f0')])
    AppLauncher(root_window)
    root_window.mainloop()

5. 总结:

通过本文的介绍,我们使用Python实现了一个集成工具箱应用,涵盖了从程序路径获取、图标提取到程序分类展示和启动等多个功能模块。这个工具不仅能帮助用户轻松管理并启动程序,还具备了良好的用户体验。通过使用Tkinter和Win32API,我们可以创建一个强大且易于扩展的应用,帮助提升工作效率。

如果你对这个项目感兴趣,可以根据自己的需求进一步优化和扩展功能,例如支持更多类型的程序文件、改进图标展示、增加搜索和排序功能等。希望通过本文的示例,你能深入理解如何利用Python构建桌面应用,提升你的开发技能。

作者:探客白泽

物联沃分享整理
物联沃-IOTWORD物联网 » Python与Tkinter实战集成工具箱开发指南

发表回复