I'm trying to create a pyqt5 application on Linux mint 19 (cinnamon). I have a 'delete' and 'apply' buttons, but i want to give them more relevant look. Like on this pictures:
native Linux mint buttons
more
It is a native look of buttons with apply or delete role on linux mint and I want to create buttons like this in my application, but I haven't found a way to do so.
It seems that windows and mac have qtmacextras
and qtwinextras
modules for such stuff. Linux have some kind of qtx11extras
, but that module does not provide such functionality.
While the solution already provided works perfectly, is not completely application wide, especially if you want to control the appearance of buttons of dialogs.
Even if you apply the stylesheet to the application, you'll need to ensure to correctly set the selectors in parents: in the other answer, for example, setting the background property without selectors will clear the app stylesheet inheritance of the children.
Assuming the stylesheet is set to the application and all other stylesheets set are carefully written, the problem comes with dialog windows.
If the dialog is created manually, alternate colors can be set by specifying the stylesheet for each button for which you want the alternate color, but this is impossible for those created with static functions.
In that case, the only possibility is to use a QProxyStyle. The following is a possible implementation that also allows to set custom colors and fonts, and automatically sets an alternate color for "negative" roles of dialog button boxes (cancel, ignore, etc).
In this example I just applied the style to the application, but the message box is created using the information()
static function. The "Alternate" button is manually set using a custom property: button.setProperty('alternateColor', True)
.
class ColorButtonStyle(QtWidgets.QProxyStyle):
def __init__(self, *args, **kwargs):
if isinstance(kwargs.get('buttonFont'), QtGui.QFont):
self._buttonFont = kwargs.pop('buttonFont')
else:
self._buttonFont = QtWidgets.QApplication.font()
self._buttonFont.setPointSize(20)
super().__init__(*args, **kwargs)
self._buttonFontMetrics = QtGui.QFontMetrics(self._buttonFont)
self._defaultButtonColor = QtGui.QColor(109, 180, 66)
self._defaultTextColor = QtGui.QColor(QtCore.Qt.white)
self._alternateButtonColor = QtGui.QColor(240, 74, 80)
self._alternateTextColor = None
self._alternateRoles = set((
QtWidgets.QDialogButtonBox.RejectRole,
QtWidgets.QDialogButtonBox.DestructiveRole,
QtWidgets.QDialogButtonBox.NoRole,
))
def _polishApp(self):
self.polish(QtWidgets.QApplication.instance())
def buttonFont(self):
return QtGui.QFont(self._buttonFont)
@QtCore.pyqtSlot(QtGui.QFont)
def setButtonFont(self, font):
if not isinstance(font, QtGui.QFont) or font == self._buttonFont:
return
self._buttonFont = font
self._buttonFontMetrics = QtGui.QFontMetrics(self._buttonFont)
self._polishApp()
def defaultButtonColor(self):
return QtGui.QColor(self._defaultButtonColor)
@QtCore.pyqtSlot(QtCore.Qt.GlobalColor)
@QtCore.pyqtSlot(QtGui.QColor)
def setDefaultButtonColor(self, color):
if isinstance(color, QtCore.Qt.GlobalColor):
color = QtGui.QColor(color)
elif not isinstance(color, QtGui.QColor):
return
self._defaultButtonColor = color
self._polishApp()
def alternateButtonColor(self):
return QtGui.QColor(self._alternateButtonColor)
@QtCore.pyqtSlot(QtCore.Qt.GlobalColor)
@QtCore.pyqtSlot(QtGui.QColor)
def setAlternateButtonColor(self, color):
if isinstance(color, QtCore.Qt.GlobalColor):
color = QtGui.QColor(color)
elif not isinstance(color, QtGui.QColor):
return
self._alternateButtonColor = color
self._polishApp()
def alternateRoles(self):
return self._alternateRoles
def setAlternateRoles(self, roles):
newRoles = set()
for role in roles:
if isinstance(role, QtWidgets.QDialogButtonBox.ButtonRole):
newRoles.add(role)
if newRoles != self._alternateRoles:
self._alternateRoles = newRoles
self._polishApp()
def setAlternateRole(self, role, activate=True):
if isinstance(role, QtWidgets.QDialogButtonBox.ButtonRole):
if activate and role in self._alternateRoles:
self._alternateRoles.add(role)
self._polishApp()
elif not activate and role not in self._alternateRoles:
self._alternateRoles.remove(role)
self._polishApp()
def defaultTextColor(self):
return QtGui.QColor(self._defaultTextColor)
@QtCore.pyqtSlot(QtCore.Qt.GlobalColor)
@QtCore.pyqtSlot(QtGui.QColor)
def setDefaultTextColor(self, color):
if isinstance(color, QtCore.Qt.GlobalColor):
color = QtGui.QColor(color)
elif not isinstance(color, QtGui.QColor):
return
self._defaultTextColor = color
self._polishApp()
def alternateTextColor(self):
return QtGui.QColor(self._alternateTextColor or self._defaultTextColor)
@QtCore.pyqtSlot(QtCore.Qt.GlobalColor)
@QtCore.pyqtSlot(QtGui.QColor)
def setAlternateTextColor(self, color):
if isinstance(color, QtCore.Qt.GlobalColor):
color = QtGui.QColor(color)
elif not isinstance(color, QtGui.QColor):
return
self._alternateTextColor = color
self._polishApp()
def drawControl(self, element, option, painter, widget):
if element == self.CE_PushButton:
isAlternate = False
if widget and isinstance(widget.parent(), QtWidgets.QDialogButtonBox):
role = widget.parent().buttonRole(widget)
if role in self._alternateRoles:
isAlternate = True
elif widget.property('alternateColor'):
isAlternate = True
if isAlternate:
color = self.alternateButtonColor()
textColor = self.alternateTextColor()
else:
color = self.defaultButtonColor()
textColor = self.defaultTextColor()
if not option.state & self.State_Enabled:
color.setAlpha(color.alpha() * .75)
textColor.setAlpha(textColor.alpha() * .75)
# switch the existing palette with a new one created from it;
# this shouldn't be necessary, but better safe than sorry
oldPalette = option.palette
palette = QtGui.QPalette(oldPalette)
palette.setColor(palette.ButtonText, textColor)
# some styles use WindowText for flat buttons
palette.setColor(palette.WindowText, textColor)
option.palette = palette
# colors that are almost black are not very affected by "lighter"
if color.value() < 32:
lightColor = QtGui.QColor(48, 48, 48, color.alpha())
else:
lightColor = color.lighter(115)
darkColor = color.darker()
if option.state & self.State_MouseOver:
# colors that are almost black are not very affected by "lighter"
bgColor = lightColor
lighterColor = lightColor.lighter(115)
darkerColor = darkColor.darker(115)
else:
bgColor = color
lighterColor = lightColor
darkerColor = darkColor
if option.state & self.State_Raised and not option.state & self.State_On:
topLeftPen = QtGui.QPen(lighterColor)
bottomRightPen = QtGui.QPen(darkerColor)
elif option.state & (self.State_On | self.State_Sunken):
if option.state & self.State_On:
bgColor = bgColor.darker()
else:
bgColor = bgColor.darker(125)
topLeftPen = QtGui.QPen(darkColor)
bottomRightPen = QtGui.QPen(lighterColor)
else:
topLeftPen = bottomRightPen = QtGui.QPen(bgColor)
painter.save()
painter.setRenderHints(painter.Antialiasing)
painter.translate(.5, .5)
rect = option.rect.adjusted(0, 0, -1, -1)
painter.setBrush(bgColor)
painter.setPen(QtCore.Qt.NoPen)
painter.drawRoundedRect(rect, 2, 2)
if topLeftPen != bottomRightPen:
roundRect = QtCore.QRectF(0, 0, 4, 4)
painter.setBrush(QtCore.Qt.NoBrush)
# the top and left borders
tlPath = QtGui.QPainterPath()
tlPath.arcMoveTo(roundRect.translated(0, rect.height() - 4), 225)
tlPath.arcTo(roundRect.translated(0, rect.height() - 4), 225, -45)
tlPath.arcTo(roundRect, 180, -90)
tlPath.arcTo(roundRect.translated(rect.width() - 4, 0), 90, -45)
painter.setPen(topLeftPen)
painter.drawPath(tlPath)
# the bottom and right borders
brPath = QtGui.QPainterPath(tlPath.currentPosition())
brPath.arcTo(roundRect.translated(rect.width() - 4, 0), 45, -45)
brPath.arcTo(
roundRect.translated(rect.width() - 4, rect.height() - 4), 0, -90)
brPath.arcTo(
roundRect.translated(0, rect.height() - 4), 270, -45)
painter.setPen(bottomRightPen)
painter.drawPath(brPath)
if option.state & self.State_HasFocus:
focusColor = QtGui.QColor(textColor).darker()
focusColor.setAlpha(focusColor.alpha() * .75)
painter.setPen(focusColor)
painter.setBrush(QtCore.Qt.NoBrush)
painter.drawRoundedRect(rect.adjusted(2, 2, -2, -2), 2, 2)
painter.setFont(self._buttonFont)
oldMetrics = option.fontMetrics
option.fontMetrics = self._buttonFontMetrics
self.drawControl(self.CE_PushButtonLabel, option, painter, widget)
painter.restore()
# restore the original font metrics and palette
option.fontMetrics = oldMetrics
option.palette = oldPalette
return
super().drawControl(element, option, painter, widget)
def sizeFromContents(self, contentsType, option, size, widget=None):
if contentsType == self.CT_PushButton:
if option.text:
textSize = option.fontMetrics.size(
QtCore.Qt.TextShowMnemonic, option.text)
baseWidth = size.width() - textSize.width()
baseHeight = size.height() - textSize.height()
text = option.text
else:
baseWidth = size.width()
baseHeight = size.height()
text = 'XXXX' if not option.icon else ''
buttonTextSize = self._buttonFontMetrics.size(
QtCore.Qt.TextShowMnemonic, text)
if not widget or widget.font() != QtWidgets.QApplication.font():
buttonTextSize = buttonTextSize.expandedTo(
QtWidgets.QApplication.fontMetrics().size(
QtCore.Qt.TextShowMnemonic, text))
margin = self.pixelMetric(self.PM_ButtonMargin, option, widget)
newSize = QtCore.QSize(
buttonTextSize.width() + baseWidth + margin * 2,
buttonTextSize.height() + baseHeight + margin)
return newSize.expandedTo(
super().sizeFromContents(contentsType, option, size, widget))
return super().sizeFromContents(contentsType, option, size, widget)
app = QtWidgets.QApplication(sys.argv)
app.setStyle(ColorButtonStyle())
# ...
Starting from this you could also add other properties to better control the style, such as the radius of the rounded rectangles (just replace every "4" with your variable in the border drawing, and draw the background using half of it):
painter.drawRoundedRect(rect, self._radius / 2, self._radius / 2)
if topLeftPen != bottomRightPen:
roundRect = QtCore.QRectF(0, 0, self._radius, self._radius)
painter.setBrush(QtCore.Qt.NoBrush)
# the top and left borders
tlPath = QtGui.QPainterPath()
tlPath.arcMoveTo(roundRect.translated(
0, rect.height() - self._radius), 225)
tlPath.arcTo(roundRect.translated(
0, rect.height() - self._radius), 225, -45)