2. Tabelle mit Datei Ein- und Ausgabe

In diesem Beispiel geht es darum eine einfache Standard-Tabelle zu erstellen, die ihren Inhalt aus einer Datei liest und bei Änderung des Inhalts die Möglichkeit einräumt, per Knopfdruck die veränderten Daten auch wieder in die Datei zurück zu schreiben. Eine Tabelle in Qt besteht aus zwei Elementen, einem QTableWidget und einem QTableWidgetItem. Während das QTableWidget quasi eine Art "Rahmen" darstellt und die Tabelle mit Spalten und Zeilen definiert, beschreibt das QTableWidgetItem den Inhalt einer einzelnen Zelle. TableItems werden dazu benutzt, den Inhalt einer Zelle zu verwalten, sei es nun Text, ein Icon oder eine Checkbox. Legen wir los:

Nachdem wir einen Ordner mit dem Namen tabelle erstellt haben, starten wir den Qt-Designer und erstellen eine neue Dialog-Box mit den Standard-Buttons OK und Abbrechen. Dem Dialog geben wir den Namen tabelleWindow. Anschließend ziehen wir aus der Gruppe Item Widgets (Item-Based) das Widget Table Widget in unseren Dialog und ziehen die Ränder in eine angenehme Größe. Unserem TableWidget geben wir den Namen tabelleWidget. Als letztes ziehen wir noch zwei weitere Buttons in unseren Dialog. Einem geben wir die Aufschrift Load/Reload und den Namen reloadButton, dem anderem die Aufschrift Save und den Namen saveButton. Bei letzterem Button nehmen wir in den Eigenschaften bei enabled das Häckchen raus, setzen ihn also auf deaktiviert. Der saveButton soll sich erst aktivieren, wenn eine Zelle in unserer Tabelle verändert wurde. Das ganze Widget speichern wir unter der Bezeichnung tabelle.ui. Damit ist unsere Oberfläche schon fertig und sieht in etwa so aus:

Bild: tabelle-ui.jpg

Wie man sieht, ist unser TableWidget noch vollkommen leer, d.h. wir haben noch gar keine Zeilen und Spalten definiert. Müssen wir auch nicht, da wir die Anzahl besagter Zeilen und Spalten aus unserer Textdatei entnehmen können. Möchte man die Textdatei nicht von Hand schreiben, sondern gleich mit unserem Programm erstellen, selektiert man das TableWidget und kann ganz unten unter den Eigenschaften eine Zeilenanzahl (rowCount) und Spaltenanzahl (columnCount) voreinstellen. Nach dem Programmstart kann man dann die Spalten mit den gewünschten Einträgen füllen und ein Druck auf den saveButton erstellt die Textdatei. Allerdings wird bei diesem Vorgehen keine Kopfzeile geschrieben, da die Standard-Nummerierung im Layout keinen realen Wert darstellt, den man auslesen könnte. Anstelle der Nummerierung wird eine leere Zeile geschrieben. Bei einem Reload der Datei, wird sogar die Standard-Zahl im Spalten-Index durch die Leerzeile entfernt und man hat Spalten ganz ohne Index. Möchte man dem abhelfen und beim ersten Erstellen der Datei auch gleich die Kopfzeile haben, öffnet man mit einem Doppelklick das Bearbeitungsmenü des TableWidgets und kann per Doppelklick die Zahlen in Bezeichnungen umändern. Diese können dann als ganz normaler QString ausgelesen werden.

Diesmal benötigen wir zusätzlich zu unseren vier Dateien noch eine fünfte Datei:

  • tabelle.ui - die grafische Oberfläche vom Qt-Designer erstellt (siehe oben).
  • main.cpp - das Hauptprogramm.
  • tabelle.h - enhält die Deklarierung eigener Klassen und Slots.
  • tabelle.cpp - enthält die Verbindungen und Funktionen der Signale und Slots.
  • tabellendaten.txt - eine simple Textdatei in der die einzelnen Zellen der Tabelle in einer speziellen Form gespeichert sind.
Die Grundlagen für die vier Standard-Dateien könnt ihr im vorigen Kapitel nachlesen. Ansonsten werde ich hier nur die für unser Tabellenprogramm relevanten Einträge erklären. Fangen wir mit dem Aussehen unser Daten-Datei an. Diese hat folgenden Inhalt:

tabellendaten.txt

"Name" "Vorname" "Straße" "Wohnort"
"Hacker" "Horst" "Otto-Waalkes-Straße" "Teerheim"
"Schwarzenegger" "Arnold" "California Drive" "USA"
"Doe" "Jane" "Route 66" "Texas"

Wie man sieht, sind die einzelnen Einträge für die Spalten in Anführungsstriche eingefaßt, damit ein Eintrag auch Leerzeichen beinhalten kann. Die Anführungsstriche dienen somit als Trenner. Ich benutze hier denselben Algorithmus wie ich ihn schon für die String-Trennung in C++ verwendet habe. Nur für die Befehle in Qt in etwas abgeänderter Form. Die erste Zeile unserer Daten-Datei verwenden wir als Spalten-Index für unsere Tabelle.

main.cpp

#include <QApplication>

#include "tabelle.h"

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
tabelleWindow *dialog = new tabelleWindow;
dialog->show();
return app.exec();
}

Die main.cpp enthält keine Besonderheiten und daher gibt es nichts weiteres dazu zu sagen.

tabelle.h

# ifndef TABELLE_H
# define TABELLE_H

# include <QDialog>

# include "ui_tabelle.h"

class tabelleWindow : public QDialog, public Ui::tabelleWindow
{
Q_OBJECT

public:
tabelleWindow(QWidget *parent = 0);

private slots:
void neuladen();
void speichern();
void aenderung(QTableWidgetItem *);

};

# endif

Unter private slots: stehen die Definitionen für unsere 3 eigenen Slots. Einmal für das Laden der Datei in die Tabelle, für das Speichern und ein Slot der das Signal für eine Änderung in unserer Tabelle erhält. Dieser Slot hat später die einzige Aufgabe unseren saveButton zu aktivieren.

tabelle.cpp

# include <QtGui>

# include "tabelle.h"

tabelleWindow::tabelleWindow(QWidget *parent): QDialog(parent)
{
setupUi(this);

connect(saveButton, SIGNAL(clicked()), this, SLOT(speichern()));
connect(reloadButton, SIGNAL(clicked()), this, SLOT(neuladen()));
connect(tabelleWidget, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(aenderung(QTableWidgetItem *)));

}


void tabelleWindow::neuladen()
{
int s;
int row,column;
int zeilen=0;
int spalten=0;
int pos1,pos2,lastpos;
QString line;
QString eintrag[99][99];

QString verzeichnis=QCoreApplication::applicationDirPath();
verzeichnis.append("/tabellendaten.txt");

QFile datei(verzeichnis);
if (datei.open(QIODevice::ReadOnly | QIODevice::Text))
{
QApplication::setOverrideCursor(Qt::WaitCursor);

QTextStream in(&datei);
while(!in.atEnd())
{
line = in.readLine();
s=0;
pos1=0;
lastpos=0;
while(pos1!=-1)
{
pos1=line.indexOf("\"",lastpos);
lastpos=pos1+1;
pos2=line.indexOf("\"",lastpos);
lastpos=pos2+1;
if(pos1!=-1)
{
eintrag[zeilen][s]=line.mid((pos1+1),(pos2-pos1-1));
s++;
}
}

// maximale Anzahl der benötigten Spalten ermitteln
if(s>spalten){spalten=s;}
zeilen++;
}
}
datei.close();

tabelleWidget->clear();
tabelleWidget->setRowCount(zeilen-1);
tabelleWidget->setColumnCount(spalten);

for(row=0;row<zeilen;row++)
{
for(column=0;column<spalten;column++)
{
QTableWidgetItem *newItem = new QTableWidgetItem;
newItem->setText(eintrag[row][column]);
if(row==0)
{
tabelleWidget->setHorizontalHeaderItem(column, newItem);
}
else
{
tabelleWidget->setItem(row-1, column, newItem);
}
}
}

QApplication::restoreOverrideCursor();
saveButton->setEnabled(false);
}


void tabelleWindow::speichern()
{
QString inhalt;
int z,i;

QString verzeichnis=QCoreApplication::applicationDirPath();
verzeichnis.append("/tabellendaten.txt");

QFile datei(verzeichnis);
if(datei.open(QFile::WriteOnly | QFile::Truncate))
{
QTextStream out(&datei);

QApplication::setOverrideCursor(Qt::WaitCursor);

for(i=0;i<tabelleWidget->columnCount();i++)
{
if(tabelleWidget->horizontalHeaderItem(i)!=0)
{
inhalt = tabelleWidget->horizontalHeaderItem(i)->text();
if(i==tabelleWidget->columnCount()-1)
{
out << "\"" << inhalt << "\"";
}
else
{
out << "\"" << inhalt << "\" ";
}
}
}
out << endl;

for(z=0; z<tabelleWidget->rowCount(); z++)
{
for(i=0; i<tabelleWidget->columnCount(); i++)
{
if(tabelleWidget->item(z,i)!=0)
{
inhalt = tabelleWidget->item(z,i)->text();

if(!inhalt.isEmpty())
{
if(i==tabelleWidget->columnCount()-1)
{
out << "\"" << inhalt << "\"";
}
else
{
out << "\"" << inhalt << "\" ";
}
}
}
}
out << endl;
}
}
QApplication::restoreOverrideCursor();
datei.close();

saveButton->setEnabled(false);
}


void tabelleWindow::aenderung(QTableWidgetItem *)
{
saveButton->setEnabled(true);
}

Der Code unserer tabelle.cpp sieht auf den ersten Blick nach sehr viel aus, aber keine Angst, es ist eigentlich halb so wild. Den Anfang machen unsere 3 Signale-Slots-Verbindungen. saveButton und reloadButton sind klar. Sie verbinden das clicked()-Signal der Knöpfe mit den entsprechenden Slots speichern und neuladen. Neu ist das Signal itemChanged der dritten Verbindung, welches von unserem TableWidget gesendet wird und mit dem Slot aenderung verbunden ist. Das Signal itemChanged wird immer dann ausgelöst, wenn in der Tabelle eine Zelle verändert wurde.

Unser erster Slot neuladen dient dazu, die Daten aus unserer Textdatei tabellendaten.txt einzulesen. Mit QCoreApplication::applicationDirPath() kann man sich den momentanen Verzeichnispfad in dem unser Programm ausgeführt wird holen. An diesen Pfad hängen wir noch mit append den Namen unserer Datei an. Anschließend wird mittels QFile die Datei geöffnet und im Fall eines erfolgreichen Öffnens (if(datei.open)) der nachfolgende Code ausgeführt. QIODevice::ReadOnly öffnet die Datei nur zum lesen und QIODevice::Text definiert sie als Textdatei. Der Befehl QApplication::setOverrideCursor(Qt::WaitCursor); macht aus unserem Mauszeiger eine beschäftigte Sanduhr, damit man sieht, daß das Programm arbeitet, sollte das Laden der Tabelle länger dauern. Mittels eines QTextStream kann man die geöffnete Datei Zeile für Zeile auslesen lassen und einer QString-Variablen übergeben. In unserem Fall unser QString-Array eintrag welches 99 Spalten und 99 Zeilen ermöglicht. Durch die while-Schleife while(!in.atEnd()) wird solange in der Datei gelesen, bis das Ende erreicht ist. Es folgt mein Algorithmus um die einzelnen Spalten anhand der Anführungsstriche zu zerlegen. Durch die darauf folgende if-Schleife if(s>spalten){spalten=s;} kann die Textdatei in einer Zeile auch weniger Einträge enthalten. Trotzdem wird die Tabelle immer so groß gemacht, daß keine Spalten einer Zeile abgeschnitten werden. datei.close() schließt unser Textdokument wieder. Mittels tabelleWidget->clear(); wird die gesamte Tabelle geleert. Eventuell vorhandene Zeilen und Spalten bleiben jedoch bei diesem Vorgang erhalten. Mit tabelleWidget->setRowCount(zeilen-1); und tabelleWidget->setColumnCount(spalten); wird die Anzahl der Spalten und Zeilen festgelegt. Sozusagen wird unser "Tabellenrahmen" in den wir später unsere Daten eintragen können erstellt. Durch die vorige Zählung der Zeilen und Spalten wird die Tabelle immer nur so groß gemacht, wie die Anzahl der Daten in der Textdatei. Da wir die erste Zeile unserer Datei als Kopf-Layout verwenden wollen, müssen wir die Zeilen um 1 kürzen. Tabellenzeilen und -spalten beginnen immer bei 0, daher müssen wir später unsere rows um -1 wegen der fehlenden, ersten Zeile herabsetzen. Mit den ineinander verschachtelten for-Schleifen werden die einzelnen Spalten mit den Daten aus unserem QString-Array befüllt. Dabei sieht das Konstrukt zum befüllen schon wild aus, denn ein QTableWidget kann nur ein QTableWidgetItem an eine Zelle übergeben, während man dem QTableWidgetItem einen QString übergeben kann. Sozusagen kann man den QString nicht direkt in die Tabelle eintragen, sondern muß den Umweg über das Item gehen. Aus diesem Grund müssen wir mittels QTableWidgetItem *newItem = new QTableWidgetItem; ein neues Item mit dem Namen newItem erstellen. Diesem Item übergeben wir mit newItem->setText(eintrag[row][column]); unseren QString eintrag. Anschließend wird abgefragt ob die Zeile gleich 0 ist, damit die erste Zeile mittels setHorizontalHeaderItem in den Tabellenkopf eingetragen wird. Alle anderen Zeilen werden mittels setItem in unsere Tabellen-Zellen eingetragen. QApplication::restoreOverrideCursor(); stellt unseren Standard-Mauszeiger wieder her und saveButton->setEnabled(false); setzt den saveButton auf Disabled. Jetzt werdet ihr sagen, warum das denn, wir haben doch den saveButton schon im Qt-Designer auf Disabled gesetzt? Tja, das Problem ist, daß durch das Eintragen unserer Tabellendaten das Signal itemChanged ausgelöst wurde, da sich die Items ja verändert haben, welches den Slot aenderung() aufruft, in dem der saveButton aktiviert wird. Der saveButton soll aber erst aktiviert werden, wenn wir selbst was dran ändern und nicht schon beim Laden der Daten!

Der Slot speichern fängt ähnlich an wie der Slot neuladen, da ja auch hier eine Datei-Operation ansteht. Zuerst wird wieder das momentane Verzeichnis des Programms geholt und der Name unserer Datei angehängt. Wieder wird mit QFile die Datei geöffnet. Diesmal mittels QFile::WriteOnly zum schreiben. Der Zusatz QFile::Truncate löscht einen eventuell vorhandenen Inhalt gnadenlos. Da unsere Tabellenköpfe keinen Zeilenwert besitzen, müssen wir diese mit einer for-Schleife separat eintragen. Also macht die erste for-Schleife mit dem Befehl tabelleWidget->horizontalHeaderItem(i)->text(); nichts anderes als die Layout-Einträge unserer Tabelle als erste Zeile in die Textdatei zu schreiben. Man beachte wieder das wilde "Umwegs-Konstrukt" aus QTableWidget->QTableWidgetItem->text().
Damit das Programm beim Auslesen der HeaderItems nicht abstürzt ist die Datenübergabe in die if-Schleife if(tabelleWidget->horizontalHeaderItem(i)!=0) eingefasst, welche die Übergabe nur zuläßt, wenn auch Daten vorhanden sind. Die if-Abfrage (i==tabelleWidget->columnCount()-1) bewirkt nur, daß zwischen den Spalteneinträgen mit ihren Anführungsstrichen ein Leerzeichen gesetzt wird, während es am Ende natürlich fehlen soll. Theoretisch könnte man die Leerzeichen auch weglassen und sich die if-Abfrage schenken, da mein Zerlegungs-Algorithmus nur auf die Anführungsstriche geht und ihn Leerzeichen nicht interessieren. Ich persönlich finde es aber optisch hübscher, falls man mal einen Blick in die Textdatei wirft, wenn sich zwischen den Anführungsstrichen ein Leerzeichen befindet.
Als nächstes kommen wieder die zwei ineinander verschachtelten for-Schleifen, welche sämtliche Spalten und Zeilen durchlaufen. Dabei gibt der Befehl tabelleWidget->rowCount() die Anzahl der Zeilen in der Tabelle wieder und tabelleWidget->columnCount() die Anzahl der Spalten. Die Abfrage if(!inhalt.isEmpty()) ist sehr wichtig, da die Übergabe einer leeren Zelle in die Datei mittels des Operators zu einem Programmabsturz führt. Anschließend wird der Mauszeiger wieder hergestellt, die Datei geschlossen und natürlich der saveButton wieder Disabled.

Gegen unsere beiden ersten Slots ist der letzte geradezu lachhaft langweilig, beinhaltet er doch lediglich, daß der saveButton aktiviert wird.

Natürlich hat unser Programm so wie es jetzt ist einen kleinen Haken: Durch die festgelegte Größe des Arrays ist die maximale Zeilen- und Spaltenanzahl begrenzt. Daher ist es sinnvoller die Daten nicht über das Array einzutragen, sondern gleich direkt beim auslesen in die Tabelle zu schreiben. Da man vorher die erforderliche Zeilen- und Spaltenanzahl nicht kennt, muß man dynamisch mittels tabelleWidget->insertRow(row); und tabelleWidget->insertColumn(column); neue Zeilen und Spalten einfügen. Durch diese "Dynamik" ist es egal wie groß die Textdatei ist, solange der Arbeitsspeicher dafür ausreicht. Um die Daten auf direktem Weg in die Tabelle zu schreiben, müssen wir nur unseren Slot neuladen etwas abändern:

tabelle.cpp (Slot neuladen ohne Array)

void tabelleWindow::neuladen()
{
int s=-1;
int row=0;
int column=0;
int pos1,pos2,lastpos;
QString line;
QString eintrag;

QString verzeichnis=QCoreApplication::applicationDirPath();
verzeichnis.append("/tabellendaten.txt");

QFile datei(verzeichnis);
if(datei.open(QIODevice::ReadOnly | QIODevice::Text))
{
QApplication::setOverrideCursor(Qt::WaitCursor);
tabelleWidget->clear();
tabelleWidget->setRowCount(0);
tabelleWidget->setColumnCount(0);

QTextStream in(&datei);
while(!in.atEnd())
{
line = in.readLine();
tabelleWidget->insertRow(row);

column=0;
pos1=0;
lastpos=0;
while(pos1!=-1)
{
pos1=line.indexOf("\"",lastpos);
lastpos=pos1+1;
pos2=line.indexOf("\"",lastpos);
lastpos=pos2+1;
if(pos1!=-1)
{
if(s<column)
{
tabelleWidget->insertColumn(column);
s=column;
}
eintrag=line.mid((pos1+1),(pos2-pos1-1));
QTableWidgetItem *newItem = new QTableWidgetItem;
newItem->setText(eintrag);

// Für das Design der Zellen (siehe Anhang ganz unten)
// newItem->setTextAlignment(Qt::AlignCenter);
// newItem->setBackground(QColor(0, 0, 255, 255));
// newItem->setForeground(QColor(255,255,255,255));
// newItem->setFont(QFont("Arial", 12));

if(row==0)
{
tabelleWidget->setHorizontalHeaderItem (column, newItem);
}
else
{
tabelleWidget->setItem(row-1, column, newItem);
}
column++;
}
}
row++;
}
tabelleWidget->removeRow(row-1);
}
datei.close();

QApplication::restoreOverrideCursor();
saveButton->setEnabled(false);
}

Wie man an der Variablen-Deklaration sieht, fallen zwei int-Variablen für die Zeilen und Spalten weg, welche wir vorher für die for-Schleifen zum Eintragen der Daten in die Tabelle benötigt haben. Aus unserer int-Variablen s wird ein Zähler für die maximale Spaltenanzahl. Dieser Zähler hilft später mit einer if-Schleife nur soviel neue Spalten hinzuzufügen, wie auch benötigt werden. Die benötigten Zeilen ergeben sich aus den Zeilen der Datei. Am Anfang des Auslesevorgangs löschen wir erst einmal die Tabelle mit tabelleWidget->clear(); und setzen die Spalten und Zeilen auf 0, damit auch wirklich nur die Spalten und Zeilen erstellt werden, für die Werte vorhanden sind. Das Erstellen einer Zeile übernimmt dabei tabelleWidget->insertRow(row);. Der int-Wert row bezieht sich auf die Position an der die Zeile eingefügt werden soll. Gleiches gilt für tabelleWidget->insertColumn(column); für die Spalten. Hierfür kommt die angesprochene if-Abfrage if(s<column) zum Einsatz, welche nur Spalten hinzufügt, wenn sie benötigt werden. Der Rest ist wie gehabt. Der benötigte QString eintrag wird ausgeschnitten, dem QTableWidgetItem übergeben und in die entsprechende Zelle eingetragen. Da wir die erste Zeile unserer Daten-Datei als Spalten-Index verwenden, rutschen natürlich alle nachfolgenden Zeilen um eins nach oben, daher müssen wir die letzte Zeile -da leer- wieder löschen. Dies erledigt tabelleWidget->removeRow(row-1);.
Damit ist unsere Beispiel-Tabelle fertig und wer sie selbst kompilieren will, kann sich hier ein komprimiertes tar-Archiv mit den 5 benötigten Dateien herunterladen: tabelle.tar.gz

Noch zur Ergänzung falls jemand seine Tabelle optisch aufpeppen will:
tabelleWidget->horizontalHeader()->setResizeMode(column, QHeaderView::ResizeToContents);
Dieser Befehl ändert die Spaltenbreite automatisch auf die Inhaltsgröße, d.h. die Spalten werden mindestens so groß gemacht, daß der Längste Eintrag noch komplett lesbar ist. Allerdings wird dadurch automatisch das manuelle Resizen abgeschaltet, d.h. man kann die Spalten auch nicht mehr selbst von Hand in der Größe verändern. Kommt man auf die Idee den ResizeMode nach ResizeToContents wieder auf Interaktiv zurück zu schalten, werden auch die Spalten und Zeilen wieder den Standardwerten entsprechend neu resized, daher hilft das leider nichts.
Möchte man die Größe mit genau definierten Werten voreinstellen helfen folgende Befehle:
tabelleWidget->verticalHeader()->resizeSection(1,60);
macht die Zeile an Position eins 60 Pixel hoch.
tabelleWidget->horizontalHeader()->resizeSection(5,160);
macht die Spalte an Position fünf 160 Pixel breit.
Bei den resizeSection-Befehlen bleibt der ResizeMode auf Interaktiv und man kann die Größe der Spalten und Zeilen mit der Maus verändern. Und für die Ausrichtung im Header:
tabelleWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignRight);
Setzt die Default-Ausrichtung auf Rechtsbündig, somit werden alle Spalten-Indexe auf Rechtsbündig gesetzt. Möchte man den Text in den Zellen auch ausrichten muß man das für jedes Item separat machen, mittels
newItem->setTextAlignment(Qt::AlignCenter);
wird das entsprechende Item Horizontal UND Vertikal zentriert. Etwas Hintergrundfarbe gibt es mit
newItem->setBackground(QColor(0, 0, 255, 255));
Die Farbangabe setzt sich zusammen aus (Rot, Grün, Blau, Transparenz), wobei jeder der vier Werte in einem Bereich von 0-255 eingestellt werden kann. Das Gleiche gibt es auch für die Schriftfarbe:
newItem->setForeground(QColor(255, 255, 255, 255));
Und zu guter Letzt die Schrift selbst:
newItem->setFont(QFont("Arial", 12));
setzt die verwendete Schrift in den Zellen auf Arial mit einer Größe von 12 Pixeln.