試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

Vala言語で.ini形式に近い設定ファイルの読み書きを行う

Vala言語では、GLibの機能として用意されているGKeyFileという構文解析/ファイル入出力機能を利用して、Windowsの.iniファイルに近い形式の設定ファイル*1を処理することができる。

使い方

  • 最初にGLib.KeyFileオブジェクトを生成する(コンストラクタ引数はない)
  • GLib.KeyFileオブジェクトに値を設定するにはset_boolean()set_integer()set_double()set_string()といった型ごとの関数をグループ名とキー名、変数を引数として呼ぶ
  • 値が設定済みのGLib.KeyFileオブジェクトからグループ名とキー名により変数の値を取り出したい場合はget_boolean()get_integer()get_double()get_string()といった型ごとの関数を呼び、引数はグループ名とキー名で、戻り値として値が得られる
  • 値が設定済みのGLib.KeyFileオブジェクトの設定をファイルに書き出したい場合、メンバ関数to_data()を用いることで設定ファイルの内容となる文字列データが得られるのでこれをGLib.DataOutputStreamオブジェクトput_string()で書き出す
  • 書き出された設定ファイルの内容を既存のGLib.KeyFileオブジェクトに読み込ませたい場合はメンバ関数load_from_file()を用いる
  • set_comment()では(グループ名とキー名、コメント文字列を指定することで)該当する説明のコメントが書き出せるが、Vala 0.7.4の時点では「error: too few arguments to function ‘g_key_file_set_comment’」となり、C言語のGLib APIの引数の数に合わない*2valac-Cオプションで生成したC言語のコード内のg_key_file_set_comment()の最後に引数NULLを追加すると正常に動作するが、Vala言語で問題が起こらないようにするためにどうすればよいのかは不明。

今回扱った関数の他にもロケールに基づいて値を変える設定を扱ったり*3、複数の値をひとまとめ(リスト)にして扱ったりもできる*4ようになっている。
また、load_from_data_dirs()を用いると、一般ユーザの設定データディレクトリとシステムの設定データディレクトリからファイルを探索して読み込みを行うことができるため、アプリケーションの設定ファイルを起動時に読み込ませるのに使える(ただ、下のテストコードでは使用していない)。
(2009/7/14)load_from_data_dirs()はデータ用ディレクトリ(一般ユーザの場所としては${HOME}/.local/share/以下)から探索するため、設定ファイルの場所を指定するのには使いにくい。
そこで、読み込むファイルの場所を

GLib.Path.build_filename (GLib.Environment.get_user_config_dir (), [ファイル名文字列])

のように指定してメンバ関数load_from_file()を呼ぶようにすると${HOME}/.config/ディレクトリの中に対して設定ファイルを読み書きできる。また、サブディレクトリを作成する場合にもこの要領で中のファイル名を

GLib.Path.build_filename (GLib.Environment.get_user_config_dir (), [アプリケーション名など], [ファイル名文字列])

のようにして決めてload_from_file()を使用するようにすればよい。
なお、このGLib.Path.build_filename()Pythonos.path.join()に相当し、パスの要素を文字列リストとして入力するとOSごとの区切り文字を用いて連結する(結果が戻り値となる)。

GIOのストリームの後始末に関する補足

(2009/7/23)GIOのDataOutputStreamなどのストリームについてはストリームオブジェクトの寿命とともに自動的に解放される(RAIIの設計になっている)ため、特に後始末の類は記述しない。

コード例

下のコードを実行すると幾つかのGUI部品を含んだGTK+のウィンドウが表示され、その状態(値)を「File」メニュー項目でキーファイルに書き出すことができ、書き出されたキーファイルを読み込むとGUI部品に反映する。
[任意]ファイル名: keyfiletest.vala

using GLib;
using Gtk;

/*
 * valac --pkg gtk+-2.0 --pkg gio-2.0 -o keyfiletest keyfiletest.vala
 */

namespace KeyFileTest
{
  class MainWindow : Gtk.Window
  {
    Gtk.AccelGroup accelgroup;
    Gtk.ImageMenuItem item_quit;
    Gtk.ImageMenuItem item_open;
    Gtk.ImageMenuItem item_save;
    Gtk.ImageMenuItem item_saveas;
    Gtk.Menu menu_file;
    Gtk.MenuItem item_file;
    Gtk.MenuBar menubar;
    Gtk.CheckButton checkbtn;
    Gtk.SpinButton spinbtn_int;
    Gtk.SpinButton spinbtn_double;
    Gtk.Entry entry_str;
    Gtk.Label label_bool;
    Gtk.Label label_int;
    Gtk.Label label_double;
    Gtk.Label label_str;
    Gtk.HBox hbox_bool;
    Gtk.HBox hbox_int;
    Gtk.HBox hbox_double;
    Gtk.HBox hbox_str;
    Gtk.VBox vbox;
    GLib.KeyFile keyfile;
    string? outfile = null;  // 「?」付きはnull値をとれることを示す
    public MainWindow ()
    {
      /* ショートカットキー(アクセラレータ) */
      this.accelgroup = new Gtk.AccelGroup ();
      this.add_accel_group (this.accelgroup);
      /* メニュー項目 */
      this.item_quit = new Gtk.ImageMenuItem.from_stock (Gtk.STOCK_QUIT, accelgroup);
      this.item_open = new Gtk.ImageMenuItem.from_stock (Gtk.STOCK_OPEN, accelgroup);
      this.item_save = new Gtk.ImageMenuItem.from_stock (Gtk.STOCK_SAVE, accelgroup);
      this.item_saveas = new Gtk.ImageMenuItem.from_stock (Gtk.STOCK_SAVE_AS, accelgroup);
      this.menu_file = new Gtk.Menu ();
      this.menu_file.prepend (item_quit);
      this.menu_file.prepend (new Gtk.SeparatorMenuItem ());
      this.menu_file.prepend (item_saveas);
      this.menu_file.prepend (item_save);
      this.menu_file.prepend (item_open);
      this.item_file = new Gtk.MenuItem.with_mnemonic ("_File");
      this.item_file.set_submenu (menu_file);
      this.menubar = new Gtk.MenuBar ();
      this.menubar.append (item_file);
      /* GUI部品 */
      this.checkbtn = new Gtk.CheckButton ();
      this.spinbtn_int = new Gtk.SpinButton.with_range (0, 99999, 1);
      this.spinbtn_double = new Gtk.SpinButton.with_range (0, 99999, 0.01);
      this.entry_str = new Gtk.Entry ();
      this.label_bool = new Gtk.Label.with_mnemonic ("_bool:");
      this.label_bool.mnemonic_widget = this.checkbtn;
      this.label_int = new Gtk.Label.with_mnemonic ("_int:");
      this.label_int.mnemonic_widget = this.spinbtn_int;
      this.label_double = new Gtk.Label.with_mnemonic ("_double:");
      this.label_double.mnemonic_widget = this.spinbtn_double;
      this.label_str = new Gtk.Label.with_mnemonic ("_str:");
      this.label_str.mnemonic_widget = this.entry_str;
      this.hbox_bool = new Gtk.HBox (false, 0);
      this.hbox_bool.pack_start (this.label_bool, false, false, 0);
      this.hbox_bool.pack_end (this.checkbtn, false, false, 0);
      this.hbox_int = new Gtk.HBox (false, 0);
      this.hbox_int.pack_start (this.label_int, false, false, 0);
      this.hbox_int.pack_end (this.spinbtn_int, false, false, 0);
      this.hbox_double = new Gtk.HBox (false, 0);
      this.hbox_double.pack_start (this.label_double, false, false, 0);
      this.hbox_double.pack_end (this.spinbtn_double, false, false, 0);
      this.hbox_str = new Gtk.HBox (false, 0);
      this.hbox_str.pack_start (this.label_str, false, false, 0);
      this.hbox_str.pack_end (this.entry_str, false, false, 0);
      this.vbox = new Gtk.VBox (false, 0);
      this.vbox.pack_start (this.menubar, false, false, 0);
      this.vbox.pack_start (this.hbox_bool, false, false, 0);
      this.vbox.pack_start (this.hbox_int, false, false, 0);
      this.vbox.pack_start (this.hbox_double, false, false, 0);
      this.vbox.pack_start (this.hbox_str, false, false, 0);
      this.add (this.vbox);
      /* シグナル */
      this.item_open.activate += () =>
      {
        int resid;
        string infile;
        Gtk.FileChooserDialog chooserdlg = new FileChooserDialog ("Open keyfile", this,  Gtk.FileChooserAction.OPEN, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK);
        chooserdlg.local_only = true;
        resid = chooserdlg.run ();
        infile = chooserdlg.get_filename ();
        chooserdlg.destroy ();
        if (resid == Gtk.ResponseType.OK)
        {
          /* 次に「(上書き)保存」したときに同じファイルに書き出せるようにする */
          this.outfile = infile;
          /* 読み込み */
          this.load_keyfile (infile);
        }
      };
      this.item_save.activate += () =>
      {
        /* 一度書き込んでいればそのファイルに上書き・そうでなければ名前を付けて保存 */
        if (this.outfile != null)
          this.save_keyfile (this.outfile);
        else
          this.item_saveas.activate ();
      };
      this.item_saveas.activate += () =>
      {
        int resid;
        string outfile;
        Gtk.FileChooserDialog chooserdlg = new FileChooserDialog ("Write keyfile", this,  Gtk.FileChooserAction.SAVE, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK);
        chooserdlg.local_only = true;
        chooserdlg.do_overwrite_confirmation = true;
        resid = chooserdlg.run ();
        outfile = chooserdlg.get_filename ();
        chooserdlg.destroy ();
        if (resid == Gtk.ResponseType.OK)
        {
          /* 次に「(上書き)保存」したときに同じファイルに書き出せるようにする */
          this.outfile = outfile;
          /* 書き出し */
          this.save_keyfile (outfile);
        }
      };
      this.item_quit.activate += Gtk.main_quit;
      this.destroy += Gtk.main_quit;
      /* キーファイル */
      this.keyfile = new GLib.KeyFile ();
    }
    void load_keyfile (string infile)
    {
      try
      {
        this.keyfile.load_from_file (infile, GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS);
        /* 値を取得してGUI部品に反映 */
        this.checkbtn.active = this.keyfile.get_boolean ("boolgroup", "boolkey");
        this.spinbtn_int.value = (double) this.keyfile.get_integer ("numgroup", "intkey");  // 型変換はスピンボタンのdoubleに合わせるため
        this.spinbtn_double.value = this.keyfile.get_double ("numgroup", "doublekey");
        this.entry_str.text = this.keyfile.get_string ("strgroup", "strkey");
      }
      catch (GLib.KeyFileError e)
      {
        GLib.warning ("GLib.KeyFileError: %s\n", e.message);
      }
      catch (GLib.FileError e)
      {
        GLib.warning ("GLib.FileError: %s\n", e.message);
      }
    }
    void save_keyfile (string outfile)
    {
      /* Vala 0.7.4の時点ではset_comment()はコンパイルエラーになって使えない? */
//      this.keyfile.set_comment ("boolgroup", "boolkey", "Boolean value(checkbox)");
      this.keyfile.set_boolean ("boolgroup", "boolkey", this.checkbtn.active);
//      this.keyfile.set_comment ("numgroup", "intkey", "Int value(spinbutton)");
      this.keyfile.set_integer ("numgroup", "intkey", (int) this.spinbtn_int.value);
//      this.keyfile.set_comment ("numgroup", "doublekey", "Double value(spinbutton)");
      this.keyfile.set_double ("numgroup", "doublekey", this.spinbtn_double.value);
//      this.keyfile.set_comment ("strgroup", "strkey", "String value(entry)");
      this.keyfile.set_string ("strgroup", "strkey", this.entry_str.text);
      /* 設定を文字列データにして書き出す */
      GLib.File f_out = GLib.File.new_for_path (outfile);
      try
      {
        GLib.DataOutputStream out_stream = new DataOutputStream (f_out.replace (null, false, GLib.FileCreateFlags.NONE, null));
        out_stream.put_string (this.keyfile.to_data (null, null), null);
      }
      catch (GLib.Error e)
      {
        GLib.warning ("GLib.Error: %s", e.message);
      }
    }
  }
  class MainClass
  {
    public static void main (string[] args)
    {
      Gtk.init (ref args);
      MainWindow win = new MainWindow ();
      win.show_all ();
      Gtk.main ();
    }
  }
}


上の画像の状態で書き出されたファイルは下のような内容(説明はここで付けたもの)となる。

[boolgroup]
boolkey=true                   # チェックボックスの状態

[numgroup]
intkey=999                     # 上のスピンボタンの値(int)
doublekey=3.1400000000000001   # 下のスピンボタンの値(double)

[strgroup]
strkey=stringstringstring      # テキストエントリの文字列

この内容のファイルを読み込ませるとGUI部品は上の画像の状態になる。

参考URL:

*1:デスクトップ環境上のデスクトップ/メニュー項目の.desktopファイルもこの形式に含まれる

*2:最後の「GError **error」に相当するものがない

*3:get_locale_string()set_locale_string()

*4:「_list」系メンバ関数