- 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, а потом продолжить выполнение главного потока.