Home > database >  PyQt's "pyqtProperty" VS python's standard "property"
PyQt's "pyqtProperty" VS python's standard "property"

Time:10-13

I have a few questions regarding the pyqtProperty class of PyQt5 package, and the standard built-in property class of python:

  1. Why use pyqtProperty over standard property? The codes given below works in both ways. What's the advantage of using pyqtProperty instead of Python's plain old property (used as a decorator on a method)? I know that I must specify the return type of a pyqtProperty, but why would I want to use it?

  2. Does pyqtProperty inherits from property? The attribute pyqtProperty.__mro__ does not show that (the result is PyQt5.QtCore.pyqtProperty, object) and I can't find a source code file of pyqtProperty. PyDoc's command refers to a .pyd file of the module PyQt5.QtCore (which is not a source code plain text file), and there is no stub file for it. But the command help(pyqtProperty) gives me information very similar to the one get with help(property), different from the one found on the official documentaion. So, where that information comes from?

  3. Why the methods decorated with @pyqtProperty or even with @property are executed when the Qobject is set up?

    Back to the codes given below: the property methods (in the both cases @pyqtProperty and @property) are executed before creating the attribute _custom_property, which gives a AttributeError. But that should not happen, and really does not happen with @property decorator with regular python classes (I've tried that too, there's a snipet below). When I use simple getter and setter methods (not decorated), that doesn't happen at all (which is normal). The strangest phenomenon is: this AttributeError occurs when I run in debug mode. In normal mode (python main.py) the property methods get executed anyway, but the error is not raised.

I'm using PyQt5 version 5.9.2 installed with Anaconda Distribution, python 3.8.


This is the contents of Ui_MainWindow.py file, generated with pyuic5:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(419, 196)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
        self.lineEdit.setAlignment(QtCore.Qt.AlignCenter)
        self.lineEdit.setObjectName("lineEdit")
        self.horizontalLayout.addWidget(self.lineEdit)
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setObjectName("pushButton")
        self.horizontalLayout.addWidget(self.pushButton)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 419, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.lineEdit.setText(_translate("MainWindow", "test"))
        self.pushButton.setText(_translate("MainWindow", "PushButton"))

This is the content of the main.py file using property decorator:

from Ui_MainWindow import Ui_MainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow,Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self._custom_property = 1
    
    @property
    def custom_property(self):
        print("This method is executed")
        return self._custom_property

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()

This is the content of the main.py file using pyqtProperty decorator:

from Ui_MainWindow import Ui_MainWindow
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import pyqtProperty

class MainWindow(QMainWindow,Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self._custom_property = 1
    
    @pyqtProperty(int)
    def custom_property(self):
        print("This method is executed")
        return self._custom_property

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec_()

I won't put the content of the main file with no decorator (just remove the @ lines in the code above).
But this is a simple file with property decorator showing it is not executed in the __init__ method

class Test:
    
    def __init__(self) -> None:
        pass
    
    @property
    def x(self):
        "This method is not executed in init"
        return self._x
    
    def setx(self,value):
        self._x = value

if __name__ == "__main__":
    a = Test()

CodePudding user response:

  1. The difference between pyqtProperty and property, not an advantage, is that these properties will be accessible through the metaObject that Qt has, and this last tool is used for various tasks in Qt such as exposing properties in QML, being able to be used in QtStyleSheet, being used in QPropertyAnimation, etc.

  2. property as pyqtProperty is a descriptor so it should not necessarily inherit from property (here is an example of how to create descriptors without inheriting from property). If you want to know the implementation of pyqtProperty then you should check the qpycore_pyqtproperty.h and qpycore_pyqtproperty.cpp file in the qpy/QtCore folder of the source code.

  3. The custom_property method call is caused by QMeraObject::connectSlotsByName() but I think it is a bug. This method tries to make the semantic connection if the name of the method meets a certain pattern: on_<object name>_<signal name>(<signal parameters>). I point out that it is a bug since if I implement the same logic with PySide2 I do not observe that behavior.

CodePudding user response:

The answer by eyllanesc is complete enough, but I'd like to extend it by explaining some situations for which using a pyqtProperty is actually required and using standard python properties won't suffice:

Qt StyleSheets

QSS provide access to Qt properties:

  1. for selectors, it's possible to define a rule whether a property matches the specified value; for instance, you could change the color of all buttons that are labelled Ok or Cancel:
QPushButton[text="Ok"] {
    color: green;
}
QPushButton[text="Cancel"] {
    color: red;
}

Note that changing the value of a property that defines appearance when the widget has already been displayed ("polished" by the style) requires to recompute the stylesheet again. In the above case, if you change the text of the button from "Ok" to "Cancel", you won't see any change in the text color. In order to achieve that, it's required to re-set the stylesheet by means of obj.setStyleSheet(obj.styleSheet()), which can be done on the widget itself or any of the parents that have a stylesheet set (including the QApplication).
The easiest way is to do self.setStyleSheet(self.styleSheet()) in the setter of the property (possibly, after checking if the value has actually changed), which will work even if the widget has no style sheet set and the rule is defined on any of its ancestors.

  1. it's possible to set properties, as long as their types are supported by QSS:
#Window {
    qproperty-minimumSize: 300px 300px;
    qproperty-geometry: rect(250 250 400 400);
}

Note that, as the documentation explains, "the qproperty syntax is evaluated only once, which is when the widget is polished by the style", so you cannot set properties with pseudo-states (like the :hover state) as they will probably be ignored or provide unexpected and unreliable behavior.

Item delegate editors

By default, item delegates set the value of an editor using its user property.

class MyCustomEditor(QtWidgets.QWidget):
    # ...
    QtCore.pyqtProperty(object, user=True)
    def myCoolProperty(self):
        # ...

This means that if you have a custom editor and use a Qt property declared with the user flag, you only need to implement createEditor() to return the custom editor (or even define the editor in a custom QItemEditorFactory for specific data types), and setEditorData() will automatically set its value without worrying about the index, the data type or the property name.

Designer plugins

This is more of a fringe scenario, as creating plugins for designer is rarely needed and it is not an easy task (the PyQt documentation is scarce and mostly outdated, and some features and functions provided for C are not implemented). But, if you do need it, the only way to have custom properties shown in the property editor box of a custom widget created with a plugin is to use Qt properties.

  • Related