vorab: Ich bin Anfänger in Python und generell noch neu in der Welt des Programmierens.
Ich möchte schon seit langem lernen, die Basics einer Programmiersprache zu lernen. Da ich am besten mit einem konkreten Ziel lerne und für ein anderes Hobby eine entsprechende Anwendung dafür hätte, habe ich in den letzten Monaten mit Hilfe von Internettutorials und ChatGPT an einer Weltzeituhr programmiert. Konkret geht es um eine grafische Anzeige mehrerer Uhren nebeneinander, die jeweils unterschiedliche Zeitzonen darstellen. Die Uhr soll später einmal auf einem Raspberry Pi laufen.
Vielleicht hat ja jemand Lust, sich meinen Code anzuschauen und ein Feedback zu geben, insbesondere dazu, ob ich gewisse Dinge zu umständlich oder gar falsch gelöst habe.
Ich habe tkinter als GUI benutzt. Kern ist die TimeZoneClockApp-Klasse mit der create_clocks-Methode. Die einzelnen Uhren sind Instanzen der Clock-Klasse.
Ich bin nämlich schon auf ein Problem gestoßen, bei dem ich unsicher bin, ob es an den eingesetzten Tools liegt oder an meiner Umsetzung: Während die Anzeige auf meinem Windows-Rechner flüssig und synchron läuft, sind die Sekundenzeiger meiner Uhren auf dem Raspberry Pi leider nicht mehr gleichmäßig, sie springen komplett asynchron.
Vielen Dank,
Martin
Code: Alles auswählen
import os
import sys
from datetime import datetime, timezone, timedelta
import math
import json
import copy
import pytz
import pyautogui
import ntplib
from screeninfo import get_monitors
from PIL import Image, ImageTk
import threading
from typing import Optional, List
import tkinter as tk
from tkinter import ttk, messagebox
class TimeZoneClockApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Time Zone Clock")
self.icon = tk.PhotoImage(file=resource_path("icon.png"))
self.iconphoto(True, self.icon)
self.version = "1.9"
# Default values used in settings.json if file is not found
self.sync_interval = -1 # in minutes or -1 for single sync
self.autosync_active = False
self.write_log = False
self.start_fullscreen = False
self.ntp_servers = [
"0.de.pool.ntp.org",
"1.de.pool.ntp.org",
"2.de.pool.ntp.org",
"3.de.pool.ntp.org",
]
self.clocksettings = [
{
"name": "UTC",
"timezone": "UTC",
"color_background": "white",
"color_frame": "grey",
"color_name": "grey",
"color_text": "grey",
"color_clockface": "white",
"color_clockframe": "grey",
"color_clockhands": "grey",
"color_secondhand": "orange",
},
{
"name": "New York",
"timezone": "America/New_York",
"color_background": "white",
"color_frame": "grey",
"color_name": "grey",
"color_text": "grey",
"color_clockface": "white",
"color_clockframe": "grey",
"color_clockhands": "grey",
"color_secondhand": "orange",
},
{
"name": "Berlin",
"timezone": "Europe/Berlin",
"color_background": "white",
"color_frame": "grey",
"color_name": "grey",
"color_text": "grey",
"color_clockface": "white",
"color_clockframe": "grey",
"color_clockhands": "grey",
"color_secondhand": "orange",
},
{
"name": "Tokyo",
"timezone": "Asia/Tokyo",
"color_background": "white",
"color_frame": "grey",
"color_name": "grey",
"color_text": "grey",
"color_clockface": "white",
"color_clockframe": "grey",
"color_clockhands": "grey",
"color_secondhand": "orange",
},
]
self.color_background = "white"
self.x = 10
self.y = 10
self.last_monitor_setup = None
self.config_file = "settings.json"
self.read_settings_from_json()
self.autosync_timer = None
self.offset = tk.DoubleVar()
self.offset.set(0.0)
self.fullscreen_active = False
self.ntp_status_text = ""
self.autosync_status_text = ""
self.clockwidth = 200
self.clockheight = 300
self.clocksdialog: Optional[ClocksDialog] = None
self.serverdialog: Optional[NTPServerDialog] = None
self.settingsdialog: Optional[SettingsDialog] = None
self.about_window: Optional[tk.Toplevel] = None
self.bind("<Button-3>", self.show_menu_on_rightclick)
self.bind("<F11>", self.toggle_fullscreen)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.ensure_window_position()
self.create_clockframe()
self.create_clocks()
self.autosync()
self.keep_active()
if self.start_fullscreen:
self.toggle_fullscreen()
def read_settings_from_json(self):
try:
with open(self.config_file, "r", encoding="utf-8") as file:
settings = json.load(file)
self.sync_interval = settings.get("sync_interval", self.sync_interval)
self.autosync_active = settings.get("autosync_active", self.autosync_active)
self.write_log = settings.get("write_log", self.write_log)
self.start_fullscreen = settings.get("fullscreen", self.start_fullscreen)
self.ntp_servers = settings.get("ntp_servers", self.ntp_servers)
self.clocksettings = settings.get("clocksettings", self.clocksettings)
self.color_background = settings.get("color_background", self.color_background)
self.x = settings.get("x", self.x)
self.y = settings.get("y", self.y)
self.last_monitor_setup = settings.get("last_monitor_setup", self.last_monitor_setup)
except FileNotFoundError:
pass
except json.JSONDecodeError:
pass
def write_settings_to_json(self):
settings = {
"sync_interval": self.sync_interval,
"autosync_active": self.autosync_active,
"write_log": self.write_log,
"fullscreen": self.start_fullscreen,
"ntp_servers": self.ntp_servers,
"clocksettings": self.clocksettings,
"color_background": self.color_background,
"x": self.x,
"y": self.y,
"last_monitor_setup": self.last_monitor_setup,
"version": self.version,
}
with open(self.config_file, "w", encoding="utf-8") as file:
json.dump(settings, file, indent=4, ensure_ascii=False)
def update_position(self, event):
self.x = self.winfo_x()
self.y = self.winfo_y()
def ensure_window_position(self):
new_monitor_setup = str(get_monitors())
if self.last_monitor_setup != new_monitor_setup:
self.x = 25
self.y = 25
self.last_monitor_setup = new_monitor_setup
def on_closing(self):
self.write_settings_to_json()
if self.autosync_timer is not None:
self.autosync_timer.cancel()
self.destroy()
def create_clockframe(self):
self.frame = tk.Frame(self)
self.frame.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
self.frame.config(bg=self.color_background)
screen_width = max(self.winfo_screenwidth(), 880)
clocks_per_row = screen_width // (self.clockwidth + 20)
self.notification_label = tk.Label(
self.frame,
text="",
bg=self.color_background,
fg="black",
font=("Helvetica", 8),
)
self.notification_label.grid(
row=0,
column=0,
padx=10,
pady=(0, 0),
columnspan=min(len(self.clocksettings), clocks_per_row),
sticky="w",
)
self.status_symbol = tk.Label(self.frame, bg=None)
self.status_symbol.config(text="●", font=("Helvetica", 12), fg="gray", bg=self.color_background)
self.status_symbol.bind("<Enter>", self.show_tooltip)
self.status_symbol.bind("<Leave>", self.hide_tooltip)
self.status_symbol.bind("<Button-1>", self.toggle_autosync)
self.status_symbol.grid(
row=0,
column=min(len(self.clocksettings), clocks_per_row) - 1,
padx=10,
pady=(0, 0),
sticky="e",
)
def create_clocks(self):
try:
for widget in self.frame.winfo_children():
if isinstance(widget, Clock):
widget.destroy()
except Exception:
pass
screen_width = max(self.winfo_screenwidth(), 880)
clocks_per_row = screen_width // (self.clockwidth + 20)
self.clocks = {}
for i, clock in enumerate(self.clocksettings):
clock = Clock(
self.frame,
self.clocksettings[i],
self.offset,
width=self.clockwidth,
height=self.clockheight,
)
row = i // clocks_per_row + 1
column = i % clocks_per_row
clock.grid(row=row, column=column, padx=10, pady=(0, 10))
self.clocks[self.clocksettings[i]["name"]] = clock
# resize the window to fit the clocks
self.update_idletasks()
self.geometry(
f"{self.frame.winfo_width()}x{self.frame.winfo_height()}+{self.x}+{self.y}"
)
self.bind("<Configure>", self.update_position)
# adjust columnspan of the notification label
self.notification_label.grid(columnspan=min(len(self.clocksettings), clocks_per_row))
def autosync(self):
"""Automatic synchronization"""
if self.autosync_active:
# kill the timer if it is still running
if self.autosync_timer is not None:
self.autosync_timer.cancel()
self.autosync_timer = None
self.autosync_status_text = "Autosync is active."
self.ntp_sync()
if self.sync_interval > 0:
self.autosync_timer = threading.Timer(self.sync_interval * 60, self.autosync)
self.autosync_timer.start()
else:
self.autosync_status_text = "Autosync is stopped."
self.reset_status_symbol()
def ntp_sync(self):
"""Compare the system time with an NTP server"""
ntp_client = ntplib.NTPClient()
for ntp_server in self.ntp_servers:
try:
response = ntp_client.request(ntp_server)
ntp_server_time = datetime.fromtimestamp(response.tx_time, timezone.utc)
log_message = f"{datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z')}: Sync with Server: {ntp_server}, Leap: {ntplib.leap_to_text(response.leap)}, Version: {response.version}, Mode: {ntplib.mode_to_text(response.mode)}, Stratum: {ntplib.stratum_to_text(response.stratum)}, Poll: {response.poll}, Precision: {response.precision}, Root Delay: {response.root_delay}, Root Dispersion: {response.root_dispersion}, Reference ID: {ntplib.ref_id_to_text(response.ref_id, response.stratum)}, Reference Timestamp: {datetime.fromtimestamp(response.ref_time, timezone.utc)}, Originate Timestamp: {datetime.fromtimestamp(response.orig_time, timezone.utc)}, Receive Timestamp: {datetime.fromtimestamp(response.recv_time, timezone.utc)}, Transmit Timestamp: {datetime.fromtimestamp(response.tx_time, timezone.utc)}, Destination Timestamp: {datetime.fromtimestamp(response.dest_time, timezone.utc)}, Offset: {response.offset}, Delay: {response.delay}\n"
if self.write_log:
with open("ntp_sync.log", "a") as file:
file.write(log_message)
self.offset.set(response.offset)
if self.offset.get() > 0:
offset_sign = "slow"
elif self.offset.get() < 0:
offset_sign = "fast"
else:
offset_sign = "correct"
if abs(self.offset.get()) >= 0 and abs(self.offset.get()) < 1:
self.ntp_status_text = f"Last synchronization on {ntp_server_time.strftime('%Y-%m-%d')} at {ntp_server_time.strftime('%H:%M:%S %Z')} using NTP-server '{ntp_server}'. Offset: {self.offset.get():.3f} s (Computer clock is {offset_sign}). Delay: {response.delay:.3f} s."
self.notification_label.config(text=self.ntp_status_text, bg=None)
self.after(5000, self.clear_notification_label)
# show a green dot in the notification area
self.status_symbol.config(text="●", font=("Helvetica", 12), fg="green3")
if not self.autosync_active:
self.after(5000, self.reset_status_symbol)
elif abs(self.offset.get()) >= 1:
self.ntp_status_text = f"System time is off by more than 1 second. Last synchronization on {ntp_server_time.strftime('%Y-%m-%d')} at {ntp_server_time.strftime('%H:%M:%S %Z')} using NTP-server '{ntp_server}'. Offset: {self.offset.get():.3f} s (Computer clock is {offset_sign}). Delay: {response.delay:.3f} s."
self.notification_label.config(text=self.ntp_status_text, bg=None)
self.after(5000, self.clear_notification_label)
# show a yellow dot in the notification area
self.status_symbol.config(text="●", font=("Helvetica", 12), fg="yellow")
break # Exit the loop if successful
except Exception as e:
last_exception = e
if self.write_log:
with open("ntp_sync.log", "a") as file:
file.write(
f"{datetime.now().astimezone().strftime('%Y-%m-%d %H:%M:%S%z')}: Failed to sync with NTP server {ntp_server}: {str(e)}\n"
)
continue
else:
self.ntp_status_text = f"Failed to check system time: {last_exception}"
self.notification_label.config(text=self.ntp_status_text, fg="red")
self.after(5000, self.clear_notification_label)
# show a red dot in the notification area
self.status_symbol.config(text="●", font=("Helvetica", 12), fg="red")
def clear_notification_label(self):
self.notification_label.config(text="", bg=None, fg="black")
def reset_status_symbol(self):
self.status_symbol.config(text="●", font=("Helvetica", 12), fg="gray")
def toggle_autosync(self, event):
if self.autosync_active:
self.stop_sync()
else:
self.start_sync()
def show_tooltip(self, event):
self.tooltip = tk.Toplevel(self)
self.tooltip.wm_overrideredirect(True)
self.tooltip.wm_geometry(f"+{event.x_root + 10}+{event.y_root + 10}")
label = tk.Label(
self.tooltip,
text=f"{self.autosync_status_text} {self.ntp_status_text}",
background=None,
relief="solid",
borderwidth=1,
font=("Helvetica", 8),
)
label.pack()
def hide_tooltip(self, event):
if self.tooltip:
self.tooltip.destroy()
def show_context_menu(self, event):
self.context_menu = tk.Menu(self, tearoff=0)
self.context_menu.add_command(label="Clocks", command=self.open_clocks_dialog)
self.context_menu.add_separator()
self.context_menu.add_command(label="NTP Server", command=self.open_server_dialog)
self.context_menu.add_separator()
self.context_menu.add_command(label="Manual sync", command=self.manual_sync)
self.context_menu.add_command(label="Stop automatic sync", command=self.stop_sync)
self.context_menu.add_command(label="Start automatic sync", command=self.start_sync)
self.context_menu.add_command(label="Show sync status", command=self.show_sync_status)
self.context_menu.add_command(label="Show log file", command=self.show_log_file)
self.context_menu.add_separator()
self.context_menu.add_command(label="Settings", command=self.open_settings)
self.context_menu.add_separator()
self.context_menu.add_command(label="Fullscreen", command=self.toggle_fullscreen)
self.context_menu.add_separator()
self.context_menu.add_command(label="About", command=self.open_about_window)
self.context_menu.add_separator()
self.context_menu.add_command(label="Exit", command=self.on_closing)
self.context_menu.post(event.x_root, event.y_root)
def show_menu_on_rightclick(self, event):
self.show_context_menu(event)
def open_clocks_dialog(self):
if self.clocksdialog is not None and self.clocksdialog.winfo_exists():
self.clocksdialog.lift()
else:
self.clocksdialog = ClocksDialog(self, "Change Clocks", self.clocksettings)
def open_server_dialog(self):
if self.serverdialog is not None and self.serverdialog.winfo_exists():
self.serverdialog.lift()
else:
self.serverdialog = NTPServerDialog(self, "Change NTP Server", self.ntp_servers)
def open_settings(self):
if self.settingsdialog is not None and self.settingsdialog.winfo_exists():
self.settingsdialog.lift()
else:
self.settingsdialog = SettingsDialog(self, "Settings")
def show_log_file(self):
try:
os.startfile("ntp_sync.log")
except Exception as e:
messagebox.showerror("Error", f"Failed to open log file: {e}")
def stop_sync(self):
"""Stop the automatic synchronization"""
if self.autosync_active:
if self.autosync_timer is not None:
self.autosync_timer.cancel()
self.autosync_timer = None
self.autosync_active = False
self.autosync_status_text = "Autosync is stopped."
self.reset_status_symbol()
self.notification_label.config(text="Autosync is stopped.", bg=None)
self.after(5000, self.clear_notification_label)
else:
self.notification_label.config(text="Autosync is already stopped.", bg=None)
self.after(5000, self.clear_notification_label)
def start_sync(self):
"""Start the automatic synchronization"""
if not self.autosync_active:
self.autosync_active = True
self.notification_label.config(text="Autosync is active.", bg=None)
self.after(5000, self.clear_notification_label)
self.autosync()
else:
self.notification_label.config(text="Autosync is already active.", bg=None)
self.after(5000, self.clear_notification_label)
def manual_sync(self):
"""Manual synchronization"""
self.ntp_sync()
def toggle_fullscreen(self, event=None):
self.fullscreen_active = not self.fullscreen_active
self.attributes("-fullscreen", self.fullscreen_active)
if self.fullscreen_active:
#self.config(menu=tk.Menu(self)) # Remove the menubar
self.bind("<Escape>", self.toggle_fullscreen)
else:
#self.config(menu=self.menubar) # Restore the menubar
self.unbind("<Escape>")
def keep_active(self):
pyautogui.press("shift")
self.after(60000, self.keep_active)
def show_sync_status(self):
messagebox.showinfo(
"Sync Status",
f"{self.autosync_status_text} {self.ntp_status_text}",
)
def open_about_window(self):
if self.about_window is not None and self.about_window.winfo_exists():
self.about_window.lift()
else:
self.about_window = tk.Toplevel(self)
self.about_window.title("About")
self.about_window.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.about_window.geometry("300x200")
self.about_window.resizable(False, False)
about_label = tk.Label(
self.about_window,
text=f"Time Zone Clock\nVersion: {self.version} \n\n Time Zone Database Version: {pytz.OLSON_VERSION}",
font=("Helvetica", 12),
)
about_label.pack()
class Clock(tk.Canvas):
def __init__(
self,
parent: tk.Frame,
settings: dict[str, str],
offset: tk.DoubleVar,
width: int,
height: int,
):
super().__init__(parent, width=width, height=height)
self.city = settings["name"]
self.timezone = settings["timezone"]
self.color_background = settings["color_background"]
self.color_frame = settings["color_frame"]
self.color_name = settings["color_name"]
self.color_text = settings["color_text"]
self.color_clockface = settings["color_clockface"]
self.color_clockframe = settings["color_clockframe"]
self.color_clockhands = settings["color_clockhands"]
self.color_secondhand = settings["color_secondhand"]
# Offset defined as difference between NTP server time and local system time
self.offset = offset
self.width = width
self.height = height
self.radius = self.width * 0.45
self.centercanvas = (self.width // 2, self.height // 2)
self.centerclock = (self.centercanvas[0], self.centercanvas[1] - 20)
self.config(
bg=self.color_background,
highlightbackground=self.color_frame,
highlightthickness=1,
)
self.city_text = self.create_text(
self.centerclock[0],
self.centerclock[1] - self.radius - 20,
text="",
font=("Helvetica", 12, "bold"),
fill=self.color_name,
)
self.date_text = self.create_text(
self.centerclock[0],
self.centerclock[1] + self.radius + 25,
text="",
font=("Helvetica", 11),
fill=self.color_text,
)
self.time_text = self.create_text(
self.centerclock[0],
self.centerclock[1] + self.radius + 45,
text="",
font=("Helvetica", 11),
fill=self.color_text,
)
self.utc_offset_text = self.create_text(
self.centerclock[0],
self.centerclock[1] + self.radius + 65,
text="",
font=("Helvetica", 11),
fill=self.color_text,
)
self.draw_clockface()
self.update_clock()
def draw_clockface(self):
self.create_oval(
self.centerclock[0] - self.radius,
self.centerclock[1] - self.radius,
self.centerclock[0] + self.radius,
self.centerclock[1] + self.radius,
fill=self.color_clockface,
outline=self.color_clockframe,
width=1,
)
for i in range(60): # Draw ticks
angle = math.radians(i * 6 - 90)
x_start = self.centerclock[0] + self.radius * 0.95 * math.cos(angle)
y_start = self.centerclock[1] + self.radius * 0.95 * math.sin(angle)
if i % 5 == 0: # Draw hour ticks
x_end = self.centerclock[0] + self.radius * 0.85 * math.cos(angle)
y_end = self.centerclock[1] + self.radius * 0.85 * math.sin(angle)
width = 2
else: # Draw minute ticks
x_end = self.centerclock[0] + self.radius * 0.90 * math.cos(angle)
y_end = self.centerclock[1] + self.radius * 0.90 * math.sin(angle)
width = 1
self.create_line(x_start, y_start, x_end, y_end, width=width, fill=self.color_clockframe)
for i in range(12): # Draw numbers
angle = math.radians(i * 30)
x_num = self.centerclock[0] + self.radius * 0.72 * math.sin(angle)
y_num = self.centerclock[1] - self.radius * 0.72 * math.cos(angle)
self.create_text(
x_num,
y_num,
text=str(i if i != 0 else 12),
font=("Helvetica", 12),
fill=self.color_clockframe,
)
def update_clock(self):
# check if timezone is valid
try:
pytz.timezone(self.timezone)
except pytz.exceptions.UnknownTimeZoneError:
self.itemconfig(self.city_text, text="Invalid timezone")
self.itemconfig(self.time_text, text="")
self.itemconfig(self.date_text, text="")
self.itemconfig(self.utc_offset_text, text="")
return
now = datetime.now(pytz.timezone(self.timezone)) + timedelta(seconds=self.offset.get())
self.delete("hands")
self.draw_hand(
now.hour % 12 * 30 + now.minute * 0.5,
self.radius * 0.6,
6,
self.color_clockhands,
)
self.draw_hand(now.minute * 6, self.radius * 0.9, 4, self.color_clockhands)
self.draw_hand(now.second * 6, self.radius * 0.95, 2, self.color_secondhand)
self.itemconfig(self.city_text, text=self.city)
self.itemconfig(self.time_text, text=now.strftime("%H:%M:%S"))
self.itemconfig(self.date_text, text=now.strftime("%a %Y-%m-%d"))
self.itemconfig(self.utc_offset_text, text=f"UTC{now.strftime("%:z")}")
# Calculate time until next second
until_next_second = 1000 - (now.microsecond // 1000)
# Schedule the next update
self.after(until_next_second, self.update_clock)
def draw_hand(self, angle, length, width, color):
angle_rad = math.radians(angle - 90)
x = self.centerclock[0] + length * math.cos(angle_rad)
y = self.centerclock[1] + length * math.sin(angle_rad)
self.create_line(
self.centerclock[0],
self.centerclock[1],
x,
y,
width=width,
fill=color,
tags="hands",
)
class ClocksDialog(tk.Toplevel):
def __init__(self, parent: TimeZoneClockApp, title: str, clocksettings: list[dict[str, str]]):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
# self.geometry("400x300")
self.resizable(False, False)
self.parent = parent
self.clocksettings = clocksettings
# Make a copy of the original clocksettings to restore them if the user cancels
self.original_clocksettings = copy.deepcopy(clocksettings)
self.selected_clock = None
self.label = ttk.Label(self, text="List of Clocks")
self.label.grid(row=0, column=0, padx=5, pady=5, sticky="w", columnspan=2)
self.listbox = tk.Listbox(self, selectmode=tk.SINGLE, width=40)
for clock in self.clocksettings:
self.listbox.insert(tk.END, clock["name"] + ": " + clock["timezone"])
self.listbox.grid(row=1, column=0, padx=5, pady=5, sticky="ew", columnspan=2)
self.move_up_button = tk.Button(self, text="Move selected clock up", command=self.move_up, width=20)
self.move_up_button.grid(row=2, column=0, padx=5, pady=5, sticky="e")
self.move_down_button = tk.Button(self, text="Move selected clock down", command=self.move_down, width=20)
self.move_down_button.grid(row=3, column=0, padx=5, pady=5, sticky="e")
self.remove_button = tk.Button(self, text="Remove selected clock", command=self.remove_clock, width=20)
self.remove_button.grid(row=4, column=0, padx=5, pady=5, sticky="e")
self.add_button = tk.Button(self, text="Add clock", command=self.add_clock, width=20)
self.add_button.grid(row=5, column=0, padx=5, pady=5, sticky="e")
self.change_button = tk.Button(self, text="Change selected clock", command=self.change_clock, width=20)
self.change_button.grid(row=2, column=1, padx=5, pady=5, sticky="w")
self.reset_button = tk.Button(self, text="Change all clocks", command=self.change_all_clocks, width=20)
self.reset_button.grid(row=3, column=1, padx=5, pady=5, sticky="w")
self.save_button = ttk.Button(self, text="Save", command=self.ok)
self.save_button.grid(row=6, column=0, padx=5, pady=5, sticky="w")
self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel)
self.cancel_button.grid(row=6, column=1, padx=5, pady=5, sticky="e")
def move_up(self):
selected_index = self.listbox.curselection()
if selected_index:
index = selected_index[0]
if index > 0:
self.swap_items(index, index - 1)
def move_down(self):
selected_index = self.listbox.curselection()
if selected_index:
index = selected_index[0]
if index < self.listbox.size() - 1:
self.swap_items(index, index + 1)
def swap_items(self, index1, index2):
self.clocksettings[index1], self.clocksettings[index2] = (
self.clocksettings[index2],
self.clocksettings[index1],
)
self.populate_listbox()
self.listbox.selection_set(index2)
def populate_listbox(self):
self.listbox.delete(0, tk.END)
for item in self.clocksettings:
self.listbox.insert(tk.END, item["name"] + ": " + item["timezone"])
def remove_clock(self):
self.selected_clock = self.listbox.curselection()
for clock in self.selected_clock:
del self.clocksettings[clock]
self.populate_listbox()
def add_clock(self):
dialog = AddClockDialog(self, "Add Clock")
self.wait_window(dialog)
if dialog.newclocksettings:
self.clocksettings.append(dialog.newclocksettings)
self.populate_listbox()
def change_clock(self):
self.selected_clock = self.listbox.curselection()
if self.selected_clock:
dialog = ChangeClockDialog(self, "Change Clock", self.clocksettings[self.selected_clock[0]])
self.wait_window(dialog)
if dialog.changedclocksettings:
self.clocksettings[self.selected_clock[0]] = dialog.changedclocksettings
self.populate_listbox()
def change_all_clocks(self):
dialog = ChangeAllClocksDialog(self, "Change All Clocks", self.clocksettings)
self.wait_window(dialog)
if dialog.changedallclocksettings:
self.clocksettings = dialog.changedallclocksettings
self.populate_listbox()
def ok(self):
self.parent.clocksettings = self.clocksettings
self.parent.create_clocks()
self.destroy()
def cancel(self):
self.clocksettings.clear()
# Restore the original clocksettings
self.clocksettings.extend(self.original_clocksettings)
self.parent.clocksettings = self.clocksettings
self.destroy()
class AddClockDialog(tk.Toplevel):
def __init__(self, parent: ClocksDialog, title: str):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.newclocksettings = {}
ttk.Label(self, text="Enter city name:").grid(row=0, column=0, padx=5, pady=5)
self.city_entry = ttk.Entry(self, width=35)
self.city_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select timezone:").grid(row=1, column=0, padx=5, pady=5)
self.timezone_combobox = ttk.Combobox(self, values=pytz.all_timezones, width=35, height=10, state="readonly")
self.timezone_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
self.timezone_combobox.current(0)
ttk.Label(self, text="Select background color:").grid(row=2, column=0, padx=5, pady=5)
self.color_background = tk.Entry(self, width=35)
self.color_background.insert(0, "white")
self.color_background.grid(row=2, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select frame color:").grid(row=3, column=0, padx=5, pady=5)
self.color_frame = tk.Entry(self, width=35)
self.color_frame.insert(0, "grey")
self.color_frame.grid(row=3, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select city color:").grid(row=4, column=0, padx=5, pady=5)
self.color_name = tk.Entry(self, width=35)
self.color_name.insert(0, "grey")
self.color_name.grid(row=4, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select digital date time color:").grid(row=5, column=0, padx=5, pady=5)
self.color_text = tk.Entry(self, width=35)
self.color_text.insert(0, "grey")
self.color_text.grid(row=5, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock face color:").grid(row=6, column=0, padx=5, pady=5)
self.color_clockface = tk.Entry(self, width=35)
self.color_clockface.insert(0, "white")
self.color_clockface.grid(row=6, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock frame color:").grid(row=7, column=0, padx=5, pady=5)
self.color_clockframe = tk.Entry(self, width=35)
self.color_clockframe.insert(0, "grey")
self.color_clockframe.grid(row=7, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock hands color:").grid(row=8, column=0, padx=5, pady=5)
self.color_clockhands = tk.Entry(self, width=35)
self.color_clockhands.insert(0, "grey")
self.color_clockhands.grid(row=8, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select second hand color:").grid(row=9, column=0, padx=5, pady=5)
self.color_secondhand = tk.Entry(self, width=35)
self.color_secondhand.insert(0, "orange")
self.color_secondhand.grid(row=9, column=1, padx=5, pady=5, sticky="w")
self.add_button = ttk.Button(self, text="Add clock", command=self.add_clock)
self.add_button.grid(row=10, column=0, columnspan=2, padx=5, pady=5)
def add_clock(self):
# Validate colors
try:
self.winfo_rgb(self.color_background.get())
self.winfo_rgb(self.color_frame.get())
self.winfo_rgb(self.color_name.get())
self.winfo_rgb(self.color_text.get())
self.winfo_rgb(self.color_clockface.get())
self.winfo_rgb(self.color_clockframe.get())
self.winfo_rgb(self.color_clockhands.get())
self.winfo_rgb(self.color_secondhand.get())
except tk.TclError:
messagebox.showerror("Invalid Color", "Please enter a valid color.")
self.lift()
return
self.newclocksettings = {
"name": self.city_entry.get(),
"timezone": self.timezone_combobox.get(),
"color_background": self.color_background.get(),
"color_frame": self.color_frame.get(),
"color_name": self.color_name.get(),
"color_text": self.color_text.get(),
"color_clockface": self.color_clockface.get(),
"color_clockframe": self.color_clockframe.get(),
"color_clockhands": self.color_clockhands.get(),
"color_secondhand": self.color_secondhand.get(),
}
self.destroy()
# a dialog to change the selected clock
class ChangeClockDialog(tk.Toplevel):
def __init__(self, parent: ClocksDialog, title: str, clocksettings: dict):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.changedclocksettings = copy.deepcopy(clocksettings)
ttk.Label(self, text="Enter city name:").grid(row=0, column=0, padx=5, pady=5)
self.city_entry = ttk.Entry(self, width=35)
self.city_entry.insert(0, self.changedclocksettings["name"])
self.city_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select timezone:").grid(row=1, column=0, padx=5, pady=5)
self.timezone_combobox = ttk.Combobox(self, values=pytz.all_timezones, width=35, height=10, state="readonly")
self.timezone_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="w")
self.timezone_combobox.set(self.changedclocksettings["timezone"])
ttk.Label(self, text="Select background color:").grid(row=2, column=0, padx=5, pady=5)
self.color_background = tk.Entry(self, width=35)
self.color_background.insert(0, self.changedclocksettings["color_background"])
self.color_background.grid(row=2, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select frame color:").grid(row=3, column=0, padx=5, pady=5)
self.color_frame = tk.Entry(self, width=35)
self.color_frame.insert(0, self.changedclocksettings["color_frame"])
self.color_frame.grid(row=3, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select city color:").grid(row=4, column=0, padx=5, pady=5)
self.color_name = tk.Entry(self, width=35)
self.color_name.insert(0, self.changedclocksettings["color_name"])
self.color_name.grid(row=4, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select digital date time color:").grid(row=5, column=0, padx=5, pady=5)
self.color_text = tk.Entry(self, width=35)
self.color_text.insert(0, self.changedclocksettings["color_text"])
self.color_text.grid(row=5, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock face color:").grid(row=6, column=0, padx=6, pady=5)
self.color_clockface = tk.Entry(self, width=35)
self.color_clockface.insert(0, self.changedclocksettings["color_clockface"])
self.color_clockface.grid(row=6, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock frame color:").grid(row=7, column=0, padx=7, pady=5)
self.color_clockframe = tk.Entry(self, width=35)
self.color_clockframe.insert(0, self.changedclocksettings["color_clockframe"])
self.color_clockframe.grid(row=7, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock hands color:").grid(row=8, column=0, padx=5, pady=5)
self.color_clockhands = tk.Entry(self, width=35)
self.color_clockhands.insert(0, self.changedclocksettings["color_clockhands"])
self.color_clockhands.grid(row=8, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select second hand color:").grid(row=9, column=0, padx=5, pady=5)
self.color_secondhand = tk.Entry(self, width=35)
self.color_secondhand.insert(0, self.changedclocksettings["color_secondhand"])
self.color_secondhand.grid(row=9, column=1, padx=5, pady=5, sticky="w")
self.save_button = ttk.Button(self, text="Change Clock", command=self.change_clock)
self.save_button.grid(row=10, column=0, columnspan=2, padx=5, pady=5)
self.reset_button = ttk.Button(self, text="Reset", command=self.reset_clock)
self.reset_button.grid(row=11, column=0, columnspan=2, padx=5, pady=5)
def change_clock(self):
# Validate colors
try:
self.winfo_rgb(self.color_background.get())
self.winfo_rgb(self.color_frame.get())
self.winfo_rgb(self.color_name.get())
self.winfo_rgb(self.color_text.get())
self.winfo_rgb(self.color_clockface.get())
self.winfo_rgb(self.color_clockframe.get())
self.winfo_rgb(self.color_clockhands.get())
self.winfo_rgb(self.color_secondhand.get())
except tk.TclError:
messagebox.showerror("Invalid Color", "Please enter a valid color.")
self.lift()
return
self.changedclocksettings["name"] = self.city_entry.get()
self.changedclocksettings["timezone"] = self.timezone_combobox.get()
self.changedclocksettings["color_background"] = self.color_background.get()
self.changedclocksettings["color_frame"] = self.color_frame.get()
self.changedclocksettings["color_name"] = self.color_name.get()
self.changedclocksettings["color_text"] = self.color_text.get()
self.changedclocksettings["color_clockface"] = self.color_clockface.get()
self.changedclocksettings["color_clockframe"] = self.color_clockframe.get()
self.changedclocksettings["color_clockhands"] = self.color_clockhands.get()
self.changedclocksettings["color_secondhand"] = self.color_secondhand.get()
self.destroy()
def reset_clock(self):
# use standard colors
self.changedclocksettings["color_background"] = "white"
self.changedclocksettings["color_frame"] = "grey"
self.changedclocksettings["color_name"] = "grey"
self.changedclocksettings["color_text"] = "grey"
self.changedclocksettings["color_clockface"] = "white"
self.changedclocksettings["color_clockframe"] = "grey"
self.changedclocksettings["color_clockhands"] = "grey"
self.changedclocksettings["color_secondhand"] = "orange"
self.destroy()
class ChangeAllClocksDialog(tk.Toplevel):
def __init__(self, parent: ClocksDialog, title: str, clocksettings: list[dict]):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.parent = parent
self.changedallclocksettings = copy.deepcopy(clocksettings)
self.changedclocksettings = copy.deepcopy(clocksettings[0])
ttk.Label(self, text="Select background color:").grid(row=2, column=0, padx=5, pady=5)
self.color_background = tk.Entry(self, width=35)
self.color_background.insert(0, self.changedclocksettings["color_background"])
self.color_background.grid(row=2, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select frame color:").grid(row=3, column=0, padx=5, pady=5)
self.color_frame = tk.Entry(self, width=35)
self.color_frame.insert(0, self.changedclocksettings["color_frame"])
self.color_frame.grid(row=3, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select city color:").grid(row=4, column=0, padx=5, pady=5)
self.color_name = tk.Entry(self, width=35)
self.color_name.insert(0, self.changedclocksettings["color_name"])
self.color_name.grid(row=4, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select digital date time color:").grid(row=5, column=0, padx=5, pady=5)
self.color_text = tk.Entry(self, width=35)
self.color_text.insert(0, self.changedclocksettings["color_text"])
self.color_text.grid(row=5, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock face color:").grid(row=6, column=0, padx=6, pady=5)
self.color_clockface = tk.Entry(self, width=35)
self.color_clockface.insert(0, self.changedclocksettings["color_clockface"])
self.color_clockface.grid(row=6, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock frame color:").grid(row=7, column=0, padx=7, pady=5)
self.color_clockframe = tk.Entry(self, width=35)
self.color_clockframe.insert(0, self.changedclocksettings["color_clockframe"])
self.color_clockframe.grid(row=7, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select clock hands color:").grid(row=8, column=0, padx=5, pady=5)
self.color_clockhands = tk.Entry(self, width=35)
self.color_clockhands.insert(0, self.changedclocksettings["color_clockhands"])
self.color_clockhands.grid(row=8, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Select second hand color:").grid(row=9, column=0, padx=5, pady=5)
self.color_secondhand = tk.Entry(self, width=35)
self.color_secondhand.insert(0, self.changedclocksettings["color_secondhand"])
self.color_secondhand.grid(row=9, column=1, padx=5, pady=5, sticky="w")
self.save_button = ttk.Button(self, text="Change all clocks", command=self.change_clock)
self.save_button.grid(row=10, column=0, columnspan=2, padx=5, pady=5)
self.reset_button = ttk.Button(self, text="Reset all clocks", command=self.reset_clock)
self.reset_button.grid(row=11, column=0, columnspan=2, padx=5, pady=5)
def change_clock(self):
# Validate colors
try:
self.winfo_rgb(self.color_background.get())
self.winfo_rgb(self.color_frame.get())
self.winfo_rgb(self.color_name.get())
self.winfo_rgb(self.color_text.get())
self.winfo_rgb(self.color_clockface.get())
self.winfo_rgb(self.color_clockframe.get())
self.winfo_rgb(self.color_clockhands.get())
self.winfo_rgb(self.color_secondhand.get())
except tk.TclError:
messagebox.showerror("Invalid Color", "Please enter a valid color.")
self.lift()
return
for clock in self.changedallclocksettings:
clock["color_background"] = self.color_background.get()
clock["color_frame"] = self.color_frame.get()
clock["color_name"] = self.color_name.get()
clock["color_text"] = self.color_text.get()
clock["color_clockface"] = self.color_clockface.get()
clock["color_clockframe"] = self.color_clockframe.get()
clock["color_clockhands"] = self.color_clockhands.get()
clock["color_secondhand"] = self.color_secondhand.get()
self.destroy()
def reset_clock(self):
# use standard colors
for clock in self.changedallclocksettings:
clock["color_background"] = "white"
clock["color_frame"] = "grey"
clock["color_name"] = "grey"
clock["color_text"] = "grey"
clock["color_clockface"] = "white"
clock["color_clockframe"] = "grey"
clock["color_clockhands"] = "grey"
clock["color_secondhand"] = "orange"
self.destroy()
# new ChangeNTPServerDialog based on tk.topLevel
class NTPServerDialog(tk.Toplevel):
def __init__(self, parent: TimeZoneClockApp, title: str, ntp_servers: list[str]):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.parent = parent
self.ntp_servers = ntp_servers
# Make a copy of the original ntp-servers
self.original_ntp_servers = copy.deepcopy(ntp_servers)
self.selected_ntp_server = None
self.label = ttk.Label(self, text="List of NTP Servers")
self.label.grid(row=0, column=0, padx=5, pady=5, sticky="w", columnspan=2)
self.listbox = tk.Listbox(self, selectmode=tk.SINGLE, width=40)
for server in self.ntp_servers:
self.listbox.insert(tk.END, server)
self.listbox.grid(row=1, column=0, padx=5, pady=5, sticky="ew", columnspan=2)
self.move_up_button = tk.Button(self, text="Move selected server up", command=self.move_up, width=20)
self.move_up_button.grid(row=2, column=0, padx=5, pady=5, sticky="e")
self.move_down_button = tk.Button(self, text="Move selected server down", command=self.move_down, width=20)
self.move_down_button.grid(row=3, column=0, padx=5, pady=5, sticky="e")
self.remove_button = tk.Button(
self,
text="Remove selected server",
command=self.remove_ntp_server,
width=20,
)
self.remove_button.grid(row=4, column=0, padx=5, pady=5, sticky="e")
self.add_button = tk.Button(self, text="Add server", command=self.add_ntp_server, width=20)
self.add_button.grid(row=5, column=0, padx=5, pady=5, sticky="e")
self.save_button = ttk.Button(self, text="Save", command=self.ok)
self.save_button.grid(row=6, column=0, padx=5, pady=5, sticky="w")
self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel)
self.cancel_button.grid(row=6, column=1, padx=5, pady=5, sticky="e")
def move_up(self):
selected_index = self.listbox.curselection()
if selected_index:
index = selected_index[0]
if index > 0:
self.swap_items(index, index - 1)
def move_down(self):
selected_index = self.listbox.curselection()
if selected_index:
index = selected_index[0]
if index < self.listbox.size() - 1:
self.swap_items(index, index + 1)
def swap_items(self, index1, index2):
self.ntp_servers[index1], self.ntp_servers[index2] = (
self.ntp_servers[index2],
self.ntp_servers[index1],
)
self.populate_listbox()
self.listbox.selection_set(index2)
def populate_listbox(self):
self.listbox.delete(0, tk.END)
for item in self.ntp_servers:
self.listbox.insert(tk.END, item)
def remove_ntp_server(self):
self.selected_ntp_server = self.listbox.curselection()
for server in self.selected_ntp_server:
del self.ntp_servers[server]
self.populate_listbox()
def add_ntp_server(self):
dialog = AddNTPServerDialog(self, "Add NTP Server")
self.wait_window(dialog)
if dialog.server:
self.ntp_servers.insert(0, dialog.server)
self.populate_listbox()
def ok(self):
self.parent.ntp_servers = self.ntp_servers
self.parent.ntp_sync()
self.destroy()
def cancel(self):
self.ntp_servers.clear()
# Restore the original ntp-servers
self.ntp_servers.extend(self.original_ntp_servers)
self.parent.ntp_servers = self.ntp_servers
self.destroy()
class AddNTPServerDialog(tk.Toplevel):
def __init__(self, parent: ClocksDialog, title: str):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.server = None
ttk.Label(self, text="Enter NTP Server:").grid(row=0, column=0, padx=5, pady=5)
self.server_entry = ttk.Entry(self, width=35)
self.server_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w")
self.add_button = ttk.Button(self, text="Add server", command=self.add_server)
self.add_button.grid(row=1, column=0, columnspan=2, padx=5, pady=5)
def add_server(self):
self.server = self.server_entry.get()
self.destroy()
class SettingsDialog(tk.Toplevel):
def __init__(self, parent: TimeZoneClockApp, title: str):
super().__init__(parent)
self.title(title)
self.iconphoto(True, tk.PhotoImage(file=resource_path("icon.png")))
self.resizable(False, False)
self.parent = parent
self.autosync_active_var = tk.BooleanVar(value=self.parent.autosync_active)
self.autosync_active = ttk.Checkbutton(self, text="Autosync", variable=self.autosync_active_var)
self.autosync_active.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.write_log_var = tk.BooleanVar(value=self.parent.write_log)
self.write_log = ttk.Checkbutton(self, text="Write log", variable=self.write_log_var)
self.write_log.grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.fullscreen_var = tk.BooleanVar(value=self.parent.start_fullscreen)
self.fullscreen = ttk.Checkbutton(self, text="Start in fullscreen mode", variable=self.fullscreen_var)
self.fullscreen.grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(self, text="Sync Interval (minutes):").grid(row=2, column=0, padx=5, pady=(5,0), sticky="w")
self.sync_interval_var = tk.IntVar(value=self.parent.sync_interval)
self.sync_interval_entry = ttk.Entry(self, textvariable=self.sync_interval_var, width=10)
self.sync_interval_entry.grid(row=2, column=1, padx=5, pady=(5,0), sticky="w")
ttk.Label(self, text="Set to -1 for a single sync at program start").grid(row=3, column=0, columnspan=2, padx=5, pady=(0,5), sticky="w")
ttk.Label(self, text="Background Color").grid(row=4, column=0, padx=5, pady=5, sticky="w")
self.background_color_var = tk.StringVar(value=self.parent.color_background)
self.background_color_entry = tk.Entry(self, textvariable=self.background_color_var, width=10)
self.background_color_entry.grid(row=4, column=1, padx=5, pady=5, sticky="w")
self.save_button = ttk.Button(self, text="Save", command=self.save_settings)
self.save_button.grid(row=5, column=0, padx=5, pady=5, sticky="w")
self.cancel_button = ttk.Button(self, text="Cancel", command=self.cancel)
self.cancel_button.grid(row=5, column=1, padx=5, pady=5, sticky="e")
def save_settings(self):
# validate colors
try:
self.winfo_rgb(self.background_color_var.get())
except tk.TclError:
messagebox.showerror("Invalid Color", "Please enter a valid color.")
self.lift()
return
self.parent.autosync_active = self.autosync_active_var.get()
self.parent.write_log = self.write_log_var.get()
self.parent.start_fullscreen = self.fullscreen_var.get()
self.parent.sync_interval = self.sync_interval_var.get()
self.parent.color_background = self.background_color_var.get()
self.parent.autosync()
self.parent.create_clocks()
self.destroy()
def cancel(self):
self.destroy()
def resource_path(relative_path):
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def main():
app = TimeZoneClockApp()
app.mainloop()
if __name__ == "__main__":
main()