Edit: Am 3.2.2024 für Kompatibilität mit QT 6.6.1 überarbeitet.
Das Projekt ist nun auch auf Codeberg verfügbar.


Dieses Tutorial beinhaltet folgende Themen:

  • Eine Datenmodellklasse wird durch eine QAbstractTableModel Klasse mit einem QTableView Widget verknüpft.
  • Die Tabelle wird editierbar.
  • Einträge können hinzugefügt und entfernt werden.
  • Für eine Spalte der Tabelle wird ein angepasstes Editorwidget erstellt.

Das Modell besteht aus den Feldern: firstName, lastName, age und gender. Später wird für das letzte Feld eine ItemDelegate Klasse angelegt um die Auswahl über eine QComboBox zu ermöglichen.

Grundlegende C++ Kenntnisse werden vorausgesetzt.

Unsere Beispiel Modellklasse:

#include <QString>

class ContactModel
{
public:
    ContactModel(const QString &firstname = "", 
                 const QString &lastname = "", 
                 int age = 0, const QString &gender = "-"):
        firstName(firstname), lastName(lastname), age(age), gender(gender) {}
    QString getFirstName() const { return firstName; }
    void setFirstName(const QString &value) { firstName = value; }
    QString getLastName() const { return lastName; }
    void setLastName(const QString &value) { lastName = value; }
    int getAge() const { return age; }
    void setAge(int value) { age = value; }
    QString getGender() const { return gender; }
    void setGender(const QString &value) { gender = value; }
private:
    QString firstName;
    QString lastName;
    int age;
    QString gender;

};

Das MainWindow besteht aus einem QTableView Widget und zwei QButton Widgets. addButton und removeButton.

Das Fenster wird wie folgt erstellt:

Um in Qt Daten in einem View Widget darzustellen, benötigen wir eine Adapter Klasse, die die Daten oder eine Referenz auf die Daten enthält und in unserem Fall von QAbstractTableModel erbt.

Das Interface unserer Klasse ist wie folgt:

#include <QWidget>
#include <QVariant>
#include <QList>
#include <QAbstractTableModel>
#include <QSharedPointer>
#include "contactmodel.h"

class ContactsContainer: public QAbstractTableModel
{
public:
    ContactsContainer(QObject *parent);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        return entries.size();
    }
    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        return 4;
    }
    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override {}
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {}
    bool insertRows(int position, int rows, const QModelIndex &parent) override {}
    bool removeRows(int position, int rows, const QModelIndex &parent) override {}
private:
    QList<QSharedPointer<ContactModel>> entries;
};

Neben dem Datensatz entries, der in einer QList gespeichert wird, enthält die Klasse die Funktionen um die Zeilen- und Spaltenanzahl (rowCount, columnCount), die Daten der Zellen (referenziert über einen QModelIndex) data sowie die Spaltenbeschriftung headerData abzufragen.

Dies sind alle notwendigen Funktionen, die implementiert werden müssen.

Auf die Funktionen setData, flags, insertRows und removeRows kommen wir später zurück.

Beginnen wird damit im Constructor unserer ContactsContainer Klasse ein paar Beispieleinträge einzufügen, damit wir sehen ob Daten in der Tabelle angezeigt werden:

ContactsContainer::ContactsContainer(QObject *parent): QAbstractTableModel(parent) {
    this->entries.push_back(
    	QSharedPointer<ContactModel>::create("John", "Lennon", 25, "Männlich"));
    this->entries.push_back(
    	QSharedPointer<ContactModel>::create("Paul", "McCartney", 23, "Männlich"));
    this->entries.push_back(
    	QSharedPointer<ContactModel>::create("George", "Harrison", 22, "Männlich"));
    this->entries.push_back(
    	QSharedPointer<ContactModel>::create("Ringo", "Starr", 25, "Männlich"));
}

Nun implementieren wird die Funktionen headerData:

QVariant ContactsContainer::headerData(int section, Qt::Orientation orientation, int role) const {
    if (role == Qt::DisplayRole) {
        if (orientation == Qt::Horizontal) {
            switch(section) {
            case 0:
                return "Vorname";
            case 1:
                return "Nachname";
            case 2:
                return "Alter";
            case 3:
                return "Geschlecht";
            default:
                return "unknown";
            }
        }
    }
    return QVariant();
}

Wir geben einfach den Namen der Spalte zurück, nach der wir über die Variable section gefragt werden. Zusätzlich prüfen wir über role welchem Zweck die Anfrage dient. In unserem Fall Qt::DisplayRole. Eine andere Möglichkeit wäre Qt::EditRole worauf wir später zurückkommen.

Sehr ähnlich sieht die Funktion data aus:

QVariant ContactsContainer::data(const QModelIndex &index, int role) const {
    if (!index.isValid() || index.row() > this->entries.size()) {
        return QVariant();
    }
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
        switch (index.column()) {
        case 0:
            return this->entries.at(index.row())->getFirstName();
        case 1:
            return this->entries.at(index.row())->getLastName();
        case 2:
            return this->entries.at(index.row())->getAge();
        case 3:
            return this->entries.at(index.row())->getGender();
        default:
            return "unknown";
        }

    }
    return QVariant();
}

Wir prüfen erstmal ob der Index gültig ist und nicht größer als unser Datensatz. Dann geben wir für die Rollen Qt::DisplayRole und Qt::EditRole den entsprechenden Wert im Datensatz für die entsprechende Spalte zurück.

Jetzt sind wir soweit die Klasse ausprobieren zu können.

Im MainWindow fügen wir den ContactsContainer als Member Variable hinzu. mainwindow.h sollte jetzt folgendermaßen aussehen:

#include <QMainWindow>
#include "contactscontainer.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_addButton_clicked();
    void on_removeButton_clicked();

private:
    Ui::MainWindow *ui;
    ContactsContainer container;
};

Im constructor initialisieren wir jetzt noch den container und verknüpfen ihn mit unserem tableView Widget:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , container(this)
{
    ui->setupUi(this);
    ui->tableView->setModel(&this->container);
}

Starten wir das Program, sollten wir folgendes Fenster sehen:

Hauptfenster

Fügen wir nun etwas Interaktivität hinzu. Dazu fehlen uns zwei Funktionen. setData um neue Daten in das Model zu schreiben, und flags, um dem TableView Widget zu sagen, welche Felder editierbar sein sollen.

bool ContactsContainer::setData(const QModelIndex &index, const QVariant &value, int role) {
    if (index.isValid() && role == Qt::EditRole) {
        switch(index.column()) {
        case 0:
            this->entries.at(index.row())->setFirstName(value.toString());
            break;
        case 1:
            this->entries.at(index.row())->setLastName(value.toString());
            break;
        case 2:
            this->entries.at(index.row())->setAge(value.toInt());
            break;
        case 3:
            this->entries.at(index.row())->setGender(value.toString());
            break;
        default:
            return false;
        }
        emit dataChanged(index, index, {role});
        return true;
    }
    return false;
}

Analog zu data schreiben wir nun den Wert in das Model. Am Ende lösen wir das dataChanged Signal aus um alle Objekte zu informieren, die mit dem Signal verknüpft sind.

Qt::ItemFlags ContactsContainer::flags(const QModelIndex &index) const {
    if (!index.isValid()) {
        return Qt::ItemIsEnabled;
    }
    return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;
}

Durch die Bitweise-Oder Operation setzen wir in dem Flag für alle Felder das Qt::ItemIsEditable flag.

Führen wir das Program aus, können wir nun per Doppelklick die Zellen des TableView Widgets bearbeiten.

Qt hält für alle QVariant Typen entsprechende ItemDelegate Klassen vor. Diese Klassen erstellen Widgets, schreiben und lesen ihre Werte, und passieren ihre Geometrie in das Tabellenfeld ein. Eine solche Klasse zu erstellen und anstelle der vorgehaltenen Klassen zu verwenden ist einfach.

Um zum Beispiel das Feld Geschlecht durch eine QComboBox zu ersetzen müssen wir nur eine eigene Klasse, die von QStyledItemDelegate erbt, erstellen.


class GenderOptionsDelegate: public QStyledItemDelegate
{
    Q_OBJECT
public:
    GenderOptionsDelegate(QObject *parent = nullptr): QStyledItemDelegate(parent) { }

    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

createEditor erstell eine Instanz des gewünschten Widgets. setEditorData schreibt Daten aus dem Model in das Widget. setModelData liest die Daten aus dem Widget und schreibt sie ins Model. updateEditorGeometry setzt die Dimensionen des Widgets anhand der Tabellenzelle.

QWidget *GenderOptionsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    QComboBox *editor = new QComboBox(parent);
    editor->setFrame(false);
    editor->addItem("-");
    editor->addItem("Männlich");
    editor->addItem("Weiblich");
    editor->addItem("Divers");
    editor->setAutoFillBackground(true);
    return editor;
}
void GenderOptionsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {
    auto value = index.model()->data(index, Qt::EditRole).toString();
    static_cast<QComboBox*>(editor)->setCurrentText(value);
}
void GenderOptionsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const {
    auto value = static_cast<QComboBox*>(editor)->currentText();
    model->setData(index, value, Qt::EditRole);
}
void GenderOptionsDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    editor->setGeometry(option.rect);
}

Jetzt müssen wir nur dem TableView Widget im Constructor unseres MainWindows mitteilen, diese Delegate Klasse zu verwenden:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , container(this)
{
    ui->setupUi(this);
    ui->tableView->setModel(&this->container);
    GenderOptionsDelegate *delegate = new GenderOptionsDelegate();
    ui->tableView->setItemDelegateForColumn(3, delegate);
}

Führen wir nun das Program aus, bekommen wir in der 4. Spalte nun statt einer EditBox eine ComboBox zu sehen.

Im letzten Schritt fügen wir noch die Funktionen insertRows und removeRows ein.

bool ContactsContainer::insertRows(int position, int rows, const QModelIndex &parent) {
    this->beginInsertRows(QModelIndex(), position, position + rows - 1);
    for (int row = 0; row < rows; ++row) {
        if (position < this->entries.length()) {
            this->entries.insert(position, QSharedPointer<ContactModel>::create());
        } else {
            this->entries.emplaceBack(QSharedPointer<ContactModel>::create());
        }
    }
    this->endInsertRows();
    return true;
}

Die Funktion bekommt die Zeile vor der die neue Zeile eingefügt werden soll. Die Anzahl der Zeilen und einen QModelIndex den wir nicht benötigen übergeben.

Als erstes sperren wir das Model durch beginInsertRows. Dann fügen wir ein neues ContactModel an entsprechender Stelle in unseren Datensatz ein. Danach entsperren wir das Model wieder, wodurch wiederum alle entsprechende Signal ausgelöst werden, die das TableView Widget über die Änderung im Model informieren.

bool ContactsContainer::removeRows(int position, int rows, const QModelIndex &parent) {
    if (position < 0 || position + rows > entries.size()) {
        return false;
    }
    this->beginRemoveRows(QModelIndex(), position, position + rows - 1);
    for (int row = 0; row < rows; ++row) {
        this->entries.removeAt(position);
    }
    this->endRemoveRows();
    return true;
}

Analog dazu werden hier die Datensätze entfernt.

Jetzt ist es an der Zeit die onClick Handler der Hinzufügen und Entfernen Buttons zu schreiben:

void MainWindow::on_addButton_clicked()
{
    auto selection = this->ui->tableView->selectionModel();
    int position = container.rowCount() + 1;
    auto indices = selection->selectedIndexes();
    if (indices.size() > 0) {
        auto index = indices.front();
        if (index.isValid()) {
            position = index.row() + 1;
        }
    }

    this->container.insertRows(position, 1, QModelIndex());
}

Wir holen uns ein QItemSelectionModel das die selektierten Zellen, Zeilen und Spalten der Table enthält. Falls etwas selektiert ist, nehmen wir uns das erste Element und dessen Zeile. Ansonsten setzen wir die Position auf das Ende des Datensatzes. Wir fügen jeweils 1 hinzu, da wir die Zeile nicht vor sondern nach der selektierten Zelle einfügen wollen.


void MainWindow::on_removeButton_clicked()
{
    auto selection = this->ui->tableView->selectionModel();
    int from = INT_MAX;
    int to = INT_MIN;
    auto indices = selection->selectedIndexes();

    for (auto &i : indices) {
        if (i.row() < from) from = i.row();
        if (i.row() > to) to = i.row();
    }
    if (from <= to) {
        this->container.removeRows(from, to - from + 1, QModelIndex());
    }
}

Analog zu oben holen wir uns ein QItemSelectionModel, berechnen diesmal das Minimum und Maximum der selektierten Zeilen und fügen 1 hinzu um die Anzahl der selektierten Zeilen zu berechnen.