Многопоточность в GUI на PyQt5 Python

Рейтинг: 2Ответов: 1Опубликовано: 08.02.2023

Я написал небольшое приложение на PyQt5. На главном окне есть таблица, которая при нажатии на кнопку "Считать" начинает заполняться данными (около 12 тысяч строк) из базы данных SQLite.

Одновременно с заполнением таблицы мне нужно чтобы появлялось окно с прогрессбаром и таймерами прошедшего времени.

Так же в левом нижнем углу окна есть индикаторы Tx и Rx - они должны мигать, когда происходит чтение/запись данных.

Собственно, сам вопрос, как максимально просто реализовать многопоточность такой программы?
Нужно ли каким-то образом запустить каждый процесс (запись данных, таймеры, индикаторы) в отдельном потоке?
Как тогда передавать данные из одного потока в другой?
Как, например, индикатор, должен узнать, что в другом потоке произошла запись в таблицу и мигнуть?

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # конфигурация главного окна и центрального виджета
        self.central_widget = QWidget(self)
        self.setGeometry(50, 50, 1500, 800)
        self.setWindowTitle('Чтение БКУ')
        self.setCentralWidget(self.central_widget)
        self.layout_main_window = QVBoxLayout()
        self.central_widget.setLayout(self.layout_main_window)
        
        # конфигурация верхней панели инструментов
        self.btn_start_reading = QPushButton("Считать")
        self.btn_start_reading.clicked.connect(self.start_read)
        self.btn_download_from_file = QPushButton("Загрузить из файла")
        self.btn_download_from_file.clicked.connect(self.download_from_file)
        self.btn_save_to_file = QPushButton("Экспорт")
        self.btn_save_to_file.clicked.connect(self.save_to_file)
        self.layout_toolbar = QHBoxLayout()
        self.layout_toolbar.addStretch(1)
        self.layout_toolbar.addWidget(self.btn_start_reading)
        self.layout_toolbar.addWidget(self.btn_download_from_file)
        self.layout_toolbar.addWidget(self.btn_save_to_file)
        self.layout_main_window.addLayout(self.layout_toolbar)

        # конфигурация таблицы
        self.layout_table = QHBoxLayout()
        self.table = QTableWidget()
        self.table.setColumnCount(9)
        self.table.setRowCount(13500)
        self.table.setHorizontalHeaderLabels(['№', 'Дата и время', 'БКУ', 'КЛ', 'АУ', 'Канал', 'Код события', 'Доп. параметр', 'Описание'])
        self.table.horizontalHeader().setStretchLastSection(True)
        self.table.horizontalHeader().setVisible(True)
        self.layout_table.addWidget(self.table)
        self.layout_main_window.addLayout(self.layout_table)

        # конфигурация статус панели
        self.layout_status_bar = QHBoxLayout()
        self.layout_status_bar.addStretch(0)
        self.layout_status_bar.setDirection(QBoxLayout.RightToLeft)
        self.btn_select_port = QPushButton('Выбрать порт')
        self.btn_select_port.clicked.connect(self.select_port)
        self.label_current_port = QLabel()
        if combobox_remembered_text() in ports_list:
            self.label_current_port.setText(f': {combobox_remembered_text()}')
        else:
            self.select_port.setCurrentIndex(ports_list.index('Не выбран'))
        # конфигурация Rx Tx:
        self.label_tx = QLabel('Tx: ')
        self.rb_tx = QRadioButton() 
        self.rb_tx.setObjectName('rb_tx')
        self.label_rx = QLabel('Rx: ')
        self.rb_rx = QRadioButton() 
        self.rb_rx.setObjectName('rb_rx') 
        self.setStyleSheet(rx_tx_stylesheet)
        # __________________________________
        self.label_trans_packets = QLabel('Передано: 0  ')
        self.label_rec_packets = QLabel('Принято: 0  ')
        self.label_count_of_log_entries = QLabel('Количество записей: 0  ')
        self.label_session_time = QLabel('00:00:00')
        self.layout_status_bar.addWidget(self.label_session_time)
        self.layout_status_bar.addWidget(self.label_count_of_log_entries)
        self.layout_status_bar.addWidget(self.label_rec_packets)
        self.layout_status_bar.addWidget(self.label_trans_packets)
        self.layout_status_bar.addWidget(self.rb_rx) 
        self.layout_status_bar.addWidget(self.label_rx)
        self.layout_status_bar.addWidget(self.rb_tx)
        self.layout_status_bar.addWidget(self.label_tx)
        self.layout_status_bar.addSpacing(30)
        self.layout_status_bar.addWidget(self.label_current_port)
        self.layout_status_bar.addWidget(self.btn_select_port)
        self.layout_main_window.addLayout(self.layout_status_bar)

        # Создание потока для мигающих индикаторов
        self.thread = ReadingRxTxThread()                                   
        self.thread.dateSignal.connect(self.update_data) 
        

    def start_read(self):
        # запуск потока для 
        self.thread.start()

        # Открытие окна с прогрессбаром для отслеживания процесса записи данных
        modal_dialog = ReadingProgressDialog()
        modal_dialog.show()
        modal_dialog.exec_()
        self.table.clearContents()
        i = 0
        # запись данных в таблицу
        for event in session.query(Event).all():
            self.table.setItem(i, 0, QTableWidgetItem(str(i+1)))
            self.table.item(i, 0).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 1, QTableWidgetItem(str(event.date_time)))
            self.table.item(i, 1).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 2, QTableWidgetItem(str(event.bku)))
            self.table.item(i, 2).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 3, QTableWidgetItem(str(event.kl)))
            self.table.item(i, 3).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 4, QTableWidgetItem(str(event.au)))
            self.table.item(i, 4).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 5, QTableWidgetItem(str(event.channel)))
            self.table.item(i, 5).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 6, QTableWidgetItem(str(event.event_code)))
            self.table.item(i, 6).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 7, QTableWidgetItem(str(event.addit_param)))
            self.table.item(i, 7).setBackground(QColor(codes_dictionary[event.event_code][1]))
            self.table.setItem(i, 8, QTableWidgetItem(str(event.description)))
            self.table.item(i, 8).setBackground(QColor(codes_dictionary[event.event_code][1]))
            i += 1
            

    def save_to_file(self):
        pass


    def download_from_file(self):
        pass


    def select_port(self):
        modal_dialog = SelectPortDialog()
        modal_dialog.show()
        modal_dialog.exec_()
        selected_port = modal_dialog.selected_port
        self.label_current_port.setText(f': {selected_port}')


    def update_data (self, text):
        if text == 'Tx':
            self.rb_tx.click()
        elif text == 'Rx':
            self.rb_rx.click()



if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())

При таком коде, при нажатии на кнопку "Считать" открывается модальное окно, но запись данных в таблицу не начинается, пока окно не закроется. А запись и отображение данных в окне должны происходить одновременно. Так же мне подсказали решение проблемы с мигающими индикаторами (Tx и Rx слева внизу таблицы), но как мне из потока с индикаторами получать информацию о записи данных в таблицу? Ниже код потока для индикаторов.

class ReadingRxTxThread(QtCore.QThread):
    dateSignal = QtCore.pyqtSignal(str)
    
    def __init__(self):    
        super().__init__()
        
    def run(self):        
        # тут вы получаете какие-то данные с прибора
        text = random.choice(['Tx', 'Rx'])
        # тут вы испускаете сигнал и передаете какие-то данные
        self.dateSignal.emit(text)
        self.msleep(50)

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

Ответы

▲ 1Принят

Пример, который вы предоставили, не является минимально-воспроизводимым и проверить его не возможно.

Логика работы вашего приложения мне видится такой:

  • основной поток, это главное окно (класс MainWindow) и
    дополнительный поток (ОДИН, класс ReadingRxTxThread);

  • в дополнительном потоке вы читаете данные из базы данных SQLite и передаете в основной поток все необходимые данные для заполнения таблицы и отображения прогрессбара.

  • в основном потоке вы:

    • принимаете все данные;
    • заполняете таблицу;
    • отображаете прогрессбар, который рекомендую разместить в статусной строке, правее прошедшего времени;
    • после чего передаете в метод update_data() данные для отображения индикатора;
    • создаете таймер времени, который стартуете там же где и запускаете дополнительный поток; независимо ни от чего отображаете время в статусной строке и закрываете таймер по завершению работы дотолнительного потока.

Все.