Search code examples
pythonqmlpyside2

QML findChild from a different component


The objective: I'm writing a Gui front-end for a Matplotlib-based library for nested samples (pip install anesthetic if you want to have a look).

How I would go about it in C++: My previous experience with QML was a C++ program, where instead of going into QML to find a canvas to render to, I created a C++ object, registered it in QML's type system, and had it behave as a QtQuick controls widget. As far as I know this is the recommended way of doing things: have all the rendering be done in QML, and have all the business-end-logic in C++.

THe best approach and why I can't do it: This approach doesn't work here. AFAIK you can only implement custom QML using C++, and I need for the program to be pure-ish Python (for others to be able to maintain it) some JS is accessible and QML is pretty easy to understand and edit, so I had no objections (C++ was a hard no).

what I got working: I have a working implementation of what I want. It was all in one file. So, naturally I wanted to split the canvas to which I'm drawing to into a separate file: figure.qml. Trouble is, I can't seem to find the object by that name whenever it's loaded from a separate file (the next step is to use a Loader, because the Figure is quite clunky).

I have a two-file project with view.qml being the root, and a component in Figure.qml. The trouble is, it only works if I load the thing with objectName: "component" in view.qml and not in Component.qml.

So how does one findChild in Pyside for an objectName that's in a different .qml file?

MWE:

main.py

import sys
from pathlib import Path
from matplotlib_backend_qtquick.backend_qtquickagg import FigureCanvasQtQuickAgg
from matplotlib_backend_qtquick.qt_compat import QtGui, QtQml, QtCore


def main():
    app = QtGui.QGuiApplication(sys.argv)
    engine = QtQml.QQmlApplicationEngine()
    displayBridge = DisplayBridge()
    context = engine.rootContext()                       
    qmlFile = Path(Path.cwd(), Path(__file__).parent, "view.qml")
    engine.load(QtCore.QUrl.fromLocalFile(str(qmlFile)))
    win = engine.rootObjects()[0]
    if win.findChild(QtCore.QObject, "figure"):
        print('success') # This fails
    app.exec_()

view.qml

import QtQuick.Controls 2.12
import QtQuick.Windows 2.12

ApplicationWindow{
   Figure {

   }
}

Figure.qml

import QtQuick.Controls 2.12 
import QtQuick 2.12

Component{
  Rectangle{ 
    objectName: "figure"
  }
}

Solution

  • Component is used to define a QML element, it does not instantiate it, therefore you cannot access the object. Creating a Figure.qml is equivalent to creating a Component, and you are creating a Component inside another Component.

    The solution is not to use Component:

    Figure.qml

    import QtQuick.Controls 2.12 
    import QtQuick 2.12
    
    Rectangle{ 
        objectName: "figure"
    }
    

    But it is not recommended to use objectName since, for example, if you create multiple components, how will you identify which component it is? o If you create the object after a time T, or use Loader or Repeater you will not be able to apply that logic. Instead of them it is better to create a QObject that allows obtaining those objects:

    from PySide2 import QtCore
    import shiboken2
    
    
    class ObjectManager(QtCore.QObject):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._qobjects = []
    
        @property
        def qobjects(self):
            return self._qobjects
    
        @QtCore.Slot(QtCore.QObject)
        def add_qobject(self, obj):
            if obj is not None:
                obj.destroyed.connect(self._handle_destroyed)
                self.qobjects.append(obj)
            print(self.qobjects)
    
        def _handle_destroyed(self):
            self._qobjects = [o for o in self.qobjects if shiboken2.isValid(o)]
    
    # ...
    object_manager = ObjectManager()
    context = engine.rootContext()
    context.setContextProperty("object_manager", object_manager)
    qmlFile = Path(Path.cwd(), Path(__file__).parent, "view.qml")
    engine.load(QtCore.QUrl.fromLocalFile(str(qmlFile)))
    # ...
    
    import QtQuick.Controls 2.12 
    import QtQuick 2.12
    
    Rectangle{ 
        Component.onCompleted: object_manager.add_qobject(this)
    }