PyQt5: Cannot set parent, new parent is in a different thread
Я использую PyQt5.
Задача: асинхронное скачивание нескольких файлов и вывод состояние в прогресс-бар.
Пример моего кода:
# self.p1 self.p2 self.p3 - это QProgressBar
import urllib.request
from PyQt5.QtWidgets import *
from PyQt5 import uic, QtCore
import sys
from threading import Thread
class DownloadExample(QWidget):
def __init__(self, path_to_save, url, exported_progress_bar, parent = None):
super(DownloadExample, self).__init__(parent)
self.path_to_save = path_to_save
self.download_url = url
self.progressBar = exported_progress_bar
self.setWindowTitle("Downloader")
def Handle_Progress(self, blocknum, blocksize, totalsize):
readed_data = blocknum * blocksize
if totalsize > 0:
download_percentage = int(readed_data * 100 / totalsize)
if download_percentage > 100:
download_percentage = 100
self.progressBar.setValue(download_percentage)
if download_percentage == 100:
self.progressBar.setValue(100)
else:
QApplication.processEvents()
def Download(self):
urllib.request.urlretrieve(self.download_url, self.path_to_save, self.Handle_Progress)
class TestProgress(QMainWindow):
def __init__(self):
super(TestProgress, self).__init__()
uic.loadUi('test.ui', self)
self.p1.setValue(0)
self.p2.setValue(0)
self.p3.setValue(0)
self.start_btn.clicked.connect(self.download_with_progress)
def download_with_progress(self):
url1 = ""
url2 = ""
url3 = ""
files = [(url1,'exmpl1.gif',self.p1),(url2,'exmpl2.gif',self.p2),(url3,'exmpl3.gif',self.p3)]
for url, name, bar in files:
down = DownloadExample(f'testpic/{name}', url, bar)
thread = Thread(target = down.Download)
thread.start()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = TestProgress()
window.show()
sys.exit(app.exec())
Проблема: постоянно выдает ошибку:
QObject::setParent: Cannot set parent, new parent is in a different thread
Минимальный воспроизводимый пример:
import urllib.request
from PyQt5 import uic, QtCore, Qt, QtWidgets
import sys
from threading import Thread
class DownloadProcess(QtWidgets.QWidget):
def __init__(self, path_to_save, url, exported_progress_bar):
super(DownloadProcess, self).__init__()
self.path_to_save = path_to_save
self.download_url = url
self.progressBar = exported_progress_bar
def Handle_Progress(self, blocknum, blocksize, totalsize):
readed_data = blocknum * blocksize
if totalsize > 0:
download_percentage = int(readed_data * 100 / totalsize)
if download_percentage > 100:
download_percentage = 100
self.progressBar.setValue(download_percentage)
if download_percentage == 100:
self.progressBar.setValue(100)
else:
QtWidgets.QApplication.processEvents()
def Download(self):
urllib.request.urlretrieve(self.download_url, self.path_to_save, self.Handle_Progress)
class TestProgress(QtWidgets.QWidget):
def __init__(self):
super(TestProgress, self).__init__()
layout = Qt.QVBoxLayout(self)
self.progressBar1 = QtWidgets.QProgressBar(self)
self.progressBar2 = QtWidgets.QProgressBar(self)
self.progressBar3 = QtWidgets.QProgressBar(self)
self.progressBar1.setGeometry(25, 45, 210, 30)
self.progressBar2.setGeometry(25, 45, 210, 30)
self.progressBar3.setGeometry(25, 45, 210, 30)
button = QtWidgets.QPushButton('Start', self)
button.clicked.connect(self.download_with_progress)
layout.addWidget(self.progressBar1)
layout.addWidget(self.progressBar2)
layout.addWidget(self.progressBar3)
layout.addWidget(button)
def download_with_progress(self):
url1 = "https://gph.is/2K71ZPc"
url2 = "https://gph.is/2K71ZPc"
url3 = "https://gph.is/2K71ZPc"
files = [
(url1,'exmpl1.gif',self.progressBar1),
(url2,'exmpl2.gif',self.progressBar2),
(url3,'exmpl3.gif',self.progressBar3)
]
for url, name, bar in files:
down = DownloadProcess(f'testpic/{name}', url, bar)
thread = Thread(target = down.Download)
thread.start()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = TestProgress()
window.show()
sys.exit(app.exec())
Ответы (2 шт):
Автор решения: S. Nick
→ Ссылка
Если вы хотите обмениваться информацией между потоками, то вы должны использовать сигналы, которые являются потокобезопасными и уведомляют о появлении новых данные.
Ваша задача может выглядеть примерно так:
import sys
import threading
import time
from PyQt5.QtCore import pyqtSignal, QObject, Qt
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import (QApplication, QMainWindow,
QProgressBar, QPushButton, QWidget, QFormLayout
)
class Signaller(QObject):
progress_changed = pyqtSignal(int)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
btn = QPushButton("Запустить задачу")
btn.setCursor(QCursor(Qt.PointingHandCursor))
btn.clicked.connect(self.button_clicked)
self.layout = QFormLayout(self.centralWidget)
self.layout.addRow(btn)
self.layout.setFormAlignment(Qt.AlignHCenter | Qt.AlignBottom)
self.num_task = 1
def button_clicked(self):
thread_name = f'task {self.num_task}'
self.num_task += 1
scanProgressbar = QProgressBar()
self.layout.insertRow(0, thread_name, scanProgressbar)
signaller = Signaller()
signaller.progress_changed.connect(scanProgressbar.setValue)
thread = threading.Thread(
target=child_func,
args=(signaller,),
daemon=True,
name=thread_name
)
thread.start()
def child_func(signaller):
for i in range(1, 101):
signaller.progress_changed.emit(i)
time.sleep(0.1)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MainWindow()
w.resize(400, 200)
w.show()
sys.exit(app.exec_())
Автор решения: Sergievsky
→ Ссылка
Спасибо S. Nick за помощь с сигналами в PyQt5. Я выберу его решение более правильным чем мое, потому что оно более универсально. Я дописал код под мой вопрос ниже:
import sys
import threading
import time
from PyQt5.QtCore import pyqtSignal, QObject, Qt
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import (QApplication, QMainWindow,
QProgressBar, QPushButton, QWidget, QFormLayout
)
import urllib.request
class DownloadProcess(QObject):
def __init__(self, url, path):
self.url = url
self.path = path
def Handle_Progress(self, blocknum, blocksize, totalsize):
readed_data = blocknum * blocksize
if totalsize > 0:
download_percentage = int(readed_data * 100 / totalsize)
if download_percentage > 100:
download_percentage = 100
self.func_to_emit.emit(download_percentage)
if download_percentage == 100:
self.func_to_emit.emit(100)
def Download(self,signaller):
self.func_to_emit = signaller.progress_changed
urllib.request.urlretrieve(self.url, self.path, self.Handle_Progress)
class Signaller(QObject):
progress_changed = pyqtSignal(int)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
btn = QPushButton("Запустить задачу")
btn.setCursor(QCursor(Qt.PointingHandCursor))
btn.clicked.connect(self.button_clicked)
self.layout = QFormLayout(self.centralWidget)
self.layout.addRow(btn)
self.layout.setFormAlignment(Qt.AlignHCenter | Qt.AlignBottom)
self.num_task = 1
def button_clicked(self):
url1 = "https://gph.is/2K71ZPc"
url2 = "https://gph.is/2K71ZPc"
url3 = "https://gph.is/2K71ZPc"
files = [
(url1,'exmpl1.gif'),
(url2,'exmpl2.gif'),
(url3,'exmpl3.gif')
]
for url, name in files:
thread_name = f'task {self.num_task}'
self.num_task += 1
scanProgressbar = QProgressBar()
self.layout.insertRow(0, thread_name, scanProgressbar)
signaller = Signaller()
signaller.progress_changed.connect(scanProgressbar.setValue)
download_please = DownloadProcess(url, name)
thread = threading.Thread(
target=download_please.Download,
args=(signaller,),
daemon=True,
name=thread_name
)
thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MainWindow()
w.resize(400, 200)
w.show()
sys.exit(app.exec_())
