- 1. Вступление
- 2. Программа
На основе одного из вопросов на форуме я написал пример использования QThread в PyQt5, а также использования метода moveToThread для перемещения объекта класса унаследованного QObject в другой поток.
В этом примере выполняется некий алгоритм, который через сигнал возвращает текст, а также цвет текста в основной графический интерфейс. Эти данные добавляются в QTextBrowser с настройкой цвета.
Программа будет выглядеть следующим образом
Вступление
Существует два основных подхода к использованию QThread в Qt :
- Создайте новый класс, наследуемый от QThread , и переопределите метод запуска.
- Создайте новый класс, наследуемый от QObject , напишите метод run, который будет выполнять некоторый код, и передайте экземпляр этого класса в другой поток с помощью метода moveToThread .
Первый метод рекомендуется использовать только в том случае, если вам действительно нужно переопределить класс потока, чтобы создать специальный функционал в классе потока. Если вам нужно выполнить какой-то код в другом потоке, то для этого нужно создать отдельный класс, который будет передан в другой поток с помощью метода moveToThread .
Также сразу хочу отметить, что нельзя передавать объекты GUI в другие потоки. Программы Qt имеют два типа потоков:
- Основной поток. Графический поток
- Рабочие процессы. Рабочие потоки
Все объекты графического интерфейса должны создаваться и работать только в потоке графического интерфейса, тогда как различные другие алгоритмы могут выполняться в рабочих потоках.
Как уже упоминалось, каждая программа имеет один поток при запуске. Этот поток называется «основной поток» (также известный как «поток GUI» в приложениях Qt). Графический интерфейс Qt должен работать в этом потоке. Все виджеты и несколько связанных с ними классов, например QPixmap, не работают во вторичных потоках. Вторичный поток обычно называют «рабочим потоком», поскольку он используется для выгрузки обработки из основного потока.
То есть если, даже если у вас что-то и сработает в другом потоке, то это будет лишь случайность, которая рано или поздно даст о себе знать. И ваша программа перестанет работать. Никогда не передавайте другие объекты GUI другим потокам.
Программа
Теперь рассмотрим код нашей программы. Обратите внимание, что алгоритм действий будет следующим
- Пишем класс, который наследуется от QObject и имеет метод run для выполнения кода в другом потоке
- В конструкторе окна создайте объект потока
- В конструкторе окна создать объект, который будет передан другому потоку
- Перенести объект в другой поток
- Соединяем сигналы и слоты
- Запустите нить
Всю инициализацию объекта и потока желательно выполнять в такой последовательности, если у вас еще нет достаточного опыта.
Однако тогда вы бы не читали эту статью.
Если поменять местами шаги 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())
Огромное спасибо!
простите за беспокойсто. Разобрался )) Спасибо Вам огромное. По сути у Вас тольько и разобрался с сигналами и слотами
в продолжение, хотелось бы уточнить такой вопрос. Испускаемые сигналы - они глобальны? то есть на сгенерированный pyqtSignal в классе А, можно ли "подписаться" в классах B,C,D своими слотами? То есть по одному сигналу, может ли каждый класс выполнять что-то свое?
Да, можно. к одному сигналу можно быть подключено какое угодно количество слотов в каком угодно наборе объектов.
спасибо Вам большое
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?
Здравствуйте.
Разрешите пару вопросов...
1. Зачем нужен running = False ?
2. Можно ли (нужно?) принудительно завершать поток?
Ещё раз спасибо огромное! Ваш ресурс пожалуй лучший по Qt и PyQt на русском (и не только) языке!
День добрый
А можете, пожалуйста, уточнить каким образом можно принудительно завершить поток?
Вызвать либо метод quit() либо эквивалентный его вариант - метод exit(0)
Спасибо большое
Не уверен, что кто-то ответил спустя столько времени, но все же. Возможно кто-то отправлять сигнал из gui во второй поток, активируя там функцию run повторно? На примере чата. На каждон отправленое сообщение из gui активировать по новой функцию Run(), в которой бекенд обработки сообщений. Просто каждый раз завершать поток и стартовать его заного - очень долго. Как использовать поток повторно, после завершения метода run?
Вы можете использовать переменную running, которой можете контролировать выполнение функции run
Главное, это правильно обработать установку переменной running в рамках вашей программы
Сначала так и использовал, но в случае установки флага running в состояние выхода из цикла, run() завершается, поток все еще живет, но заново запустить run, обращаясь к этому методу так же, как и при старте потока, уже не могу. Возможно я как-то не так это делаю ._.
Да, вы правы. Я не подумал об этом.
В этом случае я бы попытался написать программу по другому.
Например, добавить в BrowserHandler очередь сообщений, а метод run не завершать, а заставить его обрабатывать сообщения каждый раз, когда что-то добавляется в очередь сообщений. Это будет наиболее правильное решение.
Решение хорошее, сейчас так и делаю. Но все равно остается открытым вопрос подвязки ивента из вне. Проще говоря, не хочется гонять вечный цикл в run, постоянго проверяя изменения очереди (пусть даже поставим QThread.msleep(100) на каждый виток цикла). А как заставить run шевелиться только по отправке сообщения,
Попробуйте принудительно вызывать сигнал started у потока. Это является потокобезопасным. И в данном случае вызов сигнала started должно запустить выполнения метода run, а потом продолжить выполнение главного потока.