I am trying to create GUI with pyqt5 for tcp communication. I have 2 different .py file one for GUI and one for TCP. tcp.py contains tcpClients and tcpServer classes which has a function called tcp_server_connect (can be found below) to create connection between client and server. GUI has a button called connect and i want to call the function tcp_server_connect when it is pressed. When I press it GUI freezes and not respond. There is no error. I think it is because of server.serve_forever() loop but I am not sure how to solve this problem.
async def tcp_server_connect():
server = await asyncio.start_server(
handle_client, '127.0.0.1', 8888)
async with server:
await server.serve_forever()
The issue is that Qt runs on the main thread, and trying to run an asyncio
event loop on the same thread won't work. Both Qt and the event loop want to and need to control the entire thread. As a note, Qt did recently announce an asyncio
module for Python, but that is still in technical preview.
The below example, which uses PySide6 (as I have that already installed), has a Qt window with buttons to start a TCP server and then a string entry control and a button to send the message to the TCP server. There is an asyncio
event loop running on a separate thread that listens to messages and then performs the required actions. Try it out. All you need to install is PySide6.
# Core dependencies
import asyncio
import sys
from threading import Thread
# Package dependencies
from PySide6.QtWidgets import (
QApplication,
QWidget,
QPushButton,
QVBoxLayout,
QLineEdit,
)
class MainWindow(QWidget):
def __init__(self) -> None:
super().__init__()
# The the `asyncio` queue and event loop are created here, in the GUI thread (main thread),
# but they will be passed into a new thread that will actually run the event loop.
# Under no circumstances should the `asyncio.Queue` be used outside of that event loop. It
# is only okay to construct it outside of the event loop.
self._async_queue = asyncio.Queue()
self._asyncio_event_loop = asyncio.new_event_loop()
self.initialize()
def initialize(self) -> None:
"""Initialize the GUI widgets"""
self.setWindowTitle("PySide with asyncio server and client")
# Create layout
main_layout = QVBoxLayout()
self.setLayout(main_layout)
button_start_server = QPushButton(text="Start server")
line_edit_message = QLineEdit()
button_send_message = QPushButton(text="Send message")
main_layout.addWidget(button_start_server)
main_layout.addWidget(line_edit_message)
main_layout.addWidget(button_send_message)
button_start_server.pressed.connect(
lambda: self.send_message_to_event_loop("start_server")
)
button_send_message.pressed.connect(
lambda: self.send_message_to_event_loop(line_edit_message.text())
)
# Disable the user being able to resize the window by setting a fixed size
self.setFixedWidth(500)
self.setFixedHeight(200)
# Show the window
self.show()
def send_message_to_event_loop(self, message: str) -> None:
"""Send the `asyncio` event loop's queue a message by using the coroutine
`put` and sending it to run on the `asyncio` event loop, putting the message
on the queue inside the event loop. This must be done because `asyncio.Queue`
is not threadsafe.
"""
asyncio.run_coroutine_threadsafe(
coro=self._async_queue.put(message),
loop=self._asyncio_event_loop,
)
async def handle_client(
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info("peername")
print(f"Received {message!r} from {addr!r}")
writer.close()
await writer.wait_closed()
async def run_server():
server = await asyncio.start_server(handle_client, "127.0.0.1", 8888)
async with server:
print("Started server")
await server.serve_forever()
async def send_message_to_server(message: str) -> None:
# Lazily create a new connection every time just for demonstration
# purposes
_, writer = await asyncio.open_connection("127.0.0.1", 8888)
writer.write(message.encode())
await writer.drain()
writer.close()
await writer.wait_closed()
async def read_messages(queue: asyncio.Queue) -> None:
server_task = None
while True:
message = await queue.get()
match message:
case "start_server":
server_task = asyncio.create_task(run_server())
case msg:
await send_message_to_server(msg)
async def async_main(queue: asyncio.Queue):
# Launch the tasks to sit around and listen to messages
await asyncio.gather(read_messages(queue))
def start_asyncio_event_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Starts the given `asyncio` loop on whatever the current thread is"""
asyncio.set_event_loop(loop)
loop.set_debug(enabled=True)
loop.run_forever()
def run_event_loop(queue: asyncio.Queue, loop: asyncio.AbstractEventLoop) -> None:
"""Runs the given `asyncio` loop on a separate thread, passing the queue to the
event loop for any other thread to send messages to the event loop. The main
coroutine that is launched on the event loop is `async_main`.
"""
thread = Thread(target=start_asyncio_event_loop, args=(loop,), daemon=True)
thread.start()
asyncio.run_coroutine_threadsafe(async_main(queue), loop=loop)
def run_application(application: QApplication):
application.exec()
if __name__ == "__main__":
application = QApplication(sys.argv)
window = MainWindow()
async_queue = window._async_queue
asyncio_event_loop = window._asyncio_event_loop
run_event_loop(queue=async_queue, loop=asyncio_event_loop)
sys.exit(run_application(application))