水ノ茉の宣伝
準備中...
ゲームを作る予定なの水ノ茉
作業環境
- Windows 10
- Visual Studio Code
- Python 3.11
概要
Labelを元に画像ボタンを作っていきます。

Labelを使う理由
Buttonのimage引数を指定することで簡単に画像をボタンを作ることができますが、以下の理由からLabelで作成しています。

tkinter、ttkbootstrapで画像を扱う
tkinter、ttkbootstrapで画像を扱うには、Pillowやopencvで読み込んだ画像(Image、NDArray)を専用のImageクラス(以降「ImageTk」)に変換します。変換は関数ひとつで完了します。
# Pillowで画像読込
path = r"xxx\yyy\zzz.png"
image = Image.open(path)
# tkinter専用のImageクラスに変換
# PhotoImageの他にBitmapImageもある
image_tk = ImageTk.PhotoImage(image)
image引数の罠
PythonさんとC++さん、お二方と面識のある方向けの説明になってしまいますが、Pythonさんは変数の寿命を参照カウンタで管理しています。そのため、C++さんほどGCを意識せずに変数を自由に使用できます。
このような背景もありクラスに指定した引数は、そのクラスも所有していることになり、参照カウンタが進むという設計がほとんどです。
然しながらTkinterのウィジェットクラス(ButtonやLabelの基底クラス)は、そういう設計ではないみたいです。
以下のように画像を指定するとGCでImageTkが消失し結果、画像が表示されない不具合が発生します。挙動的にはC++さんでいうところの生ポインタですかね。
class ImageButton(ttk.Label):
def __init__(
self,
master:tk.Misc,
) -> None:
path = r"xxx\yyy\zzz.png"
image = Image.open(path)
image_tk = ImageTk.PhotoImage(image)
# image引数に指定された値は、おそらく生ポインタで扱われるため参照カウンタは進みません。
# その結果、コンストラクタ(__init__)を抜けるとカウンタがゼロになり、GCで無事(無事じゃない)消えます。
super().__init__(master, image=image_tk, borderwidth=0, padding=0)
初見でGCの知見がないと頭が???になる挙動な気がします。
そのため、ImageTkを扱う際には適当にselfで保持させてあげましょう。
class ImageButton(ttk.Label):
def __init__(
self,
master:tk.Misc,
) -> None:
path = r"xxx\yyy\zzz.png"
image = Image.open(path)
# self(ImageButton)がimage_tkを所持しているため、コンストラクタを抜けてもselfを破棄しない限り、参照カウンタは0にならない
self.image_tk = ImageTk.PhotoImage(image)
super().__init__(master, image=self.image_tk, borderwidth=0, padding=0)
便利なbind・callback(バインド・コールバック)
| sequence | 呼び出しタイミング |
|---|---|
| <ButtonPress-1> | マウス左ボタンがクリックされた瞬間 |
| <ButtonRelease-1> | マウス左ボタンのクリックが離された瞬間 |
| <Enter> | ウィジェットにマウスカーソルが入った瞬間 |
| <Leave> | ウィジェットからマウスカーソルが離れた瞬間 |
これらのbindに紐付けしてボタンの状態を決定しています。
状態管理にはビットフラグを使用していますが、ビットフラグがよく解らない・苦手な方はlistに置き換えてin, not inで管理してもいいかもしれません。
理解できる範疇で実装するのが安全で楽しいですからね。
実装
import tkinter as tk
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from enum import Enum
from typing import Optional, Union, Callable
from PIL import Image, ImageTk
__all__ = [
"ImageButton",
]
class ButtonState(Enum):
"""ボタンのステート
"""
NORMAL = 0x0
HOVER = 0x1
ACTIVE = 0x2
DISABLE = 0x4
class ImageButton(ttk.Label):
"""画像ボタン
"""
def __init__(
self,
master:tk.Misc,
normal:Union[str, Image.Image],
hover:Union[str, Image.Image],
active:Union[str, Image.Image],
disabled:Union[str, Image.Image],
is_disabled:bool = False,
command:Optional[Callable[[], None]] = None,
) -> None:
"""コンストラクタ
Args:
master (tk.Misc): master
normal (Union[str, Image.Image]): 通常時の画像
hover (Union[str, Image.Image]): カーソルを合わせた時の画像
active (Union[str, Image.Image]): クリックした時の画像
disabled (Union[str, Image.Image]): 無効時の画像
is_disabled (bool, optional): ボタンの無効状態 (初期値). Defaults to False.
command (Optional[Callable[[], None]], optional): カーソルを合わせた状態でクリックを離した時のコールバック. Defaults to None.
"""
self.normal_image = self.create_photo_image(normal)
self.hover_image = self.create_photo_image(hover)
self.active_image = self.create_photo_image(active)
self.disabled_image = self.create_photo_image(disabled)
super().__init__(master, image=self.disabled_image if is_disabled else self.normal_image, borderwidth=0, padding=0)
self.bind("<ButtonPress-1>", self.on_left_button_press)
self.bind("<ButtonRelease-1>", self.on_left_button_release)
self.bind("<Enter>", self.on_enter)
self.bind("<Leave>", self.on_leave)
self.command = command
# ボタンの状態
self.set_state(ButtonState.DISABLE if is_disabled else ButtonState.NORMAL)
@property
def is_hover(self) -> bool:
"""ボタンにマウスカーソルが置かれているか取得
Returns:
bool: ボタンにマウスカーソルが置かれている場合はTrueを返します。
"""
return self.is_set_state(ButtonState.HOVER)
@property
def is_active(self) -> bool:
"""ボタンがクリックされているか取得
ボタンにマウスカーソルが置かれているかは考慮しません。
Returns:
bool: ボタンがクリックされている場合はTrueを返します。
"""
return self.is_set_state(ButtonState.ACTIVE)
@property
def is_disable(self) -> bool:
"""ボタンが無効状態か取得
Returns:
bool: 無効状態の場合はTrueを返します。
"""
return self.is_set_state(ButtonState.DISABLE)
@is_disable.setter
def is_disable(self, value:bool) -> None:
"""ボタンの無効状態をセット
Args:
value (bool): 無効状態にする場合はTrueを指定します。
"""
if value:
# 無効状態は既存状態を全てクリアするため上書き
self.set_state(ButtonState.DISABLE)
self.configure(image=self.disabled_image)
else:
self.remove_state(ButtonState.DISABLE)
self.configure(image=self.normal_image)
def add_state(self, state:ButtonState) -> None:
"""ステートを追加
Args:
state (ButtonState): ステート
"""
assert isinstance(state, ButtonState), "not support type."
self.button_state |= state.value
def set_state(self, state:ButtonState) -> None:
"""ステートをセット
Args:
state (ButtonState): ステート
"""
assert isinstance(state, ButtonState), "not support type."
self.button_state = state.value
def remove_state(self, state:ButtonState) -> None:
"""ステートを削除
Args:
state (ButtonState): ステート
"""
assert isinstance(state, ButtonState), "not support type."
self.button_state &= ~state.value
def is_set_state(self, state:ButtonState) -> bool:
"""ステートがセットされているか
Args:
state (ButtonState): ステート
Returns:
bool: セットされている場合はTrueを返します。
"""
assert isinstance(state, ButtonState), "not support type."
return self.button_state & state.value
def on_left_button_press(self, event:tk.Event) -> None:
"""左クリックが押された瞬間
Args:
event (tk.Event): イベントプロパティ
"""
if self.is_disable:
return
self.add_state(ButtonState.ACTIVE)
self.configure(image=self.active_image)
def on_left_button_release(self, event:tk.Event) -> None:
"""左クリックが離された瞬間
Args:
event (tk.Event): イベントプロパティ
"""
if self.is_disable:
return
is_call_command = self.is_active and self.is_hover
self.remove_state(ButtonState.ACTIVE)
self.configure(image=self.hover_image if self.is_hover else self.normal_image)
if is_call_command:
self.safe_command()
def on_enter(self, event:tk.Event) -> None:
"""ボタンにマウスカーソルが置かれた、合わせた
Args:
event (tk.Event): イベントプロパティ
"""
if self.is_disable:
return
self.add_state(ButtonState.HOVER)
self.configure(image=self.active_image if self.is_active else self.hover_image)
def on_leave(self, event:tk.Event) -> None:
"""ボタンからマウスカーソルが離れた
Args:
event (tk.Event): イベントプロパティ
"""
if self.is_disable:
return
self.remove_state(ButtonState.HOVER)
self.configure(image=self.hover_image if self.is_active else self.normal_image)
def safe_command(self) -> None:
"""例外全無視コールバック実行
"""
if self.command is None:
return
try:
self.command()
except Exception as e:
pass
@staticmethod
def create_photo_image(path_or_data:Union[str, Image.Image]) -> ImageTk.PhotoImage:
"""PhotoImageの作成
画像読込や不正なImageの場合は、32x32ピクセルのパープルで塗りつぶした画像を作成します。
Args:
path_or_data (Union[str, Image.Image]): 画像パスかImage
Returns:
ImageTk.PhotoImage: PhotoImage
"""
try:
if isinstance(path_or_data, str):
return ImageTk.PhotoImage(Image.open(path_or_data))
elif isinstance(path_or_data, Image.Image):
return ImageTk.PhotoImage(path_or_data)
except Exception as e:
return ImageTk.PhotoImage(Image.new("RGB", (32, 32), (255, 0, 255)))
サンプル

| リソースパス | 画像 右クリック > 名前を付けて画像を保存...でサンプルを再現可能です。 |
|---|---|
| resources\sample_normal.png | ![]() |
| resources\sample_normal.png | ![]() |
| resources\sample_normal.png | ![]() |
| resources\sample_normal.png | ![]() |
| resources\sample_normal.png | ![]() |
class SampleWindow(ttk.Window):
def __init__(self) -> None:
super().__init__()
sample_paths = [
r"resources\sample_normal.png",
r"resources\sample_hover.png",
r"resources\sample_active.png",
r"resources\sample_disabled.png",
]
sample_sprite = cv2.imread(r"resources\sample_sprite.png")
sample_sprite = cv2.cvtColor(sample_sprite, cv2.COLOR_BGR2RGB)
sample_images = [
Image.fromarray(sample_sprite[ 0:20, 0:20], mode="RGB"),
Image.fromarray(sample_sprite[ 0:20, 20:40], mode="RGB"),
Image.fromarray(sample_sprite[ 0:20, 40:60], mode="RGB"),
Image.fromarray(sample_sprite[ 0:20, 60:80], mode="RGB"),
]
self.sample_image_button = ImageButton(self, *sample_paths)
self.sample_image_button.grid(column=0, row=0, sticky=W)
self.sample2_image_button = ImageButton(self, *sample_images)
self.sample2_image_button.grid(column=1, row=0, sticky=W)
self.sample3_image_button = ImageButton(self, *sample_paths, is_disabled=True)
self.sample3_image_button.grid(column=2, row=0, sticky=W)
self.sample4_image_button = ImageButton(self, *sample_images, is_disabled=True)
self.sample4_image_button.grid(column=3, row=0, sticky=W)
if __name__ == "__main__":
app = SampleWindow()
app.mainloop()
参考
- ttkbootstrap – ttkbootstrap
- ImageTk Module – Pillow (PIL Fork) 11.0.0 documentation
- 商用可の無料(フリー)のアイコン素材をダウンロードできるサイト『icon rainbow』 | カラフルな商用利用可能なアイコン素材を無料でダウンロード!!




