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

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

PyGTKでGtkBuilderをレイアウトに用いながらGUI部品のクラスを継承

PyGTKでは、GladeやGtkBuilderといったXML形式のユーザインターフェース定義(ファイルあるいは文字列)を読み込んでGUIのインターフェース部分に使用することができる。これは便利ではあるのだが、GUI部品のクラスを継承してその部品に関する値や処理をまとめて分けておく*1ことができないため、コードの分かりやすさは落ちてしまう。

GtkBuilderのUI記述をコードに組み込んだ*2下のような例を考える。実行にはバージョン2.12以上のPyGTKが必要。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import sys
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import gtk
except:
  print >> sys.stderr, 'Error: PyGTK is not installed'
  sys.exit(1)
if gtk.pygtk_version < (2,12,0):
  errmsg = 'PyGTK >= 2.12.0 required'
  if gtk.pygtk_version < (2,4,0):
    print >> sys.stderr, errmsg
  else:
    errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_CLOSE, message_format=errmsg)
    errdlg.set_title('This program cannot be run')
    errdlg.run()
  sys.exit(1)


class UIDefAndHandlers(gtk.Builder):
  (
    COLUMN_NUM,
    COLUMN_NAME,
  ) = range(2)
  xml = '''
<?xml version="1.0"?>
<interface>
  <object class="GtkUIManager" id="uimanager1">
    <child>
      <object class="GtkActionGroup" id="actiongroup1">
        <child>
          <object class="GtkAction" id="menuitem1">
            <property name="name">menuitem1</property>
            <property name="label" translatable="yes">_File</property>
          </object>
        </child>
        <child>
          <object class="GtkAction" id="imagemenuitem5">
            <property name="stock_id" translatable="yes">gtk-quit</property>
            <property name="name">imagemenuitem5</property>
            <signal handler="on_quit" name="activate"/>
          </object>
        </child>
      </object>
    </child>
    <ui>
      <menubar name="menubar1">
        <menu action="menuitem1">
          <menuitem action="imagemenuitem5"/>
        </menu>
      </menubar>
    </ui>
  </object>
  <object class="GtkWindow" id="window1">
    <property name="width_request">400</property>
    <property name="height_request">300</property>
    <signal handler="on_quit" name="delete_event"/>
    <child>
      <object class="GtkVBox" id="vbox1">
        <property name="visible">True</property>
        <child>
          <object class="GtkMenuBar" constructor="uimanager1" id="menubar1">
            <property name="visible">True</property>
          </object>
          <packing>
            <property name="expand">False</property>
          </packing>
        </child>
        <child>
          <object class="GtkHBox" id="hbox1">
            <property name="visible">True</property>
            <child>
              <object class="GtkScrolledWindow" id="scrolledwindow1">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
                <child>
                  <object class="GtkTreeView" id="treeview1">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="rules_hint">True</property>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="label1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">This is
a
TEST</property>
                <property name="justify">GTK_JUSTIFY_CENTER</property>
              </object>
              <packing>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkStatusbar" id="statusbar1">
            <property name="visible">True</property>
            <property name="spacing">2</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>
'''
  # 便宜上データをここに置くことにする
  data = \
  [
    (1, 'Carrot'),
    (2, 'Sandal'),
    (3, 'Yacht'),
    (4, 'Sesame salt'),
  ]
  def __init__(self):
    gtk.Builder.__init__(self)
  def init_widgets(self):
    self.add_from_string(self.xml)
    self.connect_signals(self)
    self.window = self.get_object('window1')
    # ツリービューの設定
    self.liststore = gtk.ListStore(int, str)
    self.treeview = self.get_object('treeview1')
    self.treeview.set_model(self.liststore)
    # セルレンダラの設定
    self.renderer_num = gtk.CellRendererText()
    self.renderer_num.connect('edited', self.on_text_edited, self.COLUMN_NUM)
    self.renderer_num.set_property('editable', True)
    self.renderer_name = gtk.CellRendererText()
    self.renderer_name.connect('edited', self.on_text_edited, self.COLUMN_NAME)
    self.renderer_name.set_property('editable', True)
    # コラムの設定
    self.col_num = gtk.TreeViewColumn('No.',
                                      self.renderer_num,
                                      text=self.COLUMN_NUM)
    self.col_num.set_max_width(150)
    self.col_num.set_resizable(True)
    self.col_name = gtk.TreeViewColumn('Name',
                                       self.renderer_name,
                                       text=self.COLUMN_NAME)
    # コラムを追加
    self.treeview.append_column(self.col_num)
    self.treeview.append_column(self.col_name)
    # 複数行選択を可能にする(今回の例ではあまり意味はない)
    self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
    # データを追加
    for rec in self.data:
      self.treeview.get_model().append(rec)
    self.window.show_all()
  def on_text_edited(self, widget, path, new_text, col):
    """
    テキスト用セルが編集されたときの処理
    col(ユーザデータ)はコラム番号
    """
    model = self.treeview.get_model()
    if col == self.COLUMN_NUM:
      try:
        model.set_value(model.get_iter(path), col, int(new_text))
      except ValueError:
        pass
    else:
      model.set_value(model.get_iter(path), col, new_text)
  def on_quit(self, widget, event=None):
    """
    終了
    """
    gtk.main_quit()

class GtkBuilderTestWithoutInheritance:
  """
  クラスの継承を用いないGtkBuilderのテスト
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    builder = UIDefAndHandlers()
    builder.init_widgets()
    gtk.main()


if __name__ == '__main__':
  app = GtkBuilderTestWithoutInheritance()
  app.main()

この場合、ツリービューはXMLの中に記述されているため、その設定*3は読み込まれるが、コラムの設定やハンドラなどを別のクラスにまとめることは簡単にはできない。結果、全体として読みづらいコードになってしまっている。

ユーザインターフェース定義から部品を抜いて空白にする

GTK+のユーザインターフェースは、VBoxやHBoxなどのレイアウト用途のコンテナ(入れ物)部品を用いてウィンドウやダイアログを分割してその中に個別の部品を入れていく形となっている。GladeでGladeファイルやGtkBuilderファイルを作成する場合もこの流れで作業を行うのだが、レイアウトの部分だけを作成して個別の部品の入る予定の部分を空にしておいても、後からPythonのコード側からget_object()でこのコンテナ部品を取り出しておけば、別途クラスを継承して作成した部品を子としてその中に入れることができる。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

import sys
try:
  import pygtk
  pygtk.require('2.0')
except:
  pass
try:
  import gtk
except:
  print >> sys.stderr, 'Error: PyGTK is not installed'
  sys.exit(1)
if gtk.pygtk_version < (2,12,0):
  errmsg = 'PyGTK >= 2.12.0 required'
  if gtk.pygtk_version < (2,4,0):
    print >> sys.stderr, errmsg
  else:
    errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK, message_format=errmsg)
    errdlg.set_title('This program cannot be run')
    errdlg.run()
  sys.exit(1)


class TreeViewWithColumn(gtk.TreeView):
  """
  コラムを含んだツリービュー
  クラス継承させるため、GtkBuilderのUI記述には含めていない
  """
  (
    COLUMN_NUM,
    COLUMN_NAME,
  ) = range(2)
  def __init__(self, *args, **kwargs):
    gtk.TreeView.__init__(self, *args, **kwargs)  # 必須
    # セルレンダラの設定
    self.renderer_num = gtk.CellRendererText()
    self.renderer_num.connect('edited', self.on_text_edited, self.COLUMN_NUM)
    self.renderer_num.set_property('editable', True)
    self.renderer_name = gtk.CellRendererText()
    self.renderer_name.connect('edited', self.on_text_edited, self.COLUMN_NAME)
    self.renderer_name.set_property('editable', True)
    # コラムの設定
    self.col_num = gtk.TreeViewColumn('No.',
                                      self.renderer_num,
                                      text=self.COLUMN_NUM)
    self.col_num.set_max_width(150)
    self.col_num.set_resizable(True)
    self.col_name = gtk.TreeViewColumn('Name',
                                       self.renderer_name,
                                       text=self.COLUMN_NAME)
    # コラムを追加
    self.append_column(self.col_num)
    self.append_column(self.col_name)
    # 複数行選択を可能にする(今回の例ではあまり意味はない)
    self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
  def on_text_edited(self, widget, path, new_text, col):
    """
    テキスト用セルが編集されたときの処理
    col(ユーザデータ)はコラム番号
    """
    model = self.get_model()
    if col == self.COLUMN_NUM:
      try:
        model.set_value(model.get_iter(path), col, int(new_text))
      except ValueError:
        pass
    else:
      model.set_value(model.get_iter(path), col, new_text)

class UIDefAndHandlers(gtk.Builder):
  xml = '''
<?xml version="1.0"?>
<interface>
  <object class="GtkUIManager" id="uimanager1">
    <child>
      <object class="GtkActionGroup" id="actiongroup1">
        <child>
          <object class="GtkAction" id="menuitem1">
            <property name="name">menuitem1</property>
            <property name="label" translatable="yes">_File</property>
          </object>
        </child>
        <child>
          <object class="GtkAction" id="imagemenuitem5">
            <property name="stock_id" translatable="yes">gtk-quit</property>
            <property name="name">imagemenuitem5</property>
            <signal handler="on_quit" name="activate"/>
          </object>
        </child>
      </object>
    </child>
    <ui>
      <menubar name="menubar1">
        <menu action="menuitem1">
          <menuitem action="imagemenuitem5"/>
        </menu>
      </menubar>
    </ui>
  </object>
  <object class="GtkWindow" id="window1">
    <property name="width_request">400</property>
    <property name="height_request">300</property>
    <signal handler="on_quit" name="delete_event"/>
    <child>
      <object class="GtkVBox" id="vbox1">
        <property name="visible">True</property>
        <child>
          <object class="GtkMenuBar" constructor="uimanager1" id="menubar1">
            <property name="visible">True</property>
          </object>
          <packing>
            <property name="expand">False</property>
          </packing>
        </child>
        <child>
          <object class="GtkHBox" id="hbox1">
            <property name="visible">True</property>
            <child>
              <object class="GtkScrolledWindow" id="scrolledwindow1">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
                <child>
                  <placeholder/>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="label1">
                <property name="visible">True</property>
                <property name="label" translatable="yes">This is
a
TEST</property>
                <property name="justify">GTK_JUSTIFY_CENTER</property>
              </object>
              <packing>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkStatusbar" id="statusbar1">
            <property name="visible">True</property>
            <property name="spacing">2</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>
'''
  # 便宜上データをここに置くことにする
  data = \
  [
    (1, 'Carrot'),
    (2, 'Sandal'),
    (3, 'Yacht'),
    (4, 'Sesame salt'),
  ]
  def __init__(self):
    gtk.Builder.__init__(self)
  def init_widgets(self):
    self.add_from_string(self.xml)
    self.connect_signals(self)
    self.window = self.get_object('window1')
    self.treeview = TreeViewWithColumn(model=gtk.ListStore(int, str))
    self.treeview.set_rules_hint(True)  # これ(シマシマの設定)は場合によってはクラス内に書いてもOK
    # 入れたいコンテナのオブジェクトを取り出してその中に部品を入れる
    self.get_object('scrolledwindow1').add(self.treeview)
    # データを追加
    for rec in self.data:
      self.treeview.get_model().append(rec)
    self.window.show_all()
  def on_quit(self, widget, event=None):
    """
    終了
    """
    gtk.main_quit()

class GtkBuilderTestWithInheritance:
  """
  クラスの継承を用いるGtkBuilderのテスト
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    builder = UIDefAndHandlers()
    builder.init_widgets()
    gtk.main()


if __name__ == '__main__':
  app = GtkBuilderTestWithInheritance()
  app.main()

この方法の欠点はその部品自体に関する設定やシグナルをXMLの記述から利用できないことだが、XMLの記述は主に(手動でコードを書くと面倒な)レイアウトを行う部分だけに対して行っていると考えれば、コンテナ部品を含めた全ての部品を手動で生成していく場合よりも全体としてのコードは見やすくなると言える。
なお、この手法はPyGTKに限らず、例えばMonoDevelopで統合されているGUIデザイナsteticを用いてGtk#アプリケーションを作成する場合にも使用できる。

コンテナ以外のGUI部品のクラスは全て継承したほうがよいか

もちろん、全ての部品に対して継承を行ってもよいのだが、見栄えと処理を分離することのメリットを最大限活かすことなどを考えると、上の例のようにそのGUI部品に関する値や処理を多く持っていて分離させたいという場合以外はそのまま部品を配置しておいたほうが良さそう。

関連記事:

参考URL:

*1:ただし、クラスの設計図としての性質上、そのクラスから複数のオブジェクト(インスタンス)を作成する場合において、オブジェクトごとに個別の設定を行いたいときにはクラス内には記述できない・作成する数が決まっているのなら更に継承を行って個別に異なる部分を子クラスで設定するということはできる

*2:gtk.Builderオブジェクトメンバ関数add_from_string()で文字列として読み込むことができる・この部分をファイルにコピペして保存してバージョン3.5系以上のGladeで開くと中身を見ることができる

*3:例えば1行ごとのシマシマを付けるかどうか