PYQT6 multiple Buttons Layout

262 views Asked by At

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())
1

There are 1 answers

0
musicamante On BEST ANSWER

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:

  • setting the geometry of the label is useless, since it's being added to a layout manager; you should also get the actual height from the scaled pixmap (the fact that it may be the same is irrelevant: the point is consistency);
  • the geometry_points are not consistent with their polygons, because your usage of setGeometry() is wrong: if you look at the first image in this post, you'll see that the actual bounding rectangle of each polygon has a big offset on its top and left corners; the setGeometry() override that accepts coordinates uses the size (width and height) as third and fourth arguments, while you're using actual points of the bottom right corner; in reality, those polygons should always be aligned to the origin point of the local widget coordinates (aka: 0x0) and then you should set the geometry based on the bounding rectangle of the widget;
  • your usage of QPoints and lists of them causes extreme verbosity and is also inconsistent; you don't need performance there, so use existing Qt functions and classes instead (which are quite fast, actually): QPointF and QPolygonF; then eventually transform them using toPoint(), toPolygon() or boundingRect() and toRect(); also, QPolygon and QPolygonF already provide constructors for QPoint/QPointF iterables;

Other issues:

  • you should never overwrite existing functions of a class for a purpose that is not the same as the default implementation; geometry() is an existing and quite important function of all QWidgets, and while it's not a virtual (so, your override will be ignored by Qt), overwriting it is wrong anyway;
  • the convention on buttons is that they are clicked when the mouse button is released within their geometry, not right after being pressed;
  • you're not checking which mouse button causes the press event;
  • if an event is not handled, it should always be marked as ignored;
  • you shall never set generic stylesheet properties on parent/container widgets (like you did in MainWindow); selector types should always be used for such widgets;
  • all those repeated show/hide calls are unnecessary: that's just boilerplate you could have been avoided by using a dedicated function; besides, if you intend to switch between "pages", use QStackedWidget or put the buttons in a further container widget (QGridLayout allows adding items to already occupied "cells");

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).