[gelöst] QAbstractListModel, Datenbasis dynamisch ändern?

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hallo,

ich habe ein grundsätzliches Problem mit Qt4 und den MVC-Konzept. Ich habe es mittlerweile geschafft, ein eigenes Model zu erstellen, welches ich in einem QListView auch sauber angezeigt bekomme.

Anbei mal mein Model-Code - ich vermute der ist ein wenig dirty, weil ich die Query ohne Exception Handling aufrufe und das ganze auch im innern erledige. Oder ist das prinzipiell sogar ok / sinnvoll?

Code: Alles auswählen

class SongModel(QtCore.QAbstractListModel):
    def __init__(self, cursor):
        QtCore.QAbstractListModel.__init__(self)
        self.cursor = cursor
        res = self.cursor.execute("select sid, name from songs")
        self.built_data(res)

    def built_data(self,  rows):
        self.songs = {}
        for song in rows:
            self.songs[int(song[0])] = unicode(song[1])

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.songs)

    def data(self, index, role): 
        if role != QtCore.Qt.DisplayRole:
            return QtCore.QVariant()
        return QtCore.QVariant(self.songs[index.row()])
Nun möchte ich aber einen Filter einbauen, durch den ein neues (kürzeres dict) als Model-Grundlage dient. Was wäre eine gute Strategie von außen die Datenbasis des Models zu ändern und zusätzlich den View (also das QListView Widget) zu aktualisieren?
Zuletzt geändert von Hyperion am Mittwoch 27. Mai 2009, 08:53, insgesamt 2-mal geändert.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ok, ich habe das Problem nun folgendermaßen gelöst:

Die Idee ist folgende: Man baut die Datenbasis neu auf, indem man von außen die built_data()-Methode aufruft und die aktuellen Ergebnisse übergibt. (Wie man die Daten neu setzt, ist natürlich beliebig. Ich habe eben diese Methode geschrieben - geht natürlich auch anders.)

Wichtig ist es dann, die update()-Methode des Viewports des QListViews aufzurufen. Wird auch in der PyQt4-Doku ausdrücklich erwähnt - man muss nur mal genau lesen ;-)

Hier nun der Ausschnitt meiner filter()-Methode:

Code: Alles auswählen

    def filter_songs(self, search_string):
        query = u"select sid, name from songs where name like ?"
        res = self.cursor.execute(query, [u"%%%s%%" % search_string])
        self.song_model.built_data(res)
        self.ui.list_songs.viewport().update()
lunar

Für sonderlich gelungen halte ich den Ansatz nicht. Das Model sollte den Zugriff unabhängig von der konkreten Datenhaltung machen, genau diese Trennung aber hebst du wieder auf, in dem du die Filterung über SQL-Ausdrücke außerhalb des Models implementierst.

Auch wundere ich mich, warum du ein eigenes Model für den Datenbankzugriff implementierst. Qt hat fertige Modellklassen für den Datenbankzugriff.

Ich würde eine dieser Klassen nutzen und die Suche über QSortFilterProxyModel implementieren. Das ist sicherlich nicht so effizient, weil die Suche nicht über die Datenbank läuft. Allerdings trennt dieser Ansatz die Verantwortlichkeiten sauber auf, die GUI ist nicht auf Interna des Models angewiesen. Außerdem ist der Ansatz einfacher zu implementieren und verständlicher.

Sollte die Performance zu schlecht werden, kann man immer noch manuell optimieren, in dem man von QSortFilterProxyModel ableitet.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

lunar hat geschrieben:Für sonderlich gelungen halte ich den Ansatz nicht. Das Model sollte den Zugriff unabhängig von der konkreten Datenhaltung machen, genau diese Trennung aber hebst du wieder auf, in dem du die Filterung über SQL-Ausdrücke außerhalb des Models implementierst.
Ich bin mir nicht sicher, ob ich Dich hier richtig verstehe. Ich dachte ein Model dient dazu Daten vorzuhalten. Diese Daten sind ja mehr als die reinen Anzeigedaten. In meinem Falle eben eine Nummer und ein Name. Diese stammen aus einer DB.
Wie befüllt man nun so ein Model? Befüllt sich das Model selber? (So habe ich das ja implementiert) Oder kritisierst Du die Änderungen von außen?
Auch wundere ich mich, warum du ein eigenes Model für den Datenbankzugriff implementierst. Qt hat fertige Modellklassen für den Datenbankzugriff.
Es war nur so nen Schnellschuss - ich will das eigentlich noch auf Elixir umstellen. Oder sind die Qt-Klassen in dem Falle die bessere Wahl?
Ich würde eine dieser Klassen nutzen und die Suche über QSortFilterProxyModel implementieren. Das ist sicherlich nicht so effizient, weil die Suche nicht über die Datenbank läuft. Allerdings trennt dieser Ansatz die Verantwortlichkeiten sauber auf, die GUI ist nicht auf Interna des Models angewiesen. Außerdem ist der Ansatz einfacher zu implementieren und verständlicher.

Sollte die Performance zu schlecht werden, kann man immer noch manuell optimieren, in dem man von QSortFilterProxyModel ableitet.
Dann schaue ich mir mal diese Klassen genauer an. Ich hoffe ich komme damit alleine weiter :-)
lunar

Ein Modell befüllt man nicht. Ein Modell repräsentiert die Daten der darunter liegenden Datenhaltung. Manchmal ist das eine Liste, manchmal eben eine Datenbank. Beiden Szenarien gemein ist, dass das Modell die Daten nicht selbst vorhält, sondern nur bei Bedarf aus der Datenhaltung lädt. Dieses Laden musst du implementieren (und je nach Fähigkeiten des Models auch die Manipulation).

Mein Kritikpunkt bezieht sich nicht auf die Implementierung deines Modells, die hast du ja gar nicht gezeigt. Mein Kritikpunkt ist, dass die Abfrage von außen über SQL-Ausdrücke erfolgt, und der Zustand des Models von außen entsprechend den Ergebnissen der Ausdrücke geändert wird. Das bedeutet, dass man außerhalb des Modells über dessen Interna (nämlich die Speicherung in einer DB) Bescheid wissen muss. Das führt den MVC-Entwurf und die Trennung zwischen Datenhaltung und Datenanzeige ad absurdum.

Grundsätzlich sollte das Modell sich nur über die Standardmethoden von Qt4 nutzen lassen! Programmiere ein Modell immer so, dass du es gegen ein beliebiges anderes austauschen könntest.

Ich kenne deine Anwendung nicht, daher kann ich auch nicht beurteilen, welcher Weg besser wäre. Wenn die Datenbank nur mit den Mitteln von Qt4 abgefragt bzw. manipuliert werden kann/soll, sind die Qt4-Klassen mit Sicherheit einfacher und schneller zu nutzen. Allerdings sind diese Klassen bei weitem nicht so mächtig wie SQLAlchemy. Auch bieten sie kein ORM (dafür aber den Modell-basierten Zugriff).

Welche Wahl besser ist, hängt stark von der Anwendung ab. Eine einfache CRUD-Anwendung ist mit den Qt4-Klassen wesentlich schneller implementiert. Eine komplexe Datenbanklogik mit sehr komplizierten Abfragen dagegen lässt sich mit der Mächtigkeit von SQLAlchemy besser implementieren.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

lunar hat geschrieben:Ein Modell befüllt man nicht. Ein Modell repräsentiert die Daten der darunter liegenden Datenhaltung. Manchmal ist das eine Liste, manchmal eben eine Datenbank. Beiden Szenarien gemein ist, dass das Modell die Daten nicht selbst vorhält, sondern nur bei Bedarf aus der Datenhaltung lädt.
Hm ... also ich habe mich ja unter anderem an einem Beispiel von Dir orientiert:
http://files.lunaryorn.de/view/snippets ... istview.py

In Zeile 13 ist das self.pages doch ein Attribut auf eine Liste, die die Daten beinhaltet. Damit hält das Model doch die Daten vor, oder nicht?
Dieses Laden musst du implementieren (und je nach Fähigkeiten des Models auch die Manipulation).
Ok, aber das Laden ist doch in meinem Falle ein SQL-Zugriff? Ich glaube ich verstehe immer noch nicht, wofür genau das Modell da ist ...
Mein Kritikpunkt bezieht sich nicht auf die Implementierung deines Modells, die hast du ja gar nicht gezeigt.
Das Modell habe ich doch im ersten Post gezeigt.

*seufz* Ist irgend wie alles komplizierter als ich dachte ;-)
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

So, ich habe mein Script nun umgeschrieben und mich an diesem Tutorial orientiert:
http://doc.trolltech.com/4.4/itemviews- ... model.html
Dazu gibts ja auch das passende PyQt-Demo-Code Fragment.

Mit dem QStandardItemModel und dem dazwischen geschalteten QSortFilterProxyModel() war es ein Kinderspiel mein Problem zu lösen.

Ich habe nun eine Funktion, die mir mein Basis-Modell erstellt:

Code: Alles auswählen

def create_song_model(conn, parent):
    cursor = conn.cursor()
    model = QtGui.QStandardItemModel(0, 2, parent)
    
    model.setHeaderData(0, QtCore.Qt.Horizontal, QtCore.QVariant("ID (sid)"))
    model.setHeaderData(1, QtCore.Qt.Horizontal, QtCore.QVariant("Songname"))
    
    query = "select sid, name from songs"
    res = cursor.execute(query)

    for row in res:
        model.insertRow(0)
        model.setData(model.index(0, 0), QtCore.QVariant(int(row[0])))
        model.setData(model.index(0, 1), QtCore.QVariant(unicode(row[1])))

    return model
Wie sieht es nun mit dem Laden aus der DB aus? Das findet ja nun außerhalb des Models aus. Aber dafür hält das Modell auch sämtliche Daten. (Da es sich hier um < 10.000 handelt, sehe ich das erst einmal aus Ressourcen-Sicht nicht so als Problem an.) Du hast ja geschrieben, dass sich ein Modell nur bei Bedarf die benötigten Daten holt ... das ist ja hier nun auch nicht der Fall?

Ich hoffe ich stelle mich grad nicht zu dumm an ... ist aber auch schon spät ;-)
lunar

Hyperion hat geschrieben:In Zeile 13 ist das self.pages doch ein Attribut auf eine Liste, die die Daten beinhaltet. Damit hält das Model doch die Daten vor, oder nicht?
Nein, die Liste hält die Daten vor. Das Modell sorgt lediglich dafür, dass ein View über eine standardisierte Schnittstelle auf diesen Inhalt zugreifen kann.
Ok, aber das Laden ist doch in meinem Falle ein SQL-Zugriff? Ich glaube ich verstehe immer noch nicht, wofür genau das Modell da ist ...
Natürlich lädst du die Daten über SQL-Befehle. Allerdings kopierst du die Daten nicht mit einer initialen Abfrage in das Modell, sondern lädst sie sinnvollerweise erst dann aus der Datenbank, wenn der View sie abfragt.

Die interne Speicherung des Modells ist letztlich aber auch egal, mir geht es nicht darum, wie du speicherst, sondern – wie gesagt – darum, dass du das Modell von außen lädst und dessen interne Speicherung veränderst.
Das Modell habe ich doch im ersten Post gezeigt.
Sorry, ich hatte dein erstes Posting bei meiner Antwort gar nicht mehr richtig in Erinnerung ...
Hyperion hat geschrieben:Wie sieht es nun mit dem Laden aus der DB aus? Das findet ja nun außerhalb des Models aus. Aber dafür hält das Modell auch sämtliche Daten. (Da es sich hier um < 10.000 handelt, sehe ich das erst einmal aus Ressourcen-Sicht nicht so als Problem an.) Du hast ja geschrieben, dass sich ein Modell nur bei Bedarf die benötigten Daten holt ... das ist ja hier nun auch nicht der Fall?
Nein, natürlich nicht. Dieser Ansatz ist auch nur bedingt besser als der erste. Wieso nimmst du nicht einfach eine der fertigen SQL-Modelle von Qt4? Damit könntest du dir diesen ganzen Unsinn sparen ... irgendwie machst du dir die Sache gerade schwerer als nötig ...
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

So, muss diesen Thread noch einmal aufleben lassen :-)

@lunar: Noch mal vielen Dank für Deine "Geduld". Ich habe es nun so hin bekommen, wie Du das von Anfang an erklärt hattest - zumindest denke ich das mal.

Ich habe mal ein Minimalbeispiel zusammengestellt, welches den grundlegenden Aufbau zeigt: Link

Dazu habe ich jetzt aber eine Frage zu einem mir nicht schlüssigen Verhalten:

Wenn ich wie hier gezeigt zunächst das Modell anlege und an die Variable model binde, klappt alles, wie es soll. Rufe ich aber setModel() folgendermaßen auf, klappt es nicht:

Code: Alles auswählen

# klappt nicht
view.setModel(create_model())
Wieso? Hoffe es gibt dazu eine einfache Erklärung - hat mich einige Zeit gekostet, das rauszufinden ...
lunar

Bei einem Aufruf wie ".setModel(create_model())" existiert weder auf Python- noch auf Qt4-Seite eine Referenz auf das erzeugte Modell, so dass der Python-GC das Objekt löscht.

Das kann man entweder über das Binden an einen Namen verhindert (so dass der Refcount immer mindestens eins ist), oder in dem man dem Modell ein Qt4-Objekt (im Idealfall den dazugehörigen View) als Vaterobjekt mitgibt. Genau für diesen Zweck gibt es nämlich den Parameter "parent" im Konstruktor von Qt4-Objekten. Du solltest dir angewöhnen, dieses Parameter immer sinnvoll zu setzen, um Qt4 bei der Speicherverwaltung zu unterstützen, und solche "magischen" Fehler zu verhindern. Das betrifft beispielsweise auch den View, der bei deinem Code ebenfalls kein "parent" hat.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Vielen Dank für die Erklärung.

Nun stellt sich mir aber die Frage: Welchen parent hätte denn der view hier sinnvoller weise?

Wenn ich dem Modell als parent den View mitgeben würde, müßte ich ja die GUI vorher initialisieren, richtig? Also würde so eine create_model()-Funktion dann ggf. aus dem Konstruktor eines komplexeren GUI-Konstruktes aufgerufen werden? Bei meinem Hauptscript leite ich eben von QWidget ab und lade dann im Konstruktor die GUI erst aus dem passenden UI-File nach. So weit ich informiert bin, ist das ja ein gängiger Weg, der dann aber eben diese Vorgehensweise implizieren würde - oder liege ich da falsch?
lunar

Der View hat den Vater, den der GUI-Designer dafür angibt. Ich denke, du wolltest fragen, welchen Vater das Modell erhalten soll, oder? In diesem Fall lautet die Antwort: Den View, und ja, du musst die dann natürlich GUI vorher erzeugen. Da du aber eh nicht mehr tust, als das Modell dem View zu übergeben (und zwar nachdem dieser erzeugt wurde), sehe ich da das Problem nicht ...
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

lunar hat geschrieben:Der View hat den Vater, den der GUI-Designer dafür angibt.
Nee, ich meinte den view aus meinem Mini-Beispiel in Zeile 29.
Ich denke, du wolltest fragen, welchen Vater das Modell erhalten soll, oder? In diesem Fall lautet die Antwort: Den View, und ja, du musst die dann natürlich GUI vorher erzeugen. Da du aber eh nicht mehr tust, als das Modell dem View zu übergeben (und zwar nachdem dieser erzeugt wurde), sehe ich da das Problem nicht ...
Ich habe nie gesagt, dass ich darin ein Problem sehe. Ich wollte mich nur vergewissern, dass ich da den richtigen Ansatz verfolge :-)
Antworten