Python中基于Tkinter+PyInstaller将程序打包成exe工具(第24节)


在本教程第21章【Python程序打包】的第1节教程中,已经详细介绍了如何通过Python的PyInstaller模块将Python程序打包成exe文件。在Windows系统中,exe文件是最常见的可执行文件,直接双击即可打开程序。目前,Windows中大部分软件包括聊天软件、游戏软件、办公软件等基本上都会通过exe文件进行启动。

随着现代全球化软件的开发,开发者需要解决如何将Python程序移植到不同的操作系统上运行,打包成exe可执行文件是解决跨平台运行问题的直接方法,它使得Python程序可以在没有安装Python解释器的计算机上运行,从而扩大了软件的应用范围。

对于非技术用户来说,打包后的exe文件无需用户安装Python环境即可运行,直接双击即可使用,极大降低了使用门槛。

exe文件具有跨平台兼容性,Python程序打包成exe文件后,可以将程序代码、依赖库及运行所需要的环境整合到单一文件或目录中,支持Windows、Mac、Linux等多平台运行,无需针对不同系统调整环境配置。 ‌

PyInstaller是Python中免费且开源的第三方库,主要功能是将Python脚本及其依赖项打包为独立的exe可执行文件,使得Python程序可以在未安装Python解释器的计算机上直接运行,本节教程将介绍如何通过Python的Tkinter库和PyInstaller库利用图形用户界面将Python程序打包成独立的exe可执行文件。

1、Python中基于Tkinter+PyInstaller将程序打包成exe工具介绍

首先,电脑上需要提前安装PyInstaller库,可以通过pip执行命令安装“pip install pyinstaller”。PyInstaller库将Python程序打包成exe时,需要通过配置文件来定制打包过程,配置文件允许开发者指定不同的选项,比如选择单文件模式还是多文件模式,打包后是否显示控制台窗口,打包后是否显示软件ico图标,打包后是否显示软件信息等,我们可以通过Tkinter简单的GUI图形界面一键配置所有的打包选项。

功能说明

单文件模式:如果勾选“单文件打包”,可以将Python程序文件打包进一个单独的exe文件中,单个文件便于用户下载和安装。但是,单文件可能无法处理大量的资源文件,特别是那些动态加载的资源,同时单文件由于路径问题,无法生成动态的资源文件,比如无法生成数据库“db”文件。

多文件模式:如果没有勾选“单文件打包”,则默认以“多文件模式”模式打包。在多文件模式中,PyInstaller会创建一个包含所有必需文件的目录,通常包括一个exe文件、一个资源文件夹以及其他依赖文件。开发者在引用资源文件时,可以直接使用文件名,因为所有文件都在同一个目录中。

版本信息配置:如果勾选“添加版本信息”,且成功设置版本信息后,会在当前脚本工作目录下创建一个名为“version_info.txt”的配置文件。在Windows平台上,为应用程序添加版本信息是一个非常有用的功能,版本信息可以帮助用户和开发者识别应用程序的版本、公司名称、版权信息等,同时也能提升应用程序的专业性和可信度。查看软件的版本信息,可以通过鼠标右键点击exe文件 -> 属性 -> 详细信息:可以看到版本号、公司名称、版权信息等。

配置程序图标:如果想为打包后的exe文件添加图标,可以点击“选择图标”按钮,选择指定的ico图标,如果你没有ico图标文件,可以使用在线图标制作工具将png、jpeg等格式的图片转换为ico格式。

配置控制台窗口:如果勾选“无控制台窗口”,当我们双击打包后的exe文件时,不会显示黑色的控制台窗口。对于GUI图形应用程序,通过勾选这个选项可以避免出现命令行窗口。如果你的应用是基于控制台(例如,使用了input()函数),则不能勾选“无控制台窗口”,运行exe文件时必须显示控制台窗口。

2、Python中基于Tkinter+PyInstaller将程序打包成exe工具界面演示

Python中基于Tkinter+PyInstaller将程序打包成exe工具

Python中基于Tkinter+PyInstaller将程序打包成exe工具

3、Python中基于Tkinter+PyInstaller将程序打包成exe工具,完整代码如下所示:

动手练一练:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os, sys, subprocess, threading, time, webbrowser

# 获取当前脚本文件所在的目录
default_directory = os.path.dirname(os.path.abspath(__file__))
# 将当前脚本文件所在的目录设置为工作目录
os.chdir(default_directory)

# 定义版本信息文件
version_file = "version_info.txt"

try:
    # 检测是否安装了pyinstaller模块
    import PyInstaller
except ImportError: 
    # 如果没有安装,则提示先安装pyinstaller模块
    messagebox.showwarning("警告", "请先安装pyinstaller模块:pip install pyinstaller")
    # 退出程序
    sys.exit()

# 定义主窗口
class ExePack:
    def __init__(self, app):
        self.app = app
        self.running = False
        self.start_time = 0
        self.elapsed_time = 0
        self.app.title("Python程序利用PyInstaller打包exe工具")

        # 创建窗口“帮助”菜单栏
        self.menu_bar = tk.Menu(self.app)
        self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
        self.file_menu.add_command(label="关于程序", command=self.program_about)
        self.file_menu.add_command(label="程序讲解", command=self.help_window)
        self.file_menu.add_separator()  
        self.file_menu.add_command(label="退出", command=self.app.quit)
        self.menu_bar.add_cascade(label="帮助", menu=self.file_menu)
        self.app.config(menu=self.menu_bar)

        # 设置窗口居中
        window_width = 800
        window_height = 660
        screen_width = self.app.winfo_screenwidth()
        screen_height = self.app.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        self.app.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))
        self.app.resizable(width=False, height=False)

        # 配置根窗口网格布局
        self.app.grid_columnconfigure(0, weight=1)
        self.app.grid_rowconfigure(0, weight=1)

        # 窗口样式配置
        self.style = ttk.Style()
        self.set_styles()

        # 主界面创建
        self.create_widgets()

    # 定义样式函数
    def set_styles(self):
        self.style.configure("TButton", padding=6)
        self.style.configure("TLabel", padding=3)
        self.style.configure("TEntry", padding=3)
        self.style.configure("Blue.TLabel", foreground="blue", font=("宋体", 20))

    # 定义主界面组件
    def create_widgets(self):
        main_frame = ttk.Frame(self.app, padding=15)
        main_frame.grid(row=0, column=0, sticky="nsew")
        main_frame.grid_columnconfigure(1, weight=1)
        main_frame.grid_rowconfigure(3, weight=1)

        # 定义标题面板
        title_label = ttk.Label(main_frame, text="Python程序利用PyInstaller打包exe工具", font=('宋体', 20, 'bold'))
        title_label.grid(row=0, column=0, columnspan=3, pady=15)

        # Python脚本文件选择
        file_label = ttk.Label(main_frame, text="Python脚本路径:")
        file_label.grid(row=1, column=0, sticky="w", pady=3)
        self.file_entry = ttk.Entry(main_frame)
        self.file_entry.grid(row=1, column=1, sticky="ew", padx=5)
        file_button = ttk.Button(main_frame, text="浏览", command=self.choose_file)
        file_button.grid(row=1, column=2, padx=5)

        # 配置打包选项框架
        config_frame = ttk.LabelFrame(main_frame, text="打包选项", padding=10)
        config_frame.grid(row=2, column=0, columnspan=3,
                           pady=10, sticky="ew")
        config_frame.grid_columnconfigure(1, weight=1)

        # 打包选项内容
        self.single_var = tk.BooleanVar()
        single_set = ttk.Checkbutton(config_frame, text="单文件打包", variable=self.single_var)
        single_set.grid(row=0, column=0, sticky="w", padx=5)

        # 选择是否显示控制台窗口
        self.no_console_var = tk.BooleanVar(value=True)
        no_console_set = ttk.Checkbutton(config_frame, text="无控制台窗口", variable=self.no_console_var)
        no_console_set.grid(row=0, column=1, sticky="w", padx=5)

        # 图标设置
        icon_label = ttk.Label(config_frame, text="程序图标:")
        icon_label.grid(row=1, column=0, sticky="w", pady=5)
        self.icon_entry = ttk.Entry(config_frame)
        self.icon_entry.grid(row=1, column=1, sticky="ew", padx=5)
        icon_choose = ttk.Button(config_frame, text="选择图标", command=self.choose_icon, width=10)
        icon_choose.grid(row=1, column=2)

        # 选择是否添加版本信息
        version_set = ttk.Button(config_frame, text="设置版本信息", command=self.add_version, width=10)
        version_set.grid(row=2, column=0)
        self.version_var = tk.BooleanVar()
        version_add = ttk.Checkbutton(config_frame, text="添加版本信息", variable=self.version_var)
        version_add.grid(row=2, column=1, sticky="w", padx=5)

        # 选择输出目录
        output_label = ttk.Label(main_frame, text="输出目录:")
        output_label.grid(row=3, column=0, sticky="w", pady=3)
        self.output_entry = ttk.Entry(main_frame)
        self.output_entry.grid(row=3, column=1, sticky="ew", padx=5)
        output_button = ttk.Button(main_frame, text="选择目录", command=self.choose_output)
        output_button.grid(row=3, column=2, padx=5)

        # 显示日志区域
        report_frame = ttk.LabelFrame(main_frame, text="打包日志", padding=5)
        report_frame.grid(row=4, column=0, columnspan=3, pady=10, sticky="nsew")
        report_frame.grid_rowconfigure(0, weight=1)
        report_frame.grid_columnconfigure(0, weight=1)

        self.report_text = tk.Text(report_frame, wrap=tk.WORD, font=('宋体', 10), height=10)
        scrollbar = ttk.Scrollbar(report_frame, orient="vertical", command=self.report_text.yview)
        self.report_text.configure(yscrollcommand=scrollbar.set)
        self.report_text.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")

        # 打包按钮
        self.start_button = ttk.Button(main_frame, text="开始打包", command=self.pack_start)
        self.start_button.grid(row=5, column=1, pady=10)

        # 打包时间统计
        time_frame = ttk.LabelFrame(main_frame)
        time_frame.grid(row=6, column=1)

        self.count_label = ttk.Label(time_frame, text="打包时间统计:", style="Blue.TLabel")
        self.count_label.grid(row=0, column=0, sticky="w")
        self.time_label = ttk.Label(time_frame, text="00:00:00", style="Blue.TLabel")
        self.time_label.grid(row=0, column=1, sticky="w")

    # 关于程序页面
    def program_about(self):
        about = tk.Tk()
        about.title('关于程序')

        # 设置窗口居中
        window_width = 570
        window_height = 490
        screen_width = about.winfo_screenwidth()
        screen_height = about.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        about.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))
        about.resizable(width=False, height=False)

        about_frame = tk.Frame(about, width=570, height=490)
        about_frame.pack()
        tk.Label(about_frame, text='PyInstaller打包exe工具', font=("宋体", 16)).place(x=150, y=20)
        tk.Label(about_frame, text='使用编程语言:Python', font=("宋体", 14)).place(x=50, y=70)
        tk.Label(about_frame, text='提前安装库:pip install pyinstaller', font=("宋体", 14)).place(x=50, y=120)
        tk.Label(about_frame, text='版本信息文件名:version_info.txt', font=("宋体", 14)).place(x=50, y=170)
        tk.Label(about_frame, text='设置版本信息:版本信息设置成功后自动保存为txt文件', font=("宋体", 14)).place(x=50, y=220)
        tk.Label(about_frame, text='文件版本输入:必须输入整数', font=("宋体", 14)).place(x=50, y=270)
        tk.Label(about_frame, text='打包时间统计:可以统计开始打包到结束的时间', font=("宋体", 14)).place(x=50, y=320)
        tk.Label(about_frame, text='选择程序图标:选择指定ico图标(可选)', font=("宋体", 14)).place(x=50, y=370)
        tk.Label(about_frame, text='创作者:www.pyhint.com', font=("宋体", 14)).place(x=50, y=420)
        about.mainloop()

    # 程序讲解页面
    def help_window(self):
        webbrowser.open("https://www.pyhint.com/article/157.html")

    # 创建窗口,用于添加版本信息
    def add_version(self):
        self.add_window = tk.Toplevel(self.app)
        self.add_window.title("添加版本信息")

        # 设置窗口居中
        window_width = 600
        window_height = 540
        screen_width = self.add_window.winfo_screenwidth()
        screen_height = self.add_window.winfo_screenheight()
        x = (screen_width - window_width) / 2
        y = (screen_height - window_height) / 2
        self.add_window.geometry('%dx%d+%d+%d' % (window_width, window_height, x, y))
        self.add_window.resizable(width=False, height=False)

        # 创建标题标签控件
        Show_result = tk.Label(self.add_window, text=">>>添加版本信息<<<", bg = "white", fg = "black", font = ("宋体", 18), bd = '0', anchor = 'center')
        Show_result.place(x = 90, y = 30, width = 400, height = 50)

        # 创建一个StringVar变量并设置文件版本默认值
        tk.Label(self.add_window, text="文件版本(整数):", font = ('宋体', 12), width = 20).place(x = 75, y = 120, anchor = 'nw')

        # 创建四个输入框
        self.entry1 = tk.Entry(self.add_window, font = ('宋体', 15), width = 4)
        self.entry1.place(x = 240, y = 120, anchor = 'nw')
        label1 = tk.Label(self.add_window, text=",", font = ('宋体', 12), width = 1)
        label1.place(x = 290, y = 120)
        self.entry2 = tk.Entry(self.add_window, font = ('宋体', 15), width = 4)
        self.entry2.place(x = 310, y = 120, anchor = 'nw')
        label2 = tk.Label(self.add_window, text=",", font = ('宋体', 12), width = 1)
        label2.place(x = 360, y = 120)
        self.entry3 = tk.Entry(self.add_window, font = ('宋体', 15), width = 4)
        self.entry3.place(x = 380, y = 120, anchor = 'nw')
        label3 = tk.Label(self.add_window, text=",", font = ('宋体', 12), width = 1)
        label3.place(x = 430, y = 120)
        self.entry4 = tk.Entry(self.add_window, font = ('宋体', 15), width = 4)
        self.entry4.place(x = 450, y = 120, anchor = 'nw')

        # 插入默认值
        self.entry1.insert(0, "2")
        self.entry2.insert(0, "3")
        self.entry3.insert(0, "4")
        self.entry4.insert(0, "5")

        # 创建一个StringVar变量并设置开发软件的公司名默认值
        company_name_value = tk.StringVar()
        company_name_value.set("xxx有限公司")
        tk.Label(self.add_window, text="开发软件的公司名:", font = ('宋体', 12), width = 20).place(x = 75, y = 170, anchor = 'nw')
        self.company_name_entry = tk.Entry(self.add_window, textvariable=company_name_value, font = ('宋体', 15), width = 25)
        self.company_name_entry.place(x = 240, y = 170, anchor = 'nw')

        # 创建一个StringVar变量并设置软件描述默认值
        description_value = tk.StringVar()
        description_value.set("xxx管理工具")
        tk.Label(self.add_window, text="软件描述:", font = ('宋体', 12), width = 20).place(x = 75, y = 220, anchor = 'nw')
        self.description_entry = tk.Entry(self.add_window, textvariable=description_value, font = ('宋体', 15), width = 25)
        self.description_entry.place(x = 240, y = 220, anchor = 'nw')

        # 创建一个StringVar变量并设置软件版本默认值
        file_version_value = tk.StringVar()
        file_version_value.set("2.3.4.5")
        tk.Label(self.add_window, text="软件版本:", font = ('宋体', 12), width = 20).place(x = 75, y = 270, anchor = 'nw')
        self.file_version_entry = tk.Entry(self.add_window, textvariable=file_version_value, font = ('宋体', 15), width = 25)
        self.file_version_entry.place(x = 240, y = 270, anchor = 'nw')

        # 创建一个StringVar变量并设置产品名称默认值
        filename_value = tk.StringVar()
        filename_value.set("xxx.exe")
        tk.Label(self.add_window, text="产品名称:", font = ('宋体', 12), width = 20).place(x = 75, y = 320, anchor = 'nw')
        self.filename_entry = tk.Entry(self.add_window, textvariable=filename_value, font = ('宋体', 15), width = 25)
        self.filename_entry.place(x = 240, y = 320, anchor = 'nw')

        # 创建一个StringVar变量并设置软件版权声明默认值
        copyright_value = tk.StringVar()
        copyright_value.set("xxx有限公司版权所有")
        tk.Label(self.add_window, text="软件版权声明:", font = ('宋体', 12), width = 20).place(x = 75, y = 370, anchor = 'nw')
        self.copyright_entry = tk.Entry(self.add_window, textvariable=copyright_value, font = ('宋体', 15), width = 25)
        self.copyright_entry.place(x = 240, y = 370, anchor = 'nw')

        # 创建一个StringVar变量并设置产品名称默认值
        product_name_value = tk.StringVar()
        product_name_value.set("xxx产品名")
        tk.Label(self.add_window, text="产品名称:", font = ('宋体', 12), width = 20).place(x = 75, y = 420, anchor = 'nw')
        self.product_name_entry = tk.Entry(self.add_window, textvariable=product_name_value, font = ('宋体', 15), width = 25)
        self.product_name_entry.place(x = 240, y = 420, anchor = 'nw')

        # 获取文件版本输入的内容,返回一个元组
        def get_entries():
            # 获取每个Entry组件的内容
            entry1_content = int(self.entry1.get())
            entry2_content = int(self.entry2.get())
            entry3_content = int(self.entry3.get())
            entry4_content = int(self.entry4.get())

            # 将内容组合成一个元组
            content_tuple = (entry1_content, entry2_content, entry3_content, entry4_content)
            return content_tuple

        # 生成txt版本信息配置文件
        def create_file():
            # 获取文件版本输入框内的内容
            numbers = [self.entry1.get(), self.entry2.get(), self.entry3.get(), self.entry4.get()]

            # 检查每个输入框是否为空
            for number in numbers:
                if not number:
                    messagebox.showerror("错误", "输入的文件版本不能为空!")
                    return
                else:
                    # 判断是否有效整数
                    try:
                        num_results = int(number)
                    except ValueError:
                        messagebox.showwarning("警告", "文件版本请输入有效的整数!")
                        return

            # 获取Entry组件的内容
            filevers = get_entries()
            company_name = self.company_name_entry.get()
            description = self.description_entry.get()
            file_version = self.file_version_entry.get()
            filename = self.filename_entry.get()
            copyright = self.copyright_entry.get()
            product_name = self.product_name_entry.get()

            # 定义要写入的内容
            lines = f'''
# UTF-8
VSVersionInfo(
ffi=FixedFileInfo(
# 注意:filevers和prodvers是包含4个元素的元组,可以将不需要的元素设置为0
filevers={filevers},  # 文件版本,比如2.3.4.5,鼠标悬浮在exe文件上会显示,也会在详细信息中显示
prodvers={filevers}, # 生产商
# 两个位掩码,无需更改它
mask=0x3f,
flags=0x0,
OS=0x4, # 设置此文件的操作系统,0x4表示NT,无需更改它
fileType=0x1, # 文件类型,0x1表示该文件是一个应用程序
subtype=0x0, # 文件的功能,0x0表示此类型未定义函数
date=(0, 0) # 创建日期和时间戳
),
kids=[
StringFileInfo(
[
StringTable(
    u'040904B0',
    [StringStruct(u'CompanyName', u'{company_name}'),                # 开发软件的公司名
    StringStruct(u'FileDescription', u'{description}'),                  # 软件描述
    StringStruct(u'FileVersion', u'{file_version}'),                     # 软件版本
    StringStruct(u'InternalName', u'{filename}'),                          # 内部名称
    StringStruct(u'LegalCopyright', u'{copyright}'),    # 软件版权声明
    StringStruct(u'OriginalFilename', u'{filename}'),                     # 原始文件名
    StringStruct(u'ProductName', u'{product_name}'),                     # 产品名称
    StringStruct(u'ProductVersion', u'{file_version}')])                      #产品版本
]),
VarFileInfo([VarStruct(u'Translation', [2052, 1200])]) # 语言,中文简体
]
)
'''
            # 获取所有输入框的内容
            entries = [company_name, description, file_version, filename, copyright, product_name,]

            # 检查每个输入框是否为空
            for entry in entries:
                if not entry:
                    messagebox.showerror("错误", "输入的内容不能为空!")
                    return

            # 创建txt文件
            try:
                with open(version_file, 'w', encoding='utf-8') as file:
                    file.write(lines)
                self.version_var.set(True)
                self.add_window.destroy()
                messagebox.showinfo("成功", f"版本信息设置成功!")
            except Exception as e:
                messagebox.showerror("错误", f"版本信息设置时发生错误:{e}")

        # 定义“保存”按钮
        save_button = ttk.Button(self.add_window, text="保存", command=create_file, width=15)
        save_button.place(x = 145, y = 470, anchor = 'nw')

        # 定义“取消”按钮
        cancel_button = ttk.Button(self.add_window, text="取消", command=self.add_window.destroy, width=15)
        cancel_button.place(x = 320, y = 470, anchor = 'nw')

    # 选择要打包的Python脚本文件
    def choose_file(self):
        path = filedialog.askopenfilename(
            filetypes=[("Python文件", "*.py"), ("所有文件", "*.*")])
        if path:
            self.file_entry.delete(0, tk.END)
            self.file_entry.insert(0, os.path.normpath(path))

    # 选择要打包的图标文件
    def choose_icon(self):
        path = filedialog.askopenfilename(
            filetypes=[("图标文件", "*.ico"), ("所有文件", "*.*")])
        if path:
            self.icon_entry.delete(0, tk.END)
            self.icon_entry.insert(0, os.path.normpath(path))

    # 选择打包后输出的目录
    def choose_output(self):
        path = filedialog.askdirectory()
        if path:
            self.output_entry.delete(0, tk.END)
            self.output_entry.insert(0, os.path.normpath(path))

    # 在日志区域显示消息
    def report_message(self, message):
        self.report_text.insert(tk.END, message + "\n")
        self.report_text.see(tk.END)
        self.app.update_idletasks()

    # 显示打包过程时间
    def update(self):
        if self.running:
            self.elapsed_time = time.time() - self.start_time
            self.time_label.config(text=self.format_time(self.elapsed_time))
            self.app.after(1000, self.update)

    # 启动打包过程
    def pack_start(self):
        self.elapsed_time = 0
        self.time_label.config(text="00:00:00")
        file_path = self.file_entry.get()
        output_path = self.output_entry.get()
        if not file_path:
            messagebox.showerror("错误", "请选择要打包的Python脚本!")
            return
        if not os.path.exists(file_path):
            messagebox.showerror("错误", "指定的Python脚本不存在!")
            return
        if not output_path:
            messagebox.showerror("错误", "请选择输出目录!")
            return

        # 创建PyInstaller命令
        cmd = ["pyinstaller", "--clean"]

        # 打包命令中添加基本选项
        if self.single_var.get():
            cmd.append("--onefile")
        if self.no_console_var.get():
            cmd.append("--noconsole")
        if self.version_var.get():
            if os.path.exists(version_file):
                cmd.extend(["--version-file", f'"{version_file}"'])
            else:
                messagebox.showerror("错误", "请先设置版本信息!")
                return

        # 打包命令中添加图标
        if icon_path := self.icon_entry.get():
            if not os.path.exists(icon_path):
                messagebox.showerror("错误", "指定的图标文件不存在!")
                return
            cmd.extend(["--icon", f'"{icon_path}"'])

        # 打包命令中添加输出目录
        if output_path:
            cmd.extend(["--distpath", f'"{output_path}"'])

        cmd.append(f'"{file_path}"')

        # 初始化界面状态
        self.start_button.config(state=tk.DISABLED)
        self.report_text.delete(1.0, tk.END)
        self.report_message(">>> 开始打包命令: " + " ".join(cmd))

        if not self.running:
            self.start_time = time.time() - self.elapsed_time
            self.running = True
            self.update()

        # 通过线程执行打包命令
        threading.Thread(
            target=self.start_command,
            args=(cmd,),
            daemon=True
        ).start()

    # 开始执行打包命令
    def start_command(self, cmd):
        try:
            process = subprocess.Popen(
                " ".join(cmd),
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                shell=True,
                encoding="utf-8",
                errors="replace"
            )

            # 打包过程实时输出打包信息
            while True:
                output = process.stdout.readline()
                if output == "" and process.poll() is not None:
                    break
                if output:
                    self.app.after(0, self.report_message, output.strip())

            # 打包返回结果处理
            result = process.poll()
            if result == 0:
                if self.running:
                    self.running = False
                self.app.after(0, self.report_message, ">>> 打包成功完成!")
                self.app.after(0, messagebox.showinfo,
                                "完成", f"打包操作成功完成!耗时:{int(self.elapsed_time)}秒")
            else:
                if self.running:
                    self.running = False
                error_message = f">>> 打包失败,错误码:{result}"
                self.app.after(0, self.report_message, error_message)
                self.app.after(0, messagebox.showerror,
                                "错误", f"打包失败,错误码:{result}")

        except Exception as e:
            error_message = f">>> 发生异常错误:{str(e)}"
            self.app.after(0, self.report_message, error_message)
            self.app.after(0, messagebox.showerror,
                            "异常错误", f"发生未预期错误:{str(e)}")
        finally:
            self.app.after(0, self.start_button.config, {"state": tk.NORMAL})

    # 格式化时间
    def format_time(self, elapsed_time):
        hours, remainder = divmod(int(elapsed_time), 3600)
        minutes, seconds = divmod(remainder, 60)
        return f"{hours:02}:{minutes:02}:{seconds:02}"

# 当前模块直接被执行
if __name__ == "__main__":
    # 创建主窗口
    app = tk.Tk()
    window = ExePack(app)
    # 开启主循环,让窗口处于显示状态
    app.mainloop()