Search code examples
pythonpygamepyqt5python-multithreadingmidi

How to click pushbutton twice while function is still running?


I have something like this : self.pushButton.clicked.connect(self.clicked)

I click my button and it calls the function clicked(), all good so far.

The Clicked() function plays a midi note and then does time.sleep() however instead of actually using time.sleep() i call another funciton loop() that does the time.sleep for me:

    def loop(self):
    print('we in the loop')
    global a
    for x in range(10):

        if a == 4:
            print('break')
            break

        else:
            print('0.1 second sleep')
            time.sleep(0.1)
                

This will do a 1 second time.sleep unless a == 4.clicked() will then turn off the midi note after the loop is finished.

Now i want it so if i click that same pushButton again while the loop is running then a = 4 and the loop will be broken and the original note will stop before 1 second has passed. e.g. if i click pushbutton while on the 5th cycle of the loop it will break, go back to clicked() and turn the note off early.

I want to be able to break this loop as soon as i have clicked the button for the second time.(the same button)

The problem is that while this loop is running it refuses to accept the second button press. Once the whole of Clicked() is finished running it will then que that second click, but i dont want this. I dont want to wait for Clicked to finish. I want to click the button while clicked() is running and have that second button press make a = 4 so that the loop will stop.

I have tried doing this with threading but it doesn't seem to make a difference, the second click of the button will not register until clicked() is finished.

What i want to see:

I click the button and it plays a midi note for 1 second. If i press that same button again before 1 second is up then it cuts the note early and starts the note again.

Basically i want to change the length of the loop if i click the button again.

Here is my actual code:

from PyQt5 import QtCore, QtGui, QtWidgets
import pygame
import pygame.midi
import time
import threading

pygame.midi.init()
player = pygame.midi.Output(0)
player.set_instrument(1)

a = 0

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(998, 759)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(430, 80, 121, 71))
        self.pushButton.setObjectName("pushButton")

        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 998, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        'THIS IS THE BUTTON'
        self.pushButton.clicked.connect(self.clicked)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "C"))

    'during each loop of loop() check to see if a == 4 and if it does then break'
    def loop(self):
        print('we in the loop')
        global a
        for x in range(10):

            if a == 4:
                print('break')
                break

            else:
                print('0.1 second sleep')
                time.sleep(0.1)



    def clicked(self):
        print('clicked')
        global player
        global a
        a = a + 2
        player.note_on(60, 127)
        self.loop()
        player.note_off(60, 127)
        










if __name__ == "__main__":
    import sys


    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()





    sys.exit(app.exec_())

I assume the answer has something to do with threading but when i tried this it made no difference because i had one thread in loop() and one thread in clicked() but this made no difference as the second click will never be recognised as long as clicked() is running because clicked() is the function the button is actually calling. I made clicked() do a = a + 2 so that on the second click i know that a == 4 and wanted to use that to end the loop.


Solution

  • Your code has several problems:

    • Do not use time.sleep in an eventloop even in small intervals since they will still block the eventloop and generate for example: GUI freeze, the slots associated with the signals are not invoked, etc.

    • Do not use global variables as they are difficult to debug.

    In this case it is better to use a QTimer and implement the logic in a controller class, on the other hand do not modify the code generated by pyuic so in this case you will have to restore that code and save it in a ui.py file.

    from functools import cached_property
    from dataclasses import dataclass
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    import pygame.midi
    
    from ui import Ui_MainWindow
    
    
    @dataclass
    class MidiController(QtCore.QObject):
        note: int = 60
        velocity: int = 127
        channel: int = 0
    
        @cached_property
        def timer(self):
            timer = QtCore.QTimer(singleShot=True)
            timer.timeout.connect(self.off)
            return timer
    
        @cached_property
        def player(self):
            pygame.midi.init()
            player = pygame.midi.Output(0)
            player.set_instrument(1)
            return player
    
        def start(self, dt=1 * 1000):
            self.timer.setInterval(dt)
            self.timer.start()
            self.on()
    
        @property
        def running(self):
            return self.timer.isActive()
    
        def stop(self):
            self.timer.stop()
            self.off()
    
        def on(self):
            self.player.note_on(self.note, self.velocity, self.channel)
    
        def off(self):
            self.player.note_off(self.note, self.velocity, self.channel)
    
    
    class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setupUi(self)
    
            self.pushButton.clicked.connect(self.handle_clicked)
    
        @cached_property
        def midi_controller(self):
            return MidiController()
    
        def handle_clicked(self):
            # update parameters
            self.midi_controller.note = 60
            # self.midi_controller.velocity = 127
    
            if self.midi_controller.running:
                self.midi_controller.stop()
            else:
                self.midi_controller.start()
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())