Tutorial Qt – Capitulo 08 – XML

O que é XML? Existem muitas descrições do que XML é, melhores e mais profundas do que iremos falar aqui. O W3C tem uma boa página de introdução se você quiser saber mais. A documentação oficial do Qt XML também é uma boa referência. Esse tutorial será um guia pragmático do XML. Existe bem mais para conhecer, e bem mais que precisará conhecer. O propósito aqui é mostrar como começar a trabalhar com Qt e XML.

O Qt oferece suporte ao XML como um módulo, por isso alguns usuários pagos podem não ter acesso a ele, mas a edição livre e a enterprise possuem suporte. Saiba disso antes de tentar usar os exemplos.

Agora você poderia perguntar: Por quê usar XML? Existem muitas razões mas algumas são que é fácil de ler (para humanos e computadores), fácil de usar a partir do código e torna fácil trocar dados com outras aplicações. Provavelmente existem outras razões, mas esses são algumas.
O Qt oferece duas maneiras de interagir com conteúdo XML: DOM e SAX. O SAX é o mais simples dos dois, lê e traduz a medida que requisitamos a informação. O DOM lê o arquivo XML inteiro e guarda em uma árvore na memória. Essa árvore pode então ser lida e manipulada antes de ser passada adiante ou ser escrita de volta no disco.

Com o SAX, é mais difícil modicar um dado fornecido. Por outro lado, requer menos espaço na memória embora ainda seja tão utilizável quanto o DOM em muitas situações.

O DOM requer mais memória. O documento inteiro é mantido na memória. Isso, porém, nos dá a possibilidade de modificar e trabalhar o documento livremente na memória e depois gravar de volta no disco. Isso é muito útil em muitas situações.

Antes de apresentar algum código, existem algumas convenções que são importantes. Um documento XML consiste de um número de tags, onde essas tags podem conter dados e ter atributos. O exemplo abaixo mostra isso. As tags não podem ser aninhadas, isto é, a tag que está sendo fechada precisa ser a que estava aberta por último.

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

Agora vamos passar para a tarefa desse capitulo. A aplicação de livro de endereços será atualizada com um formato de arquivo baseado em XML. Antes que possamos fazer algo precisamos ser capazes de ler e escrever XML. Vamos ver como isso é feito.

Escrevendo com DOM

Nessa seção, o objetivo é pegar um registro de Contato e escreve-lo como XML. A API do DOM será usada. Aqui está o nosso plano de ação:

  1. Crie um documento (um QDomDocument)
  2. Crie um elemento raiz
  3. Para cada contato, coloque no documento
  4. Escreva o resultado em um arquivo

A primeira parte é fácil. O código de uma linha é mostrado abaixo. O “AdBookML” é o nome de nossa convenção.

QDomDocument doc( "AdBookML" );

Por quê um elemento raiz é necessário? Para termos um ponto de partida. Nosso elementos raiz será chamado adbook. Esse elemento é criado com o código abaixo:

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

Para a terceira parte um função será usadas: ContactToNode, que é mostrada abaixo:

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

Essa parte é um pouco mais difícil de entender que as duas primeiras. Em primeiro lugar, um elementos chamado contact é criado. Depois são criados três atributos: name, phone e email. Para cada atributo um valor é adicionado. O método setAtribute substitue um atributo existente com o mesmo nome ou cria um novo. Cada chamada dessa função terminará com um elemento que pode ser adicionado a raiz do documento.

A quarta parte é abrir um arquivo, criar um stream de texto e então chamar o método toString() do documento DOM. O exemplo abaixo mostra a função main() completa (incluindo as duas primeiras partes e a chamada a função da terceira 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;
}

Finalmente, o exemplo abaixo mostra o arquivo resultante.


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

Lendo com DOM

Nessa seção, o plano é ler o documento que nós criamos em uma aplicação e acessa-lo como um documento DOM. Essa tarefa será divida nas seguintes partes:

  1. Criar um documento DOM a partir de um arquivo
  2. Encontrar a raiz e garantir que é um livro de endereço.
  3. Encontrar todos os contatos
  4. Encontrar todos os atributos interessantes de cada elemento

A primeira parte é mostrada no código abaixo. Um documento vazio é instanciado e o conteúdo do arquivo é associado a ele (se o arquivo foi aberto corretamente). Depois disso, o arquivo pode ser descartado já que o documento inteiro foi carregado na memória.


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

O próximo passo é encontrar o elemento raiz, ou o elemento do documento, como o Qt se refere a ele. Depois ele é checado para garantir que é um elemento “adbook” e nenhum outro. O código para isso é mostrado abaixo.


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

As terceira e quarta parte são combinadas em um loop. Cada elemento é checado, se for um contato, os atributos são analisados, se não forem são ignorados. Observe que o método attribute permite um valor padrão, portante, se o atributo estiver faltando nós obteremos uma string vazia. O código é mostrado a seguir.


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

Observe que usando XML nos é permitido manipular facilmente a compatibilidade de versões antigas do arquivo, já que tipos de elementos e atributos adicionais são ignorados, tornando fácil armazenar dados adicionais em versões futuras.

Lendo com SAX

Essa seção se parecerá muito com a anterior já que se trata de leitura de dados. A diferença é que dessa vez, um leitor SAX será usado. Isso significa que a origem, isso é, o arquivo, precisa estar aberto durante toda a operação pois não haverá buffering.

O SAX no Qt é mais fácil de implementar usando o QXmlSimpleReader e uma sub-classe personalizada de QXmlDefaultHandler. O manipulador padrão tem métodos que podem ser chamados pelo leitor quando um documento inicia, ou um elemento é aberto ou fechado. Nosso manipulador é mostrado no exemplo abaixo e tem dois propósitos: coletar informações do cliente e saber quando está dentro da tag adbook ou não.


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

O método startDocument é chamado na primeira vez que o documento é aberto. Ele é usado para inicializar o estado da classe para não estar em uma tag adbook. Para cada tag que é aberta. o método startElement é chamado. Ele garante que se uma tag adbook é encontrada, o estado seja alterado, e se um contato for encontrado enquanto estiver dentro da tag adbook os atributos sejam lidos. Finalmente, o método endElement é chamado toda vez que uma tag de fechamento for encontrada. Se uma tag de fechamento do adbook for encontrada, o estado é atualizado.

Usar o AdBookParser é fácil. O exemplo abaixo mostra o código. Primeiro o arquivo é associado ao parser como origem dos dados, depois o manipulador é associado ao leitor que será usado para traduzir os dados da origem. Isso pode parecer um pouco simples, e talvez seja. No mundo das aplicações reais é recomendado usar um QXmlErrorHandler.


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

O uso do parser SAX permite também implementar a compatibilidade reversa de forma fácil, já que tags e atributos não identificados serão ignorados. Também permite a tradução parcial de arquivos XML ilegais. Por exemplo, apenas a abertura de cada tag de contato é necessária.

Atualizando o Livro do endereços com XML

A atualização começará como as alterações da classe Contact. Haverá um construtor para criar um Contato a partir de um QDomElement e um para criar um contato vazio. Haverá um método para criar um QDomElement de um contato dado. O cabeçalho e a implementação são mostradas nos dois exemplos a seguir. A implementação é colocada em um novo arquivo chamado contact.cpp.


class Contact
{
public:
QString name,
eMail,
phone;
Contact( QString iName = "", QString iPhone = "", QString iEMail = "" );
Contact( const QDomElement &e );
QDomElement createXMLNode( QDomDocument &d );
};
#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;
}
Observe que o código de createXMLNode é mais ou menos parecido com o código de leitura com DOM da seção correspondente desse capitulo.
Para que possamos manipular o carregamento e salvamento dos arquivo dois novo métodos serão adicionados a classe frmMain. Eles serão usados para facilitar os argumentos de linha de comando e não terão nada a fazer com a interface com o usuário e como o usuário escolhe salvar ou carregar os arquivos. A implementação também é mais ou menos copiada das seções anteriores referentes a leitura e escrita com DOM. A diferença é que elas manipulam coleções de contatos e a listview e também fornecem um feedback melhor quando algo inesperado ocorre. O código é mostrado a seguir.


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

A interface com o usuário precisa ser capaz de pedir ao usuário pelo nome do arquivo, tanto para carregar e salvar. Essa parte é convenientemente manipulada pela classe QFileDialog através dos membros estáticos getOpenFileName e getSaveFileName.

Agora vamos partir para a interface com o usuário. Primeiro crie três novas ações: aFileLoad, aFileSave e aFileSaveAs. Elas serão colocadas no menu File. Veja a tabela abaixo para os detalhes.

Widget Property New Value
aFileLoad text Load…
aFileSave text Save
aFileSaveAs text Save As…

Cada uma terá um slot para ela. Eles serão chamados loadFile, saveFile e saveFileAs, que serão conectados as ações. Veja a figura abaixo para um visão de como as conexões deverão ficar no Designer.

The connections
The connections

Para a opção salvar funcionar, o nome do arquivo do último carregamento precisa ser armazenado. Para isso, uma nova variável privada, QString m_filename, é adicionada ao formulário como mostrado na figura abaixo:

The object members or frmMain
The object members or frmMain

O exemplo abaixo mostra a implementação dos slots. Observe quão fácil é manipular as caixas de diálogos, e também como fazer que as opções Save e SaveAs cooperam de forma adequada.


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

Sumário

O código do exemplo desse capitulo pode ser baixado aqui ex09.tar
Traduzido de http://www.digitalfanatics.org/projects/qt_tutorial/chapter09.html