Skip to content

visualdynamics.gui

gui

Desktop GUI. Start with visualdynamics-gui [files...] or python -m visualdynamics.

Functions:

Name Description
qt_binding

Which Qt pyqtgraph is bound to, or None if it is still free.

check_qt_binding

Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

Functions:

qt_binding

qt_binding() -> str | None

Which Qt pyqtgraph is bound to, or None if it is still free.

None means nothing has imported pyqtgraph yet, so importing visualdynamics's own widgets first will decide it in our favour.

Source code in src/visualdynamics/gui/__init__.py
def qt_binding() -> str | None:
    """Which Qt pyqtgraph is bound to, or None if it is still free.

    None means nothing has imported pyqtgraph yet, so importing visualdynamics's
    own widgets first will decide it in our favour.
    """
    import sys

    module = sys.modules.get('pyqtgraph')
    return None if module is None else module.Qt.QT_LIB

check_qt_binding

check_qt_binding() -> None

Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

pyqtgraph binds to one Qt for the life of the process, chosen when it is first imported: whichever binding is already in sys.modules, and failing that its own order of preference, which puts PyQt6 ahead of PySide6. Having PySide6 installed is not enough.

visualdynamics is a PySide6 application, so a pyqtgraph on any other binding hands back plot widgets a PySide6 layout will not accept. That surfaced as QSplitter.addWidget called with wrong argument types from deep inside the main window, naming nothing useful.

So: import PySide6 first, which decides it in our favour whenever pyqtgraph has not yet been imported. When it has — sdynpy imports it with PyQt5 at import sdynpy — the choice cannot be undone, and all that is left is to say so before a window is built out of the mismatch.

Source code in src/visualdynamics/gui/__init__.py
def check_qt_binding() -> None:
    """Point pyqtgraph at the same Qt as the rest of the GUI, or explain.

    pyqtgraph binds to one Qt for the life of the process, chosen when it is
    first imported: whichever binding is already in `sys.modules`, and
    failing that its own order of preference, which puts **PyQt6 ahead of
    PySide6**. Having PySide6 installed is not enough.

    visualdynamics is a PySide6 application, so a pyqtgraph on any other binding hands
    back plot widgets a PySide6 layout will not accept. That surfaced as
    `QSplitter.addWidget called with wrong argument types` from deep inside
    the main window, naming nothing useful.

    So: import PySide6 first, which decides it in our favour whenever
    pyqtgraph has not yet been imported. When it has — sdynpy imports it
    with PyQt5 at `import sdynpy` — the choice cannot be undone, and all
    that is left is to say so before a window is built out of the mismatch.
    """
    import importlib

    import PySide6.QtWidgets  # noqa: F401  — imported for its side effect

    # deliberately not an import statement: these two are ordered, and an
    # import sorter would put pyqtgraph first and undo the whole point
    binding = importlib.import_module('pyqtgraph').Qt.QT_LIB
    if binding == 'PySide6':
        return
    raise RuntimeError(
        f'pyqtgraph is using {binding}, but visualdynamics is a PySide6 application, '
        f'and widgets from two Qt bindings cannot share a window.\n'
        f'\n'
        f'pyqtgraph picks its binding when it is first imported, and '
        f'something imported it before visualdynamics — sdynpy does this, with PyQt5, '
        f'at "import sdynpy". The choice cannot be undone in a running '
        f'process.\n'
        f'\n'
        f'`visualdynamics.launch_gui()` handles this by starting the app in a '
        f'fresh process; this path is for running the window inside the '
        f'current one, which needs an interpreter that has not imported '
        f'sdynpy — or PYQTGRAPH_QT_LIB=PySide6 set before any import, '
        f'which then breaks sdynpy\'s own plotting in that process.')