IBM : developerWorks : Linux library
Download it now!
PDF (89 KB)
Free Acrobat™ Reader

Making application programming easy with GNOME libraries, Part 3
Adding file saving and loading using libxml

George Lebl
developerWorks columnist and GNOME project member
December 1999

Contents:
 Writing the XML code
 Creating an XML tree
 Saving XML documents
 Creating the GUI
 Resources
 About the author

Last month, George showed you how to build a genealogy program with the GNOME libraries. This month he expands the application by making it possible to load and save data using the libxml library, a set of routines and structures for manipulating an XML tree in memory.

Having built a functioning genealogy application in last month's GNOMEnclature column, we still need to add a way to load and save the application's data. This is a two-part process: we need to include file dialogs in the GUI, and use the libxml to read and save the file. We will be extending the application developed in the previous installment.

Let's first concentrate on the actual loading and saving. For this we will use libxml. Libxml is actually a set of routines and structures for manipulating an XML tree in memory. You can load or save this tree to file with one call (xmlParseFile and xmlSaveFile). There are several ways we can use the library for our purposes. We could completely abandon our way of storing the data in memory, and use the XML document as our data storage. We could also keep both the document and our own data structures in memory and update them simultaneously. Or, we could load the document, create our own data structures from the resulting XML tree, and then free the XML tree. Then when we want to save, we create an XML tree, save it to disk, and then free it. Because we already have a way of storing the data in memory, using glib data containers, I have chosen the last of these options. Glib containers are easier to manage than the XML tree so they are more suitable for storing the data while it is in memory.

Now that we've figured out the method we will be using for storing the data, we need to come up with a format for our file. Because the data we have is actually a tree, this is well-suited to XML. An XML document has a root node, and we will call this node "Genealogy." The root node will have children of type "Person," and each such node can have two children of the type "Person." Note that children of the node are the parents of the person in real life. The "Person" nodes also have three properties: "name," "dob," and "dod," for name, date of birth, and date of death, respectively. With this structure an XML file would look like the following:

 <?xml version="1.0"?>
  <Genealogy>
    <Person name="Vaclav I." dob="After 905" dod="28.9. 929 or 935">
      <Person name="Vratislav I." dob="Unknown" dod="13.2. 921">
        <Person name="Borivoj I." dob="Unknown" dod="Around 894"/>
        <Person name="Ludmila" dob="Unknown" dod="16.9. 921"/>
      </Person>
      <Person name="Drahomir" dob="Unknown" dod="Unknown"/>
    </Person>
  </Genealogy>

This represents the family tree of a Czech ruler and saint, Vaclav I -- at least as far back as we know it.

Now let's get to the actual code (see the entire listing). First we need to figure out how to compile this program. We'll just take our old makefile and add "xml" to the list of GNOME libraries to link on the gnome-config command line. It will thus look like:

  CFLAGS=-g -Wall `gnome-config --cflags gnome gnomeui xml`
  LDFLAGS=`gnome-config --libs gnome gnomeui xml`

  all: gnome-genealogy-manager

  clean:
          rm -f *.o core gnome-genealogy-manager

We also need to add some includes to our .c file. There are two files we need to add, tree.h and parser.h from the gnome-xml include directory. So we will add the following:

  #include <gnome-xml/tree.h>
  #include <gnome-xml/parser.h>

Writing the XML code
Now we are ready to create the actual XML code. Before we start, however, let me note that I have added a function since the last article, called add_person, which will allocate a new Person structure and add it to our tree. This is now used in the add_person_cb and add_parent_cb. It is useful because we are adding persons to our internal data structure all over the place.

We need to write a function that loads an XML file. We will call it read_xml_file, and have it take a filename as a parameter and return a Boolean so that we can check if it failed or succeeded. We first define an xmlDocPtr variable, which is just a pointer to an XML document. Then we call xmlParseFile to get a new XML document from a file. So our code will look something like:

  xmlDocPtr doc;
  doc = xmlParseFile(filename);

In case something went wrong and the returned doc pointer is NULL, we should abort the function and return FALSE.

When we have an XML document, we need to check if it's a document with genealogy data in it, so we check the name of the root node. If it is "Genealogy" then we have the right file. (It is a good idea to be paranoid when checking for NULLs.) Thus, our check will look like the following:

  if(/* if there is no root element */
     !doc->root ||
     /* if it doesn't have a name */
     !doc->root->name ||
     /* if it isn't a Genealogy node */
     g_strcasecmp(doc->root->name,"Genealogy")!=0) {
          xmlFreeDoc(doc);
          return FALSE;
  }

First we confirm that doc->root isn't NULL, then we confirm that the name of doc->root isn't NULL. When we are sure that doc->root->name is a string, we use g_strcasecmp -- a glib function for portable case-insensitive string comparison -- to compare the doc->root->name to "Genealogy". If any of these tests fail, we free the XML tree using xmlFreeDoc and return FALSE to indicate failure to load the file.

If we have come this far, we know that we have the right file and we have it loaded in the XML tree pointed to by doc. We call parse_doc function, which we will define to clean our view and all our current data and load the new data from the XML tree. First we do this:

  /* clear our view */
  gtk_clist_clear(GTK_CLIST(clist));

  /* clear our old data */
  clear_all_data();

The function gtk_clist_clear will clear our list view, and clear_all_data will clear all our internal data. I will explain this function later.

We must now iterate through all the children of the root node in the document and add them as persons to our structures and to the view. We use xmlNodePtr type as a pointer to an XML tree node. Each node has two fields, which we will use for the traversal. Each has a 'childs' field, which is an xmlNodePtr to the first child of this node (that 's' on the end there is not a mistake). Then each node also has a 'next' pointer to the next node in the list of children. Because the root node is also just a normal xmlNode, we can use the following to browse through all the children and run add_xml_person_to_data on each of them:

  xmlNodePtr node;

  /* find <Person> nodes and add them to the list, this just
     loops through all the children of the root of the document */
  for(node = doc->root->childs; node != NULL; node = node->next) {
          /* add the person to the list, there are no children so
             we pass NULL as the node of the child */
          add_xml_person_to_data(node,NULL);
  }

Now we need to define what add_xml_person_to_data does. It takes an xmlNodePtr to a node and a GNode pointer to the parent (meaning the child in real life) of this node. Inside this function we first check if the name of the XML node is actually "Person"; this is done in the same manner as for the root node, if we fail, we will just return from the function, which will skip to the next node.

Once we have the "Person" node, we need to get the "name," "dob," and "dod" properties and add the person to our internal structures. To get the properties we call xmlGetProp, with the node pointer and a string with the name of the property. This function will return a pointer directly into the tree where the property is stored, so make sure you do not free this string. Now we also need to provide defaults for the values, in case we get back a NULL signifying that the property didn't exist. For date of birth and date of death, this is simple; we can just point the string to a local string buffer as in:

  char *dob, *dod;

  dob = xmlGetProp(xmlnode,"dob");
  /* if unspecified, make it "Unknown" */
  if(!dob) dob = "Unknown";

  dod = xmlGetProp(xmlnode,"dod");
  /* if unspecified, make it "Unknown" */
  if(!dod) dob = "Unknown";

For the name, the process is a bit different. We want to create a unique name with the global number, just like we do when we are adding a new person. Thus, we use g_strdup_printf and we also must make sure we free the string after use:

  char *name;
  GNode *node;

  /* get the name of the person */
  name = xmlGetProp(xmlnode,"name");

  if(name) {
          /* add the person to the database */
          node = add_person(name,dob,dod,child);
  } else {
          /* no name, so we need to make one up */
          name = g_strdup_printf("Unnamed person %d",++number);
          /* add the person to the database */
          node = add_person(name,dob,dod,child);
          g_free(name);
  } 

If we do get some string back from xmlGetProp(xmlnode,"name"), we just call add_person with the strings that we got and the child pointer (that's the second argument to add_xml_person_to_data). In case we get no name, we create a new name, call add_person, and free the name.

Now we need to traverse all the children and call add_xml_person_to_data recursively. This time we will pass the GNode pointer that we got from add_person. To traverse the list of children, we set xmlnode to its childs pointer. Then we traverse the list by always setting xmlnode to xmlnode->next. We do this in the following manner:

  xmlnode = xmlnode->childs;
  /* go through all the parents and add them as well */
  while(xmlnode) {
          /* use the 'node' as the child of the parents */
          add_xml_person_to_data(xmlnode,node);
          xmlnode = xmlnode->next;
  }

This should be all that we need to load the XML file. We now simply free the doc pointer in read_xml_file as we don't need the XML tree anymore.

I should explain a bit about the clear_all_data function that we used above. This function traverses the trees GList and for each element, which is a GNode pointer, we call a free_person_fe function, and then use g_node_children_foreach to traverse all children and call the free_person_fe for each of the children of the node. This looks like:

  g_node_children_foreach(node,G_TRAVERSE_ALL,free_person_fe,NULL);

The free_person_fe function is in standard form for g_node_children_foreach, and looks like:

  static void
  free_person_fe(GNode *node, gpointer data)
  {
	  Person *person = node->data;
	  g_free(person->name);
	  g_free(person->dob);
	  g_free(person->dod);
	  g_free(person);
  }

Having freed all the Person structures in the node, we do g_node_destroy on the node, which will free memory associated with the node. After we have done this for all the nodes, we have to free our 'trees' GList. We do this by g_list_free. Then we just set trees to NULL and we are now ready to add new data.

Creating an XML tree
We have completed half our XML code. We have yet to write code to create an XML tree from our internal structures and then to save the tree to disk. We write a function, write_xml_file, which will take the file name and return a Boolean to indicate success or failure.

The first thing we need to do is to create a new xmlDoc and make a new root node. This will create our document and the root node:

  xmlDocPtr doc;

  /* create new xml document with version 1.0 */
  doc = xmlNewDoc("1.0");
  /* create a new root node "Genealogy" */
  doc->root = xmlNewDocNode(doc, NULL, "Genealogy", NULL);

The "1.0" is the XML version and you should just use "1.0". The xmlNewDocNode takes the document as the first argument, a name-space as the second (we don't use name-space so we pass a NULL), a name of the node as the third argument, and the text content as the fourth argument.

We still have to add all the nodes for all the persons. To do so, we must iterate through the trees GList of GNodes. We write a function, write_person_to_xml, to which we pass the XML node to which we want to add persons as the children of the node (parents in real life), and the GNode pointer for the person that we want to add. This iteration will look like:

  /* loop through all our trees */
  for(list = trees; list != NULL; list = list->next) {
          GNode *node = list->data;
          write_person_to_xml(doc->root,node);
  }

This will add all the top-level GNode nodes to the root node of the XML tree. Inside write_person_to_xml we create a new XML node with xmlNewChild call, which takes the parent node as the first argument, name-space as second, name of the node as the third, and the textual content as the last argument. We don't use any name-space so we pass NULL; we also don't have any textual content so we pass NULL for that as well. We then need to set some properties of the node, such as "name," "dob," and "dod." We do this with xmlSetProp:

  Person *person;
  xmlNodePtr newxml;

  person = node->data;

  /* make a new xml node (as a child of xmlnode) with an
     empty content */
  newxml = xmlNewChild(xmlnode,NULL,"Person",NULL);
  /* set properties on it */
  xmlSetProp(newxml,"name",person->name);
  xmlSetProp(newxml,"dob",person->dob);
  xmlSetProp(newxml,"dod",person->dod);

Now we also need to add the "parents" (the real-world parents, that is) of this node. We can be sure that there are only two, but there can also be one or none. So we can do the following while calling write_person_to_xml recursively:

  /* if we have a parent, add it to our xml node */
  if(node->children) {
          write_person_to_xml(newxml,node->children);
          /* if we have a second parent, add it to our xml node */
          if(node->children->next)
                  write_person_to_xml(newxml,node->children->next);
  }

We could implement this as a loop with probably the same amount of code, and you'd need to do a loop if you didn't know how many children the node has.

Saving the XML document
Now that we have an XML document tree with all our data in it we need to write it to disk. To do that we call xmlSaveFile with the filename as the first argument and the document pointer as the second argument. You should check its return as it will return -1 on error and you should warn the user about this. We return FALSE from our saving function in case this happens. After this we just free the document with xmlFreeDoc as it's no longer needed.

Another thing that we do in our reading and saving functions is that we keep the name of the last correctly read or written file in a global string variable named 'filename'. This helps us set the defaults for the file dialogs and is used for the simple "Save" menu item.

Creating the GUI
OK, now we are ready to add the GUI. GNOME itself doesn't yet have a file dialog, so we will use the normal GTK+ one. So let's start with the open dialog. We define an open_cb callback, which we bind in our menu definitions. Inside we create a new GtkFileSelection widget with gtk_file_selection_new to which we pass the dialog title. Because we will need to access things directly in the GtkFileSelection structure, we define our local variable pointer as that instead of GtkWidget as usual, and we cast to GtkFileSelection when we call the gtk_file_selection_new method. The structure has two members, ok_button and cancel_button, which are just widget pointers to the buttons on the dialog. So you bind the "clicked" signal on those, just like you would on any normal GtkButton. Here's what we do:

  GtkFileSelection *fsel;

  /* make a new gtk file selection */
  fsel = GTK_FILE_SELECTION(gtk_file_selection_new("Open"));

  /* Connect the signals for Ok and Cancel */
  gtk_signal_connect(GTK_OBJECT(fsel->ok_button), "clicked",
                     GTK_SIGNAL_FUNC(open_ok), fsel);
  /* connect gtk_widget_destroy to the cancel button, so that
  we just kill the dialog */
  gtk_signal_connect_object(GTK_OBJECT(fsel->cancel_button), "clicked",
                            GTK_SIGNAL_FUNC(gtk_widget_destroy), 
                            GTK_OBJECT(fsel));

The first connect is just a straightforward gtk_signal_connect passing the pointer to the file selection dialog as data to the open_ok handler. The second is a special form of signal connection; it will call gtk_widget_destroy with its argument set to GTK_OBJECT(fsel). This is useful so that we don't have to define a completely new handler function for this. We need to do a bit more setup on the dialog and show it:

  gtk_window_position(GTK_WINDOW(fsel), GTK_WIN_POS_MOUSE);
  gtk_window_set_transient_for(GTK_WINDOW(fsel),GTK_WINDOW(app));
  gtk_widget_show(GTK_WIDGET(fsel));

The first call sets the position of the dialog to appear around the mouse; we do this because this is not a GNOME dialog. If it were a GNOME dialog, we wouldn't do this as it would already be set for us by GnomeDialog. The second call is basically the same as gnome_dialog_set_parent, but for GtkWindows; it just sets the parent of our dialog to be the application window, which lets the window manager know that this is a dialog belonging to our app and behave appropriately. The last call just shows the dialog. Note that this dialog is not modal, and doesn't block the main window. It doesn't really need to and it is better user interface policy not to block when possible.

Inside the open_ok handler we call gtk_file_selection_get_filename to get the filename from the file selection widget. This is a pointer to an internal buffer, so don't free it. If we do get a filename and we can load it, we destroy the dialog. If we get an error, we need to display an error box, this time with the file selection window as a parent of the error dialog box. The whole construct looks like:

  char *fname;

  /* get the filename */
  fname = gtk_file_selection_get_filename(fsel);
  if(/* if no file was selected */
     !fname ||
     /* or we can't read it */
     !read_xml_file(fname)) {
          GtkWidget *dlg;

          /* make a new dialog with the file selection as parent */
          dlg = gnome_error_dialog_parented("Cannot open file",
                                            GTK_WINDOW(fsel));
	  gtk_window_set_modal(GTK_WINDOW(dlg), TRUE);
  } else {
          /* we have read the file without a problem so just
             close the open dialog */
          gtk_widget_destroy(GTK_WIDGET(fsel));
  }

Note that the code for handling errors creates a new dialog with gnome_error_dialog_parented, and sets the file selection dialog as its parent. We also set the error dialog to be modal with gtk_window_set_modal method with a TRUE argument; this is so that the user is forced to close or respond to the dialog before proceeding. We don't need to use the gtk_widget_show method here, as gnome_error_dialog_parented will have already shown the dialog for us.

For the save_as_cb, which does our "Save As" dialog, we do a very similar thing. The difference here is that we want to set as default the last file that was saved or loaded, so we write:

  /* if we have a current filename add it to the box */
  if(filename)
          gtk_file_selection_set_filename(fsel, filename);

'filename' is our global variable where we store the last loaded or saved file. Our OK button handler, save_ok, is almost exactly the same as open_ok, except for calling write_xml_file instead of read_xml_file.

We also need to write a handler for the "Save" menu, which should just save the current data to 'filename', or if that is not set, it should call save_as_cb. This is defined in save_cb. This function is very trivial:

  /* if no default filename is set, call save_as_cb */
  if(!filename) {
          save_as_cb(NULL,NULL);
          /* try writing the file now */
  } else if(!write_xml_file(filename)) {
          /* tell the user what's wrong according to his preferences,
             it can be on the status bar or a dialog */
          gnome_app_error(GNOME_APP(app),"Cannot save file");
  }

That is all that is needed for simple reading and writing of XML files. Next time we'll look at GNOME Canvas, a high-level engine for creating structured graphics.

Resources

Other sites of interest to GNOME developers include the following:

About the author
George (Jiri or Jirka in Czech) Lebl was born in Prague, Czechoslovakia, and now lives in San Diego, California, where he is trying to finish his degree. After several years using non-UNIX operating systems, he started using UNIX and became a UNIX bigot about four years ago, and a Free Software bigot about two years ago. He joined the GNOME project in the fall of 1997, and finally became a C bigot as well. And, most importantly, he's a VI user. He can be reached at
jirka@5z.com.





What do you think of this article?

Killer! Good stuff So-so; not bad Needs work Lame!

Comments?


PrivacyLegalContact