Traduzione a cura di Fabrizio Angius [qtsolutions -A-T- gmx.net]
English TOC.
Qt si basa sul Qt object model. E' questa architettura a rendere Qt potente e facile da usare. Il tutto si basa sulla classe QObject e sul tool moc.
Facendo derivare tutte le classi dalla classe QObject, si ottengono una serie di vantaggi. Eccoli elencati qui:
Ciascuna di queste proprieta' viene discussa di seguito. Prima di continuare, e' importante ricordare che Qt e' C++ standard con in piu' alcune macro, un po' come una qualsiasi applicazione C/C++. Non c'e' niente di strano o di non standard in tutto cio', come dimostra l'elevata portabilita' del codice.
Quando si crea un'istanza di una classe derivata da QObject, e' possibile passare al costruttore un puntatore a un oggetto padre. Questa e' la base della gestione semplificata della memoria. Quando un padre viene cancellato [ossia si esegue una delete su di esso - n.d.T.], vengono cancellati anche tutti i suoi figli. Cio' significa che una classe derivata da QObject puo' creare istanze di figli di QObject passando this come padre senza preoccuparsi della loro distruzione.
Per capire meglio, vediamo subito un esempio. L'esempio 2-1 mostra la classe che abbiamo preso, appunto, come esempio. Eredita QObject e segnala su console tutto cio' che succede. In questo modo sara' ovvio e facile capire quello che succede e quando.
// Un VerboseObject ci dice continuamente quello che sta facendo
class VerboseObject : public QObject
{
public:
VerboseObject( QObject *parent=0, char *name=0 ) : QObject( parent, name )
{
std::cout << "Created: " << QObject::name() << std::endl;
}
~VerboseObject()
{
std::cout << "Deleted: " << name() << std::endl;
}
void doStuff()
{
std::cout << "Do stuff: " << name() << std::endl;
}
};
Per fare con la classe qualcosa di utile e' necessario un metodo main. Viene mostrato nell'esempio 2-2. Notate che come prima cosa si crea un'istanza di QApplication. Questo e' necessario in pressoche' tutte le applicazioni Qt ed e' buona pratica per evitare problemi inutili. Il codice crea una gerarchia di memoria mostrata nella figura 2-1.
int main( int argc, char **argv )
{
// Crea un'applicazione
QApplication a( argc, argv );
// Crea istanze
VerboseObject top( 0, "top" );
VerboseObject *x = new VerboseObject( &top, "x" );
VerboseObject *y = new VerboseObject( &top, "y" );
VerboseObject *z = new VerboseObject( x, "z" );
// Fai qualcosa per fermare l'ottimizzatore
top.doStuff();
x->doStuff();
y->doStuff();
z->doStuff();
return 0;
}
Assicuratevi di aver capito bene come l'albero venga tradotto nel codice dell'esempio 2-2 e viceversa.
L'esempio 2-3 mostra un esempio di esecuzione del codice precedente. Come si puo' vedere, tutti gli oggetti vengono cancellati, prima i genitori e poi i figli. Come potete osservare, ogni ramo viene cancellato completamente prima di passare a quello successivo. Lo si puo' vedere dalla cancellazione di z, che avviene prima di quella di y.
$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
Deleted: x
Deleted: z
Deleted: y
$
Se il riferimento al genitore viene rimosso da x e y, come mostrato nell'esempio 2-4, si verifica un memory leak [letteralmente 'buco di memoria', ma suppongo non siate cosi' nuovi alla programmazione - n.d.T. ]. Una possibile esecuzione e' mostrata nell'esempio 2-5.
...
VerboseObject *x = new VerboseObject( 0, "x" );
VerboseObject *y = new VerboseObject( 0, "y" );
...
$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
$
In questo caso, il memory leak non crea nessun problema, dato che il programma termina appena esso si verifica, ma in altri casi cio' potrebbe rappresentare una minaccia alla stabilita' dell'intero sistema.
I segnali e gli slot rendono i componenti di Qt cosi' particolarmente riutilizzabili. Infatti forniscono un meccanismo mediante il quale possiamo esporre delle interfacce che possono essere liberamente connesse tra di loro. Per esempio, una voce di menu, un pulsante, un pulsante della barra degli strumenti e qualsiasi altro oggetto possono emettere segnali corrispondenti ad "attivato", "cliccato" o qualsiasi altro evento sia piu' appropriato. Connettendo un segnale a uno slot di un qualsiasi altro oggetto, si avra' che l'evento eseguira' automaticamente la funzione relativa allo slot.
Un segnale puo' anche contenere valori, rendendo possibile connettere uno slider, una spinbox, una manopola o un qualsiasi oggetto che generi dei valori, a un qualsiasi oggetto che li possa ricevere, per esempio un'altro slider, un'altra manopola o spinbox, o qualcosa di completamente diverso come un display LCD.
Il vantaggio principale di segnali e slot e' che il chiamante non deve sapere niente del ricevente e viceversa. In questo modo e' possibile integrare facilmente molti componenti senza che chi ha progettato i componenti abbia effettivamente pensato alla configurazione poi usata. Questa e' veramente programmazione modulare.
Funzione o slot?
Per rendere le cose un po' complicate, l'ambiente di sviluppo
fornito con Qt 3.1.x e successivi spesso chiama gli slot funzioni.
Dato che nella maggior parte dei casi si comportano allo stesso
non e' un problema, ma nel resto del tutorial useremo il termine "slot".
Per poter usare segnali e slot, ogni classe va dichiarata in un file header. L'implementazione e' meglio metterla in un file cpp a parte. L'header viene quindi elaborato da uno tool di Qt chiamato moc. Il moc produce un file cpp contenente (tra l'altro) il codice che permette l'uso effettivo di segnali e slot. La figura 2-2 mostra questo processo. Osservate la convenzione usata per nominare i file nella figura (il prefisso moc_).
Potrebbe sembrare che questa ulteriore fase di compilazione complichi ulteriormente il processo di sviluppo, ma esiste un'altro tool di Qt, qmake. Esso riduce la costruzione di un'applicazione Qt ad un semplice qmake -project && qmake && make. Descriveremo il tutto in dettaglio piu' avanti in questo tutorial.
Cosa sono, in realta', segnali e slot?
Come accennato prima, un'applicazione Qt
e' C++ al 100%, ma quindi cosa sono segnali e slot in realta'?
Una parte riguarda le keyword, che vengono semplicemente sostituite
da codice C++ regolare dal preprocessore.
Gli slot sono quindi implementati come una qualsiasi funzione membro di
una classe, mentre i segnali sono implementati dal moc.
Ogni oggetto mantiene una lista delle proprie connessioni (quali slot vengono
attivati da quali segnali) e dei propri slot, che sono poi usati per costruire
la tabella delle connessioni nel metodo connect.
La dichiarazione di queste tabelle e' nascosta dalla macro
Q_OBJECT.
Ovviamente il discorso e' piu' complicato, ma si puo' vedere il tutto
dando un'occhiata a un file cpp generato dal
moc.
Il prossimo esempio e' una semplice dimostrazione dell'elevata modularita' fornita da segnali e slot. Per prima cosa, l'esempio 2-6 mostra la classe ricevente. Notate che nulla vieta ad una classe di ricevere e di inviare contemporaneamente; in altre parole una classe puo' avere sia segnali che slot.
class Reciever : public QObject
{
Q_OBJECT
public:
Reciever( QObject *parent=0, char *name=0 ) : QObject( parent, name )
{
}
public slots:
void get( int x )
{
std::cout << "Recieved: " << x << std::endl;
}
};
Questa classe inizia con la macro Q_OBJECT, necessaria se si vuole utilizzare qualche funzionalita' di Qt che vada oltre alla gestione semplificata della memoria. Questa macro contiene alcune definizioni chiave e indica al moc che deve processare la classe. Notate anche che una nuova sezione chiamata public slots si e' aggiunta alla sintassi standard.
Gli esempi 2-7 e 2-8 contengono le implementazioni delle classi mittenti. La prima classe, SenderA, implementa il segnale send emesso dalla funzione membro doEmit. La seconda classe, SenderB, emette il segnale transmit dalla funzione membro doStuff. Notate che queste classi non hanno niente in comune, se non per il fatto che derivano entrambe da QObject.
class SenderA : public QObject
{
Q_OBJECT
public:
SenderA( QObject *parent=0, char *name=0 ) : QObject( parent, name )
{
}
void doSend()
{
emit( send( 7 ) );
}
signals:
void send( int );
};
class SenderB : public QObject
{
Q_OBJECT
public:
SenderB( QObject *parent=0, char *name=0 ) : QObject( parent, name )
{
}
void doStuff()
{
emit( transmit( 5 ) );
}
signals:
void transmit( int );
};
Per mostrare come il codice del moc viene fuso con il codice scritto da programmatori umani, nell'esempio non si usa come al solito il metodo qmake ma si include esplicitamente il codice. Per invocare il moc si utilizza il seguente commando: $QTDIR/bin/moc sisl.cpp -o moc_sisl.h. La prima riga dell'esempio 2-9 include il file risultante, moc_sisl.h. Il codice dell'esempio contiene anche il codice per creare le varie istanze e per connettere i vari segnali e slot. Questo e' l'unico pezzo di codice conscio della presenza di una classe mittente e di una classe ricevente. Le classi di per se' conoscono solo le dichiarazioni, ossia le interfacce, dei segnali da emettere o da ricevere.
#include "moc_sisl.h"
int main( int argc, char **argv )
{
QApplication a( argc, argv );
Reciever r;
SenderA sa;
SenderB sb;
QObject::connect( &sa, SIGNAL(send(int)), &r, SLOT(get(int)) );
QObject::connect( &sb, SIGNAL(transmit(int)), &r, SLOT(get(int)) );
sa.doSend();
sb.doStuff();
return 0;
}
Per concludere, l'esempio 2-10 mostra il risultato di un'esecuzione di prova. I segnali vengono emessi e il ricevente mostra i risultati.
$ ./sisl
Recieved: 7
Recieved: 5
$
Valori nella connessione?
Un errore comune e' quello di credere che sia possibile specificare
i valori da inviare con i segnali quando si definisce la connessione,
p.es. connect( &a, SIGNAL(signal(5)), &b, SLOT(slot(int)) );.
Cio' non e' possibile. Quando si definisce una connessione e' possibilie
specificare solo le dichiarazioni dei segnali. Quando si emette un segnale si puo' invece
indicare un parametro, ma solo allora.
La versione corretta del codice precedente sarebbe
connect( &a, SIGNAL(signal(int)), &b, SLOT(slot(int)) );
il valore (5) andra' specificato dove si emette il segnale.
Gli oggetti Qt possono avere delle proprieta'. Si tratta semplicemente di valori con associato un tipo e almeno una funzione per la lettura dello stesso; e' anche possibile avere funzioni per impostare il valore. Queste proprieta' vengono ad esempio utilizzate dal Designer per mostrare le proprieta' dei widget. La documentazione ufficiale delle proprieta' puo' essere consultata qui.
Le proprieta' non sono solo un'ottimo modo per organizzare il codice e per specificare quali proprieta' vengano gestite da una funzione. Possono anche essere utilizzate come una forma primitiva di reflection. Qualsiasi puntatore QObject puo' avere accesso alle proprieta' dell'oggetto a cui punta. Anche se si tratta di una classe derivata e piu' complessa.
Il codice dell'esempio 2-11 mostra come dichiarare una classe con delle proprieta'. Il codice mostrato appartiene al file propobject.h. Il trucco viene realizzato dalle macro Q_PROPERTY e Q_ENUMS.
#ifndef PROPOBJECT_H
#define PROPOBJECT_H
#include <qobject.h>
#include <qvariant.h>
class PropObject : public QObject
{
Q_OBJECT
Q_PROPERTY( TestProperty testProperty READ testProperty WRITE setTestProperty )
Q_ENUMS( TestProperty )
Q_PROPERTY( QString anotherProperty READ anotherProperty )
public:
PropObject( QObject *parent=0, char *name=0 );
enum TestProperty { InitialValue, AnotherValue };
void setTestProperty( TestProperty p );
TestProperty testProperty() const;
QString anotherProperty() const { return QString( "I'm read-only!" ); }
private:
TestProperty m_testProperty;
};
#endif
Notate che non ci sono virgole tra le proprieta' della macro Q_PROPERTY. Per come e' definita la sintassi, si indica prima il tipo della proprieta', quindi il nome. Segue quindi una parola chiave, che puo' essere READ oppure WRITE, e la corrispondente funzione membro.
Funzioni per la lettura e per la scrittura
Non c'e' niente di speciale riguardo alle funzioni per la lettura
o la scrittura di proprieta'.
L'unico vincolo e' che la funzione di lettura deve ritornare un tipo
uguale alla proprieta' e non avere parametri (ossia deve essere di tipo
void) mentre la funzione di scrittura deve avere un unico
parametro dello stesso tipo della proprieta'.
E' pratica comune dichiarare le funzioni di scrittura come slot.
Cio' aumenta la facilita' di riutilizzo dei moduli, dal momento
che la maggior parte delle funzioni di scrittura sono naturalmente
degli slot. Alcune di queste sono poi anche candidate per emettere segnali.
L'esempio 2-12 mostra il codice di propobject.cpp. Si tratta dell'implementazione della classe che rappresenta un oggetto con delle proprieta'.
#include "propobject.h"
PropObject::PropObject( QObject *parent, char *name ) : QObject( parent, name )
{
m_testProperty = InitialValue;
}
void PropObject::setTestProperty( TestProperty p ) { m_testProperty = p; }
PropObject::TestProperty PropObject::testProperty() const { return m_testProperty; }
Notate che il costruttore accetta i parametri parent, di tipo QObject, e name. Questi sono poi passati alla superclasse. Le funzioni membro sono solo implementazioni d'esempio di una proprieta' a lettura/scrittura e di una proprieta' a sola lettura.
In conclusione, l'esempio 2-13 mostra il codice di main.cpp. Questo codice accede alle proprieta' attraverso l'interfaccia standard QObject e non direttamente. Una possibile esecuzione, mostrata nell'esempio 2-14, mostra che il codice funziona effettivamente. Notate che enum viene mostrato cosi' come il computer lo gestisce internamente, ossia come un intero.
#include <qapplication.h>
#include <iostream>
#include "propobject.h"
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QObject *o = new PropObject();
std::cout << o->property( "testProperty" ).toString() << std::endl;
o->setProperty( "testProperty", "AnotherValue" );
std::cout << o->property( "testProperty" ).toString() << std::endl;
std::cout << o->property( "anotherProperty" ).toString() << std::endl;
return 0;
}
$ ./prop
0
1
I'm read-only!
$
Ogni oggetto Qt ha associato un meta-oggetto. Questo oggetto e' rappresentato da un'istanza della classe QMetaObject e viene usato per fornire informazioni sulla classe corrente. Si puo' accedere al meta-oggetto mediante la funzione QObject::metaObject(). Il meta-oggetto ci fornisce alcune funzioni utili, che sono di seguito elencate.
Restituisce il nome della classe, p.es. PropObject nell'esempio della sezione precedente.
Restituisce il meta-oggetto della superclasse oppure 0 (null) se quest'ultima non esiste.
Restituisce i nomi delle proprieta' e le informazioni ausiliarie per ogni proprieta', sotto forma di QMetaProperty.
Restituisce i nomi degli slot della classe. Se il parametro opzionale super vale true vengono ritornati anche gli slot della superclasse.
Restituisce i nomi dei segnali della classe. Il parametro opzionale super e' analogo a quello della funzione signalNames.
Le funzioni elencate sopra sono solo dei suggerimenti. Le informazioni ausiliarie disponibili sono molte di piu'. Date un'occhiata alla documentazione ufficiale per maggiori dettagli.
Il codice sorgente per questo capitolo puo' essere scaricato qui.
This is a part of digitalfanatics.org and is valid XHTML.
Copyright (c) 2002-2004 by Johan Thelin (e8johan -at- digitalfanatics.org). This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 (local copy) or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.