Weltzeituhr - Erstes Programmierprojekt

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
herminator
User
Beiträge: 3
Registriert: Freitag 27. Juni 2025, 15:31

Hallo zusammen,

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()

juergenkulow
User
Beiträge: 7
Registriert: Freitag 6. Juni 2025, 08:09

Hallo herminator,

ich bekomme folgende Fehlermeldung:

Code: Alles auswählen

kulow@kulow-G73Jw:~$ python3 /tmp/uhr.py
  File "/tmp/uhr.py", line 594
    self.itemconfig(self.utc_offset_text, text=f"UTC{now.strftime("%:z")}")
                                                                   ^
SyntaxError: f-string: unmatched '('
Wie leistungsfähig ist Dein Raspberry Pi ? Heute hat ein Gaming-PC die mehrfache Gesamtleistung einer Cray-1 .
Bitte stelle Deine Fragen, denn den Erkenntnisapparat einschalten entscheidet über das einzig bekannte Leben im Universum.

Jürgen Kulow Wersten :D_üsseldorf NRW D Europa Erde Sonnensystem Lokale_Flocke Lokale_Blase Orion-Arm
Milchstraße Lokale_Gruppe Virgo-Superhaufen Laniakea Sichtbares_Universum
Sirius3
User
Beiträge: 18260
Registriert: Sonntag 21. Oktober 2012, 17:20

@juergenkulow: in früheren Python konnte man keine Anführungszeichen in {}-Ausdrücken verschachteln, Deine Python-Version ist einfach zu alt.
Der Aufruf von strftime ist aber eh unnötig, weil man die Formatierungsanweisung direkt angeben kann:

Code: Alles auswählen

text=f"UTC{now:%:z}"
herminator
User
Beiträge: 3
Registriert: Freitag 27. Juni 2025, 15:31

juergenkulow hat geschrieben: Dienstag 1. Juli 2025, 06:33 ich bekomme folgende Fehlermeldung:

Code: Alles auswählen

kulow@kulow-G73Jw:~$ python3 /tmp/uhr.py
  File "/tmp/uhr.py", line 594
    self.itemconfig(self.utc_offset_text, text=f"UTC{now.strftime("%:z")}")
                                                                   ^
SyntaxError: f-string: unmatched '('
Wie leistungsfähig ist Dein Raspberry Pi ? Heute hat ein Gaming-PC die mehrfache Gesamtleistung einer Cray-1 .
Bei mir mit Python 3.13 bekomme ich die Fehlermeldung nicht. Mit Python 3.11 tatsächlich auch.

Klar, mein Windows-Rechner ist deutlich leistungsfähiger als mein Raspy 4. Aber er kann ja z.B. Full HD Videos ruckelfrei streamen, da hätte ich gedacht, er kann 3 Sekundenzeiger synchron ticken lassen.
Sirius3 hat geschrieben: Dienstag 1. Juli 2025, 09:39 @juergenkulow: in früheren Python konnte man keine Anführungszeichen in {}-Ausdrücken verschachteln, Deine Python-Version ist einfach zu alt.
Der Aufruf von strftime ist aber eh unnötig, weil man die Formatierungsanweisung direkt angeben kann:

Code: Alles auswählen

text=f"UTC{now:%:z}"
Danke für den Hinweis. Ich vermute, dass ich eine Vielzahl solch unnötiger Dinge in meinem Code habe, die alles verlangsamen.
Sirius3
User
Beiträge: 18260
Registriert: Sonntag 21. Oktober 2012, 17:20

Beim ersten Drüberschauen fallen mir sehr viele kopierte Codepassagen auf. Die drei Clock-Dialog-Klassen kann man sicher zu einer zusammenfassen.
Alle Uhren sollten synchron von einem after-Aufruf aktualisiert werden, mit einer Zeit. Warum arbeitest Du mit Zeitzonen und einem zusätzlichen Offset? Eins von beidem ist doch überflüssig. Ich würde den Offset einmal berechnen und nicht jedesmal die Zeitzone neu ermitteln. Statt die "hands" jedesmal zu löschen und wieder neu zu erzeugen, einfach die Linien verändern.
city_text und offset_text werden sich nie, date_text nur sehr selten ändern. Die muß mal also nicht jedesmal neu setzen.
In ntp_sync veränderst Du die GUI, aber diese Methode wird über threading aufgerufen, das darf man nicht, warum benutzt Du da nicht auch `after`?
Statt aus einem Frame alle `children` zu löschen, öscht man am einfachsten den kompletten Frame und erzeugt diesen neu.
Benutzeravatar
__blackjack__
User
Beiträge: 14018
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

NTP hat in dem Programm IMHO sowieso nichts zu suchen. So etwas löst man auf Systemebene und nicht in einzelnen grafischen Anwendungen. Und genau dort *ist* das normalerweise ja auch schon gelöst. Gerade beim Raspi, weil die Geräte in der Regel ja gar keine RTC haben, und sich die Zeit per NTP holen *müssen*.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
herminator
User
Beiträge: 3
Registriert: Freitag 27. Juni 2025, 15:31

Ah super, vielen Dank. Das sind viele gute Hinweise. Die gleichzeitige Aktualisierung der Uhren mit einem einzigen Aufruf ist vielleicht die Idee, die mir fehlte. Bisher mach ich das ja in den einzelnen Uhren-Instanzen.
Auch die anderen Hinweise, wie die falsche Verwendung von threading etc., werde ich ändern.
Das Abgleichen mit dem NTP Server war tatsächlich auch eher eine kleine Spielerei, weil der Rechner, auf dem ich anfing zu programmieren, eine oft falschgehende Uhr hatte (trotz aktivierter windowseigener Synchronisation und richtiger Zeitzone). Deshalb wollte ich eine Warnung haben, wenn die Uhr wieder falsch geht. Irgendwann hab ich die Abweichung dann direkt über die erwähnte Offset-Variable korrigiert. Das ist also nicht der Zeitzonenoffset, den berücksichtige ich ja über pytz.timezone.

Auf jeden Fall vielen Dank. Ich melde mich hoffentlich in ein paar Wochen noch mal mit einem verbesserten und vereinfachten Code.
Antworten