Reputation: 5450
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
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
Reputation:
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 }
}
QVariant
do not try to add other d.t since it wont be registered as a QMetaType
.@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))
__annotations__
from the decorated class__init__
function of the class will call the __init__
of the attrs
class and will raise any errors given by itimport 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
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
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
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