Modell/Ansicht Tutorial

Jeder UI-Entwickler sollte über die ModelView-Programmierung Bescheid wissen. Ziel dieses Tutorials ist es, Ihnen eine leicht verständliche Einführung in dieses Thema zu geben.

Tabellen-, Listen- und Baum-Widgets sind Komponenten, die häufig in GUIs verwendet werden. Es gibt 2 verschiedene Möglichkeiten, wie diese Widgets auf ihre Daten zugreifen können. Der traditionelle Weg beinhaltet Widgets, die interne Container zum Speichern von Daten enthalten. Dieser Ansatz ist sehr intuitiv, führt aber in vielen nicht-trivialen Anwendungen zu Problemen bei der Datensynchronisation. Der zweite Ansatz ist die Modell-/View-Programmierung, bei der die Widgets keine internen Datencontainer unterhalten. Sie greifen über eine standardisierte Schnittstelle auf externe Daten zu und vermeiden so eine Datendopplung. Dies mag auf den ersten Blick kompliziert erscheinen, aber bei näherer Betrachtung ist es nicht nur leicht zu verstehen, sondern es werden auch die vielen Vorteile der Modell-/View-Programmierung deutlich.

Dabei lernen wir einige grundlegende Technologien von Qt kennen, wie z.B.:

  • Der Unterschied zwischen Standard- und Model/View-Widgets
  • Adapter zwischen Formularen und Modellen
  • Entwicklung einer einfachen Model/View-Anwendung
  • Vordefinierte Modelle
  • Zwischenthemen wie z.B.:
    • Baumansichten
    • Auswahl
    • Delegierte
    • Fehlersuche mit Modelltest

Sie werden auch erfahren, ob Ihre neue Anwendung einfacher mit Model/View-Programmierung geschrieben werden kann oder ob klassische Widgets genauso gut funktionieren.

Dieses Tutorial enthält Beispielcode, den Sie bearbeiten und in Ihr Projekt integrieren können. Der Quellcode des Tutorials befindet sich im Verzeichnis examples/widgets/tutorials/modelview von Qt.

Für detailliertere Informationen können Sie auch einen Blick in die Referenzdokumentation werfen

1. Einführung

Model/View ist eine Technologie, die verwendet wird, um Daten von Ansichten in Widgets zu trennen, die Datensätze verarbeiten. Standard-Widgets sind nicht dafür ausgelegt, Daten von Ansichten zu trennen, und deshalb gibt es in Qt zwei verschiedene Arten von Widgets. Beide Arten von Widgets sehen gleich aus, aber sie interagieren unterschiedlich mit Daten.

Standard-Widgets verwenden Daten, die Teil des Widgets sind.

View-Klassen arbeiten mit externen Daten (dem Modell)

1.1 Standard-Widgets

Schauen wir uns ein Standard-Tabellen-Widget genauer an. Ein Tabellen-Widget ist ein 2D-Array mit den Datenelementen, die der Benutzer ändern kann. Das Tabellen-Widget kann in einen Programmablauf integriert werden, indem die Datenelemente, die das Tabellen-Widget bereitstellt, gelesen und geschrieben werden. Diese Methode ist sehr intuitiv und in vielen Anwendungen nützlich, aber die Anzeige und Bearbeitung einer Datenbanktabelle mit einem Standard-Table-Widget kann problematisch sein. Es müssen zwei Kopien der Daten koordiniert werden: eine außerhalb des Widgets und eine innerhalb des Widgets. Der Entwickler ist für die Synchronisierung beider Versionen verantwortlich. Außerdem erschwert die enge Kopplung von Darstellung und Daten das Schreiben von Unit-Tests.

1.2 Model/View als Retter in der Not

Mit Model/View wurde eine Lösung gefunden, die eine vielseitigere Architektur verwendet. Model/View beseitigt die Datenkonsistenzprobleme, die bei Standard-Widgets auftreten können. Model/View macht es auch einfacher, mehr als eine Ansicht der gleichen Daten zu verwenden, da ein Modell an viele Ansichten weitergegeben werden kann. Der wichtigste Unterschied ist, dass Model/View-Widgets die Daten nicht hinter den Tabellenzellen speichern. Vielmehr arbeiten sie direkt mit Ihren Daten. Da die View-Klassen die Struktur Ihrer Daten nicht kennen, müssen Sie einen Wrapper bereitstellen, um Ihre Daten an die Schnittstelle QAbstractItemModel anzupassen. Eine Ansicht verwendet diese Schnittstelle, um von Ihren Daten zu lesen und in sie zu schreiben. Jede Instanz einer Klasse, die QAbstractItemModel implementiert, wird als Modell bezeichnet. Sobald der View einen Zeiger auf ein Model erhält, liest und zeigt er dessen Inhalt an und ist dessen Editor.

1.3 Überblick über die Model/View-Widgets

Es folgt ein Überblick über die Model/View-Widgets und ihre entsprechenden Standard-Widgets.

WidgetStandard-Widget
(eine Element-basierte Komfortklasse)
Model/View View Klasse
(zur Verwendung mit externen Daten)
QListWidgetQListView
QTableWidgetQTableView
QTreeWidgetQTreeView
QColumnView zeigt einen Baum als eine Hierarchie von Listen
QComboBox kann sowohl als Ansichtsklasse als auch als traditionelles Widget verwendet werden

1.4 Verwendung von Adaptern zwischen Formularen und Modellen

Die Verwendung von Adaptern zwischen Formularen und Modellen kann sich als nützlich erweisen.

Wir können in Tabellen gespeicherte Daten direkt in der Tabelle selbst bearbeiten, aber es ist viel bequemer, Daten in Textfeldern zu bearbeiten. Es gibt kein direktes Modell/Ansicht-Gegenstück, das Daten und Ansichten für Widgets trennt, die mit einem Wert (QLineEdit, QCheckBox...) anstelle eines Datensatzes arbeiten, daher benötigen wir einen Adapter, um das Formular mit der Datenquelle zu verbinden.

QDataWidgetMapper ist eine großartige Lösung, weil sie Formular-Widgets auf eine Tabellenzeile abbildet und es sehr einfach macht, Formulare für Datenbanktabellen zu erstellen.

Ein weiteres Beispiel für einen Adapter ist QCompleter. Qt verfügt über QCompleter zur Bereitstellung von Autovervollständigungen in Qt-Widgets wie QComboBox und, wie unten gezeigt, QLineEdit. QCompleter verwendet ein Modell als Datenquelle.

2. Eine einfache Model/View-Anwendung

Wenn Sie eine Model/View-Anwendung entwickeln wollen, wo sollten Sie anfangen? Wir empfehlen, mit einem einfachen Beispiel zu beginnen und es Schritt für Schritt zu erweitern. Dies erleichtert das Verständnis der Architektur erheblich. Der Versuch, die Model/View-Architektur im Detail zu verstehen, bevor man die IDE aufruft, hat sich für viele Entwickler als weniger bequem erwiesen. Es ist wesentlich einfacher, mit einer einfachen Model/View-Anwendung zu beginnen, die Demodaten enthält. Probieren Sie es aus! Ersetzen Sie einfach die Daten in den folgenden Beispielen durch Ihre eigenen.

Im Folgenden finden Sie 7 sehr einfache und unabhängige Anwendungen, die verschiedene Seiten der Model/View-Programmierung zeigen. Der Quellcode ist im Verzeichnis examples/widgets/tutorials/modelview zu finden.

2.1 Eine schreibgeschützte Tabelle

Wir beginnen mit einer Anwendung, die eine QTableView verwendet, um Daten anzuzeigen. Wir werden später Bearbeitungsmöglichkeiten hinzufügen.

(Dateiquelle: examples/widgets/tutorials/modelview/1_readonly/main.cpp)

// main.cpp
#include <QApplication>
#include <QTableView>
#include "mymodel.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QTableView tableView;
    MyModel myModel;
    tableView.setModel(&myModel);
    tableView.show();
    return a.exec();
}

Wir haben die übliche main() -Funktion:

Hier ist der interessante Teil: Wir erstellen eine Instanz von MyModel und verwenden tableView.setModel(&myModel); um einen Zeiger davon an tableView zu übergeben. tableView wird die Methoden des Zeigers aufrufen, den es erhalten hat, um zwei Dinge herauszufinden:

  • Wie viele Zeilen und Spalten angezeigt werden sollen.
  • Welcher Inhalt in jede Zelle gedruckt werden soll.

Das Modell braucht etwas Code, um darauf zu reagieren.

Wir haben einen Tabellendatensatz, also beginnen wir mit QAbstractTableModel, da es einfacher zu verwenden ist als das allgemeinere QAbstractItemModel.

(Dateiquelle: examples/widgets/tutorials/modelview/1_readonly/mymodel.h)

// mymodel.h
#include <QAbstractTableModel>

class MyModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit MyModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};

QAbstractTableModel erfordert die Implementierung von drei abstrakten Methoden.

(Dateiquelle: examples/widgets/tutorials/modelview/1_readonly/mymodel.cpp)

// mymodel.cpp
#include "mymodel.h"

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

int MyModel::rowCount(const QModelIndex & /*parent*/) const
{
   return 2;
}

int MyModel::columnCount(const QModelIndex & /*parent*/) const
{
    return 3;
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::DisplayRole)
       return QString("Row%1, Column%2")
                   .arg(index.row() + 1)
                   .arg(index.column() +1);

    return QVariant();
}

Die Anzahl der Zeilen und Spalten wird durch MyModel::rowCount() und MyModel::columnCount() bereitgestellt. Wenn die Ansicht wissen muss, wie der Text der Zelle lautet, ruft sie die Methode MyModel::data() auf. Die Zeilen- und Spalteninformationen werden mit dem Parameter index angegeben und die Rolle wird auf Qt::DisplayRole gesetzt. Andere Rollen werden im nächsten Abschnitt behandelt. In unserem Beispiel werden die Daten, die angezeigt werden sollen, generiert. In einer realen Anwendung würde MyModel ein Mitglied namens MyData haben, das als Ziel für alle Lese- und Schreibvorgänge dient.

Dieses kleine Beispiel verdeutlicht die passive Natur eines Modells. Das Modell weiß nicht, wann es verwendet wird oder welche Daten benötigt werden. Es stellt einfach jedes Mal Daten zur Verfügung, wenn die Ansicht sie anfordert.

Was geschieht, wenn die Daten des Modells geändert werden müssen? Wie erkennt die Ansicht, dass sich die Daten geändert haben und erneut gelesen werden müssen? Das Modell muss ein Signal aussenden, das angibt, welcher Bereich von Zellen sich geändert hat. Dies wird in Abschnitt 2.3 gezeigt.

2.2 Erweiterung des Nur-Lese-Beispiels mit Rollen

Das Modell steuert nicht nur, welchen Text die Ansicht anzeigt, sondern auch das Aussehen des Textes. Wenn wir das Modell leicht verändern, erhalten wir das folgende Ergebnis:

Tatsächlich muss nichts außer der Methode data() geändert werden, um Schriftarten, Hintergrundfarbe, Ausrichtung und ein Kontrollkästchen festzulegen. Nachfolgend sehen Sie die Methode data(), die das oben gezeigte Ergebnis liefert. Der Unterschied besteht darin, dass wir dieses Mal den Parameter int role verwenden, um je nach seinem Wert verschiedene Informationen zurückzugeben.

(Dateiquelle: examples/widgets/tutorials/modelview/2_formatting/mymodel.cpp)

// mymodel.cppQVariant MyModel::data(const QModelIndex &index, int role) const{ int row = index.row(); int col = index.column(); // Erzeugt eine Logmeldung, wenn diese Methode aufgerufen wird    qDebug() << QString("row %1, col%2, role %3")
           .arg(row).arg(col).arg(role); switch (role) { case Qt::DisplayRole: if (row == 0 && col == 1) return QString("<--left"); if (row == 1 && col == 1) return QString("rechts-->"); return QString("Zeile%1, Spalte%2") .arg(Zeile + 1) .arg(Spalte+1); case Qt::FontRole: if (row == 0 && col == 0) { // Schriftart nur für Zelle(0,0) ändern      QFont boldFont; boldFont.setBold(true); return boldFont; } break; case Qt::BackgroundRole: if (row == 1 && col == 2) // Hintergrund nur für Zelle(1,2) ändern return QBrush(Qt::red); break; case Qt::TextAlignmentRole: if (row == 1 && col == 1) // Textausrichtung nur für Zelle(1,1) ändern return int(Qt::AlignRight | Qt::AlignVCenter); break; case Qt::CheckStateRole: if (row == 1 && col == 0) // ein Kontrollkästchen in Zelle(1,0) einfügen return Qt::Checked; break; } return QVariant(); }

Jede Formatierungseigenschaft wird vom Modell mit einem separaten Aufruf der Methode data() angefordert. Der Parameter role wird verwendet, um dem Modell mitzuteilen, welche Eigenschaft angefordert wird:

enum Qt::ItemDataRoleBedeutungTyp
Qt::DisplayRoleTextQString
Qt::FontRoleSchriftartQFont
BackgroundRolePinsel für den Hintergrund der ZelleQBrush
Qt::TextAlignmentRoleTextausrichtungenum Qt::AlignmentFlag
Qt::CheckStateRoleunterdrückt Kontrollkästchen mit QVariant(),

setzt Kontrollkästchen mit Qt::Checked

oder Qt::Unchecked

enum Qt::ItemDataRole

Lesen Sie die Qt-Namespace-Dokumentation, um mehr über die Möglichkeiten von Qt::ItemDataRole enum zu erfahren.

Nun müssen wir feststellen, wie sich die Verwendung eines getrennten Modells auf die Leistung der Anwendung auswirkt. Wir wollen also verfolgen, wie oft die Ansicht die Methode data() aufruft. Um zu verfolgen, wie oft die Ansicht das Modell aufruft, haben wir eine Debug-Anweisung in die Methode data() eingefügt, die in den Fehlerausgabestrom protokolliert wird. In unserem kleinen Beispiel wird die Methode data() 42 Mal aufgerufen. Jedes Mal, wenn Sie mit dem Cursor über das Feld fahren, wird data() erneut aufgerufen - 7 Mal für jede Zelle. Deshalb ist es wichtig, dafür zu sorgen, dass Ihre Daten verfügbar sind, wenn data() aufgerufen wird, und dass teure Nachschlageoperationen zwischengespeichert werden.

2.3 Eine Uhr innerhalb einer Tabellenzelle

Wir haben immer noch eine schreibgeschützte Tabelle, aber dieses Mal ändert sich der Inhalt jede Sekunde, weil wir die aktuelle Zeit anzeigen.

(Dateiquelle: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    int row = index.row();
    int col = index.column();

    if (role == Qt::DisplayRole && row == 0 && col == 0)
        return QTime::currentTime().toString();

    return QVariant();
}

Es fehlt noch etwas, um die Uhr zum Ticken zu bringen. Wir müssen der Ansicht jede Sekunde mitteilen, dass sich die Zeit geändert hat und dass sie neu gelesen werden muss. Wir tun dies mit einem Timer. Im Konstruktor setzen wir sein Intervall auf 1 Sekunde und schließen sein Timeout-Signal an.

(Dateiquelle: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
    , timer(new QTimer(this))
{
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout , this, &MyModel::timerHit);
    timer->start();
}

Hier ist der entsprechende Slot:

(Dateiquelle: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

void MyModel::timerHit()
{
    // we identify the top left cell
    QModelIndex topLeft = createIndex(0,0);
    // emit a signal to make the view reread identified data
    emit dataChanged(topLeft, topLeft, {Qt::DisplayRole});
}

Wir fordern die Ansicht auf, die Daten in der oberen linken Zelle erneut zu lesen, indem wir das Signal dataChanged() ausgeben. Beachten Sie, dass wir das Signal dataChanged() nicht explizit mit der Ansicht verbunden haben. Dies geschah automatisch, als wir setModel() aufriefen.

2.4 Einrichten von Überschriften für Spalten und Zeilen

Kopfzeilen können über eine View-Methode ausgeblendet werden: tableView->verticalHeader()->hide();

Der Inhalt der Kopfzeile wird jedoch über das Modell festgelegt, weshalb wir die Methode headerData() neu implementieren:

(Dateiquelle: examples/widgets/tutorials/modelview/4_headers/mymodel.cpp)

QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        switch (section) {
        case 0:
            return QString("first");
        case 1:
            return QString("second");
        case 2:
            return QString("third");
        }
    }
    return QVariant();
}

Beachten Sie, dass die Methode headerData() auch einen Parameter role hat, der die gleiche Bedeutung hat wie in MyModel::data().

2.5 Das Minimal-Editing-Beispiel

In diesem Beispiel werden wir eine Anwendung erstellen, die automatisch einen Fenstertitel mit Inhalt füllt, indem sie die in Tabellenzellen eingegebenen Werte wiederholt. Um auf den Fenstertitel einfach zugreifen zu können, setzen wir die QTableView in eine QMainWindow.

Das Modell entscheidet, ob Bearbeitungsfunktionen verfügbar sind. Wir müssen nur das Modell ändern, damit die verfügbaren Bearbeitungsfunktionen aktiviert werden. Dies geschieht durch Neuimplementierung der folgenden virtuellen Methoden: setData() und flags().

(Dateiquelle: examples/widgets/tutorials/modelview/5_edit/mymodel.h)

// mymodel.h
#include <QAbstractTableModel>
#include <QString>

const int COLS= 3;
const int ROWS= 2;

class MyModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    MyModel(QObject *parent = nullptr);
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
    QString m_gridData[ROWS][COLS];  //holds text entered into QTableView
signals:
    void editCompleted(const QString &);
};

Wir verwenden the zweidimensionales Array QString m_gridData um unsere Daten zu speichern. Damit ist m_gridData der Kern von MyModel. Der Rest von MyModel wirkt wie ein Wrapper und passt m_gridData an die Schnittstelle QAbstractItemModel an. Wir haben auch das Signal editCompleted() eingeführt, das es ermöglicht, den geänderten Text in den Fenstertitel zu übertragen.

(Dateiquelle: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::EditRole) {
        if (!checkIndex(index))
            return false;
        //save value from editor to member m_gridData
        m_gridData[index.row()][index.column()] = value.toString();
        //for presentation purposes only: build and emit a joined string
        QString result;
        for (int row = 0; row < ROWS; row++) {
            for (int col= 0; col < COLS; col++)
                result += m_gridData[row][col] + ' ';
        }
        emit editCompleted(result);
        return true;
    }
    return false;
}

setData() wird jedes Mal aufgerufen, wenn der Benutzer eine Zelle editiert. Der Parameter index teilt uns mit, welches Feld bearbeitet wurde, und value liefert das Ergebnis des Bearbeitungsvorgangs. Die Rolle wird immer auf Qt::EditRole gesetzt, da unsere Zellen nur Text enthalten. Wenn ein Kontrollkästchen vorhanden wäre und die Benutzerrechte so eingestellt sind, dass das Kontrollkästchen ausgewählt werden kann, würden die Aufrufe auch mit der auf Qt::CheckStateRole eingestellten Rolle erfolgen.

(Dateiquelle: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

Verschiedene Eigenschaften einer Zelle können mit flags() angepasst werden.

Die Rückgabe von Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled reicht aus, um einem Editor zu zeigen, dass eine Zelle ausgewählt werden kann.

Werden durch die Bearbeitung einer Zelle mehr Daten als die Daten in dieser Zelle geändert, muss das Modell ein Signal dataChanged() ausgeben, damit die geänderten Daten gelesen werden können.

3. Zwischenthemen

3.1 Baumansicht

Sie können das obige Beispiel in eine Anwendung mit einer Baumansicht umwandeln. Ersetzen Sie einfach QTableView durch QTreeView, was zu einem Lese-/Schreibbaum führt. Am Modell müssen keine Änderungen vorgenommen werden. Der Baum wird keine Hierarchien haben, da es im Modell selbst keine Hierarchien gibt.

QListView QTableView und verwenden alle eine Modellabstraktion, die eine Mischung aus Liste, Tabelle und Baum ist. Dadurch ist es möglich, mehrere verschiedene Arten von Ansichtsklassen auf der Grundlage desselben Modells zu verwenden. QTreeView

So sieht unser Beispielmodell bisher aus:

Wir wollen einen echten Baum darstellen. Wir haben unsere Daten in den obigen Beispielen verpackt, um ein Modell zu erstellen. Diesmal verwenden wir QStandardItemModel, einen Container für hierarchische Daten, der auch QAbstractItemModel implementiert. Um einen Baum darzustellen, muss QStandardItemModel mit QStandardItembefüllt werden, die alle Standardeigenschaften von Elementen wie Text, Schriftarten, Kontrollkästchen oder Pinsel enthalten können.

(Dateiquelle: examples/widgets/tutorials/modelview/6_treeview/mainwindow.cpp)

// modelview.cpp
#include "mainwindow.h"

#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , treeView(new QTreeView(this))
    , standardModel(new QStandardItemModel(this))
{
    setCentralWidget(treeView);

    QList<QStandardItem *> preparedRow = prepareRow("first", "second", "third");
    QStandardItem *item = standardModel->invisibleRootItem();
    // adding a row to the invisible root item produces a root element
    item->appendRow(preparedRow);

    QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
    // adding a row to an item starts a subtree
    preparedRow.first()->appendRow(secondRow);

    treeView->setModel(standardModel);
    treeView->expandAll();
}

QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
                                              const QString &second,
                                              const QString &third) const
{
    return {new QStandardItem(first),
            new QStandardItem(second),
            new QStandardItem(third)};
}

Wir instanziieren einfach ein QStandardItemModel und fügen dem Konstruktor ein paar QStandardItems hinzu. Wir können dann eine hierarchische Datenstruktur erstellen, da ein QStandardItem andere QStandardItems enthalten kann. Die Knoten werden innerhalb der Ansicht ein- und ausgeklappt.

3.2 Arbeiten mit Auswahlen

Wir wollen auf den Inhalt eines ausgewählten Elements zugreifen, um ihn zusammen mit der Hierarchieebene im Fenstertitel auszugeben.

Legen wir also ein paar Elemente an:

(Dateiquelle: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

#include "mainwindow.h"

#include <QTreeView>
#include <QStandardItemModel>
#include <QItemSelectionModel>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , treeView(new QTreeView(this))
    , standardModel(new QStandardItemModel(this))
{
    setCentralWidget(treeView);
    auto *rootNode = standardModel->invisibleRootItem();

    // defining a couple of items
    auto *americaItem = new QStandardItem("America");
    auto *mexicoItem =  new QStandardItem("Canada");
    auto *usaItem =     new QStandardItem("USA");
    auto *bostonItem =  new QStandardItem("Boston");
    auto *europeItem =  new QStandardItem("Europe");
    auto *italyItem =   new QStandardItem("Italy");
    auto *romeItem =    new QStandardItem("Rome");
    auto *veronaItem =  new QStandardItem("Verona");

    // building up the hierarchy
    rootNode->    appendRow(americaItem);
    rootNode->    appendRow(europeItem);
    americaItem-> appendRow(mexicoItem);
    americaItem-> appendRow(usaItem);
    usaItem->     appendRow(bostonItem);
    europeItem->  appendRow(italyItem);
    italyItem->   appendRow(romeItem);
    italyItem->   appendRow(veronaItem);

    // register the model
    treeView->setModel(standardModel);
    treeView->expandAll();

    // selection changes shall trigger a slot
    QItemSelectionModel *selectionModel = treeView->selectionModel();
    connect(selectionModel, &QItemSelectionModel::selectionChanged,
            this, &MainWindow::selectionChangedSlot);
}

Ansichten verwalten Auswahlen innerhalb eines separaten Auswahlmodells, das mit der Methode selectionModel() abgerufen werden kann. Wir rufen das Auswahlmodell ab, um einen Slot mit seinem selectionChanged()-Signal zu verbinden.

(Dateiquelle: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
{
    // get the text of the selected item
    const QModelIndex index = treeView->selectionModel()->currentIndex();
    QString selectedText = index.data(Qt::DisplayRole).toString();
    // find out the hierarchy level of the selected item
    int hierarchyLevel = 1;
    QModelIndex seekRoot = index;
    while (seekRoot.parent().isValid()) {
        seekRoot = seekRoot.parent();
        hierarchyLevel++;
    }
    QString showString = QString("%1, Level %2").arg(selectedText)
                         .arg(hierarchyLevel);
    setWindowTitle(showString);
}

Wir erhalten den Modellindex, der der Auswahl entspricht, indem wir treeView->selectionModel()->currentIndex() aufrufen, und wir erhalten den String des Feldes, indem wir den Modellindex verwenden. Dann berechnen wir einfach das Element hierarchyLevel. Elemente der obersten Ebene haben keine Eltern und die Methode parent() gibt ein standardmäßig konstruiertes QModelIndex() zurück. Aus diesem Grund verwenden wir die Methode parent(), um zur obersten Ebene zu iterieren und dabei die während der Iteration durchgeführten Schritte zu zählen.

Das Auswahlmodell (wie oben gezeigt) kann abgerufen, aber auch mit QAbstractItemView::setSelectionModel festgelegt werden. Auf diese Weise ist es möglich, 3 View-Klassen mit synchronisierten Selektionen zu haben, da nur eine Instanz eines Selektionsmodells verwendet wird. Um ein Selektionsmodell zwischen 3 Views zu teilen, verwenden Sie selectionModel() und weisen das Ergebnis der zweiten und dritten View-Klasse mit setSelectionModel() zu.

3.3 Vordefinierte Modelle

Der typische Weg, Model/View zu verwenden, besteht darin, spezifische Daten zu verpacken, um sie mit View-Klassen nutzbar zu machen. Qt bietet jedoch auch vordefinierte Modelle für gängige zugrunde liegende Datenstrukturen. Wenn eine der verfügbaren Datenstrukturen für Ihre Anwendung geeignet ist, kann ein vordefiniertes Modell eine gute Wahl sein.

QStringListModelSpeichert eine Liste von Strings
QStandardItemModelSpeichert beliebige hierarchische Elemente
QFileSystemModeldas lokale Dateisystem kapseln
QSqlQueryModelkapselt eine SQL-Ergebnismenge
QSqlTableModelkapselt eine SQL-Tabelle
QSqlRelationalTableModelkapselt eine SQL-Tabelle mit Fremdschlüsseln
QSortFilterProxyModelSortiert und/oder filtert ein anderes Modell

3.4 Delegierte

In allen bisherigen Beispielen werden die Daten als Text oder Kontrollkästchen in einer Zelle dargestellt und als Text oder Kontrollkästchen bearbeitet. Die Komponente, die diese Darstellungs- und Bearbeitungsdienste bereitstellt, wird als Delegat bezeichnet. Wir fangen gerade erst an, mit dem Delegaten zu arbeiten, weil die Ansicht einen Standarddelegaten verwendet. Aber stellen Sie sich vor, dass wir einen anderen Editor haben wollen (z.B. einen Schieberegler oder eine Dropdown-Liste) oder stellen Sie sich vor, dass wir Daten als Grafiken darstellen wollen. Schauen wir uns ein Beispiel mit dem Namen Star Delegate an, in dem Sterne verwendet werden, um eine Bewertung anzuzeigen:

Die Ansicht hat eine setItemDelegate()-Methode, die den Standarddelegaten ersetzt und einen eigenen Delegaten installiert. Ein neuer Delegat kann geschrieben werden, indem eine Klasse erstellt wird, die von QStyledItemDelegate erbt. Um einen Delegaten zu schreiben, der Sterne anzeigt und keine Eingabemöglichkeiten hat, müssen wir nur 2 Methoden außer Kraft setzen.

class StarDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    StarDelegate(QWidget *parent = nullptr);
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const;
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const;
};

paint() zeichnet Sterne in Abhängigkeit vom Inhalt der zugrunde liegenden Daten. Die Daten können durch den Aufruf von index.data() nachgeschlagen werden. Die Methode sizeHint() des Delegaten wird verwendet, um die Abmessungen jedes Sterns zu ermitteln, damit die Zelle genügend Höhe und Breite für die Sterne bietet.

Das Schreiben von benutzerdefinierten Delegaten ist die richtige Wahl, wenn Sie Ihre Daten mit einer benutzerdefinierten grafischen Darstellung innerhalb des Gitters der View-Klasse anzeigen möchten. Wenn Sie das Raster verlassen wollen, würden Sie keinen benutzerdefinierten Delegaten, sondern eine benutzerdefinierte Ansichtsklasse verwenden.

Andere Referenzen zu Delegaten in der Qt Dokumentation:

3.5 Fehlersuche mit ModelTest

Die passive Natur von Modellen stellt den Programmierer vor neue Herausforderungen. Inkonsistenzen im Modell können die Anwendung zum Absturz bringen. Da das Modell von zahlreichen Aufrufen aus der Ansicht getroffen wird, ist es schwierig, herauszufinden, welcher Aufruf die Anwendung zum Absturz gebracht hat und welche Operation das Problem verursacht hat.

Qt Labs bietet eine Software namens ModelTest an, die Modelle überprüft, während Ihre Programmierung läuft. Jedes Mal, wenn das Modell geändert wird, scannt ModelTest das Modell und meldet Fehler mit einer Assert. Dies ist besonders wichtig für Baummodelle, da deren hierarchische Natur viele Möglichkeiten für subtile Inkonsistenzen lässt.

Im Gegensatz zu View-Klassen verwendet ModelTest Indizes, die außerhalb des Bereichs liegen, um das Modell zu testen. Das bedeutet, dass Ihre Anwendung mit ModelTest abstürzen kann, auch wenn sie ohne ModelTest perfekt läuft. Sie müssen also auch alle Indizes behandeln, die außerhalb des Bereichs liegen, wenn Sie ModelTest verwenden.

4. Gute Quellen für zusätzliche Informationen

4.1 Bücher

Die Model/View-Programmierung wird in der Qt-Dokumentation, aber auch in mehreren guten Büchern recht ausführlich behandelt.

  1. C++ GUI Programming with Qt 4 / Jasmin Blanchette, Mark Summerfield, Prentice Hall, 2. Auflage, ISBN 0-13-235416-0. Auch auf Deutsch erhältlich: C++ GUI Programmierung mit Qt 4: Die offizielle Einführung, Addison-Wesley, ISBN 3-827327-29-6
  2. Das Buch zu Qt4, Die Kunst, Qt-Anwendungen zu entwickeln / Daniel Molkentin, Open Source Press, ISBN 1-59327-147-6. Übersetzt aus Qt 4, Einführung in die Applikationsentwicklung, Open Source Press, ISBN 3-937514-12-0.
  3. Grundlagen der Qt-Entwicklung / Johan Thelin, Apress, ISBN 1-59059-831-8.
  4. Fortgeschrittene Qt-Programmierung / Mark Summerfield, Prentice Hall, ISBN 0-321-63590-6. Dieses Buch behandelt die Model/View-Programmierung auf mehr als 150 Seiten.

Die folgende Liste gibt einen Überblick über Beispielprogramme, die in den ersten drei oben genannten Büchern enthalten sind. Einige von ihnen eignen sich sehr gut als Vorlage für die Entwicklung ähnlicher Anwendungen.

Name des BeispielsVerwendete View-KlasseVerwendetes ModellBehandelte Aspekte
TeamleiterQListviewQStringListModelBuch 1, Kapitel 10, Abbildung 10.6
Farbe NamenQListViewQSortFilterProxyModel angewendet auf QStringListModelBuch 1, Kapitel 10, Abbildung 10.8
WährungenQTableViewbenutzerdefiniertes Modell basierend auf QAbstractTableModelNur lesenBuch 1, Kapitel 10, Abbildung 10.10
StädteQTableViewBenutzerdefiniertes Modell auf der Grundlage von QAbstractTableModelLesen/SchreibenBuch 1, Kapitel 10, Abbildung 10.12
Boolescher ParserQTreeViewBenutzerdefiniertes Modell auf der Grundlage von QAbstractItemModelNur LesenBuch 1, Kapitel 10, Abbildung 10.14
Spur-EditorQTableWidgetBenutzerdefinierter Delegat, der einen benutzerdefinierten Editor bereitstelltBuch 1, Kapitel 10, Abbildung 10.15
AdressbuchQListView QTableView QTreeViewBenutzerdefiniertes Modell basierend auf QAbstractTableModelLesen / SchreibenBuch2, Kapitel 8.4
Adressbuch mit SortierungQSortfilterProxyModelEinführung von Sortier- und FilterfunktionenBuch2, Kapitel 8.5
Adressbuch mit CheckboxenEinführung von Checkboxen in Model/ViewBuch2, Kapitel 8.6
Adressbuch mit transponiertem RasterBenutzerdefiniertes Proxy-Modell basierend auf QAbstractProxyModelEinführen eines benutzerdefinierten ModellsBuch2, Kapitel 8.7
Adressbuch mit Drag and DropEinführung von Drag & Drop-UnterstützungBuch2, Kapitel 8.8
Adressbuch mit benutzerdefiniertem EditorEinführung von benutzerdefinierten DelegiertenBuch2, Kapitel 8.9
AnsichtenQListView QTableView QTreeViewQStandardItemModelNur lesenBuch 3, Kapitel 5, Abbildung 5-3
BardelegateQTableViewBenutzerdefinierter Delegierter für die Präsentation basierend auf QAbstractItemDelegateBuch 3, Kapitel 5, Abbildung 5-5
BearbeitungsdelegierterQTableViewBenutzerdefinierter Delegierter für die Bearbeitung basierend auf QAbstractItemDelegateBuch 3, Kapitel 5, Abbildung 5-6
EinzelansichtBenutzerdefinierte Ansicht basierend auf QAbstractItemViewBenutzerdefinierte AnsichtBuch 3, Kapitel 5, Abbildung 5-7
ListenmodellQTableViewBenutzerdefiniertes Modell basierend auf QAbstractTableModelNur lesenBuch 3, Kapitel 5, Abbildung 5-8
treemodelQTreeViewBenutzerdefiniertes Modell basierend auf QAbstractItemModelNur lesenBuch 3, Kapitel 5, Abbildung 5-10
Ganzzahlen bearbeitenQListViewBenutzerdefiniertes Modell basierend auf QAbstractListModelLesen/SchreibenBuch 3, Kapitel 5, Listing 5-37, Abbildung 5-11
SortierenQTableViewQSortFilterProxyModel angewendet auf QStringListModelDemonstriert die SortierungBuch 3, Kapitel 5, Abbildung 5-12

4.2 Qt-Dokumentation

Qt 5.0 wird mit 19 Beispielen für Model/View ausgeliefert. Die Beispiele sind auf der Seite Item Views Examples zu finden.

Name des BeispielsVerwendete View-KlasseVerwendetes ModelAbgedeckte Aspekte
AdressbuchQTableViewQAbstractTableModel QSortFilterProxyModelVerwendung von QSortFilterProxyModel zur Erzeugung verschiedener Teilmengen aus einem Datenpool
Grundlegendes Sortier-/FiltermodellQTreeViewQStandardItemModel QSortFilterProxyModel
DiagrammBenutzerdefinierte AnsichtQStandardItemModelEntwerfen von benutzerdefinierten Ansichten, die mit Auswahlmodellen zusammenarbeiten
Farb-Editor-FabrikQTableWidgetErweiterung des Standarddelegaten um einen neuen benutzerdefinierten Editor zur Auswahl von Farben
Combo Widget MapperQDataWidgetMapper zur Abbildung von QLineEdit, QTextEdit und QComboBoxQStandardItemModelZeigt, wie eine QComboBox als Ansichtsklasse dienen kann
Benutzerdefiniertes Sortier-/FiltermodellQTreeViewQStandardItemModel QSortFilterProxyModelUnterklasse QSortFilterProxyModel für erweiterte Sortierung und Filterung
Dir AnsichtQTreeViewQFileSystemModelSehr kleines Beispiel, um zu zeigen, wie man ein Modell einer Ansicht zuordnet
Editierbares BaummodellQTreeViewBenutzerdefiniertes BaummodellUmfassendes Beispiel für die Arbeit mit Bäumen, demonstriert die Bearbeitung von Zellen und Baumstruktur mit einem zugrunde liegenden benutzerdefinierten Modell
Abrufen MehrQListViewBenutzerdefiniertes ListenmodellDynamisch wechselndes Modell
Eingefrorene SäuleQTableViewQStandardItemModel
BefragungMehrereBenutzerdefiniertes ElementmodellMehrere Ansichten
PixelatorQTableViewBenutzerdefiniertes TabellenmodellImplementierung eines benutzerdefinierten Delegaten
PuzzleQListViewBenutzerdefiniertes ListenmodellModell/Ansicht mit Ziehen und Ablegen
Einfaches DOM-ModellQTreeViewBenutzerdefiniertes BaummodellNur-Lese-Beispiel für ein benutzerdefiniertes Baummodell
Einfaches BaummodellQTreeViewBenutzerdefiniertes BaummodellNur-Lese-Beispiel für ein benutzerdefiniertes Tree Model
Einfacher Widget-MapperQDataWidgetMapper zur Abbildung von QLineEdit, QTextEdit und QSpinBoxQStandardItemModelGrundlegende QDataWidgetMapper Verwendung
TabellenkalkulationQTableViewBenutzerdefinierte Delegierte
Stern-DelegierterQTableWidgetUmfassendes Beispiel für benutzerdefinierte Delegaten.

Ein Referenzdokument für die Model/View-Technologie ist ebenfalls verfügbar.

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.