Search code examples
pythonpyqtpyqt5qthread

PyQt - Main hangs on QThread


I have a Python QT application that connects to a CG server (CasparCG). The QT application triggers a QThread which listens for hotkeys using the module pynput - and sends a command to CasparCG to playback different video files for each of the keys pressed.

In the Main GUI, I can assign video files to a list of hotkeys, and I trigger the HotKey listening thread from a submenu item.

self.actionStart_Hotkeys = QtWidgets.QAction(MainWindow)
self.menuCasparCG.addAction(self.actionStart_Hotkeys)    
self.actionStart_Hotkeys.triggered.connect(self.StartHotkeys)

The Main app and Ui_Window code is very long and has no problems - it functions like it should. Videos also playback as I would expect when the HotKeys are activated by pressing keys, but the Main window of the application freezes after a few video files are played - and I'm not sure why the main GUI is not responding to input after the HotKey thread is started.

The code up until now looks like this..

from pynput import keyboard

class HotKeys(QThread):
    def __init__(self, parent):
        QThread.__init__(self, parent)
        self.COMBINATIONS = [
            {keyboard.KeyCode(char='0')},
            {keyboard.KeyCode(char='1')},
            {keyboard.KeyCode(char='2')},
            {keyboard.KeyCode(char='3')},  
            ]

        self.caspar = None
        self.current = set()
        self.Connect()
        self.Listen()

    def exit(self, i):
        if not self.caspar == None:
            self.caspar.close
        sys.exit(i)

    def Connect(self):
        try:
            self.caspar = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.caspar.connect(("127.0.0.1", 5250))
            print("Connected to caspar")

        except socket.error:
            print("CasparCG not running, or incorrect settings.xml")
            self.exit(0)

    def execute(self, k=None): # k is videofile 
        movie = bytes("PLAY 1-20 {} \r\n".format(k), 'utf8')
        self.caspar.send(movie)

    def on_press(self, key):
        if any([key in COMBO for COMBO in self.COMBINATIONS]):
             self.current.add(key)
            if any(all(k in self.current for k in COMBO) for COMBO in self.COMBINATIONS):
                self.execute(key)

    def on_release(self, key):
        if any([key in COMBO for COMBO in self.COMBINATIONS]):
            self.current.remove(key)

    def Listen(self):
        with keyboard.Listener(on_press=self.on_press, on_release=self.on_release) as listener:
            listener.join() 

I trigger this Hotkey QThread in the Main class of the application like this...

class Main(QMainWindow, Ui_MainWindow):
    def __init__(self):
        QMainWindow.__init__(self)
        self.setupUi(self) # from Ui_MainWindow class       

    def StartHotkeys(self):
        hotkey_thread = HotKeys(self)
        hotkey_thread.start()   

and the application like this...

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv) 
    gui = Main()
    gui.show()
    sys.exit(app.exec_())

So why does Main freeze?


Solution

  • QThread is not a thread, it is a thread handler, if you want to execute a task in another thread you must do it in the run() method, that method is the only part that is executed in the other thread. In your case the Listen() task is blocking and you call it in the constructor, and the QThread constructor runs in the GUI thread, that's why your GUI freezes. The solution is to move Connect and Listen to the run() method:

    class HotKeys(QThread):
        def __init__(self, parent=None):
            QThread.__init__(self, parent)
            self.COMBINATIONS = [
                {keyboard.KeyCode(char='0')},
                {keyboard.KeyCode(char='1')},
                {keyboard.KeyCode(char='2')},
                {keyboard.KeyCode(char='3')},  
                ]
    
            self.caspar = None
            self.current = set()
    
        def run(self):
            self.Connect()
            self.Listen()
    
        def exit(self, i):
            if self.caspar:
                self.caspar.close()
            sys.exit(i)
    
        def Connect(self):
            try:
                self.caspar = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.caspar.connect(("127.0.0.1", 10000))
                print("Connected to caspar")
    
            except socket.error:
                print("CasparCG not running, or incorrect settings.xml")
                self.exit(0)
    
        def execute(self, k=None): # k is videofile 
            if self.caspar:
                movie = bytes("PLAY 1-20 {} \r\n".format(k), 'utf8')
                self.caspar.send(movie)
    
        def on_press(self, key):
            if any([key in COMBO for COMBO in self.COMBINATIONS]):
                self.current.add(key)
                if any(all(k in self.current for k in COMBO) for COMBO in self.COMBINATIONS):
                    self.execute(key)
    
        def on_release(self, key):
            if any([key in COMBO for COMBO in self.COMBINATIONS]):
                self.current.remove(key)
    
        def Listen(self):
            with keyboard.Listener(on_press=self.on_press, on_release=self.on_release) as listener:
                listener.join()