BoshWash
BoshWash

Reputation: 5450

How to dynamically create Q_Properties

I want to create Q_Properties dynamically from a tuple containing the property names as strings.

The following non_dynamic code works:

class MyObject(QObject):
    def __init__(self,startval=42):
        self.ppval = startval

    def readPP(self):
        return self.ppval

    def setPP(self,val):
        self.ppval = val

    pp = QtCore.Property(int, readPP, setPP)

My first attempt was to use the setProperty function but it returns False, meaning the property is not added to the staticMetaObject and is therefore not visible to Qt's StyleSheets.

class MyObject(QWidget):
    def __init__(self,startval=42):
        self.ppval = startval
        print(self.setProperty('pp', QtCore.Property(int, self.readPP, self.setPP)))

    def readPP(self):
        return self.ppval

    def setPP(self,val):
        self.ppval = val

Another approach was to use setAttr. but again it fails to add the property to the staticMetaObject.

class MyObject(QWidget):
    def __init__(self,startval=42):
        self.ppval = startval
        setattr(self, 'pp', QtCore.Property(int, self.readPP, self.setPP)

    def readPP(self):
        return self.ppval

    def setPP(self,val):
        self.ppval = val

So far I haven't found a way to add something manually to staticMetaObject.

Upvotes: 3

Views: 2896

Answers (5)

J. Doe
J. Doe

Reputation: 89

Found this thread while trying to use QPropertyAnimation on dynamically created properties without reinventing the wheel too much.

According to the docs for QPropertyAnimation: To make it possible to animate a property, it must provide a setter, and an old forum post hinted that that was a requirement to use Static properties.

However after experimentation I found it worked by listening for the QDynamicPropertyChangeEvent and then get the value back out of Qt:

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setProperty("my_property")

    def start_my_animation(self):
        animation = QtCore.QPropertyAnimation()
        animation.setParent(self)
        animation.setTargetObject(self)
        animation.setPropertyName(b"my_property")  # has to be bytes here
        animation.setEndValue(10)
        animation.setDuration(300)
        animation.start()

    def event(self, event):
        if event.type() == QtCore.QEvent.DynamicPropertyChange:
            property_name = event.propertyName().data().decode()  # convert to str
            print('dynamic property change:', property_name, self.property(property_name)
        return super().event(event)

Hope it helps!

Upvotes: 0

user16776498
user16776498

Reputation:

1. QQmlPropertyMap:

Although this is not answering the OP directly this is probably what who came here was looking for.

example:

# some_class_exposed_to_qml.py

class PageManager(qtc.QObject):
    currentPageChanged = qtc.Signal()
    A, B, C, D, E = range(5)

    def __init__(self, parent):
        super().__init__(parent)
        self._current_page = self.Events
        self._pages = qqml.QQmlPropertyMap()
        self._pages.insert("A", self.A)
        self._pages.insert("B", self.B)
        self._pages.insert("C", self.C)
        self._pages.insert("D", self.D)
        self._pages.insert("E", self.E)

    @qtc.Property(qqml.QQmlPropertyMap, constant=True)
    def Pages(self):
        return self._pages

main.qml

Rectangle{id: page_A
    anchors.fill: parent;
    Text{text:"i am Page A, my value is:" + PageManager.Pages.A }
}

2. exec:

DISCLAIMER:

  1. I haven't tested this so you might want to write some tests.
  2. This will ONLY work for native data types that are supported by QVariant do not try to add other d.t since it wont be registered as a QMetaType.
  3. I experienced some bugs with pycharm debugger while using this but without it it ran O.K.

Usage:

@Qdefine # this decorator will generate 1.getter, 2.setter, 3.notify 4.property
class WinSize:
    appHeight: int
    appWidth: int
a = WinSize(400, 400)

# or dynamically create dynamic properties Xd
def make_Qdefine(name: str, bases: tuple, namespace: dict):
    return Qdefine(type(name, bases, namespace))

concept:

  1. we are using attrs(you can use dataclasses though) to initiate the underlying data
  2. taking the __annotations__ from the decorated class
  3. creating an exec string that will create a class with getters setters signals and properties yada yada.
  4. the __init__ function of the class will call the __init__ of the attrs class and will raise any errors given by it
  5. every time you call the setter the notifier is emmitted
import inspect
from attr import define
from qtpy.QtCore import *


def Qdefine(original_cls):
    """this only works for native data types"""
    attered = define(original_cls)
    exc_str = []

    for name, param in inspect.signature(attered).parameters.items():
        type_name = param.annotation.__name__
        getter = f"return self.origin.{name}"
        if param.default != inspect._empty:
            getter = param.default

        exc_str.append(
f"""    
    @Slot(result={type_name})
    def get_{name}(self):
        {getter}        
        
    @Slot({type_name})
    def set_{name}(self, value):
        self.origin.{name} = value
        self.{name}_changed.emit()
        
    @Signal
    def {name}_changed():
        pass
        
    {name} = Property({type_name}, fget=get_{name}, fset=set_{name}, notify={name}_changed)
""")

    exc_str = "\n".join(exc_str)
    exec(
f"""
class Q{original_cls.__name__}(QObject):

    def __init__(self, *args, **kwargs):
         self.origin = self._origin(*args, **kwargs)
         super().__init__(None)

        
{exc_str}"""
         )
    newClass = locals().get(f"Q{original_cls.__name__}")
    setattr(newClass, '_origin', attered)
    return newClass

Upvotes: 0

MichaelH
MichaelH

Reputation: 41

I came to a similar problem as @BoshWash, with the difference, that I wanted to generate the dict programatically. As a side-effekt I have a proof-of-concept sniped.

@ekhumoro already proposed a solution which is working:

So it looks like the only way to create meta-properties dynamically would be to create a new class using type. Instances of this new class could then be used as a proxy for gathering information from the stylesheet.

An alternative solution is to add a baseClass to the "dynamic-called" object and do the initialization during __init_subclass__

I impemented/tested the approach with metaclasses, and it is actually working.

The trick is, to use the __prepare__ function in the meta function. One alternative is to use a derived funktion and use the __init_subclass__ method (I also implemented that one, but it is not as handy as the posted solution). It is important to clear the arguments of the __new__, otherwise the Qt-metaclass will fail.

The main magic happens in the line:

generatedProperties = { "foo": "FOO", "bar": 1}
    
newType = types.new_class(f"MyObjectDynamic", (MyObject,), {
                "metaclass": Meta, 
                "propertiesDict": generatedProperties
                }, lambda ns: ns)

The important point is to have an entry-point bevore the Qt-Meta object will do it's magic.

main_test.py:

import pytest

import PySide2.QtCore as QtCore
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine

from functools import partial
import types

class Meta(type(QtCore.QObject)):
    def __new__(cls, *args, **kwargs):
        # needs to be done, type(QObject) needs an empty kwargs, otherwise it is not possible to pass arguments to __prepare__
        kwargs.clear()
        x = super().__new__(cls, *args, **kwargs)
        return x
    
    @classmethod
    def __prepare__(cls, bases, *args, **kwargs): # No keywords in this case
        newDict = dict()
        if "propertiesDict" not in kwargs:
            return newDict

        propertiesDict = kwargs["propertiesDict"]

        for field in propertiesDict:
            propertyType = type(propertiesDict[field])

            s = QtCore.Signal()
            newDict[f"{field}Changed"] = s
            newDict[f"{field}Value"] = propertyType(propertiesDict[field])
            def setter(field, self, value):
                s = getattr(self, f"{field}Changed")
                setattr(self, f"{field}Value", value)
                s.emit()
            def getter(field, self):
                return getattr(self, f"{field}Value")
            setterPartial = partial(setter, field)
            getterPartial = partial(getter, field)
            newDict[field] = QtCore.Property(propertyType, fset=setterPartial, fget=getterPartial, notify=s)
        return newDict

class MyObject(QtCore.QObject):
    pass

class Helper(QtCore.QObject):
    @QtCore.Slot(str)
    def debugPrint(self, value):
        print(f"debugPrint {value}")

def test_proof_of_concept_dynamic_properties() -> None:
    
    print("\n")

    generatedProperties = { "foo": "FOO", "bar": 1}

    newType = types.new_class(f"MyObjectDynamic", (MyObject,), {
            "metaclass": Meta, 
            "propertiesDict": generatedProperties
            }, lambda ns: ns)
    x = newType()
    
    # x.fooValue = "foo"
    print(f"fooValue: {x.fooValue}")
    print(f"barValue: {x.barValue}")

    print(f"Foo: {x.foo}")
    print(f"Bar: {x.bar}")

    def funcFoo():
        print("funcFoo")
    def funcBar():
        print("funcBar")
    x.fooChanged.connect(funcFoo)
    x.fooChanged.connect(funcBar)
    
    x.fooChanged.emit()
    x.barChanged.emit()

    # show usage in QML
    app = QGuiApplication()

    h = Helper()
    engine = QQmlApplicationEngine()  # type: ignore
    engine.rootContext().setContextProperty("myObject", x)
    engine.rootContext().setContextProperty("helper", h)

    engine.load("tests/main_proof_of_concept.qml")

main_proof_of_concept.qml:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    id: appWindows
    visible: true
    width: 600
    height: 500
    title: "HelloApp"

    Component.onCompleted: {
        helper.debugPrint("Completed Running!")
        helper.debugPrint("+onCompleted Foo " + myObject.foo)
        helper.debugPrint("+onCompleted Bar " + myObject.bar)
        myObject.foo = "FOO_"
        myObject.bar = 2
        helper.debugPrint("-onCompleted Foo " + myObject.foo)
        helper.debugPrint("-onCompleted Bar " + myObject.bar)
        appWindows.close()
    }
    Connections {
        target: myObject
        function onFooChanged() {
            helper.debugPrint("onResultChanged1 " + myObject.foo)
        }
    }
    Connections {
        target: myObject
        function onBarChanged() {
            helper.debugPrint("onResultChanged2 " + myObject.bar)
        }
    }

    Text {
        anchors.centerIn: parent
        text: "Hello World "
        font.pixelSize: 24
    }

}

Upvotes: 2

ekhumoro
ekhumoro

Reputation: 120738

I'm going to stick my neck out here and say that I don't think it is possible to do this. The meta-object is created dynamically during the exceution of the python class statement, so it is not possible to add anything to it afterwards.

You can try the following after the class is constructed:

    MyObject.pp = QtCore.Property(int, MyObject.readPP, MyObject.setPP)

and it will function as a normal python property:

    obj = MyObject()
    print(obj.pp) # prints "42"

but it will not be listed as a meta-property:

    meta = obj.metaObject()
    for index in range(meta.propertyCount()):
        print('meta: %s' % meta.property(index).name())
    # only prints "meta: objectName"

The setProperty method is of no use here, because although it can be used to set the values of existing meta-properties, any new properties created in this way will not themselves become meta-properties. Instead, they will become dynamic properties, which can be read via qss but not set. Only meta-properties can be set via qss.

So it looks like the only way to create meta-properties dynamically would be to create a new class using type. Instances of this new class could then be used as a proxy for gathering information from the stylesheet.

Upvotes: 4

hyde
hyde

Reputation: 62898

Re-read what return value false means:

If the property is defined in the class using Q_PROPERTY then true is returned on success and false otherwise. If the property is not defined using Q_PROPERTY, and therefore not listed in the meta-object, it is added as a dynamic property and false is returned.

If you are actually having trouble using the dynamic property, then try adding relevant code to the question, such as the stylesheet you mention, and simply printing the property value after setting it.

Upvotes: 0

Related Questions