advcal_offen.png

Vorfreude, schönste Freude

Adventskalender mit C++ und Qt

01.12.2001 Es müssen nicht immer Schokoladefigürchen sein: Ein Bilderadventskalender lässt sich auch als GUI-Programm gestalten und eignet sich hervorragend zum Einstieg in die C++-Programmierung.

Wenn der 1. Dezember naht und kein Adventskalender Büro oder Stube ziert, macht sich bei nicht allzu wenigen großen Kindern die Enttäuschung breit. Warum also nicht selbst einen stricken und dem Freundes- oder Kollegenkreis verehren?

Um das Angenehme mit dem Nützlichen zu verbinden, suche man sich eine Herstellungsweise, die man selbst schon immer einmal lernen wollte (Adventskalender sind derart kurzlebig, dass bei der/dem Beschenkten die kindliche Freude über den Verdruss angesichts der nicht zu übersehenden Laienhaftigkeit der Herstellung überwiegen wird). Ob Sie nun Laubsägerei, Origami oder eben auch GUI-Programmierung lernen wollen, bleibt Ihnen überlassen, doch da Sie von einer Linux-Zeitschrift vermutlich keine Anleitungen zum Umgang mit Holz und Buntpapier erwarten, wenden wir uns an dieser Stelle dem letztgenannten Thema zu.

Das Handwerkszeug

Am Anfang jedes Programmierprojekts steht die Frage nach dem Werkzeug. Den Texteditor suchen Sie sich am besten selbst aus. Fehlt noch die Programmiersprache (und der entsprechende Compiler bzw. Interpreter) sowie die Frage nach dem Grad der Faulheit. Hardcore-Programmierer/innen mögen mit der Xlib-Bibliothek direkt auf dem X-Window-System arbeiten und jede Linie des Programms pixelgenau "per Hand" zeichnen [1]. Doch viel praktischer ist es, sich ein GUI-Toolkit auszusuchen, das Elemente einer grafischen Benutzeroberfläche wie Knöpfe (Buttons), Auswahllisten, Karteikartenreiter, Menüs und Werkzeugleisten als vorgefertigte Klassen bereitstellt und auch sonst allerhand Erleichterung für die Programmiererin bietet.

Die am weitesten verbreiteten Open-Source-GUI-Toolkits sind GTK+ [2] (bekannt von Gimp oder GNOME), Qt [3] (auf dem u. a. KDE basiert) und Tk [4] (meist im Zusammenhang mit der Skript-Sprache Tcl verwendet). Mit der Festlegung auf ein Toolkit bleibt jedoch keine allzu große Freiheit mehr bei der Wahl der Programmiersprache, obwohl sowohl für GTK abseits von C, Qt abseits von C++ und Tk abseits von Tcl weitere Language bindings existieren.

Bleiben wir bei einer kompilierten Sprache (der Inhalt eines Adventskalenders sollte für seine Benutzer/innen besser erst nach Weihnachten offen zugänglich sein) und entscheiden uns für C++ mit Qt, das auch für andere Unix-Betriebssysteme, Windows und seit kurzem für MacOS X existiert. Ob Qt kostenlos unter Open-Source-Lizenzen (GPL oder QPL) verwendet werden darf oder ob man eine kommerzielle Ausgabe kaufen muss, hängt von der Lizenz des geplanten Softwareprojekts ab.

Attraktiv an Qt ist, dass die Herstellerfirma Trolltech ein GUI-Programm namens designer mit dem Qt-Paket mitliefert, mit dem sich grafische Oberflächen maustechnisch zusammenklicken lassen. (Auch für GTK gibt es ein solches "Rapid Application Development"-(RAD-)Tool namens Glade [5].) Allerdings sollte man davon keine Wunder erwarten: Um die Einstellmöglichkeiten (und teilweise auch die Funktionsweise) des Designers zu verstehen, sind Qt-Grundkenntnisse unabdingbar. Zudem kommt es sehr auf den Einsatzzweck an, ob das Programm nutzbringend verwendet werden kann: Den im Folgenden zu erstellenden Adventskalender weitgehend im Designer zu bauen, ist um Einiges umständlicher als die Programmierung "von Hand".

Wer Qt nicht selbst kompiliert, sollte darauf achten, dass nicht nur das Paket mit den Laufzeitbibliotheken (meist schlicht qt-Versionsnummer oder qt2-Versionsnummer genannt), sondern auch das qt(2)-devel-Paket mit den Qt-Header-Dateien und dem "Meta Object Compiler" moc installiert ist. Der Befehl rpm -qagrep qt listet bei RPM-basierten Distributionen die installierten Qt-Pakete auf. Obwohl Qt 3.0 zum Redaktionsschluss erschien, verwenden wir im Folgenden die letzte stabile Version 2.3.1, die aktuelleren Distributionen beiliegt. Der Source-Code kompiliert aber auch mit Qt 3.0 oder etwas älteren Ausgaben problemlos.

Ein C++-Compiler (gern der g++ aus der GNU-Compiler-Suite GCC) und das make-Tool runden die Ausstattung ab. Doch da das Erstellen von Makefiles für Qt-Programme nicht gerade ein Zuckerschlecken ist, empfiehlt sich zudem die Installation des tmake-Skripts [6], das bei SuSE nicht etwa in einem tmake-RPM, sondern unter qt-freebies logiert. Qt-3.0-Benutzer/innen dürfen stattdessen auf die Neuimplementierung qmake [7] zurückgreifen, die ähnlich funktioniert und sogar in der Qt-Distribution enthalten ist.

Ehe tmake sich jedoch zum Zusammenbauen von Linux-Makefiles eignet, muss die Variable TMAKEPATH auf das Verzeichnis gesetzt werden, das die tmake-Templates und -Konfigurationsdateien für die entsprechende Plattform enthält. Für Linux mit dem g++ heißt es linux-g++ und ist etwa unter /usr/share/tmake/lib (Caldera) oder /usr/lib/tmake (SuSE) zu finden.

Zudem muss die Umgebungsvariable QTDIR das Verzeichnis beinhalten, in dem das include-Directory mit den Qt-Header-Dateien und das lib-Unterverzeichnis mit den Bibliotheksdateien für die passende Qt-Version liegen. Meist ist das /usr/lib/qt2echo $QTDIR zeigt, ob dem so ist.

Es geht um Inhalte

Noch ehe wir mit dem Programmieren beginnen, lohnt es, sich über Aussehen und Inhalt des Kalenders Gedanken zu machen: Nahe liegen 24 quadratische Bildchen, die in vier Zeilen und sechs Spalten angeordnet sind und vor dem Öffnen von einem Deckelchen unter Verschluss gehalten werden. Ein 25. Bildchen darf den geschlossenen Türchen ihre Langweiligkeit nehmen (Abbildung 1).

Abbildung 1

Abbildung 1: Das Ziel – der Adventskalender mit geschlossenen Türchen

Vom ästhetischen Standpunkt wünschenswert ist eine einheitliche Hintergrundfarbe der 24 Bilder, während Nr. 25 mit der Grundfarbe der Applikation selbst harmonieren sollte. Hier kommt man um ein wenig Bildbearbeitung nicht herum: Abbildung 2 zeigt beispielweise, wie sich mit dem Color Editor (Windows / Color Editor) des Programms xv [8] einzelne Farben ändern lassen.

Abbildung 2

Abbildung 2: Mit den Red-, Green- und Blue-Rädern ändert man einzelne Farben im xv-Color-Editor

Außerdem wird das Leben einfacher, wenn alle Bildchen im (von Qt verstandenen) xpm-Format und auf eine einheitliche Größe skaliert vorliegen. Befinden sich etwa alle Bilddateien im gif-Format in einem Sammelverzeichnis, hilft das convert-Tool aus dem ImageMagick-Paket [9] beim Konvertieren und Skalieren:

for bild in *.gif; do
 convert -geometry 64x64 $bild `basename $bild gif`xpm
 done
convert -geometry 64x64  tuer.gif tuer.xpm

Dieses kleine Shell-Skript geht der Reihe nach alle Dateien mit der Endung .gif durch und skaliert sie auf maximal 64x64 Pixel herunter (die Proportionen bleiben dabei erhalten, sodass ein Bild, das höher als breit ist, mit einer Höhe von 64 Pixeln, aber einer geringeren Breite herauskommt). Gleichzeitig wandelt convert das Ergebnis dank entsprechender Dateiendungsangabe ins xpm-Format um und legt es unter dem alten Dateinamen, aber mit der Endung .xpm statt .gif ab.

Wer sich eine so entstandene XPM-Datei einmal anschaut, sieht schnell, warum sich dieses Format gut zum Programmieren in C(++) eignet:

static char *magick[] = { […] }

Das Bild liegt bereits als Zeiger (*) auf ein Feld (Array) ([]) vom Datentyp char ("character" – (druckbares ASCII-)Zeichen) vor. Liest man das entsprechende File mit #include in eine Quellcode-Datei ein, kann man ihn unter dem Namen magick sofort ansprechen. Allerdings mag Qt XPMs lieber als Konstanten, weshalb static char * in static const char* zu ändern wäre. Zudem dürfen natürlich nicht alle 25 Zeiger magick heißen – besser wäre etwa bild_dateiname (z. B. bild_1 bis bild_24 und bild_tuer). Das lässt sich mit sed und einem kleinen Shell-Skript erledigen:

for file in *.xpm; do
 sed -e "s/char \*magick/const char\* bild_`basename $file .xpm`/" $file > _$file
 mv _$file $file
 done

sed ersetzt das erste Auftreten von char *magick in einer Zeile durch const char* bild_ plus den Dateinamen des aktuell bearbeiteten Files ohne die Dateiendung .xpm. Der Zeiger in der Datei 1.xpm wird so in bild_1 umbenannt. Da das Sternchen in derlei regulären Ausdrücken eine Sonderbedeutung hat, muss es durch ein \ geschützt werden. Die veränderten Daten landen in einer temporären Datei _originalDateiname.xpm, mit der anschließend die Originaldatei überschrieben wird (mv).

Türchen und andere Objekte

Nach diesen Vorbereitungen heißt es, sich um das Programm selbst zu kümmern. Moderne GUI-Programmierung orientiert sich am Paradigma der Objektorientierung. Welche Objekte brauchen wir also? Neben dem obligatorischen Anwendungsobjekt ein Hauptfenster, das wiederum aus 24 Türchen besteht. Diese unterscheiden sich nur unwesentlich, nämlich in der Aufschrift und dem dahinter versteckten Bildchen. Es wäre also albern, 24 mal nahezu identischen Code zu schreiben. Stattdessen bauen wir eine Klasse namens Tuerchen, die all das in sich vereint, was bei Türchen-Objekten identisch ist. Die Unterschiede legen wir erst fest, wenn wir ein individuelles Objekt dieser Klasse erzeugen.

Das Erzeugen geht selbstverständlich nicht aus dem Nichts, sondern mit einer speziellen Funktion, dem Konstruktor der Klasse, den es in einer Datei mit der Endung .cpp ("C plus plus") zu implementieren gilt. Der Konstruktor heißt immer genauso wie die Klasse selbst, in unserem Fall also Tuerchen(). Als Funktion kann er Argumente mit auf den Weg bekommen. Das bietet sich an, denn die einzelnen Türchen-Objekte dürfen gleich bei ihrer "Geburt" erfahren, welches Bild sie enthalten und für welchen Tag sie gedacht sind:

Tuerchen::Tuerchen( QPixmap* bildchen, const int tag )

Der String Tuerchen vor dem :: besagt, dass der Konstruktor Tuerchen() zur Klasse Tuerchen gehört. Dabei übergeben wir das Bildchen nicht etwa als Zeiger auf ein char-Feld, wie es in den xpm-Dateien definiert ist, sondern als Zeiger auf eine QPixmap, eine spezielle Qt-Klasse, die die Arbeit mit Pixmaps erleichtert.

Damit unsere Tuerchen-Objekte die Vorteile von Qt-GUI-Objekten genießen, müssen sie selbst welche sein. Das geht ganz einfach mit Vererbung: Wir definieren, dass sich Tuerchen die Eigenschaften einer Qt-Widget-Klasse "einverleibt". Da es im Qt-Werkzeugkasten keine bereits passende Klasse gibt, leiten wir Tuerchen von der allgemeinen Qt-Widget-Klasse QWidget ab:

: QWidget( elternwidget, name )

Hilfe!

Der QWidget-Konstruktor hätte gern zwei Argumente, doch woher wissen wir das? Keine Hexerei, denn zum Glück gibt es die API-Dokumentation zu Qt, die sich in HTML-Form unter /usr/doc/qt(2)/html/, /usr/share/doc/packages/qt/html/ o. ä. im besten Fall bereits auf dem eigenen Rechner findet. Wer Qt aus RPMs eingespielt hat, muss aufpassen: Viele Distributoren lagern die Dokumentation in Extra-Pakete (qt2-examples, qt2-tutorial und qt2-doc-html bei Caldera, qt-devel-doc bei SuSE) aus. Sofern man ständig online ist, kann man den Web-Browser auch auf http://doc.trolltech.com/ richten; für unsere Zwecke reicht die Qt-2.3.1-Dokumentation unter http://doc.trolltech.com/2.3/.

Dort finden wir unter dem Punkt API Reference die Alphabetical Class List, wo unter dem Buchstaben W ein Link auf die Dokumentation zu QWidget zeigt. Als erste öffentliche (public) Funktion ist der Konstruktor QWidget aufgeführt; ein Klick darauf bringt uns zur näheren Beschreibung (Abbildung 3).

Abbildung 3

Abbildung 3: Hilfe zum QWidget-Konstruktor

Dieser Konstruktor hätte gern drei Argumente, wobei alle ignorieren kann, wer mit dem Standardwert 0 zufrieden ist. Unsere Tuerchen-Objekte sollen jedoch keine Hauptfenster werden, wie die Null des parent-Arguments festlegt, sondern zu einem Adventskalender-Objekt gehören. Einen Zeiger auf dieses Elternfenster wollen wir also bei der Geburt eines Tuerchens mit übergeben. Namenlos soll das Baby auch nicht sein: Zwar werden wir die im zweiten Argument angegebene "Widget-ID" nie verwenden, doch guter Programmierstil ist ihre Festlegung schon.

Da der name eines Widgets möglichst eindeutig sein soll, legen wir ihn besser beim Erstellen eines Türchen-Objekts als Argument des Tuerchen-Konstruktors fest. Und warum sollten wir nicht die Flexibilität behalten, auch das Eltern-Widget bei der Tuerchen-Geburt festzulegen? Der komplette "Fußabdruck" (footprint) des Tuerchen-Konstruktors sieht dann so aus:

Tuerchen::Tuerchen( QWidget* elternwidget, QPixmap* bildchen,
                 const int tag, const char* name )
         : QWidget( elternwidget, name ){}

Mit den Zeigern elternwidget und name werden wir nichts weiter anstellen – da soll sich QWidget drum kümmern.

Bekanntmachung

Damit andere Objekte, etwa der Adventskalender selbst, wissen, wie sie Tuerchen erzeugen (und was sie sonst noch damit anstellen können), müssen sie über die Schnittstellen (API, "Application Programmers' Interface") der Tuerchen-Klasse aufgeklärt werden. Diese schreibt man in eine Header-Datei, die sich mit der #include-Direktive leicht dort einlesen lässt, wo sie gebraucht wird. Darin steht, …

#include <qwidget.h>
 class Tuerchen : public QWidget{

…, dass es sich bei Tuerchen um eine von QWidget abgeleitete Klasse handelt, die

public:
   Tuerchen( QWidget*, QPixmap*, const int, const char * );

zur allgemeinen (also öffentlichen, public) Verwendung einen Konstruktor anbietet, der vier Argumente der angegebenen Datentypen verlangt. Nicht für andere Objekte zugänglich sind hingegen die "Ablagen", in denen Tuerchen-Objekte ihre eigenen Daten aufbewahren:

private:
   int aufschrift; // Tag
   QLabel* bild; // Inhalt des Tuerchens
   QPushButton* tuer; // die "Deckplatte"
 };

eine Integer-Variable, die den Tag enthält, für den das Türchen gedacht ist, sowie Zeiger auf das Label mit dem "geheimen" Bildchen und die am entsprechenden Tag "abzunehmende" Deckplatte. Um diese darzustellen, bietet sich ein entsprechend formatierter Knopf an, auf den die neugierigen User klicken sollen – also ein Pushbutton. Die gesamte, mit dem Stichwort class beginnende Klassen-Deklaration ist eine einzige C++-Anweisung, weshalb am Ende das Semikolon nicht fehlen darf.

Doch bislang kennt noch niemand die Klassen QLabel und QPushButton. #include-Anweisungen müssten her. Da Zeiger jedoch nur auf Speicherstellen weisen, die ein entsprechendes Objekt enthalten, brauchen wir an dieser Stelle noch nicht den Ballast der gesamten API für diese Klassen mitschleppen. Mit

class QLabel;
 class QPushButton;

behaupten wir einfach nur vor der Tuerchen-Deklaration, dass diese Klassen bekannt sind. Spätestens, wenn die Klassenvariablen mit Anfangswerten belegt, initialisiert, werden (eine Aufgabe, die dem Konstruktor zufällt) und damit tatsächlich ein Objekt dieser Klasse gespeichert wird, brauchen wir die jeweilige Klassen-API jedoch. Deshalb gehört ein

#include <qlabel.h>
 #include <qpushbutton.h>

an den Anfang der Implementationsdatei tuerchen.cpp. Die API der meisten Qt-Klassen ist in einem Headerfile gleichen Namens (jedoch kleingeschrieben) verstaut, bei den Ausnahmen findet man den passenden Dateinamen am Anfang der jeweiligen Klassendokumentation.

Den Namen der Header-Datei der Tuerchen-Klasse haben wir mit tuerchen.h selbst festgelegt – doch da es sich um eine eigene API handelt, die im selben Verzeichnis wie die cpp-Datei liegt, wird sie beim Einfügen nicht in spitze Klammern, sondern in doppelte Anführungszeichen gesetzt:

#include "tuerchen.h"

Geburtshelfer

Innerhalb der geschweiften Klammern des Tuerchen-Konstruktors belegen wir jetzt die Klassenvariable aufschrift mit dem ganzzahligen Wert aus dem Parameter tag:

aufschrift = tag;

Damit bild, der Zeiger auf das Label mit dem Hintergrundbildchen, tatsächlich auf etwas zeigt, rufen wir mit dem Schlüsselwort new ein neues QLabel-Objekt ins Leben. Dessen Konstruktor ist zufrieden, wenn man ihm das Eltern-Widget als Argument übergibt. Da es ein Kind des jeweiligen Tuerchen-Objekts sein soll, reicht hier das Keyword this:

bild = new QLabel( this );

Das neue, noch leere Label-Objekt bekommt mit der QLabel-Funktion setPixmap() die im Konstruktor-Argument bildchen enthaltene xpm-Grafik aufgedrückt, …

bild->setPixmap( *bildchen );
  bild->setAlignment( Qt::AlignCenter );

…, die wir eine Zeile später zentrieren. Da AlignCenter nichts ist, was QWidget und damit Tuerchen geerbt haben, müssen wir davor den doppelten Doppelpunkt setzen, vor dem wiederum steht, in welcher Klasse (genauer in welchem Namensraum) es definiert ist – in diesem Fall ganz global im Namensraum Qt.

Nicht alle XPMs sind genau quadratisch, weshalb um manche ein Rand in der Standardfarbe des Labels, grau, verbliebe. Das sieht nicht schön aus, und so färben wir das komplette Label mit der Hintergrundfarbe der Pixmaps ein. Bei unseren Beispielbildchen ist das weiß, das netterweise als Qt::white vordefiniert ist.

bild->setPalette( Qt::white );

Genaugenommen sorgen wir mit setPalette() dafür, dass das Label in einer Farbenpalette auf der Basis von Weiß eingefärbt wird. Richtig bemerkbar macht sich dieses Detail bei 3D-Effekten weniger "flacher" Widgets wie dem QPushButton.

Wer hat nicht schon bei Adventskalendern aus Papier versucht, vorzeitig hinter das Geheimnis zu kommen? Selbst wenn sich die Türchen nicht vorzeitig öffnen lassen, kann ein verräterisches Flimmern beim Aufruf (und grafischen Aufbau) des Programms mehr als gewünscht verraten. Daher verstecken wir das bild-Objekt vorsichtshalber mit bild->hide();.

Das Pushbutton-Türchen verlangt ein wenig Vorarbeit. Zwar lesen wir mit #include "tuer.xpm" die in der Variablen bild_tuer abgelegte XPM-Beschreibung des Frontbildchens ein, doch darin ist noch keine Tageszahl integriert. Mit

QPixmap tuerfuellung( bild_tuer );

machen wir aus der Beschreibung erst einmal ein richtiges Pixmap-Objekt (keinen Zeiger!) namens tuerfuellung, das wir mit

QPainter leinwand( &tuerfuellung );

auf eine virtuelle Leinwand kleben. Jetzt holen wir einen weißen Stift für diese leinwand

leinwand.setPen( Qt::white );

und schreiben in der linken, oberen Ecke (Qt::AlignTop) eines auf dem QPainter links oben (bei den Koordinaten 0,0) beginnenden Rechtecks von 64 Pixeln Breite und eben dieser Höhe den Text, der in der Variablen aufschrift gespeichert ist:

leinwand.drawText( 0, 0, 64, 64, Qt::AlignTop, QString::number( aufschrift ) );

aufschrift ist aber eine ganzzahlige Zahl und kein Text. Den machen wir aus ihr mit Hilfe der QString-Funktion number(). Wir räumen die Staffelei weg (leinwand.end();) und erzeugen den Button mit der veränderten Pixmap, auf den unser Zeiger tuer weisen soll:

tuer = new QPushButton( this );
  tuer->setPixmap( tuerfuellung );

Auch das Türfüllungsbild misst nicht ganz 64x64 Pixel, weshalb es sich anbietet, die Farbpalette des hinter tuer versteckten Objekts entsprechend anzupassen. Mit Tools wie xv oder display erfährt man den RGB-Wert der entsprechenden Hintergrundfarbe, bei tuer.xpm auf der CD ist es ein Rotanteil von 0, ein Grünanteil von 155 und ein Blauanteil von 0. Da setPalette() auch QColor-Objekte als Argument entgegennimmt, machen wir aus dem RGB-Wert zunächst ein solches:

tuer->setPalette( QColor( 0, 155, 0 ) );

Damit das komplette Tuerchen-Objekt nicht über die Größe der darin enthaltenen Bildchen hinauswächst, legen wir noch eine feste Größe fest:

setFixedSize( 64, 64 );

Hört die Signale!

Das wäre dann auch schon fast die gesamte Arbeit des Konstruktors gewesen – wenn sich so ein Türchen nicht auch einmal öffnen sollte. Die Aufforderung dazu kommt von Benutzer/innen, die auf den sichtbaren Pushbutton des Tuerchen-Objekts klicken. Dieser sendet daraufhin ein Signal namens clicked() in die Welt. Doch wenn niemand darauf reagiert, passiert auch nichts. Daher müssen wir das clicked()-Signal von tuer mit einem Slot verbinden, einer Funktion, die dafür sorgt, dass etwas geschieht. Dazu dient die Qt-spezifischen Zeile

connect( tuer, SIGNAL( clicked() ), this, SLOT( open() ) );

… , sofern in der Klassendeklaration von Tuerchen im Include-File tuerchen.h das Stichwort (Makro) Q_OBJECT (ohne Semikolon) steht. Allerdings hat Tuerchen bislang noch gar keinen Slot namens open(). Ihn müssen wir in der API mit

private slots:
   void open();

deklarieren und in tuerchen.cpp dazu bringen, dass er auch etwas tut:

void Tuerchen::open() {

Argumente braucht open() keine und muss auch nichts zurückgeben (daher der Rückgabewert void). Was aber passiert, wenn ein Türchen angeklickt wird? Es soll geöffnet werden, aber nur, wenn der entsprechende Tag gekommen ist. Daher holen wir uns mit der QDate-Funktion currentDate() das aktuelle Datum:

QDate heute = QDate::currentDate();

Wer bereits im November zu neugierig ist, soll eine Fehlermeldung bekommen, allerdings eine andere als im Dezember (von Januar bis Oktober machen wir der Einfachheit halber kein Geheimnis aus dem Kalenderinhalt). Die Nachricht selbst legen wir in einer Hilfsvariablen vom Typ QString ab; den Monat bekommen wir aus einem QDate-Objekt mit der Funktion month() heraus:

QString nachricht;
   if ( heute.month() == 11 ){
     nachricht = "Jetzt ist aber noch November!\n"
                        "Schummeln gilt nicht!";

Das \n in der Zeichenkette nachricht steht dabei für einen Zeilenumbruch ("newline"). Im Dezember machen wir die nachricht etwas komplizierter und geben den aktuellen Tag (QDate::day()) mit an.

} else if ( heute.month() == 12 ){
     nachricht = "Heute ist erst der " +
                 QString::number( heute.day() ) +
                 ". Dezember!\n Schummeln gilt nicht!";
   }

Allerdings gibt diese Funktion einen ganzzahligen Wert zurück, den wir in einen QString umwandeln. Dabei kommt uns zugute, dass sich QStrings einfach durch Pluszeichen miteinander "addieren", also aneinander anhängen lassen.

Wer schummelt und im Dezember ein Türchen anklickt, dessen aufschrift größer als der entsprechende Tag ist, oder () im November versucht "einzubrechen", …

if ( ( heute.month() == 12 && heute.day() < aufschrift )
           || heute.month() == 11 ){

… soll auf seine Missetat aufmerksam gemacht werden (&& besagt als logisches Und, dass sowohl die davor als auch die danach stehende Bedingung stimmen muss, damit die Teilbedingung in runden Klammern wahr wird), und zwar mit einer Messagebox.

QMessageBox* infobox = new QMessageBox( "Schmuh!",
         nachricht,
         QMessageBox::NoIcon,
         QMessageBox::NoButton,
         QMessageBox::Ok,
         QMessageBox::NoButton);

Erst wenn der Übeltäter dieses kleine Fensterchen (Abbildung 4) mit der Titelzeile Schmuh! und dem in nachricht abgelegten Text weggeklickt hat, ist das Hauptfenster wieder zugänglich. Qt bietet vorgefertigte Icons für Messageboxes (QMessageBox::Information, QMessageBox::Warning und QMessageBox::Critical). Um die Meldung etwas adventlicher zu gestalten, verzichten wir vorerst darauf (QMessageBox::NoIcon). Ebenso lassen wir von den drei möglichen Knöpfen den linken und den rechten mit QMessageBox::NoButton weg, während der mittlere mit QMessageBox::Ok der OK-Button zum Wegklicken wird.

Abbildung 4

Abbildung 4: Schummeln gilt nicht

Das neue Icon setzen wir mit der QMessageBox-Funktion setIconPixmap(). QMessageBox::exec() sorgt dafür, dass die Box auf dem Bildschirm erscheint:

infobox->setIconPixmap( QPixmap( bild_tuer ) );
     infobox->exec();

Drückt die Benutzerin hingegen brav das Türchen eines erlaubten Tages, sorgen wir mit show() dafür, dass das bislang unsichtbare QLabel, auf das bild zeigt, erscheint, und dafür der sich hinter tuer versteckende QPushButton von der Bildfläche verschwindet:

} else {
     bild->show();
     tuer->hide();
   }
 }

Eine Frage der Anordnung

Ein Problem bleibt aber noch: Zwar beschränkt der Konstruktor die Größe des Tuerchens auf eine feste Größe von 64x64 Pixeln, doch wie darin Label und Pushbutton angeordnet sind, haben wir noch nicht festgelegt. Das führt zu so netten Ergebnissen wie Abbildung 5, wo das Qt-Layout-Management – Bug oder Feature? – gnadenlos dafür sorgt, dass die Pushbuttons zwar 64 Pixel breit sind, aber eine geringere Höhe bekommen. Bei Knöpfen normaler Anwendungen verbessert dieser Automatismus die Ästhetik, doch in unserem Fall zwingt er uns zu einem Kunstgriff.

Abbildung 5

Abbildung 5: Unerwartetes Ergebnis

Wir müssen immer dann, wenn sich die Größe eines Tuerchen-Objekts ändert, festlegen, wo und in welchen Abmessungen Label und Pushbutton erscheinen sollen. Da wir keine Möglichkeit vorsehen werden, die Größe der Applikation (und damit der Türchen) User-seitig anzupassen, gibt es dieses Ereignis zwar nur einmal beim Aufruf des Adventskalenders, wenn sich die Größe des Fensters von 0 auf die Endabmessungen ändert, aber auch dafür lohnt es sich, den Event Handler resizeEvent() zu reimplementieren, den Objekte der Klasse Tuerchen von QWidget erben. Diese Funktion wird automatisch immer dann aufgerufen, wenn der Window Manager ein Resize-Ereignis schickt.

In tuerchen.h deklarieren wir daher im public-Bereich

void resizeEvent( QResizeEvent * );

und füllen die Funktion in tuerchen.cpp mit Leben, indem wir sagen, dass sowohl das QLabel, auf das der Zeiger bild zeigt, als auch der QPushButton, der sich hinter tuer verbirgt, ausgehend von der Koordinaten (0,0) links oben im Eltern-Tuerchen 64 Pixel breit und ebenso hoch angeordnet werden:

void Tuerchen::resizeEvent( QResizeEvent* )
 {
   bild->setGeometry( 0, 0, 64, 64 );
   tuer->setGeometry( 0, 0, 64, 64 );
 }

Da wir das als Argument mitgeschickte QResizeEvent nicht selbst bearbeiten wollen, brauchen wir dem Zeiger darauf auch keinen Namen geben.

Das große Ganze

24 dieser Tuerchen-Objekte wollen nun erschaffen und angeordnet werden – am besten in einem Objekt einer neuen Klasse Adventskalender, die das Hauptfenster stellt. Auch diese einfache Klasse, die lediglich einen Konstruktor besitzt, leiten wir von QWidget ab und schreiben ihr API in eine Datei namens adventskalender.h (Listing 1).

Listing 1

Adventskalender-API

adventskalender.h
#include <qwidget.h>
 class Adventskalender : public QWidget
 {
 public:
   Adventskalender( QWidget *elternwidget = 0,
       const char *name = "adventskalender" );
 };

adventskalender.cpp besteht aus gar nicht besonders viel Programmierung, sondern vor allem aus Festlegungen: Welche Bildchen sollen sich hinter den einzelnen Türchen verstecken, …

#include "1.xpm" // enthaelt bild_1[…]
 #include "24.xpm" // bild_24

für welchen Tag ist welcher Türcheninhalt gedacht, ….

static const char  bilder[24] = {
   bild_1, […], bild_24 };

… und an welchen x-y-Koordinaten soll das für einen bestimmten Tag gedachte Türchen stehen? (Die erste Spalte der ersten Zeile trägt die Koordinate 0,0.)

struct pos{
   int x;
   int y;
 } position[24] = {
   {1, 0}, // Position fuer Tuerchen Nr. 1[…]
   {2, 2}  // Nr. 24
 };

Im Adventskalender-Konstruktor legen wir zunächst ein unsichtbares sechsspaltiges und vierzeiliges Gitter fest, in dem die einzelnen Türchen "einrasten" sollen:

Adventskalender::Adventskalender( QWidget *elternwidget, const char *name )
                : QWidget( elternwidget, name )
 {
   QGridLayout * gitterraster = new QGridLayout( this, 6, 4,
                                    6, 4, "gitterraster" );

Bei den letzten drei Argumenten des QGridLayout-Konstruktors handelt es sich um die Randbreite (6), den Platz zwischen den Zellen (4 Pixel) und die Widget-ID des gitterraster-Zeigers. 24 Türchen erzeugen wir nun in einer for-Schleife, deren Körper innerhalb der geschweiften Klammern erst mit der Zählervariablen i gleich 1 bis hin zur 24 durchlaufen wird (i++ bedeutet, dass der Inhalt von i am Ende jeden Schleifendurchlaufs um eins erhöht wird):

for ( int i = 1; i <= 24; i++ ){
     QString name( "tuerchen" );
     name += QString::number(i);

Damit jedes Tuerchen-Objekt eine eigene ID bekommt, setzen wir sie in der Variablen name zusammen: Beim Türchen, das im ersten Schleifendurchlauf entsteht, lautet sie tuerchen1, beim letzten Durchlauf lautet name auf tuerchen24. name += Wert ist dabei eine Kurzform für name = name + Wert.

Nachdem wir die Tuerchen-API am Anfang der Datei mit #include "tuerchen.h" innerhalb der Adventskalender-Implementierung bekannt gemacht haben, können wir den public deklarierten Tuerchen-Konstruktor nun mit den Argumenten Eltern-Widget, Hintergrundbildchen, Aufschrift und ID aufrufen:

Tuerchen * tuer = new Tuerchen( this, new QPixmap( bilder[i-1] ), i, (const char *) name );

Mit bilder[i-1] holen wir uns dabei das an i-ter Stelle abgelegte XPM aus dem bilder-Array – da die Zählung bei Array-Elementen jedoch bei 0 beginnt, müssen wir als Index i-1 angeben. Indem wir es dem QPixmap-Konstruktor als Argument übergeben, erhalten wir ein passendes Pixmap-Objekt.

Zuletzt bauen wir das neue Tuerchen an passender Stelle ins Layout-Raster ein:

gitterraster->addWidget( tuer, position[i-1].x,
                                    position[i-1].y );
   }

Als Positionsangabe dient jeweils der x- und der y-Wert aus dem position-Element mit dem Index i-1. Tuerchen Nr. 24 wird damit an der Stelle (2,2) im Raster eingebaut.

Das gesamte Adventskalender-Widget färben wir jetzt (wie den Tuerchen-Pushbutton) grün ein und teilen dem Qt-Layout-Management außerdem mit, dass es eine feste, unveränderliche Größe haben soll. Diese berechnen wir jedoch nicht, sondern sagen mit sizeHint() einfach: "Nimm die Größe, bei der alle Teil-Widgets ideal platziert sind."

setBackgroundColor( QColor( 0, 155, 0 ) );
   setFixedSize( sizeHint() );
 }

Finale

Alle Widgets sind da, fehlt nur noch das Applikationsobjekt selbst. In der (allen C/C++-Programmen eigenen) Hauptfunktion main() (Listing 2) übergeben wir eventuelle Kommandozeilen-Optionen und Argumente (in unserem Fall prophylaktisch) an die eigentliche Applikation (argv enthält diese, argc ihre Anzahl). Das neue Adventskalender-Objekt wird in Zeile 8 zum Hauptfenster der Applikation, auf die app zeigt. Zeile 9 sorgt dafür, dass es auch auf dem Bildschirm erscheint, während Zeile 10 die sogenannte Anwendungsschleife startet: Das Adventskalender-Objekt beginnt zu leben. Sobald es geschlossen wird, bekommen wir von exec() einen Rückgabewert, und das Programm beendet sich.

Listing 2

Hauptroutine

advcal.cpp
1 #include <qapplication.h>
  2 #include "adventskalender.h"
  3
  4 int main( int argc, char  argv )
  5 {
  6   QApplication * app = new QApplication( argc, argv );
  7   Adventskalender * advcal = new Adventskalender();
  8   app->setMainWidget( advcal );
  9   advcal->show();
 10   return app->exec();
 11 }

Der Code ist geschrieben (und auf CD komplett nachzulesen) – wie wird daraus jetzt ein ausführbares Programm? Hiermit schlägt tmakes (oder qmakes) große Stunde: Aus einer einfachen Projektdatei, die alle selbstgeschriebenen Header-Dateien unter HEADERS, alle Implementierungsfiles unter SOURCES und den Namen der zu generierenden ausführbaren Datei unter TARGET angibt (Listing 3), wird mit

tmake -o Makefile advcal.pro

ein Makefile. Die CONFIG-Angaben release, qt und warn_on bedeuten, dass wir eine optimalisierte Qt-Applikation erstellen wollen, bei deren Übersetzung der Compiler mehr Warnungen ausgeben soll als üblich. Dass es sich beim Ergebnis um eine Anwendung (und nicht um eine Bibliothek) handelt, legt die TEMPLATE-Variable fest.

Listing 3

Projektdatei

advcal.pro
TEMPLATE = app
 CONFIG   = qt warn_on release
 HEADERS  = adventskalender.h \
            tuerchen.h
 SOURCES  = advcal.cpp \
            adventskalender.cpp \
            tuerchen.cpp
 TARGET   = advcal

Ein make sorgt dann hoffentlich für fehlerfreies Durchkompilieren – und ein ausführbares Binary namens advcal.

Glossar

GUI

"Graphical User Interface" – grafische Benutzerschnittstelle, der für Anwender/innen sichtbare Teil einer grafischen Applikation.

Compiler

Programm, das aus menschenlesbarem Quellcode ein maschinenlesbares ausführbares Binary macht.

Interpreter

Programm, das in einer Skript-Sprache geschriebenen Quellcode interpretiert und sofort ausführt.

Xlib

Die grundlegende Bibliothek, die alle zur Entwicklung einer X-Applikation nötigen (Low-Level-)Funktionen enthält.

basename

Dieses Kommando schneidet von einem Dateinamen, den es als erstes Argument bekommt, die Pfadangabe und ggf. die als zweites Argument angegebene Dateinamensendung ab. Aus tuer.gif in der Variablen bild wird so tuer., und da das basename-Kommando in `Backticks` eingeschlossen wurde, fügt die Shell dessen Ergebnis an deren Stelle ein. So wird letztlich folgendes Kommando ausgeführt:

static

Globale (also außerhalb einer Klasse oder Funktion deklarierte) statische Variablen sind außerhalb ihrer Datei (bzw. der, in die sie mit #include eingelesen wurden) unbekannt.

//

C++-Kommentarzeichen. Alles, was hinter ihm auf der Zeile steht, ignoriert der Compiler.

aufgedrückt

Operator ->, der die darauffolgende Funktion auf das Objekt anwendet, auf das ein Zeiger zeigt. Arbeitet man hingegen mit Objekten, die direkt in Variablen gespeichert sind, so heißt der entsprechende Operator . (Punkt).

Tip a friend    Druckansicht beenden Bookmark and Share
Kommentare