diff --git a/main.py b/main.py index ba547dd..0c8fafb 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,11 @@ -from data_structure import * -import customtkinter as ctk import math +import customtkinter as ctk +from data_structure import Pile_chaine + class Expression: """manipule les expression sous forme d'arbre""" - def __init__(self, val, fils_gauche, fils_droit) -> None: + def __init__(self, val, fils_gauche=None, fils_droit=None) -> None: self.val = val self.fils_gauche = fils_gauche self.fils_droit = fils_droit @@ -15,55 +16,101 @@ class Expression: return str(self.val) return '('+self.fils_gauche.__str__()+str(self.val)+self.fils_droit.__str__()+')' + def print_tree(self, level=0): + """ Affiche le noeud joliment""" + if self.fils_gauche is not None: + self.fils_gauche.print_tree(level + 1) + print(' ' * 4 * level + '-> ' + str(self.val)) + if self.fils_droit is not None: + self.fils_droit.print_tree(level + 1) + def est_feuille(self) -> bool: - """renvoie true si le noeud est une feuille""" + """ Renvoie true si le noeud est une feuille""" if self.fils_droit is None and self.fils_gauche is None: return True return False def evalue(self, x=0) -> float: - """renvoie le résultat de l'expression""" - if self.est_feuille(): - if self.val == 'x': - return x - return float(self.val) - if self.val == '+': - return self.fils_gauche.evalue(x) + self.fils_droit.evalue(x) - if self.val == '*': - return self.fils_gauche.evalue(x) * self.fils_droit.evalue(x) - if self.val == '/': - return self.fils_gauche.evalue(x) / self.fils_droit.evalue(x) - if self.val == '^': - return self.fils_gauche.evalue(x) ** self.fils_droit.evalue(x) - if self.val == '-': - return self.fils_gauche.evalue(x) - self.fils_droit.evalue(x) - - def valeurs_de_fonction(self): - """calcul les 100 premieres valeurs""" + """ Renvoie le résultat de l'expression""" + try: + if self.est_feuille(): + if self.val == 'x': + return x + return float(self.val) + elif self.val == '+': + return self.fils_gauche.evalue(x) + self.fils_droit.evalue(x) + elif self.val == '*': + return self.fils_gauche.evalue(x) * self.fils_droit.evalue(x) + elif self.val == '/': + return self.fils_gauche.evalue(x) / self.fils_droit.evalue(x) + elif self.val == '^': + return self.fils_gauche.evalue(x) ** self.fils_droit.evalue(x) + elif self.val == '-': + return self.fils_gauche.evalue(x) - self.fils_droit.evalue(x) + elif self.val == 'ln': + return math.log(self.fils_gauche.evalue(x)) + elif self.val == '√': + return math.sqrt(self.fils_gauche.evalue(x)) + elif self.val == "cos": + return math.cos(self.fils_gauche.evalue(x)) + elif self.val == "sin": + return math.sin(self.fils_gauche.evalue(x)) + elif self.val == "tan": + return math.tan(self.fils_gauche.evalue(x)) + + except ZeroDivisionError: + raise ZeroDivisionError + + def valeurs_de_fonction(self, start, end): + """ Calcul les valeurs entre start et end""" result = [] - for i in range(-100, 100): - result.append((i, self.evalue(i))) + for i in range(start, end+1): + try: + result.append((i, self.evalue(i))) + except: + pass return result +def change_appearance_mode_event(new_appearance_mode: str) -> None: + """Change le thème de sombre à Claire""" + ctk.set_appearance_mode(new_appearance_mode) + class App(ctk.CTk): + """Classe pour l'interface graphique""" def __init__(self) -> None: super().__init__() - self.grid_columnconfigure(0, weight=0) - self.numbers = [] - for i, val in enumerate(["(", ")", "/", "^", 7,8,9,"*", 4, 5, 6, "-", 1, 2, 3, "+", "clear", 0, '.', "exe"]): # add numbers button - self.numbers.append(ctk.CTkButton(self, text=val)) - if val == "clear": - self.numbers[i]._command = self.clear_screen + 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.numbers[i]._command = lambda x=val: self.add_value(x) - self.numbers[i].grid(row=5+i//4, column=i%4+1, sticky="NSEW", padx=5, pady=5) + 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_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) @@ -75,40 +122,42 @@ class App(ctk.CTk): self.fonction_frame = ctk.CTkFrame(self, fg_color="transparent") self.fonction_screen = ctk.CTkCanvas(self.fonction_frame) - self.fonction_screen.pack(fill="both", expand=True) + self.fonction_screen.grid(sticky="nsew", column=1, row=0) self.fonction_entry = ctk.CTkEntry(self.fonction_frame) - self.fonction_entry.pack(fill="both", expand=True) - - + 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 = 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 = 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 = 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=self.change_appearance_mode_event) + 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") @@ -116,128 +165,211 @@ class App(ctk.CTk): # show selected frame if name == "Calcul": self.mode = "Calcul" - self.calcul_frame.grid(padx=20, pady=20,columnspan=4, column=1, rowspan=5, row=0, sticky="nsew") - self.numbers[-1]._command = self.calculate + 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=4, column=1, row=0, padx=20, pady=20, sticky="nsew") + 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.numbers[-1]._command = self.draw_graph + self.keys[-1]._command = self.draw_graph self.fonction_screen_height = self.fonction_screen.winfo_height() - print(self.fonction_screen_height) else: self.fonction_frame.grid_forget() - def change_appearance_mode_event(self, new_appearance_mode): - ctk.set_appearance_mode(new_appearance_mode) - 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("end", value) + self.fonction_entry.insert(ctk.INSERT, value) else: - self.calcul_entry.insert("end", value) + 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): - ratio = self.fonction_screen_height / self.fonction_screen_width # longueur par largeur du canvas + """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 + for i in range(20): # dessin des lignes verticales if i == int(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) + 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") + 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") + 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') - coo_abscisse = self.fonction_screen_height * ordonnee % 20 -6 # décalage modulo 20 par rapport à l'origine - nb_abscisse_lines = math.ceil(20*ratio)+1 # nombres de lignes horizontales à tracer - for i in range(nb_abscisse_lines): # dessin des lignes verticales - if i == int(nb_abscisse_lines * abscisse): # tracer de l'axe des abscisses - self.fonction_screen.create_line(0, 0 + i * self.fonction_screen_width/20 - coo_abscisse, self.fonction_screen_width, 0 + i * self.fonction_screen_width/20 - coo_abscisse, fill='red', width=4) - text = self.fonction_screen.create_text(15, 0 + i * self.fonction_screen_width/20 - coo_abscisse, text=min_x) - rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text),fill="white") + 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 == int(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 - coo_abscisse, text=max_x) - rect = self.fonction_screen.create_rectangle(self.fonction_screen.bbox(text),fill="white") + 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 - coo_abscisse, self.fonction_screen_width, 0 + i * self.fonction_screen_width/20 - coo_abscisse, fill='red') + 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: - exp = list(self.calcul_entry.get()) - exp = inf2npi(exp) - result = npi2tree(exp).evalue() + """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') - self.fonction_points = npi2tree(inf2npi(parse_string_to_list(self.fonction_entry.get()))).valeurs_de_fonction() - max_y = max(self.fonction_points,key=lambda item:item[1])[1] - min_y = min(self.fonction_points,key=lambda item:item[1])[1] - self.draw_framing(-100, 100, min_y, max_y) - for x, y in self.fonction_points: - image_x = self.fonction_screen_width / 2 + x * (self.fonction_screen_width/len(self.fonction_points)) - image_y = self.fonction_screen_height - (y - min_y) * self.fonction_screen_height / (abs(max_y)+abs(min_y)) - 3 + fonction_points = npi2tree(inf2npi(parse_string_to_list(self.fonction_entry.get()))).valeurs_de_fonction(-100, 100) + 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(-1000, 1000, min_y, max_y) + for x, y in fonction_points: + image_x = self.fonction_screen_width / 2 + x * (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) def parse_string_to_list(text: str) -> list: + """ Transforme le text en liste d'éléments intelligible par le programme """ text = list(text) result = [] - elem = "" + buffer_function = "" + buffer_number = "" + number_first = False # Savoir dans quel ordre sont les chiffres et les lettres for char in text: - if char.isdigit(): - elem += char - elif char == 'x': - result.append(char) + 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(): + print(char) + buffer_function += char else: - if len(elem) != 0: - result.append(elem) - elem = "" - result.append(char) - if len(elem) != 0: - result.append(elem) - elem = "" + if number_first: + print("number", char) + 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 == '(': + print("()") + 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) + print(result) return result - - - - - - def npi2tree(expr: list) -> Expression: - """renvoie l'arbre formé a partir de l'expression donnée""" + """ Renvoie l'arbre formé à partir de l'expression donnée""" pile = Pile_chaine() for val in expr: - if not val.isdigit() and val != "x": + if not type(val) is float and val != "x": # on inverse pour avoir les nombres dans le bon ordre - nombre2, nombre1 = pile.depiler(), pile.depiler() - pile.empiler(Expression(val, nombre1, nombre2)) + 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, None, None)) + 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() @@ -245,14 +377,14 @@ def inf2npi(expr: list) -> list: '+': 1, '-': 1, '*': 2, - '^': 2, + '^': 3, '/': 2, '(': 0, ')': 0 } output = [] for val in expr: - if val.isdigit() or val == 'x': + 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()]): @@ -261,22 +393,16 @@ def inf2npi(expr: list) -> list: while not operator_stack.est_vide(): if operator_stack.sommet() == '(': operator_stack.depiler() - if operator_stack.est_vide(): # test si il y a un astérix avant la parenthèse - output.append('*') - elif operator_stack.sommet() != '*': - output.append('*') - else: - output.append(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()) + print(output) return output + # [3, '-', 6, '*', 4, '+', 3] -print(parse_string_to_list('x')) -print(inf2npi(parse_string_to_list('x'))) gui = App() -gui.mainloop() \ No newline at end of file +gui.mainloop()