pythonpyqtpyqt5compositeqframe

Composite Widget containig buttons, labels, etc. How to build such and how to use it


I am looking for most proper solution for creating composite widgets that I would be able to reuse in few places across application (graphical control interface with several submenus where some complex widgets would be reused).

Mainly I was trying to create custom QWidget, QLabel and QFrame objects that I would be using in several places. I have prepared such example:

class FansLabelsRight(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        print('is it working?')
        self.setGeometry(275,65,110,140)
        self.setStyleSheet("background: rgba(255,0,0,50)")
        #self.layout = QLayout(self)
        self.pane1 = QLabel(self)
        self.pane2 = QLabel(self)
        self.pane3 = QLabel(self)
        pixmap1 = QPixmap('rtC.png')
        pixmap2 = QPixmap('rtB.png')
        pixmap3 = QPixmap('rtA.png')
        self.pane1.setPixmap(pixmap1)
        self.pane2.setPixmap(pixmap2)
        self.pane3.setPixmap(pixmap3)
        #self.layout.addWidget(self.pane1)
        #self.layout.addWidget(self.pane2)
        #self.layout.addWidget(self.pane3)
        self.pane1.setGeometry(30,10,pixmap1.width(), pixmap1.height())
        self.pane2.setGeometry(35,30,pixmap2.width(), pixmap2.height())
        self.pane3.setGeometry(40,30,pixmap3.width(), pixmap3.height())
        self.setLayout(self.layout)

        self.show()

It did not worked. As well as few other approach that I tested without positive results expect one that I build based on Q tDesigner - creating custom object which actually add to my QMainWindow class other widgets but it is not what I expected and what I read that should be possible. Other words I think of having container with subwidgets. Is it actually possible?

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        self.label = QLabel(Form)
        self.label.setGeometry(QtCore.QRect(60, 10, 61, 81))
        self.label.setPixmap(QPixmap("rtA.png"))
        self.label.setObjectName("label")
        self.label_2 = QLabel(Form)
        self.label_2.setGeometry(QtCore.QRect(70, 30, 51, 111))
        self.label_2.setPixmap(QPixmap("rtB.png"))
        self.label_2.setObjectName("label_2")
        self.label_3 = QLabel(Form)
        self.label_3.setGeometry(QtCore.QRect(60, 90, 47, 61))
        self.label_3.setPixmap(QPixmap("rtC.png"))
        self.label_3.setObjectName("label_3")
        self.pushButton = QPushButton(Form)
        self.pushButton.setGeometry(QtCore.QRect(0, 0, 75, 61))
        self.pushButton.setFlat(True)
        self.pushButton.setObjectName("pushButton")
        self.pushButton_2 = QPushButton(Form)
        self.pushButton_2.setGeometry(QtCore.QRect(0, 100, 75, 61))
        self.pushButton_2.setFlat(True)
        self.pushButton_2.setObjectName("pushButton_2")

        QtCore.QMetaObject.connectSlotsByName(Form)

Could you please suggest me how to achieve that? How to create own widget that would be build from several object and will keep whole logic between them inside its class? (As logic I understand that pressing one button will show another label and other button will decrees, etc...)

One of problem that I had was Layouts, which did not let me place these labels where I wanted them.

And here is sample of how should it looks like.


Solution

  • Doing what you want with the basic widgets Qt provides is possible, but there are some drawbacks and considerations to keep in mind.

    I suppose that the three images you want to show are those "level" shapes on the right, and if that's the case you can't use a layout because their geometries overlap: for a widget of this type and look, you have to "embed" the child widgets without assigning them to a layout.

    Then, if you want to get those semicircular arrows to react as buttons, obviously you can't use basic QPushButtons, as they have a rectangular shape. A solution would be to subclass, override the paintEvent (to provide custom drawing of the button using your image) and then use the image to create a mask for the widget, so that click events will only happen within its boundaries.

    This is a basic example of what it could be:

    class MyButton(QtWidgets.QPushButton):
        def __init__(self, image, parent):
            super().__init__(parent)
            self.image = QtGui.QPixmap(image)
            self.setFixedSize(self.image.size())
            self.setMask(self.image.createHeuristicMask())
    
        def paintEvent(self, event):
            qp = QtGui.QPainter(self)
            qp.drawPixmap(QtCore.QPoint(), self.image)
    
    
    class MyWidget(QtWidgets.QWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setFixedSize(110, 140)
            self.upButton = MyButton('uparrow.png', self)
            self.downButton = MyButton('downarrow.png', self)
            self.downButton.move(0, self.height() - self.downButton.height())
    
            self.rtcA = QtWidgets.QLabel(self)
            self.rtcA.setPixmap(QtGui.QPixmap('rtcA.png'))
    
            self.rtcB = QtWidgets.QLabel(self)
            self.rtcB.setPixmap(QtGui.QPixmap('rtcB.png'))
    
            self.rtcC = QtWidgets.QLabel(self)
            self.rtcC.setPixmap(QtGui.QPixmap('rtcC.png'))
    
            for label in self.rtcA, self.rtcB, self.rtcC:
                label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
    
            self.upButton.clicked.connect(self.up)
            self.downButton.clicked.connect(self.down)
    
            p = self.palette()
            p.setColor(p.Window, QtGui.QColor(57, 57, 57))
            self.setPalette(p)
    
        def up(self):
            print('going up')
    
        def down(self):
            print('going down')
    

    And here's how it looks:

    screenshot of the running example

    As you can see, I didn't use any layout: you can add children to a "container" widget just by adding that widget as a parent argument; this make those children appear inside the container, and they can be freely moved within the parent boundaries.

    Some small notes.
    For the 3 indicators I used images that already "translate" the actual image contents (meaning that they are placed on the top left corner of the widget). This is to ensure they are correctly aligned and to avoid continuous trial and error tests to get the position on the widget; for example, the third image is actually this:

    image with alpha channel shown

    To make this work as expected, I had to make those widgets "transparent for mouse events": since they are added after the buttons, they would be on top of them, and the buttons wouldn't receive any mouse event because the labels will be on the way. Another possibility is to add the buttons after the labels, or, otherwise, use raise_() or lower():

        self.upButton.raise_()
        self.downButton.raise_()
        # or, alternatively
        for label in self.rtcA, self.rtcB, self.rtcC:
            label.lower()
    

    As a further possibility, you could create "mask images" for each label (with a solid-non-alpha background around the boundaries of each section) and use setMask(QtGui.QPixmap('rtcA_mask.png').createHeuristicMask()) similarly to what I did for the buttons. The benefit of this approach is that mouse events can be correctly received from those labels, if you ever need them.