PyQt6 Bauplan Fenster

Teil 5 – Grundlagen: Menüleiste & Dialog About. Endgültige Version!

Quadratische Gleichung als nutzbare Anwendung

Zurück

Diese Webseite stellt für den Quellcode Download- und Kopierfunktionen zur Verfügung. Das geht nur mit JavaScript. Wenn du mich für einen unsicheren Kantonisten hältst, verlasse diese Webseite. Alle Informationen und Angebote sind ohne Gewähr!


Python Code
#!/usr/bin/env python3 # ***************************************************************************** # Quadratische Gleichung. PyQt6-Fenster zur Lösung von a*x^2 + b*x + c = 0. # Enthält Eingabefelder, Styling, ein Fenster-Icon, Zentrierung auf dem # Bildschirm, einen eingebetteten Matplotlib-Plot sowie eine Menüleiste mit # einem "Über"-Dialog inkl. Link. # Installation der Abhängigkeit: pip install PyQt6 matplotlib # Start: python quadratische_gleichung.py (c) Hans Busche Datum: 23.07.2026 # ***************************************************************************** import sys import math import os import numpy as np from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QFrame, QDialog, QDialogButtonBox, ) from PyQt6.QtGui import QIcon, QAction from PyQt6.QtCore import Qt from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure # Verzeichnis dieser Datei, damit das Icon unabhängig vom Startort gefunden wird BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ICON_PATH = os.path.join(BASE_DIR, "icon.png") class QuadraticSolver(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Quadratische Gleichung") # --- Icon --- if os.path.exists(ICON_PATH): self.setWindowIcon(QIcon(ICON_PATH)) # --- Feste Startgröße --- self.resize(520, 560) # --- Größenbeschränkungen --- # Mindestgröße: darunter würden Eingabefelder, Button und Plot zu eng werden self.setMinimumSize(460, 500) # Maximalgröße: großzügiger als in Phase 2, da der Plot von mehr Fläche profitiert, # aber weiterhin begrenzt, damit die Formularelemente nicht unproportioniert wirken self.setMaximumSize(900, 850) # --- Eingabefelder für a, b, c --- self.input_a = QLineEdit() self.input_a.setPlaceholderText("a") self.input_a.setObjectName("coeffInput") self.input_b = QLineEdit() self.input_b.setPlaceholderText("b") self.input_b.setObjectName("coeffInput") self.input_c = QLineEdit() self.input_c.setPlaceholderText("c") self.input_c.setObjectName("coeffInput") # Formel-Labels bekommen einen eigenen Objektnamen, # damit sie sich optisch von der Ergebnis-Anzeige unterscheiden formula_label_1 = QLabel("x² +") formula_label_1.setObjectName("formulaLabel") formula_label_2 = QLabel("x +") formula_label_2.setObjectName("formulaLabel") formula_label_3 = QLabel("= 0") formula_label_3.setObjectName("formulaLabel") # Zeile mit den drei Eingabefeldern nebeneinander, # dazwischen kleine Labels zur Lesbarkeit der Formel input_row = QHBoxLayout() input_row.addWidget(self.input_a) input_row.addWidget(formula_label_1) input_row.addWidget(self.input_b) input_row.addWidget(formula_label_2) input_row.addWidget(self.input_c) input_row.addWidget(formula_label_3) # Rahmen um die Eingabezeile, damit die Formel optisch als # zusammengehörige Einheit erkennbar ist input_frame = QFrame() input_frame.setObjectName("inputFrame") input_frame.setLayout(input_row) # --- Button --- self.button_calc = QPushButton("Berechnen") self.button_calc.setObjectName("calcButton") self.button_calc.clicked.connect(self.calculate) self.button_calc.setCursor(Qt.CursorShape.PointingHandCursor) # --- Ergebnis-Anzeige --- self.result_label = QLabel("") self.result_label.setObjectName("resultLabel") self.result_label.setWordWrap(True) # --- Matplotlib-Plotbereich --- # figsize/dpi sind Startwerte; Qt skaliert das Canvas-Widget innerhalb # des Layouts ohnehin flexibel mit self.figure = Figure(figsize=(4.5, 3.2), dpi=100) self.canvas = FigureCanvas(self.figure) self.canvas.setMinimumHeight(260) self.ax = self.figure.add_subplot(111) self.draw_empty_plot() # --- Gesamtlayout (senkrecht gestapelt) --- main_layout = QVBoxLayout() main_layout.addWidget(input_frame) main_layout.addWidget(self.button_calc) main_layout.addWidget(self.result_label) main_layout.addWidget(self.canvas) main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(14) # QMainWindow kann kein Layout direkt per setLayout() erhalten – # es braucht immer ein zentrales Widget, das den Inhalt trägt central_widget = QWidget() central_widget.setLayout(main_layout) self.setCentralWidget(central_widget) self.apply_styles() self.build_menu() # Fenster mittig auf dem Bildschirm positionieren self.center_on_screen() def build_menu(self): """Erstellt die Menüleiste mit 'Datei' (Beenden) und 'Hilfe' (Über).""" menu_bar = self.menuBar() # --- Menü "Datei" --- file_menu = menu_bar.addMenu("&Datei") exit_action = QAction("Beenden", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # --- Menü "Hilfe" --- help_menu = menu_bar.addMenu("&Hilfe") about_action = QAction("Über …", self) about_action.triggered.connect(self.show_about_dialog) help_menu.addAction(about_action) def show_about_dialog(self): """Zeigt einen 'Über'-Dialog mit Beschreibung, Version und einem klickbaren Link.""" dialog = QDialog(self) dialog.setWindowTitle("Über – Quadratische Gleichung") dialog.setFixedSize(360, 220) if os.path.exists(ICON_PATH): dialog.setWindowIcon(QIcon(ICON_PATH)) info_label = QLabel( "

Quadratische Gleichung

" "

Version 1.0 · erstellt mit PyQt6 und Matplotlib

" "

Löst Gleichungen der Form a·x² + b·x + c = 0 " "und stellt Nullstellen sowie Scheitelpunkt grafisch dar.

" "

Weitere Infos zur Formel gibt es hier: " 'Quadratische Gleichung – Wikipedia

' ) # setOpenExternalLinks sorgt dafür, dass ein Klick auf den Link # automatisch im Standard-Browser des Systems geöffnet wird info_label.setOpenExternalLinks(True) info_label.setWordWrap(True) info_label.setTextFormat(Qt.TextFormat.RichText) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) button_box.accepted.connect(dialog.accept) dialog_layout = QVBoxLayout() dialog_layout.addWidget(info_label) dialog_layout.addStretch() dialog_layout.addWidget(button_box) dialog.setLayout(dialog_layout) # Eigenes Stylesheet, da QDialog als eigenständiges Top-Level-Fenster # das Stylesheet des Hauptfensters NICHT automatisch erbt dialog.setStyleSheet(""" QDialog { background-color: #f4f6fb; } QLabel { color: #1f2937; font-size: 13px; } QPushButton { background-color: #2962bd; color: #ffffff; border: none; border-radius: 6px; padding: 6px 18px; font-weight: 600; } QPushButton:hover { background-color: #2650a0; } """) dialog.exec() def apply_styles(self): """Definiert das Aussehen aller Widgets über ein zentrales QSS-Stylesheet.""" self.setStyleSheet(""" /* Fensterhintergrund */ QWidget { background-color: #f4f6fb; font-family: "Segoe UI", Arial, sans-serif; font-size: 14px; } /* Rahmen um die Eingabezeile */ QFrame#inputFrame { background-color: #ffffff; border: 1px solid #d0d7e6; border-radius: 10px; padding: 10px; } /* Eingabefelder a, b, c */ QLineEdit#coeffInput { background-color: #ffffff; border: 2px solid #c7cede; border-radius: 6px; padding: 6px 8px; min-width: 50px; max-width: 70px; font-weight: 600; color: #1f2937; } QLineEdit#coeffInput:hover { border-color: #93a3c4; } QLineEdit#coeffInput:focus { border-color: #2962bd; background-color: #eef3fc; } /* Formel-Labels (x² +, x +, = 0) */ QLabel#formulaLabel { color: #3a4257; font-weight: 600; background: transparent; } /* Berechnen-Button */ QPushButton#calcButton { background-color: #2962bd; color: #ffffff; border: none; border-radius: 8px; padding: 10px 16px; font-weight: 600; } QPushButton#calcButton:hover { background-color: #2650a0; } QPushButton#calcButton:pressed { background-color: #1e3f80; } /* Ergebnis-Anzeige */ QLabel#resultLabel { background: transparent; color: #1f2937; font-size: 14px; padding-top: 4px; } /* Menüleiste */ QMenuBar { background-color: #f4f6fb; color: #1f2937; padding: 4px; } QMenuBar::item { padding: 6px 12px; background: transparent; border-radius: 4px; } QMenuBar::item:selected { background-color: #dde6f7; } QMenu { background-color: #ffffff; border: 1px solid #d0d7e6; } QMenu::item { padding: 6px 24px 6px 12px; } QMenu::item:selected { background-color: #eef3fc; color: #2962bd; } """) def draw_empty_plot(self): """Zeigt einen leeren, aber gestylten Plot, solange noch nicht gerechnet wurde.""" self.ax.clear() self.style_axes() self.ax.text( 0.5, 0.5, "Noch keine Berechnung", transform=self.ax.transAxes, ha="center", va="center", fontsize=10, color="#9aa5b8", ) self.canvas.draw() def style_axes(self): """Zentrale Optik-Einstellungen für die Achsen, damit der Plot zum restlichen Fenster-Design passt.""" self.figure.patch.set_facecolor("#f4f6fb") self.ax.set_facecolor("#ffffff") for spine in self.ax.spines.values(): spine.set_color("#c7cede") self.ax.tick_params(labelsize=8, colors="#3a4257") self.ax.grid(True, linestyle="--", alpha=0.4) self.ax.axhline(0, color="#9aa5b8", linewidth=1) self.ax.axvline(0, color="#9aa5b8", linewidth=1) def update_plot(self, a, b, c, real_roots, vertex): """Zeichnet die Parabel inklusive Nullstellen und Scheitelpunkt.""" self.ax.clear() vx, vy = vertex punkte_von_interesse = list(real_roots) + [vx] x_min = min(punkte_von_interesse) x_max = max(punkte_von_interesse) # Rand links/rechts, damit die Kurve nicht am Bildrand klebt; # bei nur einem Punkt (z.B. Scheitel = einzige Nullstelle) trotzdem sinnvoller Bereich spanne = max(x_max - x_min, 2.0) rand = spanne * 0.6 + 1.0 xs = np.linspace(x_min - rand, x_max + rand, 400) ys = a * xs**2 + b * xs + c self.ax.plot(xs, ys, color="#2962bd", linewidth=2, label="f(x)") if real_roots: self.ax.scatter( real_roots, [0] * len(real_roots), color="#e14b4b", zorder=5, s=45, label="Nullstellen" ) for rx in real_roots: self.ax.annotate( f"{rx:.3g}", (rx, 0), textcoords="offset points", xytext=(0, 10), ha="center", fontsize=8, color="#e14b4b", ) self.ax.scatter([vx], [vy], color="#f5a623", zorder=5, s=45, label="Scheitelpunkt") self.ax.annotate( f"S({vx:.3g}, {vy:.3g})", (vx, vy), textcoords="offset points", xytext=(8, -12), fontsize=8, color="#c9860a", ) self.style_axes() self.ax.set_title("Graph der Parabel", fontsize=10, color="#3a4257") self.ax.legend(loc="upper center", fontsize=7, framealpha=0.9) self.figure.tight_layout() self.canvas.draw() def center_on_screen(self): """Positioniert das Fenster mittig auf dem primären Bildschirm.""" screen = QApplication.primaryScreen() if screen is None: return screen_geometry = screen.availableGeometry() window_geometry = self.frameGeometry() center_point = screen_geometry.center() window_geometry.moveCenter(center_point) # move() erwartet die obere linke Ecke, daher aus window_geometry entnehmen self.move(window_geometry.topLeft()) def calculate(self): """Liest a, b, c aus, prüft die Eingabe, berechnet Nullstellen/Scheitelpunkt und aktualisiert sowohl den Ergebnistext als auch den Plot.""" try: a = float(self.input_a.text().replace(",", ".")) b = float(self.input_b.text().replace(",", ".")) c = float(self.input_c.text().replace(",", ".")) except ValueError: self.result_label.setText("Bitte gültige Zahlen für a, b und c eingeben.") self.draw_empty_plot() return if a == 0: self.result_label.setText("a darf nicht 0 sein (sonst keine quadratische Gleichung).") self.draw_empty_plot() return # Scheitelpunkt: x = -b/2a, y = f(x) an dieser Stelle vx = -b / (2 * a) vy = a * vx**2 + b * vx + c diskriminante = b**2 - 4 * a * c if diskriminante > 0: x1 = (-b + math.sqrt(diskriminante)) / (2 * a) x2 = (-b - math.sqrt(diskriminante)) / (2 * a) self.result_label.setText(f"Zwei Lösungen: x1 = {x1:.4g}, x2 = {x2:.4g}") self.update_plot(a, b, c, real_roots=[x1, x2], vertex=(vx, vy)) elif diskriminante == 0: x = -b / (2 * a) self.result_label.setText(f"Eine Lösung: x = {x:.4g}") self.update_plot(a, b, c, real_roots=[x], vertex=(vx, vy)) else: realteil = -b / (2 * a) imagteil = math.sqrt(-diskriminante) / (2 * a) self.result_label.setText( f"Keine reelle Lösung: x1 = {realteil:.4g} + {imagteil:.4g}i, " f"x2 = {realteil:.4g} - {imagteil:.4g}i" ) # Keine reellen Nullstellen -> leere Liste, Kurve und Scheitelpunkt # werden trotzdem angezeigt, damit man sieht, warum es keine Lösung gibt self.update_plot(a, b, c, real_roots=[], vertex=(vx, vy)) def main(): app = QApplication(sys.argv) window = QuadraticSolver() window.show() sys.exit(app.exec()) if __name__ == "__main__": main() # ***************************************************************************** # Ende # *****************************************************************************


Warum der Wechsel von QWidget zu QMainWindow?
Eine "richtige" Menüleiste (menuBar()) gibt es nur bei QMainWindow – das ist Qt's Klasse für Fenster mit klassischer Anwendungsstruktur (Menü, Toolbar, Statusleiste, zentraler Inhalt). Ein reines QWidget hat diese Infrastruktur nicht eingebaut.

setCentralWidget() statt setLayout()
Das ist der wichtigste strukturelle Unterschied: Bei QMainWindow darf man kein Layout direkt auf das Fenster setzen. Stattdessen packt man den kompletten bisherigen Inhalt (alle Eingabefelder, Button, Plot) in ein normales QWidget, und dieses wird per self.setCentralWidget(central_widget) als "Hauptbereich" registriert. Menüleiste, Statusleiste etc. liegen dann automatisch außerhalb dieses Bereichs.

Menüaufbau mit QAction
exit_action.triggered.connect(self.close) – nutzt die geerbte close()-Methode, kein eigener Code nötig.
exit_action.setShortcut("Ctrl+Q") - Tastenkombination, wird automatisch im Menü angezeigt.
Das & vor Buchstaben (&Datei, &Hilfe) definiert Mnemonics – mit Alt+D bzw. Alt+H lässt sich das Menü auch per Tastatur öffnen, der Buchstabe wird im Menü unterstrichen dargestellt.

Der About-Dialog, habe ich mit einem Link erweitert
QLabel kann Rich Text (HTML) darstellen – ich nutze normale "h3", "p", "a href=..."-Tags. info_label.setOpenExternalLinks(True) ist der entscheidende Aufruf: Ohne diese Zeile reagiert der Link zwar visuell (blau, unterstrichen), aber ein Klick passiert nichts. Mit True öffnet Qt den Link automatisch über QDesktopServices.openUrl() im Standardbrowser des Systems – ganz ohne eigenen Klick-Handler. Aktuell verlinke ich beispielhaft auf den Wikipedia-Artikel zur quadratischen Gleichung. Du kannst die URL im Code (href="...") einfach durch deine eigene Seite ersetzen – z. B. dein GitHub-Repo, deine Portfolio-Seite, o. Ä.

Warum QDialog ein eigenes Stylesheet braucht
Das ist ein Stolperstein, den ich bewusst kommentiert habe: Ein QDialog ist – auch wenn man ihm einen parent übergibt – ein eigenständiges Top-Level-Fenster. Stylesheets vererben sich in Qt nur innerhalb der Widget-Hierarchie eines Fensters, nicht über Fenstergrenzen hinweg. Ohne eigenes dialog.setStyleSheet(...) hätte der Dialog also plötzlich das native OS-Aussehen statt des Blau/Weiß-Themes der restlichen App. Zusatz-Tipp: Falls du später einen eigenen Domain-Link (z. B. deine Webseite) einbauen willst, ersetze einfach die URL in show_about_dialog() – Text und Styling bleiben gleich.


PyQt6 Bauplan 5
Zurück