Проблема с QThreadPool или QThread в PyQt, или же как взаимодействуют объекты в PyQt
Всем привет, есть несколько вопросов!
Сразу предисловие, я пытаюсь разобраться в этом фреймворке и пока плохо ориентируюсь, если есть подобные темы решения данной проблемы, пожалуйста киньте ссылку. Весь код в конце поста!
- Столкнулся с проблемой в PyQT в использовании QThread объекта, пытаюсь в поток передать метод класса главного окна по скачиванию видео "self.download", и решил это сделать через передачу ссылки на сам объект "main_window\self", есть подозрение что оно падает на моменте апдейта элемента "textEdit".
- Также пытался воспользоваться QthreadPool, но проблема не решилась.
- Почему в PyQT нет аналогии с объектом "Concurrent" как в C++? Там есть метод "map\mapper" по аналогии как в объекте "Pool" метод "map", который сам управляется, нам лишь нужно в этот метод передавать метод и коллекцию для исполнения.
Что ожидаю от выполнения кода: что нажимая на кнопку "Download", будет запускать метод "run_multi_download", в котором будет создаваться новый поток на каждую ссылку и дергать метод класса главного окна "self.download". И в конечном счете после завершения скачивания нам нужно асинхронно апдейтить элемент главного окна, что видео скачено.
Проблема: как по мне, при попытке обновить элемент, то есть добавить текст, происходит падение в отдельном потоке хоть и передаю ссылку на сам объект основного потока. Не знаю, прав ли я, и как это решить. Ну и собственно ловлю в консоле это "Process finished with exit code -1073740791 (0xC0000409)".
П.С.: я уже читал кучу тем с проблемой и переписку разрабов данного фрейма, что не получится словить "тихое падение" или же "uncaught\unhandled exception" для отдельных потоков или же слотов.
Да я тут много всякого разного экспериментировал с кодом, поэтому много не нужного кода, почищу когда найду проблему.
main.py
import sys
import traceback
# from exception_handler import pyqt_catch_exception_slot, log_uncaught_exceptions
from youtube_thread import YouTubeThread
from pytube import YouTube
from PyQt6 import QtWidgets, QtCore, uic
from PyQt6.QtCore import pyqtSlot, QThreadPool
from PyQt6.QtWidgets import QFileDialog, QMainWindow
def excepthook(type_, value, traceback_):
traceback.print_exception(type_, value, traceback_)
QtCore.qFatal('')
class YouTubeInstance:
def __init__(self):
self.youtube = YouTube
self.path = None
def set_path(self, path):
self.path = path
def download_file(self, url):
local_yt = self.youtube(url=url)
local_yt.streams.get_highest_resolution().download(self.path)
return f'Video \"{local_yt.title}\" has been downloaded successfully!'
class YouTubeMultiDownloader(QMainWindow):
def __init__(self):
super().__init__()
self.ui = uic.loadUi('YTMultiDownloader_3.ui', self)
self.ui.folder_opt.setTextMargins(20, 0, 0, 0)
self.urls = None
self.path = None
# self.thread_pool = QThreadPool()
# self.youtube = YouYouTubeInstance()
self.youtube = YouTube
self.yt_thread = YouTubeThread
# buttons connection with backend
self.ui.folder_btn.clicked.connect(self.get_directory)
self.ui.download_btn.clicked.connect(self.run_multi_downloads)
def _get_urls_from_form(self):
self.urls = self.ui.youtube_urls.toPlainText().split('\n')
def get_directory(self):
path_to_dir = str(QFileDialog.getExistingDirectory(self, "Select folder"))
self.path = path_to_dir
# self.youtube.set_path(path_to_dir)
self.ui.folder_opt.setText(path_to_dir)
@pyqtSlot()
def download_file(self, url):
local_yt = self.youtube(url=url)
local_yt.streams.get_highest_resolution().download(self.path)
resp_text = f'Video \"{local_yt.title}\" has been downloaded successfully!'
self.ui.youtube_opt.setText(resp_text)
@pyqtSlot()
def run_multi_downloads(self):
self.ui.youtube_urls.setEnabled(False)
self.ui.download_btn.setEnabled(False)
self.ui.folder_btn.setEnabled(False)
self._get_urls_from_form()
for url in self.urls:
yt_thread = self.yt_thread(main_window=self, url=url)
yt_thread.start()
# self.thread_pool.start(lambda: self.download_file(url))
# print(self.thread_pool.activeThreadCount())
# self.ui.youtube_opt.setText('\n'.join(self.pool(self._get_urls_from_form()).map(self.youtube.download_file, self.urls)))
self.ui.youtube_urls.setEnabled(True)
self.ui.download_btn.setEnabled(True)
self.ui.folder_btn.setEnabled(True)
if __name__ == "__main__":
sys.excepthook = excepthook
app = QtWidgets.QApplication([])
application = YouTubeMultiDownloader()
application.show()
app.exec()
# sys.exit(app.exec())
'''
pyvid = ['https://youtu.be/qn-lo0AXy5s', 'https://www.youtube.com/watch?v=LVdY83s9TQQ']
download(pyvid[0])
'''
thread.py
from PyQt6 import QtCore
class YouTubeThread(QtCore.QThread):
any_signal = QtCore.pyqtSignal(int)
def __init__(self, main_window, url):
super().__init__()
self.app = main_window
self.url = url
def run(self):
self.app.download_file(url=self.url)
YTMultiDownloader_3.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>main_window</class>
<widget class="QMainWindow" name="main_window">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>990</width>
<height>648</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>990</width>
<height>648</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>990</width>
<height>648</height>
</size>
</property>
<property name="windowTitle">
<string>YouTubeMultiDownloader</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>yt.png</normaloff>yt.png</iconset>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(234, 234, 234);</string>
</property>
<widget class="QWidget" name="central_widget">
<widget class="QPushButton" name="folder_btn">
<property name="geometry">
<rect>
<x>770</x>
<y>80</y>
<width>180</width>
<height>60</height>
</rect>
</property>
<property name="toolTip">
<string><html><head/><body><p>Folder to download all files.</p></body></html></string>
</property>
<property name="whatsThis">
<string><html><head/><body><p><br/></p></body></html></string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(255, 0, 0);
border: 1px solid #000000;
border-radius: 30;
color: rgb(255, 255, 255);
font: 75 14pt "Leelawadee UI";
}
QPushButton:pressed {
background-color: rgb(203, 0, 0);
}</string>
</property>
<property name="text">
<string>Select folder</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
<widget class="QPushButton" name="download_btn">
<property name="geometry">
<rect>
<x>400</x>
<y>540</y>
<width>191</width>
<height>60</height>
</rect>
</property>
<property name="toolTip">
<string><html><head/><body><p>Button to start download files.</p></body></html></string>
</property>
<property name="whatsThis">
<string><html><head/><body><p><br/></p></body></html></string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton {
background-color: rgb(255, 0, 0);
border: 1px solid #000000;
border-radius: 30;
color: rgb(255, 255, 255);
font: 75 14pt "Leelawadee UI";
}
QPushButton:pressed {
background-color: rgb(203, 0, 0);
}</string>
</property>
<property name="text">
<string>Download</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
<widget class="QLineEdit" name="folder_opt">
<property name="geometry">
<rect>
<x>40</x>
<y>80</y>
<width>681</width>
<height>60</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(255, 0, 0);
border: 1px solid #000000;
border-radius: 30;
color: rgb(255, 255, 255);
font: 75 14pt "Leelawadee UI";</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string/>
</property>
</widget>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>40</x>
<y>190</y>
<width>911</width>
<height>301</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontal_layout">
<item>
<widget class="QTextEdit" name="youtube_urls">
<property name="toolTip">
<string><html><head/><body><p><span style=" font-size:12pt;">Paste youtube links here!</span></p></body></html></string>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);
font: 75 12pt "Leelawadee UI";
border: 1px solid #000000;
border-radius: 15;
background-color: rgb(255, 0, 0);</string>
</property>
<property name="placeholderText">
<string>Paste youtube links here...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>41</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QTextEdit" name="youtube_opt">
<property name="toolTip">
<string><html><head/><body><p><br/></p></body></html></string>
</property>
<property name="whatsThis">
<string><html><head/><body><p><br/></p></body></html></string>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);
font: 75 12pt "Leelawadee UI";
border: 1px solid #000000;
border-radius: 15;
background-color: rgb(255, 0, 0);</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
<resources/>
<connections/>
<slots>
<slot>choose_folder()</slot>
<slot>run_multi_downloads()</slot>
</slots>
</ui>
Ответы (1 шт):
Когда мы создаем кнопку на интерфейсе и в коде прописываем методом "connect", то мы связываем сигнал и слот. Сигналом будет выступать событие, то есть когда мы нажимаем на кнопку, то будет происходить вызов этого сигнала. И сигнал дальше дергает слот, который мы соединили методом "connect". В метод "connect" всегда прокидывают аргумент в виде метода класса, который будет дергаться когда придет сигнал.
Оно аналогично работает как callbacks просто в python, то есть когда мы объявили сначала первую функцию, и когда объявляем вторую функцию, и во второй функции вызываем первую:
def first():
pass
def second():
return first()
То сигналом для первой функции выступает вторая функция, а слотом выступает собственно сама первая функция.
Также стоит учитывать, что сигналы бывают разные. Мы можем не прокидывать в сигнал какую-либо сигнатуру, а сам метод находящийся в слоте не будет ожидать каких-либо аргументов на вход. Либо, если нам нужно прокинуть какие-то данные, то мы должны в pyqtSignal указать сигнатуру в виде типа данных, которые мы ожидаем прокинуть.
Про кастомные сигналы не стоит забывать так как мы захотим модифицировать логику работы нашей программы. Можно создавать несколько разных сигналов в одном объекте, чтобы на разную логику отправлялись разные сигналы с разными данными. И не стоит забывать, что мы также можем передать несколько типов сигнатур в один сигнал, но логику через сигналы все же стоит разграничивать, это считается хороший тоном.
Повторюсь! Моя задача состояла в том, чтобы запустить на каждую ссылку отдельный поток и скачать видео\аудио с ютуба, после чего мне необходимо было понять что видео\аудио было скачено, и нужно было как-то вернуть информацию об завершении этого действия. Собственно тут сработал один и единственно верный механизм(который я описал выше), который существует в Qt. Подсказал S.Nick - Документация по Signals & Slots. Также есть и видос на эту тему - нажать сюда.
Собственно, чтобы воплотить мою идею, мне надо было создать как атрибут класса pyqtSignal с той сигнатурой, которую я буду передавать обратно в главное окно. Дальше когда видео\аудио скачивалось мне нужно было написать такой код:
from PyQt6 import QtCore
from pytube import YouTube
class YouTubeThread(QtCore.QThread):
thread_signal = QtCore.pyqtSignal(str)
def __init__(self) -> None:
super().__init__()
self.youtube = YouTube
self.url = None
self.path = None
self.is_video = None
def __download_video(self) -> None:
local_yt = self.youtube(url=self.url)
local_yt.streams.get_highest_resolution().download(self.path)
self.thread_signal.emit(f'Video \"{local_yt.title}\" has been downloaded successfully!')
def __download_audio(self) -> None:
local_yt = self.youtube(url=self.url)
local_yt.streams.get_audio_only().download(self.path)
self.thread_signal.emit(f'Audio \"{local_yt.title}\" has been downloaded successfully!')
def init_args(self, url: str, path: str = None, video: bool = True) -> None:
self.url = url
self.path = path
self.is_video = video
def run(self) -> None:
if self.is_video:
self.__download_video()
else:
self.__download_audio()
То есть после скачивания, я обращался к сигналу по средством метода "emit", и передавал данные в вызванный метод, которые мне надо было передать в главное окно программы для обновления текстового элемента.
Есть также отдельные треды, по схожей теме:
