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?
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()