XML classes for Qt

Contents

The QXmlPutGet library is a pair of classes, QXmlPut and QXmlGet, that allows handling of XML documents very conveniently. They are specifically made for writing application data to XML linearly (e.g. application configuration, widget state, session files, etc.) and reading them back in. Of course, they can also be used to work with third party XML documents.

Qt currently offers three environments for working with XML: QXmlStreamReader/writer, DOM and SAX. None of them make it as easy and intuitive as possible, to work with XML in a Qt application. Thus QXmlPutGet was created, which conceptually can be placed between the QXmlStreamReader/Writer and the DOM approach (internally, it uses DOM).

Setup

  • Get the latest version of QXmlPutGet from the download section at the bottom of this page.
  • Use the qxmlputget.h and .cpp file like any other ordinary class file

Documentation

The complete API documentation is available either online, or as a package in the download section. The package contains the documentation as a HTML hierarchy (the same you access online) and as a qch-help file for QtCreator/Assistant integration. If you use QtCreator or Assistant, you should definetly consider using the qch-help file, it’s great!
The integration of the qch file is pretty straight forward: Copy the qxmlputget.qch file to a place where it should be stored (e.g. the local QtCreator config directory). In QtCreator, go to the program settings and find the help section. In the tab Documentation or similar, you see a list of loaded documentation modules and some buttons to add/remove modules. Click the add button and select the qxmlputget.qch file. That’s it!
Now, when you place the cursor on any QXmlPutGet related class or function, press F1 and you find help just like you know it from Qt components.

Basic usage

The QXmlPutGet library consists of two classes: QXmlPut for writing and QXmlGet for reading XML.

First, Let’s see how we read simple XML files. Assume the following XML is inside the file data.xml:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <name>Max Planck</name>
  <birth>1858</birth>
  <nobelprize>yes</nobelprize>
</root>

This is how you use QXmlGet to read its content:

QXmlGet xmlGet;
xmlGet.load("data.xml");
if (xmlGet.find("name"))
  QString name = xmlGet.getString(); // returns "Max Planck"
if (xmlGet.find("birth"))
  int birthYear = xmlGet.getInt(); // returns 1858
if (xmlGet.find("nobelprize"))
  bool nobelPrize = xmlGet.getBool(); // returns true

the load function actually returns a boolean value we should have checked. It tells us whether the loading was successful. Further, it offers three output parameters (message, error line, error column) that carry information when the XML parsing failed.

As you can see, reading consists of two steps: navigating to the tag in the current hierarchy level with find and, if that returns true (i.e. a tag with the specified name was found), reading the content of the tag with the appropriate get(…) function. QXmlPutGet supports many Qt types for writing into tags, including basic types like QString, int, double and bool. But also more complex types like QSize, QRect, QPoint, QPen, QBrush, QFont, QColor, QDateTime, QStringList and even QByteArray and QImage can be written to/read from tags with such a simple interface (see the documentation for those functions).

Here’s how we can create such an XML file with QXmlPut:

QXmlPut xmlPut("root");
xmlPut.putString("name", "Max Planck");
xmlPut.putInt("birth", 1858);
xmlPut.putBool("nobelprize", true);
xmlPut.save("data.xml");

Now that was easy.

Nested tags

Currently, our XML file isn’t much better than a plain list of entries. XML’s speciality is organizing data in nested structures. So let’s improve our file structure (I’m skipping loading/saving code for brevity):

QXmlPut xmlPut("root");
xmlPut.descend("physicist");
xmlPut.putString("name", "Max Planck");
xmlPut.putInt("birth", 1858);
xmlPut.putBool("nobelprize", true);
xmlPut.rise();
xmlPut.descend("physicist");
xmlPut.putString("name", "Erwin Schrödinger");
xmlPut.putInt("birth", 1887);
xmlPut.putBool("nobelprize", true);
xmlPut.rise();

Which will produce:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <physicist>
    <name>Max Planck</name>
    <birth>1858</birth>
    <nobelprize>yes</nobelprize>
  </physicist>
  <physicist>
    <name>Erwin Schrödinger</name>
    <birth>1887</birth>
    <nobelprize>yes</nobelprize>
  </physicist>
</root>

So descend and rise are the magic functions that let us create nested tag hierarchies with QXmlPut. In QXmlGet, these functions exist, too. There, descend descends into the current tag (i.e. the one previously found by the find function) and rise leaves it again.

But what’s with the two tags, both called “physicist” in the top hierarchy level? The simple find function of QXmlGet will always just find Max Planck, i.e. the first occurence of the tag name. That’s where findNext comes into play:

while (xmlGet.findNext("physicist"))
{
  xmlGet.descend();
  if (xmlGet.find("name"))
    QString name = xmlGet.getString(); // "Max Planck" in first, "Erwin Schrödinger" in second iteration
  (...)
  xmlGet.rise();
}

findNext starts searching at the current element and returns true as long as it finds further entries with the specified tag name. If no more entries are found, it returns false once, and then resets the internal current element of the QXmlGet instance to the beginning of the hierarchy level (this means the current element is pointing to the parent of the hierarchy level). If, for any reason, you wish to reset the current element to the start manually, you can call findReset.

 Tag attributes

Every XML tag can carry multiple attributes. Consider the following XML content:

<birth location="Kiel">1858</birth>
<nobelprize title="discovery of energy quanta" year="1919">yes</nobelprize>

Here we have a total of three attributes: location for the birth tag as well as title and year for the tag nobelprize.
Here’s how you can read such attributes:

if (xmlGet.find("birth"))
{
  QString birthPlace = xmlGet.getAttributeString("location", "unknown");
  int birthYear = xmlGet.getInt();
}
if (xmlGet.find("nobelprize"))
{
  QString nobelTitle = xmlGet.getAttributeString("title", "unknown");
  int nobelYear = xmlGet.getAttributeInt("year");
  bool nobelPrize = xmlGet.getBool();
}

We’ve changed the default return value to “unknown” for the birth location and nobelprize title. If the respective attribute can’t be found, this will be returned instead. All “get(…)” functions optionally accept such a default value. Before reading, we could have also checked, whether the attribute actually exists, with xmlGet.hasAttribute(QString name).

The writing part is straight forward, too:

xmlPut.putInt("birth", 1858);
xmlPut.setAttributeString("location", "Kiel");
xmlPut.putBool("nobelprize", true);
xmlPut.setAttributeString("title", "discovery of energy quanta");
xmlPut.setAttributeInt("year", 1919);

Nested tags with subroutines

When you work with sufficiently sophisticated XML structures, it makes sense to split the handling in separate, independent functions. QXmlPutGet was designed to allow this in an elegant manner. Both QXmlGet and QXmlPut are lightweight when copied, since nearly all their members use constant shared data, i.e. only references need to be copied. Of course, the underlying XML document isn’t copied either, since we want to access the same document with every copy of QXmlGet/Put.

Delegating XML work to another function works by passing a QXmlGet/Put instance (by value). The function can then use this copy, to do it’s work on the document, without disturbing the original instance (e.g. by forgetting to rise after a descend). Further, QXmlPut/Get offer two special functions, that return copies of themselves with useful properties in this situation:

The method restricted() returns an instance that isn’t allowed to rise beyond the current hierarchy. This way you can make sure certain functions don’t accidentally access portions of the XML file they shouldn’t have anything to do with. You will typically use this method when your main function needs to handle some XML inside the same hierarchy as the subfunction. When it’s done with its XML, it calls the subfunction with a restricted copy of the used QXmlPut/Get instance.

The method descended() is useful when the subfunction handles the hierarchy inside a tag completely on its own. It returns an instance of QXmlPut/Get that is descended in the current element (in QXmlPut it creates that element first, just like descend), and also restricted to it.

Enough theory, the following example makes use of this concept.

Slightly more realistic example

An application for managing a store has to save the inventory of the store. Further, we’ll want to save some user interface settings, too. We want three functions for this:, writeXmlInventory, writeXmlInventoryItem and writeXmlSettings. At the same time, In our XML file, the two shall each have their own hierarchy, in the tags <inventory> on the data side, and <settings> on the user interface side.

Let’s look at the XML file first:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE SMData>
<storemanager>
  <inventory>
    <item category="3" name="Milk">
      <quantity>40</quantity>
      <price>1.85</price>
    </item>
    <item category="7" name="Orange">
      <quantity>53</quantity>
      <price>0.49</price>
    </item>
    <item category="7" name="Banana">
      <quantity>27</quantity>
      <price>1.29</price>
    </item>
  </inventory>
  <settings>
    <geometry width="943" left="137" height="543" top="422"/>
    <soldoutcolor>#ff1414</soldoutcolor>
  </settings>
</storemanager>

Each item is represented by a struct InventoryItem like this:

struct InventoryItem
{
  int category, quantity;
  QString name;
  double price;
};

And our data store is just a QList<InventoryItem> inventory;

Here’s how the XML file is generated (assuming the inventory list is filled with the three items Milk, Orange and Banana as shown above):

QXmlPut xmlPut("storemanager", "1.0", "UTF-8", true, "SMData");
writeXmlInventory(xmlPut.descended("inventory"));
writeXmlSettings(xmlPut.descended("settings"));
xmlPut.save("storemanager.xml");

// Using the functions:

void MainWindow::writeXmlSettings(QXmlPut xmlPut)
{
  xmlPut.putRect("geometry", geometry());
  xmlPut.putColor("soldoutcolor", QColor(255, 20, 20));
}

void MainWindow::writeXmlInventory(QXmlPut xmlPut)
{
  for (int i=0; i<inventory.size(); ++i)
    writeXmlInventoryItem(xmlPut.descended("item"), inventory.at(i));
}

void MainWindow::writeXmlInventoryItem(QXmlPut xmlPut, const InventoryItem &item)
{
  xmlPut.setAttributeString("name", item.name);
  xmlPut.setAttributeInt("category", item.category);
  xmlPut.putInt("quantity", item.quantity);
  xmlPut.putDouble("price", item.price);
}

As you can see in the writeXmlInventoryItem function, the subfunction can access (here write) attributes to the parent tag. This is due to the fact, that the current element is the parent tag, when nothing else has been written to the hierarchy yet. It’s similar for QXmlGet, too: the current element is the parent tag when no navigation took place via find, findNext etc., or when findReset was just called.

So here’s how to read that xml file back in:

QXmlGet xmlGet;
xmlGet.load("storemanager.xml");
if (xmlGet.docType() == "SMData")
{
  if (xmlGet.find("inventory"))
    readXmlInventory(xmlGet.descended());
  if (xmlGet.find("settings"))
    readXmlSettings(xmlGet.descended());
} else
  QMessageBox::critical(this, "StoreManager", "Wrong doctype in storemanager.xml!");

// Using the functions:

void MainWindow::readXmlSettings(QXmlGet xmlGet)
{
  if (xmlGet.find("geometry"))
    setGeometry(xmlGet.getRect(geometry())); // the fail-safe default geometry is the current geometry()
  if (xmlGet.find("soldoutcolor"))
    QColor soldOutColor = xmlGet.getColor(Qt::red); // fail-safe default color is red
}

void MainWindow::readXmlInventory(QXmlGet xmlGet)
{
  while (xmlGet.findNext("item"))
  {
    InventoryItem newItem = readXmlInventoryItem(xmlGet.descended());
    if (!newItem.name.isEmpty()) // don't add potentially corrupt data
      inventory.append(newItem);
  }
}

InventoryItem MainWindow::readXmlInventoryItem(QXmlGet xmlGet)
{
  InventoryItem item;
  item.name = xmlGet.getAttributeString("name"); // returns an empty string if "name" attribute not found
  item.category = xmlGet.getAttributeInt("category");
  if (xmlGet.find("quantity"))
    item.quantity = xmlGet.getInt();
  if (xmlGet.find("price"))
    item.price = xmlGet.getDouble();
  return item;
}

Of course, the soldoutcolor is really just a dummy here, so the geometry tag doesn’t feel so alone in the settings hierarchy.

Saving and loading UI state

The QXmlPutGet library offers very convenient facilities to save and load the state of QWidget-based UIs. Methods such as for example QXmlPut::saveLineEdit or QXmlPut::saveAbstractSlider (and the corresponding QXmlGet::loadLineEdit / QXmlGet::loadAbstractSlider) methods allow saving the states of such widgets. Even more convenient is the method-pair QXmlPut::saveWidget and QXmlGet::loadWidget, which automatically detects the type and saves/loads the widget state. If you wish to save/load all child widgets in a parent widget, consider using QXmlPut::saveWidgetsRecursive and QXmlGet::loadWidgetsRecursive, which also allow specifying a name filter prefix/suffix, and an exclusion list. More information on the respective methods can be found in the documentation.

Reading from and writing to the same document

You may have noticed, that reading and writing was strictly separated. QXmlGet has no way of modifying the underlying XML document and QXmlPut has no way of reading from it. In many cases, as the store example above, this strict separation of responsibility is sensible and sufficient. However, if you need to work on XML content dynamically, e.g. perform modification- and reading operations interleaved, maybe even dependent on eachother, what we’ve learned so far wouldn’t help much.

QXmlGet and QXmlPut each have a special constructor to convert between the classes to solve this issue:

QXmlGet xmlGet;
xmlGet.load("file.xml");
xmlGet.findAndDescend("someTag"); // combines find and descend in one call, see documentation
if (xmlGet.getAttributeBool("modifyme"))
{
  QXmlPut xmlPut(xmlGet); // here's QXmlPut's special constructor taking QXmlGet
  xmlPut.putString("childTag", "hello");
  xmlPut.save("file.xml");
}

This code loads file.xml, descends into <someTag> and checks whether it has a boolean attribute called “modifyme” set to true, like this: <someTag modifyme="yes">. If that is so, it writes a <childTag>hello</childTag> entry inside the <someTag> hierarchy and saves the modified file.

Note that both xmlGet and xmlPut instances can safely be used alternatingly. So when xmlPut writes a new tag, xmlGet will be able to find and read it right away.

Currently, there’s no QXmlPutGet way of removing a tag from an existing XML document. If you need to do this, consider accessing the underlying QDomDocument via the document and element functions and doing it with the DOM API. Watch out that existing QXmlPut/Get instances aren’t on or inside the node about to be removed, or at least don’t use those specific instances after the remove operation, since they’ll be in an invalid state.

Download

QXmlPutGet source files: QXmlPutGet.tar.gz
QXmlPutGet documentation: QXmlPutGet-doc.tar.gz
Release date: 15.10.19