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_())
→ Ссылка