プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

Tkinter Treeview の列のソート(CSV viewer機能アップ)【Python】

このエントリーをはてなブックマークに追加

Treeview の列をソートする方法をサンプルコードで紹介します。

サンプルは、以前に作成した CSV ビューアで、列のソート機能を追加しました。
表の列見出しをクリックしてソートします。

CSV ビューアは Tkinter の Treeview ウィジェットを表形式にして CSV を表示しています。

基になっているアプリ 列のソート以外の実装方法はこちらの記事で紹介しています。
 📄CSV viewerアプリの作り方(ドラッグアンドドロップ)【Python】

目次

◆アプリのサンプル画像

▽列「1」を降順にソートした結果

CSV viewer

◆機能・特長

ソート機能についての特徴です。

  • 列の見出しをクリックするとソートします
  • 再度クリックするとソート順を逆にします
  • 要素が数字だけの列は数字としてソートします
  • 要素に数字が含まれているかどうかは自動判別します
  • 表の何行目までを見出し行(ソートしない)として扱うかを画面で設定できます

◆列のソート

Treeview の列のソートを実現するコードは公開されています。
ですが、質問に対する回答という形で公開されているので見つけづらいかもしれません。

Treeview の列のソートについて整理しました。

◇考え方

見出しがクリックされた時に動作するソートメソッドを定義します。
そのメソッドで、アイテムの値でソートし、その順にアイテムを移動します。

【ソートメソッドの動作】

  • 指定した列の値とアイテムIDをタプルにしたリストを作成
    リストにするのはソートするため
    アイテムIDを保持するのは move() メソッドで指示するため
  • リストをソートする
  • move() メソッドで新しい位置に移動する
    ソート後の順番が新しい位置、リストに保持したアイテムIDを指定してmoveする

CSV ビューアで追加している考慮点

  • 行に1行おきに背景色を付け直す
    アイテムを移動するとアイテムに設定している tag 情報もそのまま移動するので縞模様が崩れます。
    そのため行の背景色を付け直します。
    そもそもの付け方はこちらの CSV viewerの記事の「1行おきに背景色を設定する」 を参照してください。
  • ソースとする際にまず数値として扱ってソートし例外が発生したら文字として扱ってソートする

◇使用している Treeview クラスのメソッド

本対応で使用している Treeview クラスのメソッドを説明します。

  • get_children(iten=None):子アイテムの取得
    引数:親とするアイテムのID。無しは最上位アイテム。
    戻り値:子アイテムのIDをタプルで返す

  • set(item, column=None, value=None):データ値の取得・設定

    • 引数 column=None, value=None:全列取得
      戻り値:辞書
    • 引数 value=None:指定列の取得
      戻り値:データ値
    • 引数 value あり:設定
      戻り値:無効
  • move(item, parent, index):アイテム移動

    • 引数
      • item:移動元アイテムID
      • parent:移動先親のアイテムID (最上位:空文字列)
      • index:親アイテムからの相対インデックス

◇数字としてソートするために

CSV ファイルを読み込んだ各列のデータは文字列です。
数字のみの列を文字としてソートすると桁の少ないデータが期待するようにソートされません。
したがって、数字のみの列は数字として扱ってソートします。

対応は簡単で、まず、数字としてソートします。
その際に int() メソッドを使い文字から数値へ変換します。
int() メソッドは引数の値が数値に変換できないと例外が発生します。
例外が発生した場合は列全体を文字列としてソートし直します。

ソートの対象となっているのがタプルのリストなので単純に sort() メソッドの key 引数に key=int と指定できません。なのでラムダ式を使います。
タプルの最初の要素を int() メソッドに渡します。

l1.sort(key=lambda t: int(t[0]), reverse=reverse)

◇複数回クリックされた時のために

列の見出しを複数回クリックしたときに常に昇順でしかソートされないと、ソートが機能していないように見えます。
また、ユーザーにとっても降順でソートそれる機能があった方が便利です。

したがって、複数回クリックされた時はソートの昇順、降順を切り替えます。
そのために sort() メソッドの reverse 引数に渡す値を切り替えます。

切替方は、見出しがクリックされた時に実行する関数定義の引数を切り替えます。

   tv.heading(col_name, command=lambda c=col_name: \
                self.treeview_sort_column(tv, c, not reverse))

◇公開されているコード

▽カラムソートメソッド

   def treeview_sort_column(tv, col, reverse):
        l = [(tv.set(k, col), k) for k in tv.get_children('')]
        l.sort(reverse=reverse)

        # rearrange items in sorted positions
        for index, (val, k) in enumerate(l):
            tv.move(k, '', index)

        # reverse sort next time
        tv.heading(col, text=col, command=lambda _col=col: \
                     treeview_sort_column(tv, _col, not reverse))
    [...]

▽見出しの設定

   # ヘッダーの定義でクリックされた時の関数を指定する
    columns = ('name', 'age')
    treeview = ttk.TreeView(root, columns=columns, show='headings')
    for col in columns:
        treeview.heading(col, text=col, command=lambda _col=col: \
                     treeview_sort_column(treeview, _col, False))
    [...]

CSV Viewer で対応したコード


▽カラムソートメソッド

    def treeview_sort_column(self, tv:ttk.Treeview, col_name:str, reverse:bool):
        """
        カラムの見出しをクリックしたらソートする
        Args:
            Treeview:   ツリービュー
            str         ソートのキーとなるカラム名
            bool:       昇順か降順か
        """
        h_lines = self.var_heading.get()    # 見出しの行数
        # ツリービューの対象列の値をリスト化。値は値とアイテムのタプル
        l = [(tv.set(item_, col_name), item_) for item_ in tv.get_children('')]
        l0 = l[:h_lines]
        l1 = l[h_lines:]
        try:
            l1.sort(key=lambda t: int(t[0]), reverse=reverse)    # まず数字としてソート
        except ValueError:
            l1.sort(reverse=reverse)                             # 例外が発生したら文字としてソート
        l = l0 + l1

        # rearrange items in sorted positions
        for index, (_, item_) in enumerate(l):
            tv.move(item_, '', index)
            tags1 = []              # tag設定値の初期化
            if index & 1:               # 奇数か? i % 2 == 1:
                tags1.append("odd") # 奇数番目(treeviewは0始まりなので偶数行)だけ背景色を変える(oddタグを設定)
            tv.item(item_, tags=tags1)

        # reverse sort next time
        tv.heading(col_name, command=lambda c=col_name: \
                self.treeview_sort_column(tv, c, not reverse))

▽見出しの設定
 こちらはfor文は省略しています

    # tree.heading(col_name, text=col_name)  # 見出しの設定
    tree.heading(col_name, text=col_name, command=lambda c=col_name:\
                self.treeview_sort_column(tree, c, False))

◇その他のコードの解説

分かりにくいところを解説します。

  • ソート対象のリストの作成
    l = [(tv.set(item_, col_name), item_) for item_ in tv.get_children('')]

    • tv.get_children('') は Treeview の最上位からすべての子供(行)のアイテムIDを返します
    • tv.set(item_, col_name) はアイテムIDの行の指定した列の値を返します
    • 結果的に(指定した列の値, 行のアイテムID)というタプルのリストができます
  • ソートの対象の分割
    l0 = l[:h_lines]l1 = l[h_lines:]

    • ソートしない行とソートする行を分けています

◆画面の更新

表の何行目までを見出し行として扱うかを画面で設定できるようにするため、画面にウィジェットを追加します。

【追加したウィジェット

  • ラベル:lbl_heading
  • ウィジェット変数:var_heading
  • エントリー:ety_heading

【追加したコード】

        self.lbl_heading = tk.Label(parent, text="ソート時の見出し行数")
        self.var_heading = tk.IntVar(value=1)
        self.ety_heading = tk.Entry(parent, textvariable=self.var_heading, width=3, justify=tk.RIGHT)



        self.ety_heading.pack(side=tk.RIGHT, fill=tk.Y, padx=(0,5))
        self.lbl_heading.pack(side=tk.RIGHT, fill=tk.Y, padx=(5,0))

◆全体のソースとバイナリ(アプリ)

全体のソースはこちらから取得できます。
取得先:GitHub juu7g/Python-CSV-Viewer

バイナリ(アプリ)はこちらから取得できます。
ダウンロード先:Github からダウンロード

※ソースには入れていませんがバイナリにはオリジナルアイコンを入れました

使ってみませんか 上記のバイナリ(アプリ)はこちらの記事の出力結果を見るのに便利です。
よかったら使ってみませんか。
 📄はてなブログのスターとブックマークの数を取得するアプリ【フリー】

◆さいごに

自分で作ったCSVビューアを自分で使っています。
なかなか便利なのですが、列のソートができなくて歯がゆい思いをしていました。

並べ替えて見たくなる CSV データって、結構あります。

対応していて気になることがあったのですが、保留しています。
何かというと、同じ列の見出しを複数回クリックした場合の対応方法です。
見出しクリックで動作する関数を、次のクリックのために引数を変えて再登録しています。
フラグを使った方が良い気がするのですが、フラグも列数分持たなければならないなら、関数定義を変えても同じ事かと保留しています。

ソートした列を知らせる方法も検討していました。
見出しの文字を変えるのが一番簡単なのですが、文字を列の識別に使っているので対応していません。

ソート状態をソート前の状態にリセットする方法もあったようですが、実装していません。
ファイルを読み直した方が手っ取り早いと思いました。

EXCEL ビューアも提供しています。
本記事の公開時点では列のソートには未対応です。
そっと更新するかもしれません。

あわせて読みたい  📄SQLクライアントアプリの作り方(Tkinterで表)【Python】
 📄Excel viewerアプリの作り方(Tkinterでタブと表)【Python】
 📄CSV viewerアプリの作り方(ドラッグアンドドロップ)【Python】

◇ご注意

本記事は次のバージョンの下で動作した内容を基に記述しています。

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。

◆参考

投稿: