На основі одного з питань на форумі я написав приклад по використанню QThread в PyQt5, а також використання методу moveToThread для переміщення об'єкта класу наследованного QObject в інший потік.
В даному прикладі проводиться виконання якогось алгоритму, які через сигнал повертає текст, а також колір текст в головний GUI. Ці дані додаються в QTextBrowser з установкою кольору.
Програма буде виглядати наступним чином
Вступ
Існують два основні підходи для використання QThread в Qt :
- Створити новий клас, який успадкований від QThread і перевизначити метод run
- Створити новий клас, який успадкований від 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 об'єкти в інші потоки.
Програма
А тепер розглянемо код нашої програми. Зверніть увагу, що алгоритм дій буде наступний
- Пишемо клас, який успадкований від 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")) # Объект, который будет перенесён в другой поток для выполнения кода 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())
Огромное спасибо!
простите за беспокойсто. Разобрался )) Спасибо Вам огромное. По сути у Вас тольько и разобрался с сигналами и слотами
в продолжение, хотелось бы уточнить такой вопрос. Испускаемые сигналы - они глобальны? то есть на сгенерированный 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, а потом продолжить выполнение главного потока.