9 May 2020

Tutorial: Qt TableView Widget

Edit: Am 3.2.2024 für Kompatibilität mit QT 6.6.1 überarbeitet.

Das Projekt ist nun auch auf Codeberg verfügbar.

https://codeberg.org/lriecken/qt_table_view_widget_tutorial

---

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.

Blogroll

Online Services

Neocities Eine moderne Version von Geocities. Manches Gem zu finden.
Gemini Protokolldefinition Eine moderne Version von Gopher
Posteo Ein sehr guter und sympathsicher Berliner Email Service.

Retro Computing

CBBS Outpost Aktuelle Liste von C64/C128 BBS Servern