IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux  >

利用 GNOME 库来简化应用编程,第 3 部分

利用 libxml 添加文件保存和装入

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

George Lebl (jirka@5z.com), developerWorks 专栏作家和 GNOME 项目成员

1999 年 12 月 01 日

上个月,George 说明了如何使用 GNOME 库来构建家谱程序。这个月,通过使用libxml 库(用于在内存中操纵 XML 树的一组例程和结构)装入和保存数据成为可能,他扩展了该应用程序。

在构建了 上个月 GNOMEnclature专栏中介绍的实用家谱应用程序后,我们仍需要增加一种装入和保存应用程序数据的方法。这是由两部分组成的过程:首先我们需要在 GUI 中包含文件对话框,其次使用 libxml 读取和保存文件。下面我们将扩充上一部分中开发的应用程序。

首先让我们专门看一下实际的装入和保存。因此我们将使用 libxml。libxml 实际上是用于在内存中操纵 XML 树的一组例程和结构。可以使用一个调用(xmlParseFile 和 xmlSaveFile)将这棵树装入到或保存到文件中。有几种可以使用该库的方法针对我们的目的。我们将完全抛弃在内存中存储数据,而使用 XML 文档作为数据存储器。也可以将文档和自己的数据结构都保存在内存中,然后同时更新它们。或者,我们可以装入文档,从产生的 XML 树中创建自己的数据结构,然后释放 XML 树。希望保存时,先创建 XML 树,将它保存到磁盘,然后再释放它。因为我们已经有了使用 glib 数据容器在内存中存储数据的方法,所以我选择最后一种选项。glib 容器比 XML 树更易于管理,因此当它们在内存中时更适合于存储数据。

既然我们已经知道了用于存储数据的方法,现在需要想出一种文件格式。因为我们所拥有的数据实际上是树,它非常适合于 XML 。XML 文档有个根节点,我们将这个节点称为 "Genealogy"。根节点将有 "Person" 类型的子节点,每个这样的节点可以有两个 "Person" 类型的子节点。请注意,节点的子节点是实际生活中的双亲。 "Person" 节点也有三个特性:"name"、"dob" 和 "dod",分别对应于姓名、出生日期和死亡日期。使用这种结构的 XML 文件看起来如下:

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

它代表了捷克统治者和圣徒,Vaclav 一世的家族树 -- 至少是我们所知的最早的家族树。

现在让我们谈谈实际的代码( 请参阅完整清单)。首先需要考虑如何编译这个程序。我们将只使用旧的 makefile,并将 "xml" 添加到 GNOME 库列表来在 gnome-config 命令行上链接。因此它看上去就象:

   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
 
                

我们还需要向 .c 文件中添加一些包含文件。需要添加两个文件,gnome-xml include 目录中的 tree.h 和 parser.h。因此我们将添加以下内容:

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

编写 XML 代码


现在我们可以创建实际的 XML 代码了。但在开始之前,让我说明一下,我添加了上篇文章中名为 add_person 的函数,它将分配一个新的 Person 结构并将它添加到树中。现在它用在 add_person_cb 和 add_parent_cb 中。因为我们到处将人员添加到内部数据结构中,所以它非常有用。

需要编写装入 XML 文件的函数。将称它为 read_xml_file,并让它将文件名作为参数,返回一个布尔值,这样我们就可以知道它成功还是失败了。首先定义一个 xmlDocPtr 变量,它只是指向 XML 文档的指针。然后调用 xmlParseFile 来从文件中获得新的 XML 文档。因此代码看上去如下:

   xmlDocPtr doc;
   doc = xmlParseFile(filename);
 
                

万一有什么错误并且返回的 doc 指针为 NULL,就应该异常终止函数并返回 FALSE。

有了一个 XML 文档后,需要检查它是否是包含家谱数据的文档,所以我们检查根节点的名称。如果它是 "Genealogy",那么就是正确的文件。(检查 NULL 值时多思考一下比较好。)因此,我们的检查看上去类似:

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

首先确认 doc->root 不是 NULL,然后确认 doc->root 的名称不是 NULL。当确保 doc->root->name 是字符串时,使用 g_strcasecmp -- 一个 glib 函数,用于可移植的不区分大小写的字符串比较 -- 来比较 doc->root->name 和 "Genealogy"。如果以上任一测试失败,使用 xmlFreeDoc 释放 XML 树,然后返回 FALSE 来表明装入文件失败。

如果执行完这一步,我们就知道文件是正确的,并且已将它装入到由 doc 指向的 XML 树。调用 parse_doc 函数,我们将它定义成清除视图和所有当前数据,并从 XML 树装入新数据。首先执行:

   /* clear our view */
   gtk_clist_clear(GTK_CLIST(clist));
 
   /* clear our old data */
   clear_all_data();
 
                

函数 gtk_clist_clear 将清除列表视图,而 clear_all_data 将清除所有内部数据。我将在稍后对这个函数加以说明。

现在必须遍历文档中根节点的所有子节点,并将它们作为人员添加到结构和视图中。使用 xmlNodePtr 类型作为指向 XML 树节点的指针。每个节点都有两个字段,它们将用于遍历。每个节点都有一个 'childs'(子节点)字段,它是指向该节点第一个子节点的 xmlNodePtr(结尾这里的 's' 不是错误)。每个节点还有一个指向子节点列表中下一个节点的 'next' 指针。因为根节点也只是一般的 xmlNode,我们可以使用以下代码来浏览所有子节点,并对它们每一个运行 add_xml_person_to_data:

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

现在需要定义 add_xml_person_to_data 的操作。它使用一个指向节点的 xmlNodePtr 和指向该节点父节点的 GNode 指针(实际生活中的子女)。在这个函数内部,我们首先检查 XML 节点的名称是否确实是 "Person";执行的方式与对根节点使用的方式相同,如果失败,我们就从函数返回,跳到下一个节点。

一旦有了 "Person" 节点,需要获得 "name"、"dob" 和 "dod" 特性,并将人员添加到内部结构中。要获得特性,我们使用节点指针和带有特性名称的字符串调用 xmlGetProp。这个函数将返回直接指到存储特性的树中的指针,因此要确保不释放这个字符串。现在我们还需要为值提供缺省值,以防获得表示不存在的 NULL。对于出生日期和死亡日期,这很简单;我们只需要按如下将字符串指向本地字符串缓冲区:

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

对于名称,过程稍有不同。我们希望创建一个带有全局号码的唯一名称,就如同在添加新人员时所做的那样。因此使用 g_strdup_printf,并且必须确保在使用了以下代码后释放字符串:

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

如果我们确实从 xmlGetProp(xmlnode,"name") 中获得了一些字符串,只需要使用获得的字符串和子节点指针(这是 add_xml_person_to_data 的第二个自变量)调用 add_person。万一没有获得名称,就创建一个新名称,调用 add_person,然后释放名称。

现在需要遍历所有子节点并递归调用 add_xml_person_to_data。这次我们将传递从 add_person 中获得的 GNode 指针。要遍历子节点列表,为它的子节点指针设置 xmlnode。然后通过将 xmlnode 设置为 xmlnode->next 来遍历列表。我们使用以下方式来做到这一点:

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

这就是装入 XML 文件需要执行的所有操作。现在只需要释放 read_xml_file 中的 doc 指针,因为我们不再需要 XML 树了。

我应该再解释一下上面用到的 clear_all_data 函数。这个函数遍历树 GList,对每个元素,即 GNode 指针,我们调用 free_person_fe 函数,然后使用 g_node_children_foreach 遍历所有子节点并对节点的每个子节点调用 free_person_fe。它看上去如下:

   g_node_children_foreach(node,G_TRAVERSE_ALL,free_person_fe,NULL);
 
                

free_person_fe 函数是 g_node_children_foreach 的标准形式,它看上去象:

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

释放了节点中的所有 Person 结构后,我们对节点执行 g_node_destroy,它将释放与节点相关的内存。在对所有节点执行了这个操作后,必须释放 'trees' GList。 通过 g_list_free 来执行这一步。然后就将树设置成 NULL,现在就可以添加新数据了。





回页首


创建 XML 树


我们已完成一半 XML 代码。还必须编写代码以从内部结构中创建 XML 树,然后将树保存到磁盘上。我们编写了函数 write_xml_file,它将获取文件名,然后返回一个布尔值,表明成功或失败。

需要做的第一件事是创建新的 xmlDoc 和一个新的根节点。以下代码将创建文档和根节点:

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

"1.0" 是 XML 的版本,并且应该只使用 "1.0"。xmlNewDocNode 将文档作为第一个自变量、将名称空间作为第二个自变量(我们不使用名称空间,因此传递 NULL)、将节点名称作为第三个自变量、将文本内容作为第四个自变量。

我们仍然必须为所有人添加所有节点。要这样做,必须遍历 GNode 的树 GList。我们编写了函数 write_person_to_xml,向它传递 XML 节点,将人员作为节点的子节点(实际生活中的双亲)传递给这个节点,以及希望添加的人员的 GNode 指针。这个遍历看上去如下:

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

这将所有顶级 GNode 节点添加到 XML 树的根节点。在 write_person_to_xml 内部,使用 xmlNewChild 调用创建了一个新的 XML 节点,它将双亲节点作为第一个自变量,将名称空间作为第二个,将节点名作为第三个,将文本内容作为最后一个自变量。不使用任何名称空间,所以传递 NULL;而且还没有任何文本内容,因此同样传递 NULL。然后,需要设置节点的一些特性,例如 "name"、"dob" 和 "dod"。使用 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);
 
                

现在还需要添加该节点的“双亲”(即现实世界中的双亲)。可以确定只有两个双亲,但也可以是一个或没有。因此可以在递归调用 write_person_to_xml 时执行以下操作:

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

可以将它作为一个带有可能相同数量代码的循环来实现,如果您不知道节点有多少个子节点,需要执行一个循环。





回页首


保存 XML 文档


现在我们有了一个带有所有数据的 XML 文档了,需要将它写入磁盘。要执行这个操作,将文件名作为第一个自变量,将文档指针作为第二个自变量调用 xmlSaveFile。应该检查它的返回,如果返回 -1,就表示有错误,应该警告用户注意它。万一发生这种情况,从保存函数返回 FALSE。这之后,我们就使用 xmlFreeDoc 释放文档,因为不再需要它了。

在读取和保存函数中所做的另一件事是在名为 'filename' 的全局字符串变量中保存最后一次正确读取或写入文件的名称。这帮助我们设置文件对话框的缺省值,并用在简单的 Save 菜单项中。





回页首


创建 GUI


好,现在准备添加 GUI。GNOME 本身还没有文件对话框,因此我们将使用一个普通的 GTK+。让我们从打开对话框开始。定义一个 open_cb 回调,在菜单定义中将绑定它。在内部,我们使用 gtk_file_selection_new 创建一个 GtkFileSelection 窗口小部件,将对话框标题传递给这个函数。因为将需要直接访问 GtkFileSelection 结构中的数据,所以将本地变量指针定义为这样而不是按通常那样定义为 GtkWidget,并且在调用 gtk_file_selection_new 方法时强制转换成 GtkFileSelection。结构有两个成员, ok_button 和 cancel_button,它们只是指向对话框上按钮的窗口小部件。所以在它们上面绑定 clicked 信号,如同在任何普通的 GtkButton 上一样。代码如下:

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

第一个连接只是简单的 gtk_signal_connect,它将指向文件选择对话框的指针作为 open_ok 处理程序的数据传递。第二个连接是一种特殊格式的信号连接;它将用设置成 GTK_OBJECT(fsel) 的自变量调用 gtk_widget_destroy。这样就无需为它定义全新的处理程序函数,这非常有用。需要对该对话框再做一些设置然后显示它:

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

第一个调用设置对话框位置出现在鼠标附近;这样做是因为它不是个 GNOME 对话框。如果是 GNOME 对话框,我们就不用这样做,因为 GnomeDialog 已经设置了。第二个调用基本上与 gnome_dialog_set_parent 一样,但是用于 GtkWindows 的;它只将对话框的父窗口设置成应用程序窗口,这样可以让窗口管理器知道它是属于应用程序的一个对话框并能正确使用。最后一个调用显示对话框。请注意,这个对话框不是模态的,并且不阻挡主窗口。它实际上不需要这样,如果可能,最好使用不阻挡用户界面的策略。

在 open_ok 处理程序内部调用 gtk_file_selection_get_filename,以从文件选项窗口小部件获取文件名。这是指向内部缓冲区的指针,所以不要释放它。如果我们确实获得了文件名并可以装入它,就消除对话框。如果有任何错误,需要显示错误框,这时,文件选项窗口就作为错误对话框的双亲。整个构造看起来如下:

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

请注意,用于处理错误的代码使用 gnome_error_dialog_parented 创建一个新的对话框,并将文件选择对话框设置为它的父窗口。我们还使用带有 TRUE 自变量的 gtk_window_set_modal 方法将错误对话框设置为模态的;这就强制用户在继续之前必须关闭或响应对话框。这里我们不需要使用 gtk_widget_show 方法,因为 gnome_error_dialog_parented 已显示了对话框。

对于用于 "Save As" 对话框的 save_as_cb,我们执行非常类似的操作。不同的是我们希望将最后保存或装入的文件设置成缺省值,因此编写了:

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

'filename' 是全局变量,将最后装入或保存的文件存储在其中。除了调用 write_xml_file 而不是 read_xml_file 以外,OK 按钮处理程序 save_ok 几乎与 open_ok 相同。

还需要为 "Save" 菜单编写处理程序,它只应该将当前数据保存到 'filename' 中,如果没有设置,它应该调用 save_as_cb。这是在 save_cb 中定义的。这个函数非常琐碎:

   /* 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");
   }
 
                

这就是对 XML 文件进行简单读写需要的所有代码。下一次,我们将讨论 GNOME Canvas,一种用于创建结构化图形的高级引擎。



参考资料

对 GNOME 开发者有帮助的其它一些站点包括:



关于作者

George (在捷克是 Jiri 或 Jirka)Lebl 出生在捷克斯洛伐克布拉格,现在居住在美国加利福尼亚州圣迭哥,他将在那里获得学位。在使用了几年非 UNIX 操作系统后,他开始使用 Unix,在四年前成为 Unix 的追随者,两年前成为“自由软件”追随者。他在 1997 年秋天参加了 GNOME 项目,最后又成了 C 的追随者。最重要的是,他是一个 VI 使用者。可通过 jirka@5z.com与他联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款