今日も見に来てくださってありがとうございます。まだ、朝は肌寒いですねぇ。
さて、予言通り、先日のスクリプトをリファクタリングしたいと思います。ポイントは、まず、Frameを継承しているところですね。次は、コンスタント値を定義しているところ、そして、大枠とテキストをリフレッシュしているところを分離するところでしょうか。ちょっとやってみたいと思います。
Frameの変更
まずは、単純にFrameをTkに置き換えてみます。そして、実行。
(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
File "SchulteTable2.py", line 73, in <module>
shulteTable = SchulteTable()
File "SchulteTable2.py", line 22, in __init__
super().__init__(master)
File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk
(base) C:\work\tkinter_example>
思った通り、エラーがでましたが、、、まずは、Tkの初期化、__init__()のところですね。アプリケーションレベルの初期化になりますので、masterをパラメータで受け取る必要はありません。master
関連を削除していきたいと思います。そうすると、タイトルの設定も、self.master.title("Schulte Table")
ではなくて、self.title("Schulte Table")
と変更する必要がありますね。あと、self.pack()
となっている部分もFrameではなくなったので、削除します。あとは、importからもFrameを削除しましょう。これだけでも__init__()
が随分とスッキリしましたね。
修正前の__init__()
def __init__(self, master=None):
if master == None:
master = Tk()
super().__init__(master)
master.title("Schulte Table")
self.master = master
self.pack()
self.create_widgets()
self.bind("<Button-1>", self.redraw_text)
修正後の__init__()
def __init__(self):
super().__init__()
self.title("Schulte Table")
self.create_widgets()
self.bind("<Button-1>", self.redraw_text)
実行してみたところ、またしてもエラーが発生しました。
(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
File "SchulteTable2.py", line 73, in <module>
shulteTable = SchulteTable()
File "SchulteTable2.py", line 22, in __init__
super().__init__(master)
File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk
(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
File "SchulteTable2.py", line 69, in <module>
shulteTable = SchulteTable()
File "SchulteTable2.py", line 22, in __init__
self.create_widgets()
File "SchulteTable2.py", line 38, in create_widgets
command=self.master.destroy)
AttributeError: 'NoneType' object has no attribute 'destroy'
(base) C:\work\tkinter_example>
ええと、destroy
という属性はありませんよ、と言われていますね。destroy
はTk
の属性でしたね。Frame
を使っていたから、その親のmaster
から呼び出していたのでした。なので、ここのself.master.destroy
は、self.destroy
に変更します。再度実行してみますと、画面はバッチリ表示されましたが、クリックするとエラーがでました。
(base) C:\work\tkinter_example>python SchulteTable2.py
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 1705, in __call__
return self.func(*args)
File "SchulteTable2.py", line 43, in redraw_text
messagebox.showinfo("結果",f"かかった時間は、{self.elapse_time:0.2f}秒です。")
File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 2101, in __getattr__
return getattr(self.tk, attr)
AttributeError: '_tkinter.tkapp' object has no attribute 'elapse_time'
ほお、elapse_time
がありません、とおっしゃいますか。なるほど、self.text
が存在しないので、redraw_text
がうまく動作していないようです。なぜ、text
を__init__()
で初期化しなかったのでしょうね。ちょっと謎ですが、過去のことは追求してもわかりません。self.text
に変更して__init__()
の中へ移動して初期化するよう変更します。
初期化の問題は解決できたようですが、挙動がおかしくなりました。クリックしてスタートするのは問題なさそうです。ただスタートしたあとに再度クリックしたら停止するはずなのですけど、クリックする位置によっては、停止した後すぐにまたスタートしてしまいます。イベントのバインドに問題があるようです。挙動から考えると、Tk
を継承したトップレベルでバインドして、さらにその子供のウィジェットにもバインドしたときに、イベントに対する処理を二度実行してしまっているようです。Frameの場合はそんなことなかったのに、不思議ですね。今回は、トップレベルのイベントで充分ですので、個々のウィジェットのバインドは削除しましょう。
そして、classのすぐ下に定義してある変数を何とか整理したいです。名前空間がモジュール内にあるので、クラス定義の外に出してしまいましょう。そうすると、「self.」を書かなくてよくなります。それでもまだ横に長いのと、わかりやすさのために値をいったん変数に入れて、後で見たときに少し確認しやすくしましょう。あと、一回も参照されていない、self.line1
への代入はやめてしまいましょう。
いろいろと改善した結果が以下のソースです。
from tkinter import Tk, Button, Canvas, Label, messagebox
from random import sample
from time import time
MARGIN = 10
DISTANCE = 50
WIDTH = 5
HEIGHT = 5
FSIZE = 25
class SchulteTable(Tk):
def __init__(self):
super().__init__()
self.title("Schulte Table")
self.text = list()
self.create_widgets()
self.bind("<Button-1>", self.redraw_text)
def create_widgets(self):
self.info = Label(self, text="Tap anywhere to start", font=("",14))
self.info.pack(side="top")
width = MARGIN*2+WIDTH*DISTANCE
height = MARGIN*2+HEIGHT*DISTANCE
self.canvas = Canvas(self,width=width, height=height)
self.canvas.pack(side="top")
for i in range(WIDTH+1): #縦線
x0, y0 = MARGIN + i * DISTANCE, MARGIN
x1, y1 = MARGIN + i * DISTANCE, MARGIN + DISTANCE * HEIGHT
self.canvas.create_line(x0,y0,x1,y1)
for i in range(HEIGHT+1): #横線
x0, y0 = MARGIN, MARGIN + i * DISTANCE
x1, y1 = MARGIN + WIDTH * DISTANCE, MARGIN + i * DISTANCE
self.canvas.create_line(x0,y0,x1,y1)
self.quit = Button(self, text="QUIT", fg="red", command=self.destroy)
self.quit.pack(side="bottom")
def redraw_text(self, event):
if not len(self.text):
self.info["text"] = "Tap anywhere to STOP"
self.time = time()
self.after(100, self.time_update)
else:
for t in self.text:
self.canvas.delete(t)
self.text = list()
message = f"かかった時間は、{self.elapse_time:0.2f}秒です。"
messagebox.showinfo("結果",message)
self.info["text"] = "Tap anywhere to start"
return
counter = 0
answers = sample(range(1,WIDTH*HEIGHT+1),WIDTH*HEIGHT)
for x in range(WIDTH):
for y in range(HEIGHT): # 数字のテキスト描画
x0 = MARGIN + x * DISTANCE + DISTANCE / 2
y0 = MARGIN + y * DISTANCE + DISTANCE / 2
ans = str(answers[counter])
text = self.canvas.create_text(x0,y0,text=ans,font=("",FSIZE))
self.text.append(text)
counter+=1
def time_update(self):
if not len(self.text):
return
self.elapse_time = time() - self.time
self.info["text"] = f"Tap anywhere to STOP:{self.elapse_time:0.2f}"
if len(self.text):
self.after(10, self.time_update)
if __name__ == '__main__':
shulteTable = SchulteTable()
shulteTable.mainloop()
どうでしょう、少しは分かりやすくなったでしょう。もしかして、単なる自己満足でしょうか。。。