CSVファイルのビューワーをTkinterで作成する

pythonの標準GUIライブラリ「Tkinter」を用いたアプリ開発の練習として,CSVファイルのビューワーアプリを作成してみました(車輪の再発明とか言わないこと)。

こちらで公開しているコードについては,必ずしも最適な記述がされている訳ではありませんので参考程度にご利用ください。なお,間違いやよりよい記法についてお気づきの点がございましたら,ご指摘いただけると幸いです。

(本ページにて紹介しているコードはgithubにて公開しています。)

DataAnalOji.hatena.sample/python_samples/tkinter/08_csv_viewer.py at master · Data-Anal-Ojisan/DataAnalOji.hatena.sample
samples for my own blog. Contribute to Data-Anal-Ojisan/DataAnalOji.hatena.sample development by creating an account on GitHub.

開発環境

python: 3.7.9
numpy: 1.19.3

スクリプト全容

次のスクリプトのように,CsvViewerクラス作成し,start()メソッドでルートウィンドウや各種widgetを呼び出しています。また,[read]ボタン押しに対応する処理についてもメソッドとして記述しています。

# -*- coding: utf-8 -*-
"""
@author: data-anal-ojisan
"""

import os
import csv
import numpy as np
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox


class CsvViewer:

    def __init__(self):

        self.root = None  # ルートウィンドウ
        self.data = None  # 読み込まれたcsvファイルの内容
        self.tree = None  # csvファイルの内容を表示するttk.Treeviewテーブル

    def start(self):

        self.call_root_window()
        self.call_csv_reader_widget()
        self.call_treeview_widget()
        self.root.mainloop()

    def call_root_window(self):
        """
        ルートウィンドウを呼び出す
        """
        self.root = tk.Tk()
        self.root.geometry('500x500')
        self.root.title('CsvViewer')

    def call_csv_reader_widget(self):
        """
        csvファイルを読み込むためのウィジェットを呼び出す関数
        """
        # widget配置のフレームを作成
        frame = tk.Frame(self.root, relief="ridge", bd=1)
        frame.pack(fill=tk.BOTH, padx=5, pady=5)

        # ラベルを作成
        tk.Label(frame, text='Reference file >>').pack(side=tk.LEFT)

        # CSVファイルのファイルパスを指定する入力フィールドを作成
        entry_field = tk.Entry(frame)
        entry_field.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        # ファイルダイアログを呼び出すボタンを作成
        tk.Button(frame, text='...', command=lambda: self.set_path(entry_field)).pack(side=tk.LEFT)

        # CSVファイルを読み込み,Treeviewに内容を表示するボタンを作成
        tk.Button(frame, text='read',
                  command=lambda: self.read_csv(entry_field.get(),  # entry_fieldに入力されているファイルパス
                                                )).pack(side=tk.LEFT)

    def call_treeview_widget(self):
        """
        ttk.Treeviewを呼び出し,X軸Y軸のスクロールバーを追加する関数
        """
        # widget配置のフレームを作成
        frame = tk.Frame(self.root)
        frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=1)

        # CSVファイルの内容を表示するTreeviewを作成
        self.tree = ttk.Treeview(frame)
        self.tree.column('#0', width=50, stretch=tk.NO, anchor=tk.E)
        self.tree.grid(row=0, column=0, sticky=tk.W + tk.E + tk.N + tk.S)

        # X軸スクロールバーを追加する
        hscrollbar = ttk.Scrollbar(frame, orient=tk.HORIZONTAL, command=self.tree.xview)
        self.tree.configure(xscrollcommand=lambda f, l: hscrollbar.set(f, l))
        hscrollbar.grid(row=1, column=0, sticky=tk.W + tk.E + tk.N + tk.S)

        # Y軸スクロールバーを追加する
        vscrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=vscrollbar.set)
        vscrollbar.grid(row=0, column=1, sticky=tk.W + tk.E + tk.N + tk.S)

    def set_path(self, entry_field):
        """
        tk.Entryの内容をクリアした後にファイルダイアログを呼び出し
        選択したファイルパスを,tk.Entryに記入する関数。
        :param entry_field: tk.Entry
        """
        # tk.Entryに記入されている内容をクリアする
        entry_field.delete(0, tk.END)

        # 実行ファイルの絶対パスを取得する
        abs_path = os.path.abspath(os.path.dirname(__file__))

        # 初期ディレクトリを実行ファイルの絶対パスにしたファイルダイアログを呼び出す
        file_path = filedialog.askopenfilename(initialdir=abs_path)

        # ファイルダイアログの選択結果をtk.Entryの内容に挿入する
        entry_field.insert(tk.END, str(file_path))

    def read_csv(self, path):
        """
        ファイルパスの拡張子がcsvの場合に内容を読み込み,ttk.Treeviewに内容を表示する。
        拡張子がcsv以外の場合はメッセージボックスを表示する。
        :param path: tk.Entryに入力されている文字列
        """
        # ファイルパスから拡張子を取得する
        extension = os.path.splitext(path)[1]

        # 拡張子がcsvの場合
        if extension == '.csv':

            # csvファイルの内容を読み込む
            with open(path) as f:
                reader = csv.reader(f)
                self.data = [row for row in reader]

            self.show_csv()

        # 拡張子がcsv以外の場合
        else:
            messagebox.showwarning('warning', 'Please select a csv file.')

    def show_csv(self):
        """
        読み込んだcsvファイルの内容をttk.Treeviewに表示する。
        """
        # Treeviewの内容をクリアする
        self.tree.delete(*self.tree.get_children())

        # 列番号をTreeviewに追加する
        self.tree['column'] = np.arange(np.array(self.data).shape[1]).tolist()

        # 列のヘッダーを更新する
        for i in self.tree['column']:
            self.tree.column(i, width=100, anchor=tk.E)
            self.tree.heading(i, text=str(i))

        # 行番号及びCSVの内容を表示する
        for i, row in enumerate(self.data, 0):
            self.tree.insert('', 'end', text=i, values=row)


if __name__ == '__main__':
    viewer = CsvViewer()
    viewer.start()

__init__コンストラクタ

クラスのインスタンス生成時に呼び出されるコンストラクタ。スクリプトの可読性のため,生成するインスタンス変数一覧をNoneで初期化しています。

    def __init__(self):

        self.root = None  # ルートウィンドウ
        self.data = None  # 読み込まれたcsvファイルの内容
        self.tree = None  # csvファイルの内容を表示するttk.Treeviewテーブル

startメソッド

後述のメソッドを実行し,ルートウィンドウやウィジェットを呼び出すためのウィジェットです。スクリプト全容末尾2行にあるように,CsvViewerのインスタンスを生成後,startメソッドを実行することでcsvファイルのビューワーを起動しています。

    def start(self):

        self.call_root_window()
        self.call_csv_reader_widget()
        self.call_treeview_widget()
        self.root.mainloop()

call_root_windowメソッド

ルートウィンドウを呼び出すメソッドです。実行時のウィンドウサイズの指定や,ウィンドウタイトルの指定など,ルートウィンドウに関する設定はここで行っています。

    def call_root_window(self):
        """
        ルートウィンドウを呼び出す
        """
        self.root = tk.Tk()
        self.root.geometry('500x500')
        self.root.title('CsvViewer')

call_csv_reader_widgeメソッド

読込対象のcsvファイルを指定し,内容の読み込みと描画を行うメソッドです。

[…]ボタンからset_pathメソッドを実行することでファイルダイアログを呼び出し,[read]ボタンからread_csvメソッドを実行することで,csvファイルの内容を読み込み,ビューワー内のテーブルにその内容を表示しています。

通常,tk.Buttonのcommandで実行する関数には引数を渡せないのですが,lambdaを利用することで引数を渡しています。

    def call_csv_reader_widget(self):
        """
        csvファイルを読み込むためのウィジェットを呼び出す関数
        """
        # widget配置のフレームを作成
        frame = tk.Frame(self.root, relief="ridge", bd=1)
        frame.pack(fill=tk.BOTH, padx=5, pady=5)

        # ラベルを作成
        tk.Label(frame, text='Reference file >>').pack(side=tk.LEFT)

        # CSVファイルのファイルパスを指定する入力フィールドを作成
        entry_field = tk.Entry(frame)
        entry_field.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        # ファイルダイアログを呼び出すボタンを作成
        tk.Button(frame, text='...', command=lambda: self.set_path(entry_field)).pack(side=tk.LEFT)

        # CSVファイルを読み込み,Treeviewに内容を表示するボタンを作成
        tk.Button(frame, text='read',
                  command=lambda: self.read_csv(entry_field.get(),  # entry_fieldに入力されているファイルパス
                                                )).pack(side=tk.LEFT)

set_pathメソッド

上述のcall_csv_reader_widgeメソッド内のボタンウィジェットで呼び出されるメソッドです。引数はtk.Entryオブジェクトになります。

すでに入力されている内容をクリアし,ファイルダイアログで指定されたファイル選択結果であるファイルパスをtk.Entryに挿入します。

    def set_path(self, entry_field):
        """
        tk.Entryの内容をクリアした後にファイルダイアログを呼び出し
        選択したファイルパスを,tk.Entryに記入する関数。
        :param entry_field: tk.Entry
        """
        # tk.Entryに記入されている内容をクリアする
        entry_field.delete(0, tk.END)

        # 実行ファイルの絶対パスを取得する
        abs_path = os.path.abspath(os.path.dirname(__file__))

        # 初期ディレクトリを実行ファイルの絶対パスにしたファイルダイアログを呼び出す
        file_path = filedialog.askopenfilename(initialdir=abs_path)

        # ファイルダイアログの選択結果をtk.Entryの内容に挿入する
        entry_field.insert(tk.END, str(file_path))

read_csvメソッド

tk.Entryに入力されている内容を受け取り,それがcsvファイルだった場合にはcsvファイルの内容の読み込みを行い,show_csvメソッドを実行することで,csvファイルの内容をビューワー内のテーブルに表示します。

csvファイルではなかった場合や,tk.Entryに入力がなかった場合は,エラーメッセージを表示するような処理を行います。

    def read_csv(self, path):
        """
        ファイルパスの拡張子がcsvの場合に内容を読み込み,ttk.Treeviewに内容を表示する。
        拡張子がcsv以外の場合はメッセージボックスを表示する。
        :param path: tk.Entryに入力されている文字列
        """
        # ファイルパスから拡張子を取得する
        extension = os.path.splitext(path)[1]

        # 拡張子がcsvの場合
        if extension == '.csv':

            # csvファイルの内容を読み込む
            with open(path) as f:
                reader = csv.reader(f)
                self.data = [row for row in reader]

            self.show_csv()

        # 拡張子がcsv以外の場合
        else:
            messagebox.showwarning('warning', 'Please select a csv file.')

call_treeview_widgetメソッド

Tkinter.ttkのTreeviewを呼び出し,csvファイルの内容を表示させるためのテーブル領域を作成するためのメソッドです。

ビューワーのウィンドウサイズ変更に合わせてテーブルの表示領域サイズが変更されるように,ウィジェットを配置するフレームのcolumnconfigure / rowconfigureを設定しています。また,サイズの大きいcsvファイルを読み込んだ場合,表示領域がビューワーのウィンドウサイズを超過してしまうため,X軸Y軸のスクロールバーを設置しています。

    def call_treeview_widget(self):
        """
        ttk.Treeviewを呼び出し,X軸Y軸のスクロールバーを追加する関数
        """
        # widget配置のフレームを作成
        frame = tk.Frame(self.root)
        frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=1)

        # CSVファイルの内容を表示するTreeviewを作成
        self.tree = ttk.Treeview(frame)
        self.tree.column('#0', width=50, stretch=tk.NO, anchor=tk.E)
        self.tree.grid(row=0, column=0, sticky=tk.W + tk.E + tk.N + tk.S)

        # X軸スクロールバーを追加する
        hscrollbar = ttk.Scrollbar(frame, orient=tk.HORIZONTAL, command=self.tree.xview)
        self.tree.configure(xscrollcommand=lambda f, l: hscrollbar.set(f, l))
        hscrollbar.grid(row=1, column=0, sticky=tk.W + tk.E + tk.N + tk.S)

        # Y軸スクロールバーを追加する
        vscrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=vscrollbar.set)
        vscrollbar.grid(row=0, column=1, sticky=tk.W + tk.E + tk.N + tk.S)

show_csvメソッド

read_csvメソッド内で呼び出される,Tkinter.ttkのTreeviewの表示を更新するためのメソッドです。繰り返しcsvファイルを読み込む動作に対応するため,実行時にはTreeviewの内容をクリアするような処理が行われます。

Treeviewの内容の更新としては,単純に読み込んだcsvファイルの内容を行ごとにテーブル末尾から挿入している(Treeview.insert())だけです。

    def show_csv(self):
        """
        読み込んだcsvファイルの内容をttk.Treeviewに表示する。
        """
        # Treeviewの内容をクリアする
        self.tree.delete(*self.tree.get_children())

        # 列番号をTreeviewに追加する
        self.tree['column'] = np.arange(np.array(self.data).shape[1]).tolist()

        # 列のヘッダーを更新する
        for i in self.tree['column']:
            self.tree.column(i, width=100, anchor=tk.E)
            self.tree.heading(i, text=str(i))

        # 行番号及びCSVの内容を表示する
        for i, row in enumerate(self.data, 0):
            self.tree.insert('', 'end', text=i, values=row)

csvビューワーの起動

CsvViewerクラスのインスタンスを生成し(viewer変数),生成したインスタンスからstartメソッドを実行することで,startメソッド内の処理が行われビューワーが起動します。

if __name__ == '__main__':
    viewer = CsvViewer()
    viewer.start()

動作としてはあまり軽快とは言えず,少しでも容量の大きいcsvファイルを読み込むとカクついてしまいます。おそらくですが,Treeviewが現在表示している領域以外の領域情報の保持しているために処理が切迫されることで,このカクツキが発生しているものと思われます。この部分の動作速度をどうにか改善したいのですが,現段階の私では解決できず。どなたか解決策をご存知の方がいらっしゃれば教えていただけると嬉しいです。

(以下の動画は,有名なboston housingのデータセットを読み込んでみたもの。スライドバーの移動がマウスの移動に追いついておらず,また表示の更新も遅れて実行されている。)

コメント