PyQt6 Bauplan Fenster

Teil 4 – Grundlagen: Matplotlib-Einbettung in Qt

Quadratische Gleichung als nutzbare Anwendung

Zurück

Matplotlib besitzt eine integrierte Unterstützung für PyQt6. Du kannst Matplotlib-Diagramme direkt als Widgets in eine PyQt6-Benutzeroberfläche einbetten. Dafür stellt die Bibliothek spezielle Backend-Klassen bereit. "FigureCanvasQTAgg" Diese Klasse fungiert als reguläres Qt-Widget (QWidget). Sie rendert das Matplotlib-Diagramm und kann überall in Ihr PyQt6-Layout eingefügt werden. Diese Quelle habe ich angezapft!

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 import sys import math import os import numpy as np from PyQt6.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QFrame, ) from PyQt6.QtGui import QIcon 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(QWidget): 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) self.setLayout(main_layout) self.apply_styles() # Fenster mittig auf dem Bildschirm positionieren self.center_on_screen() 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; } """) 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()


Vorbereitung:
Fenstergrößen anpassen, da nun ein Plot-Bereich Platz braucht.
Nun die Matplotlib-Canvas erstellen und ins Layout einfügen.
Die Plot-Methoden hinzufügen (draw_empty_plot, style_axes, update_plot) direkt nach apply_styles.
Die calculate()-Funktion, [Zeile 271 / 312], erweitern, sodass sie nach der Lösung den Plot aktualisiert (und bei Fehlern/a=0 den leeren Plot zeigt).
Jetzt Header-Kommentar aktualisieren und mit dem ersten Testlauf alles prüfen.

Matplotlib-Einbettung in Qt:
Matplotlib bringt einen eigenen Qt-Backend mit: FigureCanvasQTAgg ist ein QWidget, das eine Matplotlib-Figure rendert. Dadurch kann ich es genau wie jedes andere Qt-Widget behandeln – self.canvas wird einfach per main_layout.addWidget(self.canvas) ins Layout gehängt und verhält sich wie Button oder Label auch.

Figure vs. Axes:
Figure ist die gesamte "Zeichenfläche" (inkl. Hintergrund). self.ax (eine Axes-Instanz via add_subplot(111)), [Zeile 103], ist das eigentliche Koordinatensystem, in das geplottet wird. Ich erstelle beide einmal im Konstruktor und rufe bei jeder Berechnung self.ax.clear() auf, statt jedes Mal eine neue Figure zu bauen – das ist deutlich performanter und vermeidet Speicherlecks bei wiederholten Klicks.

Trennung der Optik-Logik (style_axes):
Da ich die Achsen bei jedem Update mit clear() zurücksetze, gehen auch alle Styling-Einstellungen (Gitter, Rahmenfarben, Nulllinien) verloren. Daher habe ich das in eine eigene Methode style_axes() ausgelagert, die nach jedem clear() erneut aufgerufen wird – sowohl im leeren Zustand (draw_empty_plot), [Zeile 190, 104,280,285], als auch nach einer Berechnung (update_plot), [Zeile 214,298,302,312]. So bleibt der Look konsistent mit dem restlichen Fenster (gleiche Blau-/Grautöne wie im QSS-Stylesheet).

Dynamischer x-Wertebereich:
Statt eines festen Plot-Bereichs (z. B. immer -10 bis 10) berechne ich die Spanne aus den tatsächlich interessanten Punkten – Nullstellen und Scheitelpunkt: x_min/x_max aus diesen Punkten plus ein proportionaler Randfaktor. Das sorgt dafür, dass die Parabel bei kleinen wie bei großen Koeffizienten immer gut sichtbar und zentriert im Bild liegt, statt z. B. bei c=1000000 nur eine winzige Kurve am Bildrand zu zeigen.

Drei Sonderfälle im Plot:
Zwei reelle Nullstellen: beide werden als rote Punkte markiert, mit Achsenbeschriftung. Eine Lösung (Diskriminante = 0): Nullstelle und Scheitelpunkt fallen zusammen – der Plot zeigt trotzdem beide Marker sauber übereinander. Keine reelle Lösung: Ich übergebe eine leere real_roots-Liste. Die Parabel und der Scheitelpunkt werden trotzdem gezeichnet – so sieht man visuell sofort, warum es keine Lösung gibt (die Parabel schneidet die x-Achse nicht).

Leerer Ausgangszustand:
draw_empty_plot() zeigt beim Programmstart (und bei Fehleingaben) einen dezenten Platzhaltertext statt eines nackten weißen Rechtecks – das wirkt weniger wie ein Darstellungsfehler.


PyQt6 Bauplan 4
Zurück