| 1 |
ninoborges |
8 |
###############################################################################
|
| 2 |
|
|
# PyClock: a clock GUI, with both analog and digital display modes, a
|
| 3 |
|
|
# popup date label, clock face images, resizing, etc. May be run both
|
| 4 |
|
|
# stand-alone, or embbeded (attached) in other GUIs that need a clock.
|
| 5 |
|
|
###############################################################################
|
| 6 |
|
|
|
| 7 |
|
|
from Tkinter import *
|
| 8 |
|
|
import math, time, string
|
| 9 |
|
|
|
| 10 |
|
|
|
| 11 |
|
|
###############################################################################
|
| 12 |
|
|
# Option configuration classes
|
| 13 |
|
|
###############################################################################
|
| 14 |
|
|
|
| 15 |
|
|
|
| 16 |
|
|
class ClockConfig:
|
| 17 |
|
|
# defaults--override in instance or subclass
|
| 18 |
|
|
size = 200 # width=height
|
| 19 |
|
|
bg, fg = 'beige', 'brown' # face, tick colors
|
| 20 |
|
|
hh, mh, sh, cog = 'black', 'navy', 'blue', 'red' # clock hands, center
|
| 21 |
|
|
picture = None # face photo file
|
| 22 |
|
|
|
| 23 |
|
|
class PhotoClockConfig(ClockConfig):
|
| 24 |
|
|
# sample configuration
|
| 25 |
|
|
size = 320
|
| 26 |
|
|
picture = 'gifs/ora-pp.gif'
|
| 27 |
|
|
bg, hh, mh = 'white', 'blue', 'orange'
|
| 28 |
|
|
|
| 29 |
|
|
|
| 30 |
|
|
###############################################################################
|
| 31 |
|
|
# Digital display object
|
| 32 |
|
|
###############################################################################
|
| 33 |
|
|
|
| 34 |
|
|
class DigitalDisplay(Frame):
|
| 35 |
|
|
def __init__(self, parent, cfg):
|
| 36 |
|
|
Frame.__init__(self, parent)
|
| 37 |
|
|
self.hour = Label(self)
|
| 38 |
|
|
self.mins = Label(self)
|
| 39 |
|
|
self.secs = Label(self)
|
| 40 |
|
|
self.ampm = Label(self)
|
| 41 |
|
|
for label in self.hour, self.mins, self.secs, self.ampm:
|
| 42 |
|
|
label.config(bd=4, relief=SUNKEN, bg=cfg.bg, fg=cfg.fg)
|
| 43 |
|
|
label.pack(side=LEFT)
|
| 44 |
|
|
|
| 45 |
|
|
def onUpdate(self, hour, mins, secs, ampm, cfg):
|
| 46 |
|
|
mins = string.zfill(str(mins), 2)
|
| 47 |
|
|
self.hour.config(text=str(hour), width=4)
|
| 48 |
|
|
self.mins.config(text=str(mins), width=4)
|
| 49 |
|
|
self.secs.config(text=str(secs), width=4)
|
| 50 |
|
|
self.ampm.config(text=str(ampm), width=4)
|
| 51 |
|
|
|
| 52 |
|
|
def onResize(self, newWidth, newHeight, cfg):
|
| 53 |
|
|
pass # nothing to redraw here
|
| 54 |
|
|
|
| 55 |
|
|
|
| 56 |
|
|
###############################################################################
|
| 57 |
|
|
# Analog display object
|
| 58 |
|
|
###############################################################################
|
| 59 |
|
|
|
| 60 |
|
|
class AnalogDisplay(Canvas):
|
| 61 |
|
|
def __init__(self, parent, cfg):
|
| 62 |
|
|
Canvas.__init__(self, parent,
|
| 63 |
|
|
width=cfg.size, height=cfg.size, bg=cfg.bg)
|
| 64 |
|
|
self.drawClockface(cfg)
|
| 65 |
|
|
self.hourHand = self.minsHand = self.secsHand = self.cog = None
|
| 66 |
|
|
|
| 67 |
|
|
def drawClockface(self, cfg): # on start and resize
|
| 68 |
|
|
if cfg.picture: # draw ovals, picture
|
| 69 |
|
|
try:
|
| 70 |
|
|
self.image = PhotoImage(file=cfg.picture) # bkground
|
| 71 |
|
|
except:
|
| 72 |
|
|
self.image = BitmapImage(file=cfg.picture) # save ref
|
| 73 |
|
|
imgx = (cfg.size - self.image.width( )) / 2 # center it
|
| 74 |
|
|
imgy = (cfg.size - self.image.height( )) / 2
|
| 75 |
|
|
self.create_image(imgx+1, imgy+1, anchor=NW, image=self.image)
|
| 76 |
|
|
originX = originY = radius = cfg.size/2
|
| 77 |
|
|
for i in range(60):
|
| 78 |
|
|
x, y = self.point(i, 60, radius-6, originX, originY)
|
| 79 |
|
|
self.create_rectangle(x-1, y-1, x+1, y+1, fill=cfg.fg) # mins
|
| 80 |
|
|
for i in range(12):
|
| 81 |
|
|
x, y = self.point(i, 12, radius-6, originX, originY)
|
| 82 |
|
|
self.create_rectangle(x-3, y-3, x+3, y+3, fill=cfg.fg) # hours
|
| 83 |
|
|
self.ampm = self.create_text(3, 3, anchor=NW, fill=cfg.fg)
|
| 84 |
|
|
|
| 85 |
|
|
def point(self, tick, units, radius, originX, originY):
|
| 86 |
|
|
angle = tick * (360.0 / units)
|
| 87 |
|
|
radiansPerDegree = math.pi / 180
|
| 88 |
|
|
pointX = int( round( radius * math.sin(angle * radiansPerDegree) ))
|
| 89 |
|
|
pointY = int( round( radius * math.cos(angle * radiansPerDegree) ))
|
| 90 |
|
|
return (pointX + originX+1), (originY+1 - pointY)
|
| 91 |
|
|
|
| 92 |
|
|
def onUpdate(self, hour, mins, secs, ampm, cfg): # on timer callback
|
| 93 |
|
|
if self.cog: # redraw hands, cog
|
| 94 |
|
|
self.delete(self.cog)
|
| 95 |
|
|
self.delete(self.hourHand)
|
| 96 |
|
|
self.delete(self.minsHand)
|
| 97 |
|
|
self.delete(self.secsHand)
|
| 98 |
|
|
originX = originY = radius = cfg.size/2
|
| 99 |
|
|
hour = hour + (mins / 60.0)
|
| 100 |
|
|
hx, hy = self.point(hour, 12, (radius * .80), originX, originY)
|
| 101 |
|
|
mx, my = self.point(mins, 60, (radius * .90), originX, originY)
|
| 102 |
|
|
sx, sy = self.point(secs, 60, (radius * .95), originX, originY)
|
| 103 |
|
|
self.hourHand = self.create_line(originX, originY, hx, hy,
|
| 104 |
|
|
width=(cfg.size * .04),
|
| 105 |
|
|
arrow='last', arrowshape=(25,25,15), fill=cfg.hh)
|
| 106 |
|
|
self.minsHand = self.create_line(originX, originY, mx, my,
|
| 107 |
|
|
width=(cfg.size * .03),
|
| 108 |
|
|
arrow='last', arrowshape=(20,20,10), fill=cfg.mh)
|
| 109 |
|
|
self.secsHand = self.create_line(originX, originY, sx, sy,
|
| 110 |
|
|
width=1,
|
| 111 |
|
|
arrow='last', arrowshape=(5,10,5), fill=cfg.sh)
|
| 112 |
|
|
cogsz = cfg.size * .01
|
| 113 |
|
|
self.cog = self.create_oval(originX-cogsz, originY+cogsz,
|
| 114 |
|
|
originX+cogsz, originY-cogsz, fill=cfg.cog)
|
| 115 |
|
|
self.dchars(self.ampm, 0, END)
|
| 116 |
|
|
self.insert(self.ampm, END, ampm)
|
| 117 |
|
|
|
| 118 |
|
|
def onResize(self, newWidth, newHeight, cfg):
|
| 119 |
|
|
newSize = min(newWidth, newHeight)
|
| 120 |
|
|
#print 'analog onResize', cfg.size+4, newSize
|
| 121 |
|
|
if newSize != cfg.size+4:
|
| 122 |
|
|
cfg.size = newSize-4
|
| 123 |
|
|
self.delete('all')
|
| 124 |
|
|
self.drawClockface(cfg) # onUpdate called next
|
| 125 |
|
|
|
| 126 |
|
|
|
| 127 |
|
|
###############################################################################
|
| 128 |
|
|
# Clock composite object
|
| 129 |
|
|
###############################################################################
|
| 130 |
|
|
|
| 131 |
|
|
ChecksPerSec = 10 # second change timer
|
| 132 |
|
|
|
| 133 |
|
|
class Clock(Frame):
|
| 134 |
|
|
def __init__(self, config=ClockConfig, parent=None):
|
| 135 |
|
|
Frame.__init__(self, parent)
|
| 136 |
|
|
self.cfg = config
|
| 137 |
|
|
self.makeWidgets(parent) # children are packed but
|
| 138 |
|
|
self.labelOn = 0 # clients pack or grid me
|
| 139 |
|
|
self.display = self.digitalDisplay
|
| 140 |
|
|
self.lastSec = -1
|
| 141 |
|
|
self.onSwitchMode(None)
|
| 142 |
|
|
self.onTimer( )
|
| 143 |
|
|
|
| 144 |
|
|
def makeWidgets(self, parent):
|
| 145 |
|
|
self.digitalDisplay = DigitalDisplay(self, self.cfg)
|
| 146 |
|
|
self.analogDisplay = AnalogDisplay(self, self.cfg)
|
| 147 |
|
|
self.dateLabel = Label(self, bd=3, bg='red', fg='blue')
|
| 148 |
|
|
parent.bind('<ButtonPress-1>', self.onSwitchMode)
|
| 149 |
|
|
parent.bind('<ButtonPress-3>', self.onToggleLabel)
|
| 150 |
|
|
parent.bind('<Configure>', self.onResize)
|
| 151 |
|
|
|
| 152 |
|
|
def onSwitchMode(self, event):
|
| 153 |
|
|
self.display.pack_forget( )
|
| 154 |
|
|
if self.display == self.analogDisplay:
|
| 155 |
|
|
self.display = self.digitalDisplay
|
| 156 |
|
|
else:
|
| 157 |
|
|
self.display = self.analogDisplay
|
| 158 |
|
|
self.display.pack(side=TOP, expand=YES, fill=BOTH)
|
| 159 |
|
|
|
| 160 |
|
|
def onToggleLabel(self, event):
|
| 161 |
|
|
self.labelOn = self.labelOn + 1
|
| 162 |
|
|
if self.labelOn % 2:
|
| 163 |
|
|
self.dateLabel.pack(side=BOTTOM, fill=X)
|
| 164 |
|
|
else:
|
| 165 |
|
|
self.dateLabel.pack_forget( )
|
| 166 |
|
|
self.update( )
|
| 167 |
|
|
|
| 168 |
|
|
def onResize(self, event):
|
| 169 |
|
|
if event.widget == self.display:
|
| 170 |
|
|
self.display.onResize(event.width, event.height, self.cfg)
|
| 171 |
|
|
|
| 172 |
|
|
def onTimer(self):
|
| 173 |
|
|
secsSinceEpoch = time.time( )
|
| 174 |
|
|
timeTuple = time.localtime(secsSinceEpoch)
|
| 175 |
|
|
hour, min, sec = timeTuple[3:6]
|
| 176 |
|
|
if sec != self.lastSec:
|
| 177 |
|
|
self.lastSec = sec
|
| 178 |
|
|
ampm = ((hour >= 12) and 'PM') or 'AM' # 0...23
|
| 179 |
|
|
hour = (hour % 12) or 12 # 12..11
|
| 180 |
|
|
self.display.onUpdate(hour, min, sec, ampm, self.cfg)
|
| 181 |
|
|
self.dateLabel.config(text=time.ctime(secsSinceEpoch))
|
| 182 |
|
|
self.after(1000 / ChecksPerSec, self.onTimer) # run N times per second
|
| 183 |
|
|
|
| 184 |
|
|
|
| 185 |
|
|
###############################################################################
|
| 186 |
|
|
# Stand-alone clocks
|
| 187 |
|
|
###############################################################################
|
| 188 |
|
|
|
| 189 |
|
|
class ClockWindow(Clock):
|
| 190 |
|
|
def __init__(self, config=ClockConfig, parent=None, name=''):
|
| 191 |
|
|
Clock.__init__(self, config, parent)
|
| 192 |
|
|
self.pack(expand=YES, fill=BOTH)
|
| 193 |
|
|
title = 'PyClock 1.0'
|
| 194 |
|
|
if name: title = title + ' - ' + name
|
| 195 |
|
|
self.master.title(title) # master=parent or default
|
| 196 |
|
|
self.master.protocol('WM_DELETE_WINDOW', self.quit)
|
| 197 |
|
|
|
| 198 |
|
|
|
| 199 |
|
|
###############################################################################
|
| 200 |
|
|
# Program run
|
| 201 |
|
|
###############################################################################
|
| 202 |
|
|
|
| 203 |
|
|
if __name__ == '__main__':
|
| 204 |
|
|
def getOptions(config, argv):
|
| 205 |
|
|
for attr in dir(ClockConfig): # fill default config obj,
|
| 206 |
|
|
try: # from "-attr val" cmd args
|
| 207 |
|
|
ix = argv.index('-' + attr)
|
| 208 |
|
|
except:
|
| 209 |
|
|
continue
|
| 210 |
|
|
else:
|
| 211 |
|
|
if ix in range(1, len(argv)-1):
|
| 212 |
|
|
if type(getattr(ClockConfig, attr)) == type(0):
|
| 213 |
|
|
setattr(config, attr, int(argv[ix+1]))
|
| 214 |
|
|
else:
|
| 215 |
|
|
setattr(config, attr, argv[ix+1])
|
| 216 |
|
|
import sys
|
| 217 |
|
|
config = ClockConfig( )
|
| 218 |
|
|
#config = PhotoClockConfig( )
|
| 219 |
|
|
if len(sys.argv) >= 2:
|
| 220 |
|
|
getOptions(config, sys.argv) # clock.py -size n -bg 'blue'...
|
| 221 |
|
|
myclock = ClockWindow(config, Tk( )) # parent is Tk root if standalone
|
| 222 |
|
|
myclock.mainloop( ) |