概要
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は、削除時の確認メッセージを出さないようにします。