Обращайте особое внимание на комментарии, начинающиеся со слов «что сделать», где приводятся предложения по дальнейшему усовершенствованию. Подобно всем программным системам этот калькулятор может продолжать развиваться с течением времени (и фактически так и происходит с выходом каждого нового издания этой книги). Поскольку он написан на языке Python, подобные улучшения легко можно реализовать в будущем.
Пример 19.20. PP4E\Lang\Calculator\calculator.py
#!/usr/local/bin/python
############################################################################ PyCalc 3.0+: программа калькулятора и компонент графического интерфейса на Python/tkinter.
Вычисляет выражения по мере ввода, перехватывает нажатия клавиш на клавиатуре для ввода выражений; в версии 2.0 были добавлены диалоги для ввода произвольного программного кода, отображение истории вычислений, настройка шрифтов и цветов, вывод справочной информации о программе, предварительный импорт констант из модулей math/random и многое другое;
3.0+ (PP4E, номер версии сохранен):
— адаптирована для работы под управлением Python 3.X (только)
— убрана обработка клавиши ‘L‘ (тип long теперь отсутствует в языке)
3.0, изменения (PP3E):
— теперь для поля ввода вместо состояния ‘disabled‘ используется состояние ‘readonly‘, иначе оно окрашивается в серый цвет (исправлено в соответствии с изменениями в версии 2.3 библиотеки Tkinter);
— исключено расширенное отображение точности для чисел с плавающей точкой за счет использования str(), вместо ‘x‘/repr() (исправлено в соответствии с изменениями в Python);
— настраивается шрифт в поле ввода, чтобы текст в нем выглядел крупнее;
— используется флаг justify=right для поля ввода, чтобы обеспечить выравнивание по правому краю, а не по левому;
— добавлены кнопки ‘E+’ и ‘E-‘ (и обработка клавиши ‘E‘) для ввода чисел
в экспоненциальной форме; вслед за нажатием клавиши ‘E‘ вообще должен следовать ввод цифр, а не знака + или -;
— убрана кнопка ‘L‘ (но нажатие клавиши ‘L‘ все еще обрабатывается): теперь излишне, потому что Python автоматически преобразует числа, если они оказываются слишком большими (в прошлом кнопка ‘L‘ выполняла эту операцию принудительно);
— повсюду используются шрифты меньшего размера;
— автоматическая прокрутка в конец окна с историей вычислений
что сделать: добавить режим включения запятых (смотрите str.format и пример
в "Изучаем Python"); добавить поддержку оператора ‘**’; разрешить ввод ‘+’ и ‘J‘ для комплексных чисел; использовать новый тип Decimal для вещественных чисел с фиксированной точностью; сейчас для ввода и обработки комплексных чисел можно использовать диалог ‘cmd‘, но такая возможность отсутствует в главном окне; предупреждение: точность представления чисел и некоторые особенности поведения PyCalc в настоящее время обусловлены особенностями работы функции str();
############################################################################
from tkinter import * # виджеты, константы
from PP4E.Gui.Tools.guimixin import GuiMixin # метод quit
from PP4E.Gui.Tools.widgets import label, entry, button, frame # конструкторы # виджетов
Fg, Bg, Font = ‘black’, ‘skyblue’, (‘courier’, 14, ‘bold’) # настр. по умолч.
debugme = True def trace(*args):
if debugme: print(args)
############################################################################
— Основной класс — работает с интерфейсом пользователя; расширенный Frame
— в новом Toplevel или встроенный в другой элемент-контейнер
############################################################################
class CalcGui(GuiMixin, Frame):
Operators = "+-*/=" # списки кнопок
Operands = ["abcd", "0123", "4567", "89()"] # настраиваемые def __init__(self, parent=None, fg=Fg, bg=Bg, font=Font):
Frame.__init__(self, parent)
self.pack(expand=YES, fill=BOTH) # все элементы растягиваются self.eval = Evaluator() # встроить обработчик стека
self.text = StringVar() # создать связанную перемен.
self.text.set("0")
self.erase = 1 # затем убрать текст "0"
self.makeWidgets(fg, bg, font) # построить граф. интерфейс
if not parent or not isinstance(parent, Frame):
self.master.title(‘PyCalc 3.0’) # заголов., если владеет окном
self.master.iconname("PyCalc") # то же для привязки клавиш self.master.bind(‘<KeyPress>’, self.onKeyboard)
self.entry.config(state=’readonly’) # 3.0: не ‘disabled’=серый else:
self.entry.config(state=’normal’)
self.entry.focus()
def makeWidgets(self, fg, bg, font): # 7 фреймов плюс поле ввода
self.entry = entry(self, TOP, self.text) # шрифт, цвет настраиваемые self.entry.config(font=font) # 3.0: make display larger
self.entry.config(justify=RIGHT) # 3.0: справа, не слева
for row in self.Operands:
frm = frame(self, TOP) for char in row:
button(frm, LEFT, char,
lambda op=char: self.onOperand(op), fg=fg, bg=bg, font=font)
frm = frame(self, TOP)
for char in self.Operators:
button(frm, LEFT, char,
lambda op=char: self.onOperator(op), fg=bg, bg=fg, font=font)
frm = frame(self, TOP)
button(frm, LEFT, ‘dot ‘, lambda: self.onOperand(‘.’))
button(frm, LEFT, ‘ E+ ‘, lambda: self.text.set(self.text.get()+’E+’))
button(frm, LEFT, ‘ E- ‘, lambda: self.text.set(self.text.get()+’E-‘)) button(frm, LEFT, ‘cmd ‘, self.onMakeCmdline) button(frm, LEFT, ‘help’, self.help)
button(frm, LEFT, ‘quit’, self.quit) # из guimixin
frm = frame(self, BOTTOM)
button(frm, LEFT, ‘eval ‘, self.onEval)
button(frm, LEFT, ‘hist ‘, self.onHist)
button(frm, LEFT, ‘clear’, self.onClear)
def onClear(self):
self.eval.clear()
self.text.set(‘0’)
self.erase = 1
def onEval(self):
self.eval.shiftOpnd(self.text.get()) # посл. или единств. операнд
self.eval.closeall() # применить все оставш. операторы
self.text.set(self.eval.popOpnd()) # вытолкнуть: след. оператор? self.erase = 1
def onOperand(self, char):
if char == ‘(‘:
self.eval.open() self.text.set(‘(‘) # очистить текст далее
self.erase = 1
elif char == ‘)’:
self.eval.shiftOpnd(self.text.get()) # послед. или единств.
# вложенный операнд
self.eval.close() # вытолкнуть: след. оператор?
self.text.set(self.eval.popOpnd())
self.erase = 1
else:
if self.erase:
self.text.set(char) # очистить последнее значение
else:
self.text.set(self.text.get() + char) # иначе добавить
# в операнд
self.erase = 0
def onOperator(self, char):
self.eval.shiftOpnd(self.text.get()) #
self.eval.shiftOptr(char) #
self.text.set(self.eval.topOpnd()) #
#
self.erase = 1 #
# def onMakeCmdline(self):
new = Toplevel() #
new.title(‘PyCalc command line’) #
frm = frame(new, TOP) #
label(frm, LEFT, ‘>>>’).pack(expand=NO)
var = StringVar()
ent = entry(frm, LEFT, var, width=40) onButton = (lambda: self.onCmdline(var, onReturn = (lambda event: self.onCmdline(var, ent)) button(frm, RIGHT, ‘Run’, onButton).pack(expand=NO) ent.bind(‘<Return>’, onReturn)
var.set(self.text.get())
def onCmdline(self, var, ent): # выполняет команду в окне
try:
value = self.eval.runstring(var.get())
var.set(‘OKAY‘) # выполняет в eval
if value != None: # с пространством имен в словаре
self.text.set(value) # выражение или инструкция
self.erase = 1
var.set(‘OKAY => ‘+ value) except:
var.set(‘ERROR’)
ent.icursor(END)
ent.select_range(0, END)
def onKeyboard(self, event):
pressed = event.char
if pressed != »:
if pressed in self.Operators:
self.onOperator(pressed)
else:
for row in self.Operands:
if pressed in row:
self.onOperand(pressed)
break
else: # 4E: убрана клавиша ‘Ll’
if pressed == ‘.’:
self.onOperand(pressed) # может быть
# началом операнда
if pressed in ‘Ee’: # 2e10, без +/-
self.text.set(self.text.get()+pressed) # нет: не удал.
elif pressed == ‘\r’: self.onEval() # Enter=eval
elif pressed == ‘ ‘:
self.onClear() # пробел=очистить
elif pressed == ‘\b’:
self.text.set(self.text.get()[:-1]) # забой elif pressed == ‘?’:
self.help()
def onHist(self):
# выводит окно с историей вычислений
from tkinter.scrolledtext import ScrolledText # или PP4E.Gui.Tour new = Toplevel() #создать новое окно
ok = Button(new, text="OK", command=new.destroy)
ok.pack(pady=1, side=BOTTOM) # добавл. первым — усекается посл. text = ScrolledText(new, bg=’beige’) # добавить Text + полосу прокрут. text.insert(‘0.0’, self.eval.getHist()) # получить текст Evaluator text.see(END) # 3.0: прокрутить в конец
text.pack(expand=YES, fill=BOTH)
# новое окно закрывается нажатием кнопки ok или клавиши Enter new.title("PyCalc History")
new.bind("<Return>", (lambda event: new.destroy()))
ok.focus_set() # сделать новое окно модальным:
new.grab_set() # получить фокус ввода, захватить приложение
new.wait_window() # не вернется до вызова new.destroy
def help(self):
self.infobox(‘PyCalc’, ‘PyCalc 3.0+\n’
‘A Python/tkinter calculator\n’
‘Programming Python 4E\n’
‘May, 2010\n’
‘(3.0 2005, 2.0 1999, 1.0 1996)\n\n’
‘Use mouse or keyboard to\n’ ‘input numbers and operators,\n’ ‘or type code in cmd popup’)
############################################################################ # класс вычисления выражений встраивается в экземпляр CalcGui
# и используется им для вычисления выражений
############################################################################
class Evaluator:
def __init__(self):
self.names = {} # простр. имен для переменных
self.opnd, self.optr = [], [] # два пустых стека
self.hist = [] # журнал предыдущ. вычислений
self.runstring("from math import *") # предварит. импорт модулей
self.runstring("from random import *") # в простр. имен калькулятора
def clear(self):
self.opnd, self.optr = [], [] # оставить имена нетронутыми
if len(self.hist) > 64: # ограничить размер истории
self.hist = [‘clear’]
else:
self.hist.append(‘—clear—‘)
# вытолк./вернуть верх.|послед. операнд
# для отображ. и использования
# или x.pop(), или del x[-1]
def topOpnd(self)
# верхн. операнд (конец списка)
def open(self)
# трактовать ‘(‘ как оператор
def close(self): #
self.shiftOptr(‘)’) #
self.optr[-2:] = [] #
#
def closeall(self):
while self.optr: #
self.reduce() #
try:
self.opnd[0] = self.runstring(self.opnd[0])
except:
self.opnd[0] = ‘*ERROR*’ # вытолкнуть, иначе снова добавится
afterMe = {‘*’: [‘+’, ‘-‘, ‘(‘, ‘=’], # член класса
‘/’: [‘+’, ‘-‘, ‘(‘, ‘=’], # не выталкивать операторы для клав.
‘+’: [‘(‘, ‘=’], # если это предыдущ. оператор: push
‘-‘: [‘(‘, ‘=’], # иначе: pop/eval предыдущ. оператор
‘)’: [‘(‘, ‘=’], # все левоассоциативные ‘=’: [‘(‘] }
def shiftOpnd(self, newopnd): # втолкнуть операнд для оператора,
self.opnd.append(newopnd) # ‘)’, eval
def shiftOptr(self, newoptr): # применить операторы с приорит. <=
while (self.optr and
self.optr[-1] not in self.afterMe[newoptr]):
self.reduce()
self.optr.append(newoptr) # втолкнуть этот оператор над результатом
# операторы предполаг. стирание след. операндом def reduce(self):
trace(self.optr, self.opnd)
try: # свернуть верхнее выражение
operator = self.optr[-1] # вытолк. верх. оператор (в конце)
[left, right] = self.opnd[-2:] # вытолк. 2 верх.
# операнда (в конце)
self.optr[-1:] = [] # удалить срез на месте
self.opnd[-2:] = []
result = self.runstring(left + operator + right) if result == None:
result = left # присваивание? клавиша имени перем.
self.opnd.append(result) # втолкнуть строку результ. обратно except:
self.opnd.append(‘*ERROR*’) # ошибка стека/числа/имени
def runstring(self, code):
try: # 3.0: not ‘x’/repr
result = str(eval(code, self.names, self.names)) # вычислить
self.hist.append(code + ‘ => ‘ + result) # добавить в журнал except:
exec(code, self.names, self.names) # инструкция: None
self.hist.append(code) result = None
return result
def getHist(self):
return ‘\n’.join(self.hist)
def getCalcArgs():
from sys import argv # получить арг. команд. строки в словаре
config = {} # пример: -bg black -fg red
for arg in argv[1:]: # шрифт пока не поддерживается
if arg in [‘-bg’, ‘-fg’]: # -bg red’ -> {‘bg’:’red’}
try:
config[arg[1:]] = argv[argv.index(arg) + 1]
except:
pass
return config
if __name__ == ‘__main__’:
CalcGui(**getCalcArgs()).mainloop() # по умолчанию окно верхнего уровня
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011