Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. Dynamic creation of QObject with Signals and QProperties
QtWS25 Last Chance

Dynamic creation of QObject with Signals and QProperties

Scheduled Pinned Locked Moved Unsolved Qt for Python
3 Posts 2 Posters 1.2k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • J Offline
    J Offline
    james2048
    wrote on last edited by
    #1

    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"
            }
        }
    }
    
    1 Reply Last reply
    0
    • J Offline
      J Offline
      james2048
      wrote on last edited by
      #2

      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.

      1 Reply Last reply
      0
      • J Offline
        J Offline
        james2048
        wrote on last edited by
        #3

        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
        
        1 Reply Last reply
        4

        • Login

        • Login or register to search.
        • First post
          Last post
        0
        • Categories
        • Recent
        • Tags
        • Popular
        • Users
        • Groups
        • Search
        • Get Qt Extensions
        • Unsolved