Model/View-Architektur

Alles rund um die Programmierung mit Qt
Antworten
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Model/View-Architektur

Beitrag von Thorsten »

Ich habe ein kleines Problem

Ich habe folgende Datenstruktur:

Code: Alles auswählen

class Datensatz {
...
	int x;
	QString y;
	bool z;
};
die ich in mit Hilfe einer List (QList<Datensatz>) in meinem "Dokument" verwalte. Ich lese die Daten beim Öffnen
aus einer XML-Datei der folgenden Form:

<datensatz x="4367" y="Hallo" z="ja" />

Jetzt möchte ich diese Liste bearbeiten können, d. h. neue Elemente einfügen und ausgewählte löschen können.

Ich habe mir mal die Model/View-Architektur und die Beispiele angesehen. Leider hat mir das irgendwie nicht weitergeholfen. Die Beispiele sind immer sehr speziell und ich habe eingentlich eher das Gefühl, dass sie nur zeigen, dass man mit wenig Code, etwas machen kann, was nach mehr aussieht. Dahinter versteckt sich dann irgenteine eingebaute Qt-Funktion, die aber nichts anderes kann, als genau das was im Beispiel gezeigt wird.

Ich denke, dass ein QTableView das beste für mich ist. Doch wie bekomme ich meine die Datenquelle (hier Meine Liste) an ein Model gekoppelt? Jede Zeile soll einem Datensatz und jede Spalte einer ihren Eigenschaften (hier x,y,z) entsprechen. Auch will ich nicht immer alle Eigenschaften im TableView anzeigen lassen, die Datensätze bearbeiten und löschen können.

Werden die Daten im Model selbst gespeichert, so dass ich dann keine Liste mehr brauche, oder kann das Model auf die Liste zugreifen?

Kann mir da jemand mit etwas Code weiterhelfen. Welche Klassen kann ich direkt verwenden? Welche muss ich reimplementieren?

Mir freundlichen Grüßen.
Thorsten
upsala
Beiträge: 3946
Registriert: 5. Februar 2006 20:52
Wohnort: Landshut
Kontaktdaten:

Beitrag von upsala »

Ich würde mal hier anfangen.

Statt der der QAbstractListModel-Klasse kannst du dann QAbstractTableModel verwenden, wobei du dann columnCount() noch reimplementieren mußt. Wenn du dann mal eine Spalte geschafft hast, dürfte der Sprung zu mehr Spalten nicht mehr weit sein...
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Beitrag von Thorsten »

Danke, das werde ich mal probieren. Wenn ich nicht weiter komme, werde ich mich wieder mit mehr Details melden.

Gruß
RHBaum
Beiträge: 1436
Registriert: 17. Juni 2005 09:58

Beitrag von RHBaum »

In "Qt4 Einfuehrung in die Applikationsentwicklung" wird das QT Model View konzept, zumindest die Grundlagen, ganz gut dargestellt.

Wichtig ist halt, das man das Prinzip versteht, und vor allem die Vor und Nachteile von Sowas.

Das wiederum ist ned generell QT Thema, sondern eher Design pattern Thema.

Also, wenn du einmal das prinzip verstanden hasst, dann kannst du Gott und das Universum in so nen modell packen :-)
Es muss nur halt erstmal Klick machen, aber das wird schon ...

Ciao ...
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Beitrag von Thorsten »

Vielen Dank!

Es funktioniert fantastisch. Genau so wie ich es mir vorgestellt hatte. Ich kann jetzt meine Daten darstellen ich habe ein Model und ein Delegate und kann meine Daten damit auch, je nach Spalte und Type des Elementes bearbeiten. Nutze dann dafür Widgets wie Combobox, Spinbox, LineEdit u. ä.

Dafür musste ich erst erkennen, dass man mit dem Parameter index nicht nur auf die Zeilen (index.row()), sondern mit der Funktion index.column() auch auf die Spalten der Liste zugreifen kann. Auch ist noch eine reimplementierung der Funktion "int columnCount(const QModelIndex &parent) const" nötig, die die Anzahl der Spalten zurückgibt. Innerhalb der Funktionen "data" und "setData" kann man nun mit Hilfe eines switch-Statements und der Funktion index.column() für jede Spalte die entsprechenden Daten zurückgeben.

Das entsprechende Delegate habe ich vom SpinBoxDelegate-Beispiel übernommen und noch um die Mehrspaltenfunktionen erweitert.
Auch hier sind switch-Statements in den Funktionen nötig um die jeweiligen "Editoren" zu erstellen und mit den richtigen Werten zu initialisieren und später die Daten in das richtige Datenelement zurückzuschreiben.

Zu guter Letzt braucht man dann nur folgendes, wobei das tableView natürlich schon vorhanden sein muss (kann ja mit dem Designer erstellt worden sein):

MyModel* pMyModel = new MyModel(myData, this);
tableView->setModel(pMyModel);
MyDelegate* pMyDelegate = new MyDelegate(this);
tableView->setItemDelegate(pMyDelegate );

Trotzdem habe ich noch zwei Probleme:

Erstens:

Ich muss meine ganze Liste im Model speichern. Das heißt, wenn ich das Model erzeuge, gebe ich dem Konstruktor die Liste mit und er speichert eine Kopie davon. Das finde ich nicht so wirklich elegant. Was gibt es da für Lösungsmöglichkeiten? Sollte ich vielleicht nur einen Zeiger im Model speichern? Das Buch "Qt4 Einführung in die Applikationsentwicklung" habe ich hier, aber das Beispiel im Kapitel "8.4 Eigene Modelle implementieren" geht dem Problem auch nicht nach.

Zweitens:

Was mache ich, wenn ich neue Datensätze einfügen will oder schon bestehende löschen möchte. Da komme ich im Moment nicht wirklich weiten. Mit schweben zwei Möglichkeiten vor. Entweder ich erstelle noch zwei Buttons "+" und "-" oder ich erstelle unten in der Liste eine Dummy-Zeile. Diese wird dann zum neuen Element, wenn ich sei bearbeiten möchte. Die zweite Möglichkeit finde ich am elegantesten. Kann mir da jemand noch einen Tipp geben, wie ich das implementieren kann? Die beiden Funktionen, die in der Dokumentation und auch im vorgestellt werden, machen mich auch nicht schlauer. Wie und wo müssen diese jetzt aufgerufen werden? Ich will einfach eine markierte Zeile löschen oder am Ende ein neues Element hinzufügen.

Fazit:
@RHBaum: Ja, das Prinzip hab ich jetzt verstanden, auch wenn es mit der Umsetzung hier und da noch etwas hakt. So wie es aussieht, kann man damit wirklich viel machen. Doch welches sind jetzt die genauen Vor- und Nachteile, von denen du sprichst? Bevor ich jetzt alles mögliche in meinem Programm damit implementiere, wäre es nicht schlecht davon mehr zu erfahren. Darüber schweigt auch Dokumentation und Buch.

Gruß
upsala
Beiträge: 3946
Registriert: 5. Februar 2006 20:52
Wohnort: Landshut
Kontaktdaten:

Beitrag von upsala »

Ob du die Daten im Modle vorhalten willst, oder nicht hängt von deiner Datenquelle ab. Normalerweise ist direkt im Model vorteilhafter. Sollte man aber größere Datenmengen darstellen wollen, als in den Speicher passen kann man auch dynamisch nachladen. (z.B. bei großen Datenbanken)

Das Löschen von Rows/Columns ist im Tutorial beschrieben. Das einfügen auch, und wie du schon schreibst ist es sinnvoll ein leeres Element einzufügen und dies dem User dann füllen zu lassen.
RHBaum
Beiträge: 1436
Registriert: 17. Juni 2005 09:58

Beitrag von RHBaum »

Sollte ich vielleicht nur einen Zeiger im Model speichern?
Jeee nach dem was du willst ....

Ne eigene Datenhaltung nur fuer oder im Model macht nur bei 2 Punkten sinn.

1. Die Struktur der Daten ist sehr simpel (Stringlist) und das was man mit macht ist auch eher trivial. Um sich ne eigene Modelklasse zu sparen, schiesst man alles in ne Stringlist, und nimmt gleich nen StringlistModel.

2. Mann will die Ein und ausgabe abpuffern, also den User temporaere enderungen Vornehmen lassen, und dann erst mit ner seperaten Aktion in die eigentliche Daten uebernehmen.

Fuer alle anderen Faelle kommt man besser wenn man direkt auf den Daten arbeitet :-)

Und Zeiger ? Qt ist C++ ! Referenz da wo geht, Zeiger da wo man muss ! Also ich wuerd ne Referenz auf deine Datenklasse empfehlen :-)

Zu 2tens ...
Ne Dummy Zeile ist eher aufwendiger geht aber auch. DU muesstest dein Model dazu bringen, immer eine Zeile mehr anzuzeigen, als deine eigentlichen daten haben.
Bei index.row das immer abfangen und die Dummy Zeile Schreiben, vielelicht so gar mit ner anderen Farbe etc. Und wenn er die "bearbeitet", dann den wert in dein Datenmodell einfuegen, die dummy zeile wuerde es dann ne zeile tiefer setzen , ned vergessen die aenderung dem View mitzuteilen ....

Einfacher ist von aussen was einzufuegen ....

Ich faend Context Menues elegent ....

Zum bearbeiten, musst das Model auf editiable schalten ....
Du kannst dem model sogar sagen, das es Zeilen vom View aus loeschen kann (entf druecken bei auswahl etc. )
Dann schiesst dir das view die Events ans model durch, und du musst drauf reagieren ....
du musst dann einfach nur RemoveRows() re-implementieren ...


A removeRows() implementation must call beginRemoveRows() before the rows are removed from the data structure, and it must call endRemoveRows() immediately afterwards.
A removeColumns() implementation must call beginRemoveColumns() before the columns are removed from the data structure, and it must call endRemoveColumns() immediately afterwards.

Allgemein zu den MVC:

Nachteile gibt es:
- viele eigene Besonderheiten sind recht umstaendlich zu implementieren, das ganze ist halt sehr abstract ...
- Performance, ne eigene frickelei die direkt auf nen Itembasierten view alles zusammenschraubt, ist fast immer schneller.

die Nachteile sind aber ganz selten relevant.

demgegenueber stehen die Vorteile:
- MVC ist toll wenn man von seinen Daten mehrere "Sichten" braucht. Alle views benutzen die selbe Schnittstelle zum Datenmoddel.
- Man kann die Views recht schnell austauschen, und recht gut neue implementieren (mal als liste, mal als dropdown, mal als table)
- Abstrakte einheitliche Schnittstelle, du kannst nen Modell bauen und nen anderen entwickler geben, der braucht die daten etc gar ned zu kennen, weis aber wie er ueber das model drauf zugreifen kann. Das modell sagt ihm auch, was alles unterstuetzt wird ...
- Man kann Modells kaskadieren, das heisst der andere entwickler kann nen Modell dazwischenhaengen, was die Ansicht fuer die views "umbaut", ohne die daten kennen zu muessen.

In umfangreicheren Projekten ist sowas Goldwert ....

Hier noch paar links:

http://de.wikipedia.org/wiki/Model_View_Controller
http://de.wikipedia.org/wiki/Entwurfsmuster

Ciao ...
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Beitrag von Thorsten »

Danke für die ausführlichen Antworten. Ich bin ganz begeistert und baue gerade meine ganze Anwendung auf MVC um.

Für den Zugriff auf die Daten habe intuitiv schon mit einer Referenz gearbeitet, was sehr gut funktioniert aber trotzdem sehr langsam ist. (Ich muss mal schauen an was es genau liegt.) Ich habe etwas 5000 bis 10000 Elemente (ingesamt ca. 1 MB), die ich zuvor aus einer Datei lese. In einem anderen Fall habe ich etwas 20 bis 50 Elemente (insgesamt ca. 4 kB), hier werde ich die Daten wohl im Model selbst speichern. Da ich dem Dialog dann noch ein "Abbrechen"-Button spendieren kann. Nur wenn der Anwender auf "Ok" klickt, werden die Daten zurück ins Dokument geschrieben.

Die vier Möglichkeiten zum Erstellen und Löschen von Datensätzen, die ich im Moment überblicke:
- "+"- und "-"-Buttons: Ist sicher die einfachste Variante, die auch der dümmste Anwender versteht. -> Habe schon mit der Implementation angefangen.
- Kontextmenü: Ist sicher als zusätzliche Funktion gut. Aber ausschließlich ein Kontextmenü anzubieten, finde ich für den Anwender etwas versteckt.
- Tastatur: Das Löschen durch "Entf" werde ich in jedem Fall implementieren. (aber später)
- Dummy-Zeile: Finde ich am elegantesten. Scheint aber aufwendiger zu sein. Wenn ich erstmal richtig durchblicke aber sicher auch nicht unmöglich.

Vor- und Nachteile habe ich verstanden. Danke für die Wikipedia-Links.

Weitere Probleme machen die Spaltenbreite der Views diese sind noch nicht sehr anwenderfreundlich. Wie kann ich die dynamisch an die größe der gesammten "Views" bzw. an den Inhalt einer Spalte anpassen? Ich habe mal

Code: Alles auswählen

tableView->verticalHeader()->setResizeMode(QHeaderView::ResizeToContents);
ausprobiert. Ich glaube aber, dass diese Funktion nur den Inhalt des Headers berücksichtigt und nicht den Inhalt der darunter liegenden Items. :-(

Gerade bin ich noch auf ein Problem gestoßen. Ich habe einigen Daten in einer Map gespeichert. Diese hat aber keinen Index, über den ich auf die Daten zugreifen kann. Andererseit ist der Schlüssel aber auch in den Elementen selbst gespeichert. Wie kann sie trotzdem durch ein Model abstrahieren?

Gruß
RHBaum
Beiträge: 1436
Registriert: 17. Juni 2005 09:58

Beitrag von RHBaum »

Ich habe einigen Daten in einer Map gespeichert. Diese hat aber keinen Index, über den ich auf die Daten zugreifen kann. Andererseit ist der Schlüssel aber auch in den Elementen selbst gespeichert. Wie kann sie trotzdem durch ein Model abstrahieren?
RowCount bekommst du immer ziemlich performant aus der Map selber.

fuer die data() abfrage, koenntest du immer bei begin einspringen, und dann index mal den iterator erhoehen.
Fuer kleinere Mengen ok, fuer groessere mengen eher nich.
Vorteil waere, du kannst immer direkt drauf arbeiten.

besser nen vector dann und im vector zeiger auf deine Map-datenpaare halten. So kannst per index schnell aufs richtige paar springen.
nachteil ... sobald sich an deiner Map was anedert, musst den vector nachziehen ... was auch aufwendig sein kann ....

Die richtige loesung in sachen performance ist also abhaengig, wieviele daten du hasst und wie oft die daten im modell sich aendern.

Normal puffert das view selber schon viel ab .... wenn du z.b. rowcount erhoehst, und die ersten daten schon anzeigt, fragt der view nich die daten noch mal ab, sondern erweitert nur den scrollbalken.

Erst wenn der user anfaengt zu scrollen werden die daten die angezeigt werden muessen abgefragt.

Das sollt man auch ausnutzen fuer die performance ...

Oftmals macht es auch keinen sinn jede Aenderung anzuzeigen, sondern auf nem puffer zu arbeiten und nur aller x millisekunden komplett den zustand abzufragen (pollen)

Ciao ...
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Beitrag von Thorsten »

Ich habe mich entschlossen das Map-Problem folgendermaßen zu lösen, da die Datenelement der Map den Schlüssel eh enthalten und die Datenmenge überschaubar ist, nehme ich einfach die Funktion

Code: Alles auswählen

QList<T> QMap::values () const 
um eine Liste mit den Datenelementen zu erstellen. Wenn ich die Änderungen die ich an den Daten dann wirklich übernehmen will, Anwender klickt auf "ok" und nicht auf "Abbrechen", baue ich die Map einfach neu auf. Das ist für mich zur Zeit die einfachste Methode, wenn es späten Probleme mit der Geschwindigkeit gibt, kann ich ja immernoch direkt auf der Map arbeiten.

Das einfügen von Elementen klappt mittlerweile sehr gut.

Ich habe folgendes im Model implementiert:

Code: Alles auswählen

	virtual bool insertRows(int iPosition, int iRows, const QModelIndex &parent = QModelIndex())
	{
		Q_UNUSED(parent);
		
		beginInsertRows(QModelIndex(), iPosition, iPosition + iRows - 1);

		for (int r = 0; r < iRows; ++r)
			p_Datenliste.insert(iPosition, Datenelement());

		endInsertRows();
		return true;
	}

	virtual bool removeRows(int iPosition, int iRows, const QModelIndex &parent = QModelIndex())
	{
		Q_UNUSED(parent);
		
		beginRemoveRows(QModelIndex(), iPosition, iPosition + iRows - 1);

		for (int r = 0; r < iRows; ++r)
			p_Datenliste.removeAt(iPosition);

		endRemoveRows();
		return true;
	}
Hier rufe ich die Funktionen auf:

Code: Alles auswählen

void Dialog::on_pbnInsert_clicked()
{ 
	p_pModel->insertRows(p_pModel->rowCount(), 1);
	tableView->scrollToBottom();
	tableView->resizeRowsToContents();
	tableView->edit(p_pVereinTableModel->index(p_pModel->rowCount() - 1, 0));
}

void Dialog::on_pbnDelete_clicked()
{
//	p_pModel->removeRows(int iPosition, 1);
}
Leider habe ich ein Problem mit der Funktion zum entfernen von Reihen:
Wie komme ich an den Wert von iPosition? Oder besser: Wie komme ich an die Informationen über die Markierung von Elementen. Ich habe meinem View im Designer mitgeteilt, dass nur immer eine einzelne Zeile markiert werden kann. Was auch wunderbar funktioniert. Doch wie komme ich jetzt an deren Index? Wie kann ich den Button "pbnDelete" grau unterlegen, wenn keine Zeile markiert ist?

Gruß
Zuletzt geändert von Thorsten am 6. Februar 2008 22:31, insgesamt 1-mal geändert.
upsala
Beiträge: 3946
Registriert: 5. Februar 2006 20:52
Wohnort: Landshut
Kontaktdaten:

Beitrag von upsala »

Dazu suchst du ab hier weiter:

Code: Alles auswählen

QItemSelectionModel *QAbstractItemView::selectionModel () const
Thorsten
Beiträge: 55
Registriert: 29. Dezember 2004 22:47
Wohnort: Aachen

Beitrag von Thorsten »

Danke!

Code: Alles auswählen

void Dialog::on_pbnDelete_clicked()
{
	QModelIndexList listRows = tableView->selectionModel()->selectedRows();
	if (!listRows.isEmpty())
		p_pModel->removeRows(listRows.first().row(), 1);	
}
Diese funktioniert ganz toll.
Antworten