Python Tkinter (ttk) Treeviewで表を作る　(2024.11.25)

(2024.11.26) 少し変更。クリップボードコピー機能を追加。指定した行に色をつけた。

# はじめに
```Tkinter(ttk)```の```Treeview```を使う練習で、手持ちのガジェットを表示するプログラムを作ってみた。
機能は以下の通り。

+ ２次元 numpy 配列で定義したデータを Treeview で表示する。
+ 指定行の背景色・文字色を指定する（tag の利用）。
+ 表示内容を pandas DataFrame に変換し csv ファイル出力する。
+ 指定行をクリップボードコピーする（エディタにペースト可能）。

当方の環境は以下の通り。
MacBook Pro 14"
Chip Apple M1 Pro
Memory 16GB
macOS Sequoia 15.1
Python 3.13.0
Tcl/Tk 9.00

参考にしたサイトは以下の通り。

Treeviewの使い方
[https://yuya-blog.net/【完全独学python】tkinterのtreeviewでオシャレな表を簡単作成/](https://yuya-blog.net/【完全独学python】tkinterのtreeviewでオシャレな表を簡単作成/)

スクロールバー
[https://office54.net/python/tkinter/python-tkinter-scrollbar](https://office54.net/python/tkinter/python-tkinter-scrollbar)

Treeviewに表示された内容をDataFrame化
[https://teratail.com/questions/311037](https://teratail.com/questions/311037)


## 作例
まず作例の写真を示そう。

### Tab1（All 2020-）
![Screenshot 2024-11-26 at 12.49.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/38f236b4-cc5b-decd-c771-38591f76562c.png)

### Tab2 (Only Apple)
![Screenshot 2024-11-26 at 12.49.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/7c0b2511-8250-0713-d831-3a10d297f662.png)


## 能書
### なぜこれを作ろうと思ったのか？
たまに、「この Mac はいつ買ったんだっけ？」とか思うことがあり、それらの情報をスピーディーに表示するものが欲しくなったから。もちろん```Terminal```を立ち上げて```python ...```なんていうことはせず、```Autometor```で```app```化、Stream Deck に登録してボタンひとつで確認できるようにしている。

### こういうのは Notion とかでやるのが普通じゃない？
データは手元にあったほうがうれしいし、Notion の使い方もよくわからない。

### エクセルで十分でない？
その通り。だけど自分で作ってみたかった。
実際、Tab1 の All 2020- は以下のエクセルファイルを読み込んで表示しているだけである。

![Screenshot 2024-11-26 at 12.50.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/70af6a69-ce67-ac0c-a755-0eae953d985c.png)


## 機能説明
### Tab1（All 2020-）
上記のように、エクセルで作成してあるデータを、```pandas```で読み込み、```numpy```配列に変換後、```Treeview```で表示している。
表示は、Apple製品は黄色背景色で、メインで使用中の機器を水色背景で、すでに手元にない・使えないものを灰色文字としている。色をつける方法は、```tag```を指定し、```tag_configure```で背景色・文字色を指定するものである。

### Tab2 (Only Apple)
データはプログラム内で```numpy```配列として定義、```Treeview```で表示している。

### 共通
#### CSV 出力
「```Output csv```」のボタンを設け、データバックアップのため、```csv```ファイル出力できるようにしている。出力したら```messagebox```でメッセージを表示する。

#### クリップボードコピー
「```c```」キーを押すとクリップボードに選択行（カーソル行）をコピーする。標準ショートカット「```cmd + v```」でテキストエディタにペーストできる。ペースト結果は以下のようにカッコ付きタプルとなる。```pyperclip```をインストールしておく必要あり。

```
('o', 'Caldigit USB-C HDMI Dock', 'Amazon', '2021.02.24', '2021.02.25', '26,400')
```

```
pip install pyperclip
```


## コード
```python
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import sys
import pandas as pd
import numpy as np
import pyperclip


def inp_data_1():
    dir='/Users/kk/stream_deck/'
    fnameR=dir+'inp_gadget.xls'
    df=pd.read_excel(fnameR,sheet_name=0,header=0,index_col=None)
    data=df.to_numpy(dtype=object)
    return data


def inp_data_2():
    data =np.array([
    ['MacBook Pro 13" (Mid 2014)',   '2015.02.01', 132624, '2020.12.29', 22400],
    ['iPad mini 4 (64GB)',           '2016.03.12',  58104, '2021.01.04', 19000], 
    ['iMac Retina 21.5"',            '2018.12.14', 183384, '2021.01.04', 53000],
    ['iPad Air 4, 64GB,Wi-Fi',       '2020.11.08',  69080, '2021.11.13', 42000],
    ['MacBook Air (M1 2020) 8/256GB','2020.12.28', 115280, '2021.11.13', 64050],
    ['M1 iPad Pro 12.9 128GB',       '2021.09.23', 129800, '2023.08.25', 96000],
    ['iPad mini 6 64GB',             '2021.11.07',  59800, '2023.08.25', 53000], 
    ['Apple Pencil',                 '2020.11.02',  15950, '---', '---'],
    ['iPhone SE White 64GB',         '2021.07.21',  12848, '---', '---'],
    ['Magic Keyboard',               '2021.10.04',  10800, '---', '---'],
    ['Magic Trackpad',               '2021.10.04',  13800, '---', '---'],
    ['MacBook Pro (14-in 2021 M1 Pro 16/512GB)', '2021.10.27', 239800, '---', '---'],
    ['Apple Watch Series 7 45mm',    '2022.04.17',  52800, '---', '---'],
    ['iPhone 13 mini 128GB blue',    '2022.07.03',  86800, '---', '---'],
    ['11-in iPad Pro Wi-Fi 128GB spacegray', '2023.08.24', 124800, '---', '---']
    ],dtype=object)
    return data


def outcsv(nnn,tree,col):
    data=[]
    for ii in tree.get_children():
        s=tree.item(ii,'values')
        data.append(s)
    df=pd.DataFrame(data)
    df.columns=col
    dir='/Users/kk/stream_deck/'
    match nnn:
        case 1: fnameW='out_gadget1.csv'
        case 2: fnameW='out_gadget2.csv'
    fnameW=dir+fnameW
    df.to_csv(fnameW,index=False)
    s='saved as '+fnameW
    messagebox.showinfo('',s)


def copy_row(event,tree):
    # copy on clipboard
    ii = tree.focus()
    s = tree.item(ii, 'values')
    pyperclip.copy(s)


def b_end():
    sys.exit()


def table(nnn):
    global tab1,tab2
    match nnn:
        case 1:
            tabn=tab1
            data=inp_data_1()
            tp=(1,2,3,4,5,6)
            col=['ox','Item','Shop','Order','Arrive','JPY']
            ww=[50,400,100,100,100,100]
            ac=[tk.CENTER,tk.W,tk.CENTER,tk.CENTER,tk.CENTER,tk.E]
        case 2:
            tabn=tab2
            data=inp_data_2()
            tp=(1,2,3,4,5)
            col=['Item','Buy','JPY (buy)','Sell','JPY (sell)']
            ww=[400,100,100,100,100]
            ac=[tk.W,tk.CENTER,tk.E,tk.CENTER,tk.E]
    nrow=len(data)
    irow=20
    if irow<=nrow: n=irow
    else: n=nrow

    tree = ttk.Treeview(tabn, style='Treeview',height=n)
    tree['columns'] = tp
    tree.column('#0', width=0, stretch=tk.NO)
    tree.heading('#0', text='', anchor=tk.W)

    for i in range(0,len(col)):
        tree.column(i+1,width=ww[i],anchor=ac[i])
    for i in range(0,len(col)):
        tree.heading(i+1,text=col[i],anchor=ac[i])
    match nnn:
        case 1:
            for i, (ox,item,shop,d1,d2,p,c) in enumerate(data):
                if str(p).isdecimal()==True: pp = '{:,.0f}'.format(p)
                else: pp=p
                tree.insert('','end',iid=i,values=(ox,item,shop,d1,d2,pp),tags=c)
            tree.tag_configure(2,background='#00ffff')
            tree.tag_configure(1,background='#ffff66')
            tree.tag_configure(-1,foreground='#aaaaaa')
        case 2:
            for i, (item,d1,p1,d2,p2) in enumerate(data):
                if str(p1).isdecimal()==True: pp1 = '{:,.0f}'.format(p1)
                else: pp1=p1
                if str(p2).isdecimal()==True: pp2 = '{:,.0f}'.format(p2)
                else: pp2=p2
                tree.insert('','end',iid=i,values=(item,d1,pp1,d2,pp2))
    tree.grid(row=0,column=0)
    tree.bind('<KeyPress-c>', lambda event:copy_row(event,tree))

    if irow<=nrow:
        scrollbar=ttk.Scrollbar(tabn, orient='vertical', command=tree.yview)
        scrollbar.grid(row=0,column=1,sticky=(tk.N, tk.S))
        tree.configure(yscrollcommand=scrollbar.set)
    btn=ttk.Button(tabn,text='Output csv', command=lambda:outcsv(nnn,tree,col))
    btn.grid(row=1,column=0)


def main():
    global tab1,tab2
    root = tk.Tk()
    root.resizable(False,False)
    root.title('py_gadget.py')
    style = ttk.Style()
    style.theme_use('clam')
    fontn='Calibri 11'
    fontb='Calibri 11 bold'
    style.configure('Treeview.Heading',font=(fontb))
    style.configure('Treeview',font=(fontn),rowheight=25)
    style.configure('TButton',font=(fontb))
    style.configure('TNotebook.Tab',font=(fontb))

    nb = ttk.Notebook(root)
    tab1 = ttk.Frame(nb)
    tab2 = ttk.Frame(nb)
    nb.add(tab1, text='All 2020-')
    nb.add(tab2, text='Only Apple')
    nb.pack()

    for nnn in [1,2]:
        table(nnn)

    q_button=ttk.Button(root,text='Exit',command=b_end)
    q_button.pack()

    root.mainloop()


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

以　上