HZK
@HZK (Ritoku Sakamae)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

tkinter:アナログ時計のマイクロ秒針の動きが非連続になる

解決したいこと

 プログラム上の時計の良い点は、機械的動作の制限がないことと考えています。
そこで、アップルウォッチの文字盤に似せた上で、実際のアナログ時計ではみられないマイクロ秒針または秒点(まずは1秒で1周)を作成してみました。

 開始時には滑らかな円運動を描いていますが、やがて動きが緩慢となり、最終的には2、3箇所の座標をずれながら移動するようになります。
 おそらく処理速度の遅延が関与していると疑っていますが、これを解決できる方法、さらにはより早く周回させる方法はありますでしょうか。ご教授よろしくお願いします。

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

 マイクロ秒針・秒点が滑らかに動くには、update関数のafterの引数指定は4が適切と考えていますが、1分以内に動作が緩慢となります。
 ソースコード上では、マイクロ秒点で記述おり、マイクロ秒針は三連引用符で囲っています。両方の動作を比較した場合、マイクロ秒点の方が、動作がおかしくなるのが早い印象があります。

 なお、引数指定が1000の場合、処理速度の遅延を処理する方法として以下の記述を教えて頂きました。
self.master.after(1000 - microsecond // 1000, self.update)

 しかし、本件では引数の数値と処理速度が近似しているため、この手法は使うことができませんでした。

 なお、ソースコード上でこの記述をコメントアウトしていますが、これを実行した場合に、引数が単に1000の場合は、マイクロ秒点がわずかずつ進むので、処理速度の遅延の蓄積を視覚化できたのは、面白い現象だと感じました。

該当するソースコード

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

# キャンバスのサイズの設定
CANVAS_WIDTH = 400
CANVAS_HEIGHT = CANVAS_WIDTH
CANVAS_SIZE = CANVAS_WIDTH

# 針の長さの設定
LENGTH_HOUR_HAND = CANVAS_SIZE / 2 * 0.5
LENGTH_MINUTE_HAND = CANVAS_SIZE / 2 * 0.7
LENGTH_SECOND_HAND = CANVAS_SIZE / 2 * 0.8
LENGTH_MICROSEC_HAND = CANVAS_SIZE / 2 * 0.9

# 針の色の設定
COLOR_HOUR_HAND = "white"
COLOR_MINUTE_HAND = "white"
COLOR_SECOND_HAND = "red"
COLOR_MICROSEC_HAND = "orange"

# 針の太さの設定
WIDTH_HOUR_HAND = 10
WIDTH_MINUTE_HAND = 8
WIDTH_SECOND_HAND = 2
WIDTH_MICROSEC_HAND = 3

# 時計画面の色設定
BG_COLOR = "Gainsboro"
FG_COLOR = "gray"

# 時計の盤面を表す円の半径の設定
CLOCK_OVAL_RADIUS = CANVAS_SIZE / 2

# 時計の数字の位置の設定(中心からの距離)
DISTANCE_NUMBER = CANVAS_SIZE / 2 * 0.85

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
  
    @classmethod       
    def time_string(cls):
        now_s = datetime.now(tz=cls.JST)
        now_st =now_s.strftime('%H:%M:%S')
        return now_st    

class Drawer:
    def __init__(self, master):
        self.master = master

        # 描画した針のオブジェクトを覚えておくリストを用意
        self.hands = []
        # 針の色のリストを用意
        self.colors = [
            COLOR_HOUR_HAND, COLOR_MINUTE_HAND, COLOR_SECOND_HAND, COLOR_MICROSEC_HAND
        ]
        # 針の太さのリストを用意
        self.widths = [
            WIDTH_HOUR_HAND, WIDTH_MINUTE_HAND, WIDTH_SECOND_HAND, WIDTH_MICROSEC_HAND
        ]
        # 針の長さのリストを用意
        self.lengths = [
            LENGTH_HOUR_HAND, LENGTH_MINUTE_HAND, LENGTH_SECOND_HAND, LENGTH_MICROSEC_HAND
        ]
        # キャンバスの中心座標を覚えておく
        self.center_x = CANVAS_WIDTH / 2
        self.center_y = CANVAS_HEIGHT / 2

        self.createClock()
        
    def createClock(self):
        self.canvas = tk.Canvas(
            self.master,
            width=CANVAS_WIDTH,
            height=CANVAS_HEIGHT,
            highlightthickness=0,
        )
        self.canvas.pack()

        # 時計の盤面を表す円を描画する
        x1 = self.center_x - CLOCK_OVAL_RADIUS
        y1 = self.center_y - CLOCK_OVAL_RADIUS
        x2 = self.center_x + CLOCK_OVAL_RADIUS
        y2 = self.center_y + CLOCK_OVAL_RADIUS

        self.canvas.create_oval(
            x1, y1, x2, y2,
            fill=BG_COLOR,
            width=2,
            outline=FG_COLOR
        )

        # 時計の盤面上に数字を描画する
        for hour in range(1, 13):
            angle = hour * 360 / 12 - 90
            # 描画位置を計算
            x1 = self.center_x
            y1 = self.center_x
            dx = DISTANCE_NUMBER * math.cos(math.radians(angle))
            dy = DISTANCE_NUMBER * math.sin(math.radians(angle))
            x2 = x1 + dx
            y2 = y1 + dy

            self.canvas.create_text(
                x2, y2,
                font=("Times New Roman", 40),
                fill="white",
                text=str(hour)
            )
        #装飾
        x1 = self.center_x - CLOCK_OVAL_RADIUS*0.65
        y1 = self.center_y - CLOCK_OVAL_RADIUS*0.65
        x2 = self.center_x + CLOCK_OVAL_RADIUS*0.65
        y2 = self.center_y + CLOCK_OVAL_RADIUS*0.65
        self.canvas.create_oval(
            x1, y1, x2, y2,
            fill="silver",
            width=10,
            outline="lightgray"
        )

    # 針を描画する'''
    def drawHands(self, hour, minute, second, microsec):
        # 各線の傾きの角度を計算指定リストに追加
        angles = []

        #時針も1分ずつ動かす
        hour_minute = (hour*360/12) +(minute*30/60)
        angles.append(hour_minute - 90)        
        
        angles.append(minute * 360 / 60 - 90)
        angles.append(second * 360 / 60 - 90)

        angles.append(microsec*360/1000000-90)

        # 線の一方の座標をキャンバスの中心とする
        x0 = self.center_x
        y0 = self.center_y

        #リスト要素の最初の二つのみ
        for angle, length, width, color in zip(angles[0:2], self.lengths[0:2], self.widths[0:2], self.colors[0:2]):
            x1 = x0 + length * math.cos(math.radians(angle))
            y1 = y0 + length * math.sin(math.radians(angle))

            hand = self.canvas.create_line(
                x0, y0, x1, y1,
                fill=color,
                width=width,
                capstyle=tk.ROUND
            )

            # 描画した線のIDを覚えておくリスト
            self.hands.append(hand)
            
        #秒針
        x2 = x0 + self.lengths[2] * math.cos(math.radians(angles[2]))
        y2 = y0 + self.lengths[2] * math.sin(math.radians(angles[2]))
        
        x_1 = x0 -0.1*(self.lengths[2] * math.cos(math.radians(angles[2])))
        y_1 = y0 -0.1*(self.lengths[2] * math.sin(math.radians(angles[2])))
        
        hand2 = self.canvas.create_line(
                x_1, y_1, x0, y0, x2, y2,
                fill=self.colors[2],
                width=self.widths[2],
            )
        self.hands.append(hand2)
            
        #マイクロセカンド針と点の座標   
        x3 = x0 + self.lengths[3] * math.cos(math.radians(angles[3]))
        y3 = y0 + self.lengths[3] * math.sin(math.radians(angles[3])) 
        
        #マイクロセカンド針の場合
        """
        hand3 = self.canvas.create_line(
            x0, y0, x3, y3,
            fill=self.colors[3],
            width=self.widths[3],
            )       
        """ 
        
        #マイクロセカンド点の場合
        hand3 = self.canvas.create_oval(
            x3-7, y3-7, x3+7, y3+7,
            fill=self.colors[3],
            outline="",
        )
        # 描画した線のIDを覚えておくリスト
        self.hands.append(hand3)         

    #針を表現する線の位置を更新する
    def updateHands(self, hour, minute, second, microsec):
        angles = []
        #angles.append(hour * 360 / 12 - 90)
        
        hour_minute = (hour*360/12) +(minute*30/60)
        angles.append(hour_minute - 90)        
        
        angles.append(minute * 360 / 60 - 90)
        angles.append(second * 360 / 60 - 90)
        #マイクロ針は、1秒で1週
        angles.append(microsec*360/1000000-90)
        
        # 線の一方の点の座標は常に時計の中心
        x0 = self.center_x
        y0 = self.center_y

        for hand, angle, length in zip(self.hands[0:2], angles[0:2], self.lengths[0:2]):
            x1 = x0 + length * math.cos(math.radians(angle))
            y1 = y0 + length * math.sin(math.radians(angle))

            # coordsメソッドにより描画済みの線の座標を変更する
            hand = self.canvas.coords(
                hand,
                x0, y0, x1, y1
            )
        
        #秒針
        x2 = x0 + self.lengths[2] * math.cos(math.radians(angles[2]))
        y2 = y0 + self.lengths[2] * math.sin(math.radians(angles[2]))
            
        x_1 = x0 -0.1*(self.lengths[2] * math.cos(math.radians(angles[2])))
        y_1 = y0 -0.1*(self.lengths[2] * math.sin(math.radians(angles[2])))            
 
        hand2 = self.canvas.coords(
            self.hands[2],
            x_1, y_1, x2, y2
            )
    
        #マイクロセカンド点・針
        x3 = x0 + self.lengths[3] * math.cos(math.radians(angles[3]))
        y3 = y0 + self.lengths[3] * math.sin(math.radians(angles[3]))
        
        """
        hand3 = self.canvas.coords(
            self.hands[3],
            x0, y0, x3, y3
            )
        
        """
        hand3 = self.canvas.coords(
            self.hands[3],
            x3-7, y3-7, x3+7, y3+7
            )

        #中心点の装飾
        self.canvas.create_oval(
            x0-5, y0-5, x0+5, y0+5,
            fill="red",
            width=2,
            outline="black"
        )

class AnalogClock:
    def __init__(self, master):
        self.master = master
        self.drawer = Drawer(master)

        self.draw()

    def draw(self):
        hour, minute, second, microsec = Timer.time()
        self.drawer.drawHands(hour, minute, second, microsec)

    def update(self):
        hour, minute, second, microsec = Timer.time()
        self.drawer.updateHands(hour, minute, second, microsec)

        self.master.after(4, self.update)      
        #self.master.after(1000, self.update)
        #self.master.after(1000 - microsec // 1000, self.update)

if __name__ == "__main__":
    app = tk.Tk()
    clock = AnalogClock(app)
    clock.update()
    app.mainloop()
0

2Answer

updateHandsメソッドの中で毎回self.canvas.create_ovalを呼んで描画オブジェクトを増やしていて、どんどん重くなる原因になっていそうですね。
試しにcreate_ovalを削除したら遅くなりませんでした。

2Like

Comments

  1. @HZK

    Questioner

    @shiracamus
    いつもありがとうございます。
     処理速度の遅延だとばかり思っていましたが、あらためてtkinterにおける描画オブジェクトの負荷の大きさを痛感しました。
    今後はafter で更新する関数の内容にも注意を払っていきます。今後ともよろしくお願い致します。

Comments

  1. 中心点の装飾を改善してみました。30分くらい廻してみましが遅延は見られません。

    # -*- coding: utf-8 -*-
    import tkinter as tk
    import math
    from datetime import datetime, timedelta, timezone
    
    # キャンバスのサイズの設定
    CANVAS_WIDTH = 400
    CANVAS_HEIGHT = CANVAS_WIDTH
    CANVAS_SIZE = CANVAS_WIDTH
    
    # 針の長さの設定
    LENGTH_HOUR_HAND = CANVAS_SIZE / 2 * 0.5
    LENGTH_MINUTE_HAND = CANVAS_SIZE / 2 * 0.7
    LENGTH_SECOND_HAND = CANVAS_SIZE / 2 * 0.8
    LENGTH_MICROSEC_HAND = CANVAS_SIZE / 2 * 0.9
    
    # 針の色の設定
    COLOR_HOUR_HAND = "white"
    COLOR_MINUTE_HAND = "white"
    COLOR_SECOND_HAND = "red"
    COLOR_MICROSEC_HAND = "orange"
    
    # 針の太さの設定
    WIDTH_HOUR_HAND = 10
    WIDTH_MINUTE_HAND = 8
    WIDTH_SECOND_HAND = 2
    WIDTH_MICROSEC_HAND = 3
    
    # 時計画面の色設定
    BG_COLOR = "Gainsboro"
    FG_COLOR = "gray"
    
    # 時計の盤面を表す円の半径の設定
    CLOCK_OVAL_RADIUS = CANVAS_SIZE / 2
    
    # 時計の数字の位置の設定(中心からの距離)
    DISTANCE_NUMBER = CANVAS_SIZE / 2 * 0.85
    
    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
      
        @classmethod       
        def time_string(cls):
            now_s = datetime.now(tz=cls.JST)
            now_st =now_s.strftime('%H:%M:%S')
            return now_st    
    
    class Drawer:
        def __init__(self, master):
            self.master = master
    
            # 描画した針のオブジェクトを覚えておくリストを用意
            self.hands = []
            # 針の色のリストを用意
            self.colors = [
                COLOR_HOUR_HAND, COLOR_MINUTE_HAND, COLOR_SECOND_HAND, COLOR_MICROSEC_HAND
            ]
            # 針の太さのリストを用意
            self.widths = [
                WIDTH_HOUR_HAND, WIDTH_MINUTE_HAND, WIDTH_SECOND_HAND, WIDTH_MICROSEC_HAND
            ]
            # 針の長さのリストを用意
            self.lengths = [
                LENGTH_HOUR_HAND, LENGTH_MINUTE_HAND, LENGTH_SECOND_HAND, LENGTH_MICROSEC_HAND
            ]
            # キャンバスの中心座標を覚えておく
            self.center_x = CANVAS_WIDTH / 2
            self.center_y = CANVAS_HEIGHT / 2
    
            self.createClock()
            
        def createClock(self):
            self.canvas = tk.Canvas(
                self.master,
                width=CANVAS_WIDTH,
                height=CANVAS_HEIGHT,
                highlightthickness=0,
            )
            self.canvas.pack()
    
            # 時計の盤面を表す円を描画する
            x1 = self.center_x - CLOCK_OVAL_RADIUS
            y1 = self.center_y - CLOCK_OVAL_RADIUS
            x2 = self.center_x + CLOCK_OVAL_RADIUS
            y2 = self.center_y + CLOCK_OVAL_RADIUS
    
            self.canvas.create_oval(
                x1, y1, x2, y2,
                fill=BG_COLOR,
                width=2,
                outline=FG_COLOR
            )
    
            # 時計の盤面上に数字を描画する
            for hour in range(1, 13):
                angle = hour * 360 / 12 - 90
                # 描画位置を計算
                x1 = self.center_x
    -           y1 = self.center_x
    +           y1 = self.center_y
                dx = DISTANCE_NUMBER * math.cos(math.radians(angle))
                dy = DISTANCE_NUMBER * math.sin(math.radians(angle))
                x2 = x1 + dx
                y2 = y1 + dy
    
                self.canvas.create_text(
                    x2, y2,
                    font=("Times New Roman", 40),
                    fill="white",
                    text=str(hour)
                )
            #装飾
            x1 = self.center_x - CLOCK_OVAL_RADIUS*0.65
            y1 = self.center_y - CLOCK_OVAL_RADIUS*0.65
            x2 = self.center_x + CLOCK_OVAL_RADIUS*0.65
            y2 = self.center_y + CLOCK_OVAL_RADIUS*0.65
            self.canvas.create_oval(
                x1, y1, x2, y2,
                fill="silver",
                width=10,
                outline="lightgray"
            )
    +       #中心点の装飾
    +       x0 = self.center_x
    +       y0 = self.center_y
    +       self.hand4 = self.canvas.create_oval(
    +           x0-5, y0-5, x0+5, y0+5,
    +           fill="red",
    +           width=2,
    +           outline="black"
    +       )
    
        # 針を描画する'''
        def drawHands(self, hour, minute, second, microsec):
            # 各線の傾きの角度を計算指定リストに追加
            angles = []
    
            #時針も1分ずつ動かす
            hour_minute = (hour*360/12) +(minute*30/60)
            angles.append(hour_minute - 90)        
            
            angles.append(minute * 360 / 60 - 90)
            angles.append(second * 360 / 60 - 90)
    
            angles.append(microsec*360/1000000-90)
    
            # 線の一方の座標をキャンバスの中心とする
            x0 = self.center_x
            y0 = self.center_y
    
            #リスト要素の最初の二つのみ
            for angle, length, width, color in zip(angles[0:2], self.lengths[0:2], self.widths[0:2], self.colors[0:2]):
                x1 = x0 + length * math.cos(math.radians(angle))
                y1 = y0 + length * math.sin(math.radians(angle))
    
                hand = self.canvas.create_line(
                    x0, y0, x1, y1,
                    fill=color,
                    width=width,
                    capstyle=tk.ROUND
                )
    
                # 描画した線のIDを覚えておくリスト
                self.hands.append(hand)
                
            #秒針
            x2 = x0 + self.lengths[2] * math.cos(math.radians(angles[2]))
            y2 = y0 + self.lengths[2] * math.sin(math.radians(angles[2]))
            
            x_1 = x0 -0.1*(self.lengths[2] * math.cos(math.radians(angles[2])))
            y_1 = y0 -0.1*(self.lengths[2] * math.sin(math.radians(angles[2])))
            
            hand2 = self.canvas.create_line(
                    x_1, y_1, x0, y0, x2, y2,
                    fill=self.colors[2],
                    width=self.widths[2],
                )
            self.hands.append(hand2)
                
            #マイクロセカンド針と点の座標   
            x3 = x0 + self.lengths[3] * math.cos(math.radians(angles[3]))
            y3 = y0 + self.lengths[3] * math.sin(math.radians(angles[3])) 
            
            #マイクロセカンド針の場合
            """
            hand3 = self.canvas.create_line(
                x0, y0, x3, y3,
                fill=self.colors[3],
                width=self.widths[3],
                )       
            """ 
            
            #マイクロセカンド点の場合
            hand3 = self.canvas.create_oval(
                x3-7, y3-7, x3+7, y3+7,
                fill=self.colors[3],
                outline="",
            )
            # 描画した線のIDを覚えておくリスト
            self.hands.append(hand3)         
    
        #針を表現する線の位置を更新する
        def updateHands(self, hour, minute, second, microsec):
            angles = []
            #angles.append(hour * 360 / 12 - 90)
            
            hour_minute = (hour*360/12) +(minute*30/60)
            angles.append(hour_minute - 90)        
            
            angles.append(minute * 360 / 60 - 90)
            angles.append(second * 360 / 60 - 90)
            #マイクロ針は、1秒で1週
            angles.append(microsec*360/1000000-90)
            
            # 線の一方の点の座標は常に時計の中心
            x0 = self.center_x
            y0 = self.center_y
    
            for hand, angle, length in zip(self.hands[0:2], angles[0:2], self.lengths[0:2]):
                x1 = x0 + length * math.cos(math.radians(angle))
                y1 = y0 + length * math.sin(math.radians(angle))
    
                # coordsメソッドにより描画済みの線の座標を変更する
                hand = self.canvas.coords(
                    hand,
                    x0, y0, x1, y1
                )
            
            #秒針
            x2 = x0 + self.lengths[2] * math.cos(math.radians(angles[2]))
            y2 = y0 + self.lengths[2] * math.sin(math.radians(angles[2]))
                
            x_1 = x0 -0.1*(self.lengths[2] * math.cos(math.radians(angles[2])))
            y_1 = y0 -0.1*(self.lengths[2] * math.sin(math.radians(angles[2])))            
     
            hand2 = self.canvas.coords(
                self.hands[2],
                x_1, y_1, x2, y2
                )
        
            #マイクロセカンド点・針
            x3 = x0 + self.lengths[3] * math.cos(math.radians(angles[3]))
            y3 = y0 + self.lengths[3] * math.sin(math.radians(angles[3]))
            
            """
            hand3 = self.canvas.coords(
                self.hands[3],
                x0, y0, x3, y3
                )
            
            """
            hand3 = self.canvas.coords(
                self.hands[3],
                x3-7, y3-7, x3+7, y3+7
                )
    
            #中心点の装飾
    -       """
    -       self.canvas.create_oval(
    -           x0-5, y0-5, x0+5, y0+5,
    -           fill="red",
    -           width=2,
    -           outline="black"
    -       )
    -       """
    +       self.canvas.tag_raise(self.hand4)
    
    
    class AnalogClock:
        def __init__(self, master):
            self.master = master
            self.drawer = Drawer(master)
    
            self.draw()
    
        def draw(self):
            hour, minute, second, microsec = Timer.time()
            self.drawer.drawHands(hour, minute, second, microsec)
    
        def update(self):
            hour, minute, second, microsec = Timer.time()
            self.drawer.updateHands(hour, minute, second, microsec)
    
            self.master.after(4, self.update)      
            #self.master.after(1000, self.update)
            #self.master.after(1000 - microsec // 1000, self.update)
    
    if __name__ == "__main__":
        app = tk.Tk()
        clock = AnalogClock(app)
        clock.update()
        app.mainloop()
    
  2. @HZK

    Questioner

    @nak435
    参考urlとコード上の間違い・改善点を教えて頂き感謝しております。lift()は知っていましたが、tag_raise()も学習できました。
     処理速度の遅延だと誤解していたので、threadingや指定座標の固定など、あやうく見当違いの方面にいくところでした。あらためて描画オブジェクト作成の負担度を実感できました。ありがとうございました。

Your answer might help someone💌