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_())
3 Answers 3
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 :)
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.
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_())
pyqtSignal
instead of just setting the text directly? \$\endgroup\$