Auf dieser Seite

Model/View-Tutorial

Jeder UI-Entwickler sollte sich mit der Model-View-Programmierung auskennen. Das Ziel dieses Tutorials ist es, Ihnen eine leicht verständliche Einführung in dieses Thema zu bieten.

Tabellen-, Listen- und Baum-Widgets sind Komponenten, die in grafischen Benutzeroberflächen häufig zum Einsatz kommen. Es gibt zwei verschiedene Möglichkeiten, wie diese Widgets auf ihre Daten zugreifen können. Der traditionelle Ansatz besteht darin, dass Widgets interne Container zum Speichern von Daten enthalten. Dieser Ansatz ist sehr intuitiv, führt jedoch in vielen nicht trivialen Anwendungen zu Problemen bei der Datensynchronisation. Der zweite Ansatz ist die Model-View-Programmierung, bei der Widgets keine internen Datencontainer verwalten. Sie greifen über eine standardisierte Schnittstelle auf externe Daten zu und vermeiden so Datenduplikate. Das mag auf den ersten Blick kompliziert erscheinen, aber bei genauerer Betrachtung ist es nicht nur leicht zu verstehen, sondern die vielen Vorteile der Model-View-Programmierung werden auch deutlicher.

Verzeichnis und dessen Inhalt in einer Baumansicht

Dabei werden wir einige grundlegende Technologien kennenlernen, die Qt bereitstellt, wie zum Beispiel:

  • Der Unterschied zwischen Standard- und Model-View-Widgets
  • Adapter zwischen Formularen und Modellen
  • Entwicklung einer einfachen Model-View-Anwendung
  • Vordefinierte Modelle
  • Fortgeschrittene Themen wie:
    • Baumansichten
    • Auswahl
    • Delegaten
    • Debugging mit Modelltests

Außerdem erfahren Sie, ob sich Ihre neue Anwendung mit Model-View-Programmierung einfacher erstellen lässt 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 empfehlen wir Ihnen außerdem, einen Blick in die Referenzdokumentation zu werfen.

1. Einführung

„Model/View“ ist eine Technologie, die dazu dient, Daten von Darstellungen in Widgets zu trennen, die mit Datensätzen arbeiten. Standard-Widgets sind nicht für die Trennung von Daten und Darstellungen ausgelegt, weshalb Qt zwei verschiedene Arten von Widgets bereitstellt. Beide Arten von Widgets sehen gleich aus, interagieren jedoch unterschiedlich mit den Daten.

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

Struktur, bei der die „Darstellung“ von den „Daten“ getrennt ist

Ansichtsklassen arbeiten mit externen Daten (dem Modell).

Struktur, die für die Daten eine „Ansicht“ und ein „Modell“ verwendet

1.1 Standard-Widgets

Schauen wir uns ein Standard-Tabellen-Widget einmal genauer an. Ein Tabellen-Widget ist ein zweidimensionales Array aus Datenelementen, die der Benutzer ändern kann. Das Tabellen-Widget lässt sich in einen Programmablauf integrieren, indem die vom Tabellen-Widget bereitgestellten Datenelemente gelesen und geschrieben werden. Diese Methode ist sehr intuitiv und in vielen Anwendungen nützlich, doch die Anzeige und Bearbeitung einer Datenbanktabelle mit einem Standard-Tabellen-Widget kann problematisch sein. Zwei Kopien der Daten müssen aufeinander abgestimmt werden: eine außerhalb des Widgets und eine innerhalb des Widgets. Der Entwickler ist für die Synchronisation beider Versionen verantwortlich. Darüber hinaus erschwert die enge Kopplung von Darstellung und Daten das Schreiben von Unit-Tests.

1.2 Modell/Ansicht als Lösung

Model/View bietet eine Lösung, die auf einer vielseitigeren Architektur basiert. Model/View beseitigt die Probleme mit der Datenkonsistenz, die bei Standard-Widgets auftreten können. Model/View erleichtert zudem die Verwendung mehrerer Ansichten derselben Daten, da ein Modell an viele Ansichten weitergegeben werden kann. Der wichtigste Unterschied besteht darin, dass Model/View-Widgets keine Daten hinter den Tabellenzellen speichern. Tatsächlich arbeiten sie direkt mit Ihren Daten. Da View-Klassen die Struktur Ihrer Daten nicht kennen, müssen Sie einen Wrapper bereitstellen, damit Ihre Daten der Schnittstelle ` QAbstractItemModel ` entsprechen. Eine View nutzt diese Schnittstelle, um aus Ihren Daten zu lesen und in diese zu schreiben. Jede Instanz einer Klasse, die „ QAbstractItemModel “ implementiert, wird als Modell bezeichnet. Sobald die Ansicht einen Zeiger auf ein Modell erhält, liest sie dessen Inhalt, zeigt ihn an und fungiert als dessen Editor.

1.3 Überblick über die Modell-/Ansicht-Widgets

Hier finden Sie eine Übersicht über die Modell-/Ansicht-Widgets und die ihnen entsprechenden Standard-Widgets.

WidgetStandard-Widget-
(eine auf Elementen basierende Komfortklasse)
Model/View-View-Klasse
(zur Verwendung mit externen Daten)
Verzeichnis, dessen Inhalt als Liste angezeigt wirdQListWidgetQListView
Verzeichnis, dessen Inhalt als Tabelle angezeigt wirdQTableWidgetQTableView
Verzeichnis, dessen Inhalt als Baumstruktur angezeigt wirdQTreeWidgetQTreeView
Verzeichnis, dessen Inhalt in Spalten angezeigt wirdQColumnView zeigt einen Baum als Hierarchie von Listen an
Kombinationsfeld, das einen Eintrag im Modell anzeigtQComboBox kann sowohl als View-Klasse als auch als herkömmliches Widget fungieren

1.4 Verwendung von Adaptern zwischen Formularen und Modellen

Adapter zwischen Formularen und Modellen können sich als nützlich erweisen.

Wir können in Tabellen gespeicherte Daten direkt aus der Tabelle heraus bearbeiten, aber es ist wesentlich komfortabler, Daten in Textfeldern zu bearbeiten. Für Widgets, die mit einem einzelnen Wert (QLineEdit, QCheckBox...) statt mit einem Datensatz arbeiten, gibt es kein direktes Model/View-Äquivalent, das Daten und Ansichten voneinander trennt; daher benötigen wir einen Adapter, um das Formular mit der Datenquelle zu verbinden.

QDataWidgetMapper ist eine hervorragende Lösung, da es Formular-Widgets einer Tabellenzeile zuordnet und das Erstellen von Formularen für Datenbanktabellen sehr vereinfacht.

Formular mit persönlichen und sonstigen Angaben

Ein weiteres Beispiel für einen Adapter ist QCompleter. Qt bietet mit QCompleter die Möglichkeit, Autovervollständigungen in Qt Widgets wie QComboBox und, wie unten gezeigt, QLineEdit bereitzustellen. QCompleter verwendet ein Modell als Datenquelle.

Ergänzung mit Vorschlägen für die Figur „Em“

2. Eine einfache Model-View-Anwendung

Wo sollten Sie ansetzen, wenn Sie eine Model-View-Anwendung entwickeln möchten? Wir empfehlen, mit einem einfachen Beispiel zu beginnen und dieses Schritt für Schritt zu erweitern. Das 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 praktisch erwiesen. Es ist wesentlich einfacher, mit einer einfachen Model-View-Anwendung zu beginnen, die über Beispieldaten verfügt. Probieren Sie es aus! Ersetzen Sie einfach die Daten in den folgenden Beispielen durch Ihre eigenen.

Im Folgenden finden Sie 7 sehr einfache und eigenständige Anwendungen, die verschiedene Aspekte der Model-View-Programmierung veranschaulichen. Den Quellcode finden Sie im Verzeichnis „ examples/widgets/tutorials/modelview “.

2.1 Eine schreibgeschützte Tabelle

Wir beginnen mit einer Anwendung, die eine „ QTableView “ zur Darstellung von Daten verwendet. Die Bearbeitungsfunktionen werden wir später hinzufügen.

(Quelldatei: 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 kommt der interessante Teil: Wir erstellen eine Instanz von `MyModel` und verwenden ` tableView.setModel(&myModel);`, um einen Zeiger darauf an ` tableView` zu übergeben. ` tableView ` ruft die Methoden des erhaltenen Zeigers auf, um zwei Dinge herauszufinden:

  • Wie viele Zeilen und Spalten angezeigt werden sollen.
  • Welcher Inhalt in die einzelnen Zellen ausgegeben werden soll.

Das Modell benötigt etwas Code, um darauf zu reagieren.

Da wir über einen Tabellendatensatz verfügen, beginnen wir mit „ QAbstractTableModel “, da diese Funktion einfacher zu verwenden ist als die 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.

(Quelldatei: 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 von den Methoden ` MyModel::rowCount()` und ` MyModel::columnCount()` bereitgestellt. Wenn die Ansicht den Text einer Zelle abfragen muss, ruft sie die Methode ` MyModel::data()` auf. Die Zeilen- und Spalteninformationen werden über den Parameter ` index ` angegeben, und die Rolle wird auf ` Qt::DisplayRole` gesetzt. Weitere Rollen werden im nächsten Abschnitt behandelt. In unserem Beispiel werden die anzuzeigenden Daten generiert. In einer realen Anwendung würde „ MyModel “ über ein Element namens „ MyData “ verfügen, das als Ziel für alle Lese- und Schreibvorgänge dient.

Dieses kleine Beispiel veranschaulicht den passiven Charakter eines Modells. Das Modell weiß nicht, wann es verwendet wird oder welche Daten benötigt werden. Es stellt lediglich jedes Mal Daten bereit, wenn die Ansicht diese anfordert.

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

2.2 Erweiterung des „Read-Only“-Beispiels um Rollen

Das Modell steuert nicht nur, welcher Text in der Ansicht angezeigt wird, sondern auch dessen Darstellung. Wenn wir das Modell geringfügig ändern, erhalten wir folgendes Ergebnis:

Tabelle, deren Zellen eine benutzerdefinierte Formatierung aufweisen

Tatsächlich muss außer der Methode ` data()` nichts geändert werden, um Schriftarten, Hintergrundfarbe, Ausrichtung und ein Kontrollkästchen festzulegen. Nachstehend finden Sie die Methode ` data()`, die das oben gezeigte Ergebnis erzeugt. Der Unterschied besteht darin, dass wir diesmal den Parameter `int role` verwenden, um je nach dessen Wert unterschiedliche Informationen zurückzugeben.

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

// mymodel.cpp
QVariant MyModel::data(const QModelIndex&index, int role) const
{
    int row = index.row();
    int col = index.column();
    // Erzeugt eine Protokollmeldung, wenn diese Methode aufgerufen wird
    qDebug() << QString("row %1, col%2, role %3")
           .arg(Zeile).arg(Spalte).arg(Rolle);

    switch (Rolle) {
    case Qt::DisplayRole:
        if (row== 0&& col== 1) return QString("<--links");
        if (row== 1&& col== 1) return QString("right-->");

        return QString("Zeile%1, Spalte%2")
                .arg(row + 1)
                .arg(col+ 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::rot);
        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) // Füge ein Kontrollkästchen zur Zelle (1,0) hinzu
            return Qt::Checked;
        break;
    }
    return QVariant();
}

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

enum Qt::ItemDataRoleBedeutungTyp
Qt::DisplayRoletextQString
Qt::FontRolefontQFont
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

Weitere Informationen zu den Möglichkeiten der Enumeration „ Qt::ItemDataRole “ finden Sie in der Dokumentation zum Qt-Namespace.

Nun müssen wir ermitteln, wie sich die Verwendung eines separaten Modells auf die Leistung der Anwendung auswirkt. Dazu verfolgen wir, wie oft die Ansicht die Methode ` data()` aufruft. Um zu erfassen, wie oft die Ansicht das Modell aufruft, haben wir in der Methode ` data()` eine Debug-Anweisung eingefügt, die eine Meldung in den Fehlerausgabestrom schreibt. In unserem kleinen Beispiel wird ` data()` 42 Mal aufgerufen. Jedes Mal, wenn Sie den Mauszeiger über das Feld bewegen, wird ` data()` erneut aufgerufen – sieben Mal pro Zelle. Deshalb ist es wichtig, sicherzustellen, dass Ihre Daten verfügbar sind, wenn ` data()` aufgerufen wird, und dass aufwendige Suchvorgänge zwischengespeichert werden.

2.3 Eine Uhr in einer Tabellenzelle

Tabelle, in der die Uhrzeit in einer Zelle angezeigt wird

Wir haben immer noch eine schreibgeschützte Tabelle, doch diesmal ändert sich der Inhalt jede Sekunde, da wir die aktuelle Uhrzeit anzeigen.

(Quelldatei: 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, damit die Uhr tickt. Wir müssen der Ansicht jede Sekunde mitteilen, dass sich die Uhrzeit geändert hat und dass sie erneut ausgelesen werden muss. Dazu verwenden wir einen Timer. Im Konstruktor setzen wir dessen Intervall auf 1 Sekunde und verbinden sein Timeout-Signal.

(Quelldatei: 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 weisen die Ansicht an, die Daten in der Zelle oben links erneut zu lesen, indem wir das Signal „ dataChanged()“ auslösen. Beachten Sie, dass wir das Signal „ dataChanged()“ nicht explizit mit der Ansicht verbunden haben. Dies geschah automatisch, als wir „ setModel()“ aufgerufen haben.

2.4 Einrichten von Kopfzeilen für Spalten und Zeilen

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

Tabelle mit den Spaltenüberschriften

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

(Quelldatei: 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()` ebenfalls einen Parameter `role` hat, der dieselbe Bedeutung hat wie in ` MyModel::data()`.

2.5 Das Beispiel zur minimalen Bearbeitung

In diesem Beispiel erstellen wir eine Anwendung, die den Fenstertitel automatisch mit Inhalten füllt, indem sie die in Tabellenzellen eingegebenen Werte wiederholt. Um einfach auf den Fenstertitel zugreifen zu können, fügen wir die Methode ` QTableView ` in eine ` QMainWindow` ein.

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

(Quelldatei: 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 das zweidimensionale Array ` the ` und ` QString ` m_gridData, um unsere Daten zu speichern. Damit bildet ` m_gridData ` den Kern von ` MyModel`. Der Rest von ` MyModel ` fungiert als Wrapper und passt ` m_gridData ` an die Schnittstelle ` QAbstractItemModel ` an. Außerdem haben wir das Signal ` editCompleted() ` eingeführt, das es ermöglicht, den geänderten Text in den Fenstertitel zu übertragen.

(Quelldatei: 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 bearbeitet. Der Parameter „ index “ gibt an, 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. Wäre ein Kontrollkästchen vorhanden und wären die Benutzerberechtigungen so eingestellt, dass das Kontrollkästchen ausgewählt werden darf, würden die Aufrufe ebenfalls mit der Rolle „ Qt::CheckStateRole “ 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 anzuzeigen, dass eine Zelle ausgewählt werden kann.

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

3. Weiterführende Themen

3.1 TreeView

Sie können das obige Beispiel in eine Anwendung mit einer Baumansicht umwandeln. Ersetzen Sie dazu einfach QTableView durch QTreeView, wodurch eine Lese-/Schreib-Baumstruktur entsteht. Am Modell müssen keine Änderungen vorgenommen werden. Der Baum weist keine Hierarchien auf, da das Modell selbst keine Hierarchien enthält.

Baumansicht zur Darstellung von Tabellendaten ohne Hierarchie

QListView, QTableView und QTreeView verwenden alle eine Modellabstraktion, bei der es sich um eine Kombination aus Liste, Tabelle und Baum handelt. Dadurch ist es möglich, mehrere verschiedene Arten von Ansichtsklassen aus demselben Modell zu verwenden.

Diagramm zum Vergleich von Listenmodell, Tabellenmodell und Baummodell

So sieht unser Beispielmodell bisher aus:

Schematische Darstellung der aktuellen Modellstruktur

Wir möchten einen echten Baum darstellen. In den obigen Beispielen haben wir unsere Daten in ein Modell verpackt. Dieses Mal verwenden wir „ QStandardItemModel “, einen Container für hierarchische Daten, der auch „ QAbstractItemModel “ implementiert. Um einen Baum darzustellen, muss „ QStandardItemModel “ mit „ QStandardItems“ gefüllt werden, die alle Standard-Eigenschaften von Elementen wie Text, Schriftarten, Kontrollkästchen oder Pinsel enthalten können.

Schematische Darstellung des Modells und der Ansicht

(Quelldatei: 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 übergeben dem Konstruktor einige ` QStandardItems `. Da ein ` QStandardItem ` andere ` QStandardItems` enthalten kann, lassen sich so hierarchische Datenstrukturen erstellen. Die Knoten können innerhalb der Ansicht ein- und ausgeklappt werden.

3.2 Arbeiten mit Auswahlen

Wir möchten auf den Inhalt eines ausgewählten Elements zugreifen, um ihn zusammen mit der Hierarchieebene im Fenstertitel anzuzeigen.

Ansicht eines Modells, in dem der ausgewählte Inhalt blau dargestellt ist

Erstellen wir also ein paar Elemente:

(Quelldatei: 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 in einem separaten Auswahlmodell, das mit der Methode ` selectionModel()` abgerufen werden kann. Wir rufen das Auswahlmodell ab, um einen Slot mit dessen Signal ` selectionChanged()` zu verbinden.

(Quelldatei: 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 ermitteln den der Auswahl entsprechenden Modellindex durch Aufruf von ` treeView->selectionModel()->currentIndex()` und erhalten die Zeichenkette des Feldes mithilfe des Modellindexes. Anschließend berechnen wir einfach den ` hierarchyLevel` des Elements. Elemente der obersten Ebene haben keine übergeordneten Elemente, und die Methode ` parent()` gibt ein standardmäßig konstruiertes ` QModelIndex()` zurück. Aus diesem Grund verwenden wir die Methode ` parent()`, um bis 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 werden, es kann aber auch mit ` QAbstractItemView::setSelectionModel` festgelegt werden. Auf diese Weise ist es möglich, drei Ansichtsklassen mit synchronisierten Auswahlen zu haben, da nur eine Instanz eines Auswahlmodells verwendet wird. Um ein Auswahlmodell zwischen drei Ansichten gemeinsam zu nutzen, verwenden Sie ` selectionModel()` und weisen Sie das Ergebnis der zweiten und dritten Ansichtsklasse mit ` setSelectionModel()` zu.

3.3 Vordefinierte Modelle

Die typische Vorgehensweise bei der Verwendung von Model/View besteht darin, bestimmte Daten zu verpacken, um sie für Ansichtsklassen nutzbar zu machen. Qt stellt jedoch auch vordefinierte Modelle für gängige zugrunde liegende Datenstrukturen bereit. Wenn eine der verfügbaren Datenstrukturen für Ihre Anwendung geeignet ist, kann ein vordefiniertes Modell eine gute Wahl sein.

QRangeModelPasst einen vorhandenen C++-Container oder -Bereich an
QStringListModelSpeichert eine Liste von Zeichenfolgen
QStandardItemModelSpeichert beliebige hierarchische Elemente
QFileSystemModelKapseln das lokale Dateisystem
QSqlQueryModelKapseln eines SQL-Ergebnissatzes
QSqlTableModelKapseln eine SQL-Tabelle
QSqlRelationalTableModelKapseln einer SQL-Tabelle mit Fremdschlüsseln
QSortFilterProxyModelSortiert und/oder filtert ein anderes Modell

3.4 Delegaten

In allen bisherigen Beispielen werden Daten als Text oder Kontrollkästchen in einer Zelle dargestellt und als Text oder Kontrollkästchen bearbeitet. Die Komponente, die diese Darstellungs- und Bearbeitungsfunktionen bereitstellt, wird als Delegat bezeichnet. Wir stehen erst am Anfang der Arbeit mit dem Delegaten, da die Ansicht einen Standarddelegaten verwendet. Stellen Sie sich jedoch vor, wir wollten einen anderen Editor verwenden (z. B. einen Schieberegler oder eine Dropdown-Liste). Oder stellen Sie sich vor, wir wollten Daten als Grafiken darstellen. Sehen wir uns ein Beispiel namens „Star Delegate“ an, in dem Sterne zur Darstellung einer Bewertung verwendet werden:

Tabelle mit Liedern und deren Sternebewertungen

Die Ansicht verfügt über eine Methode ` setItemDelegate()` (), die den Standard-Delegaten ersetzt und einen benutzerdefinierten 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 bietet, müssen wir lediglich zwei Methoden überschreiben.

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 Aufruf von ` index.data()` abgerufen werden. Die Methode ` sizeHint()` des Delegaten wird verwendet, um die Abmessungen der einzelnen Sterne zu ermitteln, sodass die Zelle genügend Höhe und Breite bereitstellt, um die Sterne aufzunehmen.

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

Weitere Verweise auf Delegaten in der Qt-Dokumentation:

3.5 Debugging mit ModelTest

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

Qt Labs stellt eine Software namens ModelTest zur Verfügung, die Modelle während der Programmausführung überprüft. Jedes Mal, wenn das Modell geändert wird, scannt ModelTest das Modell und meldet Fehler mit einem Assert. Dies ist besonders wichtig für Baummodelle, da ihre hierarchische Struktur viele Möglichkeiten für subtile Inkonsistenzen bietet.

Im Gegensatz zu Ansichtsklassen verwendet ModelTest Indizes außerhalb des zulässigen Bereichs, um das Modell zu testen. Das bedeutet, dass Ihre Anwendung mit ModelTest abstürzen kann, auch wenn sie ohne ModelTest einwandfrei läuft. Daher müssen Sie bei der Verwendung von ModelTest auch alle Indizes behandeln, die außerhalb des zulässigen Bereichs liegen.

Beispiele

Qt enthält zahlreiche Beispiele für das Modell-View-Konzept. Sie finden diese auf der Seite „Beispiele für Item-Views “.

© 2026 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.