1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
| #!/usr/bin/env python
#-*- coding: utf-8 -*-
#
# pyCalc mini.py <version 0.1>
#
# Copyright 2016 benbout <benbout62@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# FRENCH :
# pyCalc mini est une calculatrice que j'ai voulu développer
# pour tester mes quelques connaissances en Python et plus géné
# -ralement en POO dont je suis encore néophyte.
#
# pyCalc mini utilise Tkinter pour l'interface graphique. Le choix de
# Tkinter est purement personnel et ne constitue pas un choix technique.
#
# pyCalc mini est construit autour d'un patron MVC Observer, ce choix
# est lui aussi purement personnel, pédagogique et non technique.Il est
# sur que le choix d'un autre patron pour ce type de programme pourrait
# etre largement plus optimisé, je doute qu'un MVC observer soit
# le meilleur choix pour l'élaboration d'un tel programme. En fait,
# je ne connais que ce patron je suis en train d'en étudier plusieurs,
# et j'ai voulu tester celui ci pour l'étudier.
# Néanmoins, meme si cette construction n'offre pas le
# programme le plus rapide, je trouve qu'il s'insere assez bien dans
# la modélisation que j'avais faites en amont.
#
# Le type Observable (la classe définie dans ce code source) a été
# écrite par Brian Kelley. J'avais la flemme de faire le mien vu qu'au
# final cela aurait été quasiment la meme chose.
# Les maquettes de patterns de Brian Kelley sont disponibles à cette
# Adresse : (edit j ai oublié ! désolé (bon en fait j'ai la flemme,
# cherchez sur github.))
#
# La vue est séparée en plusieurs classes pour plus de clarté.
# Aucune de ces classes n'est parente ou fille d'une autre.
#
# A aucun moment le model n'entre en communication directe avec la vue.
#
#
# Cette premiere version a été testée sous debian, je ne garantie rien
# en ce qui concerne windows (notamment sur l'affichage) et à ceux qui
# ont des claviers Qwerty (bien que j'ai fait l'effort d'utiliser
# des keysym pour les bind donc a priori ca devrait passer)
#
# Ca vous botte d'approfondir les fonctionnalités de cette calculette ?
# Libre à vous, à mon avis il serait utile de créer votre propre
# Analyseur pour remplacer la fonction buil-in eval() et de créer
# Vos propres opérateurs.
# Vous pouvez aussi améliorer le confort visuel par exemple. Pour
# le moment quand vous cliquez sur des touches de votre pavé
# numerique, vous pouvez ecrire vos opérations dans la calculette mais
# il n y a pas d'effets de boutons pressés automatiquement en fonction
# du chiffre tappé etc.
#
# - Benjamin Boutoille dit BenBout, membre de la com
# -munauté Développez.net
import tkinter as tk
from tkinter import StringVar
class Observable:
""" Objets relationnels pour le model et le controller, les objets
de l'observable stockent et restituent les données
qu'ils contiennent. Dans ce programme, chaque
objet de l'observable est lié à un seul observeur mais les
objets sont concus pour etre observés par plusieurs autres
objets si nécessaire (voir les fonctions membres add_callback
et _docallbacks).
"""
def __init__(self, initialValue=None):
self.data = initialValue
self.callbacks = {}
def add_callback(self, func):
self.callbacks[func] = 1
def del_callback(self, func):
del self.callback[func]
def _docallbacks(self):
for func in self.callbacks:
func(self.data)
def set(self, data):
self.data = data
self._docallbacks()
def get(self):
return self.data
def unset(self):
self.data = None
class Model:
""" Contient la plupart des opérations complexes. Le model
est en lien direct avec le controller. Certaines décisions
du controller ne nécessitant pas d'opérations manipulant
des données en transit sont directement effectuées dans la
partie controller et n'apparaissent pas ici.
Plusieurs variables du constructeur sont des référence
d'objets de l'Observable, pour assurer les requetes vers l'
observable depuis le model.
"""
def __init__(self):
self.op_line = Observable("")
self.result_line = Observable("0")
#*le result status est un booléen pour savoir si l'utilisateur
# vient de faire afficher un resutat. C'est utile par exemple
# si l'on clique sur un opérateur après avoir affiché un
# resultat, ainsi on check ce booléen et s'il est true alors
# on prend l affichage du resultat et on l'integre à la ligne
# d'opération suivi de l'opérateur qui vient d'etre tapé. voir
# fonction membre : add_operator.
self.result_status = False
def add_number(self, number):
""" Appelé par le controller.
Simple opération d'ajout du nombre dans la ligne
d'opération.
"""
if self.is_result_just_show():
self.reset_result_status()
string = self.op_line.get()
string += number
self.op_line.unset()
self.op_line.set(string)
def add_operator(self, operator):
""" Appelé par le controller.
Dans le premier cas l'utilisateur a cliqué
sur un opérateur à la suite d'un resultat affiché.
Quand c'est le cas, certaines calculettes affichent ANS
dans la ligne d'opération en cours. Pour ma part je
souhaite que le resultat soit affiché dans la ligne d'opé
-ration en cours et non sous un alias du nom de ANS.
Dans le deuxieme cas un resultat n a pas été fraichement
affiché, on ajoute simplement l'opérateur à la suite
de l'opération stockée (op_line).
"""
if self.is_result_just_show():
string = self.result_line.get()
self.reset_result_status()
string += operator
self.op_line.set(string)
self.result_line.unset()
self.result_line.set("0")
else:
string = self.op_line.get()
string += operator
self.op_line.unset()
self.op_line.set(string)
def op_line_backward(self):
""" Appelé par le controller pour effacer le dernier
caractère stocké dans la ligne d'opération.
"""
string = self.op_line.get()
string = string[0:(len(string)-1)] #optimisable ? surement.
self.op_line.unset()
self.op_line.set(string)
def clear_op_line(self):
""" Appelé par le controller pour effacer la ligne d'opération
et reinitialiser la ligne de resultat.
"""
self.op_line.unset()
self.result_line.unset()
self.op_line.set("")
self.result_line.set("0")
def is_result_just_show(self):
""" Appelé par une autre fonction membre du Model.
Retourne 1 si un resultat a fraichement été affiché.
Retourne 0 autrement.
Retourne un entier, retourner un type booléen est mieux
peut etre ? logiquement oui, niveau optimisation
technique je ne sais pas.
"""
if self.result_status == True:
return 1
return 0
def reset_result_status(self):
self.result_status = 0
def get_op_result(self):
""" Appelé par le controller pour traiter le calcul de
la ligne d'opération en cours.
Le calcul de la ligne d'opération utilise la fonction
d'analyse eval().
"""
operation = self.op_line.get()
if operation:
try:
# pourquoi je force la conversion de l'eval en float ?
# la méthode is_integer() ne s'applique qu'à un
# type float.
result = float(eval(operation))
self.result_line.unset()
if result.is_integer():
self.result_line.set(str(int(result)))
else:
self.result_line.set(str(result))
self.result_status = True
except SyntaxError:
self.result_line.unset()
self.result_line.set("SYNTAX ERROR")
else:
self.result_line.unset()
self.result_line.set("0")
class NavBar(tk.Menu):
""" Contient les widgets en relation avec la barre de menu et
la barre de menu elle meme.
"""
def __init__(self, root):
tk.Menu.__init__(self, root)
self.menu = tk.Menu(self, tearoff=0,relief="flat")
self.add_cascade(label="Menu",
menu=self.menu)
class Screener(tk.Frame):
""" Partie de la vue qui comprend l'affichage des
operations et du resultat. Les deux lignes utilisent
deux widgets label, j'ai tenté de le faire en tk.Text
mais le resultat s'est avéré décevant (manque de soup
-lesse. Deux méthodes en plus du constructeur, rien de
spécial, elles servent au raffraichissement des informa
-tions (voir controller)
"""
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.config(bg="lightblue")
self.v = StringVar()
self.y = StringVar()
self.op_screen = tk.Label(self,
textvariable=self.v,
justify="left",
bg="lightblue",
width=25,
height=2,
anchor="e",
font=("Verdana",8),
fg="gray")
self.result_screen = tk.Label(self,
textvariable =self.y,
justify="right",
bg="white",
width=25,
height=2,
anchor="e",
font=("Helvetica", 14),
highlightthickness=1)
self.op_screen.pack(side="top")
self.result_screen.pack(side="top")
def refresh_op_line(self, string):
self.v.set(string)
def refresh_result_line(self, string):
self.y.set(string)
class Keypad(tk.Frame):
""" Partie de la vue qui comprend les boutons de la
calculette. Contient la construction initiale et
la disposition sur la grille. Plusieurs dictionnaires
contiennent des configurations dites communes suivant le type
de bouton.
"""
# common key conf
ck_conf = {
"width" : 4,
"relief" : "flat",
"background" : "beige"
}
# special key conf
sp_conf = {
"width" : 4,
"highlightbackground" : "gray78",
"relief" : "flat",
"background" : "light gray"
}
# common .grid conf
cg_conf = {
"sticky" : ("w","e", "n", "s"),
"padx" : 3,
"pady" : 2
}
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.config(bg="lightblue")
# Num keys
self.num_key = tuple(
tk.Button(self, **self.ck_conf) for x in range(0,10))
self.num_key[0].config(height=2) #Reconfiguration de key 0
# le float est considéré comme partie d'un nombre dans ce
# programme et non comme opérateur. Voir les fonctions du
# model pour comprendre pourquoi.
self.float_key = tk.Button(self, text=".",
height=2, **self.sp_conf)
# Op keys
self.op_add_key = tk.Button(self, text="+", height=2, **self.sp_conf)
self.op_div_key = tk.Button(self, text="/", **self.sp_conf)
self.op_mult_key = tk.Button(self, text="*", **self.sp_conf)
self.op_sous_key = tk.Button(self, text="-", **self.sp_conf)
# Res key
self.op_res_key = tk.Button(self, text="=", **self.ck_conf)
# Cancel Keys
self.op_cancel_key = tk.Button(self, text="C",
height=2, **self.sp_conf)
self.op_cancelall_key = tk.Button(self, text="CE",
height=2, **self.sp_conf)
# Paren keys
self.parenleft_key = tk.Button(self, text="(",
height=2, **self.sp_conf)
self.parenright_key = tk.Button(self, text=")",
height=2, **self.sp_conf)
#Display all keys
self.num_key[0].grid(row=5, column=2, **self.cg_conf)
self.num_key[1].grid(row=4, column=1, **self.cg_conf)
self.num_key[2].grid(row=4, column=2, **self.cg_conf)
self.num_key[3].grid(row=4, column=3, **self.cg_conf)
self.num_key[4].grid(row=3, column=1, **self.cg_conf)
self.num_key[5].grid(row=3, column=2, **self.cg_conf)
self.num_key[6].grid(row=3, column=3, **self.cg_conf)
self.num_key[7].grid(row=2, column=1, **self.cg_conf)
self.num_key[8].grid(row=2, column=2, **self.cg_conf)
self.num_key[9].grid(row=2, column=3, **self.cg_conf)
self.float_key.grid(row=5, column=1, **self.cg_conf)
self.op_add_key.grid(row=5, column=4, **self.cg_conf)
self.op_div_key.grid(row=2, column=4, **self.cg_conf)
self.op_sous_key.grid(row=4, column=4, **self.cg_conf)
self.op_res_key.grid(row=5, column=3, **self.cg_conf)
self.op_mult_key.grid(row=3, column=4, **self.cg_conf)
self.op_cancel_key.grid(row=1, column=1, **self.cg_conf)
self.op_cancelall_key.grid(row=1, column=2, **self.cg_conf)
self.parenleft_key.grid(row=1, column=3, **self.cg_conf)
self.parenright_key.grid(row=1, column=4, **self.cg_conf)
class Controller:
""" Classe decisionnaire, couplée directement à la vue et au model
et indirectement à l'observable (les liaisons sont initialisées
dans le constructeur).
Vous remarquerez que plusieurs configurations d'objets
initialités de la vue sont définies ici pour des raisons
pratiques.
Attention la gestion des touches du clavier (du clavier
physique) sont définies ici. J'aurais pu au moins le mettre
dans une autre classe liée à celle ci mais comme on ne créé
aucun widget visuel par rapport à cela,
il s'agit vraiment d'une vue très abstraite
et le peu de données à initialiser m'ont convaincu de tout
laisser dans le controller, du moins pour le moment. Peut
etre que si vous ou aviez envie de poursuivre la
construction de pyCalc mini, il deviendrait necessaire à terme
de balancer tout ca dans une autre classe pour plus de
lisibilité.
"""
def __init__(self, root):
self.root = root
#init view
self.screener = Screener(root)
self.navbar = NavBar(root)
self.load_menu_config()
root.config(menu=self.navbar)
self.keypad = Keypad(root)
self.load_keypad_config()
self.screener.pack(side="top", fill="x")
self.keypad.pack(fill="x")
# init keybind event
root.bind("<Key>", self.on_key_pressed)
self.load_keybind_config()
# init model
self.model = Model()
self.model.op_line.add_callback(self.on_op_line_changed)
self.model.result_line.add_callback(self.on_result_line_changed)
self.on_op_line_changed(self.model.op_line.get())
self.on_result_line_changed(self.model.result_line.get())
def add_number(self, number):
""" Appelé par la vue lors du clic sur un bouton de type
nombre ou la virgule. Envoie un message au Model pour
traiter l'opération.
"""
self.model.add_number(number)
def op_line_backward(self):
""" Appelé par la vue lors du clic sur le bouton d effacement.
Envoie un message au Model pour traiter l'opération.
"""
self.model.op_line_backward()
def clear_op_line(self):
""" Appelé par la vue lors du clic sur le bouton CE.
Envoie un message au Model pour traiter l'opération.
"""
self.model.clear_op_line()
def add_operator(self, operator):
""" Appelé par la vue lors du clic sur un bouton de type
operator.
Envoie un message au Model pour traiter l'opération.
"""
self.model.add_operator(operator)
def show_op_result(self):
""" Appelé par la vue lors du clic sur le bouton de resultat.
Envoie un message au Model pour traiter l'opération.
"""
self.model.get_op_result()
def on_op_line_changed(self, op_line):
""" Observe l'objet contenant les données d' op_line.
Est appelé quand op_line est modifié.
"""
self.screener.refresh_op_line(op_line)
def on_result_line_changed(self, result):
""" Observe l'objet contenant les données de result line.
Est appelé quand result_line est modifié.
"""
self.screener.refresh_result_line(result)
def load_keybind_config(self):
self.event_key = {
"KP_0" : lambda x="0": self.add_number(x),
"KP_1" : lambda x="1": self.add_number(x),
"KP_2" : lambda x="2": self.add_number(x),
"KP_3" : lambda x="3": self.add_number(x),
"KP_4" : lambda x="4": self.add_number(x),
"KP_5" : lambda x="5": self.add_number(x),
"KP_6" : lambda x="6": self.add_number(x),
"KP_7" : lambda x="7": self.add_number(x),
"KP_8" : lambda x="8": self.add_number(x),
"KP_9" : lambda x="9": self.add_number(x),
"KP_Multiply" : lambda x=" * ": self.add_operator(x),
"KP_Add" : lambda x=" + ": self.add_operator(x),
"KP_Subtract" : lambda x=" - ": self.add_operator(x),
"KP_Divide" : lambda x=" / ": self.add_operator(x),
"comma" : lambda x =".": self.add_number(x),
"KP_Decimal" : lambda x="." : self.add_number(x),
"parenleft" : lambda x= "(": self.add_operator(x),
"parenright" : lambda x=")": self.add_operator(x),
"=" : self.show_op_result,
"Return" : self.show_op_result,
"BackSpace" : self.op_line_backward,
"Delete" : self.clear_op_line,
"KP_Enter" : self.show_op_result
}
def on_key_pressed(self, event):
""" Appelé lors d'une saisie au clavier. Traite un
dictionnaire de fonctions.
"""
#print(event.keysym)
try :
self.event_key[event.keysym]()
except KeyError:
pass
def load_keypad_config(self):
""" Parametrages des touches pour l'appel des fonctions
liées. Ca aurait pu etre directement mis dans le
constructeur mais c'est plus lisible ainsi.
"""
# Config num keys (rappel float est considéré comme num)
self.keypad.float_key.config(
command=lambda x=".": self.add_number(x))
for i in range(len(self.keypad.num_key)):
istring = str(i)
self.keypad.num_key[i].config(
text = i,
command=lambda x=istring: self.add_number(x))
# Config op keys
self.keypad.op_add_key.config(
command=lambda x =" + ": self.add_operator(x))
self.keypad.op_div_key.config(
command=lambda x =" / ": self.add_operator(x))
self.keypad.op_mult_key.config(
command=lambda x =" * ": self.add_operator(x))
self.keypad.op_sous_key.config(
command=lambda x =" - ": self.add_operator(x))
self.keypad.parenleft_key.config(
command=lambda x="(": self.add_number(x))
self.keypad.parenright_key.config(
command=lambda x=")": self.add_number(x))
self.keypad.op_res_key.config(
command=self.show_op_result)
# config param keys
self.keypad.op_cancelall_key.config(
command=self.clear_op_line)
self.keypad.op_cancel_key.config(
command=self.op_line_backward)
def load_menu_config(self):
self.navbar.menu.add_command(
label ="Quitter",
command=self.quit_program)
def quit_program(self):
self.root.destroy()
# --------------------------
# --------------------
# ---------------
if __name__ == '__main__':
root = tk.Tk()
root.title("pyCalc mini*")
app = Controller(root)
root.mainloop() |
Partager