Вся реализация PyClock находится в одном файле, за исключением предварительно подготовленных объектов с настройками стилей. Если посмотреть в конец примера 11.12, можно заметить, что объект часов можно создать, либо передав конструктору объект с настройками, либо определив параметры настройки в аргументах командной строки, как показано ниже (в этом случае сценарий просто сам создаст объект с настройками):
C:\…\PP4E\Gui\Clock> clock.py -bg gold -sh brown -size 300
Вообще говоря, для запуска часов этот файл можно выполнить непосредственно, с аргументами или без; импортировать его и создать объекты, используя объекты с настройками, чтобы часы выглядели более индивидуально; или импортировать и прикрепить его объекты к другим графическим интерфейсам. Например, PyGadgets из главы 10 запускает этот файл с параметрами командной строки, управляющими внешним видом часов.
Пример 11.12. PP4E\Gui\Clock\clock.py
############################################################################## PyClock 2.1: часы с графическим интерфейсом на Python/tkinter.
В обоих режимах отображения, аналоговом и цифровом, могут выводить метку с датой, графические изображения на циферблате, изменять размеры и так далее. Могут запускаться автономно или встраиваться (прикрепляться) в другие графические интерфейсы, где требуется вывести текущее время.
Новое в версии 2.0: клавиши s/m устанавливают таймер, отсчитывающий секунды/ минуты перед выводом всплывающего сообщения; значок окна.
Новое в версии 2.1: добавлена возможность выполнения под управлением Python 3.X (2.X больше не поддерживается)
##############################################################################
from tkinter import *
from tkinter.simpledialog import askinteger import math, time, sys
############################################################################## # Классы параметров настройки
##############################################################################
class ClockConfig:
# умолчания — переопределите в экземпляре или в подклассе
size = 200 # ширина=высота
bg, fg = ‘beige’, ‘brown’ # цвет циферблата, рисок
hh, mh, sh, cog = ‘black’, ‘navy’, ‘blue’, ‘red’ # стрелок, центра picture = None # файл картинки
class PhotoClockConfig(ClockConfig):
# пример комплекта настроек
size = 320
picture = ‘../gifs/ora-pp.gif’
bg, hh, mh = ‘white’, ‘blue’, ‘orange’
############################################################################## # Объект цифрового интерфейса
##############################################################################
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: при изменении размеров можно было бы # изменять размер шрифта
def onUpdate(self, hour, mins, secs, ampm, cfg):
mins = str(mins).zfill(2) # или ‘%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 # здесь ничего перерисовывать не требуется
############################################################################## # Объект аналогового интерфейса
##############################################################################
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): # при запуске и изменении размеров
if cfg.picture: # рисует овалы, картинку
try:
self.image = PhotoImage(file=cfg.picture) # фон
except:
self.image = BitmapImage(file=cfg.picture) # сохранить ссылку imgx = (cfg.size — self.image.width()) // 2 # центрировать
imgy = (cfg.size — self.image.height()) // 2 # 3.x деление //
self.create_image(imgx+1, imgy+1, anchor=NW, image=self.image)
originX = originY = radius = cfg.size // 2 # 3.x деление //
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) # минуты 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) # часы
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): # вызывается из
if self.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 деление // |
hour = hx, hy |
hour + (mins / 60.0) = 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
############################################################################## # Составной объект часов
##############################################################################
ChecksPerSec = 10 # частота проверки системного времени
class Clock(Frame):
def __init__(self, config=ClockConfig, parent=None):
Frame.__init__(self, parent)
self.cfg = config
self.makeWidgets(parent) # дочерние виджеты компонуются методом pack,
self.labelOn = 0 # но клиенты могут использовать pack или grid
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() # таймер обратного отсчета
self.after(1000 // ChecksPerSec, self.onTimer) # вызывать N раз в сек.
# 3.x // целочисленное # деление с усечением 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):
# ВНИМАНИЕ: только один активный таймер,
# текущее состояние таймера не отображается
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() # поднять над другими окнами
if sys.platform[:3] == ‘win’: # в Windows — на полный экран win.state(‘zoomed’)
############################################################################## # Автономные часы
##############################################################################
appname = ‘PyClock 2.1’
# использовать новые окна Tk, Toplevel со своими значками и так далее from PP4E.Gui.Tools.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)
# для обратной совместимости: рамки окна устанавливаются вручную, # передается родитель
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) # владелец=parent или окно по умолчанию self.master.protocol(‘WM_DELETE_WINDOW’, self.quit)
##############################################################################
# Запуск программы
##############################################################################
if __name__ == ‘__main__’:
def getOptions(config, argv):
for attr in dir(ClockConfig): # заполнить объект с настройками
try: # из арг. ком. строки “-attr val”
ix = argv.index(‘-’ + attr) # пропустит внутр. __x__ 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()) # при автономном выполнении
#myclock = ClockPopup(ClockConfig(), ‘popup’) # родителем является корневое myclock = ClockMain(config) # окно Tk
myclock.mainloop()
И наконец, в примере 11.13 приводится модуль, выполняемый сценарием PyDemos, — в нем определяется несколько стилей часов и производится запуск одновременно семи экземпляров часов, прикрепляемых к новым окнам верхнего уровня для создания демонстрационного эффекта (хотя на практике обычно достаточно иметь на экране одни часы, даже мне!).
Пример 11.13. PP4E\Gui\Clock\clockStyles.py
# предопределенные стили часов
from clock import *
from tkinter import mainloop
gifdir = ‘../gifs/’
if __name__ == ‘__main__’: from sys import argv if len(argv) > 1:
gifdir = argv[1] + ‘/’
class PPClockBig(PhotoClockConfig):
picture, bg, fg = gifdir + ‘ora-pp.gif’, ‘navy’, ‘green’
class PPClockSmall(ClockConfig):
size = 175
picture = gifdir + ‘ora-pp.gif’
bg, fg, hh, mh = ‘white’, ‘red’, ‘blue’, ‘orange’
class GilliganClock(ClockConfig):
size = 550
picture = gifdir + ‘gilligan.gif’
bg, fg, hh, mh = ‘black’, ‘white’, ‘green’, ‘yellow’
class LP4EClock(GilliganClock):
size = 700
picture = gifdir + ‘ora-lp4e.gif’ bg = ‘navy’
class LP4EClockSmall(LP4EClock):
size, fg = 350, ‘orange’
class Pyref4EClock(ClockConfig):
size, picture = 400, gifdir + ‘ora-pyref4e.gif’
bg, fg, hh = ‘black’, ‘gold’, ‘brown’
class GreyClock(ClockConfig):
bg, fg, hh, mh, sh = ‘grey’, ‘black’, ‘black’, ‘black’, ‘white’
class PinkClock(ClockConfig):
bg, fg, hh, mh, sh = ‘pink’, ‘yellow’, ‘purple’, ‘orange’, ‘yellow’
class PythonPoweredClock(ClockConfig):
bg, size, picture = ‘white’, 175, gifdir + ‘pythonPowered.gif’
if __name__ == ‘__main__’:
root = Tk()
for configClass in [
ClockConfig,
PPClockBig,
#PPClockSmall,
LP4EClockSmall,
#GilliganClock,
Pyref4EClock,
GreyClock,
PinkClock,
PythonPoweredClock
]:
ClockPopup(configClass, configClass.__name__)
Button(root, text=’Quit Clocks’, command=root.quit).pack() root.mainloop()
При запуске этот сценарий создает множество часов различного вида, как показано на рис. 11.24. Объекты конфигурации поддерживают большое число параметров. Судя по семи парам часов, отображаемых на экране, пришло время перейти к последнему примеру.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011