6
\$\begingroup\$

I have designed a basic interface for a smart mirror I made. I coded it to include the time, date, and several phrases that refresh every 24 hours. The phrases are just some random quotes as examples.

I was wondering if there were anyways I could streamline this code or even improve the layout of the interface.


import tkinter as tk
import sys
import time
import calendar
import random
import datetime as dt
from tkinter import *

# Root is the name of the Tkinter Window. This is important to remember.
root=tk.Tk()

""" DICTIONARY PHRASES """
phrases = ["       I never had a policy; I have just tried to do my very best each and every day. -- Abraham Lincoln",
"      There are some things you learn best in calm, and some in storm. -- Willa Cather",
"       If a man does his best, what else is there? -- George S. Patton"]


class Clock(tk.Label):
    """ Class that contains the clock widget and clock refresh """

    def __init__(self, parent=None, seconds=True, colon=False):
        """
        Create and place the clock widget into the parent element
        It's an ordinary Label element with two additional features.
        """
        tk.Label.__init__(self, parent)

        self.display_seconds = seconds
        if self.display_seconds:
            self.time     = time.strftime('%I:%M:%S %p')
        else:
            self.time     = time.strftime('%I:%M:%S %p').lstrip('0')
        self.display_time = self.time
        self.configure(text=self.display_time, width=11)

        if colon:
            self.blink_colon()

        self.after(200, self.tick)


    def tick(self):
        """ Updates the display clock every 200 milliseconds """
        if self.display_seconds:
            new_time = time.strftime('%I:%M:%S %p')
        else:
            new_time = time.strftime('%I:%M:%S %p').lstrip('0')
        if new_time != self.time:
            self.time = new_time
            self.display_time = self.time
            self.config(text=self.display_time)
        self.after(200, self.tick)


    def blink_colon(self):
        """ Blink the colon every second """
        if ':' in self.display_time:
            self.display_time = self.display_time.replace(':',' ')
        else:
            self.display_time = self.display_time.replace(' ',':',1)
        self.config(text=self.display_time)
        self.after(1000, self.blink_colon)



class FullScreenApp(object):
    def __init__(self, master, **kwargs):
        self.master=master
        pad=3
        self._geom='200x200+0+0'
        master.geometry("{0}x{1}+0+0".format(
            master.winfo_screenwidth()-pad, master.winfo_screenheight()-pad))
        master.bind('<Escape>',self.toggle_geom)            
    def toggle_geom(self,event):
        geom=self.master.winfo_geometry()
        print(geom,self._geom)
        self.master.geometry(self._geom)
        self._geom=geom

def phrase_refresh():
    new_phrase = random.choice(phrases)
    e.configure(text=new_phrase, wrap=650) # e is your label
    root.after(86400, phrase_refresh) # Delay measured in milliseconds. Currently set to 24 hours. (There are 86400 milliseconds in a day)
    return new_phrase

# Sets background color to black
root.configure(bg="black")

# Removes the window bar at the top creating a truely fullscreen
root.wm_attributes('-fullscreen','true')
tk.Button(root, text="", bg="black", fg="black", command=lambda root=root:quit(root)).pack()

#Spacer
v = Label(root, text="", fg="white", bg="black")
v.pack(anchor=NW)

# this displays the clock known as clock1
clock1 = Clock(root)
clock1.pack(anchor=NW)

# This gives the clock format.
clock1.configure(bg='black',fg='white',font=("helvetica",60))

# Add the date to the tkinter window
w = Label(root, text=f"{dt.datetime.now():%a, %b %d %Y}", fg="white", bg="black", font=("helvetica", 30), width=16)
w.pack(anchor=NW)

#Spacer
s = Label(root, text="", fg="white", bg="black")
s.pack(anchor=NW)

#Spacer
p = Label(root, text="", fg="white", bg="black")
p.pack(anchor=NW)


# Add the phrase to the tkinter window
e = Label(root, fg="white", bg="black", font=("helvetica", 17))
phrase_refresh()
e.pack(anchor=NW)


root.mainloop()

\$\endgroup\$
3
\$\begingroup\$

Here are a few comments:

Eliminate extra/unused imports

Might be better to load quotes from a file (or from a "Quote of the Day" web site (RSS feed). That way you don't have to edit the source to add new ones.

It's not necessary to make a separate Clock class. It seems to complicate things. The code below has one function on a callback timer. It "over samples" the time 5x per second. When the second or day (could be anything) changes, a function is called to update the time/date/phrase.

Time format displays seconds whether display_seconds is True or False.

class FullScreenApp isn't used.

Here's my take on the display part. Getting quotes from a file or URL is an exercise for the reader:

import datetime
import random

import tkinter as tk

MESSAGE_WIDTH = 400
SHOW_SECONDS = False
BLINK_COLON = True

""" DICTIONARY PHRASES """
phrases = [
    "I never had a policy; I have just tried to do my very best each and every day. -- Abraham Lincoln",
    "There are some things you learn best in calm, and some in storm. -- Willa Cather",
    "If a man does his best,\n what else is there? -- George S. Patton",
    "Better to write for yourself and have no public, than to write for the public and have no self. -- Cyril Connolly",
    "Life is far too important a thing ever to talk seriously about. -- Oscar Wilde",
    "Indeed, history is nothing more than a tableau of crimes and misfortunes. -- Voltaire",
    "In great affairs men show themselves as they wish to be seen; in small things they show themselves as they are. -- Nicholas Chamfort",
    "The first step to getting the things you want out of life is this: Decide what you want. -- Ben Stein",
    "Life is what happens to you while you're busy making other plans. -- John Lennon",
    "For four-fifths of our history, our planet was populated by pond scum. -- J. W. Schopf",
    "History will be kind to me for I intend to write it. -- Sir Winston Churchill",
    "History is the version of past events that people have decided to agree upon. -- Napoleon Bonaparte",
    "You create your opportunities by asking for them. -- Patty Hensen",
    "If everyone had a dad like mine, no one would have sex tapes. -- Tina Fey"
    ]

root = tk.Tk()
root.configure(bg="black")

prev_time = datetime.datetime(1,1,1)

time_display = tk.StringVar(root, " "*len(" HH:MM:SS PM "))
date_display = tk.StringVar(root, " "*len("aaa, bbb dd, YYYY"))
phrase_display = tk.StringVar(root, " "*len(max(phrases, key=len)))
author_display = tk.StringVar(root, " "*MESSAGE_WIDTH)

def tick():
    global prev_time

    curr_time = datetime.datetime.now()

    if curr_time.second != prev_time.second:
        update_time(curr_time)

    if curr_time.second//15  != prev_time.second//15:    # this is for testing, it makes the quote update every 15 seconds
#    if curr_time.day != prev_time.day:                  # normally use this one to update quote every day
        update_phrase()

    if curr_time.day != prev_time.day:
        update_date(curr_time)

    prev_time = curr_time

    root.after(200, tick)


def update_date(curr_time):
    date_display.set(f"{curr_time:%a, %b %d, %Y}")


def update_phrase():
    phrase, author = random.choice(phrases).split('--')                              
    phrase_display.set(phrase.strip())
    author_display.set(f"-- {author.strip()}")


def update_time(curr_time):
    #       colon            no colon 
    fmt=[["   %H:%M %p  ", "   %H %M %p  "],        # without seconds
         [" %H:%M:%S %p ", " %H %M %S %p "]      # with seconds
        ][SHOW_SECONDS][BLINK_COLON and curr_time.second % 2]

    time_display.set(f"{curr_time:{fmt}}")


# Removes the window bar at the top creating a truely fullscreen
#root.wm_attributes('-fullscreen','true')
tk.Button(root, text="quit", bg="black", fg="red", command=root.quit).pack()

#Spacer
tk.Label(root, text="", fg="white", bg="black").pack()

# this displays the clock known as clock
clock = tk.Label(root, textvariable=time_display, bg='black',fg='white',font=("helvetica",60))
clock.pack()

# Add the date to the tkinter window
date = tk.Label(root, textvariable=date_display, fg="white", bg="black", font=("helvetica", 30), width=16)
date.pack()

#double height Spacer
tk.Label(root, text="\n\n", fg="white", bg="black").pack()

# Add the phrase to the tkinter window
text = tk.Message(root, textvariable=phrase_display, fg="white", bg="black", font=("helvetica", 17), width=400, justify="left")
text.pack()

author = tk.Message(root, textvariable=author_display, fg="white", bg="black", font=("helvetica", 17), width=400, justify='right', anchor='e')
author.pack()

#Spacer
tk.Label(root, text="", fg="white", bg="black").pack()

# starts the clock
tick()

root.mainloop()

# In some developement environments root.quit doesn't fully destroy the application; hence
# the call to root.destroy().  But clicking the close button (the 'X') on a window does destroy
# the application, so calling root.destroy() raises a TclError for trying to destroy it a
# second time.  This quiets the error message. 
try:
    root.destroy()
except tk.TclError:
    pass
\$\endgroup\$
4
  • \$\begingroup\$ Sweet! Thank you for the suggestion! I will definitely look into it 👍 \$\endgroup\$ – Megastrik3 Feb 2 '20 at 12:54
  • \$\begingroup\$ Didn't know the format specifier gets passed along to the datetime object like this. This is shorter (and more readable) than writing curr_time.strftime("%a, %b %d, %Y"), nice! \$\endgroup\$ – Graipher Feb 2 '20 at 16:32
  • \$\begingroup\$ @Graipher, f-strings call the __format__(self, format_spec) method. So it works with anything that defines that method. User defined classes can define their own format_spec. \$\endgroup\$ – RootTwo Feb 2 '20 at 18:36
  • \$\begingroup\$ @RootTwo: Yeah, and I even read somewhere recently that datetime just implements __format__, I just never connected the dots and realized that this means I can get rid of that strfmt... \$\endgroup\$ – Graipher Feb 2 '20 at 19:10
3
\$\begingroup\$

Congratulations on writing a tk program. They can be hard.

Moving code out of the top level is an improvement you can in any program. Top level code creates global variables, makes it unclear what might be used below, and so on. You could just do this:

def main():
    # Sets background color to black
    root.configure(bg="black")
    ...
        except tk.TclError:
            pass

main()

It may seem minor, but it makes it easier to see that root, time_display and others are global, but that author is not.

One fix at a time. Keep hacking! Keep notes.

\$\endgroup\$
1
  • \$\begingroup\$ Okay, I will definitely be trying that! Anything to make my code better and more efficient. \$\endgroup\$ – Megastrik3 Feb 2 '20 at 12:55
2
\$\begingroup\$

Good start but a few things should be cleaned up.

Try to follow a standard. Preferably PEP8. You have extra spacing between = and no spacing between , in places and its messy.

  1. delete from tkinter import * this is for 2 reason. The first is you are already doing import tkinter as tk so you do not need a 2nd import of tkinter. Two import * can cause problems if imported names overlap and get overwritten.

  2. Remove imports you are not using. IE: calenddar, sys

  3. Fix clean up formatting to follow PEP8. Single spaces between methods double spaces between functions/class and so on.

  4. Remove unneeded/redundant comments. Comments should be reserver for explaining things that are not obvious in the code.

  5. You write you tick code twice. One in __init__ and once in tick. You can remove the instance in __init__ and simply call tick right away while adding a default value for self.time and self.display_time in the init.

  6. You use 200 milliseconds in your after statement. This is 5 times more than you need. 1 second intervals will suffice. There is no need to call the function 5 times more than is needed.

  7. The class FullScreenApp is never used. So either implement it somewhere or remove it.

  8. Your return in phrase_refresher does nothing for you. Its returning to a call with no variable assignment and then its returning to itself forever after that. You can remove the return.

  9. Your quit button does not need a lambda. You can save a reference to quit.

  10. Your quit button has no word and is the same color as background. This will make it very had for someone to click it :D

  11. You are already inheriting from tk.Label so why not do the same for the root window?

  12. Lastly you do not need spacer labels. You can use padx and pady to determine spacing between widgets.

See below reworked example. Let me know if you have any questions.

import tkinter as tk
import datetime as dt
import random
import time

phrases = ["       I never had a policy; I have just tried to do my very best each and every day. -- Abraham Lincoln",
           "       There are some things you learn best in calm, and some in storm. -- Willa Cather",
           "       If a man does his best, what else is there? -- George S. Patton"]


class Clock(tk.Label):
    """ Class that contains the clock widget and clock refresh """

    def __init__(self, parent=None, seconds=True, colon=False):
        """
        Create and place the clock widget into the parent element
        It's an ordinary Label element with two additional features.
        """
        tk.Label.__init__(self, parent, width=11)
        self.display_seconds = seconds
        self.time = None
        if colon:
            self.blink_colon()
        self.tick()

    def tick(self):
        """ Updates the display clock every 200 milliseconds """
        if self.display_seconds:
            new_time = time.strftime('%I:%M:%S %p')
        else:
            new_time = time.strftime('%I:%M:%S %p').lstrip('0')
        print(new_time)
        if self.time is not None:
            if new_time != self.time:
                self.time = new_time
                self.display_time = self.time
                self.config(text=self.display_time)
        else:
            self.time = new_time
        self.after(1000, self.tick)

    def blink_colon(self):
        """ Blink the colon every second """
        if ':' in self.display_time:
            self.display_time = self.display_time.replace(':', ' ')
        else:
            self.display_time = self.display_time.replace(' ', ':', 1)
        self.config(text=self.display_time)
        self.after(1000, self.blink_colon)


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.configure(bg="black")
        self.wm_attributes('-fullscreen', 'true')
        tk.Button(self, text="Quit", bg="black", fg="white", command=self.quit).pack()

        clock1 = Clock(self)
        clock1.configure(bg='black', fg='white', font=("helvetica", 60))

        w = tk.Label(self, text=f"{dt.datetime.now():%a, %b %d %Y}",
                     fg="white", bg="black", font=("helvetica", 30), width=16)
        clock1.pack(anchor='nw', pady=(25, 0))
        w.pack(anchor='nw', pady=(0, 30))

        self.e = tk.Label(self, fg="white", bg="black", font=("helvetica", 17))
        self.phrase_refresh()
        self.e.pack(anchor='nw')

    def phrase_refresh(self):
        new_phrase = random.choice(phrases)
        self.e.configure(text=new_phrase, wrap=650)
        self.after(86400, self.phrase_refresh)


App().mainloop()

Lastly I am not sure if you wanted to have the timer at the top left. Judging by the fact you fullscreen I am guessing you want it centered. For this I would prefer to use grid() over pack() as grid allows us to set weights for rows and columns making it IMO easier to center everything.

See this example:

import tkinter as tk
import datetime as dt
import random
import time


phrases = ["I never had a policy; I have just tried to do my very best each and every day. -- Abraham Lincoln",
           "There are some things you learn best in calm, and some in storm. -- Willa Cather",
           "If a man does his best, what else is there? -- George S. Patton"]


class Clock(tk.Label):
    """ Class that contains the clock widget and clock refresh """
    def __init__(self, parent=None, seconds=True, colon=False):
        """
        Create and place the clock widget into the parent element
        It's an ordinary Label element with two additional features.
        """
        tk.Label.__init__(self, parent, width=11)
        self.display_seconds = seconds
        self.time = None
        if colon:
            self.blink_colon()
        self.tick()

    def tick(self):
        """ Updates the display clock every 200 milliseconds """
        if self.display_seconds:
            new_time = time.strftime('%I:%M:%S %p')
        else:
            new_time = time.strftime('%I:%M:%S %p').lstrip('0')
        print(new_time)
        if self.time is not None:
            if new_time != self.time:
                self.time = new_time
                self.display_time = self.time
                self.config(text=self.display_time)
        else:
            self.time = new_time
        self.after(1000, self.tick)

    def blink_colon(self):
        """ Blink the colon every second """
        if ':' in self.display_time:
            self.display_time = self.display_time.replace(':', ' ')
        else:
            self.display_time = self.display_time.replace(' ', ':', 1)
        self.config(text=self.display_time)
        self.after(1000, self.blink_colon)


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.configure(bg="black")
        self.wm_attributes('-fullscreen', 'true')
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

        tk.Button(self, text="Quit", bg="black", fg="white", command=self.quit).grid(row=0, column=0)

        frame = tk.Frame(self, bg="black")
        frame.grid(row=1, column=0)

        clock1 = Clock(frame)
        clock1.configure(bg='black', fg='white', font=("helvetica", 60))

        w = tk.Label(frame, text=f"{dt.datetime.now():%a, %b %d %Y}",
                     fg="white", bg="black", font=("helvetica", 30), width=16)
        clock1.grid(row=2, column=0, pady=(25, 0))
        w.grid(row=3, column=0, pady=(0, 30))

        self.e = tk.Label(frame, fg="white", bg="black", font=("helvetica", 17))
        self.phrase_refresh()
        self.e.grid(row=6, column=0)

    def phrase_refresh(self):
        new_phrase = random.choice(phrases)
        self.e.configure(text=new_phrase, wrap=650)
        self.after(86400, self.phrase_refresh)


App().mainloop()
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Okay, cool! Thanks for the feedback 👍 \$\endgroup\$ – Megastrik3 Feb 22 '20 at 22:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.