HZK
@HZK (Ritoku Sakamae)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

tkinter:クラスのインスタンス化の引数に、Canvasのoptionを渡す方法

解決したいこと

 この半年間はクラス化を集中的に勉強しており、最近はネット上の記事を参考にして、自習しています。
 「デジタル時計作り方(7セグ表示版)」 https://daeudaeu.com/tkinter-digital-clock/ はとても参考になりました。
 次の課題として、クラスのインスタンス化の際に、引数を指定することで、表示色を変更することを考えました。具体的には、初期設定のオレンジ色を、引数で青色に変更を試みています。
 引数で、Canvas上の図形optionを変更することになるので、苦労しています。解決策や問題点など、ご教授お願いします。

発生している問題・エラー

 最初は、コード上は定数となっている色を、インスタンス作成時の引数で変更することを考えました。しかし、引数の追加処理が増えすぎたため断念しました。
 次に、インスタンス作成時に渡されたfillオプションで、色変更を検討しました。
https://memopy.hatenadiary.jp/entry/2017/06/17/172611
を参考にして、コードを修正しました。

 その結果、デジタル時計のコロン部分はうまくいきましたが、数字部分がうまくいきません。実行時には一瞬表示できるものの、時間更新がうまくいきません。どうやら動作はしていますが、表示されていません。
 引数である(self, master, cnf, **kw)についての理解が足りないのか、
afterにおける引数の渡し方が悪いのか、分かりません。
 いろいろと変数を追加・削除し、エラーが出なくなりましたが、ここまで4週間程の試行錯誤で、心が折れました。コードが長くて申し訳ありません。

該当するソースコード

# -*- coding: utf-8 -*-
import tkinter as tk
from datetime import datetime, timedelta, timezone

#幅の設定
SEG_WIDTH = 12
CANVAS_WIDTH_NUMBER = 60
CANVAS_WIDTH_COLON = 30

# 色の設定
COLOR_BG = "black"  
COLOR_SEG_ON = "orange"  
COLOR_SEG_OFF = "gray20"  

class ColonCanvas(tk.Canvas):
    canvas_width = CANVAS_WIDTH_COLON
    canvas_height = CANVAS_WIDTH_NUMBER * 2 - SEG_WIDTH

    def __init__(self, master=None, cnf={}, **kw):  
        super().__init__(master, cnf, **kw)
     
    def draw(self, master, cnf, **kw):
        for i in range(2):
            # 横方向の中心はキャンバスの中心
            center_x = ColonCanvas.canvas_width / 2
            # 縦方向の中心はキャンバスの1/3 or 2/3の位置
            center_y = (i + 1) * ColonCanvas.canvas_height / 3

            # 長方形の各辺の長さが20となるように座標を設定
            x1 = center_x - 10
            y1 = center_y - 10
            x2 = center_x + 10
            y2 = center_y + 10

            # 長方形を描画
            id_1=self.create_rectangle(x1, y1, x2, y2)
            #
            self.itemconfig(id_1, fill = "", width = 0)
            self.itemconfig(id_1, **kw)

class NumberCanvas(tk.Canvas):
    # 各種パラメータの設定。
    canvas_width = CANVAS_WIDTH_NUMBER
    seg_width = SEG_WIDTH
     #
    canvas_height = canvas_width * 2 - seg_width
    seg_length = canvas_width - seg_width

    # 数字0から9をセグ表示する際の、各セグの点灯or消灯の情報のリスト
    ON_OFF_INFOS = [
        # 上, 上左, 上右, 中, 下左, 下右, 下
        [True, True, True, False, True, True, True],  # 0
        [False, False, True, False, False, True, False],  # 1
        [True, False, True, True, True, False, True],  # 2
        [True, False, True, True, False, True, True],  # 3
        [False, True, True, True, False, True, False],  # 4
        [True, True, False, True, False, True, True],  # 5
        [True, True, False, True, True, True, True],  # 6
        [True, True, True, False, False, True, False],  # 7
        [True, True, True, True, True, True, True],  # 8
        [True, True, True, True, False, True, True]  # 9
    ]

    # 各セグ0〜9を描画するときの情報のリスト 
    SEG_DRAW_PARAMS = [
        # [回転するか?, 横方向の移動量, 縦方向の移動量]
        [False, canvas_width / 2, seg_width / 2],
        [True, seg_width / 2, canvas_height / 2 - seg_length / 2],
        [True, canvas_width - seg_width / 2, canvas_height / 2 - seg_length / 2],
        [False, canvas_width / 2, canvas_height / 2],
        [True, seg_width / 2, canvas_height / 2 + seg_length / 2],
        [True, canvas_width - seg_width / 2, canvas_height / 2 + seg_length / 2],
        [False, canvas_width / 2, canvas_height - seg_width / 2]
    ]
    # 基準セグの、六角形の、各頂点のx座標とy座標
    XS = [
        - canvas_width / 2 + seg_width,  # 左上
        - canvas_width / 2 + seg_width / 2,  # 左中
        - canvas_width / 2 + seg_width,  # 左下
        canvas_width / 2 - seg_width,  # 右下
        canvas_width / 2 - seg_width / 2,  # 右中
        canvas_width / 2 - seg_width  # 右上
    ]
    YS = [
        - seg_width / 2,  # 左上
        0,  # 左中
        seg_width / 2,  # 左下
        seg_width / 2,  # 右下
        0,  # 右中
        - seg_width / 2  # 右上
    ]

    def __init__(self, master=None, cnf={}, **kw):  
        super().__init__(master, cnf, **kw)
        self.segs = []

    def draw(self, master, cnf, **kw):
        '''各座標で7つのセグを一気に描画する'''
        for draw_param in NumberCanvas.SEG_DRAW_PARAMS:
            is_rotate, x_shift, y_shift = draw_param

            if is_rotate:
                # 回転必要な場合は、基準セグの頂点の座標を90度を回転
                r_xs = [-n for n in NumberCanvas.YS]
                r_ys = [n for n in NumberCanvas.XS]

            else:
                # 回転不要な場合は基準セグの頂点の座標をそのまま使用
                r_xs = NumberCanvas.XS
                r_ys = NumberCanvas.YS

            # 基準セグの各頂点を移動
            t_xs = [n + x_shift for n in r_xs]
            t_ys = [n + y_shift for n in r_ys]

            # 移動後の座標に六角形を描画
            seg = self.create_polygon(
                t_xs[0], t_ys[0],
                t_xs[1], t_ys[1],
                t_xs[2], t_ys[2],
                t_xs[3], t_ys[3],
                t_xs[4], t_ys[4],
                t_xs[5], t_ys[5],
                fill = COLOR_SEG_OFF,
                width = 0,
            )
            # 描画した六角形のIDをリストに格納
            self.segs.append(seg)

    def update(self, num, master, cnf, **kw):
        '''数字numをセグ表示する'''
        for seg, is_on in zip(self.segs, NumberCanvas.ON_OFF_INFOS[num]):
            if is_on:
                # 点灯する場合のセグの色
                color = COLOR_SEG_ON
                #工夫した箇所
                self.itemconfig(seg, fill = "")                
                self.itemconfig(seg, **kw)
            else:
                # 消灯する場合のセグの色
                color = COLOR_SEG_OFF
            
            #本来のコード
            #self.itemconfig(seg, fill = color)
            
class Timer:
    def __init__(self):
        self.JST = timezone(timedelta(hours=9))
    def time(self):
        now = datetime.now(tz=self.JST)
        return now.hour, now.minute, now.second

class Drawer:
    '''時計を描画するクラス'''
    CANVAS_NUMBER = 1
    CANVAS_COLON = 2

    def __init__(self, master=None, cnf={}, **kw):    
        # 各種設定を行なった後に時計の盤面を描画
        self.initSetting(master)
        self.createClock()

    def initSetting(self, master):
        '''時計描画に必要な設定を行う'''

        # ウィジェットの作成先を設定
        self.master = master
        # 作成するキャンバスの種類をリストにする
        self.canvas_types = [
            Drawer.CANVAS_NUMBER,
            Drawer.CANVAS_NUMBER,
            Drawer.CANVAS_COLON,
            Drawer.CANVAS_NUMBER,
            Drawer.CANVAS_NUMBER,
            Drawer.CANVAS_COLON,
            Drawer.CANVAS_NUMBER,
            Drawer.CANVAS_NUMBER
        ]

        self.number_canvases = []
        self.colon_canvases = []

    def createClock(self):
        #6個の数字キャンバスと2個のコロンキャンバスを作成
        for canvas_type in self.canvas_types:
            if canvas_type == Drawer.CANVAS_NUMBER:
                canvas = NumberCanvas(
                    self.master,
                    width=NumberCanvas.canvas_width,
                    height=NumberCanvas.canvas_height,
                    bg=COLOR_BG,
                    highlightthickness=0,
                )
                self.number_canvases.append(canvas)

            else:
                # コロン表示用のキャンバスを作成
                canvas = ColonCanvas(
                    self.master,
                    width=ColonCanvas.canvas_width,
                    height=ColonCanvas.canvas_height,
                    bg=COLOR_BG,
                    highlightthickness=0,
                )
                self.colon_canvases.append(canvas)

            # 左から順番にpackで詰めていく
            canvas.pack(side=tk.LEFT, padx=10, pady=10)

    def draw(self, master, cnf, **kw):        
        for canvas in self.colon_canvases:
            canvas.draw(master, cnf, **kw)

        for canvas in self.number_canvases:
            canvas.draw(master, cnf, **kw)
            
    def update(self, hour, minute, second, master, cnf, **kw):
        #個々のインスタンスに対して、メソッドを実行している:セグ表示を更新する'''
        nums = []
        nums.append(hour // 10)
        nums.append(hour % 10)
        nums.append(minute // 10)
        nums.append(minute % 10)
        nums.append(second // 10)
        nums.append(second % 10)

        for canvas, num in zip(self.number_canvases, nums):
            canvas.update(num, master, cnf, **kw)

class DigitalClock:
    def __init__(self, master=None, cnf={}, **kw):    
        self.master = master

        # 各種クラスのオブジェクトを生成
        self.timer = Timer()
        self.drawer = Drawer(master)

        # コロンとセグを描画する
        self.draw(master, cnf, **kw)

        # 時刻をセグ表示する
        self.update(master, cnf, **kw)

    def draw(self, master, cnf, **kw):
        '''コロンとセグを描画する'''
        self.drawer.draw(master, cnf, **kw)

    def update(self, master, cnf, **kw):  
        '''時刻をセグ表示を更新する'''

        hour, minute, second = self.timer.time()        

        self.drawer.update(hour, minute, second, master, cnf, **kw)
    
        self.master.after(1000, self.update, master, cnf)

if __name__ == "__main__":
    app = tk.Tk()
    # 背景色を設定
    app.config(bg=COLOR_BG)
    DigitalClock(app, fill="blue")

    app.mainloop()


0

1Answer

self.master.after(1000, self.update, master, cnf)**kw を渡していないのが原因かと思いますが、試しにafterメソッドの引数に**kwを追加してみたところ残念ながら実行時エラーになってしまいました。
定数と言っても変更可能なので、処理途中で別の値を再代入することができますよ。
定数を各クラス変数に持たせてしまう手も考えられますね。
インスタンス生成時に指定するのであれば、インスタンス変数に保存してはいかがでしょうか。
引数が増えるようであれば、cnf引数同様に辞書型にすればいいかと思います。

下記コードでは、引数名を color_seg_on にしてみました。不要な引数は削除しています。
他にもいろいろ手を加えてみました。
参考になりましたら幸いです。

import tkinter as tk
from datetime import datetime, timedelta, timezone

# サイズ設定
SEG_WIDTH = 12  # このサイズを変更するだけで全体サイズが連動して変更される
SEG_HEIGHT = SEG_WIDTH * 4
COLON_SIZE = SEG_WIDTH * 5 // 3
CANVAS_WIDTH_NUMBER = SEG_WIDTH * 5
CANVAS_WIDTH_COLON = CANVAS_WIDTH_NUMBER // 2
CANVAS_HEIGHT = CANVAS_WIDTH_NUMBER * 2 - SEG_WIDTH
CANVAS_PAD = SEG_WIDTH * 5 // 6

# 色の設定
COLOR_BG = "black"
COLOR_SEG_ON = "orange"
COLOR_SEG_OFF = "gray20"


class ColonCanvas(tk.Canvas):
    '''コロンを表示するクラス'''
    WIDTH = CANVAS_WIDTH_COLON
    HEIGHT = CANVAS_HEIGHT

    def __init__(self, master, color_seg_on, cnf={}, **kw):
        super().__init__(master, cnf, **kw)
        self.color_seg_on = color_seg_on
        self._build()

    def _build(self):
        '''コロン(縦並びの正方形2個)を作成する'''
        # 横方向の中心はキャンバスの中心
        center_x = self.WIDTH // 2
        # 縦方向の中心はキャンバスの1/3 と 2/3の位置
        for center_y in self.HEIGHT * 1 // 3, self.HEIGHT * 2 // 3:
            # 正方形を作成
            x1 = center_x - COLON_SIZE // 2
            y1 = center_y - COLON_SIZE // 2
            x2 = center_x + COLON_SIZE // 2
            y2 = center_y + COLON_SIZE // 2
            id_ = self.create_rectangle(x1, y1, x2, y2)
            self.itemconfig(id_, fill=self.color_seg_on, width=0)


class DigitCanvas(tk.Canvas):
    '''数字1桁を7セグメントで表示するクラス'''
    WIDTH = CANVAS_WIDTH_NUMBER
    HEIGHT = CANVAS_HEIGHT

    # 0~9の数字を表示する7セグメントの点灯(True)消灯(False)情報
    SEG_ON = (
        # 上, 左上, 右上, 中, 左下, 右下, 下
        (True, True, True, False, True, True, True),  # 0
        (False, False, True, False, False, True, False),  # 1
        (True, False, True, True, True, False, True),  # 2
        (True, False, True, True, False, True, True),  # 3
        (False, True, True, True, False, True, False),  # 4
        (True, True, False, True, False, True, True),  # 5
        (True, True, False, True, True, True, True),  # 6
        (True, True, True, False, False, True, False),  # 7
        (True, True, True, True, True, True, True),  # 8
        (True, True, True, True, False, True, True)  # 9
    )
    # 横長六角形の基準セグメントの各頂点のx座標とy座標
    SEG_XS = (
        -WIDTH // 2 + SEG_WIDTH,  # 左上
        -WIDTH // 2 + SEG_WIDTH // 2,  # 左中
        -WIDTH // 2 + SEG_WIDTH,  # 左下
        +WIDTH // 2 - SEG_WIDTH,  # 右下
        +WIDTH // 2 - SEG_WIDTH // 2,  # 右中
        +WIDTH // 2 - SEG_WIDTH,  # 右上
    )
    SEG_YS = (
        -SEG_WIDTH // 2,  # 左上
        0,  # 左中
        +SEG_WIDTH // 2,  # 左下
        +SEG_WIDTH // 2,  # 右下
        0,  # 右中
        -SEG_WIDTH // 2,  # 右上
    )
    # 基準セグメント座標を7セグメントの表示位置に変換するための回転・移動情報
    SEG_TRANSFORM = (
        # [回転するか?, 横方向の移動量, 縦方向の移動量]
        (False, WIDTH // 2, SEG_WIDTH // 2),  # 横上
        (True, SEG_WIDTH // 2, HEIGHT // 2 - SEG_HEIGHT // 2),  # 縦左上
        (True, WIDTH - SEG_WIDTH // 2, HEIGHT // 2 - SEG_HEIGHT // 2),  # 縦右上
        (False, WIDTH // 2, HEIGHT // 2),  # 横中
        (True, SEG_WIDTH // 2, HEIGHT // 2 + SEG_HEIGHT // 2),  # 縦左下
        (True, WIDTH - SEG_WIDTH // 2, HEIGHT // 2 + SEG_HEIGHT // 2),  # 縦右下
        (False, WIDTH // 2, HEIGHT - SEG_WIDTH // 2)  # 横下
    )

    def __init__(self, master, color_seg_on, cnf={}, **kw):
        super().__init__(master, cnf, **kw)
        self.color_seg_on = color_seg_on
        self._build()

    def _build(self):
        '''基準セグメント座標を回転・移動して7つの六角形セグメントを作成する'''
        self.segs = []
        for is_rotate, x_shift, y_shift in self.SEG_TRANSFORM:
            if is_rotate:
                # 回転必要な場合は、基準セグメントの頂点の座標を90度を回転
                r_xs = [-n for n in self.SEG_YS]
                r_ys = [n for n in self.SEG_XS]
            else:
                # 回転不要な場合は、基準セグメントの頂点の座標をそのまま使用
                r_xs = self.SEG_XS
                r_ys = self.SEG_YS

            # 基準セグメントの各頂点を移動
            t_xs = [n + x_shift for n in r_xs]
            t_ys = [n + y_shift for n in r_ys]

            # 移動後の座標に六角形を作成してリストに追加
            seg = self.create_polygon(
                *[xy for xys in zip(t_xs, t_ys) for xy in xys],
                fill=COLOR_SEG_OFF,
                width=0,
            )
            self.segs.append(seg)

    def update(self, digit):
        '''digitの数字を7セグメントで表示する'''
        for seg, seg_on in zip(self.segs, self.SEG_ON[digit]):
            color = self.color_seg_on if seg_on else COLOR_SEG_OFF
            self.itemconfig(seg, fill=color)


class Timer:
    '''現在時刻を取得するクラス'''
    JST = timezone(timedelta(hours=9))

    @classmethod
    def time(cls):
        now = datetime.now(tz=cls.JST)
        return now.hour, now.minute, now.second, now.microsecond


class Display:
    '''時刻(時分秒)を表示するクラス'''
    CANVAS_CLASSES = (
        DigitCanvas,  # 時の10の桁
        DigitCanvas,  # 時の1の桁
        ColonCanvas,
        DigitCanvas,  # 分の10の桁
        DigitCanvas,  # 分の1の桁
        ColonCanvas,
        DigitCanvas,  # 秒の10の桁
        DigitCanvas,  # 秒の1の桁
    )

    def __init__(self, master, color_seg_on):
        '''6個の数字キャンバスと2個のコロンキャンバスを作成'''
        self.digit_canvases = []
        for canvas_class in self.CANVAS_CLASSES:
            canvas = canvas_class(
                master,
                color_seg_on,
                width=canvas_class.WIDTH,
                height=canvas_class.HEIGHT,
                bg=COLOR_BG,
                highlightthickness=0,
            )
            if canvas_class == DigitCanvas:
                self.digit_canvases.append(canvas)
            # 左から順番にpackで詰めていく
            canvas.pack(side=tk.LEFT, padx=CANVAS_PAD, pady=CANVAS_PAD)

    def update(self, hour, minute, second):
        '''時分秒の各桁をDigitCanvasに表示する'''
        digits = (
            *divmod(hour, 10),
            *divmod(minute, 10),
            *divmod(second, 10),
        )
        for canvas, digit in zip(self.digit_canvases, digits):
            canvas.update(digit)


class DigitalClock:

    def __init__(self, master, color_seg_on):
        self.master = master
        self.display = Display(master, color_seg_on)

    def update(self):
        '''表示時刻を更新する'''
        hour, minute, second, microsecond = Timer.time()
        self.display.update(hour, minute, second)
        # 単に1000を指定すると処理遅延が蓄積して一度に2秒進むことがあるため、
        # microsecondを使って次の秒までの残り時間を計算して指定する
        self.master.after(1000 - microsecond // 1000, self.update)


if __name__ == "__main__":
    app = tk.Tk()
    app.title("Digital Clock")
    app.config(bg=COLOR_BG)
    clock = DigitalClock(app, color_seg_on="blue")
    clock.update()  # オブジェクト生成が終わってから更新処理開始
    app.mainloop()  # 描画・イベント処理ループ開始
1Like

Comments

  1. @HZK

    Questioner

    @shiracamus
    また返事を頂き、ありがとうございます。
     この半年間は前回頂いた助言をもとに、クラス化を重点的に学習していました。今回の回答も、複数の貴重な助言が含まれ、まだ全部を消化しきれてない状況です。そこで、一旦感謝の意を表明させて頂くほかに、教えて頂きたいことがあります。

    update関数のafterの引数指定の箇所
    >単に1000を指定すると処理遅延が蓄積して一度に2秒進むことがあるため
     この箇所は調べても良く分かりませんでした。ただ、前にtkinterでアナログ時計を自作した時に、思い当たる現象がありました。
     microsecondを使って秒針よりも早い動きの針と点を盤面上を周回させた時に、点の場合においては、最初はうまく周回できていたのが、徐々に動きが飛び飛びとなり最終的には3~4つの点に集約されるというものです

    > microsecondを使って次の秒までの残り時間を計算して指定する
    これをすることで、1ループ毎に処理される数値が異なって入力されるから、蓄積が発生せず、処理遅延しないという意味でしょうか。

  2. 1000を指定すると、処理時間+1000ミリ秒後に呼び出されます。
    例えばupdate処理に(極端ですが)300ミリ秒かかるとして、09:30から開始すると、
    09:30:00.000
    09:30:01.300 (update処理時間300ミリ秒+1000ミリ秒後に呼び出される)
    09:30:02.600 (処理時間の遅れが蓄積されていく)
    09:30:03.900
    09:30:05.200
    という時刻に呼び出され、09:30:04 が表示されないことになります。
    マイクロ秒で時間調整すると処理時間にブレがあってもほぼ1秒毎に呼び出せます。
    09:30:00.000
    09:30:01.293 (関数先頭での時間から計算してるので処理時間分は遅延する)
    09:30:02.302 (次の.000までの時間を指定してるのでほぼ1秒後に呼び出される)
    09:30:03.287
    09:30:04.329
    のような感じで。
    updateメソッドで現在時刻をマイクロ秒までprintして確認してみてください。

  3. @HZK

    Questioner

    返事が遅れて申し訳ありませんでした。ようやくコード全体の理解ができました。
    今回学習した内容を列挙すると以下となります。

    1)冒頭での定数まとめと入力数値の単純化
    2)メソッド先頭のアンダースコア
    3)tkinterにおける色指定の引数の設定方法
    4)定数命名のセンス
    5)クラスメソッド
    6)tk.Canvas継承クラスにおける初期化メソッド修正により、クラス呼出しで描画可能にする
    7)複数キャンバス整列クラスにおけるコードの効率化
    8)三項演算子による描画の座標指定
    9)*によるリスト内容の展開
    10)divmodeの使い方
    11)afterの引数指定のテクニック
    なお、負荷が重くないのか、以下のようにprint上は有意な差がみられませんでした。

    microsecond処理した場合
    18 56 3325
    18 57 3108
    18 58 2925
    18 59 3328
    19 0 2407
    19 1 2528
    19 2 10211

    1000のままの場合
    21 45 573754
    21 46 575647
    21 47 578183
    21 48 580854
    21 49 584325
    21 50 586977
    21 51 589547

    しかし、microsecond処理の潜在能力は十分に伝わりました。
    12)main関数でオブジェクト生成と更新を明確に区別すること。  以上。

    参考書の知識だけだと具体性が乏しいためか理解の定着が悪いですが、
    本件助言のように生きた知識はとても学習効果が高いため感銘を受けています。
    今回も想定以上に多くの収穫が得られており、本当にありがとうございました。

  4. なお、負荷が重くないのか、以下のようにprint上は有意な差がみられませんでした。

    microsecond処理の方は
    18 56 003325
    18 57 003108
    18 58 002925
    18 59 003328
    19 00 002407
    19 01 002528
    19 02 010211
    という時刻に呼び出されていて、処理時間のブレはあるものの、ほぼ1秒間隔(毎秒003ミリ秒あたり)で呼ばれています。
    処理時間は3ミリ秒程度のようです。

    1000のままの方は3ミリ秒くらいずつ着々と増えていて遅れが蓄積されていますよね。
    いずれ 998000 を超え、その次に表示が1秒飛ぶことになります。

    あと、divmode ではなく divmod (division and modulo)です。

  5. @HZK

    Questioner

    @shiracamus
    重ね重ねのコメント、本当にありがとうございます。
    これで、処理時間の見方も理解できました。
    今後もご指導賜れますと幸いです。

Your answer might help someone💌