Python tkinter (ttk) を少しいじってみる (2024.11.16)

## はじめに
tkinter をいじり始めたついでに、もう少しいじってみることにした、ここでは、簡単なカスタマイズ事例を紹介している。
なお、当方の環境は以下の通り。

MacBook Pro 14"
Chip Apple M1 Pro
Memory 16GB
macOS Sequoia 15.1
Python 3.13.0
Tcl/Tk 9.00
matplotlib 3.9.2

### （感想）
つい最近、何気なく```brew update```と```brew upgrade```を実行したら、```tcl/tk```のバージョンが```8.6```から```9.0```にかわり、```tkinter```が動かなくなった。もちろん Python を再インストールしたのだが、
(1) ```ttkthemes```のテーマが使えなくなる、
(2) ```Canvas```に```matplotlib```での作図を貼り付けるために必要な、```FigureCanvasTkAgg```がインポートできない
という事象に見舞われた。
このため、ここでは、```ttk```にビルトインされているテーマ（```clam```）を使うことにした。

私のような素人は、バージョン管理に対する考え方が甘く、「なんでも最新を入れておけばいいや」という考えをもちがちであるが、注意が必要であることを思い知らされた。

個人で使用するプログラムであれば、最新版を入れてうまく動けば別に問題ないのだが、インポートできないとか、変なエラーが出るという事態への対応で余分な労力を割くリスク低減のためには、標準機能のみを使うことが有効であると感じた。

## 作例
作例の画面写真は以下の通り。カスタマイズの色のセンス（黄色背景とか）については、自分の見やすさを優先しているため、ご容赦願いたい。

![IMG_2171.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/ffb27a8d-b26f-513e-6f81-ffbe5a0f955d.jpeg)

上記写真では、４つの Python プログラムを同時実行しているが、以下のプログラムで実現している。なお、下記コードではプログラムが終了しないので、ターミナルから```ctl+c```を打って終了させる。

```python
import subprocess

from tkinter import ttk
s = ttk.Style()
print(s.theme_names())
#('aqua', 'clam', 'alt', 'default', 'classic')

cmd0='python -m tkinter'
cmd1='python py_tk1.py'
cmd2='python py_tk2.py'
cmd3='python py_tk3.py'

subprocess.Popen(cmd0,shell=True)
subprocess.Popen(cmd1,shell=True)
subprocess.Popen(cmd2,shell=True)
subprocess.Popen(cmd3,shell=True)
```

写真の中身は以下の通り。

+ １番左（```tkinter```）：```tcl/tk```のバージョンを表示する
+ 左から２番目（```py_tk1.py```）：テーマを何も指定しない場合。カスタマイズもなし。```aqua```が適用されている模様。
+ 左から３番目（```py_tk2.py```）：テーマに```clam```を指定。カスタマイズなし。
+ １番右（```py_tk3.py```）：```clam```を自分でカスタマイズしたもの

上記プログラム ```py_tk1.py``` のコードは以下の通り。```main()```関数の３〜６行目（コメントアウトしているところ）をいじれば、```py_tk2.py```となる。なお、やっていることは他愛もないことなので、説明は省略する。

```python: py_tk1.py
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import numpy as np


def end_q():
    quit()


def click_1(tab1):
    messagebox.showinfo('','Button-1 clicked')
    tab1.focus_set()


def click_2(tab1):
    messagebox.showinfo('','Button-2 clicked')
    tab1.focus_set()


def main():
    root =tk.Tk()
    root.resizable(False,False)
    root.title('py_tk1.py')
    #root.title('py_tk2.py')
    #style=ttk.Style()
    #style.theme_use('clam')

    frame0=ttk.Frame(root)
    label0=ttk.Label(frame0,text='Test frame')
    label0.pack()

    nb = ttk.Notebook(frame0)
    tab1 = ttk.Frame(nb)
    tab2 = ttk.Frame(nb)
    tab3 = ttk.Frame(nb)
    nb.add(tab1, text='Tab1')
    nb.add(tab2, text='Tab2')
    nb.add(tab3, text='Tab3')
    nb.pack(fill=tk.X)

    frame1=ttk.Frame(tab1)
    label_t1a=ttk.Label(frame1,text='label_t1a')
    label_t1b=ttk.Label(frame1,text='label_t1b')
    entry_t1a=ttk.Entry(frame1,justify=tk.CENTER,width=10)
    entry_t1b=ttk.Entry(frame1,justify=tk.CENTER,width=10)
    entry_t1a.insert(tk.END,'entry_t1a')
    entry_t1b.insert(tk.END,'entry_t1b')
    button_t1a=ttk.Button(frame1,text='button_t1a',command=lambda:click_1(tab1))
    button_t1b=ttk.Button(frame1,text='button_t1b',command=lambda:click_2(tab1))
    label_t1a.grid(row=0,column=0,pady=(3,3))
    entry_t1a.grid(row=0,column=1,pady=(3,3))
    label_t1b.grid(row=1,column=0,pady=(3,3))
    entry_t1b.grid(row=1,column=1,pady=(3,3))
    button_t1a.grid(row=2,column=0,columnspan=2,pady=(3,3))
    button_t1b.grid(row=3,column=0,columnspan=2,pady=(3,3))
    frame1.pack(anchor=tk.CENTER)

    label_t2a=ttk.Label(tab2,text='This is Tab2')
    label_t2a.pack()

    label_t3a=ttk.Label(tab3,text='This is Tab3')
    label_t3a.pack()

    frame_lf=ttk.LabelFrame(frame0,text='LabelFrame',relief=tk.FLAT)
    n,m=4,3
    lrc=np.empty((n,m),dtype=object)
    for i in range(0,n):
        for j in range(0,m):
            s='lrc[{0},{1}]'.format(i,j)
            lrc[i,j]=ttk.Label(frame_lf,text=s,borderwidth=1,relief=tk.SOLID,anchor=tk.CENTER,width=8)
            lrc[i,j].grid(row=i,column=j)
    frame_lf.pack()
    frame0.pack()

    q_button=ttk.Button(root,text='Quit',command=end_q)
    q_button.pack()

    root.mainloop()


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

## カスタマイズの説明
見た目をカスタマイズした```py_tk3.py```について少し解説しておく。

### テーマの指定
まず、テーマ```clam```を指定する。

```python
    style=ttk.Style()
    style.theme_use('clam')
```

### 全体のフォントと色
```wedgit```全体のフォントと背景色・前面色（フォントの色）を変更する。```Labelframe```の```Label```だけは太字（bold）とする。

```python 
    fontn='Calibri 14'
    fontb='Calibri 14 bold'
    bg_base,fg_base='#ffff00','#393939'
    style.configure('.',font=(fontn))
    style.configure('.',background=bg_base,foreground=fg_base)
    style.configure('TLabelframe.Label',font=(fontb))
```

### タブの装飾
タブに表示する文字のフォントを太字とし、非選択時の背景色・前面色を```configure```で指定する。また選択時の背景色・前面色を```map```を用いて指定する。

```python
    style.configure('TNotebook.Tab',
        font=(fontb),
        background='#dddddd',
        foreground='#777777')
    style.map('TNotebook.Tab',
        foreground=[('selected',fg_base)],
        background=[('selected',bg_base)])
```

### ボタンの装飾
ボタンの非選択時の背景色・前面色、見た目（relief）を```configure```で指定する。また選択時（マウスホバー時及び処理実行時）の背景色・前面色を```map```を用いて指定する。

 ```python
    style.configure('TButton',
        background='#eeeeee',
        foreground='#393939',
        relief=tk.RAISED)
    style.map('TButton',
        foreground=[('active','#0000ff')],
        background=[('active','#00ffff')])
```

### Entryのフォント
なお、下記のように、```Entrey```は```wedgit```を定義する段階でフォントを指定する必要がある。

```python     
entry_t1a=ttk.Entry(frame1,justify=tk.CENTER,width=10,font=(fontn))
```

## カスタマイズしたコード
コード全文は以下の通り。

```python: py_tk3.py
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import numpy as np


def end_q():
    quit()


def click_1(tab1):
    messagebox.showinfo('','Button-1 clicked')
    tab1.focus_set()


def click_2(tab1):
    messagebox.showinfo('','Button-2 clicked')
    tab1.focus_set()


def main():
    root =tk.Tk()
    root.resizable(False,False)
    root.title('py_tk3.py')
    style=ttk.Style()
    style.theme_use('clam')
    fontn='Calibri 14'
    fontb='Calibri 14 bold'
    bg_base,fg_base='#ffff00','#393939'
    style.configure('.',font=(fontn))
    style.configure('.',background=bg_base,foreground=fg_base)
    style.configure('TLabelframe.Label',font=(fontb))
    style.configure('TNotebook.Tab',
        font=(fontb),
        background='#dddddd',
        foreground='#777777')
    style.map('TNotebook.Tab',
        foreground=[('selected',fg_base)],
        background=[('selected',bg_base)])
    style.configure('TButton',
        background='#eeeeee',
        foreground='#393939',
        relief=tk.RAISED)
    style.map('TButton',
        foreground=[('active','#0000ff')],
        background=[('active','#00ffff')])

    frame0=ttk.Frame(root)
    label0=ttk.Label(frame0,text='Test frame',font=(fontb))
    label0.pack()

    nb = ttk.Notebook(frame0)
    tab1 = ttk.Frame(nb)
    tab2 = ttk.Frame(nb)
    tab3 = ttk.Frame(nb)
    nb.add(tab1, text='Tab1')
    nb.add(tab2, text='Tab2')
    nb.add(tab3, text='Tab3')
    nb.pack(fill=tk.X)

    frame1=ttk.Frame(tab1)
    label_t1a=ttk.Label(frame1,text='label_t1a')
    label_t1b=ttk.Label(frame1,text='label_t1b')
    entry_t1a=ttk.Entry(frame1,justify=tk.CENTER,width=10,font=(fontn))
    entry_t1b=ttk.Entry(frame1,justify=tk.CENTER,width=10,font=(fontn))
    entry_t1a.insert(tk.END,'entry_t1a')
    entry_t1b.insert(tk.END,'entry_t1b')
    button_t1a=ttk.Button(frame1,text='button_t1a',command=lambda:click_1(tab1))
    button_t1b=ttk.Button(frame1,text='button_t1b',command=lambda:click_2(tab1))
    label_t1a.grid(row=0,column=0,pady=(3,3))
    entry_t1a.grid(row=0,column=1,pady=(3,3))
    label_t1b.grid(row=1,column=0,pady=(3,3))
    entry_t1b.grid(row=1,column=1,pady=(3,3))
    button_t1a.grid(row=2,column=0,columnspan=2,pady=(3,3))
    button_t1b.grid(row=3,column=0,columnspan=2,pady=(3,3))
    frame1.pack(anchor=tk.CENTER)

    label_t2a=ttk.Label(tab2,text='This is Tab2')
    label_t2a.pack()

    label_t3a=ttk.Label(tab3,text='This is Tab3')
    label_t3a.pack()

    frame_lf=ttk.LabelFrame(frame0,text='LabelFrame',relief=tk.FLAT)
    n,m=4,3
    lrc=np.empty((n,m),dtype=object)
    for i in range(0,n):
        for j in range(0,m):
            s='lrc[{0},{1}]'.format(i,j)
            lrc[i,j]=ttk.Label(frame_lf,text=s,borderwidth=1,relief=tk.SOLID,anchor=tk.CENTER,width=8)
            lrc[i,j].grid(row=i,column=j)
    frame_lf.pack()
    frame0.pack()

    q_button=ttk.Button(root,text='Quit',command=end_q)
    q_button.pack()

    root.mainloop()


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

以　上