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_())
\$\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\$
    – zondo
    Aug 18, 2016 at 0:49
  • 1
    \$\begingroup\$ @zondo because its 'good practice' to let the main thread handle anything GUI related \$\endgroup\$ 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 :)

\$\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.

\$\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_())
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.