PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Qt Grundlagen Tutorial



anda_skoa
25-11-2004, 18:38
Beipieldatei: lektion1.cpp


#include <qapplication.h>
#include <qpushbutton.h>

int main( int argc, char* argv[] )
{
QApplication app( argc, argv );

QPushButton helloworld( "Hello World", 0 );
helloworld.resize( 100,30 );

app.setMainWidget( &helloworld );

helloworld.show();

return app.exec();
}

Nun betrachten wir das Programm Zeile für Zeile:


#include <qapplication.h>

Dieser Header deklariert die Klasse QApplication (http://doc.trolltech.com/3.0/qapplication.html).
Eine Instanz der Klasse QApplication wird benötigt, wenn das Programm das Eventhandling von Qt benutzen möchte.
Events benötigt man wenn man

Widgets benutzt, d.h. wenn man eine GUI machen möchte
Sockets benutzt, d.h. wenn man Netzwerkkommunikation machen möchte
QProcess benutzt, d.h. wenn man andere Programme als Kindprozesse starten möchte


In diesem Fall trifft (a) zu.



#include <qpushbutton.h>

Dieser Header deklariert die Klasse QPushButton (http://doc.trolltech.com/3.0/qpushbutton.html).
Ein QPushButton ist ein klassischer Button.



int main( int argc, char *argv[] )

Beginn der normalen main Funktion eines C++ Programms.



QApplication app( argc, argv );

Wir erzeugen unsere QApplication Instanz und verwenden als Parameter die Kommandozeilen Argumente des Programms.
Qt definiert ein paar mögliche Argumente und benutzt sie, wenn sie in argv enthalten sind.
Die verbleibenden Argumente verhält man über die Methoden argc() bzw. argv() der QApplication Instanz.



QPushButton helloworld( "Hello World", 0 );

Dies erzeugt einen QPushButton, dessen Eigenschaften wir mit Hilfe seiner Methoden ändern können.
Das erste Argument gibt den Text an, der im QPushButton stehen soll, der zweite das Parent Widget.
Das Parent Widget ist das GUI Element, in dem das neue Widget enthalten sein soll.
Hier ist der Parent 0, d.h. der Button ist hat keinen Parent.

Hat ein QObject (das gilt nicht nur für QWidgets) ein Parent QObject, dann wird der Parent alle seine Kindobjekte löschen, wenn er selbst gelöscht wird.
Das ist vorallem bei Widgets praktisch, weil man nicht alle Elemente eines Fenster einzeln löschen muss, sondern es reicht, das Fenster zu löschen.



helloworld.resize( 100,30 );

Die Methode resize eine QWidget ändert seine Größe. Die Parameter sind die Breite (width) und die Höhe (height).
Der Button wird also auf 100 Pixel Breite und 30 Pixel Höhe gesetzt.



app.setMainWidget( &helloworld );

Setzt unseren QPushButton helloworld als MainWidget der Applikation. Normalerweise hat jede Qt GUI-Applikation ein MainWidget.
Wird dieses geschlossen, beendet sich auch die QApplication.



helloworld.show();


Ein Widget ist nach seiner Erzeugung nicht automatisch sichtbar.
Es muss erst die Methode show aufgerufen werden.
Widgets die eine Parent haben, werden sichtbar, wenn der Parent sichtbar wird, außer der Parent war bei Erzeugung des Kindwidgets bereits sichtbar.



return app.exec();

Hier übergeben wir der Qt-Application ( app ) das Kommando.
Sobald diese beendet wird, liefert app einen Rückgabewert und die main Funktion wird ebenfalls beendet.

Kompilierung:

Um diese Datei zu kompilieren, braucht man ein paar Argumente für den C++ Compiler.
Da wir Qt Headerdateien verwenden, brauchen wir den Pfad an dem sie liegen.
Da wir anschliessend mit der Qt Bibliothek linken wollen, brauchen wir auch den Pfad der Bibliothek.

Um die beiden Pfade zu erhalten, rufen wir locate auf

#> locate qapplication.h
/usr/lib/qt-3.0.3/include/qapplication.h
/usr/share/doc/qt-devel-3.0.3/html/qapplication.html


Das zweite ist der Pfad der Dokumentation, aber den brauchen wir nicht.
Unsere Qt Header sind hier also in /usr/lib/qt-3.0.3/include

#> locate libqt
/usr/lib/qt-3.0.3/lib/libqt-mt.so.3
/usr/lib/qt-3.0.3/lib/libqt-mt.so.3.0
/usr/lib/qt-3.0.3/lib/libqt-mt.so.3.0.3
/usr/lib/qt-3.0.3/lib/libqt.so.3
/usr/lib/qt-3.0.3/lib/libqt.so.3.0
/usr/lib/qt-3.0.3/lib/libqt.so.3.0.3
/usr/lib/qt-3.0.3/lib/libqt-mt.so
/usr/lib/qt-3.0.3/lib/libqt.so


Der Pfad zur Bibliothek ist also /usr/lib/qt-3.0.3/lib
Die Qt Bibliothek liegt hier sowohl mit (libqt-mt.so) Thread Unterstützung vor, also auch ohne (libqt.so).
Welche von beiden benutzt wird, ist für unser Beispiel nicht wichtig.

(Die Pfade sind von System zu System unterschiedlich)

Der Aufruf lautet nun wie folgt:

#> g++ -o lektion1 lektion1.cpp -Wall -I/usr/lib/qt-3.0.3/include -L/usr/lib/qt-3.0.3/lib -lqt-mt


Zur Erklärung:

g++ : der C++ Compiler unter Linux.
-o lektion1 : der Name, den das Programm haben soll
lektion1.cpp : Unsere Quelltextdatei
-Wall : aktiviert alle Warnung des Kompilers
-I/usr/lib/qt-3.0.3/include: Angabe eines Pfades für Headerdateien (das I ist ein großes i)
-L/usr/lib/qt-3.0.3/lib : Angabe eines Pfads für Bibliotheken
-lqt-mt : die Bibliothek, mit der unser Programm gelinkt werden soll (das l ist ein kleines L)
Der Aufruf mit der Qt ohne Threadunterstütztung unterscheidet sich nur in diesem letzten Argument: -lqt


Treten beim Kompilieren und Linken keine Fehler auf, kann das Programm nun gestartet werden:

#> ./lektion1

anda_skoa
25-11-2004, 18:47
Der Button aus Lektion 1 hat noch keine Funktion.
Es wäre doch fein, wenn ein Klick auf den Button das Fenster schliessen würde.

Das geht ganz leicht, mit nur einem weiteren Befehl:

lektion2.cpp


#include <qapplication.h>
#include <qpushbutton.h>

int main( int argc, char* argv[] )
{
QApplication app( argc, argv );

QPushButton helloworld( "Hello World", 0 );
helloworld.resize( 100,30 );

app.setMainWidget( &helloworld );

QObject::connect(&helloworld, SIGNAL(clicked()), &app, SLOT(quit()));

helloworld.show();

return app.exec();
}

Betrachten wir die zusätzliche Zeile:



QObject::connect(&helloworld, SIGNAL(clicked()), &app, SLOT(quit()));


Wir sehen im wesentlichen zwei Sachen:

den Aufruf der offensichtlich statische Methode connect der Klasse QObject
die Anwendung von zwei Makros: SIGNAL und SLOT


QObject (http://doc.trolltech.com/3.0/qobject.html) ist die Basis aller Klassen, die in Qt an Events beteilgt sind.

In der Sektion "Static Public Members" finden wir auch die oben benutze Funktion:


bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * member )

Was macht nun diese Methode?

Sie verbindet ein Signal eines Senderobjektes mit einem Slot eines Empfängerobjektes.

Signale und Slots sind eine Eigenheit von Qt.
Sie erlauben das verbinden zwei Objekten, ohne dass sich die beiden Partner kennen müssen.
Nur der Verbinder muss beiden Instanzen kennen, jedoch nicht unbedingt ihren jeweiligen Typ, solange er weiß, dass es QObject Subklassen sind.

Signal und Slots sind eine Art lokales Event.

Jede QObject Subklasse kann Signale aussenden und Signale empfangen.
Wenn ein Signal nicht verbunden ist, verpufft es einfach.

Ein Signal kann mit mehreren Slots verbunden sein.
In diesem Fall werden alle Slots in zufälliger Reihenfolge aktiviert, also ausgeführt.

In unserem Beispiel verknüpfen wir das Signal clicked() des QPushButtons mit dem Slot quit() der QApplication Instanz.
Signale und Slots befinden sich in der Qt Dokumentation immer in eigene Sektionen der jeweiligen Klassen.

clicked() ist in dem Fall nicht direkt in QPushButton sondern bereits in der
Basisklasse QButton (http://doc.trolltech.com/3.0/qbutton.html) deklariert.

Wie die leere Klammer andeutet, hat das Signal clicked() keinen Parameter, transportiert also keine Daten.
Darum können wir es nur mit einem Slot verbinden, der keinen Parameter
erwartet.
Glücklicherweise ist quit() genau so ein Slot. :)

Die Parameter von Signal und Slot müssen immer in ihrem Typ übereinstimmen.
Das wird allerdings erst zur Laufzeit überprüft, der Compiler meldet also keinen etwaigen Fehler. So etwas sieht man dann auf der Konsole, von der aus das Programm gestarte wurde.

Dieser Effekt kann leicht demonstiert werden, wenn man clicked() gegen toggled(bool) austauscht.

Unsere Programmzeile würde in diesem Fall so aussehen:


QObject::connect(&helloworld, SIGNAL(toggled(bool)), &app, SLOT(quit()));


Kompilierung

Die Kompilierung erfolgt wie in der ersten Lektion:

#> g++ -o lektion2 lektion2.cpp -Wall -I/usr/lib/qt-3.0.3/include -L/usr/lib/qt-3.0.3/lib -lqt-mt

Wenn das Programm nun ausgeführt wird, wird bei einem Klick auf den Button das Programm beendet.
Durch den Klick sendet der Button sein clicked() Signal aus.
Anders als in Lektion 1 ist dieses Signal nun mit quit() der QApplication verbunden. Darum wird von Qt diese Methode ausgeführt, was die QApplication dazu veranlasst, sich zu beenden.

anda_skoa
25-11-2004, 18:55
In dieser Lektion haben wir es nun mit drei Dateien zu tun:

lektion3.cpp: enthält wie immer die main Funktion
qttutwidget.h: enthält die Deklaration einer Beispielwidgetklasse
qttutwidget.cpp: enthält die Implementation des Widgets

lektion3.cpp


#include <qapplication.h>
#include <qpushbutton.h>

#include "qttutwidget.h"

int main( int argc, char* argv[] )
{
QApplication app( argc, argv );

QtTutWidget helloworld( "Hello World", 0 );
helloworld.resize( 100,30 );

app.setMainWidget( &helloworld );

helloworld.show();

return app.exec();
}


qttutwidget.h


#ifndef QTTUTWIDGET_H
#define QTTUTWIDGET_H

#include <qlabel.h>

class QtTutWidget : public QLabel
{
public:
QtTutWidget(QString text, QWidget* parent=0, const char* name=0);
};

#endif


qttutwidget.cpp


#include "qttutwidget.h"

QtTutWidget::QtTutWidget(QString text, QWidget* parent, const char* name) : QLabel(text, parent, name)
{
}


Erklärungen der Änderungen

Unser Hauptprogramm sieht fast wie das aus Lektion 1 aus.
Es gibt hier nur zwei Unterschiede:



#include "qttutwidget.h"

Hier inkludieren wir den Header unseres neuen Widgets.



QtTutWidget helloworld( "Hello World", 0 );

Und hier erzeugen wir es. Der Typ von helloworld ist nun nicht mehr QPushButton sondern unser neues Widget.

Sehen wir uns die beiden neuen Dateien an:



#ifndef QTTUTWIDGET_H
#define QTTUTWIDGET_H

#endif

Hier sehen wir eine spezielle Konstruktion aus Preprozessor Direktiven.
Wir bräuchten sie hier nicht, aber im Normallfall gehört so etwas in ein Headerfile, um es vor Mehrfachinkludierung zu schützen.

Wie funktioniert das?
Die erste Zeile prüft, ob ein gewisses Symbol (QTTUTWIDGET_H) definiert ist.
ifndef (if not defined) wird war, wenn das Symbol nicht definiert ist.

Alle Zeilen bis zum endif werden darum vom Preprozessor ausgeblendet, wenn das QTTUTWIDGET_H definert ist.
Genau das passiert in der zweiten Zeile.

Wird der Header zum erstenmal inkludiert, ist das Symbol nicht bekannt, der Header wird also vollständig übernommen, aber gleichzeitig wird durch das define das Symbol bekannt gemacht.
Wird der Header nun ein weiteres Mal inkludiert, kennt der Preprozessor das Symbol und übergeht den Header.



#include <qlabel.h>

Der Header für QLabel (http://doc.trolltech.com/3.0/qlabel.html) wird inkludiert.
Das tun wir, weil das die Basisklasse unsere Widgets sein wird.



class QtTutWidget : public QLabel

Unser Widget mit dem Klassennamen QtTutWidget wird deklariert und wir geben an, dass es von QLabel abgeleitet ist.



QtTutWidget(QString text, QWidget* parent=0, const char* name=0);

Der Konstruktor unseres Widgets wird deklariert. So einen ähnlichen gibt es auch von QLabel.
Der erste Parameter ist der darzustellende Text, der zweite das Parentwidget (siehe Lektion 1) und der dritte ist der Name des Objekts (alle QObject Instanzen können einen Namen haben).

Den zweiten und dritten Parameter haben wir mit einem Standardwert (=0) ausgestattet, das erlaubt uns diese Parameter nicht unbedingt angeben zu müssen.



#include "qttutwidget.h"

In der cpp Datei inkludieren wir natürlich den zugehörigen Header.



QtTutWidget::QtTutWidget(QString text, QWidget* parent, const char* name) : QLabel(text, parent, name)
{
}

Die Implementation unseres Konstruktors.
Sie hat einen leeren Funktionskörper, weil wir nichts spezielles tun möchten.
Die Parameter werden ungeändert an den Konstruktor der Basisklasse weitergegeben.

Erstellung

Die Kommandos zur Programmerstellung werden nun in zwei Bereiche getrennt, die eigentliche Kompilierung und das Linken.
Das Kompilieren erzeugt aus dem Quellcode einen binären Maschinencode, das Linken verbindet solche Dateien dann zu einem Programm.


#> g++ -c -o lektion3.o lektion3.cpp -Wall -I/usr/lib/qt-3.0.3/include

Kompiliert lektion3.cpp in ein Objectfile. Da wir in diesem Schritt nicht linken, brauchen wir keine Linkeranweisungen.
Das -c sagt dem Compiler, dass wir nur kompilieren wollen.

#> g++ -c -o qttutwidget.o qttutwidget.cpp -Wall -I/usr/lib/qt-3.0.3/include

Kompiliert die Widgetklasse.

Jetzt noch linken:

#> g++ -o lektion3 lektion3.o qttutwidget.o -Wall -L/usr/lib/qt-3.0.3/lib -lqt-mt


Die Eingangsdateien des Linkerschritts sind nun die Resultate der Kompilierschritte.
Hier braucht man wieder die Linkeranwesiungen, kann aber diesmal die -I Angabe weglassen.

anda_skoa
25-11-2004, 19:09
Wenn wir, wie in Lektion 3, ein Projekt auf mehrere Dateien aufgeteilt haben, brauchen wir ein Linkerkommando und pro CPP Datei ein Compilerkommando.

Außerdem müssen wir uns darum kümmern, welche Dateien neu kompiliert werden müssen, wenn wir eine Datei ändern.
Die Änderung einer CPP Datei erfordert nur, dass diese neu kompiliert wird, eine Änderung eines Headers erfordert potentiell die Neukompilierung jeder CPP Datei, die diesen Header direkt oder indirekt (über einen anderen Header) inkludiert.

Abhilfe für diese Problematik schaffen uns Makefiles.

Ein Makefile ist eine Steuerdatei für das make Programm.
Makefiles bestehen aus einer Listen von Regeln, die die Reihenfolge der auszuführenden Befehle angibt.

Eine Regel ist wie folgt aufgebaut (<tab> entspricht einem Tabulator, d.h. im Editor die Tabulator-Taste drücken)



Name: Abhängigkeiten
<tab>Kommando
.
.
.
<tab>Kommando

Der Name oder Target einer Regel ist im Makefile eindeutig zu wählen.
Eine Regel wird über ihren Namen aktiviert, sowohl innerhalb des Makefiles als auch von außerhalb, also wenn man make aufruft.

#> make Name

Wird beim Aufruf von make kein Name angegeben, wird die erste Regel im Makefile aktiviert.

Bei der Aktivierung werden zuerst die Abhängigkeiten geprüft.
Ist eine Abhängigkeit der Name einer weiteren Regel, wird diese Regel aktiviert.
Ist die Abhängigkeit eine Datei, wird geprüft, ob das Datum der letzten Änderung jünger ist, als da entsprechende Datum der Zieldatei, die von der Regel erzeugt wird.
Ist das der Fall, werden die Kommandos der Reihe nach ausgeführt.

Eine Regel kann beliebig viele Kommandos haben. Das Ende ist durch eine Leerzeile gekennzeichnet.

Beispiel: Makefile für Lektion 3

Datei Makefile:


lektion3: lektion3.o qttutwidget.o
<tab>g++ -o lektion3 lektion3.o qttutwidget.o -Wall -L/usr/lib/qt-3.0.3/lib -lqt-mt

lektion3.o: lektion3.cpp qttutwidget.h
<tab>g++ -c -o lektion3.o lektion3.cpp -Wall -I/usr/lib/qt-3.0.3/include

qttutwidget.o: qttutwidget.cpp qttutwidget.h
<tab>g++ -c -o qttutwidget.o qttutwidget.cpp -Wall -I/usr/lib/qt-3.0.3/include

Wir können nun mit

#> make lektion3

oder einfach nur

#> make

das Programm aus Lektion 3 erstellen lassen und make kümmert sich darum, welche Kommandos erforderlich sind.

Wie geht make dabei vor?

Zuerst wird die Regel lektion3 aktiviert.
Ihre Abhängigkeiten sind die beiden Targets lektion3.o und qttutwidget.o
Die Namen wurden so gewählt, dass sie wie die zu erzeugenden Dateien heißen, damit man leichter den Überblick behält.

Daher werden also zunächst die beiden abhängigen Regeln aktiviert.
Hat sich im Vergleich zu lektion3.o einer der beiden Dateien lektion3.cpp oder qttutwidget.h geändert, wird lektion3.o neu kompiliert.

Ähnliches gilt für qttutwidget.o

Hat eine der beiden Regeln ihr Kommando ausgeführt, führt nun auch die oberste Regel ihr Kommando aus und linkt das Programm neu.

Variablen
Zur leichteren Übertragbarkeit können in Makefiles auch Variablen verwendet werden.
Die Schreibweise ist dabei ähnlich wie in Shellskripten.

Datei Makefile, diesmal mit Variablen


QTDIR=/usr/lib/qt-3.0.3
INCLUDES=-I$(QTDIR)/include
LIBS=-L$(QTDIR)/lib -lqt-mt

lektion3: lektion3.o qttutwidget.o
<tab>g++ -o lektion3 lektion3.o qttutwidget.o -Wall $(LIBS)

lektion3.o: lektion3.cpp qttutwidget.h
<tab>g++ -o lektion3.o -c lektion3.cpp -Wall $(INCLUDES)

qttutwidget.o: qttutwidget.cpp qttutwidget.h
<tab>g++ -o qttutwidget.o -c qttutwidget.cpp -Wall $(INCLUDES)


Makefiles generieren mit qmake

Bei vielen Dateien und komplexeren Abhängigkeiten wird das händische Erstellen von Makefiles zu einer schwierigen und zeitaufwendigen Aufgabe.

Was liegt also näher, als nach einem Hilfmittel zu suchen, dass Makefiles erstellen leichter macht.
So ein Tool wird von Qt mitgebracht: qmake (http://doc.trolltech.com/3.0/qmake-manual.html)

Ganz ohne Steuerdatei geht es damit auch nicht, aber die qmake Projektdateien sind sehr simpel aufgebaut:

lektion3.pro


TEMPLATE = app

CONFIG += qt thread warn_on

SOURCES = lektion3.cpp qttutwidget.cpp
HEADERS = qttutwidget.h

TARGET = lektion3


Sehen wir uns die Datei genauer an:


TEMPLATE = app

Wir möchten ein Makefile für eine Applikation. qmake kennt verschiedene Vorlagen für unterschiedliche Projekttypen:

app - für eine Applikation. Das ist auch die Standardvorlage und wird benutzt, wenn nichts anderes angegeben wird.
lib - für eine Bibliothek
subdirs - erzeugt ein Makefile für eine Projekt, in dem die eigentlichen Projektteile in Unterverzeichnisse des Projektverzeichnisses liegen.




CONFIG += qt thread warn_on

Wir wollen mit Qt linken, mit der Version mit Threadunterstützung und wir hätten gerne Compilerwarnungen.
Das warn_on wird für den g++ zu einem -Wall.

Es gibt für die CONFIG noch eine Reihe andere Möglichkeiten, die alle in der Dokumentation zu qmake angeführt sind.

Das += wirkt dabei wie der C++ Operator +=, d.h. die neuen Optionen werden an eventuell zuvor gesetzte angefügt.
Dieser Operator kann auch bei SOURCES, HEADERS und INTERFACES benutzt werden.



SOURCES = lektion3.cpp qttutwidget.cpp

Diese Zeile gibt an, welche CPP Dateien zu unserem Projekt gehören.



HEADERS = qttutwidget.h

Diese Zeile gibt an, welche Header Dateien zu unserem Projekt gehören.
Das wird vorallem dann wichtig, wenn man eigene Signal/Slot Deklarationen hat.



TARGET = lektion3

Das gibt an, wie unser Endprodukt, in unserem Fall das Programm, heißen soll.

Um mit Hilfe dieser Datei und qmake ein Makefile erzeugen zu können, müssen wir in der Konsole einige Vorbereitungen treffen.



#> export QMAKESPEC=linux-g++

Das setzt die Umgebungsvariable QMAKESPEC, die qmake sagt, für welche Plattform und welchen Compiler das Makefile erzeugt werden soll.
Das ist bei Qt wichtig, da das selbe Programm auf verschiedenen Plattformen mit verschiedenen Compilern kompilierbar sein soll und das selbe auch für die Projektdatei gelten sollte.

Als nächstes überprüfen wir, ob die Umgebungsvariable QTDIR gesetzt ist:

#> echo $QTDIR
/usr/lib/qt-3.0.3

Ok, in unserem Fall ist alles in Ordnung.

Kommt kein Pfad, muss QTDIR erst gesetzt werden

#> export QTDIR=/usr/lib/qt-3.0.3

Bei Systemen wo die Headerdateien und Bibliotheken nicht unterhalb eines gemeinsamen Verzeichnis liegen, muss man einen kleinen Trick anwenden :)

Dieser Trick besteht darin, ein Pseudo Qt Verzeichnis anzulegen, dass dann symbolische Links zu den einzelnen Verzeichnisen enthält.
(Unter Debian haben das der Qt Packager schon gemacht. Diese Verzeichnis ist /usr/share/qt3)

Das macht man entweder als root in einem systemweiten Verzeichnis oder als User in einem Verzeichnis wo man Schreibrechte hat.

Hier als Beispiel mit root Rechten

#> mkdir /usr/qtdir
#> cd /usr/qtdir
#> ln -s /usr/lib/qt lib
#> ln -s /usr/bin bin
#> ln -s /usr/include/qt3 include

Damit enthält unser qtdir Verzeichnis die erwarteten Unterverzeichnisse bin, include und lib.
Unsere Variable QTDIR setzen wir dann auf /usr/qtdir

Nach diesen Vorbereitungen können wir nun qmake starten

#> qmake -o Makefile lektion3.pro

Das sieht wie ein Compileraufruf aus und hat praktisch eine sehr ähnliche Funktion.
-o gibt an wie das Produkt heißen soll, danach kommt die Datei, die als Grundlage dient, also unsere Projektdatei.

Jetzt können wir unser Projekt wie zuvor mit den manuell erzeugten Makefiles erstellen lassen

#> make

qmake hat noch ein paar weitere Targets erzeugt, zum Beispiel clean.
Ein

#> make clean

löscht alle Dateien, die durch einen normalen make Aufruf erzeugt werden, also das ausführbare Programm und die Objectfiles.

Wird die Projektdatei geändert, erkennt das das alte Makefile über eine Regel, die die Projektdatei als Abhängigkeit hat, und erzeugt ein neues Makefile.

anda_skoa
25-11-2004, 20:31
In dieser Lektion werden wir sehen, wie man auf GUI Events, wie zB. Mausklicks, reagieren kann.

Als Basis nehmen wir die Dateien aus Lektion 4

Die Datei lektion5.pro ist absolut ident mit lektion3.pro, braucht also nur kopiert zu werden.
Für lektion5.cpp gilt das selbe, eventuell muss die Breite des Widgets vergrößert werden.

qttutwidget.h und qttutwidget.cpp wurden nun folgendermaßen erweitert:

qttutwidget.h


#ifndef QTTUTWIDGET_H
#define QTTUTWIDGET_H

#include <qlabel.h>

class QtTutWidget : public QLabel
{
public:
QtTutWidget(QString text, QWidget* parent=0, const char* name=0);

protected:
void mousePressEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event);
};

#endif


qttutwidget.cpp


#include "qttutwidget.h"

QtTutWidget::QtTutWidget(QString text, QWidget* parent, const char* name) : QLabel(text, parent, name)
{
setPaletteBackgroundColor(Qt::black);
setPaletteForegroundColor(Qt::white);
setMouseTracking(true);
}

void QtTutWidget::mousePressEvent(QMouseEvent* event)
{
QColor color = paletteBackgroundColor();
int red = color.red();
int green = color.green();
int blue = color.blue();

switch (event->button())
{
case Qt::LeftButton:
red = 255;
break;

case Qt::MidButton:
green = 255;
break;

case Qt::RightButton:
blue = 255;
break;

default:
break;
}

color.setRgb(red, green, blue);
setPaletteBackgroundColor(color);
}

void QtTutWidget::mouseReleaseEvent(QMouseEvent* event)
{
QColor color = paletteBackgroundColor();
int red = color.red();
int green = color.green();
int blue = color.blue();

switch (event->button())
{
case Qt::LeftButton:
red = 0;
break;

case Qt::MidButton:
green = 0;
break;

case Qt::RightButton:
blue = 0;
break;

default:
break;
}

color.setRgb(red, green, blue);
setPaletteBackgroundColor(color);
}

void QtTutWidget::mouseMoveEvent(QMouseEvent* event)
{
QString posString;
posString.sprintf(" W:%d,%d / G:%d,%d ",
event->pos().x(), event->pos().y(),
event->globalPos().x(), event->globalPos().y());
setText(posString);
}

Das ist ja einiges an neuem Code, also wieder schön langsam der Reihe nach im Detail.



protected:
void mousePressEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event);

In Qt werden die GUI Events an das jeweilige Widget zugestellt und dort in eine Eventhandler Methode verarbeitet.
Diese Methoden sind in der protected Sektion von QWidget deklariert.

Die Basisimplementation in der Klasse QWidget tut einfach nichts und auch QLabel, von dem wir ja ableiten, reagiert auf keine Mouse Events.
Wir müssen also die Methoden überschreiben, wenn dir etwas mit den Events machen wollen.
Hier überschreiben wir die Methoden für Drücken und Loslassen einer Maustaste und für Mausbewegungen.



setPaletteBackgroundColor(Qt::black);
setPaletteForegroundColor(Qt::white);

Wir setzen die Hintergrundfarbe unseres Widgets auf Schwarz und die Vordergrundfarbe auf Weiß.
Qt definiert im Namensraum Qt einige nützliche Konstanen, unter anderem auch Farbwerte.
Welche Farbwerte das sind, kann man in der Dokumentation von QColor (http://doc.trolltech.com/3.0/qcolor.html) nachlesen.



setMouseTracking(true);

Wir wollen über alle Mausbewegungen in unserem Widget informiert werden, auch wenn keine Taste gedrückt wird.
Darum aktivieren wir hier das Mouse-Tracking.



void QtTutWidget::mousePressEvent(QMouseEvent* event)
{
QColor color = paletteBackgroundColor();
int red = color.red();
int green = color.green();
int blue = color.blue();

Der Kopf einer unserer überschriebener Mouse Event Methode.
Zuerst holen wir uns die aktuelle Hintergrundfarbe.
Dann ermitteln wir die Werte der drei Grundfarben Rot, Grün und Blau (RGB Farbmodell).



switch (event->button())
{
case Qt::LeftButton:
red = 255;
break;

case Qt::MidButton:
green = 255;
break;

case Qt::RightButton:
blue = 255;
break;

default:
break;
}

Nun sehen wir über die Methode button() im QMouseEvent (http://doc.trolltech.com/3.0/qmouseevent.html) nach, welche Taste das Event ausgelöst hat.
Die im Namensraum Qt deklarierte Aufzählung ButtonState enthält vier Werte: LeftButton, MidButton, RightButton und NoButton.
Wir wollen auf Klicks reagieren also interessieren uns hier nur die drei "echten" Buttons.
Je nachdem welche Button zum Auslösen des Events geführt hat, ändern wir eine der drei Grundfarben auf ihr Maximum.



color.setRgb(red, green, blue);
setPaletteBackgroundColor(color);


Wir mischen nun die drei Gundfarben wieder zu einer Gesamtfarbe und setzen diese neue Farbe als unsere neue Hintergrundfarbe.

Die Methode mouseReleaseEvent macht praktisch genau das Gegenteil, wir gehen darum nicht näher daruf ein.



void QtTutWidget::mouseMoveEvent(QMouseEvent* event)
{
QString posString;
posString.sprintf(" W:%d,%d / G:%d,%d ",
event->pos().x(), event->pos().y(),
event->globalPos().x(), event->globalPos().y());
setText(posString);
}

In unserer Methode für Mausbewegungen wollen wir die Position des Mauszeiger in unserem Widget anzeigen lassen.
Bequemerweise kann unser Widget ja bereits einen Text anzeigen, weil es ja von QLabel abgeleitet ist.
Wir müssen also die Mauskoordinaten in einen QString umwandeln.

Dazu verwenden wir die Methode sprintf() der Klasse QString.
Diese Methode verhält sich praktisch wie die gleichnamige Funktion aus der C Bibliothek, nur dass sie in einen QString statt in ein char Array schreibt.

Unser Formatstring zeigt an, dass wir vier Ganzzahlen ausgeben möchten.
Diese Zahlen sind die Position des Mauszeigers im Widget (nach W: ) und die Position des Mauszeigers in globalen Koordinaten (nach G: ).
In QMouseEvent sind praktischerweise beide Positionen durch Methodenaufrufen verfügbar gemacht.
Der Rückgabewert beider Funktionen ist vom Typ QPoint (http://doc.trolltech.com/3.0/qpoint.html).

Der so erzeugte QString wird mit der Methode setText, die unser Widget von QLabel geerbt hat, gesetzt.
Um Weiteres brauchen wir uns dann nicht mehr zu kümmern, dass erledigt unser QLabel Erbe :)

Erzeugung

Wie in Lektion 4 gezeigt wird mit Hilfe der .pro Datei ein Makefile generiert, dass uns dann mit make ein Programm erzeugt.

anda_skoa
25-11-2004, 20:45
Diese Lektion zeigt, wie man mehrere Widgets in einem übergeordneten Widget zusammenfassen kann.
Außerdem wird gezeigt, wie man die Funktionalität der Widgets benutzt.

Erst mal wieder unsere Dateien:

lektion6.pro


TEMPLATE = app

CONFIG += qt thread warn_on

SOURCES = lektion6.cpp qttutwidget.cpp
HEADERS = qttutwidget.h

TARGET = lektion6

MOC_DIR = mocs
OBJECTS_DIR = obj

Die letzten beiden Zeilen sind neu. Sie weisen qmake an, zwei Verzeichnisse als Ablageort für bestimmte Nebenprodukte des Erstellungsprozesses zu benutzen.
Das MOC_DIR enthält dann die von MOC, dem Qt Meta Object Compiler, generierten Dateien.
Das OBJECTS_DIR enthält die vom Compiler erzeugten Object Dateien (.o Dateien).

Diese Maßnahme hält das Projektverzeichnis sauberer.

lektion6.cpp


#include <qapplication.h>

#include "qttutwidget.h"

int main( int argc, char* argv[] )
{
QApplication app( argc, argv );

QtTutWidget tutwidget( 0 );
tutwidget.resize( 200,30 );

app.setMainWidget( &tutwidget );

tutwidget.show();

return app.exec();
}

Bis auf minimale Änderungen identisch mit lektion5.cpp

qttutwidget.h


#ifndef QTTUTWIDGET_H
#define QTTUTWIDGET_H

#include <qwidget.h>

class QLineEdit;

class QtTutWidget : public QWidget
{
Q_OBJECT
public:
QtTutWidget(QWidget* parent=0, const char* name=0);

protected slots:
void slotButtonClicked();

protected:
QLineEdit* m_lineEdit;
};

#endif

Unser Widget ist nun direkt von QWidget abgeleitet. Es muss diesmal kein spezielles Aussehen haben, da wir es nur als Container für die anderen Widgets benutzen.



class QLineEdit;

Eines dieser Widget ist ein QLineEdit (http://doc.trolltech.com/3.0/qlinedit.html). Wir haben in unserem Hauptwidget einen Pointer auf eine Instant dieser Klasse, darum müssen wir den Typ hier bekannt machen (forward declaration).



Q_OBJECT

Unser Widget besitzt einen von uns neu deklarierten Slot und muss darum mit dem Q_OBJECT Makro markiert werden.
Diese Markierung führt dazu, dass qmake beim Erstellen des Makefiles eine Regel für den MOC und diese Datei erstellt.
Der MOC generiert dann Code, der für die Signal/Slot Benutzung wichtig ist.
Fehlt dieses Makro, dann ist unser Slot nur eine normale Methode!



protected slots:
void slotButtonClicked();

Hier deklarieren wir einen parameterlosen Slot.
Wie bei Methoden kann es public, protected und private Slots geben.
Der Name der Methode ist egal, wir haben hier jedoch "slot" als Namensteil gewählt, um in der CPP Datei leichter erkennen zu können, dass wir es mit einem Slot zu tun haben.



protected:
QLineEdit* m_lineEdit;

Deklarierung eines QLineEdit Pointers.
QWidget Instanzen werden normalerweise immer mit new am Heap erzeugt, liegen also als Pointer vor.
Ein QLineEdit ist ein einzeiliges Texteingabefeld.

qttutwidget.cpp


#include <qpushbutton.h>
#include <qlineedit.h>
#include <qlayout.h>
#include <qmessagebox.h>

#include "qttutwidget.h"

QtTutWidget::QtTutWidget(QWidget* parent, const char* name) : QWidget(parent, name)
{
QLayout* layout = new QHBoxLayout(this);

m_lineEdit = new QLineEdit(this);
QPushButton* button = new QPushButton("Hier klicken", this);

layout->add(m_lineEdit);
layout->add(button);

QObject::connect(button, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
}

void QtTutWidget::slotButtonClicked()
{
QString text = m_lineEdit->text();
if (text.stripWhiteSpace().isEmpty()) return;

QMessageBox::information(this, "Wichtig", text);
}


Die CPP Datei unseres Widgets ist nun viel kürzer als in der letzten Lektion, dafür gibt es mehr #include Direktiven:


#include <qpushbutton.h>
#include <qlineedit.h>
#include <qlayout.h>
#include <qmessagebox.h>


qpushbutton.h kennen wir ja bereits.
qlineedit.h ist die Headerdatei für unseren QLineEdit.
qlayout.h ist eine Headerdatei, die verschiedene Layoutklassen deklariert.
Qt verfügt über einen einfachen, aber sehr effektiven Layouting Mechanismus, der die Größe von Widgets zur Laufzeit verändern kann, um die Sichtbarkeit der Wigdetinhalte zu gewährleisten.
Wir verwenden in hier, um uns nicht weiter um die Positionen der Widgets in unserem Containerwidget kümmern zu müssen.
Das Layout wird sicher stellen, dass beide Kindwidgets sichtbar sind und sich nicht überlappen.

qmessagebox.h ist der Header der Klasse QMessageBox (http://doc.trolltech.com/3.0/qmessagebox.html).
Diese Klasse kann für kleine Meldungen und Rückfragen benutzt werden, die dann vom Benutzer bestätigt werden müssen.




QtTutWidget::QtTutWidget(QWidget* parent, const char* name) : QWidget(parent, name)

Da wir nun von QWidget ableiten, benutzen wir natürlich auch diesen Konstruktor.



QLayout* layout = new QHBoxLayout(this);

Hier ezeugen wir ein QHBoxLayout (http://doc.trolltech.com/3.0/qhboxlayout.html).
Ein HBoxLayout ordnet die Widget in diesem Layout horizontal an.
Als Parameter bekommt sein Konstruktor das Widget, für dessen Layout es zuständig ist, also in diesem Fall unser Containerwidget.



m_lineEdit = new QLineEdit(this);
QPushButton* button = new QPushButton("Hier klicken", this);

Wir erzeugen unsere beiden Widgets und geben als Parentwidget unser Containerwidget an.
Das hat zur Folge, dass sie gelöscht werden, wenn unser Widget gelöscht wird. Darum brauchen wir keine delete Aufrufe im Destruktor unseres Widgets.
Den Pointer auf den Button brauchen wir hier nur im Konstruktor, darum haben wir im Gegensatz zum LineEdit eine lokale Variable gewählt.



layout->add(m_lineEdit);
layout->add(button);

Wir geben die beiden Widgets in unser Layout. Zuerst den LineEdit, denn er soll links vom Button sein.
Wäre das Layout ein VBoxLayout, dann wäre der LineEdit über dem Button.



QObject::connect(button, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));

Wir verbinden das clicked() Signal des Buttons mit unserem Slot.



void QtTutWidget::slotButtonClicked()

Der Slot sieht in der CPP Datei wie eine normale Methode aus und kann auch wie eine normale Methode aufgerufen werden.
In unserem Fall wird er Slot über den Signal/Slot Mechanismus von Qt aufgerufen, wann immer der Button geklickt wird.



QString text = m_lineEdit->text();

Wir holen uns den Text, der gerade im LineEdit steht uns speichern ihn in einer lokalen Variable.



if (text.stripWhiteSpace().isEmpty()) return;

Hier prüfen wir, ob der Text leer ist.
Eigentlich würde isEmpty() reichen, aber wir wollen auch keine Eingaben zulassen, die nur aus Leerzeichen bestehen, darum entfernen wir alle Leerzeichen vom Anfang und vom Ende des Textes, bevor wird die Prüfung machen.
Falls der Text leer ist, kehren wir umgehend aus der Funktion zurück.



QMessageBox::information(this, "Wichtig", text);

Für die Standardfälle brauchen wir selbst keine QMessageBox anzulegen, denn Trolltech hat dafür ein paar statische Methoden vorbereitet, die wir nur noch mit den nötigen Parametern aufrufen müssen.
Hier verwenden wir information, was in einer MessageBox mit einem Rufzeichen-Icon und einem OK Button resultiert.

Als Fenstertitel setzen wir "Wichtig" und als Text unseren LineEdit Inhalt.

Erzeugung

Wie auch in den letzten Lektionen mit qmake und make.

Experimente

Experimentieren mit HBoxLayout und VBoxLayout.
Beobachten des Layoutings beim Verändern der Fenstergröße mit der Maus.
Ausprobieren verschiedener static Methoden von QMessageBox.

anda_skoa
25-11-2004, 20:51
In der vorigen Lektion habe wir unser Widget "händisch", also durch C++ Code, mit Unterwidgets ausgestattet.
Das ist natürlich bei größeren Widgets oder Dialogen mühsam und durch das fehlende visuelle Feedback nicht immer wie gewünscht hinzukriegen.

Trolltech bietet dafür den Designer an, ein Programm, mit dem man mit der Maus Widgets auf anderen Widgets plazieren und layouten kann.

Nach dem Start erscheint zuerst ein Dialog (template.png), der uns erlaubt, unser Widget basierend auf einer Vorlage zu erstellen.
Wir wählen einfach QWidget.

Nun ändern wir im Property Editor den Namen des Widgets auf QtTutWidgetBase (siehe properties.png).
Der Name wird dann zum Namen der generierten Klasse.
Ich wähle für Widgets, von denen ich dann ableite um sie mit Funktionn zu füllen, immer den eigentlichen Namen + Base.

Als Nächstes platzieren wird einen QLineEdit und einen QPushButton auf dem Widget (siehe elements.png).
Die verfügbaren Element sind links in einem Unterfenster des Designers, in verschiedenen Kategorien unterteilt, aufgelistet.

Das Auswählen erfolgt durch einfaches Klicken, das Platzieren durch Drücken und Ziehen der Maus.
Dann ändern wird im Property Editor die Namen der beiden Elemente. Diese Namen werden dann später die Variablennamen der beiden Elemente sein.
Wir haben hier m_button und m_lineEdit gewählt.

Anschliessend ändern wir den Text des Buttons. Das kann durch Doppelklick oder über den Property Editor erfolgen.

Die Widgets in Lektion 6 wurden durch ein QHBoxLayout verwaltet.
Das erreichen wird so:

Zunächst wird unser Hauptwidget angeklickt. Zur Kontrolle sieht man am besten im Property Editor nach, ob dort die Eigenschaften des Widgets erscheinen.
Dann Klicken wir im Toolbar des Designer auf das Symbol für das horizontale Layout, drei vertikale Striche (siehen layout.png).

Dadurch wird unser Hauptwidget diesem Layout unterworfen. Man hat sofort ein visuelles Feedback, wie sich das auf die platzierten Komponenten auswirkt (siehe layouted.png)

Wir verkleinern nun unser Hauptwidget unter Kontrolle des Layouts, bis es eine angenehme Größe hat.

Der Designer erlaubt es, das erstellte Widget unter Anwendung verfügbarer WidgetStyles in einer Vorschau darzustellen.
Die erste Option im Menü Preview nimmt den aktuellen Style (siehe preview.png).
Die Vorschau ist aus funktionaler Natur: ein Button reagiert auf Klicks, der LineEdit zeigt einen blinkenden Cursor und erlaubt Texteingaben.

Jetzt speichern wir unsere Arbeit, am besten unter dem Namen des Widgets.
Das ist auch der Dateiname, den der Designer vorschlägt (siehe saving.png).

Integration

Jetzt passen wir unsere übrigen Dateien an.

Zuerst die Projektdatei lektion7.pro


TEMPLATE = app

CONFIG += qt thread warn_on

SOURCES = lektion7.cpp qttutwidget.cpp
HEADERS = qttutwidget.h
FORMS = qttutwidgetbase.ui

TARGET = lektion7

MOC_DIR = mocs
OBJECTS_DIR = obj
UI_DIR = uics

Wir sehen zwei neue Zeilen.


FORMS = qttutwidgetbase.ui

Wie HEADERS und SOURCES gibt FORMS eine Liste von Dateien an, in Falle von FORMS eben Dateien des Designer.


UI_DIR = uics

Diese Zeile gibt an, in welches Unerverzeichnis die Dateien kommen, die UIC, der User Inferface Compiler, aus den .ui Datein generiert.

Diese Dateien sind normale Header und Quelldateien, sollten aber nicht geändert werden, weil bei einem erneuten UIC Lauf alle Änderungen wieder überschrieben werden.
Um sie von unserern "echten" Dateien zu trennen, verbannen wir sie mit der UI_DIR Zeile eben ins Interverzeichnis uics.

lektion7.cpp ist identisch mit lektion6.cpp

qttutwidget.h


#ifndef QTTUTWIDGET_H
#define QTTUTWIDGET_H

// local includes
#include "qttutwidgetbase.h"

class QLineEdit;

class QtTutWidget : public QtTutWidgetBase
{
Q_OBJECT
public:
QtTutWidget(QWidget* parent=0, const char* name=0);

protected slots:
void slotButtonClicked();
};

#endif

Als include brauchen wir nun den generierten Header. Der Name wird aus dem Namen der .ui Datei generiert und heißt darum bei uns qttutwidgetbase.h

Die Basisklasse unserer QtTutWidgets ist nun auch statt QWidget QtTutWidgetBase. Das hier ist der Name, den wir dem Widget im Property Editor gegeben haben.

Weiters ist die Sektion mit der QLineEdit Variable hinfällig geworden.

qttutwidget.cpp


#include <qpushbutton.h>
#include <qlineedit.h>
#include <qmessagebox.h>

#include "qttutwidget.h"

QtTutWidget::QtTutWidget(QWidget* parent, const char* name) : QtTutWidgetBase(parent, name)
{
QObject::connect(m_button, SIGNAL(clicked()), this, SLOT(slotButtonClicked()));
}

void QtTutWidget::slotButtonClicked()
{
QString text = m_lineEdit->text();
if (text.stripWhiteSpace().isEmpty()) return;

QMessageBox::information(this, "Wichtig", text);
}

Im Konstruktor wurde auch hier QWidget durch QtTutWidgetBase ersetzt. Der Funktionskörper ist nun viel kürzer, weil ja wir ja weder Layout noch Element anlegen müssen.
Die Variable des Buttons heißt nun m_button (meine Schreibweise für Membervariablen), darum ändert sich der connect() Aufruf ebenfalls.

Die Slot Implementation ist völlig identisch, da wir als Namen für die LineEdit Variable wieder m_lineEdit gewählt haben.