2
\$\begingroup\$

I just began learning how to use signals and slots in PyQt5, and so I made a cute (pun intended) little program to display just one button. When the button is pressed a thread is spawned which will count from 0 to 99 and display the current count onto the button. However, when the button is counting, the program will not allow the user to spawn another thread.

I am about to actually use my knowledge for a much heavier task, and I wanted to know if there was an easier way to do what I did. Or, perhaps my way is not very efficient? Thanks in advance!

import threading
import sys
import time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Application(QMainWindow):
 counter = pyqtSignal(str)
 counting = False
 def __init__(self):
 super(Application, self).__init__()
 self.button = QPushButton()
 self.button.setText('99')
 self.button.clicked.connect(self.startCounting)
 self.counter.connect(self.button.setText)
 self.layout = QVBoxLayout()
 self.layout.addWidget(self.button)
 self.frame = QFrame()
 self.frame.setLayout(self.layout)
 self.setCentralWidget(self.frame)
 def startCounting(self):
 if not self.counting:
 self.counting = True
 thread = threading.Thread(target=self.something)
 thread.start()
 def something(self):
 for x in range(100):
 self.counter.emit(str(x))
 time.sleep(.01)
 self.counting = False
if __name__ == '__main__':
 app = QApplication(sys.argv)
 window = Application()
 window.show()
 sys.exit(app.exec_())
asked Aug 17, 2016 at 22:01
\$\endgroup\$
2
  • 1
    \$\begingroup\$ I have never used PyQt, so I am asking for my own edification: why do you use a pyqtSignal instead of just setting the text directly? \$\endgroup\$ Commented Aug 18, 2016 at 0:49
  • 1
    \$\begingroup\$ @zondo because its 'good practice' to let the main thread handle anything GUI related \$\endgroup\$ Commented Aug 18, 2016 at 1:03

3 Answers 3

2
\$\begingroup\$

Looks good to me!

super minor nits:

 thread = threading.Thread(target=self.something)
 thread.start()

only uses the thread variable once, so you might as well do:

 threading.Thread(target=self.something).start()

Also, the only thing you use from threading is Thread so you might as well change your import to:

from threading import Thread

and then it can be just:

Thread(target=self.something).start()

...but again, these are super minor things! It looks good to me; I may have to look at pyQT again :)

answered Aug 21, 2016 at 6:38
\$\endgroup\$
1
\$\begingroup\$

since you already used signals/slots mechanism in your program you can easily replace python thread mechanism with QThread() and make it use separate Counter() object to divide program into separate logical blocks:

# Similar to threading.thread(target=self.counter.start)
self.counterThread = QThread()
self.counter = Counter()
self.counter.moveToThread(self.counterThread)
self.counterThread.started.connect(self.counter.start)

Where the Counter() class (often named as Worker() class) is:

class Counter(QObject):
 '''
 Class intended to be used in a separate thread to generate numbers and send
 them to another thread.
 '''
 newValue = pyqtSignal(str)
 stopped = pyqtSignal()
 def __init__(self):
 QObject.__init__(self)
 def start(self):
 '''
 Count from 0 to 99 and emit each value to the GUI thread to display.
 '''
 for x in range(100):
 self.newValue.emit(str(x))
 time.sleep(.01)
 self.stopped.emit()

By using this approach you can even modify Counter() object to receive some data from the GUI on-the-fly and react accordingly.

Another minor thing is that "from package import *" was used. Usually it is considered a bad practice since you import all the contents of the package. In this case all QtCore and QtWidgets modules were imported which is ~2/3 of the PyQt5 package itself I believe))) It is more verbose but much better to use:

from PyQt5.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, QFrame, QApplication
from PyQt5.QtCore import pyqtSignal, QObject, QThread

or:

from PyQt5 import QtWidgets, QtCore
...
self.button = QtWidgets.QPushButton()

That way you and other code readers always know which object belongs to which package as well.

Here is the complete code rewritten according to those notes:

'''
https://codereview.stackexchange.com/questions/138992/simple-pyqt5-counting-gui
'''
import sys
import time
from PyQt5.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, QFrame, QApplication
from PyQt5.QtCore import pyqtSignal, QObject, QThread
class Counter(QObject):
 '''
 Class intended to be used in a separate thread to generate numbers and send
 them to another thread.
 '''
 newValue = pyqtSignal(str)
 stopped = pyqtSignal()
 def __init__(self):
 QObject.__init__(self)
 def start(self):
 '''
 Count from 0 to 99 and emit each value to the GUI thread to display.
 '''
 for x in range(100):
 self.newValue.emit(str(x))
 time.sleep(.01)
 self.stopped.emit()
class Application(QMainWindow):
 def __init__(self):
 QMainWindow.__init__(self)
 # Configuring widgets 
 self.button = QPushButton()
 self.button.setText('99')
 self.layout = QVBoxLayout()
 self.layout.addWidget(self.button)
 self.frame = QFrame()
 self.frame.setLayout(self.layout)
 self.setCentralWidget(self.frame)
 # Configuring separate thread
 self.counterThread = QThread()
 self.counter = Counter()
 self.counter.moveToThread(self.counterThread)
 # Connecting signals
 self.button.clicked.connect(self.startCounting)
 self.counter.newValue.connect(self.button.setText)
 self.counter.stopped.connect(self.counterThread.quit)
 self.counterThread.started.connect(self.counter.start)
 def startCounting(self):
 '''
 Start counting if no other counting is done.
 '''
 if not self.counterThread.isRunning():
 self.counterThread.start()
if __name__ == '__main__':
 app = QApplication(sys.argv)
 window = Application()
 window.show()
 sys.exit(app.exec_())

Keep up the good work and best wishes.

answered Nov 27, 2018 at 20:45
\$\endgroup\$
1
\$\begingroup\$

Yesterday I spent the day researching how threading should be done in PyQt5 for one of my own projects and found that a lot of the documentation was incorrect, including official documentation.

This was the best documentation we were able to find: Here
Here is a proof of concept example I came away with and later implemented.

Basically you have a Thread class where the code to be executed is in the run function:

class Thread(QRunnable):
 def __init__(self):
 super(Thread, self).__init__() 
 self.signal = Signals() 
 @pyqtSlot()
 def run(self):
 time.sleep(5)
 result = "Some String"
 self.signal.return_signal.emit(result)

This makes a call to a Signal class that will store the signal as a subclass of PyQt5.QtCore.QObject:

class Signals(QObject):
 return_signal = pyqtSignal(str)

Then the basic Application class that creates an instance of a PyQt5.QtCore.QThreadPool in the __init__ function and creates and calls the thread in clickCheckbox which sends the return signal to function_thread:

class App(QWidget):
 def __init__(self):
 super().__init__()
 self.title='Hello, world!'
 self.left=2100
 self.top=500
 self.width=640
 self.height=480
 self.threadpool = QThreadPool()
 self.initUI()
 def initUI(self):
 self.setWindowTitle(self.title)
 self.setGeometry(self.left,self.top,self.width,self.height)
 checkbox = QCheckBox('Check Box', self)
 checkbox.stateChanged.connect(self.clickCheckbox)
 self.show()
 def clickCheckbox(self):
 thread = Thread()
 thread.signal.return_signal.connect(self.function_thread)
 self.threadpool.start(thread)
 def function_thread(self, signal):
 print(signal)

Initialising Application Window:

if __name__=='__main__':
 app=QApplication(sys.argv)
 ex=App()
 sys.exit(app.exec_())

Applying all this, here is what I would suggest you do to your code:

import sys
import time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Signal(QObject):
 signal = pyqtSignal(str)
class Thread(QRunnable):
 def __init__(self, counting):
 super(Thread, self).__init__()
 self.counting = counting
 self.signal = Signal()
 def run(self):
 for x in range(100):
 self.signal.signal.emit(str(x))
 time.sleep(.01)
 counting = False
 self.signal.signal.emit(str(counting))
class Application(QMainWindow):
 counting = False
 def __init__(self):
 super(Application, self).__init__()
 self.button = QPushButton()
 self.button.setText('99')
 self.button.clicked.connect(self.startCounting)
 self.layout = QVBoxLayout()
 self.layout.addWidget(self.button)
 self.frame = QFrame()
 self.frame.setLayout(self.layout)
 self.setCentralWidget(self.frame)
 self.threadpool = QThreadPool()
 def startCounting(self):
 thread = Thread(self.counting)
 thread.signal.signal.connect(self.something)
 self.threadpool.start(thread)
 def something(self, signal):
 if signal == "False":
 print("Counting is Complete")
 else:
 print(signal)
if __name__ == '__main__':
 app = QApplication(sys.argv)
 window = Application()
 window.show()
 sys.exit(app.exec_())
Stephen Rauch
4,31412 gold badges24 silver badges36 bronze badges
answered Jul 12, 2019 at 1:34
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.