Seite 1 von 1
Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 30. April 2018, 22:35
von Ant-on-Hu
Hallo zusammen!
Ich befasse mich noch nicht sehr lange mit Python. Für die Programm-Oberfläche benutze ich auch wegen des Designers sehr gerne Qt bzw. PyQt.
Jetzt möchte ich ein Projekt umsetzen, für das ich ein sehr umfangreiches Formular habe, mit vielen Funktionen (Mitgliederverwaltung eines Vereins).
Das Problem: Der Programmcode wird sehr schnell umfangreich und auch unübersichtlich.
Meine Frage: Kann ich das Programm z.B. wie folgt aufteilen? (Ich hab ein bisschen was zu Model-View-Controller-Konzepten gelesen
)
Programmstart über main.py:
Code: Alles auswählen
import sys
from PyQt5.QtWidgets import QApplication
from controller.controller import Controller
class Main(QApplication):
def __init__(self, *args):
super().__init__(*args)
self.controller=Controller()
if __name__ == "__main__":
app = Main(sys.argv)
sys.exit(app.exec_())
main.py startet quasi den Controller:
Code: Alles auswählen
from views.view import View
class Controller():
"""
- startet die View mit dem Formular und das Model
- nimmt die Signale des Formulars entgegen
- steuert die Funktionen des Formulars
"""
def __init__(self, *args, **kwargs):
super().__init__()
# Formular anzeigen Controller-Objekt (self) + Template übergeben
self.view = View(self, "template.ui")
# Das Hauptformular öffnen
self.view.show()
# -------------------------------------------------------------------------
# Slots
# -------------------------------------------------------------------------
def textaendernc(self):
self.view.lineEdit.setText("Button gedrückt!")
Die View öffnet das Formular und gibt die Signale an den Controller weiter, bzw. empfängt die Signale des Controllers:
Code: Alles auswählen
from PyQt5.QtWidgets import QMainWindow, QWidget
from PyQt5.uic import loadUi
class View(QMainWindow):
"""
Öffnet das Formular template.ui und stellt Schnittstellen zum Zugriff auf die
Formularfelder bereit
"""
def __init__(self, controller, template):
super().__init__()
# Verweis auf den Controller
self.controller=controller
# Formular erstellen und anzeigen
self.createLayout(template)
# Signale verknüpfen
self.createConnects()
def createLayout(self, template):
#Mit dem QT-Designer erstelltes Formular (.ui-File) laden
formpfad="views/template/" + template
self.ui=loadUi(formpfad, self)
def createConnects(self):
"""
Connects des Formulars erstellen
"""
self.ui.pushButton.clicked.connect(self.textaendern)
# ---------------------------------------------------------
# Slots
# ---------------------------------------------------------
def textaendern(self):
"""
Text in der Lineedit ändern - über den Controller
"""
self.controller.textaendernc()
Das ist nur ein vereinfachtes Schema, das fertige Programm wird sehr viel länger werden. Ich überlege auch, ob ich die Datenbankanbindung ebenfalls in ein eigenes Objekt auslagere (Model) und ob ich evtl. auch mehrere spezielle Controller (z.B. MitgliederController, BeitragController, ...) erzeuge.
Der Code funktioniert so zwar, aber ich bin mir dennoch nicht sicher, ob die Anbindung des View-Objekts (oder künftig der anderen Controller bzw. des Models) so gemacht werden kann, nämlich indem ich den Controller sich selbst übergeben lasse (hier im Aufruf der View per self -> "self.view = View(self, "template.ui")" ).
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Freitag 4. Mai 2018, 12:11
von Sophus
@Ant-on-Hu: Ich habe mal deinen Beispiel-Quelltext überflogen. Dein Controller wirkt irgendwie falsch. Denn das Aufrufen und Verarbeiten von GUI-Signale und Slots (in diesem Falle PyQT) gehört in View. Ich versuche dir mal ein Beispiel zu liefern, um das View-Controller-Model-Konzept näher zu bringen. Du willst sicherlich mit der Datenbank arbeiten, da du ja die Mitglieder eines Vereins verwalten willst, richtig? Um das bewältigen zu können, brauchen wir eine entsprechende Bibliothek, die mit der Datenbank kommuniziert. Nehmen wir mal als Beispiel SQLAlchemy (es gibt noch andere Bibliotheken). Diese Bibliothek könnte man in diesem Falle als Model betrachten. Denn SQLAlchemy sendet die Daten zur Datenbank, generiert Abfragen etc. Aber SQLAlchemy denkt ja nicht für dich. Du musst Tabellen, Felder, Schema etc (z.B. per ORM) einrichten. Im Zuge dieser Einrichtung befinden wir uns auf Ebene des Controllers. Dieser Kommuniziert einseits mit dem Model (SQLAlchemy) und andererseits mit dem View (die Benutzeroberfläche) - zum Beispiel das Anzeigen der Datensätze über QTreeView. Das Ganze sähe in Etwa so aus:
VIEW (deine Benutzeroberfläche) <----> Controller (ORM) <----> Model (SQLAlchemy)
Zu deiner Aufteilungs-Frage. Für jede neue GUI lege ich grunsätzlich ein neues Modul an. In diesem Modul kommen auch die Signale und SLots und die dazugehörigen Methoden rein. Und wenn ich eine Funktion schreibe, die ich auch außerhalb meines derzeitigen Projektes weiterverwenden kann, lege ich wieder ein neues Modul an und portiere dort meine Funktionen hin. Und im Falle der Datenbank-Verarbeitung lege ich wieder ein weiteres Modul an, wo die Tabellen, Schemen, Felder etc definiert sind. Je umfangreicher dein Projekt, je mehr Module hat man am Ende.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Freitag 4. Mai 2018, 21:16
von Ant-on-Hu
Hallo Sophus, danke für Deine Antwort!
Du hast recht, es ist logisch, dass die Slots in die View gehören und die Methoden greifen dann aber über den Controller auf das Model zu.
Da die Objekte (View <-> Controller) miteinander kommunizieren müssen, also beiden jeweils das andere Objekt bekannt sein muss, muss ich beim Erzeugen des View-Objekts durch den Controller
"self" mit übergeben, z.B. so:
Code: Alles auswählen
# Formular anzeigen Controller-Objekt (self) + Template übergeben
self.view = View(self, "template.ui")
Ist das so in Ordnung?
Beim Model müsste es reichen, wenn es einfach durch den Controller erzeugt wird, es braucht ja selbst nicht auf Controller-Funktionen zurückgreifen.
Ich würden das Model z.B. so im Controller erzeugen und anschließend die Funktion zum Erstellen einer ersten Tabelle aufrufen:
Code: Alles auswählen
# Model erzeugen
self.model = Model()
self.model.db_erstellen("Adressen")
felder = [["Anrede","TEXT"],
["Vorname","TEXT"],
["Nachname","TEXT"],
["Ortsteil","TEXT"],
...]
self.model.tab_erstellen("Adressen", felder)
Die Adressdatenbank würde ich am Anfang einfach per SQLite erstellen, vielleicht aber auch mit TinyDB. Die haben den Vorteil, dass ich am Schluss alles in eine .exe - Datei pressen und dem Vorstand zur Installation auf seinen Rechner mitgeben kann. Ich glaub nicht, dass er Python überhaupt installiert hat, geschweige denn die richtige Version oder irgendwelche Zusatzmodule oder Datenbanken. Aber das wird am Schluss wohl noch eine ganz eigene Herausforderung!
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Samstag 5. Mai 2018, 14:03
von Sophus
@Ant-on-Hu: Bezüglich des MVC-Konzeptes ist dein Grundgedanke richtig. Aber bedenke, dass dies nur ein Konzept ist, und keine Verpflichtung. In meinem obrigen Beispiel entsteht automatisch ein MVC-Konzept. Aber man muss nicht um jeden Preis dieses Konzept einhalten. Beispiel: Du hast eine GUI und ein weiteres Modul, in welches du einige nützliche Funktionen untergebracht hast - die du auch in anderen Projekten anwenden kannst. An dieser Stelle würde ich keinen Controller anlegen. Hier würde ich nach MV-Schema arbeiten. Das Modul mit den Funktionen steht stellvertretend für Model und die GUI eben für View. Das heißt, ich würde auf der View-Seite das Modul mit den Funktionen importieren. Ich würde dann direkt von View aus mit den Funktionen arbeiten, und keinen Controller dazwischen schalten. Angenommen der Anwender würde auf der Benutzeroberfläche - sagen wir mal - Werte eingeben, dann würde ich diese Werte direkt von View aus an die entsprechend importerte Funktion weiterreichen. Das sähe in meiner Vorstellung so aus:
Modul mit Funktionen (Model) <---> GUI (View)
Zu der Datenbank. Ich sehe TinyDB nicht als Datenbank. Sonst könnte man hingehen und Excel als Datenbank bezeichnen. Aber ich möchte keine Grundsatzdiskussion führen. SQLite wird ab Windows XP von Haus aus unterstützt. Darüber hinaus ist SQLite plattformunabhängig. Wenn dein Verstand allerdings Windows 98 oder Millenium besitzt, ist natürlich mit mehr aufwand verbunden. Angenommen, dein Vorstand hat Windows Xp oder höher, dann erledigt SQLalchemy das schon für dich. Wenn dein Programm fertig geschrieben ist, erstellst du ganz einfach deine Exe-Datei - ohne SQLite-Datenbank. Und sobald der Vorstand dein Programm auf seinem Rechner ausführt, legt SQLalchemy ganz automatisch die SQLite-Datei im selben Programm-Ordner an - mit dazugehörigen Schemen, Tabellen, Felder etc. Du musst also deinem Vorstand keine Datenbank mitliefern. Wenn SQLAlchemy mitkriegt, dass keine SQLite-Datei existiert, worauf er zugreifen möchte, dann wird vor Ort auf dem Rechner deines Vorstandes die SQLite-Datei angelegt.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Samstag 5. Mai 2018, 21:05
von Ant-on-Hu
@Sophus: Vielen Dank für Deine Hilfe! Das MVC-Konzept ist für mich nicht in Stein gemeißelt, aber ich finde es ist eine sehr logische Aufteilung für ein umfangreiches Projekt. Ich hab bisher nur noch nirgends ein Beispiel gefunden, in dem ein Objekt ein anderes erzeugt und sich selbst mit self an dieses Objekt übergibt. Das hat mich ein wenig verunsichert. Es ist aber ein sehr direkter Weg um 2 Objekte "miteinander bekannt" zu machen! Danke also nochmals!
Das von Dir beschriebene MV-Konzept habe ich bisher für kleinere Projekte angewendet. Das MVC-Konzept würde mir für dieses Projekt gut passen, weil ich über den Controller weitere Module auch nach und nach anbinden kann, z.B. künftig Beitragseinzug, Kontoführung, Datenlieferung für die Internetseite, und was uns sonst noch so alles im Kopf rumschwirrt
.
SQLalchemy werde ich mir noch genauer ansehen, ich kenne es bisher noch nicht. Es scheint aber ein sehr interessanter Weg für die saubere Anbindung der Datenbank zu sein.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Samstag 5. Mai 2018, 23:20
von Sophus
@Ant-on-Hu: Ich habe ein kleines Beispiel gefunden:
MVC the simplest example
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Samstag 5. Mai 2018, 23:42
von Ant-on-Hu
@Sophus: Bei diesem Beispiel ist es aber auch wieder so, dass der Controller nur Funktionen der View aufrufen kann, nicht aber umgekehrt. Wenn ich PyQt verwende kommen die Aktionen ja aus der View (bzw. den Slots), so dass der Controller Funktionen der View aufrufen können muss (z.B. aktuellen Kontakt anzeigen) und die View Funktionen des Controllers aufrufen können muss (z.B. angezeigten Kontakt speichern). Die Übergabe des Controllers per self ist da schon eine gute Lösung.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 7. Mai 2018, 12:20
von Sophus
@Ant-on-Hu: Ich habe mir mal die Mühe gemacht und auf BitBucket eine neue
Repository erstellt. Hinter dieser Repository findest du insgesamt
vier Dateien (drei Python-Dateien und eine gewöhnliche *.txt-Datei). Die Struktur sieht dann wie folgt aus:
View <-- view.py
Controller <-- controller.py
Model <-- read_in_file.py
data <-- test.txt
Herunterladen kannst du den Quelltext
hier.
WICHTIG: Um das Programm zu starten musst du
view.py starten.
Kurz zur Klärung. in
view.py werden hauptsächlich die GUI-Angelegenheiten geregelt. Aber dort wird auch der
QThread() erzeugt. Da wir das
QThread-Thema an dieser Stelle nicht weiter vertiefen wollen, reicht es zu wissen, dass
QThread hierbei dazu dient als Controller zu simulieren. In
controller.py findest du eine Klasse . Aber was du gleich am Anfang der
TaskController()-Klasse siehst: jede Menge
pyqtSignal()-Signale. Darüber kommuniziert die
TaskController()-Klasse mit der View und auch umgekehrt. In der
view.py findest du ebenfalls ein
pyqtSignal()-Signal. Diese Signale bilden sozusagen die Schnittstellen. Das ist das Einzige, was der Controller oder die View voneinander wissen. Wieder einen Blick in die
TaskController()-Klasse (controller.py). Dort sieht du gleich in der
start()-Methode, dass ich mittels
self.element = read_in(...) einen Generator erzeuge, damit ich die einzelnen Zeilen aus der Datei lesen und diese dann über die Signale auf die View ausgeben kann. Die
read_in()-Funktion, die ich in ein weiteres Modul ausgelagert habe (immerhin kann ich diese Funktion in anderen Projekten benutzen), ist hierbei das Model. Denn der Model ist der Punkt, der die Datei öffnet und ausliest und anschließend wieder die Datei schließt.
Tobe dich erst einmal aus.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 7. Mai 2018, 13:38
von __deets__
Du hast da einen fetten Bug:
https://bitbucket.org/Xenophyl/mvc-conc ... view.py-61
Dadurch, dass du die "work" erst *nach* dem verbinden der Signale in den Worker-Thread schiebst, sind die connections "direct connections" statt die dringend benoetigten "queued connections". Das moveToThread gehoert also direkt hinter die Anlage des Objektes.
Desweiteren finde ich in der ganzen Diskussion die MVC-Konzepte ziemlich verwirrend dargestellt, und dein Beispiel zeigt das. Das faengt damit an, dass Threading zu verwenden hier gaenzlich unnoetig ist. Du hast einen Timer. Das reicht fuer die gewuenschte Funktionalitaet. Threads da reinzuruehren macht alles nur fehleranfaelliger (siehe ersten Kommentar).
Zweitens nutzt du kein Q*ItemModel, um deinen View zu befuellen - was so ziemlich die ganze Idee hinter MVC darstellt, und damit von dir zu gunsten einer klassischen "der zustand ist irgendwie im Widget verschnoerkelt" Loesung fuehrt. Genau das, was ein MVC ausmacht, naemlich die Moeglichkeit, verschiedene Views befuettert aus dem gleichen Modell zu nutzen, und verschiedene Controller, die ebenfalls das gleiche Modell bearbeiten, wird nicht dargestellt.
Und zu guter letzt ist das, was du Controller nennst, eigentlich eher Modell, plus durch den Timer irgendwas dazwischen.
Als Grundlage fuer ein sauberes, leicht nachvollziehbares Modell von MVC mit Qt taugt das im jetzigen Zustand leider nicht.
Und last but not least sollte man fuer neuen Code wirklich nicht mehr Qt4 verwenden. Das ist nicht mehr maintained - seit ueber drei Jahren:
https://en.wikipedia.org/wiki/Qt_version_history#Qt_4
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 7. Mai 2018, 17:23
von Sophus
@__deets__: Erwischt. Du hattest Recht. in meiner vorherigen Version war ein fetter Fehler drin. Aber ich habe meine Version überarbeitet. Ich habe QTreeView und eine QComboBox hinzugezogen. Und damit dies dem MCV-Konzept näher kommt, verwende ich QStandItemModel. Um den Effekt besser zu begreifen, wird die ComboBox in ein Extra-Fenster geladen. Beim Start des Programms bekommt man zwei Fenster: einmal Fenster mit TreeView und einmal mit ComboBox. Beide Widgets werden mit Daten gleichermaßen befüllt.
Zu deiner Anmerkung bezüglich PyQt4. Wenn man ein neues Projekt startet, hast du allerdings Recht. Mein Projekt hat schon an Tiefe und Breite zugenommen. Ich werde nicht jetzt mit der Portierung auf PyQt5 beginnen. Allgemein gefragt: kann man PyQt4 und PyQt5 paralell auf seinem Entwicklungsrechner haben? Oder wird Python da Probleme bekommen?
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 7. Mai 2018, 20:42
von __deets__
Man kann das problemlos parallel betreiben.
Re: Großes PyQt Formular auf mehrere Dateien aufteilen
Verfasst: Montag 7. Mai 2018, 23:26
von Ant-on-Hu
@Sophus: In Deinem Beispiel für ein MVC-Konzept startest du aus der View heraus, den Controller, der dann die Daten lädt. Ist soweit logisch. Wäre es aber nicht einfacher, wenn der Einstieg über den Controller erfolgt?
z.B. folgender Maßen:
Dateistruktur:
main.py
\controller
- controller.py
\model
- model.py
- namen.txt
\views
- view.py
\views\template
- template.ui
1. Programmstart über main.py:
Code: Alles auswählen
import sys
from PyQt5.QtWidgets import QApplication
from controller.controller import Controller
class Main(QApplication):
def __init__(self, *args):
super().__init__(*args)
self.controller = Controller()
if __name__ == '__main__':
app = Main(sys.argv)
sys.exit(app.exec_())
Der Controller wiederum startet die View und hat sonst mit der Darstellung der Oberfläche nichts zu tun. Die View muss aber Methoden des Controllers auslösen können, damit z.B. der nächste Datensatz geholt werden kann. Darum wird an die View auch der Controller mit self übergeben. Diesen holt der Controller über das Model-Objekt, in dem alles, was mit Datenbank zu tun hat, stattfindet. Der Controller sieht also z.B. folgendermaßen aus:
Code: Alles auswählen
from views.view import View
from model.model import Model
class Controller():
"""
Der HauptKontroller:
- startet die View mit dem Hauptformular und das Model
- nimmt die Signale des Hauptformulars entgegen
- steuert die Funktionen des Formulars
"""
def __init__(self, *args, **kwargs):
super().__init__()
self.ds_nummer = 0
# Verbindung zur Datenbank über das Model
self.model=Model()
# View das Template übergeben
self.view = View(self, "template.ui")
# Das Hauptformular öffnen
self.view.show()
self.anzahl_ds = self.model.anzahl_ds()
# ersten Datensatz gleich anzeigen
self.naechsten_ds()
def naechsten_ds(self):
if self.anzahl_ds >= self.ds_nummer + 1:
self.ds_nummer += 1
ds = self.model.ds_holen(self.ds_nummer)
self.view.ds_anzeigen(ds)
def vorhergehenden_ds(self):
if self.ds_nummer - 1 > 0:
self.ds_nummer -= 1
ds = self.model.ds_holen(self.ds_nummer)
self.view.ds_anzeigen(ds)
Die View:
Code: Alles auswählen
from PyQt5.QtWidgets import QMainWindow, QWidget
from PyQt5.uic import loadUi
class View(QMainWindow):
"""
Öffnet das Hauptformular mainform.ui und stellt Schnittstellen zum Zugriff auf die
Formularfelder bereit
"""
def __init__(self, controller, template):
super().__init__()
# Verweis auf den Controller
self.controller=controller
# Formular erstellen und anzeigen
self.createLayout(template)
# Signale verknüpfen
self.createConnects()
def createLayout(self, template):
"""
Mit dem QT-Designer erstelltes Formular (.ui-File) laden.
"""
# Mit dem QT-Designer erstelltes Formular (.ui-File) laden
# Ein Lineedit (Name: lineEdit) um die Daten an zu zeigen
# Zwei Buttons (Namen: pb_zurueck und pb_vor) um auf den nächsten, bzw. den vorhergehenden Datensatz zu springen
formpfad="views/template/template.ui"
self.ui=loadUi(formpfad, self)
def createConnects(self):
"""
Connects des Formulars erstellen
"""
self.ui.pb_zurueck.clicked.connect(self.zurueck)
self.ui.pb_vor.clicked.connect(self.vor)
def zurueck(self):
"""
Text in der Lineedit ändern - über den Controller
"""
self.controller.vorhergehenden_ds()
def vor(self):
"""
Text in der Lineedit ändern - über den Controller
"""
self.controller.naechsten_ds()
def ds_anzeigen(self, ds):
"""
Übergebenen Datensatz "ds" anzeigen
"""
self.ui.lineEdit.setText(ds)
und das Model:
Code: Alles auswählen
import sys
class Model():
"""
Das Model hält die Datenbankverbindung mit der Kontaktdatenbank
"""
def __init__(self, *args, **kwargs):
super().__init__()
self.db = "model/namen.txt"
def anzahl_ds(self):
"""
Ermittelt die Anzahl der Datensätze
"""
f = open(self.db)
anzahl_ds = len(f.readlines())
f.close()
print(anzahl_ds)
return anzahl_ds
def ds_holen(self, nr):
"""
Den nr-ten Datensatz zurück spielen
"""
f = open(self.db)
lines = f.readlines()
f.close()
return lines[nr-1]
Die txt-Datei namen.txt als "Datenbank":
[codebox=text file=Unbenannt.txt]
Hinz
Kunz
Casper
Melchior
Balthasar
Krautwurst
Rippenbiest
Hammelswade
Schnürbein
Rumpelstilzchen
[/code]