【Python/Tkinter】PandasのDataFrameを検索→表示する簡易な検索フォームを作成する

## はじめに
DataFrameを利用する際，それを検索→表示できたらなと思う場面があります。特にGUIアプリを作成する際にはあると便利です。下記にPandasのDataFrameを検索し，表示する検索フォームをTkinterを利用して作ってみたいと思います。

## サンプル
![search.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293043/7c1f2481-9bf6-9c17-1f18-dd64072d14a0.gif)

##コード全文

```py
import pandas as pd
import tkinter as tk
import tkinter.ttk as ttk


class SearchWindow(tk.Frame):
    def __init__(self, master=None, parent=None):
        super().__init__(master)
        self.master = master
        self.master.geometry("300x300")
        self.master.title("検索ウインドウ")
        self.pack()
        self.set_data()
        self.create_widgets()

    def set_data(self):
        self.data = pd.read_csv("data.csv", encoding="utf-8")
        self.colname_list = ["食品コード", "食品名"]  # 検索結果に表示させる列名
        self.width_list = [100, 200]
        self.search_col = "検索キーワード"  # 検索キーワードの入力されている列名

    def create_widgets(self):
        self.pw_main = ttk.PanedWindow(self.master, orient="vertical")
        self.pw_main.pack(expand=True, fill=tk.BOTH, side="left")
        self.pw_top = ttk.PanedWindow(self.pw_main, orient="horizontal", height=25)
        self.pw_main.add(self.pw_top)
        self.pw_bottom = ttk.PanedWindow(self.pw_main, orient="vertical")
        self.pw_main.add(self.pw_bottom)
        self.creat_input_frame(self.pw_top)
        self.create_tree(self.pw_bottom)

    def creat_input_frame(self, parent):
        fm_input = ttk.Frame(parent, )
        parent.add(fm_input)
        lbl_keyword = ttk.Label(fm_input, text="キーワード", width=7)
        lbl_keyword.grid(row=1, column=1, padx=2, pady=2)
        self.keyword = tk.StringVar()
        ent_keyword = ttk.Entry(fm_input, justify="left", textvariable=self.keyword)
        ent_keyword.grid(row=1, column=2, padx=2, pady=2)
        ent_keyword.bind("<Return>", self.search)

    def create_tree(self, parent):
        self.result_text = tk.StringVar()
        lbl_result = ttk.Label(parent, textvariable=self.result_text)
        parent.add(lbl_result)
        self.tree = ttk.Treeview(parent)
        self.tree["column"] = self.colname_list
        self.tree["show"] = "headings"
        self.tree.bind("<Double-1>", self.onDuble)
        for i, (colname, width) in enumerate(zip(self.colname_list, self.width_list)):
            self.tree.heading(i, text=colname)
            self.tree.column(i, width=width)
        parent.add(self.tree)

    def search(self, event=None):
        keyword = self.keyword.get()
        result = self.data[self.data[self.search_col].str.contains(keyword, na=False)]
        self.update_tree_by_search_result(result)

    def update_tree_by_search_result(self, result):
        self.tree.delete(*self.tree.get_children())
        self.result_text.set(f"検索結果：{len(result)}")
        for _, row in result.iterrows():
            self.tree.insert("", "end", values=row[self.colname_list].to_list())

    def onDuble(self, event):
        for item in self.tree.selection():
            print(self.tree.item(item)["values"])


def main():
    root = tk.Tk()
    app = SearchWindow(master=root)
    app.mainloop()

if __name__ == "__main__":
    main()

```

なお，読み込んでいるCSVの中身は下記のようになっています。：

```
      食品コード              食品名                          検索キーワード
0      1001        アマランサス・玄穀                 アマランサス・玄穀 あまらんさす
1      1002           あわ・精白粒                        あわ・精白粒 あわ
2      1003          あわ・あわもち                  あわ・あわもち あわ あわもち
3      1004      えんばく・オートミール  えんばく・オートミール えんばく おーとみーる おーと おーつ
4      1005        大麦・七分つき押麦         大麦・七分つき押麦 おおむぎ ななぶつきおしむぎ
```

##すこし解説
テキストを入力→Enterで検索結果がtreeに表示されます。

Enterキーで検索できるように，メソッドを紐づけています。下記の部分です：

```py
        ent_keyword.bind("<Return>", self.search)
```
検索結果を表示させるウィジェットには，TTKのTreeviewを利用しています。表形式でシンプルに表示できるので気に入っています。`search`が実行されると，その結果を`update_tree_by_search_result`が受け取ります。Treeviewに挿入する前に全削除し，その後に追加しています。この部分が下記の処理です：

```py
    def search(self, event=None):
        keyword = self.keyword.get()
        result = self.data[self.data[self.search_col].str.contains(keyword, na=False)]
        self.update_tree_by_search_result(result)

    def update_tree_by_search_result(self, result):
        self.tree.delete(*self.tree.get_children())
        self.result_text.set(f"検索結果：{len(result)}")
        for _, row in result.iterrows():
            self.tree.insert("", "end", values=row[self.colname_list].to_list())
```

なお，GIF中では触れていませんが，この検索結果を利用することも可能です。下記のコードでtreeのアイテムがダブルクリックされた際の処理として，`onDouble`を紐付けています。

```py
        self.tree.bind("<Double-1>", self.onDuble)
```

onDoubleは下記のようになっています。これで選択されたアイテムの値を取得することができます。

```py
    def onDuble(self, event):
        for item in self.tree.selection():
            print(self.tree.item(item)["values"])
```