You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
344 lines
16 KiB
344 lines
16 KiB
2 years ago
|
"""Classe d'application tk et ctk"""
|
||
|
|
||
|
import math
|
||
|
import customtkinter as ctk
|
||
|
from package.data_structure import Pile_chaine
|
||
|
from package.expression import Expression
|
||
|
|
||
|
|
||
|
def change_appearance_mode_event(new_appearance_mode: str) -> None:
|
||
|
"""Change le thème de sombre à Claire"""
|
||
|
ctk.set_appearance_mode(new_appearance_mode)
|
||
|
|
||
|
def parse_string_to_list(text: str) -> list:
|
||
|
""" Transforme le text en liste d'éléments intelligible par le programme """
|
||
|
text = list(text)
|
||
|
result = []
|
||
|
buffer_function = ""
|
||
|
buffer_number = ""
|
||
|
number_first = False # Savoir dans quel ordre sont les chiffres et les lettres
|
||
|
for char in text:
|
||
|
if char == 'e':
|
||
|
result.append(math.e)
|
||
|
elif char == '𝜋':
|
||
|
result.append(math.pi)
|
||
|
elif char.isdigit() or char == ".":
|
||
|
buffer_number += char
|
||
|
if len(buffer_function) == 0:
|
||
|
number_first = True
|
||
|
elif char.isalpha():
|
||
|
buffer_function += char
|
||
|
else:
|
||
|
if number_first:
|
||
|
result.append(float(buffer_number))
|
||
|
buffer_number = ""
|
||
|
number_first = False
|
||
|
if len(buffer_function) != 0:
|
||
|
result.append('*')
|
||
|
result.append(buffer_function)
|
||
|
buffer_function = ""
|
||
|
elif char == '(':
|
||
|
result.append('*')
|
||
|
|
||
|
else:
|
||
|
if len(buffer_function) != 0:
|
||
|
result.append(buffer_function)
|
||
|
buffer_function = ""
|
||
|
if len(buffer_number) != 0:
|
||
|
result.append(float(buffer_number))
|
||
|
buffer_number = ""
|
||
|
number_first = False
|
||
|
result.append(char)
|
||
|
if len(buffer_number) != 0:
|
||
|
result.append(float(buffer_number))
|
||
|
if len(buffer_function) != 0:
|
||
|
result.append('*')
|
||
|
result.append(buffer_function)
|
||
|
elif len(buffer_function) != 0:
|
||
|
result.append(buffer_function)
|
||
|
return result
|
||
|
|
||
|
def npi2tree(expr: list) -> Expression:
|
||
|
""" Renvoie l'arbre formé à partir de l'expression donnée"""
|
||
|
pile = Pile_chaine()
|
||
|
for val in expr:
|
||
|
if not type(val) is float and val != "x":
|
||
|
# on inverse pour avoir les nombres dans le bon ordre
|
||
|
nombre2 = pile.depiler()
|
||
|
if not pile.est_vide():
|
||
|
pile.empiler(Expression(val, pile.depiler(), nombre2))
|
||
|
else:
|
||
|
pile.empiler(Expression(val, nombre2))
|
||
|
else:
|
||
|
pile.empiler(Expression(val))
|
||
|
return pile.sommet()
|
||
|
|
||
|
def inf2npi(expr: list) -> list:
|
||
|
"""Transforme une expression infixé en notation polonaise inversée"""
|
||
|
operator_stack = Pile_chaine()
|
||
|
operator_priority = {
|
||
|
'+': 1,
|
||
|
'-': 1,
|
||
|
'*': 2,
|
||
|
'^': 3,
|
||
|
'/': 2,
|
||
|
'(': 0,
|
||
|
')': 0
|
||
|
}
|
||
|
output = []
|
||
|
for val in expr:
|
||
|
if type(val) is float or val == 'x':
|
||
|
output.append(val)
|
||
|
else:
|
||
|
if operator_stack.est_vide() or ( val == '(' or operator_priority[val] > operator_priority[operator_stack.sommet()]):
|
||
|
operator_stack.empiler(val)
|
||
|
else:
|
||
|
while not operator_stack.est_vide():
|
||
|
if operator_stack.sommet() == '(':
|
||
|
operator_stack.depiler()
|
||
|
else:
|
||
|
output.append(operator_stack.depiler())
|
||
|
if val != ')':
|
||
|
operator_stack.empiler(val)
|
||
|
while not operator_stack.est_vide():
|
||
|
output.append(operator_stack.depiler())
|
||
|
return output
|
||
|
|
||
|
|
||
|
class Ctk_app(ctk.CTk):
|
||
|
"""Classe pour l'interface graphique avec custom tkinter"""
|
||
|
def __init__(self) -> None:
|
||
|
super().__init__()
|
||
|
|
||
|
change_appearance_mode_event('dark')
|
||
|
|
||
|
self.fonction_screen_height = None
|
||
|
self.fonction_screen_width = None
|
||
|
self.grid_columnconfigure(0, weight=0)
|
||
|
self.grid_columnconfigure((1,2,3,4,5), weight=1)
|
||
|
self.grid_rowconfigure(0, weight=0)
|
||
|
self.grid_rowconfigure((1,2,3,4,5), weight=1)
|
||
|
|
||
|
# touches de la calculette
|
||
|
|
||
|
self.keys = []
|
||
|
for i, val in enumerate(["←", "cos", "sin", "tan", "→", "x", "e", "√", "^", "𝜋", 7, 8, 9, "(", ")", 4, 5, 6, "*", "/", 1, 2, 3, "+", "-",
|
||
|
"clear", 0, ".", "ln", "exe"]): # add numbers button
|
||
|
if type(val) == int:
|
||
|
self.keys.append(ctk.CTkButton(self, text=val, command=lambda x=val: self.add_value(x), fg_color=("gray50","gray20"), text_color="gray90", hover_color=("gray40", "gray30")))
|
||
|
elif val == "clear":
|
||
|
self.keys.append(ctk.CTkButton(self, text=val, command=self.clear_screen))
|
||
|
elif val == "←":
|
||
|
self.keys.append(ctk.CTkButton(self, text=val, command=self.move_cursor_left))
|
||
|
elif val == "→":
|
||
|
self.keys.append(ctk.CTkButton(self, text=val, command=self.move_cursor_right))
|
||
|
else:
|
||
|
self.keys.append(ctk.CTkButton(self, text=val, command=lambda x=val: self.add_value(x)))
|
||
|
self.keys[i].grid(row=i//5+1, column=i % 5+1, sticky="NSEW", padx=5, pady=5)
|
||
|
|
||
|
# Calcul Frame
|
||
|
|
||
|
self.calcul_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
|
||
|
|
||
|
self.calcul_screen = ctk.CTkLabel(self.calcul_frame, text="hello")
|
||
|
self.calcul_screen.pack(fill="both", expand=True)
|
||
|
self.calcul_entry = ctk.CTkEntry(self.calcul_frame)
|
||
|
self.calcul_entry.pack(fill="both", expand=True)
|
||
|
|
||
|
# Fonction Frame
|
||
|
|
||
|
self.fonction_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||
|
|
||
|
self.fonction_bornes_frame = ctk.CTkFrame(self.fonction_frame, corner_radius=None)
|
||
|
|
||
|
self.fonction_bornes_entry = ctk.CTkEntry(self.fonction_bornes_frame)
|
||
|
self.fonction_bornes_entry.insert(0, "-100,100")
|
||
|
self.fonction_bornes_entry.grid(sticky="ew", row=1, padx=10)
|
||
|
|
||
|
self.fonction_bornes_text = ctk.CTkLabel(self.fonction_bornes_frame, text="Entrez les bornes de tracé: (min, max)")
|
||
|
self.fonction_bornes_text.grid(sticky="ew", row=0)
|
||
|
|
||
|
self.fonction_bornes_frame.grid(sticky="ns", column=0, rowspan=2)
|
||
|
|
||
|
|
||
|
self.fonction_screen = ctk.CTkCanvas(self.fonction_frame)
|
||
|
self.fonction_screen.grid(sticky="nsew", column=1, row=0)
|
||
|
self.fonction_entry = ctk.CTkEntry(self.fonction_frame)
|
||
|
self.fonction_entry.grid(sticky='ew', column=1, row=1)
|
||
|
|
||
|
# navigation menu
|
||
|
self.navigation_frame = ctk.CTkFrame(self, corner_radius=0)
|
||
|
self.navigation_frame.grid_rowconfigure(4, weight=1)
|
||
|
self.navigation_frame.grid(row=0, rowspan=10, column=0, sticky="nsew")
|
||
|
|
||
|
self.titre = ctk.CTkLabel(self.navigation_frame, text="Wx Calculator", compound="left"
|
||
|
, font=ctk.CTkFont(size=15, weight="bold"))
|
||
|
self.titre.grid(row=0, column=0, padx=20, pady=20)
|
||
|
|
||
|
self.home_button = ctk.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10
|
||
|
, text="Calcul", fg_color="transparent", text_color=("gray10", "gray90")
|
||
|
, hover_color=("gray70", "gray30"), anchor="w"
|
||
|
, command=lambda: self.select_frame_by_name("Calcul"))
|
||
|
self.home_button.grid(row=1, column=0, sticky="ew")
|
||
|
|
||
|
self.function_button = ctk.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10
|
||
|
, text="Fonction", fg_color="transparent", text_color=("gray10", "gray90")
|
||
|
, hover_color=("gray70", "gray30"), anchor="w"
|
||
|
, command=lambda: self.select_frame_by_name("Fonction"))
|
||
|
self.function_button.grid(row=2, column=0, sticky="ew")
|
||
|
|
||
|
self.appearance_mode_menu = ctk.CTkOptionMenu(self.navigation_frame, values=["Light", "Dark", "System"]
|
||
|
, command=change_appearance_mode_event)
|
||
|
self.appearance_mode_menu.grid(row=4, column=0, padx=20, pady=20, sticky="s")
|
||
|
self.mode = 'Calcul'
|
||
|
|
||
|
# select default frame
|
||
|
|
||
|
self.select_frame_by_name("Calcul")
|
||
|
|
||
|
def select_frame_by_name(self, name):
|
||
|
"""Change de mode : passe de calcul à fonction"""
|
||
|
# set button color for selected button
|
||
|
self.home_button.configure(fg_color=("gray75", "gray25") if name == "Calcul" else "transparent")
|
||
|
self.function_button.configure(fg_color=("gray75", "gray25") if name == "Fonction" else "transparent")
|
||
|
|
||
|
# show selected frame
|
||
|
if name == "Calcul":
|
||
|
self.mode = "Calcul"
|
||
|
self.calcul_frame.grid(padx=20, pady=20, columnspan=5, column=1, row=0, sticky="nsew")
|
||
|
self.keys[-1]._command = self.calculate
|
||
|
|
||
|
else:
|
||
|
self.calcul_frame.grid_forget()
|
||
|
if name == "Fonction":
|
||
|
self.mode = "Fonction"
|
||
|
self.fonction_frame.grid(columnspan=5, column=1, row=0, padx=20, pady=20, sticky="ns")
|
||
|
self.fonction_frame.update()
|
||
|
self.fonction_screen_width = self.fonction_screen.winfo_width()
|
||
|
self.keys[-1]._command = self.draw_graph
|
||
|
self.fonction_screen_height = self.fonction_screen.winfo_height()
|
||
|
else:
|
||
|
self.fonction_frame.grid_forget()
|
||
|
|
||
|
def add_value(self, value) -> None:
|
||
|
"""Ajoute le charactère à la suite de l'expression"""
|
||
|
parenthesis = False
|
||
|
if value == "e":
|
||
|
value += "^"
|
||
|
if value == "(":
|
||
|
value += ")"
|
||
|
parenthesis = True
|
||
|
elif value in ["ln", "cos", "sin", "tan", "√"]:
|
||
|
value += "()"
|
||
|
parenthesis = True
|
||
|
if self.mode == 'Fonction':
|
||
|
self.fonction_entry.insert(ctk.INSERT, value)
|
||
|
else:
|
||
|
self.calcul_entry.insert(ctk.INSERT, value)
|
||
|
if parenthesis:
|
||
|
self.move_cursor_left()
|
||
|
|
||
|
def clear_screen(self) -> None:
|
||
|
"""Enlève l'affichage actuel"""
|
||
|
if self.mode == "Calcul":
|
||
|
self.calcul_screen.configure(text="")
|
||
|
self.calcul_entry.delete('0', 'end')
|
||
|
else:
|
||
|
self.fonction_screen.delete('all')
|
||
|
self.fonction_entry.delete(0, 'end')
|
||
|
|
||
|
def move_cursor_left(self):
|
||
|
"""Déplace le curseur à gauche"""
|
||
|
if self.mode == "Calcul":
|
||
|
pos = self.calcul_entry.index(ctk.INSERT)
|
||
|
if pos > 0:
|
||
|
self.calcul_entry.icursor(pos -1)
|
||
|
else:
|
||
|
pos = self.fonction_entry.index(ctk.INSERT)
|
||
|
if pos > 0:
|
||
|
self.fonction_entry.icursor(pos-1)
|
||
|
|
||
|
def move_cursor_right(self):
|
||
|
"""Déplace le curseur à droite"""
|
||
|
if self.mode == "Calcul":
|
||
|
pos = self.calcul_entry.index(ctk.INSERT)
|
||
|
if pos < len(self.calcul_entry.get()):
|
||
|
self.calcul_entry.icursor(pos + 1)
|
||
|
else:
|
||
|
pos = self.fonction_entry.index(ctk.INSERT)
|
||
|
if pos < len(self.fonction_entry.get()):
|
||
|
self.fonction_entry.icursor(pos + 1)
|
||
|
|
||
|
def draw_framing(self, min_x, max_x, min_y, max_y):
|
||
|
"""Dessine le cadrillage du graphique"""
|
||
|
ratio = self.fonction_screen_height / self.fonction_screen_width # longueur par largeur du canvas
|
||
|
|
||
|
# position du max dans l'interval
|
||
|
abscisse = max_y / (abs(min_y)+abs(max_y))
|
||
|
ordonnee = max_x / (abs(min_x)+abs(max_x))
|
||
|
|
||
|
for i in range(20): # dessin des lignes verticales
|
||
|
if i == int(20 - 20*ordonnee):
|
||
|
self.fonction_screen.create_line(0 + i * self.fonction_screen_width/20, 0
|
||
|
, 0 + i * self.fonction_screen_width/20, self.fonction_screen_height
|
||
|
, fill='red', width=4)
|
||
|
text = self.fonction_screen.create_text(0 + i * self.fonction_screen_width/20, 10, text=max_y)
|
||
|
rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text), fill="white")
|
||
|
self.fonction_screen.tag_lower(rect, text)
|
||
|
|
||
|
text = self.fonction_screen.create_text(0 + i * self.fonction_screen_width/20,
|
||
|
self.fonction_screen_height - 10, text=min_y)
|
||
|
rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text), fill="white")
|
||
|
self.fonction_screen.tag_lower(rect, text)
|
||
|
else:
|
||
|
self.fonction_screen.create_line(0 + i * self.fonction_screen_width/20, 0
|
||
|
, 0 + i * self.fonction_screen_width/20, self.fonction_screen_height
|
||
|
, fill='red')
|
||
|
nb_abscisse_lines = int(20*ratio) # nombre de lignes horizontales à tracer
|
||
|
|
||
|
for i in range(nb_abscisse_lines): # dessin des lignes horizontales
|
||
|
if i == math.ceil(nb_abscisse_lines * abscisse): # tracer de l'axe des abscisses
|
||
|
self.fonction_screen.create_line(0, 0 + i * self.fonction_screen_width/20
|
||
|
, self.fonction_screen_width
|
||
|
, 0 + i * self.fonction_screen_width/20, fill='red'
|
||
|
, width=4)
|
||
|
text = self.fonction_screen.create_text(15, 0 + i * self.fonction_screen_width/20
|
||
|
, text=min_x)
|
||
|
rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text)
|
||
|
, fill="white")
|
||
|
self.fonction_screen.tag_lower(rect, text)
|
||
|
|
||
|
text = self.fonction_screen.create_text(self.fonction_screen_width - 15
|
||
|
, 0 + i * self.fonction_screen_width/20
|
||
|
, text=max_x)
|
||
|
rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text), fill="white")
|
||
|
self.fonction_screen.tag_lower(rect, text)
|
||
|
else:
|
||
|
self.fonction_screen.create_line(0, 0 + i * self.fonction_screen_width/20
|
||
|
, self.fonction_screen_width
|
||
|
, 0 + i * self.fonction_screen_width/20, fill='red')
|
||
|
|
||
|
def calculate(self) -> None:
|
||
|
"""Calcule dans la partie graphique"""
|
||
|
exp = parse_string_to_list(self.calcul_entry.get())
|
||
|
exp = npi2tree(inf2npi(exp))
|
||
|
exp.print_tree()
|
||
|
try:
|
||
|
result = exp.evalue()
|
||
|
except ZeroDivisionError:
|
||
|
result = "ERREUR"
|
||
|
self.calcul_screen.configure(text=result)
|
||
|
|
||
|
def draw_graph(self):
|
||
|
"""Dessine les points du graphique"""
|
||
|
self.fonction_screen.delete('all')
|
||
|
min_x, max_x = map(int, self.fonction_bornes_entry.get().split(','))
|
||
|
fonction_points = npi2tree(inf2npi(parse_string_to_list(self.fonction_entry.get()))).valeurs_de_fonction(min_x, max_x)
|
||
|
max_y = max(fonction_points, key=lambda item: item[1])[1]
|
||
|
min_y = min(fonction_points, key=lambda item: item[1])[1]
|
||
|
self.draw_framing(min_x, max_x, min_y, max_y)
|
||
|
for i, (x, y) in enumerate(fonction_points):
|
||
|
image_x = i * (self.fonction_screen_width/len(fonction_points))
|
||
|
image_y = self.fonction_screen_height - (y - min_y) * self.fonction_screen_height / (abs(max_y)+abs(min_y))
|
||
|
self.fonction_screen.create_rectangle(image_x, image_y, image_x, image_y)
|