Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example that uses PySide6 with asyncio compat #612

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Korijn marked this conversation as resolved.
Show resolved Hide resolved

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