qt-async-threads
allows Qt applications to use convenient async/await
syntax to run
computational intensive or IO operations in threads, selectively changing the code slightly
to provide a more responsive UI.
The objective of this library is to provide a simple and convenient way to improve
UI responsiveness in existing Qt applications, by using async/await
, without
having to do large scale refactorings to do so.
The widget below downloads pictures of cats when the user clicks on a button (some parts omitted for brevity):
class CatsWidget(QWidget):
def __init__(self, parent: QWidget) -> None:
...
self.download_button.clicked.connect(self._on_download_button_clicked)
def _on_download_button_clicked(self, checked: bool = False) -> None:
self.progress_label.setText("Searching...")
api_url = "https://api.thecatapi.com/v1/images/search"
for i in range(10):
try:
# Search.
search_response = requests.get(api_url)
self.progress_label.setText("Found, downloading...")
# Download.
url = search_response.json()[0]["url"]
download_response = requests.get(url)
except ConnectionError as e:
QMessageBox.critical(self, "Error", f"Error: {e}")
return
self._save_image_file(download_response)
self.progress_label.setText(f"Done downloading image {i}.")
self.progress_label.setText(f"Done, {downloaded_count} cats downloaded")
This works well, but while the pictures are being downloaded the UI will freeze a bit, becoming unresponsive.
With qt-async-threads
, we can easily change the code to:
class CatsWidget(QWidget):
def __init__(self, runner: QtAsyncRunner, parent: QWidget) -> None:
...
# QtAsyncRunner allows us to submit code to threads, and
# provide a way to connect async functions to Qt slots.
self.runner = runner
# `to_sync` returns a slot that Qt's signals can call, but will
# allow it to asynchronously run code in threads.
self.download_button.clicked.connect(
self.runner.to_sync(self._on_download_button_clicked)
)
async def _on_download_button_clicked(self, checked: bool = False) -> None:
self.progress_label.setText("Searching...")
api_url = "https://api.thecatapi.com/v1/images/search"
for i in range(10):
try:
# Search.
# `self.runner.run` calls requests.get() in a thread,
# but without blocking the main event loop.
search_response = await self.runner.run(requests.get, api_url)
self.progress_label.setText("Found, downloading...")
# Download.
url = search_response.json()[0]["url"]
download_response = await self.runner.run(requests.get, url)
except ConnectionError as e:
QMessageBox.critical(self, "Error", f"Error: {e}")
return
self._save_image_file(download_response)
self.progress_label.setText(f"Done downloading image {i}.")
self.progress_label.setText(f"Done, {downloaded_count} cats downloaded")
By using a QtAsyncRunner instance and changing the slot to an async
function, the runner.run
calls
will run the requests in a thread, without blocking the Qt event loop, making the UI snappy and responsive.
Thanks to the async
/await
syntax, we can keep the entire flow in the same function as before,
including handling exceptions naturally.
We could rewrite the first example using a ThreadPoolExecutor or QThreads, but that would require a significant rewrite of the flow of the code if we don't want to block the Qt event loop.
For full documentation, please see https://qt-async-threads.readthedocs.io/en/latest.
There are excellent libraries that allow to use async frameworks with Qt:
Those libraries fully integrate with their respective frameworks, allowing the application to asynchronously communicate with sockets, threads, file system, tasks, cancellation systems, use other async libraries (such as httpx), etc.
They are very powerful in their own right, however they have one downside in that they require your main
entry point to also be async
, which might be hard to accommodate in an existing application.
qt-async-threads
, on the other hand, focuses only on one feature: allow the user to leverage async
/await
syntax to handle threads more naturally, without the need for major refactorings in existing applications.
Distributed under the terms of the MIT license.