import
os
import
shutil
import
subprocess
import
threading
from
tkinter
import
Tk, filedialog, messagebox, StringVar, Label, Button, Entry, Checkbutton, IntVar, Text
import
sys
def
center_window(window):
window.update_idletasks()
width
=
window.winfo_width()
height
=
window.winfo_height()
x
=
(window.winfo_screenwidth()
/
/
2
)
-
(width
/
/
2
)
y
=
(window.winfo_screenheight()
/
/
2
)
-
(height
/
/
2
)
window.geometry(f
'{width}x{height}+{x}+{y}'
)
window.attributes(
'-topmost'
,
True
)
window.after(
500
,
lambda
: window.attributes(
'-topmost'
,
False
))
def
on_map(event):
root.after(
10
,
lambda
: center_window(root))
def
select_main_script():
filepath
=
filedialog.askopenfilename(filetypes
=
[(
"Python files"
,
"*.py"
)])
if
filepath:
main_script.
set
(filepath)
update_status_label(
"主脚本已选择,请继续选择其他文件或开始打包。"
)
def
select_data_folder():
folderpath
=
filedialog.askdirectory()
if
folderpath:
data_folder.
set
(folderpath)
update_status_label(
"数据文件夹已选择,请继续选择其他文件或开始打包。"
)
def
select_icon_file():
filepath
=
filedialog.askopenfilename(filetypes
=
[(
"Icon files"
,
"*.ico"
)])
if
filepath:
icon_path.
set
(filepath)
update_status_label(
"图标文件已选择,请继续选择其他文件或开始打包。"
)
def
open_folder(path):
os.startfile(path)
def
start_packaging_in_thread():
threading.Thread(target
=
pack_application, daemon
=
True
).start()
def
generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path
=
None
, hide_console
=
False
):
spec_filename
=
os.path.join(output_dir, os.path.splitext(os.path.basename(main_script_path))[
0
]
+
'.spec'
)
main_script_utf8
=
repr
(main_script_path)[
1
:
-
1
]
datas_utf8
=
[(
repr
(os.path.normpath(src).replace(
'\\', '
/
'))[1:-1], dest.replace('
\\
', '
/
'))
for
src, dest
in
datas]
hidden_imports_utf8
=
[
repr
(lib.split(
"=="
)[
0
].split(
">="
)[
0
].split(
"<="
)[
0
].strip())[
1
:
-
1
]
for
lib
in
add_libs]
icon_str
=
f
'icon="{icon_path}", '
if
icon_path
else
''
console_str
=
'console=False'
if
hide_console
else
'console=True'
with
open
(spec_filename,
'w'
, encoding
=
'utf-8'
) as f:
f.write(f
)
return
spec_filename
def
pack_application():
status_label.config(state
=
'normal'
)
status_label.delete(
'1.0'
,
'end'
)
insert_status(
"正在选择主脚本..."
)
main_script_path
=
main_script.get()
if
not
main_script_path:
messagebox.showerror(
"错误"
,
"请选择主脚本"
)
return
main_script_dir
=
os.path.dirname(main_script_path)
main_script_name
=
os.path.basename(main_script_path)
insert_status(
"选择输出文件夹..."
)
output_dir
=
filedialog.askdirectory(title
=
"选择输出文件夹"
)
if
not
output_dir:
messagebox.showwarning(
"警告"
,
"未选择输出文件夹,操作已取消。"
)
return
datas
=
[]
add_libs
=
[]
for
item
in
os.listdir(main_script_dir):
abs_path
=
os.path.join(main_script_dir, item)
if
os.path.isfile(abs_path)
and
item !
=
main_script_name:
datas.append((abs_path,
'.'
))
if
data_folder.get():
base_folder
=
data_folder.get()
target_folder_name
=
os.path.basename(base_folder)
for
root, _, files
in
os.walk(base_folder):
for
file
in
files:
abs_path
=
os.path.join(root,
file
)
rel_path
=
os.path.relpath(root, base_folder)
target_path
=
os.path.join(target_folder_name, rel_path)
if
rel_path !
=
'.'
else
target_folder_name
datas.append((abs_path, target_path))
add_lib_path
=
os.path.normpath(os.path.join(main_script_dir,
'add_libs.txt'
))
if
os.path.exists(add_lib_path):
with
open
(add_lib_path, encoding
=
'utf-8'
) as f:
add_libs
=
[line.strip()
for
line
in
f.readlines()]
icon_path_value
=
icon_path.get()
hide_console_value
=
hide_console.get()
build_dir
=
os.path.join(main_script_dir,
'build_temp'
)
os.makedirs(output_dir, exist_ok
=
True
)
os.makedirs(build_dir, exist_ok
=
True
)
insert_status(
"生成.spec文件..."
)
spec_filename
=
generate_spec_file(main_script_path, datas, add_libs, output_dir, icon_path_value,
hide_console_value)
insert_status(
"执行PyInstaller命令,过程比较长,请耐心等待..."
)
cmd
=
[
'pyinstaller'
,
'--distpath'
, output_dir,
'--workpath'
, build_dir,
'--noconfirm'
,
'--clean'
,
spec_filename
]
startupinfo
=
subprocess.STARTUPINFO()
startupinfo.dwFlags |
=
subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow
=
subprocess.SW_HIDE
try
:
subprocess.run(cmd, check
=
True
, startupinfo
=
startupinfo)
except
subprocess.CalledProcessError as e:
messagebox.showerror(
"错误"
, f
"PyInstaller 执行失败: {e}"
)
return
shutil.rmtree(
'__pycache__'
, ignore_errors
=
True
)
shutil.rmtree(build_dir, ignore_errors
=
True
)
try
:
os.remove(spec_filename)
except
OSError:
pass
open_folder(output_dir)
insert_status(
"打包完成,等待新任务..."
)
status_label.config(state
=
'disabled'
)
def
insert_status(message):
status_label.insert(
'end'
, message
+
"\n"
)
status_label.yview_moveto(
1
)
def
update_status_label(text):
status_label.config(state
=
'normal'
)
status_label.delete(
'1.0'
,
'end'
)
status_label.insert(
'end'
, text
+
"\n"
)
status_label.yview_moveto(
1
)
status_label.config(state
=
'disabled'
)
def
create_gui():
global
root, main_script, data_folder, icon_path, hide_console, status_label
root
=
Tk()
root.title(
"Python 应用程序打包工具"
)
root.geometry(
'555x350'
)
main_script
=
StringVar()
data_folder
=
StringVar()
icon_path
=
StringVar()
hide_console
=
IntVar()
initial_instructions
=
(
'\u3000\u30001.打包主程序所在目录的全部文件,如果主程序所在目录存在add_libs.txt文件还会自动打包其指定的库,'
'文件内容一行一个库,如“PyQt5==5.15.9”。'
'\n\u3000\u30002.添加数据文件夹后需注意调用方法,如:if getattr(sys, "frozen", False):解包后主目录base_path=sys._MEIPASS,'
'else:开发环境主目录base_path=os.path.dirname(os.path.abspath(__file__)),切换目录os.chdir(base_path),'
'生成全路径文件名os.path.join(base_path,"数据文件夹名","文件名")。'
'\n\u3000\u30003.打包时务必确保“upx.exe”文件与当前脚本在同一目录下!打包完成的可执行文件使用upx压缩,文件相对较小。'
)
Label(root, text
=
"主脚本(必选):"
).grid(row
=
0
, column
=
0
, padx
=
5
, pady
=
(
20
,
5
), sticky
=
'e'
)
Entry(root, textvariable
=
main_script).grid(row
=
0
, column
=
1
, padx
=
5
, pady
=
(
20
,
5
), sticky
=
'ew'
)
Button(root, text
=
"浏览"
, command
=
select_main_script).grid(row
=
0
, column
=
2
, padx
=
(
5
,
25
), pady
=
(
20
,
5
), sticky
=
'ew'
)
Label(root, text
=
"图标文件(可选):"
).grid(row
=
1
, column
=
0
, padx
=
5
, pady
=
5
, sticky
=
'e'
)
Entry(root, textvariable
=
icon_path).grid(row
=
1
, column
=
1
, padx
=
5
, pady
=
5
, sticky
=
'ew'
)
Button(root, text
=
"浏览"
, command
=
select_icon_file).grid(row
=
1
, column
=
2
, padx
=
(
5
,
25
), pady
=
5
, sticky
=
'ew'
)
Label(root, text
=
"数据文件夹(可选):"
).grid(row
=
2
, column
=
0
, padx
=
5
, pady
=
(
5
,
15
), sticky
=
'e'
)
Entry(root, textvariable
=
data_folder).grid(row
=
2
, column
=
1
, padx
=
5
, pady
=
(
5
,
15
), sticky
=
'ew'
)
Button(root, text
=
"浏览"
, command
=
select_data_folder).grid(row
=
2
, column
=
2
, padx
=
(
5
,
25
), pady
=
(
5
,
15
), sticky
=
'ew'
)
Checkbutton(root, text
=
"隐藏控制台"
, variable
=
hide_console).grid(row
=
3
, column
=
0
, padx
=
(
25
,
5
), pady
=
(
5
,
15
), sticky
=
'w'
)
Button(root, text
=
"开始打包"
, command
=
start_packaging_in_thread).grid(row
=
3
, column
=
1
, columnspan
=
1
, padx
=
5
,
pady
=
(
5
,
15
), sticky
=
'ew'
)
status_label
=
Text(root, wrap
=
'word'
, state
=
'disabled'
, height
=
9
, bg
=
'white'
, fg
=
'blue'
, relief
=
'sunken'
, bd
=
1
)
status_label.grid(row
=
4
, column
=
0
, columnspan
=
3
, padx
=
(
25
,
25
), pady
=
(
5
,
15
), sticky
=
'nsew'
)
update_status_label(initial_instructions)
root.grid_columnconfigure(
1
, weight
=
1
)
root.bind(
"<Map>"
, on_map)
root.mainloop()
if
__name__
=
=
"__main__"
:
create_gui()