【Tkinter】Canvasのメソッドを使っておしゃれなデザインを作る

Tkinterは透過色を苦手とするツールで、ウィジェットの透過色はサポートされてません(tk8.6現在)。そのため円形やキャラクターなど複雑な画像を使った場合、余った部分は背景色が付いてきてしまい思ったようにデザインできません。

そこで便利なのがCanvasウィジェットの各メソッドです。

今回は以前作成したキッチンタイマーを、Canvasウィジェットの各メソッドと画像を使っておしゃれに改造してみましょう。

Tkinterでキッチンタイマーを作ってみよう

Tkinterでキッチンタイマーを作ってみよう

【Tkinter】Canvasのメソッドを使っておしゃれなデザインを作る

目次

ウィジェットのオプションを使って画像を表示させた場合

まずはボタンウィジェットのオプションを使って画像を表示させた場合です。

分ボタンの画像

元の分ボタンの代わりに、上の丸形のボタンを配置してみます。背景は透過させてあるpng画像です。

        # Minボタン
        self.min_button_img = tk.PhotoImage(file="./img/min_button.png")
        self.min_button = tk.Button(self.canvas_bg, text="分", image=self.min_button_img)
        self.min_button.place(x=50, y=110)
ウィジェットのオプションを使って画像を表示させた場合

このように、画像の周囲が四角く灰色背景になってしまいます。

ボタンの隆起はオプションで消すことができますが、周囲に背景色が入ってしまうのは避けられません。つまりアプリ全体の背景に画像を設定していたとしても、このボタンの周囲には四角く背景色が入ってしまいます。

これでは矩形以外の画像をきれいに配置できません。

これでは矩形以外の画像をきれいに配置できません

判定は四角形だとしても、せめてボタン周囲の背景色を透明にできればいいのですが…。

canvas.create_image()で画像を表示

そこで便利なのがCanvasウィジェットのcreate_imageメソッドです。

指定したCanvasウィジェットの中に画像を表示するためのメソッドで、新たにウィジェットを作成するわけではないため周囲に背景色が表示されることはなく、画像をそのまま表示できます。

        # Minボタン
        self.min_button_img = tk.PhotoImage(file="./img/min_button.png")
        self.canvas_bg.create_image(50, 110, anchor=tk.NW, image=self.min_button_img, tag="min_button_img")
create_imageメソッドで画像を表示

これなら丸や三角形、もっと複雑な画像も自由に使ってデザインすることができます。

実際にいくつかの画像を使ってタイマーのボタン部分をデザインしてみましょう。

import tkinter as tk

 # tk.Frameを継承したApplicationクラスを作成
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)

        # ウィンドウの設定
        master.title("キッチンタイマー")
        master.geometry("430x280") # タイマーの幅は430x280

        # 実行内容
        self.pack()
        self.create_widget()

    # create_widgetメソッドを定義
    def create_widget(self):

        # 全体の親キャンバス
        self.canvas_bg = tk.Canvas(self.master, width=430, height=280, bg="#333")
        self.canvas_bg.pack()

        # Minボタン
        self.min_button_img = tk.PhotoImage(file="./img/min_button.png")
        self.canvas_bg.create_image(20, 210, anchor=tk.NW, image=self.min_button_img, tag="min_button_img")

        # 秒ボタン
        self.sec_button_img = tk.PhotoImage(file="./img/sec_button.png")
        self.canvas_bg.create_image(80, 210, anchor=tk.NW, image=self.sec_button_img, tag="sec_button_img")

        # リセットボタン
        self.reset_button_img = tk.PhotoImage(file="./img/reset_button.png")
        self.canvas_bg.create_image(140, 210, anchor=tk.NW, image=self.reset_button_img, tag="reset_button_img")

        # スタート/ストップボタン
        self.start_button_img = tk.PhotoImage(file="./img/start_button.png")
        self.canvas_bg.create_image(310, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
create_imageメソッドを使ったボタンデザイン

随分良くなりました。

以前のコンクリート打ちっぱなしのようなデザインに比べればかなりマシです。

canvas.create_text()で文字を表示

画像だけでなく、文字もラベルウィジェットで表示してしまうと背景色が付いてしまいますが、こちらもCanvasウィジェットのcreate_textメソッドを使えば背景色を入れずに表示できます。

こちらのメソッドは元のタイマーの時点で使用しているのですが、改めて表示してみましょう。

import tkinter as tk
import math

 # tk.Frameを継承したApplicationクラスを作成
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)

        # ウィンドウの設定
        master.title("キッチンタイマー")
        master.geometry("430x280") # タイマーの幅は430x280

        # 変数定義
        self.left_min = 0
        self.left_sec = 0

        # 実行内容
        self.pack()
        self.create_widget()

    # create_widgetメソッドを定義
    def create_widget(self):

        # 全体の親キャンバス
        self.canvas_bg = tk.Canvas(self.master, width=430, height=280, bg="#333")
        self.canvas_bg.pack()

        # タイマーの背景画像
        self.timer_bg_img = tk.PhotoImage(file="./img/timer_bg.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
        self.canvas_bg.create_image(0, 0, anchor=tk.NW, image=self.timer_bg_img, tag="timer_bg")

        # 分の表示
        self.canvas_bg.create_text(280, 80, text=str(math.floor(self.left_min)).zfill(2) + ":", font=("MSゴシック体", "80", "bold"), fill="#ffffff", tag="min_text", anchor="e") # 分を表示

        # 秒の表示
        self.canvas_bg.create_text(280, 80, text=str(math.floor(self.left_sec)).zfill(2), font=("MSゴシック体", "80", "bold"), fill="#ffffff", tag="sec_text", anchor="w") # 秒を表示

        # Minボタン
        self.min_button_img = tk.PhotoImage(file="./img/min_button.png")
        self.canvas_bg.create_image(30, 210, anchor=tk.NW, image=self.min_button_img, tag="min_button_img")

        # 秒ボタン
        self.sec_button_img = tk.PhotoImage(file="./img/sec_button.png")
        self.canvas_bg.create_image(90, 210, anchor=tk.NW, image=self.sec_button_img, tag="sec_button_img")

        # リセットボタン
        self.reset_button_img = tk.PhotoImage(file="./img/reset_button.png")
        self.canvas_bg.create_image(150, 210, anchor=tk.NW, image=self.reset_button_img, tag="reset_button_img")

        # スタート/ストップボタン
        self.start_button_img = tk.PhotoImage(file="./img/start_button.png")
        self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
create_textメソッドを使った時間表示

フォントの種類などこだわればもっと良くなる気がしますが、とりあえず「余計な背景色を入れずに自由にデザインする」という目的は達成できたのでこれで進めていきます。

canvas.delete()で表示したものを消す

キャンバスウィジェットのメソッドを使って作成した画像やテキストを削除する場合は、キャンバスウィジェットのdeleteメソッドを使います。

画像やテキストの作成(表示)時にオプション引数でtagを指定しておき、消したい時はdeleteメソッドでそのtagを指定して削除します。

        self.timer_bg_img = tk.PhotoImage(file="./img/timer_bg.png") # 画像を読み込み

        # create_imageメソッドで画像を表示する際にtagオプションでtimer_bgというタグを付けておく
        self.canvas_bg.create_image(0, 0, anchor=tk.NW, image=self.timer_bg_img, tag="timer_bg")

        self.canvas_bg.delete("timer_bg") # timer_bgタグが付いたものを削除

        # create_textメソッドで文字を表示する際にtagオプションでmin_textというタグを付けておく
        self.canvas_bg.create_text(280, 80, text=str(math.floor(self.left_min)).zfill(2) + ":", font=("MSゴシック体", "80", "bold"), fill="#ffffff", tag="min_text", anchor="e")

        self.canvas_bg.delete("min_text") # min_textタグが付いたものを削除

tag_bind()で処理を紐づけする

キャンバスウィジェットのメソッドで表示した画像やテキストはウィジェットではないため、ボタンウィジェットのようにcommandオプションで処理を紐づけることができません。

画像やテキストの作成(表示)時にオプション引数でtagを指定しておき、tag_bindメソッドでそのタグに処理を紐づけることができます。

書き方は次のとおりです。

キャンバス名.tag_bind("付けたタグ名", "トリガーとなる操作(イベント)", コールバック関数)

# 例
self.canvas_bg.tag_bind("min_button_img", "<ButtonRelease-1>", self.minButton_released)

概ねbindメソッドと同じですので、bindメソッドがイマイチわからない方は次の記事をご参照ください。

【Tkinter】bindメソッドによるトリガーイベントとコールバック関数の紐づけ

完成させてみる

あとは元のキッチンタイマーを作ったときと同じように、処理を作り、各ボタンに紐づけて完成です。

今回作成したキッチンタイマーアプリのソースコードです。

import tkinter as tk
import time
import math
import sys

 # tk.Frameを継承したApplicationクラスを作成
class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)

        # ウィンドウの設定
        master.title("キッチンタイマー")
        master.geometry("430x280") # タイマーの幅は430x280

        # 変数定義
        self.timer_on = False # タイマーの状態
        self.start_time = 0 # 開始時間
        self.set_time = 0 # セット時間
        self.elapsed_time = 0 # 経過時間
        self.left_time = 0 # 残り時間
        self.left_min = 0
        self.left_sec = 0
        self.after_id = 0 # after_id変数を定義

        # 実行内容
        self.pack()
        self.create_widget()

    # create_widgetメソッドを定義
    def create_widget(self):

        # 全体の親キャンバス
        self.canvas_bg = tk.Canvas(self.master, width=430, height=280, bg="#333")
        self.canvas_bg.pack()

        # タイマーの背景画像
        self.timer_bg_img = tk.PhotoImage(file="./img/timer_bg.png")
        self.canvas_bg.create_image(0, 0, anchor=tk.NW, image=self.timer_bg_img, tag="timer_bg")

        # タイマーのテキスト
        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新

        # Minボタン
        self.min_button_img = tk.PhotoImage(file="./img/min_button.png")
        self.canvas_bg.create_image(30, 210, anchor=tk.NW, image=self.min_button_img, tag="min_button_img")
        self.canvas_bg.tag_bind("min_button_img", "<Button-1>", self.min_button_clicked)
        self.canvas_bg.tag_bind("min_button_img", "<ButtonRelease-1>", self.min_button_released)

        # secボタン
        self.sec_button_img = tk.PhotoImage(file="./img/sec_button.png")
        self.canvas_bg.create_image(90, 210, anchor=tk.NW, image=self.sec_button_img, tag="sec_button_img")
        self.canvas_bg.tag_bind("sec_button_img", "<Button-1>", self.sec_button_clicked)
        self.canvas_bg.tag_bind("sec_button_img", "<ButtonRelease-1>", self.sec_button_released)

        # resetボタン
        self.reset_button_img = tk.PhotoImage(file="./img/reset_button.png")
        self.canvas_bg.create_image(150, 210, anchor=tk.NW, image=self.reset_button_img, tag="reset_button_img")
        self.canvas_bg.tag_bind("reset_button_img", "<Button-1>", self.reset_button_clicked)
        self.canvas_bg.tag_bind("reset_button_img", "<ButtonRelease-1>", self.reset_button_released)

        # start/stopボタン
        self.start_button_img = tk.PhotoImage(file="./img/start_button.png")
        self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")
        self.canvas_bg.tag_bind("start_button_img", "<Button-1>", self.start_button_clicked)
        self.canvas_bg.tag_bind("start_button_img", "<ButtonRelease-1>", self.start_button_released)

# 各ボタンが押された時の処理

    # minボタンを押した時
    def min_button_clicked(self, event):

        if self.left_min < 59: # 最大59分まで
            self.set_time += 60 # セット時間をプラス
            self.left_min += 1
            self.update_min_text() # 分の表示更新
            self.layer_adjustment() # レイヤーの前後調整

        # 暗いボタンを表示
        self.min_button_shadow_img = tk.PhotoImage(file="./img/min_button_shadow.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
        self.canvas_bg.create_image(30, 210, anchor=tk.NW, image=self.min_button_shadow_img, tag="min_button_shadow_img")

    # minボタンを離した時
    def min_button_released(self, event):
        self.canvas_bg.delete("min_button_shadow_img") # 暗いボタンを消去

    # secボタンを押した時
    def sec_button_clicked(self, event):
        if self.left_sec < 59: # 最大59秒まで
            self.set_time += 1 # セット時間をプラス
            self.left_sec += 1
            self.update_sec_text() # 秒の表示更新
            self.layer_adjustment() # レイヤーの前後調整

        # 暗いボタンを表示
        self.sec_button_shadow_img = tk.PhotoImage(file="./img/sec_button_shadow.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
        self.canvas_bg.create_image(90, 210, anchor=tk.NW, image=self.sec_button_shadow_img, tag="sec_button_shadow_img")

    # secボタンを離した時
    def sec_button_released(self, event):
        self.canvas_bg.delete("sec_button_shadow_img") # 暗いボタンを消去

    # resetボタンを押した時
    def reset_button_clicked(self, event):
        self.set_time = 0 # セット時間をリセット
        self.left_min = 0 # 残り時間(分)をリセット
        self.left_sec = 0 # 残り時間(秒)をリセット

        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新

        self.layer_adjustment() # レイヤーの前後調整

        # 暗いボタンを表示
        self.reset_button_shadow_img = tk.PhotoImage(file="./img/reset_button_shadow.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
        self.canvas_bg.create_image(150, 210, anchor=tk.NW, image=self.reset_button_shadow_img, tag="reset_button_shadow_img")

    # resetボタンを離した時
    def reset_button_released(self, event):
        self.canvas_bg.delete("reset_button_shadow_img") # 暗いボタンを消去

    # start/stopボタンを押した時
    def start_button_clicked(self, event):

        if self.timer_on == False:
            # 暗いボタンを表示
            self.start_button_shadow_img = tk.PhotoImage(file="./img/start_button_shadow.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
            self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_shadow_img, tag="start_button_shadow_img")

        elif self.timer_on == True:
            # 暗いボタンを表示
            self.start_button_shadow_img = tk.PhotoImage(file="./img/stop_button_shadow.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
            self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_shadow_img, tag="start_button_shadow_img")

    # startボタンを離した時
    def start_button_released(self, event):
        self.canvas_bg.delete("start_button_shadow_img") # 暗いボタンを消去

        if self.set_time >= 1:

            if self.timer_on == False:

                # ボタン画像を変更
                self.canvas_bg.delete("start_button_img") # 画像を消去
                self.start_button_img = tk.PhotoImage(file="./img/stop_button.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
                self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")
                self.canvas_bg.tag_bind("start_button_img", "<Button-1>", self.start_button_clicked)
                self.canvas_bg.tag_bind("start_button_img", "<ButtonRelease-1>", self.start_button_released)

                self.layer_hidden_buttons() # ストップ以外のボタンを隠す

                # タイマー開始
                self.timer_on = True

                self.start_time =time.time() # 開始時間を代入
                self.update_time() # updateTime関数を実行

            elif self.timer_on == True:

                # タイマーストップ
                self.timer_on = False

                # ボタン画像を変更
                self.canvas_bg.delete("start_button_img") # 画像を消去
                self.start_button_img = tk.PhotoImage(file="./img/start_button.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
                self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")
                self.canvas_bg.tag_bind("start_button_img", "<Button-1>", self.start_button_clicked)
                self.canvas_bg.tag_bind("start_button_img", "<ButtonRelease-1>", self.start_button_released)

                self.layer_adjustment() # レイヤーの前後調整

                self.set_time = self.left_time
                app.after_cancel(self.after_id)

    # 表示時間の更新処理
    def update_time(self):
        self.elapsed_time = time.time() - self.start_time  # 経過時間を計算
        self.left_time = self.set_time - self.elapsed_time # 残り時間を計算
        self.left_min = self.left_time // 60 # 残り時間(分)を計算
        self.left_sec = self.left_time % 60 # 残り時間(秒)を計算

        self.update_min_text() # 分の表示更新
        self.update_sec_text() # 秒の表示更新

        if self.left_time > 0.1:
            self.after_id = self.after(10, self.update_time)
        else:
            self.timer_on = False

            self.set_time = self.left_time

            # ボタン画像を変更
            self.canvas_bg.delete("start_button_img") # 画像を消去
            self.start_button_img = tk.PhotoImage(file="./img/start_button.png") # イメージは、必ずインスタンス変数に代入する必要があります。そうしないと、関数を抜けたとたん変数のメモリーが 回収され、イメージが消えてしまうからです。
            self.canvas_bg.create_image(270, 160, anchor=tk.NW, image=self.start_button_img, tag="start_button_img")
            self.canvas_bg.tag_bind("start_button_img", "<Button-1>", self.start_button_clicked)
            self.canvas_bg.tag_bind("start_button_img", "<ButtonRelease-1>", self.start_button_released)

            self.layer_adjustment() # レイヤーの前後調整

# 個別の処理

    # 分の表示更新
    def update_min_text(self):
        self.canvas_bg.delete("min_text") # 表示時間(分)を消去
        self.canvas_bg.create_text(280, 80, text=str(math.floor(self.left_min)).zfill(2) + ":", font=("MSゴシック体", "80", "bold"), fill="#ffffff", tag="min_text", anchor="e") # 分を表示
        self.layer_hidden_buttons() # ストップ以外のボタンを隠す

    # 秒の表示更新
    def update_sec_text(self):
        self.canvas_bg.delete("sec_text") # 表示時間(秒)を消去
        self.canvas_bg.create_text(280, 80, text=str(math.floor(self.left_sec)).zfill(2), font=("MSゴシック体", "80", "bold"), fill="#ffffff", tag="sec_text", anchor="w") # 秒を表示
        self.layer_hidden_buttons() # ストップ以外のボタンを隠す

    # レイヤーの前列後列調整
    def layer_adjustment(self):
        self.canvas_bg.tag_raise("timer_bg")
        self.canvas_bg.tag_raise("min_text")
        self.canvas_bg.tag_raise("sec_text")
        self.canvas_bg.tag_raise("min_button_img")
        self.canvas_bg.tag_raise("sec_button_img")
        self.canvas_bg.tag_raise("reset_button_img")
        self.canvas_bg.tag_raise("start_button_img")
        self.canvas_bg.tag_raise("start_button_shadow_img")

    # ストップ以外のボタンを隠す
    def layer_hidden_buttons(self):
        self.canvas_bg.tag_raise("timer_bg")
        self.canvas_bg.tag_raise("min_text")
        self.canvas_bg.tag_raise("sec_text")
        self.canvas_bg.tag_raise("start_button_img")
        self.canvas_bg.tag_raise("start_button_shadow_img")

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()
完成させてみる

実際に動かしてみたものがこちらです。

おしゃれ勢からするとまだまだかもしれませんが、以前作ったものよりはだいぶ良くなったのではないでしょうか。

今回の作り方なら画像をそのまま表示に使えるので、もっとおしゃれにしたいという方は背景やディスプレイ、ボタンの画像にこだわればもっともっとおしゃれにすることができます。

シンプルなデザインであればプログラムで作ることもできますが、見た目をおしゃれにしたいのであればやはり画像編集ツールの方が自由度も高く便利です。

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

コメントを残す

頂いたコメントは一読した後表示させて頂いております。
反映まで数日かかる場合もございますがご了承下さい。