Traduzione a cura di Fabrizio Angius [qtsolutions -A-T- gmx.net]
English TOC.

2. Il modello ad oggetti di Qt

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.

Gestione semplificata della memoria

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;
  }
};

Esempio 2-1

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;
}

Esempio 2-2

Assicuratevi di aver capito bene come l'albero venga tradotto nel codice dell'esempio 2-2 e viceversa.

La gerarchia di memoria

Figura 2-1 La gerarchia di memoria

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
$

Esempio 2-3

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" );
  ...

Esempio 2-4

$ ./mem
Created: top
Created: x
Created: y
Created: z
Do stuff: top
Do stuff: x
Do stuff: y
Do stuff: z
Deleted: top
$

Esempio 2-5

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.

Segnali e slot

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_).

The moc flow

Figura 2-2 Il flusso del 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;
  }
};

Esempio 2-6

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 );
};

Esempio 2-7

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 );
};

Esempio 2-8

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;
}        

Esempio 2-9

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
$

Esempio 2-10

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.

Proprieta'

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

Esempio 2-11

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; }

Esempio 2-12

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;
}

Esempio 2-13

$ ./prop
0
1
I'm read-only!
$

Esempio 2-14

Conoscenza di se' stessi

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.

Riassunto

Il codice sorgente per questo capitolo puo' essere scaricato qui.

Letture consigliate

This is a part of digitalfanatics.org and is valid XHTML.