idkman
idkman

Reputation: 169

How to set graphics for a push button using Qt Designer?

I created a pushButton with a border image using Qt Designer. The stylesheet of the pushButton looks like the following.

border-image: transparent;
border-image: url(:/button_img/main_button.png);

The button looks like as follows:

enter image description here

The button works as expected but I do not know how can I set border inset or onset for that button. My terminologies might be wrong because I am very new to GUI development. What I am looking for is when you creata a normal pushButton, you can visualy see when you click that button. But in my case the button works but I cannot see the graphics when I click the button.

How can I add the border inset or onset? Please let me know in case of additional information.

Upvotes: 0

Views: 1519

Answers (1)

musicamante
musicamante

Reputation: 48374

No, you can't; not like that.

As the name hints, the border-image CSS property is used to draw borders; while what you see seems like a "background image", it actually is a border object that splits your image in a 3x3 grid rectangle that covers the whole button area; each section is then "stretched" to fill its own rectangle.

+-----------+---------+------------+
| top left  |   top   | top right  |
+-----------+---------+------------+
|   left    |  center |   right    |
+-----------+---------+------------+
|bottom left|  bottom |bottom right|
+-----------+---------+------------+

To understand how all of this works, read the "Common Mistakes" section of the stylesheet examples documentation. Here's what actually happens (I'm using half-width/half-height margins for the sake of the argument, so only the 4 "angle" rectangles are shown, so the left, top, right, bottom and center rectangles will be null):

border-image issue screenshot

If you want to use style sheets that interactively show borders, the standard approach is have 2 different images for the button state, and both of them needs their border. If you also want to achieve the "hover" effect, you'll need a third image that will have no border at all.

The downside of this method is that if the icon is resized, the border will be resized too.

The only alternative I can think of is to subclass your own QPushButton widget and override its paintEvent method. You could also use an event filter, but that wouldn't give you any major benefit, as it would use a very similar approach anyway, while complicating things if the object tree is even slightly complex.

In the following example I use an implementation that is based on stylesheets only: Qt can use the qproperty-* stylesheet parameter to set existing properties of a class (Qt properties that are already part of the class, or that are added in the class constructor: using setProperty on an unexisting property within the __init__ method will not work). While this kind of implementation is usually not required (you could set the pixmap attribute of the class instance by hand) the advantage is that with this approach you can set everything within the stylesheet only.

class PixmapButton(QtWidgets.QPushButton):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._pixmap = QtGui.QPixmap()
        self.setCheckable(True)
        self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)

    @QtCore.pyqtProperty(QtGui.QPixmap)
    def pixmap(self):
        return self._pixmap

    @pixmap.setter
    def pixmap(self, pixmapPath):
        self._pixmap = QtGui.QPixmap(pixmapPath)

    def paintEvent(self, event):
        opt = QtWidgets.QStyleOptionButton()
        self.initStyleOption(opt)
        qp = QtGui.QPainter(self)
        # draw the basic button
        self.style().drawControl(QtWidgets.QStyle.CE_PushButton, opt, qp, self)
        if self._pixmap.isNull():
            return

        # get the minimum possible size for the pixmap icon
        minSize = min(self.width(), self.height())
        # remove the margin/padding usually necessary for the button contents
        minSize -= self.style().pixelMetric(QtWidgets.QStyle.PM_ButtonMargin, opt, self) * 2

        # create a rectangle based on the minimal size and move it to the center
        # of the button
        rect = QtCore.QRect(0, 0, minSize, minSize)
        rect.moveCenter(self.rect().center())

        # finally, draw the "icon"
        qp.drawPixmap(rect, self._pixmap.scaled(rect.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))

Here's a test window that will show the border-image implementation on top (along with its border splitting), and the subclass painting at the bottom.

class Test(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QGridLayout(self)

        layout.addWidget(QtWidgets.QLabel('basic border-image'), 0, 0, QtCore.Qt.AlignCenter)
        borderButton = QtWidgets.QPushButton()
        layout.addWidget(borderButton, 1, 0, QtCore.Qt.AlignCenter)
        borderButton.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        borderButton.setFixedSize(120, 120)
        borderButton.setStyleSheet('''
            border-image: url(play.png);
        ''')

        layout.addItem(QtWidgets.QSpacerItem(120, 0, QtWidgets.QSizePolicy.Expanding))
        layout.addWidget(QtWidgets.QLabel('expanded border-image'), 0, 1, QtCore.Qt.AlignCenter)
        borderButtonExpanded = QtWidgets.QPushButton()
        layout.addWidget(borderButtonExpanded, 1, 1)
        borderButtonExpanded.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        borderButtonExpanded.setStyleSheet('''
            border-image: url(play.png) 60 60 60 60;
            border-top: 60px transparent;
            border-bottom: 60px transparent;
            border-right: 60px transparent;
            border-left: 60px transparent;
        ''')

        layout.addItem(QtWidgets.QSpacerItem(0, 10))
        layout.addWidget(QtWidgets.QLabel('paintEvent implementation'), 2, 0, 1, 2, QtCore.Qt.AlignCenter)
        pmButton = PixmapButton()
        layout.addWidget(pmButton, 3, 0, 1, 2)
        pmButton.setMinimumSize(120, 120)
        pmButton.setStyleSheet('''
            QPushButton {
                qproperty-pixmap: url(play.png);
                /* hover mode: while there's no border shown, we can set the 
                default radius for the hover and pressed statuses */
                border: none;
                border-radius: 4px;
            }
            /* show border only if hovered or is checkable */
            QPushButton:hover, QPushButton[checkable="true"] {
                border: 1px outset green;
            }
            QPushButton:pressed, QPushButton:checked {
                border: 1px inset green;
            }
        ''')

        pmButton2 = PixmapButton()
        layout.addWidget(pmButton2, 4, 0, 1, 2)
        pmButton2.setCheckable(True)
        pmButton2.setMinimumSize(120, 120)
        pmButton2.setStyleSheet(pmButton.styleSheet())

Note: This is a very simplified implementation. It does not check if the user sets a custom icon for the button, and there should be no button text at all.

Upvotes: 2

Related Questions