Großes PyQt Formular auf mehrere Dateien aufteilen

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
Ant-on-Hu
User
Beiträge: 17
Registriert: Sonntag 18. Juni 2017, 16:21

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")" ).
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@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.
Benutzeravatar
Ant-on-Hu
User
Beiträge: 17
Registriert: Sonntag 18. Juni 2017, 16:21

Hallo Sophus, danke für Deine Antwort! :D
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! :lol:
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@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.
Benutzeravatar
Ant-on-Hu
User
Beiträge: 17
Registriert: Sonntag 18. Juni 2017, 16:21

@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 :roll: .
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.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Ant-on-Hu: Ich habe ein kleines Beispiel gefunden: MVC the simplest example
Benutzeravatar
Ant-on-Hu
User
Beiträge: 17
Registriert: Sonntag 18. Juni 2017, 16:21

@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.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@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.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

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
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__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?
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Man kann das problemlos parallel betreiben.
Benutzeravatar
Ant-on-Hu
User
Beiträge: 17
Registriert: Sonntag 18. Juni 2017, 16:21

@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]
Antworten