Razero
Razero

Reputation: 341

Pass data to thread from GUI on runtime in python

I'm trying to build a little python application, where a simple webserver runs in the background, and you can the use GUI to send different messages.

I'm using PyQt5 and Python3.6, and I've managed to pass data from the working thread to the GUI, but I don't know how to do that the other way around.

Here's a skeleton of my code:

MainWindow:

class Ui_MainWindow(object):
    def __init__(self):
        super().__init__()

        self.input = True
        # 1 - create Worker and Thread inside the Form
        self.obj = WebServer.WebServer(self.input)  # no parent!
        self.thread = QtCore.QThread()  # no parent!
        # 2 - Connect Worker`s Signals to Form method slots to post data.
        self.obj.dataReady.connect(self.onDataReady)
        # 3 - Move the Worker object to the Thread object
        self.obj.moveToThread(self.thread)
        # 4 - Connect Worker Signals to the Thread slots
        self.obj.finished.connect(self.thread.quit)
        # 5 - Connect Thread started signal to Worker operational slot method
        self.thread.started.connect(self.obj.startServer)
        # 6 - Start the thread
        self.thread.start()
        # 7 - Start the form
        self.setupUi()

    def setupUi(self): 
        # Set up the GUI
        #...

        self.MainWindow.show()

    def onDataReady(self, data):
        # Data received from working thread
        # Do stuff...

    def on_click_set2(self):
        self.input = not self.input
        print(self.input)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    ui = Ui_MainWindow()
    sys.exit(app.exec_())

WebServer:

class WebServer(QObject):
    finished = pyqtSignal()
    dataReady = pyqtSignal(dict)

    def __init__(self, input):
        super().__init__()
        self.input = input

    @pyqtSlot()
    def startServer(self): # A slot takes no params
        # self.loop = asyncio.get_event_loop()
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        coro = asyncio.start_server(self.handle_update, '192.168.2.1', 8888, loop=self.loop)
        self.server = self.loop.run_until_complete(coro)

        # Serve requests until Ctrl+C is pressed
        print('Serving on {}'.format(self.server.sockets[0].getsockname()))
        try:
            self.loop.run_forever()
        except KeyboardInterrupt:
            pass

        self.finished.emit()

    async def handle_update(self, reader, writer):
        data = await reader.read(100)
        message = data.decode()
        addr = writer.get_extra_info('peername')
        print(f'Received: {message} from {addr}')

        if self.input:
            print("Input is True")
        else:
            print("Input is False")

        reply = self.input

        print(f'Send: {reply}')
        writer.write(str(reply).encode())
        await writer.drain()

        print("Close the client socket")
        writer.close()
        self.dataReady.emit(reply)

So for example I want to pass input to the thread, and if I do like above (obviously) input always stays the initial value, and won't change in the thread when I hit the button on the GUI.

How can I do it, that the value of the input is updated whenever I hit the button (so passing a value to the thread from GUI during runtime)? I assume is similar to passing from the thread to the GUI, so emitting a signal from GUI and connecting to it on the tread, but I don't know how to find a reference to the GUI from the working thread.

So any advice on how to do that? And of course any other input regarding the code or approach to the application/background server solution is welcomed! Thanks for the help in advance!

UPDATE:

Maybe it wasn't clear what my question was, so here it is:

How can I send a value from the GUI thread to the worker thread while both of them are running parallel? (If the above code makes any sense to you use that as an example, otherwise a general example would be appreciated)

Upvotes: 2

Views: 1144

Answers (1)

eyllanesc
eyllanesc

Reputation: 243897

It is not necessary that the WebServer live in another thread, it is only necessary that the eventloop be executed in another thread. In this case, the WebServer exchanges the data with the server thread through a queue. Although the QObjects are not thread-safe, the signals are so, there is no problem in emitting the signal from a thread other than the one that the QObject lives

import asyncio
import threading
import queue
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets


class WebServer(QtCore.QObject):
    dataReady = QtCore.pyqtSignal(object)

    def startServer(self):
        self.m_loop = asyncio.new_event_loop()
        self.m_queue = queue.Queue()
        asyncio.set_event_loop(self.m_loop)
        coro = asyncio.start_server(
            self.handle_update, "127.0.0.1", 10000, loop=self.m_loop
        )
        self.server = self.m_loop.run_until_complete(coro)
        print("Serving on {}".format(self.server.sockets[0].getsockname()))
        threading.Thread(target=self.m_loop.run_forever, daemon=True).start()

    @QtCore.pyqtSlot(object)
    def setData(self, data):
        if hasattr(self, "m_queue"):
            self.m_queue.put(data)

    def stop(self):
        if hasattr(self, "m_loop"):
            self.m_loop.stop()

    async def handle_update(self, reader, writer):
        reply = ""
        data = await reader.read(100)
        message = data.decode()
        addr = writer.get_extra_info("peername")
        print(f"Received: {message} from {addr}")
        if not self.m_queue.empty():
            data = self.m_queue.get(block=False)
            reply = data
        print(f"Send: {reply}")
        writer.write(str(reply).encode())
        await writer.drain()

        print("Close the client socket")
        writer.close()
        self.dataReady.emit(reply)


class Widget(QtWidgets.QWidget):
    dataChanged = QtCore.pyqtSignal(object)

    def __init__(self, parent=None):
        super().__init__(parent)

        self.m_lineedit = QtWidgets.QLineEdit()
        button = QtWidgets.QPushButton("Send", clicked=self.onClicked)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self.m_lineedit)
        lay.addWidget(button)

        self.m_web = WebServer()
        self.m_web.startServer()
        self.dataChanged.connect(self.m_web.setData)

    @QtCore.pyqtSlot()
    def onClicked(self):
        text = self.m_lineedit.text()
        self.dataChanged.emit(text)

    @QtCore.pyqtSlot(object)
    def onDataReady(self, data):
        print(data)

    def closeEvent(self, event):
        self.m_web.stop()
        super().closeEvent(event)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = Widget()
    w.show()
    sys.exit(app.exec_())

Upvotes: 4

Related Questions