2016/11/30

GtkApplicationクラスとGMenuを用いたGTK+アプリケーションを作る

GUIツールキットGTK+を用いたプログラムを作成する際、従来の方法では

  • GTK+の初期化用関数を呼び出す
  • GtkWindowクラスのウィンドウを作成
  • メインループを開始

という形をとっていたが、GTK+の下位にある “GIO” ライブラリに新しく “アプリケーション” 機能やメニュー処理などが追加されており、GTK+側でもこれらに対応して新しい形のコード記述が行えるようになっている。

  1. 扱い方のメモ
    1. アプリケーションクラスとアプリケーションウィンドウクラス
    2. メニュー
    3. アクション
  2. 使用例
    1. Python(PyGI)
    2. Vala

扱い方のメモ

アプリケーションクラスとアプリケーションウィンドウクラス

  • GIOライブラリに基づいたメニュー項目とその選択時の処理の関連付けを扱える
    • GtkApplicationWindowクラスのウィンドウにGtkApplicationクラスのオブジェクトを関連付けることでメニューを表示することができる
  • プログラムを複数起動したときの処理を行う機能
    • アプリケーションID文字列を指定するとプログラムを複数起動した際に一番最初に起動したプログラム(インスタンス)へ新しい起動を通知して2つ目からのプログラムは即座に終了する
  • GtkApplicationではアプリケーション内の状況によって自動的に特定の名前の関数(PyGIでは “do_” が先頭に付く)が呼ばれる
    • startup: 一番最初に起動したプログラムのみ最初に呼び出される
    • activate: 一番最初に起動したかどうかに関わらず、プログラムが起動した後に呼び出される(ウィンドウ生成処理など)
    • shutdown: 起動された全てのプログラムが終了した後に呼び出される(アプリケーション全体の後始末処理など)
    • 上記の内の幾つかはGtkApplicationクラスを継承した場合に親クラスのメンバ関数を手動で呼び出す処理が必須となる
  • プログラム自体のメイン処理からの使い方としてはGtkApplicationクラスのメンバ関数run()にコマンド行引数の引数を渡す

メニュー

  • メニューは操作の対象の範囲によって2つの種類が存在する
    • ウィンドウごとの伝統的なメニューバー用のメニューとしてset_menubar()によって設定するもの
    • アプリケーション全体のメニューとしてset_app_menu()によって設定するもの(“設定” や “終了” のような項目に向いている)で、デスクトップ環境(やその設定)によってはパネル上のプログラム名部分から開くメニュー項目となる
      • (デスクトップ環境によっては)このメニューのラベル文字列はGLibのset_application_name()で指定したものが使用され、未指定だと日本語ロケールでは “アプリケーション” という名前になる
  • メニューはXML形式で記述でき、GtkBuilderクラスのオブジェクトを用いてプログラムから利用可能な形で扱うことができる
    • XMLの全体の要素がinterface要素(HTMLでのhtml要素に相当)・これはGUI部品などの記述をGtkBuilderのXMLで行う場合と同様
    • 子要素としてmenu要素をメニューの数だけ用意し、取り出す際の識別のためにそれぞれid属性を指定する
    • メニューバーでは “ファイル” “編集” などのサブメニューとなるsubmenu要素をmenuの子要素として記述
      • これらの要素の子としてsection要素を配置し、複数の関係した項目群をグループ化するのに用いる(区切られたセクション間には自動的に境界線が引かれる)
    • 各メニュー項目はitem要素で表現され、その中の各種属性値はこの子としてattribute要素で指定する形をとる(全体の記述は長くなるが、整理された形となって読み書きはしやすい)
      • ラベル(表示文字列)は “label” ,アクション文字列(選択時に呼ばれる関数と別途関連付けるための文字列)は “action” ,ショートカットキーの記述は “accel”, アイコン名(もしくはその場所)は “icon” を値にとったname属性をとる
      • ショートカットキーの内部表現は “<Primary>o” (Ctrl/⌘-o)のようになるが、XML中では不等号は “&lt;” と “&gt;” で記述する必要がある
      • “ファイル” “編集” などのサブメニュー自体のラベルについてはsubmenu要素の直下にattribute要素を記述する
    • GtkBuilderクラスのオブジェクトからget_object()で取り出したメニューは言語によってはそのままset_menubar()set_app_menu()に渡せないため、GMenuModelクラスに対する型変換が必要となる
    • GtkApplicationクラスはGtkBuilderクラスのオブジェクトを作成しなくても特定の場所(外部ファイル)にメニューのXMLファイルを配置することで自動的に読み込まれる仕組みが存在するが、本記事では扱わず、後述の使用例ではGtkBuilderクラスのオブジェクトを用いている
  • GtkApplicationWindowクラスのウィンドウの中に(GtkContainerクラスの)add()でGUI部品を入れると、(GtkWindowとは異なり)ウィンドウ全体に含まれるのではなく、自動的に挿入されるメニューバーの下の領域に描画される
    • 環境や設定によってデスクトップ環境のパネルなどにメニューを表示するようになっている場合はメニューはそちらにのみ表示される)

アクション

  • メニュー項目とそれが選択されたときに呼ばれる関数との関連付けは “アクション” と呼ばれるオブジェクトを介して行われ、直接対応付けられるわけではない
    • メニューのXML内で記述した名前のアクションは別途関数との関連付けをアプリケーション側に渡す必要がある
  • アプリケーションのオブジェクトにアクションを追加するには(GActionMapインターフェースの)add_action_entries()GActionEntryの配列を渡して一括追加するか、手動で用意したアクション(GSimpleAction型のオブジェクトを作成し、 “activate” シグナルについて別途用意したハンドラ関数と関連付ける)をadd_action()で個別に追加する
    • いずれの場合もメニュー項目が選択されたときの処理は引数を2つ(GSimpleAction型とGVariant型)とり戻り値のない関数に記述する
  • set_menubar()で追加したメニュー項目は特定のウィンドウに対してのみ行う処理となるため、関数内でget_active_window()を呼び出してアクティブなウィンドウを取得してメニュー項目が選択されたウィンドウを特定し、これに対して(ウィンドウのクラスを継承したクラスの)メンバ関数を呼び出すような形でウィンドウに対して処理を行う
    • get_active_window()GtkWindowクラスで返すため、言語によってはメンバ関数を呼び出すために型変換を行う必要がある

使用例

使用例のスクリーンショット

Python(PyGI)

[任意]ファイル名:gtkapplicationtest.py
#! /usr/bin/python

from __future__ import print_function

try:
  import gi
except ImportError:
  sys.exit ('PyGI not installed')
try:
  gi.require_version ('Gtk', '3.0')
  from gi.repository import Gtk, GLib, Gio
except ValueError:
  sys.exit ('typelib for GTK+ 3 not found')
except ImportError:
  sys.exit ('Failed to load GTK+')
import sys


class MainWindow (Gtk.ApplicationWindow):
  _num_window = 0
  def __init__ (self, application, n):
    Gtk.ApplicationWindow.__init__ (self, application = application)
    self.add (Gtk.Label ('Hello, GtkApplication !!'))
    self.set_default_size (400, 300)
    self.props.title = 'Window {0}'.format (n)
    self._num_window = n
  def open_file (self):
    print ('open_file (Window {0})'.format (self._num_window))
  def save_file (self):
    print ('save_file (Window {0})'.format (self._num_window))

class GtkApplicationTest (Gtk.Application):
  _cnt_windows = 0
  def __init__ (self):
    GLib.set_application_name ('GtkApplicationTest')
    Gtk.Application.__init__ (self, application_id = 'com.blogspot.kakurasan.gtkapplicationtest')
  def _action_new_activate_cb (self, act, param):
    self.activate ()
  def _action_open_activate_cb (self, act, param):
    self.get_active_window ().open_file ()
  def _action_save_activate_cb (self, act, param):
    self.get_active_window ().save_file ()
  def _action_quit_activate_cb (self, act, param):
    print ('quit')
    self.quit ()
  def do_activate (self):
    print ('activate')
    self._cnt_windows += 1
    MainWindow (self, self._cnt_windows).show_all ()
  def do_startup (self):
    menu_ui = '''
<interface>
 <menu id="menubar">
  <submenu>
   <attribute name="label" translatable="yes">_File</attribute>
   <section>
    <item>
     <attribute name="label" translatable="yes">_Open</attribute>
     <attribute name="action">app.open</attribute>
     <attribute name="accel">&lt;Primary&gt;o</attribute>
     <attribute name="icon">gtk-open</attribute>
    </item>
    <item>
     <attribute name="label" translatable="yes">_Save</attribute>
     <attribute name="action">app.save</attribute>
     <attribute name="accel">&lt;Primary&gt;s</attribute>
     <attribute name="icon">gtk-save</attribute>
    </item>
   </section>
  </submenu>
 </menu>
 <menu id="appmenu">
  <section>
   <item>
    <attribute name="label" translatable="yes">_New</attribute>
    <attribute name="action">app.new</attribute>
    <attribute name="accel">&lt;Primary&gt;n</attribute>
    <attribute name="icon">gtk-new</attribute>
   </item>
   <item>
    <attribute name="label" translatable="yes">_Quit</attribute>
    <attribute name="action">app.quit</attribute>
    <attribute name="accel">&lt;Primary&gt;q</attribute>
    <attribute name="icon">gtk-quit</attribute>
   </item>
  </section>
 </menu>
</interface>
'''
    action_entries = (('new', self._action_new_activate_cb, None, None, None),
                      ('open', self._action_open_activate_cb, None, None, None),
                      ('save', self._action_save_activate_cb, None, None, None),
                      ('quit', self._action_quit_activate_cb, None, None, None))
    print ('startup')
    Gtk.Application.do_startup (self)
    for name, activate, parameter_type, state, change_state in action_entries:
      act = Gio.SimpleAction (name = name, state = state, parameter_type = parameter_type)
      if activate:
        act.connect ('activate', activate)
      if change_state:
        act.connect ('change_state', change_state)
      self.add_action (act)
    builder_menu = Gtk.Builder.new_from_string (menu_ui, -1)
    self.set_menubar (builder_menu.get_object ('menubar'))
    self.set_app_menu (builder_menu.get_object ('appmenu'))
  def do_shutdown (self):
    print ('shutdown');
    Gtk.Application.do_shutdown (self)


if __name__ == '__main__':
  sys.exit (GtkApplicationTest ().run (sys.argv))

Vala

[任意]ファイル名:gtkapplicationtest.vala
// valac --pkg gtk+-3.0 gtkapplicationtest.vala -o gtkapplicationtest

using Gtk;


namespace GtkApplicationTest
{
  class MainWindow : Gtk.ApplicationWindow
  {
    int num_window = 0;
    public
    MainWindow (Gtk.Application application, int n)
    {
      GLib.Object (application: application);
      this.add (new Gtk.Label ("Hello, GtkApplication !!"));
      this.set_default_size (400, 300);
      this.title = "Window %d".printf (n);
      this.num_window = n;
    }
    public void
    open_file ()
    {
      print ("open_file (Window %d)\n", this.num_window);
    }
    public void
    save_file ()
    {
      print ("save_file (Window %d)\n", this.num_window);
    }
  }
  class GtkApplicationTest : Gtk.Application
  {
    int cnt_windows = 0;
    const GLib.ActionEntry[] action_entries =
    {
      {"new", action_new_activate_cb, null, null, null},
      {"open", action_open_activate_cb, null, null, null},
      {"save", action_save_activate_cb, null, null, null},
      {"quit", action_quit_activate_cb, null, null, null},
    };
    public
    GtkApplicationTest ()
    {
      GLib.Environment.set_application_name ("GtkApplicationTest");
      GLib.Object (application_id: "com.blogspot.kakurasan.gtkapplicationtest");
    }
    void
    action_new_activate_cb (GLib.SimpleAction act, GLib.Variant? param)
    {
      this.activate ();
    }
    void
    action_open_activate_cb (GLib.SimpleAction act, GLib.Variant? param)
    {
      ((MainWindow) (this.get_active_window ())).open_file ();
    }
    void
    action_save_activate_cb (GLib.SimpleAction act, GLib.Variant? param)
    {
      ((MainWindow) (this.get_active_window ())).save_file ();
    }
    void
    action_quit_activate_cb (GLib.SimpleAction act, GLib.Variant? param)
    {
      print ("quit\n");
      this.quit ();
    }
    protected override void
    activate ()
    {
      print ("activate\n");
      new MainWindow (this, ++cnt_windows).show_all ();
    }
    protected override void
    startup ()
    {
      const string menu_ui = """
<interface>
 <menu id="menubar">
  <submenu>
   <attribute name="label" translatable="yes">_File</attribute>
   <section>
    <item>
     <attribute name="label" translatable="yes">_Open</attribute>
     <attribute name="action">app.open</attribute>
     <attribute name="accel">&lt;Primary&gt;o</attribute>
     <attribute name="icon">gtk-open</attribute>
    </item>
    <item>
     <attribute name="label" translatable="yes">_Save</attribute>
     <attribute name="action">app.save</attribute>
     <attribute name="accel">&lt;Primary&gt;s</attribute>
     <attribute name="icon">gtk-save</attribute>
    </item>
   </section>
  </submenu>
 </menu>
 <menu id="appmenu">
  <section>
   <item>
    <attribute name="label" translatable="yes">_New</attribute>
    <attribute name="action">app.new</attribute>
    <attribute name="accel">&lt;Primary&gt;n</attribute>
    <attribute name="icon">gtk-new</attribute>
   </item>
   <item>
    <attribute name="label" translatable="yes">_Quit</attribute>
    <attribute name="action">app.quit</attribute>
    <attribute name="accel">&lt;Primary&gt;q</attribute>
    <attribute name="icon">gtk-quit</attribute>
   </item>
  </section>
 </menu>
</interface>
""";
      print ("startup\n");
      base.startup ();
      this.add_action_entries (GtkApplicationTest.action_entries, this);
      var builder_menu = new Gtk.Builder.from_string (menu_ui, -1);
      this.set_menubar ((GLib.MenuModel) (builder_menu.get_object ("menubar")));
      this.set_app_menu ((GLib.MenuModel) (builder_menu.get_object ("appmenu")));
    }
    protected override void
    shutdown ()
    {
      print ("shutdown\n");
      base.shutdown ();
    }
  }
  int
  main (string[] args)
  {
    return new GtkApplicationTest ().run (args);
  }
}
使用したバージョン:
  • GTK+ 3.20.9
  • GLib 2.50.0
  • Python 2.7.12, 3.5.2
  • Vala 0.32.1