【Python】tkinter で「ストップウォッチ」アプリを作成

ストップウォッチアプリ開発方法解説ページのアイキャッチ

今回は tkinter で「ストップウォッチ」アプリを作成していきたいと思います!

「ストップウォッチ」自体は動作や作りがシンプルですので、特に tkinter 初心者の方にオススメの題材だと思います。

作成する「ストップウォッチ」アプリ

まずはどんなストップウォッチアプリを作るのかを簡単に説明します。

画面の構成は下記のような感じにしようと思います。

ストップウォッチの画面構成

スタートボタンを押すと計測が開始されてラベルに計測時間が表示されます。

ラベルの計測時間はストップウォッチっぽく定期的に更新します。

ラベルに表示する時間を更新する様子

さらに計測中にストップボタンを押すと計測が終了し、ラベルの時間の更新も終了します。

ラベルの時間が停止する様子

ストップウォッチアプリの画面の開発

どんなアプリを作るかを決めたところで、次はアプリを作っていきましょう!

まずはアプリの画面(UI)から作っていきたいと思います。

先程決めたように、アプリの画面は下の図のようになります。

ストップウォッチの画面構成

まずはメインウィンドウを作成し、メインウィンドウの中にラベルとスタートボタン、ストップボタンを作成して配置していきます。

このアプリの画面を作成するスクリプトは下記のようになります。

ストップウォッチの画面
# -*- coding:utf-8 -*-
import tkinter

# スタートボタンの処理
def start():
	pass

# ストップボタンの処理
def stop():
	pass

# メインウィンドウ作成
app = tkinter.Tk()
app.title("stop watch")
app.geometry("200x200")

# 時間計測結果表示ラベル
label = tkinter.Label(
	app,
	text="0.00",
	width=6,
	font=("", 50, "bold"),
)
label.pack(padx=10, pady=10)

# ストップウォッチのスタートボタン
start_button = tkinter.Button(
	app,
	text="START",
	command=start
)
start_button.pack(pady=5)

# ストップウォッチのストップボタン
stop_button = tkinter.Button(
	app,
	text="STOP",
	command=stop
)
stop_button.pack(pady=5)

# メインループ
app.mainloop()

スクリプトを実行すれば、下の図のようなアプリの画面が表示されると思います。

スクリプト実行で表示される画面

上記スクリプトでは label は Label クラス、start_button と stop_button は Button クラスのインスタンスとして作成し、それぞれを pack メソッドにより配置しています。

各インスタンスの配置構成

配置については下記ページで解説していますので、配置について詳しく知りたい方はこちらをご覧ください。

ウィジェット配置方法解説ページのアイキャッチTkinterの使い方:ウィジェットの配置(pack・grid・place)

ボタンは配置していますが、現状だとボタンを押しても時間の計測は開始されません。

スタートボタンとストップボタンのインスタンス作成時(tkinter.Button() 実行時)に command を指定することで、それぞれのボタンが押された時に下記の関数が実行されるようになっています。

  • スタートボタン:start
  • ストップボタン:stop

上記スクリプトでは startstop も空の関数になっているのでボタンを押しても何も動作しません。

次はこの startstop などに処理を記述してストップウォッチの機能面の開発を行なっていきます

スポンサーリンク

ストップウォッチアプリの機能の開発

アプリの画面が出来上がったところで、次はストップウォッチとして動作してくれるように機能を開発していきましょう!

ストップウォッチアプリの機能

ストップウォッチアプリの処理の全体的な流れを図示したものが下図になります。

ストップウォッチアプリの機能

スタートボタンが押されたら「時間の計測開始」を行い、それ以降は定期的に「ラベルの時間更新」を行います。

最後にストップボタンが押されたら「時間の計測終了」を行います。

つまり、ストップウォッチアプリにおいては、大きく分けて下記の3つの機能が必要ということになります。

  • 時間の計測開始
  • ラベルの時間更新
  • 時間の計測終了

ここからは、これらの3つの機能の開発方法について解説していきたいと思います。特にストップウォッチアプリにおいては「ラベルの時間更新」がポイントとなりますので、この「ラベルの時間更新」から解説していきたいと思います。

ラベルの時間更新

ストップウォッチでは、計測時間(計測開始時点からの時間)が画面に表示され、停止するまでその計測時間が定期的に更新されていきます。

今回作成するアプリで計測時間を表示するのはラベルですので、定期的にラベルに表示する計測時間を更新する必要があります。

その方法について解説していきます。

ラベルの定期的な更新

tkinter でこういった定期的な処理を行うには、after メソッドが便利です。

after メソッドに関しては下記ページで解説していますので、こちらを参考にしていただければと思います。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

ポイントは、定期的に実行させたい関数の中で、after メソッドを実行するところです。

after メソッドで再度その関数を一定時間遅らせて実行するようにしてやることで、その関数が一度実行されると、以降は自動的に定期的に実行されるようになります(定期的に処理が行われる間隔は after メソッドの引数で指定可能です)。

今回定期的に行いたい処理は「計測時間のラベルへの表示」ですので、下記のように関数(update_time)を作成することで、「計測時間のラベルへの表示」が定期的に行われるようになります。

after メソッドの第1引数で INTERVAL(中身は 10) を設定していますので約 10ms 間隔で update_time 関数が定期的に実行されるようになります。

定期的なラベルの更新
# ラベルを更新する間隔[ms]
INTERVAL = 10

# 時間更新関数
def update_time():
	# update_time関数を再度INTERVAL[ms]後に実行
	after_id = app.after(INTERVAL, update_time)

	# 計測時間(elapsed_time_str)を計算

	# 計測時間を表示
	label.config(text=elapsed_time_str)

elapsed_time_str は計測時間です。

次はこの計測時間をどのようにして求めるかについて解説していきます。

時間の計測

時間の計測は time モジュールの time 関数(time.time()) を利用して行うことができます。

time 関数は「実行した時点の時刻を返却する関数」になります。

ですので、計測を開始するタイミングで time 関数を実行し、以降に実行した time 関数実行時の返却値との差を求めることで、計測開始から経過した時間を計測することができます。

  1. 時間の計測開始時に time 関数実行(返却値を start_time とする)
  2. 再度 time 関数実行(返却値を now_time とする)
  3. now_time - start_time の結果が 1. と 2. の間の時間になる

1. をストップウォッチのスタートボタンが押された時に実行し、2. と 3. をラベルの定期的な更新時に実行してやることで、ラベルにスタートボタンが押されてからの時間を定期的に更新しながら表示することができるようになります。

先ほど示した update_time 関数の中で 2. と .3 の処理を実行するようにしたものが下記のようになります。

定期的な時間の更新
# 時間更新関数
def update_time():
	global start_time
	global app, label
	global after_id

	# update_time関数を再度INTERVAL[ms]後に実行
	after_id = app.after(INTERVAL, update_time)

	# 現在の時刻を取得
	now_time = time.time()

	# 現在の時刻と計測開始時刻の差から計測時間計算
	elapsed_time = now_time - start_time

	# 表示したい形式に変換(小数点第2位までに変換)
	elapsed_time_str = '{:.2f}'.format(elapsed_time)
	
	# 計測時間を表示
	label.config(text=elapsed_time_str)

この update_time 関数が1度実行されれば、後に説明する after_cancel メソッドが実行されるまで、start_time を取得時点からの経過時間が定期的にラベルに更新されながら表示されるようになります。

時間の計測開始

「ラベルの時間更新」を行う update_time 関数が出来上がれば、ストップウォッチアプリはほぼ出来上がったようなものです。

残っている主な処理は下記の2つになります。

  • 「ラベルの時間更新」を開始する
  • 「ラベルの時間更新」を終了させる

前者をストップウォッチのスタートボタンクリック時に行い、スタートボタンクリック時に「時間の計測開始」が行えるようにします。

といっても「ラベルの時間更新」の開始は、単に update_time 関数を実行すれば良いだけです。

ただし、update_time 関数実行前には「時間の計測開始」時点の時刻(start_time)を設定しておくことも必要です。

時間の計測開始を行う start 関数は下記のようになります。

時間の計測開始
# スタートボタンの処理
def start():
	global app
	global start_time
	global after_id

	# 計測開始時刻を取得
	start_time = time.time()

	# update_timeをINTERVAL[ms] 後に実行
	after_id = app.after(INTERVAL, update_time)

update_time 関数を after メソッドから実行していますが、直接実行しても良いです。

start 関数はストップウォッチアプリの画面の開発のスクリプトでスタートボタン(start_button)クリック時に実行されるように command 設定が行われています。

ですので、スタートボタンクリック時に上記 start 関数が実行され、時間の計測・ラベルの時間の更新が開始されます。

時間の計測終了

時間の計測終了時(ストップウォッチのストップボタンクリック時)には下記の後者の処理を行います。

  • 「ラベルの時間更新」を開始する
  • 「ラベルの時間更新」を終了させる

update_time 関数は after メソッドにより定期的に実行され、終了処理を行わないとアプリが終了するまで延々とラベルの時間更新が行われることになります。

ですので、時間の計測終了ができるように、その定期的な処理の終了処理を行います。

定期的な処理は after メソッドが実行されることにより実現していますので、この after メソッドをキャンセルしてやることで、定期的な処理を終了させることができます。

after メソッドのキャンセルは after_cancel メソッドの実行により行うことができます。

詳細は下記ページで解説していますので詳しく知りたい方は読んでみてください。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

ここまで紹介してきた udpate_time 関数と start 関数では after メソッドの返却値を after_id に参照させていますので、after_cancel の引数にはこの after_id を指定すれば良いです。

時間の計測終了を行う stop 関数は下記のようになります。

時間の計測終了
# ストップボタンの処理
def stop():
	global after_id

	# update_time関数の呼び出しをキャンセル
	app.after_cancel(after_id)

stop 関数はストップウォッチアプリの画面の開発のスクリプトでストップボタン(stop_button)クリック時に実行されるように command 設定が行われています。

ですので、ストップボタンクリック時に上記 stop 関数が実行され、時間の計測・ラベルの時間の更新が終了されます。

ストップウォッチアプリのサンプルスクリプト

ここまでストップウォッチアプリの画面や各機能を別々に開発してきました。これら全てを合わせたストップウォッチアプリのサンプルスクリプトは下記のようになります。

ストップウォッチアプリ
# -*- coding:utf-8 -*-
import tkinter
import time

# ラベルを更新する間隔[ms]
INTERVAL = 10

# 計測開始時刻
start_time = 0

# 時間計測中フラグ
start_flag = False

# afterメソッドのID
after_id = 0

# 時間更新関数
def update_time():
	global start_time
	global app, label
	global after_id

	# update_time関数を再度INTERVAL[ms]後に実行
	after_id = app.after(INTERVAL, update_time)

	# 現在の時刻を取得
	now_time = time.time()

	# 現在の時刻と計測開始時刻の差から計測時間計算
	elapsed_time = now_time - start_time

	# 表示したい形式に変換(小数点第2位までに変換)
	elapsed_time_str = '{:.2f}'.format(elapsed_time)
	
	# 計測時間を表示
	label.config(text=elapsed_time_str)


# スタートボタンの処理
def start():
	global app
	global start_flag
	global start_time
	global after_id

	# 計測中でなければ時間計測開始
	if not start_flag:
		
		# 計測中フラグをON
		start_flag = True

		# 計測開始時刻を取得
		start_time = time.time()

		# update_timeをINTERVAL[ms] 後に実行
		after_id = app.after(INTERVAL, update_time)

# ストップボタンの処理
def stop():
	global start_flag
	global after_id

	# 計測中の場合は計測処理を停止
	if start_flag:

		# update_time関数の呼び出しをキャンセル
		app.after_cancel(after_id)

		# 計測中フラグをオフ
		start_flag = False

# メインウィンドウ作成
app = tkinter.Tk()
app.title("stop watch")
app.geometry("200x200")

# 時間計測結果表示ラベル
label = tkinter.Label(
	app,
	text="0.00",
	width=6,
	font=("", 50, "bold"),
)
label.pack(padx=10, pady=10)

# ストップウォッチのスタートボタン
start_button = tkinter.Button(
	app,
	text="START",
	command=start
)
start_button.pack(pady=5)

# ストップウォッチのストップボタン
stop_button = tkinter.Button(
	app,
	text="STOP",
	command=stop
)
stop_button.pack(pady=5)

# メインループ
app.mainloop()

start 関数と stop 関数に時間計測中かどうかを start_flag で判断する処理を加えていますが、それ以外は前述したものと同じになると思います。

実行すると下の図のような画面のアプリが起動し、

スクリプト実行で表示される画面

スタートボタンをクリックすることで時間の計測が開始され、

ラベルに表示する時間を更新する様子

ストップボタンをクリックすることで時間の計測が終了します。

ラベルの時間が停止する様子

再度スタートボタンをクリックすれば、再び 0 秒から時間の計測が再開されます。

after メソッドだけではダメな理由

このスクリプトを読んで「after メソッドで 10ms 毎に定期的にラベルの時間を更新しているのだから、わざわざ time 関数使わなくても update_time 関数の実行回数から時間が計測できるのでは?」と思う方もいるかもしれません。

めちゃめちゃいい気付きだと思います。が、after メソッドだけでは時間がずれてしまいます。

time 関数で時間を計測する場合と、time 関数は利用せず after メソッドだけで時間を計測する場合の両方の計測時間を表示するようにしたスクリプトが下記のようになります。

計測時間の比較
# -*- coding:utf-8 -*-
import tkinter
import time

# ラベルを更新する間隔[ms]
INTERVAL = 10

# 計測開始時刻
start_time = 0

# 時間計測中フラグ
start_flag = False

# afterメソッドのID
after_id = 0

# update_timeの実行回数
count = 0

# 時間更新関数
def update_time():
	global start_time
	global app, label, label_after
	global after_id
	global count

	# update_time関数を再度INTERVAL[ms]後に実行
	after_id = app.after(INTERVAL, update_time)

	# 現在の時刻を取得
	now_time = time.time()

	# 現在の時刻と計測開始時刻の差から計測時間計算
	elapsed_time = now_time - start_time

	# 表示したい形式に変換(小数点第2位までに変換)
	elapsed_time_str = '{:.2f}'.format(elapsed_time)
	
	# 計測時間を表示
	label.config(text=elapsed_time_str)

	# update_time関数の実行回数をカウントアップ
	count += 1

	# countから計測時間を計算
	after_time = count * INTERVAL

	# 表示したい形式に変換(小数点第2位までに変換)
	after_time_str = '{:.2f}'.format(after_time / 1000)

	# afterのみでの計測時間を表示
	label_after.config(text=after_time_str)

# スタートボタンの処理
def start():
	global app
	global start_flag
	global start_time
	global after_id
	global count

	# 計測中でなければ時間計測開始
	if not start_flag:
		
		# 計測中フラグをON
		start_flag = True

		# 計測開始時刻を取得
		start_time = time.time()

		# update_timeの実行回数を初期化
		count = 0

		# update_timeをINTERVAL[ms] 後に実行
		after_id = app.after(INTERVAL, update_time)

# ストップボタンの処理
def stop():
	global start_flag
	global after_id

	# 計測中の場合は計測処理を停止
	if start_flag:

		# update_time関数の呼び出しをキャンセル
		app.after_cancel(after_id)

		# 計測中フラグをオフ
		start_flag = False

# メインウィンドウ作成
app = tkinter.Tk()
app.title("stop watch")
app.geometry("200x250")

# 時間計測結果表示ラベル
label = tkinter.Label(
	app,
	text="0.00",
	width=6,
	font=("", 50, "bold"),
)
label.pack(padx=10, pady=10)

# afterのみでの時間計測結果表示ラベル
label_after = tkinter.Label(
	app,
	text="0.00",
	width=6,
	font=("", 50, "bold"),
)
label_after.pack(padx=10, pady=10)

# ストップウォッチのスタートボタン
start_button = tkinter.Button(
	app,
	text="START",
	command=start
)
start_button.pack(pady=5)

# ストップウォッチのストップボタン
stop_button = tkinter.Button(
	app,
	text="STOP",
	command=stop
)
stop_button.pack(pady=5)

# メインループ
app.mainloop()

スクリプトを実行すると下の図のような画面のアプリが表示されます。

afterのみを利用したストップウォッチとの比較

上側のラベルには、ストップウォッチアプリのサンプルスクリプトでも紹介した time 関数を利用して時間を計測した計測時間を表示しています。

下側のラベルには、time 関数を利用せず、update_time 関数の実行回数(count)を after メソッドにより定期的に update_time 関数を実行する間隔(INTERVAL、つまり 10ms)で掛け算することで計測した計測時間を表示しています。

スタートボタンを押すと下のようなアニメのように2つのラベルの数字が更新されていきます。

時間がずれる様子

ですが、下側のラベルの時間がちょっとずつ遅れて行ってしまいます。こんな感じで after メソッドだけだと時間がずれてしまいます。

この理由は下記ページでも解説していますが、after メソッドで指定した関数が実行されるのは、指定した時間経過した後かつアプリが暇な時です。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

ですので、上記スクリプトのように after メソッドで 10 ms という短い間隔で定期的に処理を実行する場合などは、アプリが忙しくて after メソッドに指定した update_time 関数が実行されるタイミングが遅れる場合があります。

このタイミングの遅れが積もり積もって、上の図のように time 関数利用時に比べて1秒以上の差が発生することになっています。

アプリが忙しくて update_time 関数の実行が遅れる様子は、アプリのサイズを変更したらしてみると分かりやすいと思います。

サイズ変更中にupdate_timeが実行されない様子

サイズ変更している間は after で指定した update_time 関数が実行されず、ラベルの更新が停止します。

update_time 関数が実行されないと count もカウントアップされないので、実行されなかった間の時間分、下側のラベルに表示される計測時間がズレることになります。

しかし、time を利用した計測時間を表示する上側のラベルの時間はズレません。

これは、update_time 関数の実行がたとえ遅れたとしても、その遅れた分 time 関数から返却される時刻も遅れた時刻になるためです。結果的に遅れた分も含めた計測時間を表示することができます。

after メソッドは定期的に処理を行うのに便利ですが、after メソッドだけに頼るとこんな感じで定期的な処理の実行タイミングがずれてアプリとして機能しなくなる可能性もあります。

今回 time 関数を利用したように、after メソッドでの定期処理の実行タイミングがずれることを考慮し、ずれても問題ないように設計することが重要です。もしくは定期処理の実行タイミングが多少ずれても問題ないアプリを作りましょう!

スポンサーリンク

まとめ

このページでは Python で tkinter を利用してストップウォッチアプリを開発する方法の解説・サンプルスクリプトの紹介を行いました。

ストップウォッチアプリは作りや動作がシンプルですので、tkinter 入門者の方にはおすすめの題材だと思います。

特に after メソッドをしっかり使えるようになるための練習に最適です。

定期的に処理を行うためには after メソッドが便利です。この after メソッドは tkinter でアプリ開発を行う上で必須級に重要なメソッドですので、しっかり覚えておきましょう!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です