Tkinterでシュルテ・テーブルをつくる リファクタリング編

 今日も見に来てくださってありがとうございます。まだ、朝は肌寒いですねぇ。

 さて、予言通り、先日のスクリプトをリファクタリングしたいと思います。ポイントは、まず、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という属性はありませんよ、と言われていますね。destroyTkの属性でしたね。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()

 どうでしょう、少しは分かりやすくなったでしょう。もしかして、単なる自己満足でしょうか。。。

コメントを残す

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


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。