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

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

PyGTKでツリービューにリストのデータ(ListStore)を表示(項目をGUI上で追加/削除、データの並べ替えや入れ替えなど)

PyGTKにおけるツリービューに関してこれまでに幾つかの例とメモを書いてきたが、データの追加に関してはあらかじめ用意しておいたものを流し込んでいただけで、削除もできなかった。そこで今回は項目をGUI上で追加/削除できるようにした。
また、同時に

  • 項目の選択について複数の選択が可能に
  • コラムを左右ドラッグ、リスト項目(データ)を上下ドラッグで移動可能

という修正もした。

メモ

  • ツリービュー項目の複数選択を可能にするにはgtk.TreeViewオブジェクトのget_selection()で得られるgtk.TreeSelectionオブジェクトのメンバ関数set_mode()でgtk.SELECTION_MULTIPLEを指定する
  • コラムについて値による並べ替えを可能にするにはgtk.TreeViewColumnオブジェクトのメンバ関数set_sort_column_id()でコラムの番号を指定
  • コラムの左右ドラッグによる入れ替えを可能にするには同オブジェクトのset_reorderable()でTrueを指定
  • ツリービュー項目を上下ドラッグで移動可能にするにはgtk.TreeViewオブジェクトのメンバ関数set_reorderable()にTrueを指定
  • データの追加については「GUI部品から値の取得を行いgtk.ListStoreオブジェクトのappend()で追加」という流れとなり、値の取得に使用するメンバ関数はオブジェクト(部品)ごとに異なる
  • 性別のアイコンについてはコンボボックスの値を判別後それに合った画像データ(gtk.gdk.Pixbufオブジェクト)を渡すようにする
  • データの一部削除についてはgtk.ListStoreオブジェクトのメンバ関数remove()によるが、gtk.TreeSelectionオブジェクトのget_selected()(単独選択モード時)かget_selected_rows()(複数選択モード時)から得られるgtk.TreeIterオブジェクトを用いて項目を指定する
  • ListStore/TreeStore内のデータの一括削除はそれぞれのメンバ関数clear()で行う
  • get_selected_rows()を使用して複数のgtk.TreeIterオブジェクトを得るには「iters = [model.get_iter(path) for path in selected]」のような書き方が便利(あとはfor文で各オブジェクトについて処理すればOK)

コード例

「PyGTKでツリービューにリストのデータ(ListStore)を表示(データを変更可能にする・コード例)」のsexmark.pyが別途必要。
例によって、この中で使用した名前は架空のものであり、実在の個人名や団体名などと一致するものがあったとしても関係はない。

#! /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)
try:
  import sexmark
except:
  print >> sys.stderr, 'Error: sexmark.py required'
  sys.exit(1)


class TreeViewWithColumn(gtk.TreeView):
  """
  コラムを含んだツリービュー
  """
  (
    COLUMN_NUM,
    COLUMN_SEX,
    COLUMN_FAMILY,
    COLUMN_GIVEN,
  ) = range(4)
  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_family = gtk.CellRendererText()
    self.renderer_family.connect('edited', self.on_text_edited, self.COLUMN_FAMILY)
    self.renderer_family.set_property('editable', True)
    self.renderer_given = gtk.CellRendererText()
    self.renderer_given.connect('edited', self.on_text_edited, self.COLUMN_GIVEN)
    self.renderer_given.set_property('editable', True)
    # コラムの設定
    self.col_num = gtk.TreeViewColumn('No.',
                                      self.renderer_num,
                                      text=self.COLUMN_NUM)
    self.col_num.set_sort_column_id(self.COLUMN_NUM)  # 並べ替え
    self.col_num.set_reorderable(True)  # 左右のドラッグでコラム入れ替え
    self.col_sex = gtk.TreeViewColumn('Sex',
                                      gtk.CellRendererPixbuf(),
                                      pixbuf=self.COLUMN_SEX)
    self.col_sex.set_reorderable(True)
    self.col_family = gtk.TreeViewColumn('Family name',
                                         self.renderer_family,
                                         text=self.COLUMN_FAMILY)
    self.col_family.set_sort_column_id(self.COLUMN_FAMILY)
    self.col_family.set_reorderable(True)
    self.col_given = gtk.TreeViewColumn('Given name',
                                        self.renderer_given,
                                        text=self.COLUMN_GIVEN)
    self.col_given.set_sort_column_id(self.COLUMN_GIVEN)
    self.col_given.set_reorderable(True)
    # コラムを追加
    self.append_column(self.col_num)
    self.append_column(self.col_sex)
    self.append_column(self.col_family)
    self.append_column(self.col_given)
    # 複数行選択を可能にする
    self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
    # コンテキストメニュー
    self.item_changesex = gtk.MenuItem('_Change Sex')
    self.menu_popup = gtk.Menu()
    self.menu_popup.add(self.item_changesex)
    self.menu_popup.show_all()
    self.item_changesex.connect('activate', self.on_changesex_activated)
    self.connect('button_press_event', self.on_button_press_event)
  def on_text_edited(self, widget, path, new_text, col):
    """
    テキスト用セルが編集されたときの処理
    col(ユーザデータ)はコラム番号
    """
    # get_model()でツリービューに関連付けられたデータを取り出して
    # 引数の情報とあわせてデータの値を変更する
    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)
  def on_button_press_event(self, widget, event):
    """
    ツリービュー上でマウスボタンが押されたときの処理
    """
    if event.button == 3:
      # データがない部分でコンテキストメニューが開けないようにする
      # (本スクリプトの場合、データの長さを数人分にしないと効果は確認できない)
      # get_path_at_pos()はTreeView内の座標から項目のパスを取得
      path_at_pos = self.get_path_at_pos(int(event.x), int(event.y))
      # クリックした場所に項目があるのが確認できた場合のみポップアップする
      if path_at_pos:  # Noneの場合がある
        self.menu_popup.popup(None, None, None, event.button, event.time)
  def on_changesex_activated(self, widget):
    """
    ツリービュー上のコンテキストメニュー項目を選択したときの処理
    性別のアイコンを変更
    """
    # 現在選択されている項目を取り出す
    # get_selected()は複数選択可モードでは使用できない
    # get_selected_rows()を用いた処理の仕方については
    # PyGTK FAQの「How do I delete multiple selections?」も参照
    (model, selected) = self.get_selection().get_selected_rows()
    iters = [model.get_iter(path) for path in selected]
    for iter in iters:
      # 現在値がmaleであればfemaleに、femaleならmaleに変更
      if model.get_value(iter, self.COLUMN_SEX) == SexIcon.male:
        model.set_value(iter, self.COLUMN_SEX, SexIcon.female)
      else:
        model.set_value(iter, self.COLUMN_SEX, SexIcon.male)

class SexIcon:
  """
  性別のアイコンデータのPixbufを保持
  各データは「[本クラス名].[メンバ変数]」で取り出す
  """
  male = gtk.gdk.pixbuf_new_from_xpm_data(sexmark.male_icon_xpm)
  female = gtk.gdk.pixbuf_new_from_xpm_data(sexmark.female_icon_xpm)

class MainWindow(gtk.Window):
  """
  メインウィンドウ
  """
  # 直接ウィンドウとは関係ないが、データは便宜上ここに用意しておくことにする
  data = \
  [
    (1, SexIcon.male, 'Tanaka', 'Ichiro'),
    (2, SexIcon.female, 'Yamana', 'Hanako'),
    (3, SexIcon.male, 'Urashima', 'Saburo'),
    (4, SexIcon.male, 'Kurusu', 'Santa'),
    (5, SexIcon.male, 'Handa', 'Fuuta'),
    (6, SexIcon.female, 'Umeno', 'Tsubomi'),
    (7, SexIcon.male, 'Yoshi', 'Yaruzo'),
    (8, SexIcon.female, 'Kawai', 'Nuko'),
    (9, SexIcon.male, 'Hoshi', 'Kintaro'),
    (10, SexIcon.female, 'Shirayuki', 'Himeko'),
    (11, SexIcon.female, 'Ashigaka', 'Yui'),
    (12, SexIcon.female, 'Ageyanagi', 'Masako'),
    (13, SexIcon.male, 'Torino', 'Kenta'),
    (14, SexIcon.male, 'Kubota', 'Mochio'),
    (15, SexIcon.female, 'Kuroi', 'Sora'),
    (16, SexIcon.male, 'Hirai', 'Shin'),
    (17, SexIcon.female, 'Akai', 'Midori'),
    (18, SexIcon.female, 'Nakano', 'Anko'),
    (19, SexIcon.male, 'Imai', 'Takeo'),
    (20, SexIcon.male, 'Kouno', 'Torio'),
    (21, SexIcon.male, 'Yoshino', 'Yasu'),
    (22, SexIcon.male, 'Komatsu', 'Taro'),
    (23, SexIcon.male, 'Kondo', 'Musashi'),
    (24, SexIcon.male, 'Ono', 'Ken'),
    (25, SexIcon.male, 'Mochida', 'Usuichi'),
    (26, SexIcon.female, 'Mochida', 'Kineko'),
    (27, SexIcon.female, 'Honma', 'Kayo'),
    (28, SexIcon.male, 'Matsuno', 'Sarunosuke'),
    (29, SexIcon.female, 'Nishi', 'Minami'),
    (30, SexIcon.female, 'Usui', 'Hikaru'),
    (31, SexIcon.male, 'Sato', 'Toshio'),
    (32, SexIcon.male, 'Doi', 'Tsubasa'),
    (33, SexIcon.female, 'Ishimaru', 'Denko'),
    (34, SexIcon.female, 'Usami', 'Mimi'),
    (35, SexIcon.male, 'Hattori', 'Shinobu'),
    (36, SexIcon.female, 'Kago', 'Yuri'),
    (37, SexIcon.male, 'Takeda', 'Ingen'),
    (38, SexIcon.male, 'Kai', 'Dankichi'),
    (39, SexIcon.male, 'Okusa', 'Ben'),
    (40, SexIcon.male, 'Hara', 'Tatsuo'),
    (41, SexIcon.female, 'Mizuno', 'Shizuku'),
    (42, SexIcon.female, 'Baba', 'Nana'),
  ]
  def __init__(self, *args, **kwargs):
    gtk.Window.__init__(self, *args, **kwargs)
    # ショートカットキー(アクセラレータ)
    self.accelgroup = gtk.AccelGroup()
    self.add_accel_group(self.accelgroup)
    # メニュー項目
    self.item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accelgroup)
    self.menu_file = gtk.Menu()
    self.menu_file.add(self.item_quit)
    self.item_file = gtk.MenuItem('_File')
    self.item_file.set_submenu(self.menu_file)
    self.menubar = gtk.MenuBar()
    self.menubar.append(self.item_file)
    # データ追加用フォーム
    self.label_num = gtk.Label('No.:')
    self.label_sex = gtk.Label('Sex:')
    self.label_familyname = gtk.Label('Family name:')
    self.label_givenname = gtk.Label('Given name:')
    self.spinbtn_num = gtk.SpinButton(gtk.Adjustment(1, 1, 999, 1, 0, 0))
    self.combo_sex = gtk.combo_box_new_text()
    self.combo_sex.append_text('Male')
    self.combo_sex.append_text('Female')
    self.combo_sex.set_active(0)  # 0番(male)を既定の項目にする
    self.entry_familyname = gtk.Entry()
    self.entry_givenname = gtk.Entry()
    self.button_add = gtk.Button(stock=gtk.STOCK_ADD)
    self.button_delete = gtk.Button(stock=gtk.STOCK_DELETE)
    self.button_clear = gtk.Button(stock=gtk.STOCK_CLEAR)
    # ツリービュー
    self.treeview = TreeViewWithColumn(model=gtk.ListStore(int, gtk.gdk.Pixbuf, str, str))
    self.treeview.set_rules_hint(True)
    self.treeview.set_reorderable(True)  # 上下ドラッグで移動できるようにする
    # ツリービュー向けスクロールウィンドウ
    self.sw = gtk.ScrolledWindow()
    self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
    self.sw.add(self.treeview)
    # レイアウト用コンテナ
    self.hbox_num = gtk.HBox()
    self.hbox_num.pack_start(self.label_num, expand=False, fill=False)
    self.hbox_num.pack_end(self.spinbtn_num, expand=False, fill=False)
    self.hbox_sex = gtk.HBox()
    self.hbox_sex.pack_start(self.label_sex, expand=False, fill=False)
    self.hbox_sex.pack_end(self.combo_sex, expand=False, fill=False)
    self.hbox_familyname = gtk.HBox()
    self.hbox_familyname.pack_start(self.label_familyname, expand=False, fill=False)
    self.hbox_familyname.pack_end(self.entry_familyname, expand=False, fill=False)
    self.hbox_givenname = gtk.HBox()
    self.hbox_givenname.pack_start(self.label_givenname, expand=False, fill=False)
    self.hbox_givenname.pack_end(self.entry_givenname, expand=False, fill=False)
    self.vbox_input = gtk.VBox()  # 入力フォームの各水平ボックスを縦に並べる
    self.vbox_input.pack_start(self.hbox_num, expand=False, fill=False)
    self.vbox_input.pack_start(self.hbox_sex, expand=False, fill=False)
    self.vbox_input.pack_start(self.hbox_familyname, expand=False, fill=False)
    self.vbox_input.pack_start(self.hbox_givenname, expand=False, fill=False)
    self.vbox_buttons = gtk.VBox()  # ボタンを縦に並べる
    self.vbox_buttons.pack_start(self.button_add)
    self.vbox_buttons.pack_start(self.button_delete)
    self.vbox_buttons.pack_start(self.button_clear)
    self.hbox = gtk.HBox()  # 2つの大きな横方向のまとまり
    self.hbox.pack_start(self.vbox_input)
    self.hbox.pack_start(self.vbox_buttons)
    self.vbox = gtk.VBox()  # ウィンドウ全体を縦に分割したもの
    self.vbox.pack_start(self.menubar, expand=False, fill=False)
    self.vbox.pack_start(self.hbox, expand=False, fill=False)
    self.vbox.pack_start(self.sw)
    # シグナル
    self.connect('delete_event', gtk.main_quit)
    self.item_quit.connect('activate', gtk.main_quit)
    self.button_add.connect('clicked', self.on_add_clicked)
    self.button_delete.connect('clicked', self.on_delete_clicked)
    self.button_clear.connect('clicked', self.on_clear_clicked)
    # データ追加
    for rec in self.data:
      self.treeview.get_model().append(rec)
    # ウィンドウ
    self.add(self.vbox)
    self.set_size_request(350, 300)
  def on_add_clicked(self, widget):
    """
    モデルにデータを追加
    """
    num = int(self.spinbtn_num.get_value())
    familyname = self.entry_familyname.get_text()
    givenname = self.entry_givenname.get_text()
    # 空の項目があると追加しないことにする
    if num == '' or familyname == '' or givenname == '':
      return
    # テキスト入力欄を空にする
    self.entry_familyname.set_text('')
    self.entry_givenname.set_text('')
    # 性別は部品が返す番号によってデータを作成する
    if self.combo_sex.get_active() == 0:
      sex = SexIcon.male
    else:
      sex = SexIcon.female
    # タプルを渡してデータ追加
    self.treeview.get_model().append((num, sex, familyname, givenname))
  def on_delete_clicked(self, widget):
    """
    選択された複数の項目をモデルから削除
    """
    (model, selected) = self.treeview.get_selection().get_selected_rows()
    iters = [model.get_iter(path) for path in selected]
    for iter in iters:
      model.remove(iter)
  def on_clear_clicked(self, widget):
    """
    モデルのデータを全て消す
    """
    self.treeview.get_model().clear()

class PyGTKTreeViewListStoreTest3:
  """
  リストを用いたツリービューのテスト3
  """
  def main(self):
    """
    アプリケーションのメイン処理
    """
    win = MainWindow()
    win.show_all()
    gtk.main()


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


追加するデータを入力

データ追加後

コラムのドラッグによる移動(順番入れ替え)

項目のドラッグによる移動

関連記事:

参考URL: