Come è nata l'idea

Iniziando un corso di informatica, a scuola, in testa girava l'idea di creare un programma capace di creare dei grafi e mi piaceva il fatto di visualizzare su gui il cammino più breve tra due nodi usando l' Algoritmo di Dijkstra.


Fase di Sviluppo

Sono partito dell'idea di creare un programma molto simile al Software CAD con l'unica cosa che bisognava aggiungere qualche figura in più quando si creano linee, meglio dire Archi.
Ogni Arco che si crea vengono creati due nodi, se premo su un nodo l'arco parte da quel nodo e finisce su un altro. Ogni arco ha il suo peso e di default viene calcolata la distanza cartesiana dal centro dei due nodi e viene divisa per 15, successivamente viene inserita in una casella di testo.
Premendo il tasto destro sull'Arco, viene aperto un piccolo menù in cui viene specificato:

Si possono poi fare diverse cose, come trovare la distanza minima tra due nodi usando l'Algoritmo di Dijkstra e in caso di errore cancellare l'Arco, il programma poi capirà quali nodi dovrà cancellare.

Commento sul Codice

- Classe Nodo

  1. class Nodo:
  2. def __init__(self, coords: list[tuple], n:int, line_id: int):
  3. self.__coords = coords
  4. self.__number = n
  5. self.__line_id = line_id

  6. def getCoords(self):
  7. return self.__coords

  8. def getNumber(self):
  9. return self.__number

  10. def getId(self):
  11. return self.__line_id
Questo è come vede il programma i Nodi dei grafi. Composto da: Gli attributi per convenzione li ho dichiarati privati, in python non avendo una keyword che specifica esplicitamente l'ambito di visibilità si usano i doppi underscore per indicare un attributo/metodo privato.
Per il resto non c'è nulla di particolare e il codice parla solo.


- Classe Arco

  1. class Arco:
  2. def __init__(self, coords: list[tuple], nodes: list[Nodo], height:int, line_id:int, bidirectional = False,**kargs):
  3. self.__coords = coords
  4. self.__nodes = nodes
  5. self.__height = height
  6. self.__line_id = line_id
  7. self.__bidirectional = bidirectional
  8. self.__kargs = kargs
  9. def getCoords(self):
  10. return self.__coords
  11. def getNodes(self):
  12. return self.__nodes
  13. def getHeight(self):
  14. return self.__height
  15. def getId(self):
  16. return self.__line_id
  17. def getBidirectional(self) -> bool:
  18. return self.__bidirectional
  19. def getKargs(self):
  20. return self.__kargs
  21. def getScheme(self, withHeight = True):
  22. s = ""
  23. s += " ".join([str(n.getNumber()) for n in self.__nodes])
  24. if withHeight:
  25. s += f" {self.__height}"
  26. if self.__bidirectional:
  27. s1 = s.split(" ")
  28. s1[0], s1[1] = s1[1], s1[0]
  29. s1 = " ".join(s1)
  30. s += f"\n{s1}"
  31. return s.strip()
  32. def getTotalInfo(self):
  33. s = self.__coords.copy()
  34. s.append({})
  35. if self.__bidirectional:
  36. s[-1]["BIDIREZIONALE"] = 1
  37. s[-1]["PESO"] = self.__height
  38. return s
  39. # ------------------------------------------ #
  40. def toggleBidirectional(self):
  41. self.__bidirectional = not self.__bidirectional
  42. def setBidirectional(self, value):
  43. self.__bidirectional = value
  44. def setHeight(self, value):
  45. self.__height = value
Questo è come vede il programma l'Arco, composto da: Sui metodi get non c'è molto da dire in quanto hanno un semplice return. C'è da aggiungere qualcosa su getScheme & getTotalInfo: Successivamente abbiamo i metodi set e uno particolare, toggle. I metodi set impostano il valore il valore del peso(setHeight) e il valore del bidirezionale (setBidirectional), solo che ancora non l'ho usato.
Il metodo che uso è il toggleBidirectional in cui mi inverte il valore di self.__bidirectional.


- Recezione Errore

  1. def logFunzione(func):
  2. def wrapper(*args):
  3. try:
  4. func(*args)
  5. except:
  6. err = traceback.format_exc()
  7. ora = time.strftime("%d-%m-%Y | %H:%M", time.localtime())
  8. messagebox.showerror("Notifica di Servizio", "Si è verificato un errore. Chiedere più informazioni al proprietario del software")
  9. with open("log.txt", "a") as file:
  10. file.write(f"[{ora}] =>{err}\n{'-'*130}\n")
  11. file.close()
  12. return wrapper
  13. # ------------------------------------------ #
  14. class GUI(ctk.CTk):
  15. ....
  16. @logFunzione
  17. def setPoint(self, event):
  18. ...
Per controllare gli eventuali errori che si verificano durante l'eseguzione del programma, senza riscrivere molto codice ho introdotto un decoratore.
Usando il blocco try-except riesco a trovare gli errori che che avvengono nella funzione, in questo caso una essenziale perché riguarda il posizionamento di Nodi & Archi, per trovare poi l'errore utilizzo il traceback che mi fornisce dettagli sull'errore e poi viene salvato nel file log.txt.
Quando si verifica un errore appare un messaggio, che dice di segnalarmelo, ho programmato una funzione segreta che mi permette di accedere al file dei log, per accederci bisogna fare tasto destro sul bottone Cancella Tutto e si aprirà una finestra in cui ci sarà il contenuto del file di log.


- Creazione Arco

  1. self.archi += 1
  2. temp = self.temp_coods.copy()
  3. pm = [(temp[0][0] + temp[1][0])/2, (temp[0][1] + temp[1][1])/2]
  4. try: angle = math.degrees(math.atan2((temp[0][1] - temp[1][1]), (temp[0][0] - temp[1][0])))
  5. except: angle = 90
  6. lenght = math.sqrt((temp[0][0] - temp[1][0])**2 + (temp[0][1] - temp[1][1])**2)
  7. lenght /= 2
  8. lenght -= 10
  9. temp = list(map(lambda x: list(x), temp))
  10. temp[0][0] = lenght*cos(angle) + pm[0] # x
  11. temp[0][1] = lenght*sin(angle) + pm[1] # y
  12. temp[1][0] = lenght*cos(angle+180) + pm[0] # x
  13. temp[1][1] = lenght*sin(angle+180) + pm[1] # y
  14. line = self.canvas.create_line(temp, fill="white", arrow="last", arrowshape=(7.0, 11.0, 6.0), activewidth=3, width=2)
Per iniziare incremento il contatore degli archi self.archi += 1.
Successivamente mi copio le coordinate in una variabile temp.
L'obiettivo è accorciare la linea di 10 punti, perché se lasciata di default oltre dare fastidio all'occhio, il disegno non è corretto.
Ora, utilizzando un po' di goniometria, considero la linea come il diametro di un cerchio con lighezza tale a quella della linea.
Calcolandomi il punto medio (pm), lo considero come centro della mia circonferenza, mi trovo la pendenza della linea (angle) e la sua lunghezza (lenght).
Visto che la lunghezza la considero come il diametro, la divido per 2, in modo da ottenere il raggio della mia circonferenza, e da li gli tolgo 10 punti, dato che serve più corta.
Adesso per sapere le nuove coordinate dei punti utilizzo la parametrizzazione di una circonferenza, usando come raggio, la nuova lunghezza lenght e come angolo angle. Faccio una precisazione sulle ultime righe dove esce come argomento del seno e coseno angle+180, è una regola degli angoli associati e, pm[0] e pm[1] sono le coordinate x e y del centro della circonferenza.
Infine creo la linea (line) usando la lista di coordinate temp.

- Riconoscimento Click

  1. if self.permitted_coods[0][0]:
  2. self.nodi += 1
  3. nodo1 = self.canvas.create_oval((x1,y1),(x2,y2), fill="#202123", outline="white", activewidth=2)
  4. text_nodo1 = self.canvas.create_text(self.temp_coods[0], text=str(self.nodi), activefill="red", fill="white", state="disabled")
  5. ogg_nodo1 = Nodo(self.temp_coods[0], self.nodi, nodo1)
  6. self.canvas.itemconfig(nodo1, tags=([pickle.dumps(ogg_nodo1)]))
  7. if event.num == 2:
  8. x,y = self.temp_coods[0]
  9. self.dict_nodes[f"{x} {y}"] = [(x,y), pickle.dumps(ogg_nodo1)]
  10. else:
  11. ogg_nodo1 = pickle.loads(self.permitted_coods[0][1])
self.permitted_coods è una matrice 2x2 in cui dice lo stato attuale nell'esatto punto in cui ho premuto, il primo elemento è un booleano in cui mi dice se è uno spazio vuoto o meno, ciò lo determina usando i tags, se c'è una determianta condizione usando quel tag, sa se è uno spazio vuoto o meno. Il secondo elemento è il tag stesso.
Se lo spazio è vuoto (if self.permitted_coods[0][0]) incrementa di 1 il counter dei nodi, self.nodi += 1 e crea la parte visiva del nodo (nodo1) rappresentandolo come un cerchio e il testo del numero del nodo. Poi viene creato un oggetto di tipo Nodo, il costruttore accetta: Dove si vede if event.num == 2 è un "metodo" per quando si deve caricare un file sul programma, quindi, senza creare un altra funzione che faccia la stessa identica cosa, all'evento gli modifico il parametro num e lo imposto uguale a 2.
In un evento num è il numero del pulsante del mouse tuttavia quando si chiama la funzione per quando si preme il tasto sinistro del mouse, il parametro num è sempre 1, impostandolo quindi a 2 capisce che si tratta di un caricamento del file e non di un suo utilizzo normale, prende le coordinate dall'array self.temp_coods e lo inserire in un dizionario (conosciuto come dict) che ha come key le coordinate sottoforma di stringa e come value le coordinate insieme all'oggetto Nodo interpretato da pickle. Quando invece rileva qualcosa recupera l'oggetto contenuto in self.permitted_coods[0][1] utilizzando pickle.