Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Dynamic creation of QObject with Signals and QProperties



  • Hi everyone,

    I use Qt with Qt Quick to create my personal applications and utilities.
    I really like the MVVM pattern, UI bindings and speed of development using WPF with C#, which I use at work.
    As such I always create a "ViewModel" QObject subclass, which I then expose to the main QML context, which contains Q_PROPERTY-ies and Q_INVOKABLE-s to use from QML. This works quite well.
    I have been using a bunch of macros to make creating and using Qt Properties faster, when a custom getter/setter is not required, for example. They create the trivial getter, setter, notify Signal, and the Q_PROPERTY call.

    I have been looking into Qt for Python to add UI to some existing Python utils.
    All works well, except I cannot find a way to remove the boilerplate of creating all the default getters and setters by hand at class creation.
    I tried using a metaclass, but realised that it wouldn't work because QObject also needs to use it.
    So I made a class factory function, which creates a class using the type() factory. I use closures to make the functions remember the name of the property.

    # create attributes dict with all Qt Properties
    .... for each property
    if initial_val is not None:
                attributes["_" + name] = initial_val
            else:
                attributes["_" + name] = _type()
    
    attributes["_" + name + "_fget"] = make_getter(name)
    attributes["_" + name + "_fset"] = make_setter(name)
    attributes[name + "_changed"] = Signal(None, name=name + "_changed")
    
    attributes[name] = Property(_type, fget=attributes["_" + name + "_fget"], 
                                            fset=attributes["_" + name + "_fset"], 
                                            notify=attributes[name + "_changed"])
    ....
    vm = type(vm_name, (QObject, ), attributes)
    return vm
    

    It seems to work: I can create a ViewModel class super quickly with the required Qt Properties. The properties are accessible from QML as normal. Here is an example of the API.

    # property type, property name, property initial value
    ViewModel = create_viewmodel("ViewModel", [(str, "testStr", "testStr"), (int, "testInt", 555)])
    vm = ViewModel()
    

    However, I am having an issue with Qt Signals. It seems like the Signal objects do not have an "emit()" method. As such any property binding with QML does not work if the property is updated from QML.

    Can somebody explain why Signal object is lacking emit(), and how to get them working correctly?
    I can see that there is some sort of "magic" going on when one declares a signal, like maybe the Shiboken subclass hook? I don't know much about how this works, or if it would be possible to trigger it manually.

    Thanks for the help in advance. If I can get this working, I think Qt + Qt Quick would be my new go-to tool for rapid development.

    Full code for my example:

    import os
    from PySide2.QtCore import *
    from PySide2.QtQml import QQmlApplicationEngine
    from PySide2.QtWidgets import QApplication
    
    def create_viewmodel(vm_name, props):
        attributes = {}
    
        for _type, name, initial_val in props:
            if initial_val is not None:
                attributes["_" + name] = initial_val
            else:
                attributes["_" + name] = _type()
            
            def make_getter(name):
                def getter(self):
                    return attributes["_" + name]
    
                return getter
    
            def make_setter(name):
                def setter(self, val):
                    attributes["_" + name] = val
                    # Fails here: does not have emit() method
                    attributes["_property_changed"].emit(name)
                    attributes[name + "_changed"].emit()
                
                return setter
    
    
            attributes["_" + name + "_fget"] = make_getter(name)
            attributes["_" + name + "_fset"] = make_setter(name)
            attributes[name + "_changed"] = Signal(None, name=name + "_changed")
    
            attributes[name] = Property(_type, fget=attributes["_" + name + "_fget"], 
                                            fset=attributes["_" + name + "_fset"], 
                                            notify=attributes[name + "_changed"])
    
        def init(self):
            print("init!")
            QObject.__init__(self)
    
    
        attributes["__init__"] = init
        attributes["_property_changed"] = Signal(str, name="_property_changed", arguments=str)
        print(attributes)
    
        vm = type(vm_name, (QObject, ), attributes)
    
        return vm
    
    # Conventional QObject subclass does work as expected. But so verbose.
    class VM2(QObject):
        test_changed = Signal()
        property_changed = Signal(str)
    
        def get_test(self):
            return self._test
    
        def set_test(self, val):
            self._test = val
            self.test_changed.emit()
            self.property_changed.emit("test")
    
        test = Property(str, get_test, set_test, notify=test_changed)
    
        def __init__(self):
            QObject.__init__(self)
            self._test = "testVM2"
    
    
    def main():
        app = QApplication([])
        
        engine = QQmlApplicationEngine()
        qml_file_path = os.path.join(os.path.dirname(__file__), 'main.qml')
        qml_url = QUrl.fromLocalFile(os.path.abspath(qml_file_path))
    
        ViewModel = create_viewmodel("ViewModel", [(str, "testStr", "testStr"), (int, "testInt", 555)])
        vm = ViewModel()
    
        vm2 = VM2()
    
        engine.rootContext().setContextProperty("viewModel", vm)
        engine.rootContext().setContextProperty("viewModel2", vm2)
        
        engine.load(qml_url)
        app.exec_()
    
    if __name__ == '__main__':
        main()
    
    import QtQuick 2.0
    import QtQuick.Window 2.0
    import QtQuick.Controls 2.11
    import QtQuick.Layouts 1.11
    
    Window {
        width: 1024
        height: 768
        visible: true
        
        ColumnLayout {
            Text {
                text: "%1 %2 %3".arg(viewModel.testStr).arg(viewModel.testInt).arg(viewModel2.test)
            }
    
            Button {
                text: "Change property"
                onClicked: viewModel.testStr = "CHANGED"
            }
        }
    }
    

  • Banned

    Okay how to do this in a nutshell -- Signals/Slots are an event driven methodology used within a thread or across threads

    You define a Signal where it will be used as follows:

    class MyClass(ClassType)
         sigMySignal = pyqtSignal()
    
         def __init__(self)
              ClassType.__init__(self)
    

    so outside the Initialization function of a class because this object will be associated with the Application Thread Event Handler (or the Thread that it resides within) then somewhere within the Class you would do something like this

    self.OtherClass = OtherClass()
    self.sigMySignal.connect(self.OtherClass.SignalHandler)
    ...
    self.sigMySignal.emit()
    

    Then within the other class you define the Slot or SignalHandler as follows:

    @pyqtSlot()
    def SignalHandler(self):
    

    So in short the signal object does have the .emit() property but I believe this is some how tied into the Threads Event Handler. Again keep in mind that Signals/Slots can work across Thread boundaries as well as within Thread boundaries and get added to the appropriate Event Queue

    Now while this does not answer your question per-sae directly perhaps it will help you understand Signals/Slots well enough to apply to it whatever you are needing it for



  • Hey Denni O,

    Thanks for your reply.

    Basically I think I have the concept of the signals and slots down, I have been using them for a while in C++ and also with PySide2.

    My post was basically to ask, why do Signals not work when created dynamically in a class factory?
    It all boils down to this issue: it seems to be that creating the signal this way works, as in, it creates a usable Signal class with "emit()" methods and more:

    test_changed = Signal()
    property_changed = Signal(str)
    

    But not this way:

    attributes[name + "_changed"] = Signal(None, name=name + "_changed")
    

    In this case, the Signal class is created but is "empty", does not have any of the Signal usual methods such as emit().

    As such I was thinking, maybe anybody knowns if Shiboken object base class is doing something extra in the first case: if so, can I replicate this to make my dynamic class work?

    Thanks in advance.



  • Hurrah! I have solved the issue.

    This is related to people trying to use the signal from the class and not on the instance (through "self"), which is a common pitfall with PySide2, seems like. Now I also fall to it....
    I suppose the attributes dict is the "class variable" and not the "instance variable" and as such does not have the methods, I guess they are generated on instantiation.

    This is beautiful! Fully dynamic class creation. Can't wait to make something with this.
    I only had to change this part of the create_viewmodel function:

    def make_getter(name):
        def getter(self):
            return getattr(self, "_" + name)
    
        return getter
    
    def make_setter(name):
        def setter(self, val):
            setattr(self, "_" + name, val)
            # Works!!
            getattr(self, "_property_changed").emit(name)
            getattr(self, name + "_changed").emit()
                
        return setter
    

Log in to reply