pygtkで遊ぼう(12) gtk.TreeView編

最近飲み会とかが多くて、なかなかサンプルコードをまとめる時間がなかったのですが、今日はたっぷりと時間があったので何とか形になりました。

こんかい作ったサンプルは、
「ファイル選択ダイアログで選択したファイル(複数可)をディレクトリツリーにして表示する」
というもです。

実行させるとこんな感じになります。

ディレクトリツリーといっても簡易的なものなので、ディレクトリが入れ子にはなりません。
まだ存在しないディレクトリは常にトップ階層のRowとして作られます。

ソースコードは以下のようになっています。

# -*- coding: utf-8 -*-

import sys
import os.path

import pygtk
if sys.platform == 'win32':
    pygtk.require('2.0')
import gtk
import gobject


# GTKスレッドの初期化
gtk.gdk.threads_init()

#
#
class FileTreeModel:
    '''
    ツリーデータモデルクラス    
    '''
    def __init__(self, targets=):
        '''
        初期化
        '''
        self.store = gtk.TreeStore(gobject.TYPE_STRING)
        self.set_src_targets(targets)
        
    def set_src_targets(self, targets):
        '''
        ターゲットを指定して、モデルを更新する。
        モデルにターゲットの情報を追加する前に、一旦全てをクリアする
        '''
        if targets == None:
            return

        self.clear_targets()
        self.append_src_targets(targets)

    def append_src_targets(self, srcList):
        '''
        ファイルパスをツリーモデルに追加する。
        '''
        if srcList == None:
            return

        for srcPath in srcList:
            # ディレクトリ名、ファイル名に分割
            srcPath = os.path.normpath(os.path.expanduser(srcPath))

            if os.path.isdir(srcPath):
                # パスがディレクトリパスだったら
                # ディレクトリ以下のファイルリストを取得して
                # 再帰呼び出しを行う。
                fileList = os.listdir(srcPath)
                self.append_src_targets(fileList)

            elif os.path.isfile(srcPath):
                # パスがファイルパスだったら、
                # まずモデル中で追加すべきディレクトリ用Rowのイテレータ
                # を探す。
                dname, fname = os.path.split(srcPath)
                aIter = self.find_iter_dir(self.store.get_iter_first(),
                                           dname)
                if aIter == None:
                    # ディレクトリ用Rowのイテレータが存在しない場合には
                    # トップレベルの階層に新規のRow追加して
                    # その子としてファイル名のRowを追加する。
                    parent = self.store.append(None, (dname,))
                    self.store.append(parent, (fname,))
                    
                else:
                    # ディレクトリ用Rowのイテレータが存在した場合には、
                    # 既にその子供として同名のファイル名が登録されて
                    # いないか調べ、存在しない場合にのみファイル名
                    # の行を追加する。
                    childIter = self.store.iter_children(aIter)
                    while childIter != None:
                        if self.store.get_value(childIter, 0) == fname:
                            break
                        childIter = self.store.iter_next(childIter)
                    else:
                        # while文をブレークせずに通過、つまり
                        # 同名のものが存在しなかった場合にのみ実行される。
                        self.store.append(aIter, (fname,))

    def find_iter_dir(self, aIter, dname):
        '''
        aIterで指定されたのと同じ階層のRowに関して
        dnameで指定された文字列をもつ行を探す。
        存在しない場合にはNoneを返す。
        '''
        if aIter == None:
            return None
        
        if self.store.get_value(aIter, 0) == dname:
            return aIter

        return self.find_iter_dir(self.store.iter_next(aIter),
                                  dname)
            
    def clear_targets(self):
        '''
        保持しているターゲットモデルの内容を全て削除する
        '''
        self.store.clear()
        
    def get_model(self):
        '''
        ツリーモデルを取得する
        '''
        return self.store

#
#
class FileTreeDispModel:
    '''
    ツリーデータモデル表示用クラス
    '''
    def construct_view(self, model):
        '''
        初期化
        '''
        # ツリービューの生成
        tree = gtk.TreeView(model)
        tree.set_rules_hint(True)
        tree.set_enable_tree_lines(True)
        tree.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_VERTICAL)

        # セルレンダラの生成
        render = gtk.CellRendererText()
        col = gtk.TreeViewColumn('File List', render, text=0)
        col.set_resizable(True)
        tree.append_column(col)

        return tree
#
#
class TestWindow:
    '''
    ウインドウクラス。
    '''

    mainMenu = '''
    
        
            
                
                
            
        
    
    '''

    def __init__(self):
        '''
        初期化
        '''
        # データモデルの生成
        self.treeModel = FileTreeModel()

        # データ表示用モデルの作成
        self.dispModel = FileTreeDispModel()

        # ツリービューの作成
        model = self.treeModel.get_model()
        self.treeView = self.dispModel.construct_view(model)
        sel = self.treeView.get_selection()
        if sel:
            sel.set_mode(gtk.SELECTION_MULTIPLE)        

        # 編集ウインドウの作成
        self.wind = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.wind.set_size_request(400, 300)
        self.wind.set_position(gtk.WIN_POS_CENTER)
        self.wind.connect('destroy', self.on_exit)

        # メニューバーの作成
        self.menubar = self.get_main_menubar()
        
        # ツリービューをウインドウ上に配置
        vbox = gtk.VBox()
        vbox.pack_start(self.menubar, False, False, 4)
        
        scrl = gtk.ScrolledWindow()
        scrl.add(self.treeView)
        vbox.pack_start(scrl)
        
        self.wind.add(vbox)        

        # ウインドウの表示
        self.wind.show_all()

    def get_main_menubar(self):
        '''
        '''
        # UIManagerの生成
        self.menuMgr = gtk.UIManager()

        # アクセラレータグループをウインドウに追加
        acclGrp = self.menuMgr.get_accel_group()
        self.wind.add_accel_group(acclGrp)

        # アクショングループの作成
        actGrpBase = gtk.ActionGroup('MenuActionGrpBase')
        actGrpBase.add_actions([('File', None, '_File')])
        self.menuMgr.insert_action_group(actGrpBase, 0)

        # Fileメニューアクションの登録
        actGrpBase.add_actions([('Open...', None,
                                 '_Open...', 'o',
                                 'Open setting file(.xml)',
                                 self.on_menu_file_open),

                                ('Exit', None,
                                 'E_xit', None,
                                 'Exit this tool',
                                 self.on_exit)])

        self.menuMgr.add_ui_from_string(self.mainMenu)
        return self.menuMgr.get_widget('/MenuBar')

    def on_menu_file_open(self, widget, event=None, data=None):
        '''
        メニューのOpenが選択された場合の処理。
        複数のファイルが選択可能なFileChooserDialogを
        表示し、ツリーに追加すべきファイルを選択させる。
        '''
        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                   gtk.STOCK_OK, gtk.RESPONSE_OK)
        
        chooser = gtk.FileChooserDialog('Choose Files',
                                        None,
                                        gtk.FILE_CHOOSER_ACTION_OPEN,
                                        buttons)

        chooser.set_position(gtk.WIN_POS_CENTER)
        chooser.set_select_multiple(True)

        response = chooser.run()
        if response == gtk.RESPONSE_OK:
            files = 
            if chooser.get_select_multiple():
                files = chooser.get_filenames()
            else:
                f = chooser.get_filename()
                files = [f]                
            # ツリーのモデルに選択されたファイルを追加
            self.treeModel.append_src_targets(files)

        chooser.destroy()
        return True

    def on_exit(self, widget, event=None, data=None):
        '''
        終了時の処理。
        GTKのイベントループを終了させる。
        '''
        print '--- quit ---'
        gtk.main_quit()

#
#
def main():
    # パッケージファイル編集ウインドウ表示
    wind = TestWindow()

    # GTKのイベントループ開始
    gtk.gdk.threads_enter()
    gtk.main()
    gtk.gdk.threads_leave()

#
#
if __name__ == '__main__':
    main()

今までも何度かgtk.TreeView()を使ったツールは作ったことがあるんですが、今回初めて(笑)PyGTKのチュートリアルを参考にしてデータモデル、表示用モデルを別々のクラスに分けました。

今回のコードは、前回までのコードよりも長くなってしまいましたが、
上記のソースコード中の赤で書かれている部分が肝で、あとはおまけのようなものだと思います。
赤で書かれている部分は、TreeViewで表示されるデータの内容を作る部分です。

ディレクトリを入れ子で表現するように変更するには、メソッドappend_src_targets(), find_iter_dir()あたり
を変更すればいいと思います。

「ここ、変なんじゃない?」とかありましたら、どしどし突っ込んでくださいまし。

つづく。