--------------------------------windows.py-------------------------------
"""
###############################################################################
Classes that encapsulate top-level interfaces.
Allows same GUI to be main, pop-up, or attached; content classes may inherit
from these directly, or be mixed together with them per usage mode; may also
be called directly without a subclass; designed to be mixed in after (further
to the right than) app-specific classes: else, subclass gets methods here
(destroy, okayToQuit), instead of from app-specific classes--can't redefine.
###############################################################################
"""
import os, glob
from tkinter import Tk, Toplevel, Frame, YES, BOTH, RIDGE
from tkinter.messagebox import showinfo, askyesno
class _window:
"""
mixin shared by main and pop-up windows
"""
foundicon = None # shared by all inst
iconpatt = '*.ico' # may be reset
iconmine = 'py.ico'
def configBorders(self, app, kind, iconfile):
if not iconfile: # no icon passed?
iconfile = self.findIcon() # try curr,tool dirs
title = app
if kind: title += ' - ' + kind
self.title(title) # on window border
self.iconname(app) # when minimized
if iconfile:
try:
self.iconbitmap(iconfile) # window icon image
except: # bad py or platform
pass
self.protocol('WM_DELETE_WINDOW', self.quit) # don't close silent
def findIcon(self):
if _window.foundicon: # already found one?
return _window.foundicon
iconfile = None # try curr dir first
iconshere = glob.glob(self.iconpatt) # assume just one
if iconshere: # del icon for red Tk
iconfile = iconshere[0]
else: # try tools dir icon
mymod = __import__(__name__) # import self for dir
path = __name__.split('.') # poss a package path
for mod in path[1:]: # follow path to end
mymod = getattr(mymod, mod) # only have leftmost
mydir = os.path.dirname(mymod.__file__)
myicon = os.path.join(mydir, self.iconmine) # use myicon, not tk
if os.path.exists(myicon): iconfile = myicon
_window.foundicon = iconfile # don't search again
return iconfile
class MainWindow(Tk, _window):
"""
when run in main top-level window
"""
def init(self, app, kind='', iconfile=None):
self.findIcon()
Tk.init(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self):
if self.okayToQuit(): # threads running?
if askyesno(self.__app, 'Verify Quit Program?'):
self.destroy() # quit whole app
else:
showinfo(self.__app, 'Quit not allowed') # or in okayToQuit?
def destroy(self): # exit app silently
Tk.quit(self) # redef if exit ops
def okayToQuit(self): # redef me if used
return True # e.g., thread busy
class PopupWindow(Toplevel, _window):
"""
when run in secondary pop-up window
"""
def init(self, app, kind='', iconfile=None):
Toplevel.init(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self): # redef me to change
if askyesno(self.__app, 'Verify Quit Window?'): # or call destroy
self.destroy() # quit this window
def destroy(self): # close win silently
Toplevel.destroy(self) # redef for close ops
class QuietPopupWindow(PopupWindow):
def quit(self):
self.destroy() # don't verify close
class ComponentWindow(Frame):
"""
when attached to another display
"""
def init(self, parent): # if not a frame
Frame.init(self, parent) # provide container
self.pack(expand=YES, fill=BOTH)
self.config(relief=RIDGE, border=2) # reconfig to change
def quit(self):
showinfo('Quit', 'Not supported in attachment mode')
# destroy from Frame: erase frame silent # redef for close ops
----------------------------------------clock.py-------------------------------
"""
###############################################################################
PyClock 2.1: a clock GUI in Python/tkinter.
With both analog and digital display modes, a pop-up date label, clock face
images, general resizing, etc. May be run both standalone, or embedded
(attached) in other GUIs that need a clock.
New in 2.0: s/m keys set seconds/minutes timer for pop-up msg; window icon.
New in 2.1: updated to run under Python 3.X (2.X no longer supported)
###############################################################################
"""
from tkinter import *
from tkinter.simpledialog import askinteger
import math, time, sys
###############################################################################
Option configuration classes
###############################################################################
class ClockConfig:
# defaults--override in instance or subclass
size = 200 # width=height
bg, fg = 'beige', 'brown' # face, tick colors
hh, mh, sh, cog = 'black', 'navy', 'blue', 'red' # clock hands, center
picture = None # face photo file
class PhotoClockConfig(ClockConfig):
# sample configuration
size = 320
picture = '../gifs/ora-pp.gif'
bg, hh, mh = 'white', 'blue', 'orange'
###############################################################################
Digital display object
###############################################################################
class DigitalDisplay(Frame):
def init(self, parent, cfg):
Frame.init(self, parent)
self.hour = Label(self)
self.mins = Label(self)
self.secs = Label(self)
self.ampm = Label(self)
for label in self.hour, self.mins, self.secs, self.ampm:
label.config(bd=4, relief=SUNKEN, bg=cfg.bg, fg=cfg.fg)
label.pack(side=LEFT) # TBD: could expand, and scale font on resize
def onUpdate(self, hour, mins, secs, ampm, cfg):
mins = str(mins).zfill(2) # or '%02d' % x
self.hour.config(text=str(hour), width=4)
self.mins.config(text=str(mins), width=4)
self.secs.config(text=str(secs), width=4)
self.ampm.config(text=str(ampm), width=4)
def onResize(self, newWidth, newHeight, cfg):
pass # nothing to redraw here
###############################################################################
Analog display object
###############################################################################
class AnalogDisplay(Canvas):
def init(self, parent, cfg):
Canvas.init(self, parent,
width=cfg.size, height=cfg.size, bg=cfg.bg)
self.drawClockface(cfg)
self.hourHand = self.minsHand = self.secsHand = self.cog = None
def drawClockface(self, cfg): # on start and resize
if cfg.picture: # draw ovals, picture
try:
self.image = PhotoImage(file=cfg.picture) # bkground
except:
self.image = BitmapImage(file=cfg.picture) # save ref
imgx = (cfg.size - self.image.width()) // 2 # center it
imgy = (cfg.size - self.image.height()) // 2 # 3.x // div
self.create_image(imgx+1, imgy+1, anchor=NW, image=self.image)
originX = originY = radius = cfg.size // 2 # 3.x // div
for i in range(60):
x, y = self.point(i, 60, radius-6, originX, originY)
self.create_rectangle(x-1, y-1, x+1, y+1, fill=cfg.fg) # mins
for i in range(12):
x, y = self.point(i, 12, radius-6, originX, originY)
self.create_rectangle(x-3, y-3, x+3, y+3, fill=cfg.fg) # hours
self.ampm = self.create_text(3, 3, anchor=NW, fill=cfg.fg)
def point(self, tick, units, radius, originX, originY):
angle = tick * (360.0 / units)
radiansPerDegree = math.pi / 180
pointX = int( round( radius * math.sin(angle * radiansPerDegree) ))
pointY = int( round( radius * math.cos(angle * radiansPerDegree) ))
return (pointX + originX+1), (originY+1 - pointY)
def onUpdate(self, hour, mins, secs, ampm, cfg): # on timer callback
if self.cog: # redraw hands, cog
self.delete(self.cog)
self.delete(self.hourHand)
self.delete(self.minsHand)
self.delete(self.secsHand)
originX = originY = radius = cfg.size // 2 # 3.x div
hour = hour + (mins / 60.0)
hx, hy = self.point(hour, 12, (radius * .80), originX, originY)
mx, my = self.point(mins, 60, (radius * .90), originX, originY)
sx, sy = self.point(secs, 60, (radius * .95), originX, originY)
self.hourHand = self.create_line(originX, originY, hx, hy,
width=(cfg.size * .04),
arrow='last', arrowshape=(25,25,15), fill=cfg.hh)
self.minsHand = self.create_line(originX, originY, mx, my,
width=(cfg.size * .03),
arrow='last', arrowshape=(20,20,10), fill=cfg.mh)
self.secsHand = self.create_line(originX, originY, sx, sy,
width=1,
arrow='last', arrowshape=(5,10,5), fill=cfg.sh)
cogsz = cfg.size * .01
self.cog = self.create_oval(originX-cogsz, originY+cogsz,
originX+cogsz, originY-cogsz, fill=cfg.cog)
self.dchars(self.ampm, 0, END)
self.insert(self.ampm, END, ampm)
def onResize(self, newWidth, newHeight, cfg):
newSize = min(newWidth, newHeight)
#print('analog onResize', cfg.size+4, newSize)
if newSize != cfg.size+4:
cfg.size = newSize-4
self.delete('all')
self.drawClockface(cfg) # onUpdate called next
###############################################################################
Clock composite object
###############################################################################
ChecksPerSec = 10 # second change timer
class Clock(Frame):
def init(self, config=ClockConfig, parent=None):
Frame.init(self, parent)
self.cfg = config
self.makeWidgets(parent) # children are packed but
self.labelOn = 0 # clients pack or grid me
self.display = self.digitalDisplay
self.lastSec = self.lastMin = -1
self.countdownSeconds = 0
self.onSwitchMode(None)
self.onTimer()
def makeWidgets(self, parent):
self.digitalDisplay = DigitalDisplay(self, self.cfg)
self.analogDisplay = AnalogDisplay(self, self.cfg)
self.dateLabel = Label(self, bd=3, bg='red', fg='blue')
parent.bind('<ButtonPress-1>', self.onSwitchMode)
parent.bind('<ButtonPress-3>', self.onToggleLabel)
parent.bind('<Configure>', self.onResize)
parent.bind('<KeyPress-s>', self.onCountdownSec)
parent.bind('<KeyPress-m>', self.onCountdownMin)
def onSwitchMode(self, event):
self.display.pack_forget()
if self.display == self.analogDisplay:
self.display = self.digitalDisplay
else:
self.display = self.analogDisplay
self.display.pack(side=TOP, expand=YES, fill=BOTH)
def onToggleLabel(self, event):
self.labelOn += 1
if self.labelOn % 2:
self.dateLabel.pack(side=BOTTOM, fill=X)
else:
self.dateLabel.pack_forget()
self.update()
def onResize(self, event):
if event.widget == self.display:
self.display.onResize(event.width, event.height, self.cfg)
def onTimer(self):
secsSinceEpoch = time.time()
timeTuple = time.localtime(secsSinceEpoch)
hour, min, sec = timeTuple[3:6]
if sec != self.lastSec:
self.lastSec = sec
ampm = ((hour >= 12) and 'PM') or 'AM' # 0...23
hour = (hour % 12) or 12 # 12..11
self.display.onUpdate(hour, min, sec, ampm, self.cfg)
self.dateLabel.config(text=time.ctime(secsSinceEpoch))
self.countdownSeconds -= 1
if self.countdownSeconds == 0:
self.onCountdownExpire() # countdown timer
self.after(1000 // ChecksPerSec, self.onTimer) # run N times per second
# 3.x // trunc int div
def onCountdownSec(self, event):
secs = askinteger('Countdown', 'Seconds?')
if secs: self.countdownSeconds = secs
def onCountdownMin(self, event):
secs = askinteger('Countdown', 'Minutes')
if secs: self.countdownSeconds = secs * 60
def onCountdownExpire(self):
# caveat: only one active, no progress indicator
win = Toplevel()
msg = Button(win, text='Timer Expired!', command=win.destroy)
msg.config(font=('courier', 80, 'normal'), fg='white', bg='navy')
msg.config(padx=10, pady=10)
msg.pack(expand=YES, fill=BOTH)
win.lift() # raise above siblings
if sys.platform[:3] == 'win': # full screen on Windows
win.state('zoomed')
###############################################################################
Standalone clocks
###############################################################################
appname = 'PyClock 2.1'
use new custom Tk, Toplevel for icons, etc.
from windows import PopupWindow, MainWindow
class ClockPopup(PopupWindow):
def init(self, config=ClockConfig, name=''):
PopupWindow.init(self, appname, name)
clock = Clock(config, self)
clock.pack(expand=YES, fill=BOTH)
class ClockMain(MainWindow):
def init(self, config=ClockConfig, name=''):
MainWindow.init(self, appname, name)
clock = Clock(config, self)
clock.pack(expand=YES, fill=BOTH)
b/w compat: manual window borders, passed-in parent
class ClockWindow(Clock):
def init(self, config=ClockConfig, parent=None, name=''):
Clock.init(self, config, parent)
self.pack(expand=YES, fill=BOTH)
title = appname
if name: title = appname + ' - ' + name
self.master.title(title) # master=parent or default
self.master.protocol('WM_DELETE_WINDOW', self.quit)
###############################################################################
Program run
###############################################################################
if name == 'main':
def getOptions(config, argv):
for attr in dir(ClockConfig): # fill default config obj,
try: # from "-attr val" cmd args
ix = argv.index('-' + attr) # will skip x internals
except:
continue
else:
if ix in range(1, len(argv)-1):
if type(getattr(ClockConfig, attr)) == int:
setattr(config, attr, int(argv[ix+1]))
else:
setattr(config, attr, argv[ix+1])
config = PhotoClockConfig()
config = ClockConfig()
if len(sys.argv) >= 2:
getOptions(config, sys.argv) # clock.py -size n -bg 'blue'...
myclock = ClockWindow(config, Tk()) # parent is Tk root if standalone
myclock = ClockPopup(ClockConfig(), 'popup')
myclock = ClockMain(config)
myclock.mainloop()