Evgenii Legotckoi
Nov. 26, 2019, 4:53 a.m.

PyQt5 - Tutorial 009. Using QThread with MoveToThread

Content

Based on one of the questions on the forum, I wrote an example on using QThread in PyQt5, as well as using the moveToThread method to move the class object of the inherited QObject to another thread.

In this example, a certain algorithm is executed, which returns the text through the signal, as well as the color of the text to the main GUI. This data is added to the QTextBrowser with a color setting.

The program will look as follows


Introduction

There are two main approaches for using QThread in Qt :

  1. Create a new class that inherits from QThread and override the run method
  2. Create a new class that inherits from QObject , write a run method that will execute some code, and transfer the instance of this class to another thread using the moveToThread method

The first method is recommended to be used only if you really need to override the stream class in order to create special functionality in the stream class. If you need to execute some code in another thread, then for this you need to create a separate class that will be transferred to another thread using the moveToThread method.

Also, I want to note right away that you cannot transfer GUI objects to other threads. Qt programs have two kinds of threads:

  • Main stream. GUI thread
  • Workflows. Worker threads

All GUI objects should create and work only in a GUI thread, while various other algorithms can be executed in worker threads.

I quote the documentation

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.

That is, if, even if something works for you in another thread, it will be only an accident that will sooner or later make itself felt. And your program will stop working. Never pass other GUI objects to other threads.

Program

Now consider the code of our program. Please note that the algorithm of actions will be as follows

  1. We write a class that inherits from QObject and has a run method for executing code in another thread
  2. In the window constructor, create a stream object
  3. In the window constructor, create an object that will be transferred to another thread
  4. Transfer the object to another stream
  5. We connect signals and slots
  6. Run the thread

It is advisable to perform all the initialization of the object and stream in this sequence, if you do not already have sufficient experience.
However, then you would not read this article.
If you swap steps 4 and 5, then you first connect the signals and slots, and then transfer the object to another stream, which will break the signal / slot connection. The application window will stop working. Or the application may just crash.

  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.  

Do you like it? Share on social networks!

c
  • Nov. 27, 2019, 7:44 p.m.

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

b
  • May 7, 2020, 2:27 a.m.
  • (edited)

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

b
  • May 7, 2020, 3:16 a.m.

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

Evgenii Legotckoi
  • May 7, 2020, 3:18 a.m.

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

b
  • May 7, 2020, 1:40 p.m.

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

YA
  • April 16, 2021, 5:25 p.m.

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
  • Aug. 22, 2021, 2:38 a.m.

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

Evgenii Legotckoi
  • Oct. 11, 2021, 11:53 a.m.

День добрый

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

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

Evgenii Legotckoi
  • Oct. 11, 2021, 1:48 p.m.

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

b
  • Oct. 11, 2021, 1:57 p.m.

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

O
  • May 16, 2022, 4:52 p.m.

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

Evgenii Legotckoi
  • May 16, 2022, 5:28 p.m.

Вы можете использовать переменную 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
  • May 16, 2022, 5:49 p.m.

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

Evgenii Legotckoi
  • May 16, 2022, 5:59 p.m.

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

O
  • May 16, 2022, 9:33 p.m.

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

Evgenii Legotckoi
  • May 17, 2022, 1:48 p.m.

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

Comments

Only authorized users can post comments.
Please, Log in or Sign up
  • Last comments
  • AK
    April 1, 2025, 11:41 a.m.
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    March 9, 2025, 9:02 p.m.
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    March 9, 2025, 4:14 p.m.
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
  • ИМ
    Nov. 22, 2024, 9:51 p.m.
    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
  • Evgenii Legotckoi
    Oct. 31, 2024, 11:37 p.m.
    Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup