Traduction par Jean-luc Biord, du Site de la communauté Qt francophone.
English TOC.

9. XML

Qu'est-ce que XML ? Il y a des descriptions beaucoup plus longues, mieux et plus détaillées concernant XML que celle-ci. W3C a une bonne page pour débuter si vous voulez en savoir plus. La documentation XML officielle de Qt  est également une bonne valeur de lecture. Ce sera un guide pragmatique de XML. Il y a bien davantage à savoir, et bien plus qu'on doit savoir. Le but est de montrer comment commencer avec Qt et XML.

Qt propose le support de XML comme module, ainsi quelques utilisateurs payants peuvent ne pas l'avoir, mais l'édition libre et l'édition entreprise l'ont. Sachez le avant d'essayer d'utiliser les exemples.

Maintenant vous pouvez demander : pourquoi employer XML ? Il y a beaucoup de raisons, mais certaines sont qu'il est facile à lire (pour des humains et des ordinateurs), facile à employer dans du code et qu'il rend facile l'échange d'information avec d'autres applications. Il y a probablement d'autres raisons bien meilleures, mais celles-ci suffisent.

Qt offre deux manières d'interagir avec le contenu XML: DOM et SAX. SAX est le plus simple des deux, à lire et analyser. DOM lit le fichier XML entièrement dans un arbre en mémoire. Cet arbre peut alors être lu et manipulé avant d'être mis de côté ou écrit de nouveau sur disque.

SAX rend difficile la modification des données fournies. En revanche, il exige un espace mémoire très faible tout en étant aussi utile que DOM dans beaucoup de situations.

DOM exige plus de mémoire. Le document entier doit être stocké en mémoire. En revanche, cela donne la capacité de modifier et travailler avec le document librement en mémoire et de le mettre alors de nouveau sur disque. C'est très maniable dans beaucoup de situations.

Voici avant le code réel de ce chapitre quelques conventions qui sont importantes. Un document XML se compose d'un certain nombre de balises, ces balises peuvent contenir des données et avoir des attributs. L'exemple 9-1 le montre. Des balises ne peuvent pas être imbriquées, c.-à-d. que la balise fermante doit correspondre à la dernière balise ouverte.

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

Exemple 9-1

Pour le travail d'aujourd'hui, l'application de carnet d'adresses sera améliorée avec un fichier au format XML. Avant que cela puisse être fait, nous devons pouvoir lire et écrire du XML. Regardons la façon dont cela doit être fait.

Écrire en utilisant DOM

Dans cette section le but est de prendre les informations d'un Contact et de les écrire ailleurs en XML. L'API DOM sera employée. Voici le plan:

  1. Créer un document (un QDomDocument)..
  2. Créer un élément racine.
  3. Pour chaque contact, l'insérer dans le document.
  4. Écrire le résultat dans un fichier.

La première partie est facile. Le code est montré dans l'exemple 9-2. "AdBookML" est par convention le nom choisi pour notre document.

QDomDocument doc( "AdBookML" );

Exemple 9-2

Pourquoi est-ce qu'un élément racine est nécessaire ? Parce qu’il doit y avoir un endroit à partir duquel commencer. Notre élément racine s'appellera adbook. Il est créé dans le code de l'exemple 9-3.

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

Exemple 9-3

Pour la troisième partie une fonction sera employée: ContactToNode. Elle est montrée dans l'exemple 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;
}

Exemple 9-4

C'est un peu plus lourd que les deux premières parties. D'abord, un élément appelé contact est créé. Puis trois attributs : name, phone et email sont ajoutés. Pour chaque attribut la valeur est renseignée. setAttribute remplace un attribut existant du même nom ou en ajoute un nouveau. Chaque appel à cette fonction finira par l'ajout d'un élément à la racine du document.

La quatrième partie est faite en ouvrant un fichier, en créant un flux de texte et puis en appelant la méthode toString() du document DOM. L'exemple 9-5 montre entièrement la fonction main (incluant les deux premières parties et les deux appels de la troisième partie).

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

Exemple 9-5

Finalement, l'exemple 9-6 montre le fichier obtenu.

<!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>

Exemple 9-6

Lire en utilisant DOM

Dans cette section le plan est de lire le document que nous venons de créer dans une application et d'y accéder comme un document DOM. Cette tâche est décomposée avec les parties suivantes:

  1. Créer le document DOM à partir d'un fichier.
  2. Trouver la racine et s'assurer que c'est un carnet d'adresses.
  3. Trouver tous les contacts.
  4. Trouver tous les attributs intéressants de chaque élément de contact.

La première partie est montrée dans le code de l'exemple 9-7. Un document vide est créé et le contenu du fichier lui est assigné (si le fichier est ouvert correctement). Après ceci, le fichier peut-être abandonné puisque le document entier a été lu en mémoire.

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

Exemple 9-7

L'étape suivante est de trouver l'élément racine, ou l'élément de document, car Qt se rapporte à lui. Alors on le vérifie pour s'assurer qui c'est un élément "adbook" et rien d'autre. Le code qui fait cela est montré est l'exemple 9-8.

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

Exemple 9-8

La troisième et quatrième étape sont combinées dans une boucle. Chaque élément est vérifié, si c'est un contact, ses attributs sont analysés, autrement il est ignoré. Notez que la méthode attribute fournit une valeur par défaut, par conséquent si l'attribut est absent nous obtenons une chaîne vide. Le code est montré dans l'exemple 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();
}

Exemple 9-9

Notez que d'employer XML nous permet de manipuler facilement des fichiers au format compatible. Comme les éléments types et attributs supplémentaires sont ignorés, il serait facile de stocker des données additionnelles dans de futures versions.

Lire en utilisant SAX

Cette section ressemblera beaucoup à la précédente puisqu'elle traite également de la lecture. La différence est que cette fois, un lecteur SAX sera employé. Ceci signifie que la source, c.-à-d. le fichier, doit être ouverte pendant l'opération entière, car il n'y aura aucun tampon en mémoire.

Il est plus facile de mettre en application SAX dans Qt en utilisant QXmlSimpleReader et une sous-classe de QXmlDefaultHandler. QXmlDefaultHandler a des méthodes qui sont appelées par le lecteur quand un document débute, ou un élément s'ouvre ou se ferme. Notre QXmlDefaultHandler est montré dans l'exemple 9-10 et a deux buts : rassembler l'information du client et savoir quand elle est à l'intérieur d'une balise adbook tag ou pas.

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

Exemple 9-10

La méthode startDocument est appelée d'abord quand le document commence. Ceci est employé pour initialiser l'état de la classe pour ne pas être dans une balise adbook. Pour chaque balise qui s'ouvre, la méthode startElement est appelée. Elle s'assure qu'une balise adbook est trouvée, l'état est modifié, et si une balise contact est trouvée alors qu'on est à l'intérieur d'une balise adbook les attributs sont lus. Finalement, la méthode endElement est appelée chaque fois qu'une balise fermante est rencontrée. Si c'est une balise adbook qui se ferme, l'état est mis à jour.

Employer AdBookParser est facile. L'exemple 9-11 montre le code. D'abord, le fichier est assigné à l'analyseur comme source, ensuite le handler est assigné au lecteur qui est utilisé pour analyser le source. Ceci peut paraître un peu trop simple, et peut-être que ça l'est. Dans des applications du monde réel, on recommande d'employer un QXmlErrorHandler. Ceci, et plus de détails sont bien décrits dans la documentation officielle.

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

Exemple 9-11

L'usage du parser SAX permet également d'implémenter une compatibilité ascendante puisque les balises et attributs inconnus sont ignorés. Il permet également d'interpréter des fichiers XML en partie illégaux. Par exemple, seulement la balise ouvrante contact est requise.

Évolution du carnet d'adresses avec XML

La mise à niveau commencera par des changements de la classe Contact. Il y aura un constructeur pour créer un Contact depuis un QDomElement et un pour créer un Contact vide. Il y aura également une méthode pour créer un QDomElement depuis le Contact courant. L'en-tête est montré dans l'exemple 9-12 et l'implémentation dans l'exemple 9-13. L'implémentation est mise dans le nouveau fichier appelé contact.cpp.

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

  QDomElement createXMLNode( QDomDocument &d );
};

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

Exemple 9-13

Notez que le code de createXMLNode est pris plus ou moins directement de la première section de lecture DOM de ce chapitre.

Afin de gérer le chargement et la sauvegarde des fichiers, deux nouvelles méthodes sont ajoutées à la classe frmMain. Elles sont conçues pour faciliter les arguments en ligne de commande et n'ont rien à voir avec l'interface utilisateur et comment les utilisations choisissent de sauver ou charger les fichiers. L'implémentation est également plus ou moins copiée des premières sections concernant la lecture et l'écriture avec DOM. La différence est qu'ils manipulent la collection des contacts et la list view et assure également une meilleure rétroaction utilisateur quand quelque chose d'inattendu se produit. Le code pour ces derniers peut être trouvé dans l'exemple 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();
}

Exemple 9-14

L'interface utilisateur doit pouvoir demander à l'utilisateur des noms de fichier, pour le chargement et la sauvegarde. C'est facilement fait par les membres statiques getOpenFileName et getSaveFileName de la classe QFileDialog .

Maintenant pour l'interface utilisateur. Créez d'abord trois nouvelles actions: aFileLoad, aFileSave et aFileSaveAs. Ils sont mis dans le menu file. Voir le tableau 9-1 pour des détails.

Widget Propriété Nouvelle Valeur
aFileLoad text Load...
aFileSave text Save
aFileSaveAs text Save As...

Tableau 9-1

Ceux-ci auront un slot chacun. Ils s'appellent: loadFile, saveFile et saveFileAs. Ils sont connectés aux actions. Voir le schéma 9-1 pour une vue de la façon dont les connexions sont faites dans Designer.

The connections

Le schéma 9-1 les connexions.

Pour le fonctionnement de l'option de sauvegarde, le nom de fichier du dernier chargement ou sauvegarde comme doit être stocké. Pour ceci, une nouvelle variable privée, QString m_filename, est ajoutée à la forme comme représenté sur le schéma 9-2.

The object members or frmMain

Le schéma 9-2 les objets membres de frmMain.

L'exemple 9-14 montre l'exécution des slots. Notez comme il est facile de manipuler les dialogues fichiers, et aussi comment sauver et sauver sous coopèrent bien.

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

Exemple 9-15

Pour l'implémentation complète, téléchargez la source d'exemple de ce chapitre.

Résumé

Pour une manière alternative de sauver et charger des fichiers, regardez le deuxième tutoriel de Trolltech.

Exercices

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