Skip to content

Commit

Permalink
Add backends for all qt libs (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein authored Nov 13, 2024
1 parent fd17d15 commit 7ba504c
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 22 deletions.
24 changes: 20 additions & 4 deletions docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,17 @@ but you can replace ``from rendercanvas.auto`` with ``from rendercanvas.glfw`` t
Support for Qt
--------------

RenderCanvas has support for PyQt5, PyQt6, PySide2 and PySide6. It detects what
qt library you are using by looking what module has been imported.
RenderCanvas has support for PyQt5, PyQt6, PySide2 and PySide6.
For a toplevel widget, the ``rendercanvas.qt.RenderCanvas`` class can be imported. If you want to
embed the canvas as a subwidget, use ``rendercanvas.qt.QRenderWidget`` instead.

Importing ``rendercanvas.qt`` detects what qt library is currently imported:

.. code-block:: py
# Import any of the Qt libraries before importing the RenderCanvas.
# This way rendercanvas knows which Qt library to use.
# Import Qt first, otherwise rendercanvas does not know what qt-lib to use
from PySide6 import QtWidgets
from rendercanvas.qt import RenderCanvas # use this for top-level windows
from rendercanvas.qt import QRenderWidget # use this for widgets in you application
Expand All @@ -64,6 +65,21 @@ embed the canvas as a subwidget, use ``rendercanvas.qt.QRenderWidget`` instead.
app.exec_()
Alternatively, you can select the specific qt library to use, making it easy to e.g. test an example on a specific Qt library.

.. code-block:: py
from rendercanvas.pyside6 import RenderCanvas, loop
# Instantiate the canvas
canvas = RenderCanvas(title="Example")
# Tell the canvas what drawing function to call
canvas.request_draw(your_draw_function)
loop.run() # calls app.exec_()
Support for wx
--------------

Expand Down
29 changes: 22 additions & 7 deletions rendercanvas/_coreutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ def __init__(self):
QT_MODULE_NAMES = ["PySide6", "PyQt6", "PySide2", "PyQt5"]


def select_qt_lib():
"""Select the qt lib to use, used by qt.py"""
# Check the override. This env var is meant for internal use only.
# Otherwise check imported libs.

libname = os.getenv("_RENDERCANVAS_QT_LIB")
if libname:
return libname, qt_lib_has_app(libname)
else:
return get_imported_qt_lib()


def get_imported_qt_lib():
"""Get the name of the currently imported qt lib.
Expand All @@ -170,13 +182,9 @@ def get_imported_qt_lib():
imported_libs.append(libname)

# Get which of these have an application object
imported_libs_with_app = []
for libname in imported_libs:
QtWidgets = sys.modules.get(libname + ".QtWidgets", None) # noqa: N806
if QtWidgets:
app = QtWidgets.QApplication.instance()
if app is not None:
imported_libs_with_app.append(libname)
imported_libs_with_app = [
libname for libname in imported_libs if qt_lib_has_app(libname)
]

# Return findings
if imported_libs_with_app:
Expand All @@ -187,6 +195,13 @@ def get_imported_qt_lib():
return None, False


def qt_lib_has_app(libname):
QtWidgets = sys.modules.get(libname + ".QtWidgets", None) # noqa: N806
if QtWidgets:
app = QtWidgets.QApplication.instance()
return app is not None


def asyncio_is_running():
"""Get whether there is currently a running asyncio loop."""
asyncio = sys.modules.get("asyncio", None)
Expand Down
11 changes: 11 additions & 0 deletions rendercanvas/pyqt5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ruff: noqa: E402, F403

import os

ref_libname = "PyQt5"
os.environ["_RENDERCANVAS_QT_LIB"] = ref_libname

from .qt import check_qt_libname
from .qt import *

check_qt_libname(ref_libname)
11 changes: 11 additions & 0 deletions rendercanvas/pyqt6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ruff: noqa: E402, F403

import os

ref_libname = "PyQt6"
os.environ["_RENDERCANVAS_QT_LIB"] = ref_libname

from .qt import check_qt_libname
from .qt import *

check_qt_libname(ref_libname)
11 changes: 11 additions & 0 deletions rendercanvas/pyside2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ruff: noqa: E402, F403

import os

ref_libname = "PySide2"
os.environ["_RENDERCANVAS_QT_LIB"] = ref_libname

from .qt import check_qt_libname
from .qt import *

check_qt_libname(ref_libname)
11 changes: 11 additions & 0 deletions rendercanvas/pyside6.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ruff: noqa: E402, F403

import os

ref_libname = "PySide6"
os.environ["_RENDERCANVAS_QT_LIB"] = ref_libname

from .qt import check_qt_libname
from .qt import *

check_qt_libname(ref_libname)
21 changes: 16 additions & 5 deletions rendercanvas/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
SYSTEM_IS_WAYLAND,
get_alt_x11_display,
get_alt_wayland_display,
get_imported_qt_lib,
select_qt_lib,
)


# Select GUI toolkit
libname, already_had_app_on_import = get_imported_qt_lib()
libname, already_had_app_on_import = select_qt_lib()
if libname:
QtCore = importlib.import_module(".QtCore", libname)
QtGui = importlib.import_module(".QtGui", libname)
Expand All @@ -52,6 +52,14 @@
)


def check_qt_libname(expected_libname):
"""Little helper for the qt backends that represent a specific qt lib."""
if expected_libname != libname:
raise RuntimeError(
f"Failed to load rendercanvas.qt with {expected_libname}, because rendercanvas.qt is already loaded with {libname}."
)


# Get version
if libname.startswith("PySide"):
qt_version_info = QtCore.__version_info__
Expand Down Expand Up @@ -320,8 +328,7 @@ def _rc_get_pixel_ratio(self):
return self.devicePixelRatioF()

def _rc_set_logical_size(self, width, height):
if width < 0 or height < 0:
raise ValueError("Window width and height must not be negative")
width, height = int(width), int(height)
parent = self.parent()
if isinstance(parent, QRenderCanvas):
parent.resize(width, height)
Expand Down Expand Up @@ -539,7 +546,11 @@ def init_qt(self):
@property
def _app(self):
"""Return global instance of Qt app instance or create one if not created yet."""
return QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
# Note: PyQt6 needs the app to be stored, or it will be gc'd.
app = QtWidgets.QApplication.instance()
if app is None:
self._the_app = app = QtWidgets.QApplication([])
return app

def _rc_call_soon(self, callback, *args):
func = callback
Expand Down
34 changes: 28 additions & 6 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ class Module:

def __init__(self, name):
self.name = name

self.filename = os.path.abspath(
os.path.join(rendercanvas.__file__, "..", self.name + ".py")
)
with open(self.filename, "rb") as f:
self.source = f.read().decode()

self.names = self.get_namespace()

def get_namespace(self):
fname = self.name + ".py"
filename = os.path.abspath(os.path.join(rendercanvas.__file__, "..", fname))
with open(filename, "rb") as f:
code = f.read().decode()

module = ast.parse(code)
module = ast.parse(self.source)

names = {}
for statement in module.body:
Expand Down Expand Up @@ -271,6 +273,26 @@ def test_qt_module():
assert timer_class.name == "QtTimer"


def test_pyside6_module():
m = Module("pyside6")
assert "from .qt import *" in m.source


def test_pyside2_module():
m = Module("pyside2")
assert "from .qt import *" in m.source


def test_pyqt6_module():
m = Module("pyqt6")
assert "from .qt import *" in m.source


def test_pyqt5_module():
m = Module("pyqt5")
assert "from .qt import *" in m.source


def test_wx_module():
m = Module("wx")

Expand Down

0 comments on commit 7ba504c

Please sign in to comment.