Skip to content

Commit

Permalink
Backends for all qt libs
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein committed Nov 12, 2024
1 parent 44c2c26 commit 20bdcca
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 16 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

0 comments on commit 20bdcca

Please sign in to comment.