概要
Pythonの標準GUIライブラリTkinterを使って、デスクトップアプリを作ってみました。
その名も『BedtimeClock』。
簡単に言えば、自分の「就寝予定時刻」「起床予定時刻」「睡眠予定時間」を表示する時計です。
この記事では、このアプリの(主観的な)有用性と、開発時に得た学びを書いていこうと思います。
※詳しい使い方やソースコードなどはGitHubのホームページをご覧下さい。
Q. 何の役に立つの?
A. 寝る前についパソコンをいじり続けてしまい、寝るのが遅くなってしまうのを防ぐ。
目先の欲求――得てして、「なんでこんなことしたんだろう……」と後悔することになるもの――を跳ね除けるには、「現在の自分の行動が、将来の自分にどのような影響を与えるのか」を認識する必要があります。
しかし、だらだらとPCをいじっているとき、「もうこんな時間か」とは考えられますが、「自分は何時に寝ることになるのか」「自分は何時間寝られるのか」「寝不足だと明日の自分はどうなるのか」という、将来のことに意識が向きません。
ならば、「就寝予定時刻や睡眠予定時間など、将来の自分に関わる情報を見せることで、それをきっかけに将来の自分の姿を想像させることができるのではないか」と考えました。
それが『BedtimeClock』を開発しようと思ったきっかけです。
学び
以下では、このアプリの開発時に得た学びなどを記していこうと思います。
間違いがありましたら、コメントで指摘して頂けると嬉しいです。
※以下に記載しているソースコードには、簡略化や説明のためにGitHubのソースコードと変えている部分があります。
Tkinter
- ウィンドウを常に最前面表示させる方法
root = Tk()
root.attributes("-topmost", True)
- ウィンドウを右上に表示する方法
# WIN_WIDTH、WIN_HEIGHTはユーザ定義変数。
root = Tk()
win_x = root.winfo_screenwidth() - WIN_WIDTH
root.geometry(f"{WIN_WIDTH}x{WIN_HEIGHT}+{win_x}+0")
Canvas
の文字列を更新するときは、create_text
メソッドよりも、itemconfigure
メソッドを使う
BedtimeClockは時計なので、一定時間ごとに画面を更新する必要があります。
毎回create_text
を呼んでテキストを生成し直していたときと比べて、itemconfigure
を呼んでテキストのconfigを変更したときの方が、圧倒的に処理が軽くなりました。
# テキストを生成するときにidを取得する。
text_id = canvas.create_text(
220, 40, text='hoge',
font=('', 50), fill='blue'
)
# idを指定して、テキストの内容や色を変更する。
canvas.itemconfigure(text_id, text='piyo', fill='red')
- 処理を待機させたいときは、
after
メソッドを使う
時計の更新は、精密さをそこまで求めないのであれば、1秒に1回くらいで十分です。
その場合、while
ループで更新するのはCPUの無駄遣いです。
after
メソッドを使えば、指定時間後に処理を行わせることができるため、更新する必要のない間は処理を停止させておくことができます。
定期的に更新する場合、更新関数の中で更新関数の呼び出しを予約し、再帰的に呼び出され続けるようにします。
以下の例では、画面を初期化する関数も作っています。
root = Tk()
def init_UI():
# 略
# 1秒後に初回呼び出しを行う。
root.after(1000, update_UI)
def update_UI():
# 略
# 1秒後にこの関数を呼び出す。
roor.after(1000, update_UI)
CheckButton
はデフォルトでは不確定状態になる
CheckButton
は、何も指定しないと不確定状態(alternate
)になります。
これは、「ユーザによる入力が為されていない状態」を表すため、「チェックが入っていない状態」、すなわち「『チェックを入れない』という入力が為された状態」と区別されます。
なので、単にチェックが入っていない状態(非選択状態)を表したいなら、不確定状態を解除しなければなりません。
また、選択状態(selected
)を表したいなら、一度不確定状態を解除してから選択状態にする必要があります。
# 非選択状態にする。
checkButton.state(['!alternate'])
# 選択状態にする。
checkButton.state(['!alternate'])
checkButton.state(['selected'])
ListBox
の選択肢をリストで指定する方法
例えば、0以上23以下の値から選択させる場合は、以下のように記述します。
listBox.insert( tkinter.END, *range(0,24) )
これで、tkinter.END
(最終要素)の直前に要素を追加していく(つまり、追加した順番に要素が並ぶ)、という指定になります。
※*range(0,24)
と書くことでレンジを展開し、可変長引数にまとめて渡すことができる。
このように、シーケンス(レンジやリストなど)を複数の変数に展開して代入することを「アンパック代入」という。
ListBox
のデフォルト値を指定する方法
# 指定要素が表示されるようにスクロールさせる。
listBox.see(default_value)
# 指定範囲の要素を選択させる(第2引数を省略すると、1要素のみ)。
listBox.select_set(default_value)
CheckButton
の値をFrame
オブジェクトから取得する方法
CheckButton
が配置されているFrame
オブジェクトから値を取得するには、CheckButton
にフレーム内で一意の名前(ここではcb
)をつけておいて、それで探します。
※以下の例では関数を定義していますが、しなくても大丈夫です。
def get_child_by_name(frame, name):
for child in frame.winfo_children():
if child.winfo_name()==name:
return child
# 例:選択状態か否かを取得する。
is_selected = get_child_by_name(frame, 'cb').instate(['selected'])
ListBox
の値をFrame
オブジェクトから取得する方法
前述のCheckButton
の場合とほとんど同じです。
curselection
メソッドは、選択要素番号(0~)のタプルを返します。
def get_child_by_name(frame, name):
for child in frame.winfo_children():
if child.winfo_name()==name:
return child
# 選択されている値を取得する。
lb = get_child_by_name(frame, 'lb')
lb_idx = lb.curselection()[0]
lb_val = lb.get(lb_idx)
- exeファイルを実行したときに起こり得るエラーについて
Pyinstaller
で生成したexeファイルを実行すると、以下のようなエラーが出ました。
Can't find a usable init.tcl in the following directories: ...
列挙されているディレクトリの中の一つにinit.tcl
を配置することで解決しました。
auto-py-to-exe
について
auto-py-to-exeは、GUIでPyinstaller
の設定・実行ができるツールです。
後からspecファイルをいじったりしなくてもいいので、楽でした。
Python
- 時刻の正規表現
ここで言う時刻とは、「0以上23以下の数値と、0以上59以下の数値を:
でつなげたもの」を指すこととします。
0以上23以下の数値は、それぞれ次のように書けます。
値の範囲 | 正規表現 |
---|---|
0~9 | [0-9] |
10~19 | 1[0-9] |
20~23 | 2[0-3] |
0以上59以下の数値は、次のように書けます。
値の範囲 | 正規表現 |
---|---|
0~59 | [1-5]?[0-9] |
※?
は「直前の文字の0回または1回の繰り返し」を表します。
つまり、上記の正規表現は([0-9]|[1-5][0-9])
を簡略化したものと言えます。
これらを合わせると、時刻の正規表現は次のように書けます。
pattern = '([0-9]|1[0-9]|2[0-3]):[1-5]?[0-9]'
また、07:05
のように0
で埋める場合は以下のように書けます。
pattern = '((0?|1)[0-9]|2[0-3]):[0-5][0-9]'
(参考:『時刻』の正規表現つくってみた)
- Pythonからシェルコマンドを実行する方法
同期処理の場合は、subprocess
モジュールのrun
メソッドを使います。
subprocess.run(any_cmd)
非同期処理の場合は、subprocess
モジュールのPopen
メソッドを使います。
subprocess.Popen(any_cmd)
ここで、注意点が1つあります。
subprocess.run
やsubprocess.Popen
を呼び出しているpyファイルをexeファイル化して実行すると、次のようなエラーが発生することがあります。
[WinError 6] ハンドルが無効です。
このページによると、引数のstdin
やstderr
に何も指定されていないとハンドルが無効になり、エラーを吐くらしいです。
私の場合は、次のようにstdout
も指定しておかないとエラーを吐きました。
subprocess.Popen(
any_cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
- 実行中のexeファイル自身の絶対パスを取得する方法
実行中のpyファイル自身の絶対パスなら、次のように取得できます。
import os
pyfile_path = os.path.abspath(__file__)
__file__
には、python3
(またはpython
)コマンドの実行時に指定したパスが格納されます。
上記のコードでは、相対パスが指定されたときのために、os.path.abspath
メソッドを使って、必ず絶対パスが返ってくるようにしています。
しかし、この方法はexeファイルの場合には使えません。
私の環境では、C:\WINDOWS\TEMP\_MEI<数字列>\<exeファイル名>
というパスが返ってきました。
実行中のexeファイル自身の絶対パスを取得するには、sys.executable
か、sys.argv[0]
を参照します。
import sys
exefile_path1 = sys.executable
# or
exefile_path2 = sys.argv[0]
ちなみに、exeファイルのディレクトリを取得するには、次のように書きます。
import os, sys
exefile_dir = os.path.dirname(sys.executable)
シェル
schtasks
の使い方
schtasks
は、Windowsのタスクスケジューラを操作するコマンドです(詳しくはこちら)。
タスクスケジューラとは、決められた時間または一定間隔でプログラムやスクリプトを実行する機能です(引用元:Wikipedia)。
タスクを作成するには、/create
を使います。
例:毎日7:00
にC:\hoge\hoge.exe
を実行するhoge-task
を作成する。
schtasks /create /tn hoge-task /tr C:\hoge\hoge.exe /sc daily /st 07:00 /f
-
/tn
は、システム上で一意である必要があります。 - 実行するファイルを指定する
tr
には、ファイルに渡す引数も指定できます。 -
/st
で指定する時刻は、HH:mm
で指定します。 -
/f
は、指定したタスクが既に存在する場合でも警告を出さないようにします。つまり、自動的に上書きさせます。
タスクを削除するには、/delete
を使います。
例:hoge-task
を削除する。
schtasks /delete /tn hoge-task /f
- 指定したタスクが存在しない場合、エラーを吐きます。
-
/f
は、削除時の確認メッセージを出さないようにします。