Evgenii Legotckoi
26 ноября 2019 г. 4: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, то вы сначала соедините сигналы и слоты, а потом перенесете объект в другой поток, что разорвет связь сигнал/слот. Окно приложения перестанет работать. Или приложение может просто вылететь.

  1. import sys
  2. import time
  3.  
  4. from PyQt5 import QtCore, QtWidgets
  5. from PyQt5.QtGui import QColor
  6.  
  7.  
  8. class Ui_Form(object):
  9. def setupUi(self, Form):
  10. Form.setObjectName("Form")
  11. Form.resize(453, 408)
  12. self.verticalLayout = QtWidgets.QVBoxLayout(Form)
  13. self.verticalLayout.setObjectName("verticalLayout")
  14. self.verticalLayout_2 = QtWidgets.QVBoxLayout()
  15. self.verticalLayout_2.setObjectName("verticalLayout_2")
  16. self.textBrowser = QtWidgets.QTextBrowser(Form)
  17. self.textBrowser.setObjectName("textBrowser")
  18. self.verticalLayout_2.addWidget(self.textBrowser)
  19. self.verticalLayout.addLayout(self.verticalLayout_2)
  20. self.horizontalLayout = QtWidgets.QHBoxLayout()
  21. self.horizontalLayout.setObjectName("horizontalLayout")
  22. spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
  23. self.horizontalLayout.addItem(spacerItem)
  24. self.pushButton = QtWidgets.QPushButton(Form)
  25. self.pushButton.setObjectName("pushButton")
  26. self.horizontalLayout.addWidget(self.pushButton)
  27. spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
  28. self.horizontalLayout.addItem(spacerItem1)
  29. self.verticalLayout.addLayout(self.horizontalLayout)
  30.  
  31. self.retranslateUi(Form)
  32. QtCore.QMetaObject.connectSlotsByName(Form)
  33.  
  34. def retranslateUi(self, Form):
  35. _translate = QtCore.QCoreApplication.translate
  36. Form.setWindowTitle(_translate("Form", "Example"))
  37. self.pushButton.setText(_translate("Form", "Input"))
  38.  
  39.  
  40. # Object, which will be moved to another thread
  41. class BrowserHandler(QtCore.QObject):
  42. running = False
  43. newTextAndColor = QtCore.pyqtSignal(str, object)
  44.  
  45. # method which will execute algorithm in another thread
  46. def run(self):
  47. while True:
  48. # send signal with new text and color from aonther thread
  49. self.newTextAndColor.emit(
  50. '{} - thread 2 variant 1.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))),
  51. QColor(0, 0, 255)
  52. )
  53. QtCore.QThread.msleep(1000)
  54.  
  55. # send signal with new text and color from aonther thread
  56. self.newTextAndColor.emit(
  57. '{} - thread 2 variant 2.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))),
  58. QColor(255, 0, 0)
  59. )
  60. QtCore.QThread.msleep(1000)
  61.  
  62.  
  63. class MyWindow(QtWidgets.QWidget):
  64.  
  65. def __init__(self, parent=None):
  66. super().__init__()
  67. self.ui = Ui_Form()
  68. self.ui.setupUi(self)
  69. # use button to invoke slot with another text and color
  70. self.ui.pushButton.clicked.connect(self.addAnotherTextAndColor)
  71.  
  72. # create thread
  73. self.thread = QtCore.QThread()
  74. # create object which will be moved to another thread
  75. self.browserHandler = BrowserHandler()
  76. # move object to another thread
  77. self.browserHandler.moveToThread(self.thread)
  78. # after that, we can connect signals from this object to slot in GUI thread
  79. self.browserHandler.newTextAndColor.connect(self.addNewTextAndColor)
  80. # connect started signal to run method of object in another thread
  81. self.thread.started.connect(self.browserHandler.run)
  82. # start thread
  83. self.thread.start()
  84.  
  85. @QtCore.pyqtSlot(str, object)
  86. def addNewTextAndColor(self, string, color):
  87. self.ui.textBrowser.setTextColor(color)
  88. self.ui.textBrowser.append(string)
  89.  
  90. def addAnotherTextAndColor(self):
  91. self.ui.textBrowser.setTextColor(QColor(0, 255, 0))
  92. self.ui.textBrowser.append('{} - thread 2 variant 3.\n'.format(str(time.strftime("%Y-%m-%d-%H.%M.%S", time.localtime()))))
  93.  
  94.  
  95. if __name__ == '__main__':
  96. app = QtWidgets.QApplication(sys.argv)
  97. window = MyWindow()
  98. window.show()
  99. sys.exit(app.exec())
  100.  

Вам это нравится? Поделитесь в социальных сетях!

c
  • 27 ноября 2019 г. 19:44

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

b
  • 7 мая 2020 г. 2:27
  • (ред.)

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

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

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

Evgenii Legotckoi
  • 7 мая 2020 г. 3:18

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

b
  • 7 мая 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?

  1. import sys
  2. import time
  3. from PyQt5 import QtWidgets
  4. from PyQt5.QtWidgets import QApplication, QPushButton, QListWidget, QVBoxLayout, QLineEdit
  5. from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot, QObject
  6. from pymodbus.client.sync import ModbusTcpClient as ModbusClient
  7. from pymodbus.transaction import ModbusRtuFramer
  8.  
  9. class RequestHandler(QObject):
  10. running = False
  11. output_signal = pyqtSignal(list)
  12.  
  13. @pyqtSlot(str, int)
  14. def run(self, ip, port, unit):
  15. client = ModbusClient(ip, port=port, framer=ModbusRtuFramer)
  16. client.connect()
  17. print("run")
  18. while True:
  19. print(int(QThread.currentThreadId()))
  20.  
  21. for i in range(2):
  22. try:
  23. m = client.read_holding_registers(0,9, unit = unit[i])
  24. self.output_signal.emit(m.registers)
  25. except:
  26. self.output_signal.emit(["ERROR"])
  27.  
  28. QThread.msleep(2000)
  29.  
  30. class MyWindow(QtWidgets.QWidget):
  31.  
  32. def __init__(self, parent=None):
  33. super().__init__()
  34. self.list1 = QLineEdit()
  35. self.buton = QPushButton("BASlAT")
  36. self.v = QVBoxLayout()
  37. self.v.addWidget(self.list1)
  38. self.v.addWidget(self.buton)
  39. self.buton.clicked.connect(self.start)
  40. self.setLayout(self.v)
  41. self.show()
  42.  
  43. @pyqtSlot(list)
  44. def addNewTextAndColor(self, output):
  45. print(output)
  46.  
  47. pass
  48.  
  49. def start(self):
  50. unit = [14, 15]
  51. self.thread = QThread()
  52. self.browserHandler = RequestHandler()
  53. self.browserHandler.moveToThread(self.thread)
  54. print(int(self.thread.currentThreadId()))
  55. self.browserHandler.output_signal.connect(self.addNewTextAndColor)
  56. self.thread.started.connect(self.browserHandler.run("192.168.1.100", 502, unit))
  57. self.thread.start()
  58.  
  59.  
  60. app = QtWidgets.QApplication(sys.argv)
  61. window = MyWindow()
  62.  
  63. sys.exit(app.exec())
  64.  
c
  • 22 августа 2021 г. 2: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

  1. class BrowserHandler(QtCore.QObject):
  2. running = False
  3. newTextAndColor = QtCore.pyqtSignal(str, object)
  4.  
  5. # method which will execute algorithm in another thread
  6. def run(self):
  7. while running:
  8. 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, а потом продолжить выполнение главного потока.

Комментарии

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