Как настроить QThread для обработки массива и его передачи в другую функцию основной программы?

Пытаюсь сделать небольшую программу и заодно освоить PyQt6.
Программа должна забирать некие данные из файла Excel, отправлять их в качестве запроса на сайт в Интернете и возвращать некий новый набор данных, которые записываются в другой файл Excel.

Естественно, на этапе отправки и ожидания ответа от сайта интерфейс зависает.

Пробую решить эту проблему с использованием QThread, но возникает ошибка:

Process finished with exit code -1073740791 (0xC0000409)

Судя по всему, ошибка возникает при инициализации потока, но в чем проблема - понять никак не могу.


Пример работающего кода, но без QThread и с зависанием GUI:

import sys
import test_ui
from PyQt6 import QtWidgets
import time


def select_loadfile():
    file = QtWidgets.QFileDialog.getOpenFileName(parent=window, filter="*xlsx")
    if file[0] == '':
        return None
    else:
        window.line1.setText(file[0])

def select_savefile():
    file = QtWidgets.QFileDialog.getOpenFileName(parent=window, filter="*xlsx")
    if file[0] == '':
        return None
    else:
        window.line2.setText(file[0])

def before_working():
    start_array = ['Data1', 'Data2', 'Data3']  # Имитация полученных из файла Excel данных

    window.progr_bar.setRange(0, len(start_array))

    return start_array

def working(input_array):
    print(f'Пришло в working {input_array}')

    result_array = []

    for item in input_array:
        time.sleep(3)  # Имитация ожидания ответа от сайта
        result_array.append(item)

        pb_value = window.progr_bar.value()
        window.progr_bar.setValue(pb_value + 1)

    return result_array

def after_working(input_array):  # Имитация сохранения данных в файле
    window.button1.setDisabled(False)
    window.button2.setDisabled(False)
    window.button_start.setDisabled(False)

    if window.line2.displayText() != '':
        print(f'Пришло в get_result {input_array} без запроса сохранения')
        window.inf_msg('Успех', 'Процесс успешно завершен')
    else:
        select_savefile()
        if window.line2.displayText() != '':
            print(f'Пришло в get_result {input_array} с запросом сохранения')
            window.inf_msg('Успех', 'Процесс успешно завершен')
        else:
            window.err_msg('Результаты проверки не сохранены')

    window.progr_bar.setValue(0)


def main():
    if window.line1.displayText() != '':
        after_working(working(before_working()))
    else:
        window.err_msg('Не выбран файл загрузки')


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = test_ui.MyWin()

    window.button1.clicked.connect(select_loadfile)
    window.button2.clicked.connect(select_savefile)
    window.button_start.clicked.connect(main)

    window.show()
    sys.exit(app.exec())

Файл графического интерфейса test_ui.py, в нем же класс Worker для отправки в поток:

    from PyQt6 import QtWidgets, QtCore
    import time
    
    
    class MyWin(QtWidgets.QWidget):
        def __init__(self, parent=None):
            QtWidgets.QWidget.__init__(self, parent)
    
            self.resize(300, 200)
            icon = self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation)
            self.setWindowIcon(icon)
            self.setWindowTitle("Выполнение долгой задачи")
    
            self.line1 = QtWidgets.QLineEdit()
            self.line1.setReadOnly(True)
            self.line2 = QtWidgets.QLineEdit()
            self.line2.setReadOnly(True)
    
            self.button1 = QtWidgets.QPushButton("Выбрать файл")
            self.button2 = QtWidgets.QPushButton("Выбрать файл")
            self.button_start = QtWidgets.QPushButton("НАЧАТЬ")
    
            self.progr_bar = QtWidgets.QProgressBar()
            self.progr_bar.setValue(0)
    
            self.box1 = QtWidgets.QHBoxLayout()
            self.box1.addWidget(self.line1)
            self.box1.addWidget(self.button1)
    
            self.box2 = QtWidgets.QHBoxLayout()
            self.box2.addWidget(self.line2)
            self.box2.addWidget(self.button2)
    
            self.main_box = QtWidgets.QVBoxLayout()
            self.main_box.addLayout(self.box1)
            self.main_box.addLayout(self.box2)
            self.main_box.addWidget(self.button_start)
            self.main_box.addWidget(self.progr_bar)
    
            self.setLayout(self.main_box)
    
        # Функция вывода сообщения об ошибке с заданным текстом
        def err_msg(self, msg_text):
            dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Icon.Warning, 'Ошибка', msg_text,
                                           buttons=QtWidgets.QMessageBox.StandardButton.Ok, parent=self)
            icon = self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning)
            dialog.setWindowIcon(icon)
            dialog.exec()
    
        # Функция вывода информационного сообщения с заданным текстом
        def inf_msg(self, msg_header, msg_text):
            dialog = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Icon.Information, msg_header, msg_text,
                                           buttons=QtWidgets.QMessageBox.StandardButton.Ok, parent=self)
            icon = self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation)
            dialog.setWindowIcon(icon)
            dialog.exec()
    
    
    class Worker(QtCore.QObject):
        progress = QtCore.pyqtSignal()
        finished = QtCore.pyqtSignal(object)
    
        def __init__(self, input_array):
            super().__init__()
            self.input_array = input_array
    
        def start(self):
            print('Начата работа в отдельном потоке')
            res_array = []
            
            for item in self.input_array:
                time.sleep(3)             # Имитация ожидания ответа от сайта
                res_array.append(item)
                self.progress.emit()
    
            print(f'Массив в потоке {res_array}')
            self.finished.emit(res_array)

Файл с основной функцией и попыткой использовать QThread - не работает:

import sys
import test_ui
from PyQt6 import QtWidgets, QtCore


def select_loadfile():
    file = QtWidgets.QFileDialog.getOpenFileName(parent=window, filter="*xlsx")
    if file[0] == '':
        return None
    else:
        window.line1.setText(file[0])


def select_savefile():
    file = QtWidgets.QFileDialog.getOpenFileName(parent=window, filter="*xlsx")
    if file[0] == '':
        return None
    else:
        window.line2.setText(file[0])


def before_working():
    start_array = ['Data1', 'Data2', 'Data3'] # Имитация данных из файла Excel

    window.progr_bar.setRange(0, len(start_array))

    return start_array

def working(input_array):
    print(f'Пришло в working {input_array}')

    result_array = []

    thread_arr = input_array

    def update_progressbar():
        pb_value = window.progr_bar.value()
        window.progr_bar.setValue(pb_value + 1)

    def get_result(inp_data):
        print(f'пришло в get_result {inp_data}')
        for item in inp_data:
            result_array.append(item)

    window.button1.setDisabled(True)
    window.button2.setDisabled(True)
    window.button_start.setDisabled(True)

    obj = test_ui.Worker(thread_arr)  # Попытка создать объект и направить в поток 
    t = QtCore.QThread()
    obj.moveToThread(t)
    t.started.connect(obj.start)
    obj.finished.connect(t.quit)
    obj.finished.connect(get_result)
    obj.progress.connect(update_progressbar)
    t.start()

    print('Поток запущен')

    return result_array

def after_working(input_array):  # Имитация функции для записи результатов в файл Excel
    window.button1.setDisabled(False)
    window.button2.setDisabled(False)
    window.button_start.setDisabled(False)

    if window.line2.displayText() != '':
        print(f'пришло в get_result {input_array}')
        window.inf_msg('Успех', 'Процесс успешно завершен')
    else:
        select_savefile()
        if window.line2.displayText() != '':
            print(f'пришло в get_result {input_array}')
            window.inf_msg('Успех', 'Процесс успешно завершен')
        else:
            window.err_msg('Результаты проверки не сохранены')

    window.progr_bar.setValue(0)


def main():
    if window.line1.displayText() != '':
        after_working(working(before_working()))
    else:
        window.err_msg('Не выбран файл загрузки')


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = test_ui.MyWin()

    window.button1.clicked.connect(select_loadfile)
    window.button2.clicked.connect(select_savefile)
    window.button_start.clicked.connect(main)

    window.show()
    sys.exit(app.exec())

Ответы (2 шт):

Автор решения: S. Nick

Чтобы получить реальную ошибку, надо запускать приложение в консоли/терминале/CMD.
Sorry, я не проверял какую реальную ошибку вы получаете.

Вам надо запомнить, что нельзя использовать в основном потоке while True: и time.sleep(3) - это блокирует интерфейс.

Вам надо запомнить, что нельзя взаимодействовать с виджетами в дополнительном потоке, это небезопасно.
Надо использовать сигналы и слоты.

Как вариант, это может выглядеть примерно так:

import sys
#import test_ui
#from PyQt6 import QtWidgets, QtCore
from PyQt5 import QtWidgets, QtCore
#import time


def before_working():
    start_array = ['Data1', 'Data2', 'Data3']  # Имитация полученных из файла Excel данных
#    window.progr_bar.setRange(0, len(start_array))
    return start_array

# !!! +++ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
class Thread(QtCore.QThread):                                      
    threadSignal = QtCore.pyqtSignal(int)                          # !!!
    
    def __init__(self):
        super().__init__()
        self.value = 0
        self.input_array = []
        self.result_array = []
        
    def run(self):
        print(f'run(self): {self.input_array}') #
        self.result_array = []
        for item in self.input_array: 
            self.result_array.append(item)
            self.msleep(3000)        # Имитация ожидания ответа от сайта
            self.value += 1
            self.threadSignal.emit(self.value)                      # !!!
        
# !!! +++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

class MyWin(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.resize(300, 200)
        icon = self.style().standardIcon(
            QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation)
        self.setWindowIcon(icon)
        self.setWindowTitle("Выполнение долгой задачи")
    
        self.line1 = QtWidgets.QLineEdit()
        self.line1.setReadOnly(True)
        
        self.line2 = QtWidgets.QLineEdit()
# ?        self.line2.setReadOnly(True)
    
        self.button1 = QtWidgets.QPushButton("Выбрать файл")
        self.button1.clicked.connect(self.select_loadfile)          # +
        self.button2 = QtWidgets.QPushButton("Выбрать файл")
        self.button2.clicked.connect(self.select_savefile)          # +
        self.button_start = QtWidgets.QPushButton("НАЧАТЬ")
        self.button_start.clicked.connect(self.begin)               # +   
    
        self.progr_bar = QtWidgets.QProgressBar()
        self.progr_bar.setValue(0)
    
        self.box1 = QtWidgets.QHBoxLayout()
        self.box1.addWidget(self.line1)
        self.box1.addWidget(self.button1)
    
        self.box2 = QtWidgets.QHBoxLayout()
        self.box2.addWidget(self.line2)
        self.box2.addWidget(self.button2)
    
        self.main_box = QtWidgets.QVBoxLayout(self)
        self.main_box.addLayout(self.box1)
        self.main_box.addLayout(self.box2)
        self.main_box.addWidget(self.button_start)
        self.main_box.addWidget(self.progr_bar)

# !!! +++ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv       
        self.thread = Thread()                                     # !!!  
        self.thread.threadSignal.connect(self.on_threadSignal)     # !!!
        self.thread.finished.connect(                              # !!!
            lambda: self.after_working(self.thread.result_array))

    def on_threadSignal(self, value):                              # !!!
        print(f'on_threadSignal(self, value): {value}') #
        self.progr_bar.setValue(value)
    
    # Функция вывода сообщения об ошибке с заданным текстом
    def err_msg(self, msg_text):
        dialog = QtWidgets.QMessageBox(
            QtWidgets.QMessageBox.Icon.Warning, 
            'Ошибка', msg_text,
            buttons=QtWidgets.QMessageBox.StandardButton.Ok, parent=self)
        icon = self.style().standardIcon(
            QtWidgets.QStyle.StandardPixmap.SP_MessageBoxWarning)
        dialog.setWindowIcon(icon)
        dialog.exec()
    
    # Функция вывода информационного сообщения с заданным текстом
    def inf_msg(self, msg_header, msg_text):
        dialog = QtWidgets.QMessageBox(
            QtWidgets.QMessageBox.Icon.Information, 
            msg_header, msg_text,
            buttons=QtWidgets.QMessageBox.StandardButton.Ok, parent=self)
        icon = self.style().standardIcon(
            QtWidgets.QStyle.StandardPixmap.SP_MessageBoxInformation)
        dialog.setWindowIcon(icon)
        dialog.exec()
        
    def select_loadfile(self):
        filePath, ok = QtWidgets.QFileDialog.getOpenFileName(
            self, filter="*xlsx")
        if filePath:
            self.line1.setText(filePath)

    def select_savefile(self):
        filePath, ok = QtWidgets.QFileDialog.getOpenFileName(
            self, filter="*xlsx")
        if filePath:
            self.line2.setText(filePath)

    def begin(self):
        print(f'line1.displayText() = {window.line1.displayText()}') #
        if self.line1.displayText():
            startArray = before_working()
            if not startArray:
                self.err_msg('Пустой startArray ???')
                return
                
            self.progr_bar.setRange(0, len(startArray))
            
            self.thread.input_array = startArray
            self.thread.value = 0
            self.button1.setDisabled(True)
            self.button2.setDisabled(True)
            self.button_start.setDisabled(True)
            self.thread.start()                                         # !!!
#            self.after_working(working(startArray))
        else:
            self.err_msg('Не выбран файл загрузки.')
 
    def after_working(self, input_array):  # Имитация сохранения данных в файле
        print(f'\ndef after_working(input_array):{input_array}\n') #
        self.button1.setDisabled(False)
        self.button2.setDisabled(False)
        self.button_start.setDisabled(False)
        self.progr_bar.setValue(0)

# ? vvv
        if self.line2.displayText() != '':
            print(f'Пришло в get_result {input_array} без запроса сохранения')
            self.inf_msg('Успех', 'Процесс успешно завершен')
        else:
            self.select_savefile()
            if self.line2.displayText() != '':
                print(f'Пришло в get_result {input_array} с запросом сохранения')
                self.inf_msg('Успех', 'Процесс успешно завершен')
            else:
                self.err_msg('Результаты проверки не сохранены')
# !!! +++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
#    window = test_ui.MyWin()
    window = MyWin()                                                  # +++
    window.show()
    sys.exit(app.exec())

введите сюда описание изображения


Установите свои импорты и проверьте.

→ Ссылка
Автор решения: sybit

Спасибо большое S. Nick, все заработало!

Вам надо запомнить, что нельзя использовать в основном потоке while True: и time.sleep(3) - это блокирует интерфейс

Да, я использовал time.sleep(3), чтобы в примере сделать имитацию ожидания ответа от сайта и соответственно зависание интерфейса. Единственное, что в функции def after_working перенес сброс статусбара в самый конец, уже после сохранения/несохранения результата.

def after_working(self, input_array):  # Имитация сохранения данных в файле
    print(f'\ndef after_working(input_array):{input_array}\n')  #
    self.button1.setDisabled(False)
    self.button2.setDisabled(False)
    self.button_start.setDisabled(False)
    if self.line2.displayText() != '':
        print(f'Пришло в get_result {input_array} без запроса сохранения')
        self.inf_msg('Успех', 'Процесс успешно завершен')
    else:
        self.select_savefile()
        if self.line2.displayText() != '':
            print(f'Пришло в get_result {input_array} с запросом сохранения')
            self.inf_msg('Успех', 'Процесс успешно завершен')
        else:
            self.err_msg('Результаты проверки не сохранены')
    self.progr_bar.setValue(0)  # Сброс статусбара
→ Ссылка