C/C++ Users Journal 0 2004
Automatic Dialog Generation For User Interaction
Reading and writing program parameters and settings is a common task in many applications. However, despite being so pervasive, this task is often implemented in an ad-hoc manner using a combination of formats and styles. There are always more important things to do, and besides, you can never know the final file format, so why put in too much effort now, right? Even when these files would need to be changed by users, typical solutions use the (Windows-like) flat .INI file format with field=value lines. Though seemingly much easier to parse, such solutions suffer from several serious drawbacks:
Additionally, allowing application users to change these setting values at runtime requires customized dialog boxes that must be changed with every change and/or addition of new parameters (see the sidebar entitled, "Automatic Dialog Generation for User Interaction").
Having dealt with all these issues over and over, we wrote a general library to help overcome problems such as these. The library we propose is stable and we have been using it for over three years. (Previous versions were posted on the Web [1] and used and reviewed by CodeProject members.) This library, which we call XMLParam, is a portable, ANSI C++-compatible, STL-based class library that uses simple XML documents for the following:
But the best thing is that most of the work is done by the library. The only thing left to do is to specify the path/value names and actual values. In fact, users only have to implement two functions (read and write) to benefit from all of these features.
Since the goal was not to parse an XML file, but to give an easy way to read and write the data, our code is based on David Hubbard's STL-based XML parser [2].
Assume you have an object in an application that shows some colored text on the screen (a demo project and full resources that implement it are available at http://www.cuj.com/code/). The parameters of your application will be the text string itself (one std::string), and the color (three int RGB values). You want to be able to save those parameters to a file and reload them later. Example 1 is a sample XML file containing these values.
Assuming we have member variables, as in Example 2(a), this code snippet shows the basics of how to write this data:
// ParamIO outXml; // received as an argument
outXml.write("PARAMS:TEXT", _text);
outXml.write("PARAMS:COLOR:RED", _red, "the red part");
outXml.write("PARAMS:COLOR:GREEN", _green);
outXml.write("PARAMS:COLOR:BLUE", _blue);
In outXML.write(), the first argument is the XML path inside the file and the second argument is the member variable whose value we'd like to write. The third (optional) argument lets you specify a comment to improve the readability of the created file. ParamIO::write() is a member template function of ParamIO, which is instantiated by the type of the second argument; in this case, std::string and int. This lets ParamIO use each member's streaming operator internally.
When the values belong to different XML paths of the tree, the actual writing order is not important. Within the same XML path, the order is the same as in the source code. The tag path names are case sensitive.
We'll now add two new members to the ColoredText class the font name and the font size; see Example 2(b). The code to read the previous XML snippet, giving default values for (potentially) missing parameters is as follows:
//ParamIO inXml; received as an argument
inXml.read("PARAMS:TEXT", _text, string("Hello world"));
inXml.read("PARAMS:COLOR:RED", _red, 0);
inXml.read("PARAMS:COLOR:GREEN", _green, 0);
inXml.read("PARAMS:COLOR:BLUE", _blue, 0);
inXml.read("PARAMS:FONT:NAME", _fontName, string("Arial"));
_fontSize = 12;
inXml.read("PARAMS:FONT:SIZE", _fontSize, _fontSize);
Reading the XML values into our members is as straightforward as writing them. ParamIO::read() is also a template member function, and it, too, is instantiated by the type of the second argument. ParamIO::read() has a third argument the default value to be used in case the requested parameter does not appear in the XML tree. When reading the XML in Example 1, since it does not contain the font info, the default values will be used.
This example shows two possible forms of using the default values:
By now, you may be wondering where this ParamIO comes from, since it is not declared locally in the aforementioned samples. Also, we have not shown where to name the file into which the data will be saved. All the internal workings of the class are inside the XMLParam library. To make an object ParamIO aware, publicly derive it from the abstract utility class XMLBase. This class (see Listing 1) has two pure virtual methods writeXML() and readXML() which you will override and implement as previously described. The other two nonvirtual functions writeXMLFile() and readXMLFile() contain the machinery to write/read the XML streams to files (see the sidebar "Parsing the XML Stream" for more details). These nonvirtual functions call the XMLBase virtual functions read and write. Since your class is publicly derived from XMLBase, you must implement the virtual methods writeXML() and readXML(); otherwise, the code will not compile. These methods will be automatically called at runtime by writeXMLFile() and readXMLFile(), respectively, due to the virtual method mechanism. This is the same idea as [4]. Listings 2 and 3 show the complete implementation of the ColoredText class.
The optional absolute path argument lets you easily aggregate the file structure with each object seeing only its own subtree while the complete file view is hidden from it.
The class MultiLineText shows several interesting and useful uses of the library, namely aggregation and dynamic vector/array I/O. MultiLineText holds a vector of ColoredText objects (Listing 4). When performing XML I/O, it first reads/writes the number of rows in the text, then delegates the actual reading/writing to its elements. Listing 5 shows how the XML tree elements are created at runtime according to the runtime size of the vector. The actual ColoredText data is written by each entry without any explicit access by the MultiLineText object.
Example 3 shows how two ParamIO XML trees can be compared to detect changes. ParamIO supports operators ==, != for full tree comparisons, and also the compare() method, which allows comparing a subtree with the same path of both trees. Example 3 also demonstrates how to work directly with ParamIO without using the XMLBase class API.
If XML text files are too big or verbose for your taste, simple extensions can add compression and decompression and/or encryption and decryption layers between the file I/O and the XML parsing. In fact, do both if it suits you. The code also contains an implementation that writes/reads zipped XML files. This implementation uses XZip [5] to read/write standard ZIP compressed files. A similar approach can be used to provide encrypted XML files.
Since the ParamIO object can interact directly with standard iostreams (see ZippedXML.cpp), any pre- or postprocessing can be done on these streams after they are written/read. You can use your own iostream-processing modules to suit your own needs, the possibilities are endless; for example, encryption, checksums and error correction data, network I/O, pipes and so on.
Our framework can be implemented using many other XML subcomponents and syntaxes. The interesting idea here is the pattern distributing/aggregating the I/O among the objects themselves while keeping the separated hierarchical ordering. We've used this library on Windows, Linux, and Solaris, and it can be easily ported to any platform that supports Standard C++ and the STL.
[1] "Read and Write Application Parameters in XML," Arnaud Brejeon, http://www.codeproject.com/soap/paramio.asp/.
[2] "A Simple STL Based XML Parser," David Hubbard, http://www.codeproject.com/cpp/stlxmlparser.asp/. We would really like to thank David for his parser; he did a tremendous job. Moreover, it saved us a lot of time, even if a few modifications to his original code were made to fulfill our goals.
[3] "Easy Navigation Through an Editable List View," Lee Nowotny, http://www.codeproject.com/listctrl/listeditor.asp/.
[4] This is similar to the idiom of having a nonvirtual operator >>() in a base class of a hierarchy that calls a virtual print() method implemented by its derived classes.
[5] "XZip and XUnzip: Add zip and/or unzip to your app with no extra .lib or .dll," Hans Dietrich, http://www.codeproject.com/cpp/xzipunzip.asp/.