Skip to content

Commit

Permalink
Merge pull request #3 from pygfx/add-examples
Browse files Browse the repository at this point in the history
Add examples
  • Loading branch information
almarklein authored Oct 25, 2024
2 parents 5f48752 + db7cc38 commit 7338f98
Show file tree
Hide file tree
Showing 11 changed files with 760 additions and 4 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ same to the code that renders to them. Yet, the GUI systems are very different
## Purpose

* Provide a generic canvas API to render to.
* Provide an event loop for scheduling events and draws.
* Provide a simple but powerful event system with standardized event objects.
* Provide various canvas implementations:
* One that is light and easily installed (glfw).
* For various GUI libraries (e.g. qt and wx), so visuzalizations can be embedded in a GUI.
* For specific platforms (e.g. Jupyter, browser).
* Provide a simple but powerful event system with standardized event objects.
* Provide an event loop for scheduling events and draws.


The main use-case is rendering with [wgpu](https://github.com/pygfx/wgpu-py),
but ``rendercanvas``can be used by anything that can render based on a window-id or
Expand All @@ -52,11 +53,11 @@ Also see the [online documentation](https://rendercanvas.readthedocs.io) and the

```py
# Select either the glfw, qt or jupyter backend
from rendercanvas.gui.auto import WgpuCanvas, loop
from rendercanvas.auto import WgpuCanvas, loop

# Visualizations can be embedded as a widget in a Qt application.
# Supported qt libs are PySide6, PyQt6, PySide2 or PyQt5.
from wgpu.gui.pyside6 import QWgpuWidget
from rendercanvas.pyside6 import QWgpuWidget


# Now specify what the canvas should do on a draw
Expand Down
21 changes: 21 additions & 0 deletions examples/gui_auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Run a wgpu example on an automatically selected backend.
"""

from rendercanvas.auto import WgpuCanvas, run

from rendercanvas.utils.cube import setup_drawing_sync


canvas = WgpuCanvas(size=(640, 480), title=f"The wgpu cube example on a {WgpuCanvas.__name__}")
draw_frame = setup_drawing_sync(canvas)


@canvas.request_draw
def animate():
draw_frame()
canvas.request_draw()


if __name__ == "__main__":
run()
18 changes: 18 additions & 0 deletions examples/gui_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
A simple example to demonstrate events.
"""

from rendercanvas.auto import WgpuCanvas, run


canvas = WgpuCanvas(size=(640, 480), title="wgpu events")


@canvas.add_event_handler("*")
def process_event(event):
if event["event_type"] != "pointer_move":
print(event)


if __name__ == "__main__":
run()
21 changes: 21 additions & 0 deletions examples/gui_glfw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Run a wgpu example on the glfw backend.
"""

from rendercanvas.glfw import WgpuCanvas, run

from rendercanvas.utils.cube import setup_drawing_sync


canvas = WgpuCanvas(size=(640, 480), title=f"The wgpu cube example on glfw")
draw_frame = setup_drawing_sync(canvas)


@canvas.request_draw
def animate():
draw_frame()
canvas.request_draw()


if __name__ == "__main__":
run()
35 changes: 35 additions & 0 deletions examples/gui_qt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""
Run a wgpu example on the Qt backend.
Works with either PySide6, PyQt6, PyQt5 or PySide2.
"""

import importlib

# The `rendercanvas.qt` module checks what Qt libs is imported, so we need to import that first.
# For the sake of making this example Just Work, we try multiple Qt libs
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
try:
QtWidgets = importlib.import_module(".QtWidgets", lib)
break
except ModuleNotFoundError:
pass


from rendercanvas.qt import WgpuCanvas, run

from rendercanvas.utils.cube import setup_drawing_sync


canvas = WgpuCanvas(size=(640, 480), title=f"The wgpu cube example on {lib}")
draw_frame = setup_drawing_sync(canvas)


@canvas.request_draw
def animate():
draw_frame()
canvas.request_draw()


if __name__ == "__main__":
run()
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

import time
import asyncio

from PySide6 import QtWidgets, QtAsyncio
from rendercanvas.qt import QWgpuWidget
from rendercanvas.utils.cube 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("Rendering in Qt with asyncio")

splitter = QtWidgets.QSplitter()

# todo: use update_mode = 'continuous' when that feature has arrived
self.button = QtWidgets.QPushButton("Hello world", self)
self.canvas = QWgpuWidget(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()
66 changes: 66 additions & 0 deletions examples/gui_qt_embed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
An example demonstrating a qt app with a wgpu viz inside.
If needed, change the PySide6 import to e.g. PyQt6, PyQt5, or PySide2.
"""

# ruff: noqa: N802, E402

import time
import importlib


# For the sake of making this example Just Work, we try multiple QT libs
for lib in ("PySide6", "PyQt6", "PySide2", "PyQt5"):
try:
QtWidgets = importlib.import_module(".QtWidgets", lib)
break
except ModuleNotFoundError:
pass

from rendercanvas.qt import QWgpuWidget
from rendercanvas.utils.cube import setup_drawing_sync


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

splitter = QtWidgets.QSplitter()

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

self.button.clicked.connect(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)

def whenButtonClicked(self):
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 (compatible with qt5/qt6)
app.exec() if hasattr(app, "exec") else app.exec_()
21 changes: 21 additions & 0 deletions examples/gui_wx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Run a wgpu example on the wx backend.
"""

from rendercanvas.wx import WgpuCanvas, run

from rendercanvas.utils.cube import setup_drawing_sync


canvas = WgpuCanvas(size=(640, 480), title=f"The wgpu cube example on wx")
draw_frame = setup_drawing_sync(canvas)


@canvas.request_draw
def animate():
draw_frame()
canvas.request_draw()


if __name__ == "__main__":
run()
42 changes: 42 additions & 0 deletions examples/gui_wx_embed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
An example demonstrating a wx app with a wgpu viz inside.
"""

import wx
from rendercanvas.wx import WgpuWidget

from rendercanvas.utils.cube import setup_drawing_sync


class Example(wx.Frame):
def __init__(self):
super().__init__(None, title="wgpu triangle embedded in a wx app")
self.SetSize(640, 480)

splitter = wx.SplitterWindow(self)

self.button = wx.Button(self, -1, "Hello world")
self.canvas1 = WgpuWidget(splitter)
self.canvas2 = WgpuWidget(splitter)

splitter.SplitVertically(self.canvas1, self.canvas2)
splitter.SetSashGravity(0.5)

sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.button, 0, wx.EXPAND)
sizer.Add(splitter, 1, wx.EXPAND)
self.SetSizer(sizer)

self.Show()


app = wx.App()
example = Example()

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)

app.MainLoop()
Empty file added rendercanvas/utils/__init__.py
Empty file.
Loading

0 comments on commit 7338f98

Please sign in to comment.