Evgenii Legotckoi
Evgenii Legotckoi25 ноября 2019 г. 17:53

PyQt5 - Урок 009. Использование QThread с применением moveToThread

Содержание

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

В этом примере выполняется некий алгоритм, который через сигнал возвращает текст, а также цвет текста в основной графический интерфейс. Эти данные добавляются в QTextBrowser с настройкой цвета.

Программа будет выглядеть следующим образом


Вступление

Существует два основных подхода к использованию QThread в Qt :

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

Первый метод рекомендуется использовать только в том случае, если вам действительно нужно переопределить класс потока, чтобы создать специальный функционал в классе потока. Если вам нужно выполнить какой-то код в другом потоке, то для этого нужно создать отдельный класс, который будет передан в другой поток с помощью метода moveToThread .

Также сразу хочу отметить, что нельзя передавать объекты GUI в другие потоки. Программы Qt имеют два типа потоков:

  • Основной поток. Графический поток
  • Рабочие процессы. Рабочие потоки

Все объекты графического интерфейса должны создаваться и работать только в потоке графического интерфейса, тогда как различные другие алгоритмы могут выполняться в рабочих потоках.

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

Как уже упоминалось, каждая программа имеет один поток при запуске. Этот поток называется «основной поток» (также известный как «поток GUI» в приложениях Qt). Графический интерфейс Qt должен работать в этом потоке. Все виджеты и несколько связанных с ними классов, например QPixmap, не работают во вторичных потоках. Вторичный поток обычно называют «рабочим потоком», поскольку он используется для выгрузки обработки из основного потока.

То есть если, даже если у вас что-то и сработает в другом потоке, то это будет лишь случайность, которая рано или поздно даст о себе знать. И ваша программа перестанет работать. Никогда не передавайте другие объекты 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"))


# Object, which will be moved to another thread
class BrowserHandler(QtCore.QObject):
    running = False
    newTextAndColor = QtCore.pyqtSignal(str, object)

    # method which will execute algorithm in another thread
    def run(self):
        while True:
            # send signal with new text and color from aonther thread
            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)

            # send signal with new text and color from aonther thread
            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)
        # use button to invoke slot with another text and color
        self.ui.pushButton.clicked.connect(self.addAnotherTextAndColor)

        # create thread
        self.thread = QtCore.QThread()
        # create object which will be moved to another thread
        self.browserHandler = BrowserHandler()
        # move object to another thread
        self.browserHandler.moveToThread(self.thread)
        # after that, we can connect signals from this object to slot in GUI thread
        self.browserHandler.newTextAndColor.connect(self.addNewTextAndColor)
        # connect started signal to run method of object in another thread
        self.thread.started.connect(self.browserHandler.run)
        # start thread
        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 г. 8:44

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

b
  • 6 мая 2020 г. 16:27
  • (ред.)

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

b
  • 6 мая 2020 г. 17:16

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

Evgenii Legotckoi
  • 6 мая 2020 г. 17:18

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

b
  • 7 мая 2020 г. 3:40

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

YA
  • 16 апреля 2021 г. 7: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
  • 21 августа 2021 г. 16:38

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

Evgenii Legotckoi
  • 11 октября 2021 г. 1:53

День добрый

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

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

Evgenii Legotckoi
  • 11 октября 2021 г. 3:48

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

b
  • 11 октября 2021 г. 3:57

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

O
  • 16 мая 2022 г. 6:52

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

Evgenii Legotckoi
  • 16 мая 2022 г. 7: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 г. 7:49

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

Evgenii Legotckoi
  • 16 мая 2022 г. 7:59

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

O
  • 16 мая 2022 г. 11:33

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

Evgenii Legotckoi
  • 17 мая 2022 г. 3:48

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
AD

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 8:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 8:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 19:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 октября 2024 г. 21:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 15:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 14:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 18:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 15:17
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 22:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 14:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 10:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

Следите за нами в социальных сетях