使用PyInstaller & InstallForge为Windows打包Tkinter应用程序

369 阅读28分钟

如果你不能与其他人分享你自己的桌面应用程序,那就没有什么乐趣了--无论这意味着以商业方式发布它,在网上分享它,还是只是把它送给你认识的人。分享你的应用程序可以让其他人从你的辛勤工作中受益

好消息是,有一些工具可以帮助你的Python应用程序做到这一点,这些工具与使用Tkinter构建的应用程序配合得很好。在本教程中,我们将看看最流行的打包Python应用程序的工具。PyInstaller

本教程分为一系列的步骤,使用PyInstaller首先构建简单的,然后是越来越复杂的Tkinter应用程序,使其成为Windows上可分发的EXE文件。你可以选择完全跟随它,或者跳到与你自己的项目最相关的例子。

最后我们用InstallForge来创建一个可分发的Windows安装程序。

你总是需要在你的目标系统上编译你的应用程序。因此,如果你想创建一个Mac .app,你需要在Mac上完成,如果是EXE,你需要使用Windows。

Example Installer for Windows 用于Windows的安装程序示例

如果你没有耐心,你可以先下载Windows的安装实例

要求

PyInstaller开箱即与Tkinter一起工作,截至目前,PyInstaller的当前版本与Python 3.6+兼容。无论你在做什么项目,你都应该能够打包你的应用程序。

你可以使用pip 安装PyInstaller

bash

pip3 install PyInstaller

如果你在打包你的应用程序遇到问题,你的第一步应该是更新你的PyInstallerhooks打包的最新版本,使用

bash

pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib

钩子模块包含了PyInstaller的特定打包指令,该模块会定期更新。

在虚拟环境中安装(可选)

你也可以选择将PyInstaller安装在一个虚拟环境中(或你的应用程序虚拟环境),以保持你的环境清洁。

bash

python3 -m venv packenv

一旦创建,通过从命令行运行激活虚拟环境----。

bash

call packenv\scripts\activate.bat

最后,安装所需的库。

蟒蛇

pip3 install PyInstaller

开始使用

从一开始就开始打包你的应用程序是个好主意,这样你就可以在开发过程中确认打包仍然有效。如果你增加了额外的依赖性,这一点尤其重要。如果你只在最后才考虑打包,那么就很难准确地调试出问题所在

在这个例子中,我们将从一个简单的骨架应用开始,它并不做任何有趣的事情。一旦我们得到了基本的打包过程,我们将扩展应用程序以包括图标和数据文件。我们将在进行过程中确认构建。

首先,为你的应用程序创建一个新的文件夹,然后在一个名为app.py 的文件中添加以下骨架应用程序。你也可以下载源代码和相关文件

蟒蛇

import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("<Button-1>", handle_button_press)
button.pack()

# Start the event loop.
window.mainloop()

这是一个基本的裸体应用,它创建了一个窗口,并为其添加了一个简单的按钮。你可以按以下方式运行这个应用程序。

bash

python app.py

这应该会产生以下窗口(在Windows 11上)。

Simple skeleton app in Tkinter Tkinter中的简单骨架应用程序

建立一个基本的应用程序

现在我们已经有了简单的应用程序的骨架,我们可以运行我们的第一个构建测试,以确保一切正常。

打开你的终端(命令提示符)并导航到包含你的项目的文件夹。现在你可以运行以下命令来运行PyInstaller构建。

蟒蛇

pyinstaller app.py

你会看到一些信息输出,给出关于PyInstaller正在做什么的调试信息。这些信息对于调试你的构建中的问题很有用,但也可以忽略不计。我在Windows 11上运行该命令得到的输出如下所示。

bash

C:\Users\Martin\pyinstaller\tkinter\basic>pyinstaller app.py
335 INFO: PyInstaller: 4.7
335 INFO: Python: 3.7.6
336 INFO: Platform: Windows-10-10.0.22000-SP0
337 INFO: wrote C:\Users\Martin\pyinstaller\tkinter\basic\app.spec
339 INFO: UPX is not available.
344 INFO: Extending PYTHONPATH with paths
['C:\\Users\\Martin\\pyinstaller\\tkinter\\basic']
1923 INFO: checking Analysis
1923 INFO: Building Analysis because Analysis-00.toc is non existent
1924 INFO: Initializing module dependency graph...
1928 INFO: Caching module graph hooks...
1951 INFO: Analyzing base_library.zip ...
7438 INFO: Caching module dependency graph...
7604 INFO: running Analysis Analysis-00.toc
7620 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by c:\users\gebruiker\appdata\local\programs\python\python37\python.exe
8188 INFO: Analyzing C:\Users\Martin\pyinstaller\tkinter\basic\app.py
8377 INFO: Processing module hooks...
8378 INFO: Loading module hook 'hook-difflib.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8380 INFO: Loading module hook 'hook-encodings.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8448 INFO: Loading module hook 'hook-heapq.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8450 INFO: Loading module hook 'hook-pickle.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8452 INFO: Loading module hook 'hook-xml.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8765 INFO: Loading module hook 'hook-_tkinter.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8888 INFO: checking Tree
8888 INFO: Building Tree because Tree-00.toc is non existent
8889 INFO: Building Tree Tree-00.toc
8959 INFO: checking Tree
8959 INFO: Building Tree because Tree-01.toc is non existent
8960 INFO: Building Tree Tree-01.toc
9036 INFO: checking Tree
9036 INFO: Building Tree because Tree-02.toc is non existent
9037 INFO: Building Tree Tree-02.toc
9058 INFO: Looking for ctypes DLLs
9063 INFO: Analyzing run-time hooks ...
9065 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py'
9073 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_win32api.py'
9117 INFO: Processing pre-find module path hook distutils from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\pre_find_module_path\\hook-distutils.py'.
9118 INFO: distutils: retargeting to non-venv dir 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib'
9204 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'
9209 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py'
9217 INFO: Looking for dynamic libraries
9434 INFO: Looking for eggs
9434 INFO: Using Python library c:\users\gebruiker\appdata\local\programs\python\python37\python37.dll
9435 INFO: Found binding redirects:
[]
9451 INFO: Warnings written to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\warn-app.txt
9483 INFO: Graph cross-reference written to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\xref-app.html
9516 INFO: checking PYZ
9517 INFO: Building PYZ because PYZ-00.toc is non existent
9517 INFO: Building PYZ (ZlibArchive) C:\Users\Martin\pyinstaller\tkinter\basic\build\app\PYZ-00.pyz
9978 INFO: Building PYZ (ZlibArchive) C:\Users\Martin\pyinstaller\tkinter\basic\build\app\PYZ-00.pyz completed successfully.
9991 INFO: checking PKG
9992 INFO: Building PKG because PKG-00.toc is non existent
9992 INFO: Building PKG (CArchive) app.pkg
10013 INFO: Building PKG (CArchive) app.pkg completed successfully.
10015 INFO: Bootloader c:\users\gebruiker\appdata\local\programs\python\python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
10015 INFO: checking EXE
10015 INFO: Building EXE because EXE-00.toc is non existent
10015 INFO: Building EXE from EXE-00.toc
10015 INFO: Copying bootloader EXE to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\app.exe
10077 INFO: Copying icon to EXE
10077 INFO: Copying icons from ['c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']
10141 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
10141 INFO: Writing RT_ICON 1 resource with 3752 bytes
10141 INFO: Writing RT_ICON 2 resource with 2216 bytes
10142 INFO: Writing RT_ICON 3 resource with 1384 bytes
10142 INFO: Writing RT_ICON 4 resource with 37019 bytes
10143 INFO: Writing RT_ICON 5 resource with 9640 bytes
10143 INFO: Writing RT_ICON 6 resource with 4264 bytes
10143 INFO: Writing RT_ICON 7 resource with 1128 bytes
10146 INFO: Copying 0 resources to EXE
10146 INFO: Emedding manifest in EXE
10147 INFO: Updating manifest in C:\Users\Martin\pyinstaller\tkinter\basic\build\app\app.exe
10206 INFO: Updating resource type 24 name 1 language 0
10209 INFO: Appending PKG archive to EXE
10739 INFO: Building EXE from EXE-00.toc completed successfully.
10743 INFO: checking COLLECT
10743 INFO: Building COLLECT because COLLECT-00.toc is non existent
10744 INFO: Building COLLECT COLLECT-00.toc
15439 INFO: Building COLLECT COLLECT-00.toc completed successfully.

如果你看看你的文件夹,你会发现你现在有两个新的文件夹distbuild

build & dist folders created by PyInstaller 由PyInstaller创建的build和dist文件夹

下面是文件夹内容的一个截断列表,显示了builddist 文件夹。

bash

.
├── app.py
├── app.spec
├── build
│   └── app
│       ├── Analysis-00.toc
│       ├── app.exe
│       ├── app.exe.manifest
│       ├── app.pkg
│       ├── base_library.zip
│       ├── COLLECT-00.toc
│       └── EXE-00.toc
└── dist
    └── app
        ├── tcl
        ├── tcl8
        ├── tk
        ├── app.exe
        ...

build 文件夹被PyInstaller用来收集和准备捆绑的文件,它包含分析的结果和一些额外的日志。在大多数情况下,你可以忽略这个文件夹的内容,除非你试图调试问题。

dist (代表 "分发")文件夹包含要分发的文件。这包括你的应用程序,捆绑成一个可执行文件,以及任何相关的库和二进制.dll 文件。

运行你的应用程序所需的一切都在这个文件夹中,这意味着你可以把这个文件夹 "分发 "给其他人,让他们运行你的应用程序。

你现在可以尝试自己运行你的应用程序,从dist 文件夹中运行名为app.exe 的可执行文件。在短暂的延迟之后,你会看到你的应用程序的熟悉窗口弹出,如下图所示。

Simple app, running after being packaged 简单的应用程序,在打包后运行

你可能也会注意到在你的应用程序运行时弹出一个控制台/终端窗口。我们很快会介绍如何阻止这种情况发生。

在与你的Python文件相同的文件夹中,除了builddist 文件夹外,PyInstaller还将创建一个.spec 文件。 在下一节中,我们将看一下这个文件,它是什么,它做什么。

Spec文件

.spec 文件包含了PyInstaller用来打包你的应用程序的构建配置和说明。每个PyInstaller项目都有一个.spec 文件,它是根据你在运行pyinstaller 时传递的命令行选项生成的。

当我们用我们的脚本运行pyinstaller 时,除了我们的 Python 应用程序文件的名称外,我们没有传入任何其他东西。这意味着我们的规范文件目前只包含默认配置。如果你打开它,你会看到与我们下面类似的东西。

蟒蛇

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')

首先要注意的是,这是一个Python文件,意味着你可以编辑它,使用Python代码来计算设置值。这对于复杂的构建来说非常有用,例如,当你针对不同的平台,想要有条件地定义额外的库或依赖性来捆绑时。

如果你在Windows上生成一个.spec 文件,路径分隔符将是\\ 。要在macOS上使用同样的.spec 文件,你需要把分隔符换成/ 。幸好/ 在Windows上也可以使用。

一旦生成了.spec 文件,你可以把它传递给pyinstaller ,而不是你的脚本,以重复之前的构建过程。现在运行它来重建你的可执行文件。

bash

pyinstaller app.spec

由此产生的构建将与生成.spec 文件的构建相同(假设你没有做任何修改)。对于许多PyInstaller配置的改变,你可以选择通过命令行参数,或者修改你现有的.spec 文件。你选择哪种方式取决于你。

调整构建

到目前为止,我们已经为一个非常基本的应用程序创建了一个简单的首次构建。现在我们将看看PyInstaller提供的几个最有用的选项,以调整我们的构建。然后我们将继续研究如何构建更复杂的应用程序。

命名你的应用程序

你可以做的最简单的改变之一是为你的应用程序提供一个合适的 "名字"。默认情况下,应用程序采用你的源文件的名称(去掉扩展名),例如mainapp 。这通常不是你想要的。

你可以为PyInstaller提供一个更好的名字,让它用于可执行文件(和dist 文件夹),方法是编辑.spec 文件,在应用程序块下添加一个name=

蟒蛇

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='Hello World',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

或者,你可以重新运行pyinstaller 命令,并将-n--name 配置标志与你的app.py 脚本一起传递。

bash

pyinstaller -n "Hello World" app.py
# or
pyinstaller --name "Hello World" app.py

由此产生的EXE文件将被命名为Hello World.exe ,并放置在文件夹dist\Hello World\

Application with custom name "Hello World" 带有自定义名称 "Hello World "的应用程序

.spec 文件的名称取自命令行上传递的名称,因此这将为你创建一个新的规格文件,在你的根文件夹中称为Hello World.spec

隐藏控制台窗口

当你运行打包的应用程序时,你会注意到一个控制台窗口在后台运行。如果你试图关闭这个控制台窗口,你的应用程序也会关闭。在GUI应用程序中你几乎不需要这个窗口,PyInstaller提供了一个简单的方法来关闭它。

Application running with terminal in background 应用程序在后台以终端运行

你可以通过两种方式之一来解决这个问题。首先,你可以编辑之前创建的.spec 文件,在EXE块下设置console=False ,如下图所示。

蟒蛇

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

另外,你可以重新运行pyinstaller 命令,并将-w,--noconsole--windowed 配置标志与你的app.py 脚本一起传递。

bash

pyinstaller -w app.py
# or
pyinstaller --windowed app.py
# or
pyinstaller --noconsole app.py

任何选项之间都没有区别。

重新运行pyinstaller 将重新生成.spec 文件。如果你对这个文件做了任何其他改动,这些改动都会丢失。

一个文件构建

在Windows上,PyInstaller有能力创建一个单文件构建,也就是一个包含所有代码、库和数据文件的单一EXE文件。这对于分享简单的应用程序来说是一种很方便的方式,因为你不需要提供一个安装程序或压缩一个文件夹的文件。

为了指定一个文件的构建,在命令行中提供--onefile 标志。

bash

pyinstaller --onefile app.py

请注意,虽然单文件构建更容易发布,但它的执行速度要比正常构建的应用程序慢。这是因为每次运行应用程序时,它必须创建一个临时文件夹来解压可执行文件的内容。这种权衡是否值得为你的应用程序提供便利,取决于你自己

使用--onefile 选项会对.spec 文件进行相当多的修改。你可以手动进行这些修改,但在第一次创建你的应用程序时使用命令行开关要简单得多。.spec

由于调试一个单文件应用程序要困难得多,你应该在创建一个单文件包之前确保所有东西都能在正常的构建中工作。

为了清楚起见,我们将继续这个教程,采用基于文件夹的构建方式。

设置一个应用程序图标

默认情况下,PyInstallerEXE文件带有以下图标。

Default PyInstaller application icon, on app.exe 默认的PyInstaller应用程序图标,在app.exe上

你可能想自定义它,使你的应用程序更容易被识别。这可以通过PyInstaller--icon=<filename> 命令行开关轻松完成。 在Windows上,图标应该以.ico 文件的形式提供。

bash

pyinstaller --windowed --icon=icon.ico app.py

IcoFx的便携版本是一个很好的免费工具,可以在Windows上创建图标。

或者,通过在你的.spec 文件中添加icon= 参数。

python

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='blarh',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          icon='icon.ico')

如果你现在重新运行构建(通过使用命令行参数,或者用你修改过的.spec 文件运行),你会看到指定的图标文件现在被设置在你的应用程序的EXE文件上。

Custom application icon (a hand) on app.exe app.exe上的自定义应用程序图标(一个手)。

然而,如果你运行你的应用程序,你将会感到失望。

The custom EXE icon is not applied to the window 自定义的EXE图标没有应用到窗口上

指定的图标没有显示在窗口上,它也不会出现在你的任务栏上。

为什么不呢?因为用于窗口的图标并不是由可执行文件中的图标决定的,而是由应用程序本身决定的。为了在我们的窗口上显示一个图标,我们需要对我们的简单应用程序进行一些修改,添加一个对window.iconphoto() 的调用。

蟒蛇

import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("<Button-1>", handle_button_press)
button.pack()

# Start the event loop.
window.iconbitmap("icon.ico")
window.mainloop()

在这里,我们将.iconbitmap 的调用添加到window 实例中。这就为我们的应用程序的窗口定义了一个要使用的图标。

如果你运行上述应用程序,你现在应该看到图标出现在窗口上。

Window showing the custom icon 显示自定义图标的窗口

即使你没有看到图标,也要继续读下去!

处理相对路径

这里有一个问题,可能不是很明显。为了证明这一点,请打开一个shell并切换到我们的脚本所在的文件夹。用以下命令运行它

bash

python3 app.py

如果图标在正确的位置,你应该看到它们。现在换到父文件夹,并尝试再次运行你的脚本(将<folder> 改为你的脚本所在的文件夹的名称)。

bash

cd ..
python3 <folder>/app.py

Window with icon missing 缺少图标的窗口。

图标没有出现。发生了什么事?

我们正在使用相对路径来引用我们的数据文件。这些路径是相对于当前工作目录而言的,而不是你的脚本所在的文件夹。因此,如果你从其他地方运行脚本,它将无法找到这些文件。

图标不显示的一个常见原因是在IDE中运行的例子,它使用项目根目录作为当前工作目录。

这在应用打包前是个小问题,但一旦安装后,你就不知道运行时的当前工作目录是什么了--如果它不对,你的应用就找不到任何东西。 在我们进一步行动之前,我们需要解决这个问题,我们可以通过使我们的路径与我们的应用程序文件夹相对应来做到这一点。

在下面更新的代码中,我们定义了一个新的变量basedir ,使用os.path.dirname 来获取__file__ 的包含文件夹,该文件夹包含当前 Python 文件的完整路径。然后我们用这个来建立图标的相对路径,使用os.path.join()

由于我们的app.py 文件在我们的文件夹的根部,所有其他的路径都是相对于它的。

python

import os
import tkinter as tk

basedir = os.path.dirname(__file__)


window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button_icon = tk.PhotoImage(file=os.path.join(basedir, "icon.png"))
button = tk.Button(text="My simple app.", image=button_icon)
button.bind("<Button-1>", handle_button_press)
button.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icon.ico"))

window.mainloop()

试着从父文件夹再次运行你的应用程序 -- 你会发现,无论你从哪里启动应用程序,图标现在都会按预期出现。

任务栏图标

不幸的是,即使图标显示在窗口上,它仍然可能不显示在任务栏上。

如果它对你来说是这样的,很好!但当你发布你的应用程序时,它可能就不工作了。但当你发布你的应用程序时,它可能就不工作了,所以无论如何,遵循接下来的步骤可能是个好主意。

Custom icon is not shown on the toolbar 自定义图标没有显示在工具栏上

为了让图标显示在任务栏上,我们需要做的最后一个调整是在我们的Python文件的顶部添加一些神秘的咒语。

当你运行你的应用程序时,Windows 会查看可执行文件并试图猜测它属于哪个 "应用程序组"。默认情况下,任何 Python 脚本(包括你的应用程序)都被归入同一个 "Python "组,因此会显示 Python 图标。为了阻止这种情况发生,我们需要为 Windows 提供一个不同的应用程序标识符。

下面的代码就是这样做的,它用一个自定义的应用程序ID调用ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID()

python

import os
import tkinter as tk

basedir = os.path.dirname(__file__)

try:
    from ctypes import windll  # Only exists on Windows.

    myappid = "mycompany.myproduct.subproduct.version"
    windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
    pass

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button_icon = tk.PhotoImage(file=os.path.join(basedir, "icon.png"))
button = tk.Button(text="My simple app.", image=button_icon)

button.bind("<Button-1>", handle_button_press)
button.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icon.ico"))

window.mainloop()

上面的列表显示了一个通用的mycompany.myproduct.subproduct.version 字符串,但你应该改变它以反映你的实际应用。其实,你为这个目的使用什么并不重要,但惯例是使用反向域名符号,com.mycompany 作为公司标识符。

把这个添加到你的脚本中后,运行它现在应该在你的窗口和任务栏上显示图标。最后一步是确保这个图标与你的应用程序一起被正确打包,并且在从dist 文件夹中运行时继续显示。

试试吧,它不会的。

问题是,我们的应用程序现在对一个外部数据文件(图标文件)有依赖性,而这个文件并不是我们源文件的一部分。为了使我们的应用程序工作,我们现在需要把这个数据文件一起分发出去。PyInstaller可以为我们做到这一点,但我们需要告诉它我们想要包括什么,以及把它放在输出的哪里。

在下一节中,我们将看一下你在管理与你的应用程序相关的数据文件方面的可用选项。

数据文件和资源

到目前为止,我们成功地建立了一个没有外部依赖性的简单应用。然而,一旦我们需要加载一个外部文件(本例中是一个图标),我们就遇到了一个问题。该文件没有被复制到我们的dist 文件夹中,所以不能被加载。

在这一节中,我们将看看我们有哪些选项能够将外部资源,如图标,与我们的应用程序捆绑在一起。

用PyInstaller捆绑数据文件

让这些数据文件进入dist ,最简单的方法就是告诉PyInstaller把它们复制过来。 PyInstaller接受一个要复制的单个文件路径的列表,以及一个相对于dist/<app name> 文件夹的文件夹路径,它应该把它们复制到那里。

与其他选项一样,这可以通过命令行参数指定。--add-data

bash

pyinstaller --windowed --icon=icon.ico --add-data="icon.ico;." app.py

你可以多次提供`--add-data`。注意,路径分隔符是针对特定平台的,在Windows上使用`;`,而在Linux或Mac上使用`:`。

或者通过规格文件的分析部分的datas 列表,如下所示。

python

a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icon.ico', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

然后执行.spec 文件,用

bash

pyinstaller app.spec

在这两种情况下,我们都是告诉PyInstaller将指定的文件icon.ico 复制到. ,也就是输出文件夹dist 。如果我们愿意,我们可以在这里指定其他的位置。在命令行中,源文件和目标文件是由路径分隔符; ,而在.spec 文件中,数值是以字符串的2元组形式提供的。

运行构建,你会看到你的.ico 文件现在在输出文件夹dist ,准备与你的应用程序一起发布。如果你从dist ,你现在应该看到窗口和任务栏上的图标,正如预期的那样。

The hand icon showing on the toolbar 工具栏上显示的手形图标

该文件必须在你的应用程序中使用相对路径加载,并且与EXE的相对位置相同,因为它与.py ,这样才能发挥作用。

如果你的图标看起来很模糊,这意味着你的.ico 文件中没有足够大的图标变化。一个.ico 文件可以在同一个文件中包含多个不同尺寸的图标。理想情况下,你希望包含16x16、32x32、48x48和256x256像素的尺寸,尽管更少的尺寸也能工作。

捆绑数据文件夹

通常情况下,你会有不止一个数据文件想要包含在你打包的文件中。最新的PyInstaller版本让你像捆绑文件一样捆绑文件夹,保持子文件夹的结构。例如,让我们扩展我们的应用程序,增加一些额外的图标,并把它们放在一个文件夹下。

蟒蛇

import os
import tkinter as tk

basedir = os.path.dirname(__file__)

try:
    from ctypes import windll  # Only exists on Windows.

    myappid = "mycompany.myproduct.subproduct.version"
    windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
    pass

window = tk.Tk()
window.title("Hello World")

label = tk.Label(text="My simple app.")
label.pack()


def handle_button_press(event):
    window.destroy()


button_close_icon = tk.PhotoImage(
    file=os.path.join(basedir, "icons", "lightning.png")
)
button_close = tk.Button(
    text="Close",
    image=button_close_icon,
)
button_close.bind("<Button-1>", handle_button_press)
button_close.pack()

button_maximimize_icon = tk.PhotoImage(
    file=os.path.join(basedir, "icons", "uparrow.png")
)
button_maximize = tk.Button(
    text="Maximize",
    image=button_maximimize_icon,
)
button_maximize.bind("<Button-1>", handle_button_press)
button_maximize.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icons", "icon.ico"))

# Start the event loop.
window.mainloop()

这些图标(PNG文件和一个用于Windows文件图标的ICO文件)被存储在一个名为 "图标 "的子文件夹下。

bash

.
├── app.py
└── icons
    ├── icon.png
    ├── icon.svg
    ├── lightning.png
    ├── lightning.svg
    ├── uparrow.png
    ├── uparrow.svg
    └── icon.ico

如果你运行这个,你会看到下面的窗口,有一个窗口图标和一个按钮图标。

Two icons 带有两个按钮图标的窗口。

路径使用的是Unix的正斜杠/ 惯例,所以它们对macOS来说是跨平台的。如果你只为Windows开发,你可以使用\\

为了将icons 文件夹复制到我们的构建应用程序中,我们只需要将该文件夹添加到我们的.spec 文件Analysis 块中。至于单个文件,我们把它作为一个元组添加到结果dist 文件夹下的源路径(来自我们的项目文件夹)和目标文件夹。

python

# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons', 'icons')],   # tuple is (source_folder, destination_folder)
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          disable_windowed_traceback=False,
          target_arch=None,
          codesign_identity=None,
          entitlements_file=None )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')

如果你使用这个规格文件运行构建,你会看到icons 文件夹被复制到dist 文件夹。如果你从该文件夹中运行应用程序,图标将按预期显示 -- 相对路径在新的位置保持正确。

用InstallForge建立一个Windows安装程序

到目前为止,我们已经用PyInstaller捆绑了应用程序,以及相关的数据文件,以供分发。这个捆绑过程的输出是一个文件夹,名为dist ,其中包含了我们的应用程序运行所需的所有文件。

虽然你可以把这个文件夹作为ZIP文件与你的用户分享,但这并不是最好的用户体验。桌面应用程序通常与安装程序一起分发,安装程序处理将可执行文件(和任何其他文件)放在正确的位置,添加开始菜单快捷方式等。

现在我们已经成功地捆绑了我们的应用程序,接下来我们将看看如何利用我们的dist 文件夹并使用它来创建一个Windows安装程序。

确保构建工作已经准备就绪。

如果你到目前为止一直在遵循本教程,你已经在/dist 文件夹中准备好了你的应用程序。如果没有,或者你的应用程序不工作,你也可以下载本教程的源代码文件,其中包括一个样本.spec 文件。如上所述,你可以使用提供的app.spec 文件运行相同的构建。

bash

pyinstaller app.spec

这样就可以把所有东西打包,准备在dist/app 文件夹中分发。运行可执行文件app.exe ,以确保所有东西都被正确捆绑,你应该和以前一样的窗口,图标可见。

Two icons 有两个图标的窗口,和一个按钮。

.spec 中的EXE 部分有一个name 参数,你可以在这里指定生成的EXE文件的名称。你可能想把它改成你的应用程序的名称。

创建一个安装程序

现在我们已经成功地捆绑了我们的应用程序,接下来我们将看看如何利用我们的dist 文件夹,并使用它来创建一个有效的Windows安装程序。

为了创建我们的安装程序,我们将使用一个叫做InstallForge 的工具。 InstallForge是免费的,你可以从这个页面下载安装程序。

现在我们来看看用InstallForge创建安装程序的基本步骤。如果你没有耐心,你可以在这里下载完成的 Installforge 安装程序

一般情况

当你第一次运行InstallForge时,你会看到这个常规标签。你可以在这里输入关于你的应用程序的基本信息,包括名称、程序版本、公司和网站。

InstallForge initial view, showing General settings InstallForge初始视图,显示常规设置

你还可以从各种可用的Windows版本中选择安装程序的目标平台。对于桌面应用程序,您目前可能只想针对Windows 7、8和10。

设置

点击左边的侧栏,打开 "设置 "下的 "文件 "页面。在这里你可以指定要捆绑在安装程序中的文件。

使用 "Add Files... "并选择PyInstaller生成的dist/app 文件夹中的所有文件。弹出的文件浏览器允许选择多个文件,所以你可以一次性全部添加,然而你需要分别添加文件夹。点击 "添加文件夹...",添加dist/app 下的任何文件夹,如icons 文件夹。

InstallForge Files view, add all files & folders to be packaged InstallForge 文件视图,添加所有要打包的文件和文件夹

一旦你完成后,滚动列表到底部,确保文件夹被列在其中。你希望dist/app 下的所有文件和文件夹都出现。但是dist/app 这个文件夹本身不应该被列出。

默认的安装路径可以保持原样。角括号之间的值,例如:<company> ,是变量,将被自动填充。

接下来,让你的用户卸载你的应用程序是件好事。尽管它无疑是很棒的,但他们可能想在将来的某个时候删除它。你可以在 "卸载 "选项卡下这样做,只需勾选方框即可。这也将使该应用程序出现在 "添加或删除程序 "中。

InstallForge add Uninstaller for your app InstallForge为您的应用程序添加卸载程序

对话框

对话框 "部分可用于向用户显示自定义信息、闪屏或许可证信息。完成 "选项卡可以让你控制安装程序完成后的情况,在这里让用户选择运行你的程序很有帮助。

要做到这一点,你需要勾选 "运行程序 "旁边的方框,并将你自己的应用程序EXE添加到该方框中。由于<installpath>\ 已经被指定,我们可以直接添加app.exe

InstallForge configure optional run program on finish install InstallForge在完成安装时配置可选的运行程序

系统

在 "系统 "下选择 "快捷方式 "以打开快捷方式编辑器。如果你愿意,你可以在这里为 "开始菜单 "和 "桌面 "指定快捷方式。

InstallForge configure Shortcuts, for Start Menu and Desktop InstallForge为开始菜单和桌面配置快捷方式

点击 "添加...",为你的应用程序添加新的快捷方式。在 "开始菜单 "和 "桌面 "快捷方式中选择,并填写名称和目标文件。这是你的应用程序EXE安装后的最终路径。由于<installpath>\ ,你只需在末尾添加你的应用程序的EXE名称,在这里app.exe

InstallForge, adding a Shortcut InstallForge,添加一个快捷方式

建立

有了基本的设置,你现在可以建立你的安装程序了。

这时你可以保存InstallForge项目,这样你就可以在将来用同样的设置重新构建安装程序。

点击底部的 "构建 "部分,打开构建面板。

InstallForge, ready to build InstallForge,准备构建

点击大图标按钮,开始构建过程。如果你还没有指定一个安装文件的位置,你会被提示一个位置。这是你希望完成的安装程序被保存的位置。

不要把它保存在你的dist 文件夹里。

构建过程将开始,收集并压缩文件到安装程序中。

InstallForge, build complete InstallForge,构建完成

一旦完成,你会被提示运行安装程序。这完全是可有可无的,但这是了解它是否工作的一个方便的方法。

运行安装程序

安装程序本身不应该有任何意外,它可以按预期工作。根据InstallForge中选择的选项,你可能有额外的面板或选项。

InstallForge, running the resulting installer InstallForge,运行生成的安装程序

逐步完成安装程序,直到它完成。你可以选择从安装程序的最后一页运行该应用程序,或者你可以在你的开始菜单中找到它。

Our demo app in the Start Menu on Windows 11 我们的演示应用程序在Windows 11的开始菜单中的开始菜单中

总结

在本教程中,我们已经介绍了如何使用PyInstaller将你的Tkinter应用程序构建成可分发的EXE,包括与你的代码一起添加数据文件。然后我们通过使用InstallForge将应用程序构建成Windows安装程序的过程。按照这些步骤,你应该能够将你自己的应用程序打包并提供给其他人使用。