Skip to content

Commit

Permalink
Add example that uses PySide6 with asyncio compat (#612)
Browse files Browse the repository at this point in the history
* Add Qt+asyncio example

* --amend

* add note for later

* tweak
  • Loading branch information
almarklein authored Oct 8, 2024
1 parent e99702f commit 1158594
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 24 deletions.
12 changes: 0 additions & 12 deletions examples/gui_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,3 @@ def animate():

# Enter Qt event loop (compatible with qt5/qt6)
app.exec() if hasattr(app, "exec") else app.exec_()


# For those interested, this is a simple way to integrate Qt's event
# loop with asyncio, but for real apps you probably want to use
# something like the qasync library.
# async def mainloop():
# await main_async(canvas)
# while not canvas.is_closed():
# await asyncio.sleep(0.001)
# app.flush()
# app.processEvents()
# loop.stop()
87 changes: 87 additions & 0 deletions examples/gui_qt_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
An example demonstrating a qt app with a wgpu viz inside.
This is the same as the ``gui_qt_embed.py`` example, except this uses
the asyncio compatible mode that was introduced in Pyside 6.6.
For more info see:
* https://doc.qt.io/qtforpython-6/PySide6/QtAsyncio/index.html
* https://www.qt.io/blog/introducing-qtasyncio-in-technical-preview
"""

# ruff: noqa: N802
# run_example = false

import time
import asyncio

from PySide6 import QtWidgets, QtAsyncio
from wgpu.gui.qt import WgpuWidget
from triangle import setup_drawing_sync


def async_connect(signal, async_function):
# Unfortunately, the signal.connect() methods don't detect
# coroutine functions, so we have to wrap it in a function that creates
# a Future for the coroutine (which will then run in the current event loop).
#
# The docs on QtAsyncio do something like
#
# self.button.clicked.connect(
# lambda: asyncio.ensure_future(self.whenButtonClicked()
# )
#
# But that's ugly, so we create a little convenience function
def proxy():
return asyncio.ensure_future(async_function())

signal.connect(proxy)


class ExampleWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.resize(640, 480)
self.setWindowTitle("wgpu triangle embedded in a qt app")

splitter = QtWidgets.QSplitter()

self.button = QtWidgets.QPushButton("Hello world", self)
self.canvas = WgpuWidget(splitter)
self.output = QtWidgets.QTextEdit(splitter)

# self.button.clicked.connect(self.whenButtonClicked) # see above :(
async_connect(self.button.clicked, self.whenButtonClicked)

splitter.addWidget(self.canvas)
splitter.addWidget(self.output)
splitter.setSizes([400, 300])

layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.button, 0)
layout.addWidget(splitter, 1)
self.setLayout(layout)

self.show()

def addLine(self, line):
t = self.output.toPlainText()
t += "\n" + line
self.output.setPlainText(t)

async def whenButtonClicked(self):
self.addLine("Waiting 1 sec ...")
await asyncio.sleep(1)
self.addLine(f"Clicked at {time.time():0.1f}")


app = QtWidgets.QApplication([])
example = ExampleWidget()

draw_frame = setup_drawing_sync(example.canvas)
example.canvas.request_draw(draw_frame)

# Enter Qt event loop the asyncio-compatible way
QtAsyncio.run()
33 changes: 21 additions & 12 deletions examples/gui_qt_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
"""

# ruff: noqa: N802
# run_example = false

import time
import importlib

from triangle import setup_drawing_sync

# For the sake of making this example Just Work, we try multiple QT libs
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
try:
Expand All @@ -16,11 +20,8 @@
except ModuleNotFoundError:
pass


from wgpu.gui.qt import WgpuWidget # noqa: E402

from triangle import setup_drawing_sync # noqa: E402


class ExampleWidget(QtWidgets.QWidget):
def __init__(self):
Expand All @@ -31,11 +32,14 @@ def __init__(self):
splitter = QtWidgets.QSplitter()

self.button = QtWidgets.QPushButton("Hello world", self)
self.canvas1 = WgpuWidget(splitter)
self.canvas2 = WgpuWidget(splitter)
self.canvas = WgpuWidget(splitter)
self.output = QtWidgets.QTextEdit(splitter)

self.button.clicked.connect(self.whenButtonClicked)

splitter.addWidget(self.canvas1)
splitter.addWidget(self.canvas2)
splitter.addWidget(self.canvas)
splitter.addWidget(self.output)
splitter.setSizes([400, 300])

layout = QtWidgets.QHBoxLayout()
layout.addWidget(self.button, 0)
Expand All @@ -44,15 +48,20 @@ def __init__(self):

self.show()

def addLine(self, line):
t = self.output.toPlainText()
t += "\n" + line
self.output.setPlainText(t)

def whenButtonClicked(self):
self.addLine(f"Clicked at {time.time():0.1f}")


app = QtWidgets.QApplication([])
example = ExampleWidget()

draw_frame1 = setup_drawing_sync(example.canvas1)
draw_frame2 = setup_drawing_sync(example.canvas2)

example.canvas1.request_draw(draw_frame1)
example.canvas2.request_draw(draw_frame2)
draw_frame = setup_drawing_sync(example.canvas)
example.canvas.request_draw(draw_frame)

# Enter Qt event loop (compatible with qt5/qt6)
app.exec() if hasattr(app, "exec") else app.exec_()
3 changes: 3 additions & 0 deletions wgpu/gui/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,9 @@ def run():
if already_had_app_on_import:
return # Likely in an interactive session or larger application that will start the Qt app.
app = get_app()

# todo: we could detect if asyncio is running (interactive session) and wheter we can use QtAsyncio.
# But let's wait how things look with new scheduler etc.
app.exec() if hasattr(app, "exec") else app.exec_()


Expand Down

0 comments on commit 1158594

Please sign in to comment.