Search code examples
qt5tornadopyqt5

Python: How to pass tornado read_message()'s result to QT5 UI element


I have a python QT5 application, which starts tornado socket client in another thread. What I want to know is, whenever I get the result in tornado's websocket.read_message(), how to pass that value to QT5 GUI label.

Here is Main.py which starts QT5 GUI application and tornado websocket client in a new thread.

import sys
import threading

from PyQt5.QtWidgets import QApplication
from app.ui.components import MainWindow

from app.client import Client
from config import SERVER_URL

def main():
    app = QApplication(sys.argv)
    form = MainWindow()
    form.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    client = Client("ws://{0}/socket".format(SERVER_URL), 5)
    thread = threading.Thread(target=lambda : client.ioloop.start())
    thread.setDaemon(True)
    thread.start()
    main()

Here is Client.py

from tornado import gen
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.websocket import websocket_connect

class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()
        self.periodic_callback = PeriodicCallback(self.keep_alive, 20000, io_loop=self.ioloop)

    @gen.coroutine
    def connect(self):
        try:
            self.ws = yield websocket_connect(self.url)
            self.periodic_callback.start()
        except Exception as e:
            pass
        else:
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            msg = yield self.ws.read_message()
            if msg is None:
                self.ws = None
                break
            print("Received: {0}".format(msg)) # HOW TO PASS msg to GUI LABEL WHENEVER IT RECEIVES A NEW VALUE

    def keep_alive(self):
        if self.ws is None:
            self.connect()
        else:
            self.ws.write_message("Hello from client")

Solution

  • The tornado thread has access to objects in memory from the original thread. So a very simple solution would be to pass QApplication or some other object from the QT app into your Client object. Then when you receive a message, you can call whatever method on the QT object.

    However, it's far from clear that this will work safely. QT objects might not be designed to be accessed from two different threads. Without knowing how a class is meant to be used, you shouldn't assume it's thread-safe. So the simple solution might mean that at the same time that some QT code is running on your main thread, the Tornado thread calls some piece of QT code which changes the state of the QT objects. The other piece of code wasn't expecting that thing to change, and so you get a crash or other failure.

    It's possible you find out that the QT library is designed to cope with this. But's there's a better way in any case - a producer-consumer queue.

    This means that you have a Queue object, which is shared between threads. This could be threading.Queue from the threading module you're already using. Every time a Websocket message is received, you put something on the queue. This could be any Python object - just the message string if it's all you need, or some custom object if you want.

    Then on another thread, you have a loop which just reads from the queue, and calls QT each time it gets a message.

    Edit - QT can probably handle this

    I don't know much about QT. However, most windowing systems actually have their own event queue, where they manage clicks, key presses, etc. If calls to QT to update something are handled on this event queue, it probably means they are thread safe. This is because the function you would call from the Tornado thread doesn't directly make changes to the GUI, rather it simply adds something to the queue to be processed when QT gets round to it.

    I had a quick look to some stuff about QT, and it seems that this probably is how QT works, and you should be find update it from the Tornado thread without implementing your own queue. See https://stackoverflow.com/a/11793684/1737957 and http://code.activestate.com/recipes/415311-qt-event-processing-python-threads/