Python Forum
[Tkinter] Help to fix my Temporizer
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Help to fix my Temporizer
#1
I need help getting my timer to work.

This timer, when started, will have a decreasing time of 2 minutes, and when it reaches time 00:01 it will automatically return to 2 minutes again, working in loop mode every time it restarts it starts automatically, but at time 02 :00 to 01:59 it will only take 0.5 milliseconds from 01:59 to 00:01 it will decrease the time by 1 second normally.

It is not necessary for the timer to reach 00:00 to restart. This timer rings an alarm in the last 10 seconds, the path is already defined in the script, I just need help to fix the timer part, I'll leave the code I'm using below.

PS: remember that when you press the Break key it resets the count and starts the 2 minutes from the beginning.

import tkinter as tk
import pygame
import os

class TimerApp:
    def __init__(self, master):
        self.master = master
        master.title("Temporizador")

        self.timer_duration = 120  # 2 minutos
        self.time_left = self.timer_duration
        self.timer_running = True  # Inicia automaticamente

        # Configurar a janela para estar sempre em cima
        master.attributes("-topmost", True)
        master.geometry("170x90+0+0")  # Dimensões fixas

        self.label = tk.Label(master, text=self.format_time(self.time_left), font=("Helvetica", 48), fg="red", bg="black")  # Fonte aumentada
        self.label.pack(pady=20)

        self.master.bind('<Pause>', lambda event: self.reset_timer())  # Liga a tecla Pause/Break para reiniciar

        # Inicializa o pygame para tocar o som
        pygame.mixer.init()
        self.alarm_sound = "C:\\Users\\miche\\OneDrive\\Documentos\\Temporizador Tibia Piranha\\Alarm Piranha.MP3"

        self.update_timer()  # Inicia o loop de atualização do temporizador

    def format_time(self, seconds):
        minutes = seconds // 60
        seconds = seconds % 60
        return f"{minutes:02}:{seconds:02}"

    def reset_timer(self):
        self.time_left = self.timer_duration
        self.label.config(text=self.format_time(self.time_left))
        pygame.mixer.music.stop()  # Para o alarme ao reiniciar

    def update_timer(self):
        if self.timer_running:
            if self.time_left > 0:
                if self.time_left <= 10:  # Toca o alarme nos últimos 10 segundos
                    self.play_alarm()
                self.time_left -= 1
                self.label.config(text=self.format_time(self.time_left))
            else:  # Quando chega a 0
                self.label.config(text=self.format_time(1))  # Mostra 00:01
                pygame.mixer.music.stop()  # Para o alarme
                self.master.after(1000, self.reset_timer)  # Aguarda 1 segundo antes de reiniciar
                return  # Sai para não atualizar mais este ciclo

        self.master.after(1000, self.update_timer)  # Atualiza a cada segundo

    def play_alarm(self):
        # Toca o som do alarme
        if not pygame.mixer.music.get_busy():  # Toca apenas se não estiver tocando
            pygame.mix
I'm using the version 3.17.2
Reply
#2
Are you trying to do something like this?

import tkinter as tk


class Timer:
    ''' Timer class creates a timer object '''
    def __init__(self, duration=0):
        self.duration = duration

    def countdown(self):
        ''' Method for doing the countdown '''
        self.duration -= 1
        return self.duration

    def update(self):
        ''' Method for formatting and returning timer '''
        duration = self.countdown()
        if duration <= 0:
            self.duration = 0
        
        #format
        hours = divmod(self.duration, 3600)
        minutes = divmod(hours[1], 60)
        seconds = divmod(minutes[1],60)
        
        return f'{hours[0]:02}:{minutes[0]:02}:{seconds[1]:02}'
        

class Window:
    ''' Window class is for displaying timer '''
    def __init__(self, parent):
        self.parent = parent
        self.parent.geometry('+300+300')
        self.parent.configure(padx=4, pady=4)

        header = tk.Label(self.parent, text='Countdown Timer', padx=5, pady=10)
        header.pack(side='top', fill='x', pady=5)
        header.configure(font=(None, 18, 'bold'), bg='#555555', fg='#ffffff')

        label = tk.Label(self.parent, wraplength=300, justify='left', anchor='w')
        label.pack(fill='x', expand=True, pady=(5,10), ipadx=5)
        label.configure(
            text='Timer reaches 0 auto reset, press (r) timer resets',
            font = ('tahoma', 12, 'italic'),
            highlightcolor = '#555555',
            highlightbackground = '#555555',
            highlightthickness = 1,
            pady=5
        )

        self.label = tk.Label(self.parent, anchor='w', padx=5, pady=5)
        self.label.pack(side='left', fill='x', expand=True)
        self.label.configure(
            text='Timer: ', font=(None, 14, 'normal'),
            highlightcolor ='#555555',
            highlightbackground='#555555',
            highlightthickness=1
            )


class Controller:
    ''' Controller class handles communication between Timer and Window classes '''
    def __init__(self, timer, window):

        # Set instance variables
        self.timer = timer
        self.window = window

        # default counter is set to 10 seconds. can be any number
        # To set for 2 minutes the number would be 120
        # to set for 1 hour the number would be 3600
        # To set for 5 hours the number would be 18000
        self.default = 10

        # Set timer duration to default - This could be any number
        # Example self.timer.duration = 120 - Would set to 2 minutes
        # You would need to make changes to other places in script.
        # Best option is to set default to timer length
        self.timer.duration = self.default
        self.window.label.configure(text=f'Timer: {self.timer.update()}')

        # Bind r key to reset function
        self.window.parent.bind('<r>', self.reset)

        # Get everything started
        self.update()

    def update(self):
        ''' Method updates display window '''
        self.window.label.configure(text=f'Timer: {self.timer.update()}')
        if self.timer.duration <= 0:
            self.timer.duration = self.default
        
        bgcolor = 'gray86'

        if self.timer.duration <= 5:
            if self.timer.duration % 2 == 0:
                bgcolor = 'orange'
            elif self.timer.duration % 2 != 0:
                bgcolor = 'orangered'
            else:
                bgcolor = 'gray86'

        self.window.label.configure(bg=bgcolor)
        self.window.parent.after(1000, self.update)

    def reset(self, event=None):
        ''' Method for resetting timer '''
        self.timer.duration = self.default
        
if __name__ == '__main__':
    root = tk.Tk()
    controller = Controller(Timer(), Window(root))
    root.mainloop()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#3
yes, that's how I need it, but I also need the timer screen to overlay any screen, for the time to be 02:00 minutes decreasing, and for an alarm to ring in the last few seconds so I know there are 10 seconds left to start again, and finally the last second and the first will only take 0.5 milliseconds to exchange and the rest always 1 second.


in the case of 00:01 to 00:00 it will only take 0.5 milliseconds and from 02:00 to 01:59 it will also take only 0.5 seconds
Reply
#4
This is about as close as I can get to what you want.
Splitting a second between the numbers you want, don't think is possible
Read the comments in the code to see how everything works
If the pause key doesn't reset the counter, just click on the counter window to reset focus.


Updated key binds
shift+r = reset
pause = pause/resume counter
esc = exit program


Changed code a little
added self.parent.wait_visibility(self.parent above self.parent.wm_attributes('-alpha', 0.5)
This changes opacity 0.0 being not visible and 1.0 being fully visible
Added check for sound file. If one is not found won't error. Uses visual only


Update:
Added class for dragging widget around on desktop

import tkinter as tk
import pygame
from pathlib import Path
from os.path import exists


# Get path to executing script
path = Path(__file__).parent

# Initialize pygame.mixer
pygame.mixer.init()

class Sound:
    ''' Sound class loads, plays, and stops sound file '''
    def __init__(self, audio=''):
        self.audio = audio
        self.verify = False
        if self.audio and exists(f'{path}/{self.audio}'):
            pygame.mixer.music.load(f'{path}/{self.audio}')
            self.verify = True

    def play(self):
        ''' Method for playing sound file '''
        if self.verify:
            pygame.mixer.music.play()

    def stop(self):
        ''' Method to stop playing sound file '''
        if self.verify:
            pygame.mixer.music.stop()


class DragIt:
    ''' DragIt class takes a widget and moves it around the desktop '''
    def __init__(self, widget):
        self.widget = widget

        # Mouse binds
        widget.bind('<1>', self.grab)
        widget.bind('<B1-Motion>', self.move)
        widget.configure(cursor='hand1')
        widget.bind('<ButtonRelease>', self.reset)

    def grab(self, event):
        ''' Method for getting start position '''
        self.widget.configure(cursor='hand2')
        self.start_x = event.x
        self.start_y = event.y

    def move(self, event):
        ''' Method for moving widget '''
        dx = event.x - self.start_x
        dy = event.y - self.start_y

        left = self.widget.winfo_x() + dx
        top = self.widget.winfo_y() + dy

        self.widget.geometry(f'+{left}+{top}')

    def reset(self, event):
        ''' Method resets cursor pointer '''
        self.widget.configure(cursor='hand1')


class Timer:
    ''' Timer class sets a countdown timer - default is 2 minutes '''
    def __init__(self, duration=120):
        self.duration = duration+1

    def update(self):
        ''' Method for updating count '''
        self.duration -= 1
        hours = divmod(self.duration, 3600)
        minutes = divmod(hours[1], 60)
        seconds = divmod(minutes[1], 60)

        return f'{hours[0]:02}:{minutes[0]:02}:{seconds[1]:02}'


class Window:
    ''' Window class is for displaying window '''
    def __init__(self, parent):
        parent.geometry('+50+50')
        parent.minsize(130,30)
        parent.maxsize(130,30)
        parent.wait_visibility(parent)
        parent.wm_attributes('-topmost', True)
        parent.wm_attributes('-alpha', 0.5)
        parent.wm_attributes('-type', 'splash')
        parent.focus_force()

        self.parent = parent

        self.label = tk.Label(parent, anchor='w', padx=10)
        self.label.pack(fill='x', expand=True)
        self.label.configure(font=(None, 18, 'normal'))


class Controller:
    ''' Controller class handles communications between Timer and Window class '''
    def __init__(self, window, timer):
        # Create instance variables
        self.window = window
        self.timer = timer
        self.action = False
        self.duration = self.timer.duration

        # Create the alarm sound 
        # Path to sound file example media/myalarm.mp3
        # If in the same directory as script, just filename.mp3 will work
        # This can also be empty for no sound or if file not found
        # should still work
        self.alarm = Sound()

        # Make the window draggable
        widget = DragIt(self.window.parent)

        # Key binds
        self.window.parent.bind('<R>', self.reset)
        self.window.parent.bind('<Pause>', self.pause)
        self.window.parent.bind('<Escape>', lambda event: self.window.parent.destroy())

        # Get it started
        self.update()

    def update(self):
        ''' Method updates thw window '''
        self.window.label.configure(text=self.timer.update())       

        # If counter is 10 or below, play alarm and change bgcolor
        if self.timer.duration <= 10:
            self.alarm.play()
            if self.timer.duration % 2 == 0:
                self.window.label.configure(bg='red', fg='white')
            else:
                self.window.label.configure(bg='orange', fg='red')

        # If timer reaches 0 - reset
        # I tried calling the reset function here but, caused problems with counter
        if self.timer.duration <= 0:
            self.timer.duration = self.duration
            self.alarm.stop()
            self.window.label.configure(bg='gray86', fg='black')

        # Call .after to update window
        self.updating = self.window.parent.after(1000, self.update)

    def reset(self, event):
        ''' Method for resetting everything - key bind is shift+r '''
        self.window.parent.after_cancel(self.updating)
        self.timer.duration = self.duration
        self.action = False
        self.window.label.configure(bg='gray86', fg='black')
        self.alarm.stop()
        self.update()

    def pause(self, event):
        ''' Method for pausing counter '''
        self.action = True if not self.action else False
        if self.action:
            self.window.parent.after_cancel(self.updating)
            self.timer.duration = self.timer.duration
            self.alarm.stop()
        else:
            self.update()


if __name__ == '__main__':
    root = tk.Tk()
    # Timer excepts milaseconds - example 120 = 2 minutes (60x2 = 120 milaseconds = 2 minutes)
    # If Timer left blank will default to 2 minutes
    controller = Controller(Window(root), Timer(15))
    root.mainloop()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#5
Using .after() to keep track of time is a bad idea. .after() is really after, as in "After the time has passed I'll get around to calling your function." If accurate reporting is important, you should record the start time and use that when computing time remaining.

I would call the after function 100 times a second. 10ms or 0.5ms is imperceptible, and there is no guarantee when the time display is repainted anyway. There will also be a lot of variability in the time between pressing Pause and mainloop calling the reset_timer() method. asking for 0.5ms timer resolution is silly in this context. I take back what I said about 100 times a second. I would call the after function 20 times a second and know there is no difference in anything I can see between that and updating 2000 times a second (0.5ms resolution).
import time
import tkinter as tk
from pathlib import Path
from pygame import mixer


class TimerApp(tk.Tk):

    def __init__(self, duration=120, alarm_start=10, alarm=None, width=8):
        super().__init__()
        self.duration = duration
        self.alarm_start = alarm_start
        self.alarm = alarm
        self.title("Temporizador")
        self.attributes("-topmost", True)
        self.bind("<Pause>", lambda event: self.reset_timer())
        self.timestr = tk.StringVar(self, "")
        tk.Label(self, textvariable=self.timestr, font=("Helvetica", 48), fg="red", bg="black", width=width).pack(
            pady=20
        )
        if self.alarm:
            mixer.init()
            mixer.music.load(self.alarm)
        self.reset_timer()

    def reset_timer(self):
        mixer.music.stop()
        self.play_alarm = False
        self.start_time = time.time()
        self.update_timer()

    def update_timer(self):
        remaining_time = self.duration + self.start_time - time.time()
        if remaining_time > 0:
            if self.alarm and not self.play_alarm and remaining_time <= self.alarm_start:
                self.play_alarm = True
                mixer.music.play()
            self.update_display(remaining_time)
            self.after(10, self.update_timer)
        else:
            self.update_display(self.duration)
            if self.play_alarm:
                mixer.music.stop()

    def update_display(self, seconds):
        if self.duration > 60:
            minutes, remainder = divmod(int(seconds + 0.999), 60)
            self.timestr.set(f"{minutes}:{remainder:>02}")
        else:
            self.timestr.set(f"{int(seconds+0.999)}")


TimerApp(duration=10, alarm_start=5, alarm=Path(__file__).parent / "alarm.mp3").mainloop()
Use default arguments instead of hard coded constants. Avoid absolute paths for support files
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020