Evgenii Legotckoi
Evgenii Legotckoi26 листопада 2019 р. 04:53

PyQt5 - Урок 009. Використання QThread із застосуванням moveToThread

На основі одного з питань на форумі я написав приклад по використанню QThread в PyQt5, а також використання методу moveToThread для переміщення об'єкта класу наследованного QObject в інший потік.

В даному прикладі проводиться виконання якогось алгоритму, які через сигнал повертає текст, а також колір текст в головний GUI. Ці дані додаються в QTextBrowser з установкою кольору.

Програма буде виглядати наступним чином


Вступ

Існують два основні підходи для використання QThread в Qt :

  1. Створити новий клас, який успадкований від QThread і перевизначити метод run
  2. Створити новий клас, який успадкований від QObject , написати метод run, який буде виконувати якийсь код, і передати інстанси цього класу в інший потік за допомогою методу moveToThread

Перший метод рекомендується використовувати тільки в тому випадку, якщо вам дійсно потрібно перевизначити клас потоку, щоб створити спеціальний функціонал в класі потоку. Якщо ж вам потрібно виконувати якийсь код в іншому потоці, то для цього потрібно створювати окремий клас, який буде перенесений в інший потік за допомогою методу moveToThread .

Також, відразу хочу зазначити, що не можна передавати GUI об'єкти в інші потоки. Програми на Qt мають два види потоків:

  • Головний потік. GUI thread
  • Робочі потоки. Worker threads

Всі GUI об'єкти повинні створювати і працювати тільки в GUI потоці, тоді, як різні інші алгоритми можуть виконуватися в робочих потоках.

Цитирую документацию

As mentioned, each program has one thread when it is started. This thread is called the "main thread" (also known as the "GUI thread" in Qt applications). The Qt GUI must run in this thread. All widgets and several related classes, for example QPixmap, don't work in secondary threads. A secondary thread is commonly referred to as a "worker thread" because it is used to offload processing work from the main thread.

Тобто якщо, навіть у вас щось і запрацює в іншому потоці, то це буде лише випадковість, яка рано чи пізно дасть про себе знати. А ваша програма перестане працювати. Ніколи не передавайте інші GUI об'єкти в інші потоки.

Програма

А тепер розглянемо код нашої програми. Зверніть увагу, що алгоритм дій буде наступний

  1. Пишемо клас, який успадкований від QObject і має метод run для виконання коду в іншому потоці
  2. У конструкторі вікна створюємо об'єкт потоку
  3. У конструкторі вікна створюємо об'єкт, який буде перенесений в інший потоку
  4. Переносимо об'єкт в інший потоку
  5. Підключаємо сигнали і слоти
  6. Запускаємо потік

Виконувати всю ініціалізацію об'єкта і потоку бажано саме в такій послідовності, якщо ви ще не володієте достатнім досвідом.
Втім тоді ви б не читали цю статтю.
Якщо поміняти місцями кроки 4 і 5 то тоді ви спочатку підключіть сигнали і слоти, а потім перенесете об'єкт в інший потоку, що розіб'є сигнал / слотове з'єднання. Перестане працювати вікно програми. Або додаток може просто впасти.

import sys
import time

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtGui import QColor


class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(453, 408)
        self.verticalLayout = QtWidgets.QVBoxLayout(Form)
        self.verticalLayout.setObjectName("verticalLayout")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout()
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.textBrowser = QtWidgets.QTextBrowser(Form)
        self.textBrowser.setObjectName("textBrowser")
        self.verticalLayout_2.addWidget(self.textBrowser)
        self.verticalLayout.addLayout(self.verticalLayout_2)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setObjectName("pushButton")
        self.horizontalLayout.addWidget(self.pushButton)
        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem1)
        self.verticalLayout.addLayout(self.horizontalLayout)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Example"))
        self.pushButton.setText(_translate("Form", "Input"))


# Объект, который будет перенесён в другой поток для выполнения кода
class BrowserHandler(QtCore.QObject):
    running = False
    newTextAndColor = QtCore.pyqtSignal(str, object)

    # метод, который будет выполнять алгоритм в другом потоке
    def run(self):
        while True:
            # посылаем сигнал из второго потока в GUI поток
            self.newTextAndColor.emit(
                '{} - thread 2 variant 1.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))),
                QColor(0, 0, 255)
            )
            QtCore.QThread.msleep(1000)

            # посылаем сигнал из второго потока в GUI поток
            self.newTextAndColor.emit(
                '{} - thread 2 variant 2.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))),
                QColor(255, 0, 0)
            )
            QtCore.QThread.msleep(1000)


class MyWindow(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super().__init__()
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        # використовуємо кнопку для додавання тексту з кольором в головному потоці
        self.ui.pushButton.clicked.connect(self.addAnotherTextAndColor)

        # створимо потік
        self.thread = QtCore.QThread()
        # створимо об'єкт для виконання коду в іншому потоці
        self.browserHandler = BrowserHandler()
        # перенесемо об'єкт в інший потік
        self.browserHandler.moveToThread(self.thread)
        # після чого підключимо всі сигнали і слоти
        self.browserHandler.newTextAndColor.connect(self.addNewTextAndColor)
        # підключимо сигнал старту потоку до методу run у об'єкта, який повинен виконувати код в іншому потоці
        self.thread.started.connect(self.browserHandler.run)
        # запустимо потік
        self.thread.start()

    @QtCore.pyqtSlot(str, object)
    def addNewTextAndColor(self, string, color):
        self.ui.textBrowser.setTextColor(color)
        self.ui.textBrowser.append(string)

    def addAnotherTextAndColor(self):
        self.ui.textBrowser.setTextColor(QColor(0, 255, 0))
        self.ui.textBrowser.append('{} - thread 2 variant 3.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))))


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec())

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

Вам це подобається? Поділіться в соціальних мережах!

c
  • 27 листопада 2019 р. 19:44

Огромное спасибо!

b
  • 07 травня 2020 р. 02:27
  • (відредаговано)

простите за беспокойсто. Разобрался )) Спасибо Вам огромное. По сути у Вас тольько и разобрался с сигналами и слотами

b
  • 07 травня 2020 р. 03:16

в продолжение, хотелось бы уточнить такой вопрос. Испускаемые сигналы - они глобальны? то есть на сгенерированный pyqtSignal в классе А, можно ли "подписаться" в классах B,C,D своими слотами? То есть по одному сигналу, может ли каждый класс выполнять что-то свое?

Evgenii Legotckoi
  • 07 травня 2020 р. 03:18

Да, можно. к одному сигналу можно быть подключено какое угодно количество слотов в каком угодно наборе объектов.

b
  • 07 травня 2020 р. 13:40

спасибо Вам большое

YA
  • 16 квітня 2021 р. 17:25

Hello. Let's say I want to send some variables to "run" define. How can we do that? I modified your code, I tried something like below, but the GUI is frozen that way. I could not be able to understand it. Can you please give me some advice?

import sys
import time
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QPushButton, QListWidget, QVBoxLayout, QLineEdit
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot, QObject
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
from pymodbus.transaction import ModbusRtuFramer

class RequestHandler(QObject):
    running = False
    output_signal = pyqtSignal(list)

    @pyqtSlot(str, int)
    def run(self, ip, port, unit):
        client = ModbusClient(ip, port=port, framer=ModbusRtuFramer)
        client.connect()
        print("run")
        while True:
            print(int(QThread.currentThreadId()))

            for i in range(2):
                try:
                    m = client.read_holding_registers(0,9, unit = unit[i])
                    self.output_signal.emit(m.registers)
                except:
                    self.output_signal.emit(["ERROR"])

            QThread.msleep(2000)

class MyWindow(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super().__init__()
        self.list1 = QLineEdit()
        self.buton = QPushButton("BASlAT")
        self.v = QVBoxLayout()
        self.v.addWidget(self.list1)
        self.v.addWidget(self.buton)
        self.buton.clicked.connect(self.start)
        self.setLayout(self.v)
        self.show()

    @pyqtSlot(list)
    def addNewTextAndColor(self, output):
        print(output)

        pass

    def start(self):
        unit = [14, 15]
        self.thread = QThread()
        self.browserHandler = RequestHandler()
        self.browserHandler.moveToThread(self.thread)
        print(int(self.thread.currentThreadId()))
        self.browserHandler.output_signal.connect(self.addNewTextAndColor)
        self.thread.started.connect(self.browserHandler.run("192.168.1.100", 502, unit))
        self.thread.start()


app = QtWidgets.QApplication(sys.argv)
window = MyWindow()

sys.exit(app.exec())

c
  • 22 серпня 2021 р. 02:38

Здравствуйте.
Разрешите пару вопросов...
1. Зачем нужен running = False ?
2. Можно ли (нужно?) принудительно завершать поток?
Ещё раз спасибо огромное! Ваш ресурс пожалуй лучший по Qt и PyQt на русском (и не только) языке!

Evgenii Legotckoi
  • 11 жовтня 2021 р. 11:53

День добрый

  1. Не нужен, случайно осталось при подготовке материала
  2. Можно, нужно, не обязательно - зависит от логики вашей программы
b
  • 11 жовтня 2021 р. 13:44

А можете, пожалуйста, уточнить каким образом можно принудительно завершить поток?

Evgenii Legotckoi
  • 11 жовтня 2021 р. 13:48

Вызвать либо метод quit() либо эквивалентный его вариант - метод exit(0)

b
  • 11 жовтня 2021 р. 13:57

Спасибо большое

O
  • 16 травня 2022 р. 16:52

Не уверен, что кто-то ответил спустя столько времени, но все же. Возможно кто-то отправлять сигнал из gui во второй поток, активируя там функцию run повторно? На примере чата. На каждон отправленое сообщение из gui активировать по новой функцию Run(), в которой бекенд обработки сообщений. Просто каждый раз завершать поток и стартовать его заного - очень долго. Как использовать поток повторно, после завершения метода run?

Evgenii Legotckoi
  • 16 травня 2022 р. 17:28

Вы можете использовать переменную running, которой можете контролировать выполнение функции run

class BrowserHandler(QtCore.QObject):
    running = False
    newTextAndColor = QtCore.pyqtSignal(str, object)

    # method which will execute algorithm in another thread
    def run(self):
        while running:
            pass

Главное, это правильно обработать установку переменной running в рамках вашей программы

O
  • 16 травня 2022 р. 17:49

Сначала так и использовал, но в случае установки флага running в состояние выхода из цикла, run() завершается, поток все еще живет, но заново запустить run, обращаясь к этому методу так же, как и при старте потока, уже не могу. Возможно я как-то не так это делаю ._.

Evgenii Legotckoi
  • 16 травня 2022 р. 17:59

Да, вы правы. Я не подумал об этом.
В этом случае я бы попытался написать программу по другому.
Например, добавить в BrowserHandler очередь сообщений, а метод run не завершать, а заставить его обрабатывать сообщения каждый раз, когда что-то добавляется в очередь сообщений. Это будет наиболее правильное решение.

O
  • 16 травня 2022 р. 21:33

Решение хорошее, сейчас так и делаю. Но все равно остается открытым вопрос подвязки ивента из вне. Проще говоря, не хочется гонять вечный цикл в run, постоянго проверяя изменения очереди (пусть даже поставим QThread.msleep(100) на каждый виток цикла). А как заставить run шевелиться только по отправке сообщения,

Evgenii Legotckoi
  • 17 травня 2022 р. 13:48

Попробуйте принудительно вызывать сигнал started у потока. Это является потокобезопасным. И в данном случае вызов сигнала started должно запустить выполнения метода run, а потом продолжить выполнение главного потока.

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
Ua

Qt - Тест 001. Сигналы и слоты

  • Результат:84бали,
  • Рейтинг балів4
Ua

Qt - Тест 001. Сигналы и слоты

  • Результат:42бали,
  • Рейтинг балів-8
ОК

Qt - Тест 001. Сигналы и слоты

  • Результат:47бали,
  • Рейтинг балів-6
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 21:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 23:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 17:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 16:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 20:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
AH
Abdul Hadi13 лютого 2025 р. 15:21
Are you Looking for best painter services in Qatar? Looking for top painter Services in Qatar? Get high-quality, affordable, and professional painting for homes & offices. Contact expert painters today!
d
dubaicushions13 лютого 2025 р. 15:17
Are Looking for custom swing cushions in Dubai for home decor? Looking for Custom Swing Cushions in Dubai? Get high-quality, weather-resistant, and stylish cushions for your outdoor swing. Order now for comfort & elegance!
d
dubaicustomizedsofa13 лютого 2025 р. 15:11
Are you Looking for a custom sofa in Dubai? Looking for a Custom Sofa in Dubai ? Get high-quality, stylish, and tailor-made sofas to match your space. Order now for comfort, luxury, and perfect design!
b
blinds1211 лютого 2025 р. 16:08
Why Bamboo Blinds Are the Perfect Choice for Your Home When it comes to enhancing the aesthetics and functionality of your living space, choosing the right window treatment is crucial. Bamboo blinds have emerged as a popular choice for homeowners wh…
i
imperial313011 лютого 2025 р. 15:40
How to Select the Right Carpet for Your Bedroom Aesthetic Choosing the perfect carpet for your bedroom involves more than just picking a color or pattern you like. Carpets can transform the ambiance of your space, adding warmth, comfort, and style. How…

Слідкуйте за нами в соціальних мережах