最後に簡単な例題として、リサイズ可能なアナログ時計を作ってみましょう。キャンバスウィジェットに長針、短針、秒針を描き、1 秒ごとに針の位置を動かします。短針は動きを滑らかにするために、1 分ごとに位置を動かします。したがって、短針を動かす角度は 360 / (12 * 60) = 0.5 度となります。
今回はユーザからの入力がなくても時計を動かさないといけなので、単純なイベント駆動型アプリケーションでは「時計」を実現することはできません。このため、プログラム自身でなんらかのきっかけを作ってやる必要があります。このような場合、役に立つメソッドが after() です。
このように、after() には単純な時間待ちを行うほかに、一定時間後に指定した command を起動するタイマーの働きも持っています。たとえば、一定間隔で実行する関数を foo() としましょう。この場合、foo() の最後で after() を使って自分自身の起動を設定すればいいのです。具体的には次のようにプログラムします。
def foo():
# foo の処理
.....
root.after(500, foo)
root はメインウィンドウを表します。これで 500 msec 後に foo() が実行されます。もっとも、厳密に 500 msec ごとに foo() が実行されるわけではありません。foo() の処理にも時間がかかりますし、Windows はマルチタスクで動作しているので、ほかのタスクの影響も受けるからです。まあ、厳密なリアルタイム処理は必要としないので、これで十分です。
現在の日付と時刻を求めるには Python のモジュール time を使います。時間を求める主な関数を示します。
gmtime(), localtime() の返り値や strftime() の format で指定できる書式は Python のマニュアルを参照してください。今回は時間に関する書式を使います。
%H 時刻(00 - 23) %I 時刻 (01 - 12) %M 分(00 - 59) %S 秒(00 - 61)
strftime() を使えば、デジタル時計は簡単に作成することができます。
リスト : デジタル時計
import tkinter as tk
from time import *
root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 24))
buff = tk.StringVar()
buff.set('')
tk.Label(textvariable = buff).pack()
# 時刻の表示
def show_time():
buff.set(strftime('%I:%M:%S'))
root.after(1000, show_time)
show_time()
root.mainloop()
メニューでフォントを変更できるように改造すると、おもしろいと思います。
デジタル時計
最初に画面を設定します。ウィンドウが小さくなると時計がよく見えないので、ウィンドウの大きさを制限します。これはメソッド minsize() と maxsize() で設定することができます。幅と高さはピクセル単位で指定します。次のリストを見てください。
リスト : 画面の設定
# メインウィンドウ
root = tk.Tk()
root.title('aclock')
root.minsize(140, 140)
root.maxsize(500, 500)
# グローバル変数
width = 140
height = 140
sin_table = []
cos_table = []
backboard = []
# キャンバス
c0 = tk.Canvas(root, width = 140, height = 140, bg = 'lightgray')
c0.pack(expand = True, fill = tk.BOTH)
# 図形の生成
circle = c0.create_oval(5, 5, 135, 135)
for i in range(60):
if i % 5 == 0:
w = 2.0
else:
w = 1.0
backboard.append(c0.create_line(i, i, 135, 135, width = w))
hour = c0.create_line(70, 70, 70, 30, fill = 'blue', width = 5.0)
min = c0.create_line(70, 70, 70, 20, fill = 'green', width = 3.0)
sec = c0.create_line(70, 70, 70, 15, fill = 'red', width = 2.0)
ウィンドウの幅と高さは 140 から 500 ピクセルの範囲に制限します。背景の円と目盛を表す図形は、変数 circle とリスト backboard に格納します。針を表す図形は変数 hour, min, sec に格納します。ここは図形を生成するだけなので、位置はでたらめでもかまいません。
さて、問題はウィンドウがリサイズされた場合です。ここで発生するイベントが Configure です。このイベントをバインドして、ウィンドウの大きさが変わったら時計を再描画すればいいわけです。バインドはメインウィンドウに対して設定すれば大丈夫です。
root.bind('<Configure>', change_size)
キャンバスウィジェットは fill と expand を設定して pack されているので、ウィンドウの大きさが変わると、キャンバスの大きさも変わります。詳しい説明は拙作のページ ウィンドウのリサイズ をお読みくださいませ。このときに Configure イベントを受け取るので、時計の大きさを変える関数 change_size() を実行します。
キャンバスウィジェットの大きさですが、これは cget() メソッドでは求めることができません。実際、ウィンドウがリサイズされキャンバスウィジェットが引き伸ばされても、最初に設定されたオプションの値そのままになっています。キャンバスウィジェットの大きさを求めるには、ウィジェットの情報を取得するメソッド winfo_width() と winfo_height() を使います。change_size() は次のようになります。
リスト : 大きさの変更
def change_size(event):
global width, height
width = c0.winfo_width()
height = c0.winfo_height()
draw_backboard()
draw_hand()
width と height は時計の大きさを表すグローバル変数で、キャンバスと同じ大きさに初期化しておきます。キャンバスの幅と高さを求め、それらの値を width と height にセットします。
図形の配置は背景を関数 draw_backboard() で、針を関数 draw_hand() で行います。これらの関数は width と height にセットされた大きさに合わせて時計を描画します。描画は coords() メソッドで図形を移動させるだけです。
針を動かす関数 draw_hand() は次のようになります。
リスト : 針の描画
def draw_hand():
t = time.localtime()
w = width / 2
h = height / 2
# 秒
n = t[5] * 12
x = w + w * sin_table[n] * 7 / 8
y = h - h * cos_table[n] * 7 / 8
c0.coords(sec, w, h, x, y)
# 分
n = t[4] * 12
x = w + w * sin_table[n] * 6 / 8
y = h - h * cos_table[n] * 6 / 8
c0.coords(min, w, h, x, y)
# 時
h1 = t[3]
if h1 >= 12: h1 -= 12
n = h1 * 60 + t[4]
x = w + w * sin_table[n] * 4 / 8
y = h - h * cos_table[n] * 4 / 8
c0.coords(hour, w, h, x, y)
まず localtime() で現在時刻を求めます。返り値のタプルには、t[3] に時間、t[4] に分、t[5] に秒が格納されています。あとは、あらかじめ計算しておいた三角関数表 sin_table と cos_table を使って座標を計算し、メソッド coords() で針を移動させます。draw_backboard() も簡単なので説明は割愛いたします。詳細は プログラムリスト をお読みくださいませ。
あとは after() メソッドを使って、1秒ずつ針を動かします。
# 表示
def show_time():
draw_hand()
root.after(1000, show_time)
show_time() は draw_hand() を呼び出して針を描画し、1 秒後に show_time() を呼び出すよう after() メソッドで設定します。最後に show_time() を実行すれば、1 秒ごとに短針が動き、時計が動作します。
デフォルトサイズのアナログ時計
ウィンドウを縮小
ウィンドウを拡大
これで、リサイズ可能なアナログ時計を作ることができました。シンプルな時計なので、少々物足りないかもしれません。興味のある方は、プログラムを改造してみてください。
#
# aclock.py : アナログ時計
#
# Copyright (C) 2006-2023 Makoto Hiroi
#
import tkinter as tk
import math, time
# メインウィンドウ
root = tk.Tk()
root.title('aclock')
root.minsize(140, 140)
root.maxsize(500, 500)
# グローバル変数
width = 140
height = 140
sin_table = []
cos_table = []
backboard = []
# キャンバス
c0 = tk.Canvas(root, width = 140, height = 140, bg = 'lightgray')
c0.pack(expand = True, fill = tk.BOTH)
# 図形の生成
circle = c0.create_oval(5, 5, 135, 135)
for i in range(60):
if i % 5 == 0:
w = 2.0
else:
w = 1.0
backboard.append(c0.create_line(i, i, 135, 135, width = w))
hour = c0.create_line(70, 70, 70, 30, fill = 'blue', width = 5.0)
min = c0.create_line(70, 70, 70, 20, fill = 'green', width = 3.0)
sec = c0.create_line(70, 70, 70, 15, fill = 'red', width = 2.0)
# データの初期化
def init_data():
for i in range(720):
rad = 3.14 / 360 * i
sin_table.append(math.sin(rad))
cos_table.append(math.cos(rad))
# 背景の描画
def draw_backboard():
w = width / 2
h = height / 2
# 楕円
c0.coords(circle, 5, 5, width - 5, height - 5)
# 目盛
for i in range(60):
n = i * 12
if n % 5 == 0:
l = 0.9
else:
l = 0.95
x1 = w + (w - 5) * sin_table[n]
y1 = h + (h - 5) * cos_table[n]
x2 = w + (w - 5) * l * sin_table[n]
y2 = h + (h - 5) * l * cos_table[n]
c0.coords(backboard[i], x1, y1, x2, y2)
# 針を描く
def draw_hand():
t = time.localtime()
w = width / 2
h = height / 2
# 秒
n = t[5] * 12
x = w + w * sin_table[n] * 7 / 8
y = h - h * cos_table[n] * 7 / 8
c0.coords(sec, w, h, x, y)
# 分
n = t[4] * 12
x = w + w * sin_table[n] * 6 / 8
y = h - h * cos_table[n] * 6 / 8
c0.coords(min, w, h, x, y)
# 時
h1 = t[3]
if h1 >= 12: h1 -= 12
n = h1 * 60 + t[4]
x = w + w * sin_table[n] * 4 / 8
y = h - h * cos_table[n] * 4 / 8
c0.coords(hour, w, h, x, y)
# 大きさの変更
def change_size(event):
global width, height
width = c0.winfo_width()
height = c0.winfo_height()
draw_backboard()
draw_hand()
# 表示
def show_time():
draw_hand()
root.after(1000, show_time)
# バインディング
root.bind('<Configure>', change_size)
# データの初期化
init_data()
# 最初の起動
draw_backboard()
show_time()
# メインループ
root.mainloop()