layoutpyqt6qpushbutton

PYQT6 multiple Buttons Layout


I can't have multiple clickable buttons

I am trying to design a GUI where I have a lot of buttons with all sorts of geometry. They are placed in all directions and not in a clear grid. I can't really use the QHBoxlayout/ QVBoxlayout. I just want to show an image when I click a button. Depending on the order in which I define my button, some work and others don't. In this case, they are all working beside "self.button_R_1". Why is that so and what can I do to fix it?

PS: currently I only have a few buttons but in the end, I need to define about 30 in no apparent pattern.

import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout, QPushButton, QGridLayout, QVBoxLayout, QStackedLayout
from PyQt6.QtGui import QPixmap, QPainter, QPolygon, QColor
from PyQt6.QtCore import Qt, QPoint, pyqtSignal

# Hier definierst du die Klasse für den angepassten Button
class CustomButton(QPushButton):
    buttonClicked = pyqtSignal()

    def __init__(self, text, geometry_points, parent=None):
        super().__init__(text, parent)
        self.geometry_points = geometry_points
    
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        
        # Button-Hintergrund zeichnen
        painter.setBrush(QColor(255, 255, 255))
        painter.setPen(Qt.GlobalColor.black)
        painter.drawPolygon(self.geometry())
        
        # Text zeichnen
        painter.setPen(Qt.GlobalColor.black)
        text_rect = self.geometry().boundingRect()
        painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self.text())

    def geometry(self):
        polygon = QPolygon()
        for point in self.geometry_points:
            polygon.append(QPoint(int(point.x()), int(point.y())))
        return polygon
    
    def mousePressEvent(self, event):
        if self.geometry().containsPoint(event.pos(), Qt.FillRule.WindingFill):
            self.buttonClicked.emit()

# Hier definierst du das Hauptfenster
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("Hintergrundbild anpassen")
        
        # Farbe des Hintergrunds
        hex_code = "#7F9EA9"
        self.setStyleSheet(f"background-color: {hex_code};")
        
        # Hintergrundbild laden
        image_path = "C:/Users/paull/Google Drive /HIWI/Arbeit/V_Modell_6.png"  
        pixmap = QPixmap(image_path)
        
        # Fenstergröße an Bildschirmgröße anpassen
        screen = QApplication.primaryScreen()
        screen_size = screen.availableGeometry().size()
        self.resize(screen_size)
        
        # Bild skalieren und herauszoomen
        scale_factor = 0.85  # Anpassen des Zoom-Faktors
        ratio_pixmap = 0.5626953125
        self.original_scaled_width = int(screen_size.width() * scale_factor)
        self.original_scaled_height = int(self.original_scaled_width * ratio_pixmap)
        self.scaled_pixmap = pixmap.scaledToWidth(self.original_scaled_width, Qt.TransformationMode.SmoothTransformation)

        # Label erstellen und Bild setzen
        self.label = QLabel(self)
        self.label.setPixmap(self.scaled_pixmap)
        self.label.setGeometry(0, 0, self.original_scaled_width, self.original_scaled_height)

        # Button-Layout erstellen
        button_layout = QGridLayout()
        button_layout.addWidget(self.label)
        
        # Button-Container erstellen
        button_container = QWidget()
        button_container.setLayout(button_layout)

        #Faktor skalierung
        self.faktor = self.original_scaled_width / 5120

        #Buttons definieren
        Faktor_Abweichung_x_R1 = 237 / 1440
        Faktor_Abweichung_y_R1 = 85 / 926
        Abweichung_x_R1 = self.original_scaled_width * Faktor_Abweichung_x_R1
        Abweichung_y_R1 = self.original_scaled_height * Faktor_Abweichung_y_R1
        geometry_points_R_1 = [
            QPoint(int(round(1711*self.faktor-Abweichung_x_R1)), int(round(553*self.faktor-Abweichung_y_R1))),
            QPoint(int(round(1875*self.faktor-Abweichung_x_R1)), int(round(553*self.faktor-Abweichung_y_R1))),
            QPoint(int(round(2135*self.faktor-Abweichung_x_R1)), int(round(1588*self.faktor-Abweichung_y_R1))),
            QPoint(int(round(1972*self.faktor-Abweichung_x_R1)), int(round(1588*self.faktor-Abweichung_y_R1)))
        ]
        self.button_R_1 = CustomButton("", geometry_points_R_1, button_container)
        self.button_R_1.buttonClicked.connect(self.handler)
        self.button_R_1.setGeometry(geometry_points_R_1[0].x(), geometry_points_R_1[0].y(), geometry_points_R_1[2].x(), geometry_points_R_1[2].y())
        
        Faktor_Abweichung_x_F1 = 220 / 1440
        Faktor_Abweichung_y_F1 = 117 / 926
        Abweichung_x_F1 = self.original_scaled_width * Faktor_Abweichung_x_F1
        Abweichung_y_F1 = self.original_scaled_height * Faktor_Abweichung_y_F1
        geometry_points_F_1 = [
            QPoint(int(round(1594*self.faktor-Abweichung_x_F1)), int(round(755*self.faktor-Abweichung_y_F1))),
            QPoint(int(round(1760*self.faktor-Abweichung_x_F1)), int(round(755*self.faktor-Abweichung_y_F1))),
            QPoint(int(round(1971*self.faktor-Abweichung_x_F1)), int(round(1588*self.faktor-Abweichung_y_F1))),
            QPoint(int(round(1803*self.faktor-Abweichung_x_F1)), int(round(1588*self.faktor-Abweichung_y_F1)))
        ]
        self.button_F_1 = CustomButton("", geometry_points_F_1, button_container)
        self.button_F_1.buttonClicked.connect(self.handler)
        self.button_F_1.setGeometry(geometry_points_F_1[0].x(), geometry_points_F_1[0].y(), geometry_points_F_1[2].x(), geometry_points_F_1[2].y())


        Faktor_Abweichung_x_F0 = 165 / 1440
        Faktor_Abweichung_y_F0 = 84 / 926
        Abweichung_x_F0 = self.original_scaled_width * Faktor_Abweichung_x_F0
        Abweichung_y_F0 = self.original_scaled_height * Faktor_Abweichung_y_F0
        geometry_points_F_0 = [
            QPoint(int(round(1200*self.faktor-Abweichung_x_F0)), int(round(554*self.faktor-Abweichung_y_F0))),
            QPoint(int(round(1710*self.faktor-Abweichung_x_F0)), int(round(554*self.faktor-Abweichung_y_F0))),
            QPoint(int(round(1760*self.faktor-Abweichung_x_F0)), int(round(755*self.faktor-Abweichung_y_F0))),
            QPoint(int(round(1250*self.faktor-Abweichung_x_F0)), int(round(755*self.faktor-Abweichung_y_F0)))
        ]
        self.button_F_0 = CustomButton("", geometry_points_F_0, button_container)
        self.button_F_0.buttonClicked.connect(self.handler)
        self.button_F_0.setGeometry(geometry_points_F_0[0].x(), geometry_points_F_0[0].y(), geometry_points_F_0[2].x(), geometry_points_F_0[2].y())
        
        Faktor_Abweichung_x_R0 = 175 / 1632
        Faktor_Abweichung_y_R0 = 38/ 918
        Abweichung_x_R0 = self.original_scaled_width * Faktor_Abweichung_x_R0
        Abweichung_y_R0 = self.original_scaled_height * Faktor_Abweichung_y_R0
        geometry_points_R_0 = [
            QPoint(int(round(1127*self.faktor-Abweichung_x_R0)), int(round(263*self.faktor-Abweichung_y_R0))),
            QPoint(int(round(1803*self.faktor-Abweichung_x_R0)), int(round(263*self.faktor-Abweichung_y_R0))),
            QPoint(int(round(1875*self.faktor-Abweichung_x_R0)), int(round(553*self.faktor-Abweichung_y_R0))),
            QPoint(int(round(1199*self.faktor-Abweichung_x_R0)), int(round(553*self.faktor-Abweichung_y_R0)))
        ]
        self.button_R_0 = CustomButton("", geometry_points_R_0, button_container)
        self.button_R_0.buttonClicked.connect(self.handler)
        self.button_R_0.setGeometry(geometry_points_R_0[0].x(), geometry_points_R_0[0].y(), geometry_points_R_0[2].x(), geometry_points_R_0[2].y())

        Faktor_Abweichung_x = 106 / 1632
        Faktor_Abweichung_y = 9 / 918
        Abweichung_x = self.original_scaled_width * Faktor_Abweichung_x
        Abweichung_y = self.original_scaled_height * Faktor_Abweichung_y
        geometry_points_Projektinitialisierung = [
            QPoint(int(round(695*self.faktor-Abweichung_x)), int(round(84*self.faktor-Abweichung_y))),
            QPoint(int(round(1757*self.faktor-Abweichung_x)), int(round(84*self.faktor-Abweichung_y))),
            QPoint(int(round(1799*self.faktor-Abweichung_x)), int(round(252*self.faktor-Abweichung_y))),
            QPoint(int(round(736*self.faktor-Abweichung_x)), int(round(252*self.faktor-Abweichung_y)))
        ]
        self.button_Projektinitialisierung = CustomButton("", geometry_points_Projektinitialisierung, button_container)
        self.button_Projektinitialisierung.buttonClicked.connect(self.handler)
        self.button_Projektinitialisierung.setGeometry(geometry_points_Projektinitialisierung[0].x(), geometry_points_Projektinitialisierung[0].y(), geometry_points_Projektinitialisierung[2].x(), geometry_points_Projektinitialisierung[2].y())

        # Zurück-Button
        self.button_zurueck = QPushButton("Zurück", button_container)
        self.button_zurueck.clicked.connect(self.backButtonClicked)
        self.button_zurueck.hide()  # Anfangs ausblenden

        # Hauptlayout erstellen
        main_layout = QHBoxLayout()
        main_layout.addWidget(button_container)
        main_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setLayout(main_layout)

    def handler(self):
        sender = self.sender()
        if sender == self.button_R_0 or sender == self.button_R_1:
            image_path = "C:/Users/paull/Google Drive /HIWI/Arbeit/R.png"
            pixmap = QPixmap(image_path)
            self.label.setPixmap(pixmap)
            self.button_Projektinitialisierung.hide()
            self.button_R_0.hide()
            self.button_R_1.hide()
            self.button_F_0.hide()
            self.button_F_1.hide()
            self.button_zurueck.show()
            print("R clicked")
        elif sender == self.button_Projektinitialisierung:
            image_path = "C:/Users/paull/Google Drive /HIWI/Arbeit/Projektinitialisierung.png"
            pixmap = QPixmap(image_path)
            self.label.setPixmap(pixmap)
            self.button_Projektinitialisierung.hide()
            self.button_R_0.hide()
            self.button_R_1.hide()
            self.button_F_0.hide()
            self.button_F_1.hide()
            self.button_zurueck.show()
            print("pro clicked")
        elif sender == self.button_F_0 or sender == self.button_F_1:
            image_path = "C:/Users/paull/Google Drive /HIWI/Arbeit/F.png"
            pixmap = QPixmap(image_path)
            self.label.setPixmap(pixmap)
            self.button_Projektinitialisierung.hide()
            self.button_R_0.hide()
            self.button_R_1.hide()
            self.button_F_0.hide()
            self.button_F_1.hide()
            self.button_zurueck.show()
            print("F clicked")

    def backButtonClicked(self):
        # Alle Buttons verbergen
        self.button_zurueck.hide()

        # Bild im gleichen Fenster anzeigen
        self.label.setPixmap(self.scaled_pixmap)
        self.label.setScaledContents(True)

        # Button anzeigen
        self.button_Projektinitialisierung.show()
        self.button_R_0.show()
        self.button_R_1.show()
        self.button_F_0.show()
        self.button_F_1.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec())

Solution

  • Your code has various issues, but the most important problem is that overlapping widgets don't allow mouse events to "pass through".

    Widgets always have a rectangular shape, and while you are using non overlapping polygons, their actual geometries do overlap.

    Add this simple line right after creating the QPainter:

    painter.drawRect(self.rect())
    

    Then you will see the following result:

    Screenshot of overlapping shapes

    Note that I also painted the button number to clarify things.

    Since widgets are always stacked in the order they are added to the parent, the button 1 is almost completely "hidden" to mouse events by the rectangles of buttons 2 and 3. You can see that by the fact that the rectangle of button 2 is drawn above that of button 1, but it's hidden by button 4.
    The problem does not exist between button 2 and 3 because the latter is added after the former, so there is no conflict.

    Whenever an input event is not handled by a widget, it's always directly propagated to the parent, completely ignoring any "sibling". You're not getting those mouse events because they are never received.

    A possible solution is to use the setMask() function; you could add the following within the __init__ of the button:

    self.setMask(QRegion(QPolygon(geometry_points)))
    

    Unfortunately, masks are pixel based (they fundamentally are 1-bit images), so the result is quite ugly due to clipping:

    Screenshot while using masks

    In order to work around this, and assuming that all the shapes are not self intersecting, a solution is to expand the polygon by using a QPainterPath along with QPainterPathStroker, which creates a new QPainterPath using the outline of a given QPen.

    With a pen width of 2, there are enough margins to ensure that edges are always properly shown within the mask, even if the mask exceeds the polygon shape. The only problem with this is that clicking right next to the edge of a "button" may be ignored if the other one is put under it in the stacking order, or the other one could be triggered if it's put above it.

    class CustomButton(QLabel):
        buttonClicked = pyqtSignal()
    
        def __init__(self, text, geometry_points, parent=None):
            super().__init__(text, parent)
            self.geometry_points = geometry_points
    
            # create a *closed* polygon (see the repeated first point)
            poly = QPolygon(geometry_points + [geometry_points[0]])
    
            # add the polygon to a QPainterPath
            path = QPainterPath()
            path.addPolygon(QPolygonF(poly))
    
            # create a stroker and a new path based on it
            stroker = QPainterPathStroker(QPen(Qt.GlobalColor.black, 2))
            outline = stroker.createStroke(path)
    
            # iterate through the new "outline" subpaths and construct
            # a *merged* region that will be used as mask
            mask = QRegion()
            for subPath in outline.toSubpathPolygons(QTransform()):
                mask |= QRegion(subPath.toPolygon())
            self.setMask(mask)
    
        ...
    

    Note that I changed the base class to QLabel for simplicity, to allow simple access to the text() property you're using while painting. In reality, your inheritance from QPushButton is completely useless: not only you're overriding both painting and mouse event handling, but you're not even using the existing clicked signal. Just subclass from a plain QWidget and store the text in an instance attribute.

    As said, there are other issues in your code.

    The geometry approach is faulty:

    Other issues:


    Finally, using intersecting and non rectangular shapes for graphical interfaces is almost always an issue when dealing with standard widgets: that's not their intended purpose.
    A more appropriate solution would be to use the Graphics View Framework instead: that API is a bit complex and advanced, but it actually allows simpler and smarter code when dealing with these situations.

    For example, you could subclass from QGraphicsObject and add a QGraphicsPolygonItem as its child; you only need to override its boundingRect() (to return childrenBoundingRect()), paint() (with just a standard pass to ignore it) and the mouse event handlers to emit an eventual signal when required; also look into the shape() function so that proper collision detection and mouse interaction is used (by default, shape() uses the bounding rectangle of the item).
    The QGraphicsObject inheritance is necessary to provide QObject capabilities (and you need it if you want to provide signals).