naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Tkinterで、ディレクトリツリー

プログラミング関連 Python Tkinter 約6日前
2017年10月12日0:30
今回はttk.TreeViewを使って、ディレクトリツリーを作成していきます。

スタックオーバーフローのものを参考に作りました。
https://stackoverflow.com/questions/16746387/tkinter-treeview-widget

起動するとこんな画面。


ディレクトリをクリックで、中身が展開されます。



ファイルをクリックすると、ファイルの絶対パスを取得します。
/home/narito/src/python/main.py


一度にディレクトリ・ファイルを全て取得するのではなく、ディレクトリをクリックした時だけ中のディレクトリ・ファイルを取得しにいく作りにしています。なので、パフォーマンス的にも安心な作りです。

全コード
import os
import tkinter as tk
import tkinter.ttk as ttk
from tkinter import filedialog


class PathTreeFrame(ttk.Frame):
    """ディレクトリ・ファイルツリーを表示するFrame."""

    def __init__(self, master, path=os.curdir):
        """初期化

        args:
            master: 親ウィジェット
            path: どのパスを起点にツリーを作るか。デフォルトはカレント

        """
        super().__init__(master)
        self.root_path = os.path.abspath(path)
        self.nodes = {}
        self.create_widgets()

    def create_widgets(self):
        """ウィジェットの作成"""
        # ツリービューの作成とスクロール設定
        self.tree = ttk.Treeview(self)
        ysb = ttk.Scrollbar(
            self, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscroll=ysb.set)

        # レイアウト。スクロールバーは拡大させない
        self.tree.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        ysb.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        # ディレクトリを開いた際と、ダブルクリック(ファイル選択)を関連付け
        self.tree.bind('<<TreeviewOpen>>', self.open_node)
        self.tree.bind('<Double-1>', self.choose_file)

        # ルートのパスを挿入
        self.insert_node('', self.root_path, self.root_path)

    def insert_node(self, parent, text, abspath):
        """Treeviewにノードを追加する

        args:
            parent: 親ノード
            text: 表示するパス名
            abspath: 絶対パス

        """
        # まずノードを追加する
        node = self.tree.insert(parent, 'end', text=text, open=False)

        # ディレクトリならば、空の子要素を追加し開けるようにしておく
        if os.path.isdir(abspath):
            self.tree.insert(node, 'end')
            self.nodes[node] = (False, abspath)
        else:
            self.nodes[node] = (True, abspath)

    def open_node(self, event):
        """ディレクトリを開いた際に呼び出される

        self.nodes[node][0]がFalseの場合はまだ開かれたことがないと判断し、
        そのディレクトリ内のパスを追加する
        一度開いたか、又はファイルの場合はself.nodes[node][0]はTrueになります

        """
        node = self.tree.focus()
        already_open, abspath = self.nodes[node]

        # まだ開かれたことのないディレクトリならば
        if not already_open:

            # 空白の要素が追加されているので、消去
            self.tree.delete(self.tree.get_children(node))

            # ディレクトリ内の全てのファイル・ディレクトリを取得し、Treeviewに追加
            for entry in os.scandir(abspath):
                self.insert_node(
                    node, entry.name, os.path.join(abspath, entry.path)
                )

            # 一度開いたディレクトリはTrueにする
            self.nodes[node] = (True, abspath)

    def choose_file(self, event):
        """ツリーをダブルクリックで呼ばれる"""
        node = self.tree.focus()
        # ツリーのノード自体をダブルクリックしているか?
        if node:
            already_open, abspath = self.nodes[node]
            if os.path.isfile(abspath):
                print(abspath)

    def update_dir(self, event=None):
        """ツリーの一覧を更新する"""
        self.create_widgets()

    def change_dir(self, event=None):
        """ツリーのルートディレクトリを変更する"""
        dir_name = filedialog.askdirectory()
        if dir_name:
            self.root_path = dir_name
            self.create_widgets()


if __name__ == '__main__':
    root = tk.Tk()
    root.title('Path Tree')
    app = PathTreeFrame(root)
    app.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))
    root.bind('<F4>', app.change_dir)
    root.bind('<F5>', app.update_dir)
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)
    root.mainloop()




F4でルートディレクトリの変更と、F5で更新処理を呼び出します。
columnconfigure, rowconfigureを設定し、今回は拡大にも対応するようにしています。
if __name__ == '__main__':
    root = tk.Tk()
    root.title('Path Tree')
    app = PathTreeFrame(root)
    app.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))
    root.bind('<F4>', app.change_dir)
    root.bind('<F5>', app.update_dir)
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)
    root.mainloop()




クラスの中身を見ていきます。
__init__内では、インスタンス変数を2つ作り、ウィジェットの作成処理を呼んでいます。
path引数は起点となるディレクトリのパスで、デフォルトはカレントです。こいつの絶対パスを、root_pathとして格納しておきます。
    def __init__(self, master, path=os.curdir):
        """初期化

        args:
            master: 親ウィジェット
            path: どのパスを起点にツリーを作るか。デフォルトはカレント

        """
        super().__init__(master)
        self.root_path = os.path.abspath(path)
        self.nodes = {}
        self.create_widgets()



nodesという辞書には、以下のように格納されていきます。
keyのI001は各ノードのIDのようなものです。
valueとなる(False, path_name)は後で使います。
{
    'I001': (False, '/home/narito/src/python'),
    ...
    ...
}



次にウィジェットを作成するcreate_widgetsメソッドです。
    def create_widgets(self):
        """ウィジェットの作成"""



ttk.TreeViewとスクロールバーを作成します。ysbが、y_scroll_bar の頭文字です。
この辺はウィジェットをスクロール可能にするための、お約束ですね。
        # ツリービューの作成とスクロール設定
        self.tree = ttk.Treeview(self)
        ysb = ttk.Scrollbar(
            self, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscroll=ysb.set)



1列目にツリーを、2列目にスクロールバーを配置します。
スクロールバーは拡大しても大きくする必要がないので、columnconfigure(1, weight=1)のような記述は必要ないです。
        # レイアウト。スクロールバーは拡大させない
        self.tree.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        ysb.grid(row=0, column=1, sticky=(tk.N, tk.S))
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)



TreeViewOpenイベントは、今回の例だとディレクトリをクリックで呼び出されます。
Double-1は、ダブルクリックです。ファイルをダブルクリック時に呼び出されたいですが...
実際はディレクトリやTreeView部分のダブルクリックでも呼ばれるので、それの判別処理もchoose_file内でしています。
        # ディレクトリを開いた際と、ダブルクリック(ファイル選択)を関連付け
        self.tree.bind('<<TreeviewOpen>>', self.open_node)
        self.tree.bind('<Double-1>', self.choose_file)



TreeView自体は、.insertでノードを追加できます。ただ、今回ならば各ディレクトリを起点に中身を全てinsertする必要があるので、insert_nodeという汎用的なメソッドを定義しています。
root_pathが/home/narito ならば、/home/narito内の全てのディレクトリ・ファイルが追加されます。
        # ルートのパスを挿入
        self.insert_node('', self.root_path, self.root_path)


そのinsert_nodeを見ていきます
    def insert_node(self, parent, text, abspath):
        """Treeviewにノードを追加する

        args:
            parent: 親ノード
            text: 表示するパス名
            abspath: 絶対パス

        """


insertで、ノードを追加します。第一引数、parentは、''(空文字)ならば最上位に、他の親ノードを指定すればそれの子として作成されます。
第二引数、'end'ならば末尾に、0なら最初に挿入されます。
textには、表示させる名前を指定します。
今回はopen=Falseと指定し、まだ開いていない状態にしておきます。
insertの返り値として、要素の識別子が入ります。各ノードのID的なものです。
        # まずノードを追加する
        node = self.tree.insert(parent, 'end', text=text, open=False)



ディレクトリならば、クリックで展開できるようにしなくてはなりません。
なので、空の子要素をinsertし、開ける状態にしておきます。子要素がないと「+」という開くボタンができません。
インスタンス変数nodesに格納するのですが、ディレクトリならば(False, abspath)のように格納します。
        # ディレクトリならば、空の子要素を追加し開けるようにしておく
        if os.path.isdir(abspath):
            self.tree.insert(node, 'end')
            self.nodes[node] = (False, abspath)
        else:
            self.nodes[node] = (True, abspath)



ディレクトリを開くと、このメソッドです。
    def open_node(self, event):
        """ディレクトリを開いた際に呼び出される

        self.nodes[node][0]がFalseの場合はまだ開かれたことがないと判断し、
        そのディレクトリ内のパスを追加する
        一度開いたか、又はファイルの場合はself.nodes[node][0]はTrueになります

        """



.focus()で、選択している要素の識別子が取得できます。
それをnodes[node]のように取得し、TrueかFalseと、絶対パスを取得します。
        node = self.tree.focus()
        already_open, abspath = self.nodes[node]




Flaseだったら、まだ開かれたことのないディレクトリと判断し、全てのディレクトリ・ファイルを取得します。
delete()には、要素の識別子を複数渡します。delete('I002', 'I003', 'I004'...)のように。
insert_nodeで追加した空要素を削除したいのですが、空要素を取得する必要があります。
なのでself.tree.get_children(node)として、ディレクトリに最初追加した空要素を取得しています。
        # まだ開かれたことのないディレクトリならば
        if not already_open:

            # 空白の要素が追加されているので、消去
            self.tree.delete(self.tree.get_children(node))




os.scandirで全てのファイル・ディレクトリを取得します。
そして、それをinsert_nodeメソッドに渡し、ノードを追加していきます。
一度取得したので、nodesにTrueにして格納しなおしておきます。
            # ディレクトリ内の全てのファイル・ディレクトリを取得し、Treeviewに追加
            for entry in os.scandir(abspath):
                self.insert_node(
                    node, entry.name, os.path.join(abspath, entry.path)
                )

            # 一度開いたディレクトリはTrueにする
            self.nodes[node] = (True, abspath)




ファイルをダブルクリックで取得する処理です。
focus()で要素の識別子を取得し、それがちゃんとノードで、ファイルならばprintで表示するだけです。
    def choose_file(self, event):
        """ツリーをダブルクリックで呼ばれる"""
        node = self.tree.focus()
        # ツリーのノード自体をダブルクリックしているか?
        if node:
            already_open, abspath = self.nodes[node]
            if os.path.isfile(abspath):
                print(abspath)



今回F5で呼ばれる、ツリーの更新処理です。
create_widgets内では、self.root_pathを起点にツリーの作成を始めます。なので、それをもう一度呼ぶだけ。
    def update_dir(self, event=None):
        """ツリーの一覧を更新する"""
        self.create_widgets()



そしてF4で呼ばれるディレクトリの変更処理です。
filedialog.askdirectory()でディレクトリを選択してもらい、それをroot_path変数に。
create_widgetsを呼べば、そのroot_pathを基に作成してくれます。
    def change_dir(self, event=None):
        """ツリーのルートディレクトリを変更する"""
        dir_name = filedialog.askdirectory()
        if dir_name:
            self.root_path = dir_name
            self.create_widgets()