目次


GNOMEnclature

GNOMEライブラリーによる簡易アプリケーション・プログラミング:第3回

libxmlを使用したファイルの保管とロードの追加

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: GNOMEnclature

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:GNOMEnclature

このシリーズの続きに乞うご期待。

先月のGNOMEnclature コラム では機能系図アプリケーションを作成しましたが、アプリケーション・データのロードおよび保管方法を追加する必要があります。これには、GUI へのファイル・ダイアログの組み込み、および libxml を使用したファイルの読み取りと保管の 2 つのプロセスがあります。アプリケーションの拡張には、前回の導入で開発したものを使用します。

最初に実際のロードと保管について説明します。これは libxml を使用して行います。libxml は、記憶域内で XML ツリーを操作するための一連のルーチンおよび構造のことです。このツリーのファイルへのロードまたは保管は、1 回の呼び出し (xmlParseFile と xmlSaveFile) で行うことができます。ライブラリーの使用方法はいくつかあります。ここに示す記憶域内のデータ保管方法ではなく、XML 文書をデータ記憶域として使用することができます。また、文書と記憶域内データ構造をともに使用して、これらを同時に更新することもできます。あるいは、文書をロードし、作成された XML ツリーから自らのデータ構造を作成して、XML ツリーを解放することもできます。この場合、保管するには XML ツリーを作成し、ディスクに保管してから解放します。例では glib データ・コンテナーを使用して、記憶域へのデータ保管方法が既に作成されているので、これらのうちの最後のオプションを使用します。glib コンテナーは XML ツリーよりも管理が容易であるため、メモリー内のデータ保管により適しています。

データ保管に使用する方法が決まりましたので、次はファイルのフォーマットについて説明します。実際に使用するデータはツリーであるため、XML に大変よく適しています。XML 文書にはルート・ノードがあり、このノードを "Genealogy (系図)" と呼びます。ルート・ノードは "Person (個人)" 型の子を持ち、これらの各ノードは "Person" 型の子を 2 つ持つことができます。ノードの子は、実生活においては親となります。"Person" ノードには "name"、"dob"、および "dod" (それぞれ名前、生誕日、死亡日) の 3 つのプロパティーがあります。この構造により、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 I までの家系図を表しています。

さて、実際のコードを見てみましょう 。最初にこのプログラムのコンパイル方法を説明します。従来の makefile を使用し、gnome-config コマンド行でリンクする GNOME ライブラリーのリストに "xml" を追加します。したがって、次のようになります。

              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 組み込みディレクトリーの tree.h および parser.h の 2 つのファイルです。以下を追加します。

              #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 ツリー・ノードへのポインターとして使用します。各ノードには 2 つのフィールドがあり、これはトラバースに使用します。それぞれに '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_person を呼び出します (これは add_xml_person_to_data の 2 番目の引き数です)。名前が取得できない場合は、新しい名前を作成し、add_person を呼び出して名前を解放します。

次にすべての子をトラバースして、add_xml_person_to_data を反復的に呼び出します。この場合は add_person から取得した GNode ポインターを渡します。子のリストをトラバースするため、その子のポインターに xmlnode を設定します。続いて xmlnode->next に常に xmlnode を設定して、リストをトラバースします。次のようにします。

              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 ファイルのロードに必要な内容です。これで XML は必要ありませんので、read_xml_file の doc ポインターを解放します。

ここで使用されている 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 を実行します。これによりノードに関連した記憶域を解放します。すべてのノードについてこれを行ったら、'ツリー' GList を解放する必要があります。これには g_list_free を使用します。次にツリーを NULL に設定し、新規データを追加する準備ができます。

XML ツリーの作成

XML コードの半分が完成しました。後は、内部構造から XML ツリーを作成し、ツリーをディスクに保管するためのコードを作成する必要があります。ここで、write_xml_file 関数を作成します。これはファイル名を取得して、成功したかどうかを示す Boolean を戻します。

最初に新規の 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 は最初の引き数に文書、2 番目の引き数にネームスペース (この例ではネームスペースを使用しないので NULL を渡します)、3 番目の引き数にノードの名前、4 番目の引き数にテキスト内容を指定します。

全個人について、すべてのノードを追加する必要があります。これには GNodes のツリー 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 コードを作成します。これは最初の引き数に親ノード、2 番目の引き数にネームスペース、3 番目の引き数にノードの名前、最後の引き数にテキスト内容を指定します。ここではネームスペースを使用しないため、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);

このノードの "親" (実生活での親) を追加します。親は 2 人だけですが、1 人または親がいない場合もあります。そこで 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 を呼び出します。最初の引き数にファイル名を、2 番目の引き数に文書ポインターを指定します。エラーが発生すると -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 の 2 つのメンバーがあります。これはダイアログのボタンへのウィジェット・ポインターです。通常の GtkButton の場合と同様に、これらに "clicked" シグナルをバインドします。次のようになります。

              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 ハンドラーへのデータとして渡します。2 番目は特殊形式のシグナル接続です。これは gtk_widget_destroy を呼び出しますが、引き数は GTK_OBJECT(fsel) に設定します。このために完全に新規のハンドラー関数を定義する必要がないので便利です。次のようにしてダイアログに必要なセットアップを追加します。

              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 により設定済みであるため、これを行う必要はありません。2 番目の呼び出しは GtkWindows を除いて、基本的には gnome_dialog_set_parent と同じです。この呼び出しはダイアログの親をアプリケーション・ウィンドウとして設定します。これによりウィンドウ・マネージャーは、ダイアログがアプリケーションのダイアログであると識別して適切に処理します。最後の呼び出しは、ダイアログの表示用の呼び出しです。このダイアログはモーダルではなく、メイン・ウィンドウをブロックすることはありません。可能であれば実際にブロックする必要はありません。むしろ、ブロックしないようにすることのほうが、望ましいユーザー・インターフェース・ポリシーです。

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 により新しいダイアログを作成し、ファイル選択ダイアログを親に設定します。また、gtk_window_set_modal に TRUE 引き数を指定して、エラー・ダイアログをモーダルに設定します。これは、ユーザーにダイアログを閉じるか、またはダイアログに応答してから進むようにさせるためです。ここでは 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' は、前回にロードまたは保管されたファイルを保管するグローバル変数です。OK ボタン・ハンドラー save_ok は、open_ok とほぼ同じですが、read_xml_file ではなく write_xml_file を呼び出します。

"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 について紹介します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=290090
ArticleTitle=GNOMEnclature: GNOMEライブラリーによる簡易アプリケーション・プログラミング:第3回
publish-date=12011999