Wiki

Generating XML

by Jasmin Blanchette

Qt provides the SAX and DOM classes for parsing XML, but does not include a dedicated class to generate XML. In this article we will develop a straightforward XmlWriter class based on QTextStream, and demonstrate how to use it in practice, with an example that generates a Qt Designer .ui file.

XmlWriter Class Definition

The XmlWriter class provides a basic API for generating XML files. First, let's see the definition of the class:

class XmlWriter
{
public:
    XmlWriter( QIODevice *device,
               const QTextCodec *codec = 0 );
     XmlWriter();
 
    void writeRaw( const QString& xml );
    void writeString( const QString& string );
    void writeOpenTag( const QString& name,
                       const AttrMap& attrs = AttrMap() );
    void writeCloseTag( const QString& name );
    void writeAtomTag( const QString& name,
                       const AttrMap& attrs = AttrMap() );
    void writeTaggedString( const QString& name,
                            const QString& string,
                            const AttrMap& attrs = AttrMap() );
    void newLine();
    void setIndentSize( int size ) { indentSize = size; }
    void setAutoNewLine( bool on ) { autoNewLine = on; }
 
private:
    ...
 
    QTextStream out;
    QString indentStr;
    int indentSize;
    bool autoNewLine;
    bool atBeginningOfLine;
};

The AttrMap parameter to writeOpenTag(), writeAtomTag(), and writeTaggedString() stores "name = value" pairs. The class inherits its functionality from QMap:

class AttrMap : public QMap<QString, QString>
{
public:
    AttrMap() { }
    AttrMap( const QString& name, const QString& value ) {
        insert( name, value );
    }
};

For example, the XML line

<animal age="25">Elephant</animal>

can be generated using this code:

XmlWriter xw( device );
xw.writeTaggedString( "animal", "Elephant",
                      AttrMap("age", "25") );

Generating an XML File

Before we examine XmlWriter's implementation, we will see how to use it to generate an XML file. For the purpose of our example, we will use a Qt Designer .ui file.

<!DOCTYPE UI><UI version="3.1">
<class>Form1</class>
<widget class="QDialog">
    <property name="name">
        <cstring>Form1</cstring>
    </property>
    <property name="caption">
        <string>Form1</string>
    </property>
    <vbox>
        <widget class="QLabel">
            <property name="name">
                <cstring>label</cstring>
            </property>
            <property name="text">
                <string>Rock &amp;&amp; Roll</string>
            </property>
        </widget>
    </vbox>
</widget>
<layoutdefaults margin="11" spacing="6"/>
</UI>

The program that generates the above .ui file follows. It may seem like overkill since it is longer than the output it produces. But in more realistic scenarios where the output is dependent on the input and on the program's state, the XmlWriter class will save significant time and effort, and will eliminate many annoying bugs because it handles XML escaping automatically.

void writeProperty( XmlWriter& xw, const QString& name,
                    const QString& type,
                    const QString& value )
{
    xw.writeOpenTag( "property", AttrMap("name", name) );
    xw.writeTaggedString( type, value );
    xw.writeCloseTag( "property" );
}

int main()
{
    QFile file;
    file.open( IO_WriteOnly, stdout );
    XmlWriter xw( &file );
    xw.setAutoNewLine( true );
    xw.writeRaw( "<!DOCTYPE UI><UI version=\"3.1\">" );
    xw.newLine();
    xw.writeTaggedString( "class", "Form1" );
    xw.writeOpenTag( "widget",
                     AttrMap("class", "QDialog") );
    writeProperty( xw, "name", "cstring", "Form1" );
    writeProperty( xw, "caption", "string", "Form1" );
    xw.writeOpenTag( "vbox" );
    xw.writeOpenTag( "widget",
                     AttrMap("class", "QLabel") );
    writeProperty( xw, "name", "cstring", "label" );
    writeProperty( xw, "text", "string", "Rock && Roll" );
    xw.writeCloseTag( "widget" );

  xw.writeCloseTag( "vbox" );
    xw.writeCloseTag( "widget" );
    AttrMap attrs;
    attrs.insert( "spacing", "6" );
    attrs.insert( "margin", "11" );
    xw.writeAtomTag( "layoutdefaults", attrs );
    xw.writeRaw( "</UI>" );
    return 0;
}

By using the XmlWriter class, we obtain a result that is formatted prettily without effort (indentation and line breaks), and we don't need to think about special characters that must be written as XML entities (e.g. "&"amp;) or about Unicode; everything is taken care of, either by XmlWriter or by QTextStream.

XmlWriter Implementation

Now let's look at XmlWriter's implementation. The constructor initializes the out text stream with an I/O device and initializes other private data members. If a codec is provided, the text stream uses it and a line is prepended to the XML file to specify the encoding (the default is Unicode UTF-8/UTF-16):

XmlWriter::XmlWriter( QIODevice *device,
                      const QTextCodec *codec )
    : indentSize( 4 ), autoNewLine( false ),
      atBeginningOfLine( true )
{
    out.setDevice( device );
    if ( codec == 0 ) {
        out.setEncoding( QTextStream::UnicodeUTF8 );
    } else {
        out.setCodec( codec );
        out << "<?xml version=\"1.0\" encoding=\""
            << protect( codec->mimeName() ) << "\"?>\n";
    }
}

The destructor appends a '"\n"' to the file in "auto new-line" mode:

XmlWriter::&nbsp;XmlWriter()
{
    if ( autoNewLine && !atBeginningOfLine )
        out << endl;
}

(On Unix, text files should always end with a '"\n"'.)

Although private, the protect() function is by far the most important. It replaces special characters with entities:

QString XmlWriter::protect( const QString& string )
{
    QString s = string;
    s.replace( "&", "&amp;" );
    s.replace( ">", "&gt;" );
    s.replace( "<", "&lt;" );
    s.replace( "\"", """ );
    s.replace( "\'", "&apos;" );
    return s;
}

The opening() private function constructs an opening tag from the name of the tag and the "name = value" attributes:

QString XmlWriter::opening( const QString& tag,
                            const AttrMap& attrs )
{
    QString s = "<" + tag;
    AttrMap::ConstIterator a = attrs.begin();
    while ( a != attrs.end() ) {
        s += " " + a.key() + "=\"" + protect( *a ) + "\"";
        ++a;
    }
    s += ">";
    return s;
}

The writePendingIndent() private function indents a tag:

void XmlWriter::writePendingIndent()
{
    if ( atBeginningOfLine ) {
        out << indentStr;
        atBeginningOfLine = false;
    }
}

Now we'll review the public functions. The newLine() function can be used to force a new line:

void XmlWriter::newLine()
{
    out << endl;
    atBeginningOfLine = true;
}

The writeRaw() function outputs raw XML:

void XmlWriter::writeRaw( const QString& xml )
{
    out << xml;
    atBeginningOfLine = false;
}

The writeString() function outputs a string, replacing special characters with entities:

void XmlWriter::writeString( const QString& string )
{
    out << protect( string );
    atBeginningOfLine = false;
}

The writeOpenTag() function outputs an opening tag with optional "name = value" attributes, e.g.  id="X23">}:

void XmlWriter::writeOpenTag( const QString& name,
                              const AttrMap& attrs )
{
    writePendingIndent();
    out << opening( name, attrs );
    indentStr += QString().fill( ' ', indentSize );
    if ( autoNewLine )
        newLine();
}

Closing tags are output by writeCloseTag(), e.g. :

void XmlWriter::writeCloseTag( const QString& name )
{
    indentStr = indentStr.mid( indentSize );
    writePendingIndent();
    out << opening( "/" + name );
    if ( autoNewLine )
        newLine();
}

The writeAtomTag() function outputs an opening-and-closing tag, e.g. X23/>}:

void XmlWriter::writeAtomTag( const QString& name,
                              const AttrMap& attrs )
{
    writePendingIndent();
    QString atom = opening( name, attrs );
    atom.insert( atom.length() - 1, "/" );
    out << atom;
    if ( autoNewLine )
        newLine();
}

The writeTaggedString() is a convenience function that outputs an opening tag, a string, and a closing tag:

void XmlWriter::writeTaggedString( const QString& name,
                                   const QString& string,
                                   const AttrMap& attrs )
{
    writePendingIndent();
    out << opening( name, attrs );
    writeString( string );
    out << opening( "/" + name );
    if ( autoNewLine )
        newLine();
}

Suggested Enhancements

The XmlWriter class is useful as it is, but there are many improvements that could be made. Here are some suggestions:

  • Emit a warning if the opening and closing tags are unbalanced.
  • Support a larger subset of XML without resorting to write Raw().
  • Use a QVariant instead of a QString as the "value" type of AttrMap, to make the construction of an AttrMap from int or double values easier.
The XmlWriter class, along with the small test application, is available in this ZIP file (8K).


Copyright © 2003 Trolltech. Trademarks