Криво собирается приложение Python
Написал приложение на Python, использовал Qt5 для удобного пользования программой, запуск через терминал проходит успешно и никаких ошибок не выдаёт, но при сборке в .ехе, приложение ломается. Ввожу в приложение нужные параметры и после этого: окно приложения (не отвечает), открывается 3 точно таких же окна.
Собирал код несколькими способами: auto-py-to-exe, pyinstaller - но всё такая же проблема
- сообщение об ошибках отсутствуют
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtGui import QIntValidator
from PyQt5.QtWidgets import QLineEdit
import cv2
import numpy as np
from tqdm import tqdm
from concurrent.futures import ProcessPoolExecutor
def read_frames(cap):
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
yield frame
def process_frame(args):
frame, frame_size, square_size, light_symbol, dark_symbol = args
frame = cv2.resize(frame, frame_size)
height, width, _ = frame.shape
symbol_frame = np.zeros((height, width, 3), dtype=np.uint8)
font = cv2.FONT_ITALIC
for i in range(0, height, square_size):
for j in range(0, width, square_size):
square = frame[i:i + square_size, j:j + square_size]
brightness = int(np.mean(square))
if brightness > 128:
symbol = light_symbol
color = (255, 255, 255)
else:
symbol = dark_symbol
color = (255, 255, 255)
# Задаем координаты для рисования символа
x, y = j, i
symbol_frame = cv2.putText(symbol_frame, symbol, (x, y), font, 0.26, color, 0)
return symbol_frame
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(465, 383)
MainWindow.setMaximumSize(QtCore.QSize(1920, 1080))
MainWindow.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
MainWindow.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(10, 10, 211, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label.setFont(font)
self.label.setObjectName("label")
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setGeometry(QtCore.QRect(10, 60, 171, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_2.setFont(font)
self.label_2.setObjectName("label_2")
self.label_3 = QtWidgets.QLabel(self.centralwidget)
self.label_3.setGeometry(QtCore.QRect(10, 100, 211, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_3.setFont(font)
self.label_3.setObjectName("label_3")
self.label_4 = QtWidgets.QLabel(self.centralwidget)
self.label_4.setGeometry(QtCore.QRect(10, 140, 211, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_4.setFont(font)
self.label_4.setObjectName("label_4")
self.label_5 = QtWidgets.QLabel(self.centralwidget)
self.label_5.setGeometry(QtCore.QRect(10, 180, 211, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_5.setFont(font)
self.label_5.setObjectName("label_5")
self.label_6 = QtWidgets.QLabel(self.centralwidget)
self.label_6.setGeometry(QtCore.QRect(10, 220, 211, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_6.setFont(font)
self.label_6.setObjectName("label_6")
self.spinBox = QtWidgets.QSpinBox(self.centralwidget)
self.spinBox.setGeometry(QtCore.QRect(190, 60, 61, 31))
self.spinBox.setObjectName("spinBox")
self.spinBox.setMinimum(2)
self.spinBox.setMaximum(50)
self.plainTextEdit_2 = QLineEdit(self.centralwidget)
self.plainTextEdit_2.setGeometry(QtCore.QRect(130, 100, 41, 31))
self.plainTextEdit_2.setObjectName("plainTextEdit_2")
self.plainTextEdit_2.setMaxLength(1)
self.plainTextEdit_3 = QLineEdit(self.centralwidget)
self.plainTextEdit_3.setGeometry(QtCore.QRect(140, 140, 41, 31))
self.plainTextEdit_3.setObjectName("plainTextEdit_3")
self.plainTextEdit_3.setMaxLength(1)
self.plainTextEdit_4 = QLineEdit(self.centralwidget)
self.plainTextEdit_4.setGeometry(QtCore.QRect(220, 180, 104, 31))
self.plainTextEdit_4.setObjectName("plainTextEdit_4")
int_validator_plainTextEdit_4 = QIntValidator()
self.plainTextEdit_4.setValidator(int_validator_plainTextEdit_4)
self.label_7 = QtWidgets.QLabel(self.centralwidget)
self.label_7.setGeometry(QtCore.QRect(330, 180, 16, 31))
font = QtGui.QFont()
font.setPointSize(20)
self.label_7.setFont(font)
self.label_7.setObjectName("label_7")
self.plainTextEdit_5 = QLineEdit(self.centralwidget)
self.plainTextEdit_5.setGeometry(QtCore.QRect(350, 180, 104, 31))
self.plainTextEdit_5.setObjectName("plainTextEdit_5")
int_validator_plainTextEdit_5 = QIntValidator()
self.plainTextEdit_5.setValidator(int_validator_plainTextEdit_5)
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(10, 270, 191, 71))
font = QtGui.QFont()
font.setPointSize(16)
self.pushButton.setFont(font)
self.pushButton.setObjectName("pushButton")
self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
self.progressBar.setGeometry(QtCore.QRect(250, 310, 181, 21))
self.progressBar.setProperty("value", 0)
self.progressBar.setObjectName("progressBar")
self.plainTextEdit_6 = QtWidgets.QPlainTextEdit(self.centralwidget)
self.plainTextEdit_6.setGeometry(QtCore.QRect(220, 10, 221, 31))
self.plainTextEdit_6.setObjectName("plainTextEdit_6")
self.plainTextEdit_7 = QtWidgets.QPlainTextEdit(self.centralwidget)
self.plainTextEdit_7.setGeometry(QtCore.QRect(220, 230, 231, 31))
self.plainTextEdit_7.setObjectName("plainTextEdit_7")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 465, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
self.plainTextEdit_6.mousePressEvent = self.choose_video
self.plainTextEdit_7.mousePressEvent = self.choose_save_path
self.pushButton.clicked.connect(self.process_data)
self.progressBar.setVisible(False)
def choose_video(self, event):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
file_name, _ = QFileDialog.getOpenFileName(None, "Выберите видео", "",
"Video Files (*.mp4 *.avi);;All Files (*)", options=options)
if file_name:
self.plainTextEdit_6.setPlainText(file_name)
def choose_save_path(self, event):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
save_path, _ = QFileDialog.getSaveFileName(None, "Куда сохранить", "", "All Files (*)", options=options)
if save_path:
if not save_path.lower().endswith('.mp4'):
save_path += '.mp4'
self.plainTextEdit_7.setPlainText(save_path)
def process_data(self):
self.progressBar.setVisible(True)
HEIGHT = int(self.plainTextEdit_4.text())
WIDTH = int(self.plainTextEdit_5.text())
mainVideo = self.plainTextEdit_6.toPlainText()
outputVideo = self.plainTextEdit_7.toPlainText()
cap = cv2.VideoCapture(mainVideo)
# успешность открытия видео
if not cap.isOpened():
#print("Ошибка: не удалось открыть видео.")
exit()
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
output_video_path = outputVideo
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
output_video_writer = cv2.VideoWriter(output_video_path, fourcc, cap.get(cv2.CAP_PROP_FPS), (WIDTH, HEIGHT))
start_frame = 0
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
pbar = tqdm(total=total_frames)
with ProcessPoolExecutor(max_workers=4) as executor:
frames_chunk = []
for frame in read_frames(cap):
frames_chunk.append((frame, (WIDTH, HEIGHT), self.spinBox.value(), self.plainTextEdit_2.text(), self.plainTextEdit_3.text()))
if len(frames_chunk) % 64 == 0:
for processed_frame in executor.map(process_frame, frames_chunk):
output_video_writer.write(processed_frame)
pbar.update()
frames_chunk = []
for processed_frame in executor.map(process_frame, frames_chunk):
output_video_writer.write(processed_frame)
pbar.update()
cap.release()
output_video_writer.release()
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Sosiska"))
self.label.setText(_translate("MainWindow", "Выберите видео"))
self.label_2.setText(_translate("MainWindow", "Размер Сетки"))
self.label_3.setText(_translate("MainWindow", "Символ 1"))
self.label_4.setText(_translate("MainWindow", "Символ 2"))
self.label_5.setText(_translate("MainWindow", "Размеры видео"))
self.label_6.setText(_translate("MainWindow", "Куда сохранить"))
self.label_7.setText(_translate("MainWindow", "x"))
self.pushButton.setText(_translate("MainWindow", "Поехали"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
Возможно я неправильно собираю приложение и что-то важное не указал - кто знает, подскажите
Ответы (1 шт):
Костыль для всех страждущих .EXE на пайтоне, но вам лень\не получается разобраться почему ЭТО не работает.
Идем на python.org и качаем Windows embeddable package (64-bit) - Python>>> Downloads>>> Windows
Далее нужно установить для него pip и нужные библиотеки, но сперва проверим что всё работает как надо.
Мой путь до скаченного «карманного» пайтона:
C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64
Win+r -> cmd > cd C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64
Проверим и введём в консоль:
python
Вывод: Python 3.11.7 (tags/v3.11.7:fa7a6f2, Dec 4 2023, 19:24:49) [MSC v.1937 64 bit (AMD64)] on win32
Отлично, всё работает. Введём в запустившийся интерпретатор следующие команды для выхода из него:
import sys
sys.exit()
Прекрасно, нам снова доступна консоль.
А теперь давайте установим pip.
Переходим по ссылке: installation
Находим: Download the script, from https://bootstrap.pypa.io/get-pip.py.
Скачиваем скрипт\копируем в txt файл и называем get-pip.py
Помещаем get-pip.py в корень нашего «карманного» пайтона:
C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\get-pip.py
Возвращаемся в нашу консоль и вводим: python get-pip.py
Не обращаем внимание на:
WARNING: The script wheel.exe is installed in 'C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\Scripts' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
WARNING: The scripts pip.exe, pip3.11.exe and pip3.exe are installed in 'C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\Scripts' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Отлично, pip установлен, но для его работы нужно подкорректировать один файл.
Возвращаемся в каталог «карманного» пайтона C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64
Ищем файл python311._pth -> Открываем его любым txt-редактором -> добавляем строчку Lib\site-packages (на следующей строке после #import site)
Проверим в консоли:
python -m pip list
Вывод:
Package Version
---------- -------
pip 23.3.2
setuptools 69.0.3
wheel 0.42.0
Отлично, всё работает!
Теперь установим какую-нибудь библиотеку:
python -m pip install NuMPI
И снова проверим:
python -m pip list
Вывод:
Package Version
---------- -------
cftime 1.6.3
netCDF4 1.6.2
NuMPI 0.4.0
numpy 1.26.3
pip 23.3.2
scipy 1.11.4
setuptools 69.0.3
wheel 0.42.0
Прекрасно, почти всё готово. Теперь вам нужно установить таким же образом все необходимые библиотеки для вашего проекта.
После этого подготавливаем 2 файла:
start.py
import subprocess
import sys
import os
current_directory = getattr(sys, '_MEIPASS', os.getcwd())
folder_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\bat.bat')
subprocess.run([folder_path], shell=True) # Выполнение бат-файла
bat.bat
@echo off
cd /d %~dp0
python.exe main.py
bat.bat -> помещаем в «карманный» пайтон python-3.11.7-embed-amd64 прямо в корень, туда же помещаем и основной скрипт(main.py)\дополнительные файлы.
При запаковке в .exe указываем папку python-3.11.7-embed-amd64 как доп.ресурсы для запаковки, а файл для запуска указываем start.py
Логика какая: Паинсталлер запускает скрипт start.py, который запускает bat.bat, который запускает «карманный» пайтон, который запускает уже нужный скрипт.
Версия без .bat:
start.py
import subprocess
import sys
import os
current_directory = getattr(sys, '_MEIPASS', os.getcwd())
executable_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\python.exe')
script_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\main.py')
# Выполнение исполняемого файла
subprocess.run([executable_path, script_path], shell=True)
Логика какая: Паинсталлер запускает скрипт start.py, который запускает «карманный» пайтон, который запускает уже нужный скрипт.
Решение конечно так себе, но с ходу лечит многие проблемы с паинсталлером.
Из минусов: .exe больше весит, скрипт дольше запускается.
Из плюсов: это работает, если всё работало до запаковки в .exe
З.Ы. Дабы всё это безобразие запускалось немного быстрее, файл(ы) исходного скрипта main.py можно сперва сконвертировать в байт-код -> main.pyc
python -c "import py_compile; py_compile.compile('main.py', 'main.pyc')"


