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

9. XML

Cosa e' l'XML? Ci sono numerose descrizioni piu' dettagliate di questa riguardo all'XML. Il W3C ha una pagina interessante da cui iniziare se volete saperne di piu'. Vale anche la pena leggere la documentazione per il modulo XML di Qt. Questa sara' una guida pragmatica all'XML. Ci sono molte piu' cose da sapere e molte piu' cose che si dovrebbero sapere. Lo scopo e' di mostrare come iniziare a lavorare con Qt e XML.

Qt offre il supporto XML come modulo, per cui i proprietari di alcune delle versioni commerciali potrebbero non averlo. E' bene che lo sappiate, prima di iniziare a utilizzare gli esempi.

Ora potreste chiedere: perche' utilizzare l'XML? Ci sono diverse ragioni, ma fondamentalmente, e' facile da leggere (per esseri umani e per computer), facile da utilizzare programmando e rende piu' facile lo scambio di informazioni con altre applicazioni. Probabilmente ci sono altre e piu' importanti ragioni, ma queste sono alcune.

Qt offre due modi per gestire l'XML: DOM e SAX. SAX e' il piu' semplice: riconoscimento dei tag man mano che si legge il file. DOM legge invece tutto il file in memoria e'albero cosi' creato puo' essere letto e manipolato prima di essere buttato via o riscritto su disco.

Con SAX diventa difficile manipolare i dati. D'altro canto, richiede pochissima memoria rimanendo valido come DOM in molte situazioni.

DOM richiede piu' memoria. L'intero documento viene mantenuto in memoria. In questo modo e' pero' possibile modificare e lavorare con il documento liberamente in memoria per poi riscriverlo su disco. Si tratta di una cosa molto comoda in determinate situazioni.

Prima di passare al codice di questo capitolo, ci sono alcune convenzioni importanti da discutere. Un documento XML consiste in una serie di tag, che possono contenere dei dati o avere degli attributi. Osservate l'esempio 9-1 in proposito. I tag non possono essere chiusi a caso, ma devono essere annidati correttamente.

<tag attribute="value" />
<tagWithData attribute="value" anotherAttribute="value">
data
</tagWithData>

Esempio 9-1

Passiamo al compito del giorno. L'applicazione della rubrica verra' aggiornata per poter utilizzare file in formato XML. Per farlo dobbiamo essere in grado di leggere e di scrivere XML, per cui vediamo come si fa.

Scrivere utilizzando DOM

L'obiettivo di questa sezione e' di scrivere un contatto in formato XML. Utilizzeremo la API DOM. Ecco cosa dovremmo fare:

  1. Creare un documento (un QDomDocument).
  2. Creare un elemento radice.
  3. Inserire ogni contatto nel documento.
  4. Scrivere il documento su file.

La prima parte e' facile. Il codice viene mostrato nell'esempio 9-2. "AdBookML" e' il nome che abbiamo dato al nostro linguaggio di markup.

QDomDocument doc( "AdBookML" );

Esempio 9-2

Perche' e' necessario un elemento radice? Serve per avere un punto di partenza. Chiameremo il nostro elemento radice adbook. Il tutto e' nel codice dell'esempio 9-3.

QDomElement root = doc.createElement( "adbook" );
doc.appendChild( root );

Esempio 9-3

Per la terza parte, utilizzeremo un metodo: ContactToNode. E' mostrato nell'esempio 9-4.

QDomElement ContactToNode( QDomDocument &d, const Contact &c )
{
   QDomElement cn = d.createElement( "contact" );

   cn.setAttribute( "name", c.name );
   cn.setAttribute( "phone", c.phone );
   cn.setAttribute( "email", c.eMail );

   return cn;
}

Esempio 9-4

E' un po' piu' difficile delle prime parti. Prima creiamo un elemento chiamato contact. Quindi aggiungiamo tre attributi: name, phone e email. Impostiamo anche il valore per ogni attributo. Il metodo setAttribute sostituisce un attributo esistente (con lo stesso nome) o ne crea uno nuovo. Ogni chiamata a questa funzione ci dara' un elemento che potra' essere aggiunto alla radice del documento.

Per la quarta parte e' necessario aprire un file, creare un flusso di testo e chiamare il metodo toString() del documento DOM. L'esempio 9-5 mostra l'intera funzione main (comprese le prime due parti e le due chiamate relative alla terza parte).

int main( int argc, char **argv )
{
  QApplication a( argc, argv );

  QDomDocument doc( "AdBookML" );
  QDomElement root = doc.createElement( "adbook" );
  doc.appendChild( root );

  Contact c;

  c.name = "Kal";
  c.eMail = "kal@goteborg.se";
  c.phone = "+46(0)31 123 4567";

  root.appendChild( ContactToNode( doc, c ) );

  c.name = "Ada";
  c.eMail = "ada@goteborg.se";
  c.phone = "+46(0)31 765 1234";

  root.appendChild( ContactToNode( doc, c ) );

  QFile file( "test.xml" );
  if( !file.open( IO_WriteOnly ) )
  return -1;

  QTextStream ts( &file );
  ts << doc.toString();

  file.close();

  return 0;
}

Esempio 9-5

L'esempio 9-6 mostra infine il file prodotto.

<!DOCTYPE AdBookML>
<adbook>
  <contact email="kal@goteborg.se" phone="+46(0)31 123 4567" name="Kal" />
  <contact email="ada@goteborg.se" phone="+46(0)31 765 1234" name="Ada" />
</adbook>

Esempio 9-6

Leggere utilizzando DOM

L'obiettivo di questa sezione e' di leggere il documento appena creato e di accedervi mediante il DOM. Questo compito e' composto dalle seguenti parti:

  1. Creare un documento DOM partendo da un file.
  2. Trovare la radice e verificare che si tratti di un file della rubrica.
  3. Trovare tutti i contatti.
  4. Trovare tutti gli attributi interessanti per ogni contatto.

Il codice relativo alla prima parte e' mostrato nell'esempio 9-7. Viene creata un'istanza di un documento vuoto e vi viene assegnato il contenuto del file (se il file e' stato aperto correttamente). Una volta che il file e' stato letto puo' essere scartato dato che il documento si trova tutto in memoria.

QDomDocument doc( "AdBookML" );
QFile file( "test.xml" );
if( !file.open( IO_ReadOnly ) )
  return -1;
if( !doc.setContent( &file ) )
{
  file.close();
  return -2;
}
file.close();

Esempio 9-7

Il prossimo passo consiste nel trovare l'elemento radice, ossia l'elemento radice come inteso da Qt. Quindi si verifica che si tratti di un "adbook" e non di qualcos'altro. Il codice e' mostrato nell'esempio 9-8.

QDomElement root = doc.documentElement();
if( root.tagName() != "adbook" )
  return -3;

Esempio 9-8

La terza e la quarta parte vengono combinate in un unico ciclo. Ciascun elemento viene controllato e se si tratta di un contatto se ne analizzano gli attributi. Notate che il metodo attribute permette di impostare un valore di default, altrimenti se un dato attributo non e' presente ci viene restituita una stringa vuota. Il codice e' mostrato nell'esempio 9-9.

QDomNode n = root.firstChild();
while( !n.isNull() )
{
  QDomElement e = n.toElement();
  if( !e.isNull() )
  {
    if( e.tagName() == "contact" )
    {
      Contact c;

      c.name = e.attribute( "name", "" );
      c.phone = e.attribute( "phone", "" );
      c.eMail = e.attribute( "email", "" );

      QMessageBox::information( 0, "Contact", c.name + "\n" + c.phone + "\n" + c.eMail );
    }
  }

  n = n.nextSibling();
}

Esempio 9-9

Notate che l'utilizzo di XML ci permette di gestire facilmente la compatibilita' con nuovi formati dei file, dato che attributi o elementi nuovi verrebbero semplicemente ignorati pur rendendo possibile aggiungere altri dati da utilizzare con le versioni piu' recenti di un programma.

Leggere utilizzando SAX

Questa sezione sembrera' molto simile alla precedente dal momento che tratta sempre la lettura di un file XML. La differenza e' che questa volta si utilizzera' un lettore SAX. In altre parole, la sorgente - p.es. un file - dovra' rimanere aperta durante tutta l'operazione di lettura dato che non vi e' alcun buffering.

Per implementare un parser SAX in Qt la cosa piu' semplice da fare e' usare un QXmlSimpleReader e una sottoclasse personalizzata di QXmlDefaultHandler. Il gestore (handler) di default mette a disposizione i metodi invocati dal lettore quando inizia un documento o quando si apre o chiude un tag. Il nostro gestore e' mostrato nell'esempio 9-10 e ha due scopi: raccogliere informazioni e sapere se si trova all'interno di un tag adbook o meno.

class AdBookParser : public QXmlDefaultHandler
{
public:
  bool startDocument()
  {
    inAdBook = false;
    return true;
  }
  bool endElement( const QString&, const QString&, const QString &name )
  {
    if( name == "adbook" )
      inAdBook = false;

    return true;
  }

  bool startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs )
  {
    if( inAdBook && name == "contact" )
    {
      QString name, phone, email;

      for( int i=0; i<attrs.count(); i++ )
      {
        if( attrs.localName( i ) == "name" )
          name = attrs.value( i );
        else if( attrs.localName( i ) == "phone" )
          phone = attrs.value( i );
        else if( attrs.localName( i ) == "email" )
          email = attrs.value( i );
      }

      QMessageBox::information( 0, "Contact", name + "\n" + phone + "\n" + email );
    }
    else if( name == "adbook" )
      inAdBook = true;

    return true;
  }

private:
  bool inAdBook;
};

Esempio 9-10

Il metodo startDocument verra' invocato per primo all'apertura del documento. Il metodo verra' utilizzato per inizializzare lo stato della classe affinche' non inizi all'interno di un tag adbook. Il metodo startElement verra' invocato ad ogni tag di apertura incontrato. Se si tratta di un tag adbook viene cambiato lo stato della classe; se si tratta di un tag contact mentre ci troviamo all'interno di un tag adbook si passa alla lettura degli attributi. Il metodo endElement verra' infine invocato ad ogni tag di chiusura. Se si tratta di un tag adbook aggiorniamo di nuovo lo stato della classe.

L'utilizzo di AdBookParser e' banale. L'esempio 9-11 mostra il codice. Per prima cosa il file viene assegnato come sorgente al parser, quindi si assegna il parser al lettore. Il tutto potrebbe sembrare eccessivamente semplice, e forse lo e'. In applicazioni reali e' consigliabile utilizzare un QXmlErrorHandler. Questo e altri dettagli sono descritti nella documentazione ufficiale.

int main( int argc, char **argv )
{
  QApplication a( argc, argv );

  AdBookParser handler;

  QFile file( "test.xml" );
  QXmlInputSource source( file );

  QXmlSimpleReader reader;
  reader.setContentHandler( &handler );

  reader.parse( source );

  return 0;
}

Esempio 9-11

L'utilizzo di un parser SAX permette anche di mantenere un'elevata compatibilita' con versioni precedenti ignorando semplicemente tag e attributi sconosciuti. Permette anche il parsing di file XML non perfettamente legali. Per esempio, e' possibile ignorare il tag di chiusura per i tag contact.

Aggiornare la rubrica per l'utilizzo di XML

L'aggiornamento iniziera' con dei cambiamenti alla classe Contact. Ci sara' un costruttore per creare un Contact partendo da un QDomElement e uno per creare un Contact vuoto. Ci sara' anche un metodo per creare un QDomElement partendo dal Contact corrente. L'header e' mostrato nell'esempio 9-12 mentre l'implementazione e' nell'esempio 9-13. L'implementazione verra' inserita in un nuovo file chiamato contact.cpp.

class Contact
{
public:
  QString name,
    eMail,
    phone;
  Contact( QString iName = "", QString iPhone = "", QString iEMail = "" );
  Contact( const QDomElement &e );

  QDomElement createXMLNode( QDomDocument &d );
};

Esempio 9-12

#include "contact.h"

Contact::Contact( QString iName, QString iPhone, QString iEMail )
{
  name = iName;
  phone = iPhone;
  eMail = iEMail;
}

Contact::Contact( const QDomElement &e )
{
  name = e.attribute( "name", "" );
  phone = e.attribute( "phone", "" );
  eMail = e.attribute( "email", "" );
}

QDomElement Contact::createXMLNode( QDomDocument &d )
{
  QDomElement cn = d.createElement( "contact" );

   cn.setAttribute( "name", name );
   cn.setAttribute( "phone", phone );
   cn.setAttribute( "email", eMail );

  return cn;
}

Esempio 9-13

Notate che il codice per createXMLNode e' praticamente lo stesso di quello della sezione precedente sulla lettura con DOM.

Per poter gestire il caricamento e il salvataggio dei file dobbiamo aggiungere altri due metodi alla classe frmMain. Sono stati pensati in modo da facilitare l'uso di parametri da riga di commando e non hanno niente a che fare con l'interfaccia utente o con il modo con cui l'utente sceglie di caricare o salvare un file. L'implementazione e' di nuovo molto simile al codice delle sezioni precedenti sulla lettura e scrittura mediante DOM. La differenza e' che questi manipolano la collezione di contatti e la listview oltre a fornire una risposta migliore all'utente in caso di eccezioni. Il codice per tutto cio' e' nell'esempio 9-14.

void frmMain::load( const QString &filename )
{
  QFile file( filename );

  if( !file.open( IO_ReadOnly ) )
  {
    QMessageBox::warning( this, "Loading", "Failed to load file." );
    return;
  }

  QDomDocument doc( "AdBookML" );
  if( !doc.setContent( &file ) )
  {
    QMessageBox::warning( this, "Loading", "Failed to load file." );
    file.close();
    return;
  }

  file.close();

  QDomElement root = doc.documentElement();
  if( root.tagName() != "adbook" )
  {
    QMessageBox::warning( this, "Loading", "Invalid file." );
    return;
  }

  m_contacts.clear();
  lvContacts->clear();

  QDomNode n = root.firstChild();
  while( !n.isNull() )
  {
    QDomElement e = n.toElement();
    if( !e.isNull() )
    {
      if( e.tagName() == "contact" )
      {
        Contact c( e );

        m_contacts.append( c );
        lvContacts->insertItem( new QListViewItem( lvContacts, c.name , c.eMail, c.phone ) );
      }
    }

    n = n.nextSibling();
  }
}

void frmMain::save( const QString &filename )
{
  QDomDocument doc( "AdBookML" );
  QDomElement root = doc.createElement( "adbook" );
  doc.appendChild( root );

  for( QValueList<Contact>::iterator it = m_contacts.begin(); it != m_contacts.end(); ++it )
    root.appendChild( (*it).createXMLNode( doc ) );

  QFile file( filename );
  if( !file.open( IO_WriteOnly ) )
  {
    QMessageBox::warning( this, "Saving", "Failed to save file." );
    return;
  }

  QTextStream ts( &file );

  ts << doc.toString();

  file.close();
}

Esempio 9-14

L'interfaccia utente dovra' poter chiedere all'utente i file da cui caricare o in cui salvare. Questo viene gestito comodamente dai metodi statici getOpenFileName e getSaveFileName della classe QFileDialog.

Passiamo all'interfaccia grafica. Per prima cosa create tre nuove azioni: aFileLoad, aFileSave e aFileSaveAs. Verranno inserite nel menu File. Guardate la tabella 9-1 per i dettagli.

Widget Proprieta' Nuovo valore
aFileLoad text Load...
aFileSave text Save
aFileSaveAs text Save As...

Tabella 9-1

Ci serviranno anche alcuni slot: loadFile, saveFile e saveFileAs. Andranno connessi alle azioni. Guardate la figura 9-1 per vedere come impostare le azioni dal Designer.

Le connessioni

Figura 9-1 Le connessioni.

Affinche' la funzione di save funzioni, e' necessario salvare il nome dell'ultimo file aperto. Per fare cio' aggiungiamo una variabile privata al form, QString m_filename, come mostrato nella figura 9-2.

I membri di frmMain

Figura 9-2 I membri di frmMain.

L'esempio 9-14 mostra l'implementazione degli slot. Osservate quanto sia facile gestire i dialog per i file e come far cooperare correttamente "save" e "save as".

void frmMain::loadFile()
{
  QString filename = QFileDialog::getOpenFileName( QString::null, "Addressbooks (*.adb)", this, "file open", "Addressbook File Open" );

  if ( !filename.isEmpty() )
  {
    m_filename = filename;
    load( filename );
  }
}

void frmMain::saveFile()
{
  if( m_filename.isEmpty() )
  {
    saveFileAs();
    return;
  }

  save( m_filename );
}

void frmMain::saveFileAs()
{
  QString filename = QFileDialog::getSaveFileName( QString::null, "Addressbooks (*.adb)", this, "file save as", "Addressbook Save As" );
  if ( !filename.isEmpty() )
  {
    m_filename = filename;
    save( m_filename );
  }
}

Esempio 9-15

Per l'implementazione completa scaricate il codice sorgente per questo capitolo.

Riassunto

Per un modo alternativo di salvare e caricare file date un'occhiata al secondo tutorial della Trolltech.

Esercizi

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