Issue
Trying to keep styling separate from the rest of my PyQt code, I try to do my styling using qss files. However, doing all the styling using this seems to be difficult (if not impossible).
What would help me achieve this would be the possibility of using custom qss properties. To give an example, I have a class that represents a line which can be either horizontal or vertical:
class Line(QWidget):
def __init__(self, direction: Qt.Orientation, thickness: int, parent: Optional[QWidget]):
super().__init__(parent)
self.setAttribute(QtCore.Qt.WA_StyledBackground, True)
if direction == Qt.Orientation.Vertical:
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
self.setFixedWidth(thickness)
else:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFixedHeight(thickness)
However, there is no way (that I know of) to set a 'thickness' property from a qss file. I can of course create subclasses from Line
: HLine
and VLine
and then, using respectively the height and width properties, set the thickness. However, that does not change the fact that it seems impossible to set custom styling properties such as thickness from a qss file. If not possible, what would be the preferred approach to achieve this?
Solution
First of all, in order to set custom properties, those must be actual properties. Initialization arguments are not sufficient, because properties might be changed at runtime, or caused by other aspects (like styling, which is your case).
Qt does allow setting properties. See the "Setting QObject Properties" section from the Style Sheet Syntax documentation:
From 4.3 and above, any designable Q_PROPERTY can be set using the qproperty- syntax.
Using the property
decorator alone is not enough, as Qt has no knowledge about the "underlying" Python binding. All custom properties that may be potentially set by style sheets must then be created as actual Qt properties, which is by using pyqtProperty
(PyQt) and Property
(PySide) decorators from the QtCore module. Note that one should always consider using custom properties of QObject subclasses as actual Qt properties, as it makes them more consistent in the Qt context.
class Line(QWidget):
_thickness = 0
_direction = Qt.Orientation(0)
def __init__(self, direction=Qt.Orientation.Horizontal, thickness=1, parent=None, **kwargs):
super().__init__(parent, direction=direction, thickness=thickness, **kwargs)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground)
@pyqtProperty(Qt.Orientation)
def direction(self):
return self._direction
@direction.setter
def direction(self, direction):
if self._direction != direction:
self._direction = direction
if direction == Qt.Orientation.Horizontal:
self.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
else:
self.setSizePolicy(
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
self.updateGeometry()
@pyqtProperty(int)
def thickness(self):
return self._thickness
@thickness.setter
def thickness(self, size):
if size < 1:
return
if self._thickness != size:
self._thickness = size
self.updateGeometry()
def minimumSizeHint(self):
return self.sizeHint()
def sizeHint(self):
if self._direction == Qt.Orientation.Horizontal:
return QSize(1, self._thickness)
return QSize(self._thickness, 1)
Note that I specifically changed the __init__
syntax so that defined properties are directly (and properly) set by the PyQt property management upon initialization.
With the above, you can use a basic selector (for instance, using the object name) and set both the direction and the thickness.
self.line = Line(objectName='test')
layout.addWidget(self.line)
...
self.setStyleSheet('''
Line#test {
qproperty-thickness: 2;
qproperty-direction: Vertical;
}
''')
Be aware about the note at the end of the documentation above, though:
Use the qproperty syntax with care, as it modifies the widget that is being painted. Also, the qproperty syntax is evaluated only once, which is when the widget is polished by the style. This means that any attempt to use them in pseudo-states such as QPushButton:hover, will not work.
This is important not only for what explained, but because it also implies that any property definition that might be used elsewhere in the style sheet as a selector may not result in proper updates.
Consider the following attempt:
app = QApplication([])
app.setStyleSheet('''
Line[direction=Horizontal] {
background: orange;
qproperty-thickness: 4;
}
Line[direction=Vertical] {
background: green;
qproperty-thickness: 12;
}
Line#topSeparator {
qproperty-direction: Vertical;
}
''')
test = QWidget()
l = QGridLayout(test)
l.addWidget(QTextEdit())
l.addWidget(Line(objectName='topSeparator'), 0, 1)
l.addWidget(QTextEdit(), 0, 2)
l.addWidget(Line(objectName='midSeparator'), 1, 0, 1, 3)
l.addWidget(QTextEdit(), 2, 0, 1, 3)
test.show()
app.exec()
While the Line#topSeparator
selector properly sets the direction of the vertical line, the color and thickness are not the expected ones.
This is because setting the Qt property from the style sheet doesn't make the widget evaluate again its current state, so the Line[direction=Horizontal]
is actually ignored.
The solution to this is to force a style repolishing, while being careful to avoid any further repolishing within the same change: since the style may attempt to alter the very property that caused the change, the result may be unnecessary calls to the style un/polishing and even infinite recursion under certain conditions.
class Line(QWidget):
_polishRecursionGuard = False
...
@direction.setter
def direction(self, direction):
if self._direction != direction:
... # as above
self._polish()
@thickness.setter
def thickness(self, size):
if size < 1:
return
if self._thickness != size:
self._thickness = size
self.updateGeometry()
self._polish()
def _polish(self):
if not self._polishRecursionGuard:
self._polishRecursionGuard = True
self.style().unpolish(self)
self.style().polish(self)
self._polishRecursionGuard = False
Note that since the introduction of actual enums in PyQt6 (and PySide6), actual enum names can be used as values for property selectors. With Qt5, only numerical values could be used for enums, so in the case above it should have been Line[direction="1"]
etc.
Obviously, you must be very careful in not using properties and selectors that may have conflicting results, such as a selector that changes a property that triggers another selector which may reset it again.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.