diff --git a/docs/backends.rst b/docs/backends.rst index 200844a..ebdf4c7 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -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 @@ -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 -------------- diff --git a/rendercanvas/_coreutils.py b/rendercanvas/_coreutils.py index a93c36c..2519760 100644 --- a/rendercanvas/_coreutils.py +++ b/rendercanvas/_coreutils.py @@ -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. @@ -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: @@ -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) diff --git a/rendercanvas/pyqt5.py b/rendercanvas/pyqt5.py new file mode 100644 index 0000000..3144556 --- /dev/null +++ b/rendercanvas/pyqt5.py @@ -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) diff --git a/rendercanvas/pyqt6.py b/rendercanvas/pyqt6.py new file mode 100644 index 0000000..da2062b --- /dev/null +++ b/rendercanvas/pyqt6.py @@ -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) diff --git a/rendercanvas/pyside2.py b/rendercanvas/pyside2.py new file mode 100644 index 0000000..1cc7c5d --- /dev/null +++ b/rendercanvas/pyside2.py @@ -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) diff --git a/rendercanvas/pyside6.py b/rendercanvas/pyside6.py new file mode 100644 index 0000000..47549ee --- /dev/null +++ b/rendercanvas/pyside6.py @@ -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) diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index 5c3ddf3..4190440 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -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) @@ -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__ @@ -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) @@ -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 diff --git a/tests/test_backends.py b/tests/test_backends.py index 8247ac1..bccedc5 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -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: @@ -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")