8 Minuten
Tutorial: Qt TableView Widget
Dieses Tutorial beinhaltet folgende Themen:
- Eine Datenmodellklasse wird durch eine
QAbstractTableModel
Klasse mit einemQTableView
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:
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");
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) {
this->entries.insert(position, 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.