import
sys
import
os
import
re
import
shutil
from PyQt6.QtWidgets
import
(QApplication, QMainWindow, QPushButton,
QFileDialog, QLabel, QVBoxLayout, QWidget,
QProgressBar, QCheckBox, QGroupBox, QGridLayout,
QLineEdit)
from PyQt6.QtCore
import
Qt, QThread, pyqtSignal
from PyQt6.QtGui
import
QIcon, QFont
import
subprocess
class ConvertThread(QThread):
progress = pyqtSignal(
str
)
finished = pyqtSignal(bool,
str
)
stop_flag = False
def __init__(self, file_path, options):
super().__init__()
self.file_path = file_path
self.options = options
self.stop_flag = False
def run(self):
if
self.stop_flag:
return
try:
script_dir = os.path.dirname(os.path.abspath(self.file_path))
dist_dir = os.path.join(script_dir,
'dist'
)
command = [
'pyinstaller'
]
if
self.options[
'onefile'
]:
command.append(
'--onefile'
)
else
:
command.append(
'--onedir'
)
if
self.options[
'noconsole'
]:
command.append(
'--noconsole'
)
command.append(
'--windowed'
)
command.extend([
'--clean'
])
command.extend([
'--distpath'
, dist_dir])
command.extend([
'--specpath'
, script_dir])
command.extend([
'--workpath'
, os.path.join(script_dir,
'build'
)])
images_dir = os.path.join(script_dir,
'images'
)
if
os.path.exists(images_dir):
if
os.
name
==
'nt'
:
command.extend([
'--add-data'
, f
'{images_dir};images'
])
else
:
command.extend([
'--add-data'
, f
'{images_dir}:images'
])
if
self.options[
'icon'
]
and
os.path.exists(self.options[
'icon'
]):
command.extend([
'--icon'
, self.options[
'icon'
]])
if
self.options[
'name'
]:
if
re.match(r
'^[a-zA-Z0-9_\-.\u4e00-\u9fa5][a-zA-Z0-9_\-.\u4e00-\u9fa5 ]*$'
, self.options[
'name'
]):
command.extend([
'--name'
, self.options[
'name'
]])
else
:
self.finished.emit(False,
"程序名称包含非法字符或格式不正确!"
)
return
command.append(self.file_path)
self.progress.emit(f
"执行命令: {' '.join(command)}"
)
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
check=True
)
except FileNotFoundError:
error_msg =
"未找到 pyinstaller 程序,请确保已正确安装!"
self.progress.emit(error_msg)
self.finished.emit(False, error_msg)
return
except subprocess.CalledProcessError as e:
error_msg = f
"返回码: {e.returncode}, 错误输出: {e.stderr or '未知错误'}"
self.progress.emit(f
"错误输出: {error_msg}"
)
self.finished.emit(False, f
"转换失败: {error_msg}"
)
return
except Exception as e:
error_msg = f
"发生未知错误: {str(e)}"
self.progress.emit(error_msg)
self.finished.emit(False, error_msg)
return
if
result.returncode == 0:
output_dir = dist_dir
output_name = self.options[
'name'
]
if
self.options[
'name'
]
else
\
os.path.splitext(os.path.basename(self.file_path))[0]
output_ext =
'.exe'
if
self.options[
'onefile'
]
else
''
output_path = os.path.join(output_dir, f
'{output_name}{output_ext}'
)
# 转换成功后,删除 build 目录
build_dir = os.path.join(script_dir,
'build'
)
if
os.path.exists(build_dir):
try:
shutil.rmtree(build_dir)
self.progress.emit(f
"已删除 build 目录: {build_dir}"
)
except Exception as e:
self.progress.emit(f
"删除 build 目录时出错: {str(e)}"
)
self.finished.emit(True, f
"转换成功!请查看 {output_path}"
)
else
:
error_msg = result.stderr
or
"未知错误"
self.progress.emit(f
"错误输出: {error_msg}"
)
self.finished.emit(False, f
"转换失败: {error_msg}"
)
except Exception as e:
error_msg = f
"发生未知错误: {str(e)}"
self.progress.emit(error_msg)
self.finished.emit(False, error_msg)
def stop(self):
self.stop_flag = True
class Py2ExeConverter(QMainWindow):
def __init__(self):
super().__init__()
base_path = getattr(sys,
'_MEIPASS'
, os.path.dirname(os.path.abspath(__file__)))
icon_path = os.path.join(base_path,
'images'
,
'64.ico'
)
try:
self.setWindowIcon(QIcon(icon_path))
except Exception:
print(
"图标文件未找到或格式不正确。"
)
self.init_ui()
def init_ui(self):
self.setWindowTitle(
"Python打包转EXE工具"
)
self.setFixedSize(600, 400)
main_widget = QWidget()
self.setCentralWidget(main_widget)
layout = QVBoxLayout()
file_group = self.create_file_selection_group()
layout.addWidget(file_group)
options_group = self.create_options_group()
layout.addWidget(options_group)
self.progress_label = QLabel(
"准备就绪"
)
self.progress_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.progress_label.setWordWrap(True)
self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(False)
self.convert_button = QPushButton(
"开始转换"
)
self.convert_button.setStyleSheet(
""
"
QPushButton {
background-color: #2196F3;
color: white;
padding: 8px;
font-weight: bold;
border-radius: 4px;
}
QPushButton:hover {
background-color: #1976D2;
}
""
")
layout.addWidget(self.progress_label)
layout.addWidget(self.progress_bar)
layout.addWidget(self.convert_button)
main_widget.setLayout(layout)
self.convert_button.clicked.connect(self.convert_to_exe)
self.selected_file =
None
self.convert_thread =
None
def create_file_selection_group(self):
file_group = QGroupBox(
"文件选择"
)
file_layout = QGridLayout()
self.file_label = QLabel(
"未选择文件"
)
self.file_label.setStyleSheet(
"background-color: #f0f0f0; border-radius: 3px;"
)
select_button = QPushButton(
"选择 Python 文件"
)
select_button.setFixedSize(260, 28)
select_button.setStyleSheet(
"background-color: #4CAF50; color: white; padding: 5px;"
)
file_layout.addWidget(self.file_label, 0, 0)
file_layout.addWidget(select_button, 0, 1)
file_group.setLayout(file_layout)
select_button.clicked.connect(self.select_file)
return file_group
def create_options_group(self):
options_group = QGroupBox(
"转换选项"
)
options_layout = QGridLayout()
self.onefile_check = QCheckBox(
"生成单个文件"
)
self.onefile_check.setChecked(True)
self.noconsole_check = QCheckBox(
"隐藏控制台窗口"
)
self.noconsole_check.setChecked(True)
self.icon_label = QLabel(
"图标文件(.ico):"
)
self.icon_path = QLineEdit()
self.icon_path.setPlaceholderText(
"可选:选择ico格式图片"
)
icon_button = QPushButton(
"上传"
)
icon_button.clicked.connect(self.select_icon)
self.name_label = QLabel(
"程序名称(.exe):"
)
self.name_input = QLineEdit()
self.name_input.setPlaceholderText(
"可选:指定输出exe名称"
)
options_layout.addWidget(self.onefile_check, 0, 0)
options_layout.addWidget(self.noconsole_check, 0, 1)
options_layout.addWidget(self.icon_label, 1, 0)
options_layout.addWidget(self.icon_path, 1, 1)
options_layout.addWidget(icon_button, 1, 2)
options_layout.addWidget(self.name_label, 2, 0)
options_layout.addWidget(self.name_input, 2, 1)
options_group.setLayout(options_layout)
return options_group
def select_file(self):
file_name, _ = QFileDialog.getOpenFileName(
self,
"选择 Python 文件"
,
""
,
"Python Files (*.py)"
)
if
file_name:
if
not
os.path.exists(file_name):
self.progress_label.setText(
"选择的 Python 文件不存在!"
)
return
self.selected_file = file_name
self.file_label.setText(f
"已选择: {os.path.basename(file_name)}"
)
default_name = os.path.splitext(os.path.basename(file_name))[0]
self.name_input.setText(default_name)
def select_icon(self):
icon_path, _ = QFileDialog.getOpenFileName(
self,
"选择图标文件"
,
""
,
"Icon Files (*.ico)"
)
if
icon_path:
if
not
os.path.exists(icon_path):
self.progress_label.setText(
"选择的图标文件不存在!"
)
return
script_dir = os.path.dirname(os.path.abspath(self.selected_file))
images_dir = os.path.join(script_dir,
'images'
)
if
not
os.path.exists(images_dir):
os.makedirs(images_dir)
icon_filename = os.path.basename(icon_path)
target_path = os.path.join(images_dir, icon_filename)
try:
shutil.copy2(icon_path, target_path)
self.icon_path.setText(target_path)
self.progress_label.setText(f
"图标文件已复制到 {target_path}"
)
except Exception as e:
self.progress_label.setText(f
"复制图标文件时出错: {str(e)}"
)
def update_progress(self, message):
self.progress_label.setText(message)
self.progress_bar.setMaximum(0)
def conversion_complete(self, success, message):
self.convert_button.setEnabled(True)
self.progress_bar.setMaximum(100)
self.progress_bar.setValue(100
if
success
else
0)
self.progress_label.setText(message)
def convert_to_exe(self):
if
not
self.selected_file:
self.progress_label.setText(
"请先选择 Python 文件!"
)
return
self.convert_button.setEnabled(False)
self.progress_label.setText(
"正在转换中..."
)
self.progress_bar.setMaximum(0)
options = {
'onefile'
: self.onefile_check.isChecked(),
'noconsole'
: self.noconsole_check.isChecked(),
'icon'
: self.icon_path
.text
(),
'name'
: self.name_input
.text
().strip()
}
self.convert_thread = ConvertThread(self.selected_file, options)
self.convert_thread.progress.connect(self.update_progress)
self.convert_thread.finished.connect(self.conversion_complete)
self.convert_thread.start()
def closeEvent(self, event):
if
self.convert_thread
and
self.convert_thread.isRunning():
self.convert_thread.stop()
self.convert_thread.
wait
()
event.accept()
if
__name__ ==
'__main__'
:
app = QApplication(sys.argv)
app.setStyle(
'Fusion'
)
window = Py2ExeConverter()
window.show()
sys
.exit
(app.exec())