6
\$\begingroup\$

I wanted to make a simple example of multithreading with QThread in PyQt5 / Python3. The script generates a set of QLineEdits and buttons to start and stop a set of threads. Each thread simply increments whatever integer was in the box before the start button is pressed, once per second. Signals and slots are used between the counting threads and the main GUI thread for thread safety. The number of boxes and button sets is assigned in "n" during class declaration.

It seems clunky to make a bunch of threads beforehand. There is some talk of QThreadPool and QRunnables, but it is my understanding that QRunnables cannot use signals and slots, so what can I do there?

import time
import sys
from PyQt5 import QtCore, QtWidgets

class countingWorker(QtCore.QObject):
    #explicit signal sent in the startCounting function
    set_box_signal = QtCore.pyqtSignal(int,str)

    #explicit slot that takes input from the start_counting_signal
    @QtCore.pyqtSlot(int,str)
    def startCounting(self,boxNumber,boxText):
        value = int(boxText)
        while True:
            value=1+value
            self.set_box_signal.emit(boxNumber,str(value))
            time.sleep(1)

class firstGui(QtWidgets.QDialog):
    #explicit signals from the start_counting_emitter function
    n = 7 # make this many counters
    start_counting_signal=QtCore.pyqtSignal(int, str)

    def __init__(self):
        super(firstGui, self).__init__()
        self.workers=[]
        self.threads=[]
        for j in range(self.n):
            self.workers.append(countingWorker())
            self.threads.append(QtCore.QThread())
            self.workers[j].moveToThread(self.threads[j])
        self.init_ui()

    def init_ui(self):
        self.boxes=[]
        self.starts=[]
        self.stops=[]
        for j in range(self.n):
            # Creating and naming the boxes
            self.boxes.append(QtWidgets.QLineEdit(self))
            self.boxes[j].setText('0')
            self.boxes[j].setObjectName('box'+str(j))
            # creating the start buttons
            self.starts.append(QtWidgets.QPushButton('Start', self))
            self.starts[j].setObjectName('start' + str(j))
            self.stops.append(QtWidgets.QPushButton('Stop', self))
            self.stops[j].setObjectName('stop' + str(j))

        # Creating a Horizontal Layout to add all the widgets
        self.hboxLayout = QtWidgets.QHBoxLayout(self)

        # Adding the widgets
        for j in range(self.n):
            self.hboxLayout.addWidget(self.boxes[j])
            self.hboxLayout.addWidget(self.starts[j])
            self.hboxLayout.addWidget(self.stops[j])

        # Setting the hBoxLayout as the main layout
        self.setLayout(self.hboxLayout)
        self.setWindowTitle('gui_4b')

        # making all of the connections
        for j in range(self.n):
            self.starts[j].clicked.connect(self.start_counting_emitter)
            self.stops[j].clicked.connect(self.stop_counting)
            self.workers[j].set_box_signal.connect(self.set_box_text)

        # show the GUI
        self.show()

    # this is an implicit slot for the startbuttons to connect to
    def start_counting_emitter(self):
        start_button_name=self.sender().objectName()
        start_number=int(start_button_name[5:])
        self.start_counting_signal.connect(self.workers[start_number].startCounting)
        if not self.threads[start_number].isRunning():
           self.threads[start_number].start()
           print('thread '+str(start_number)+' started')
           self.start_counting_signal.emit(start_number,self.boxes[start_number].text())
        self.start_counting_signal.disconnect(self.workers[start_number].startCounting)

    # implicit slot of the stop buttons
    def stop_counting(self):
        stop_button_name = self.sender().objectName()
        stop_number = int(stop_button_name[4:])
        if self.threads[stop_number].isRunning():
            self.threads[stop_number].terminate()
            print('thread '+str(stop_number)+' terminated')

    # explicit slot that takes input from the countingWorker and sets the text of the provided box
    @QtCore.pyqtSlot(int,str)
    def set_box_text(self,boxNumber,boxText):
        self.boxes[boxNumber].setText(boxText)

def main():
    app = QtWidgets.QApplication(sys.argv)
    a = firstGui()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
\$\endgroup\$
2
  • \$\begingroup\$ It turns out QRunnables actually can use signals and slots. Here is a very good reference: martinfitzpatrick.name/article/… \$\endgroup\$ – SDiv Sep 5 '17 at 13:24
  • \$\begingroup\$ So personally I'm dealing with the same thing and this is one of the best examples I have come across so far! \$\endgroup\$ – RMann Jul 14 '19 at 17:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.