GUI Grafo
Com'è 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:
- Direzione che indica il nodo di partenza e di destinazione
- Peso dell'arco, che se premuto sopra si può cambiare
- Bidirezionale appare un tick (✔) se l'arco è bidirezionale, altrimenti non comparirà nulla, se premuto l'arco viene considerato bidirezionale
Commento sul Codice
- Classe Nodo
-
class Nodo:
-
def __init__(self, coords: list[tuple], n:int, line_id: int):
-
self.__coords = coords
-
self.__number = n
-
self.__line_id = line_id
-
-
def getCoords(self):
-
return self.__coords
-
-
def getNumber(self):
-
return self.__number
-
-
def getId(self):
-
return self.__line_id
Questo è come vede il programma i Nodi dei grafi. Composto da:
- Coordinate del centro del nodo
- Numero del nodo
- ID della linea del canvas
Per il resto non c'è nulla di particolare e il codice parla solo.
- Classe Arco
- class Arco:
- def __init__(self, coords: list[tuple], nodes: list[Nodo], height:int, line_id:int, bidirectional = False,**kargs):
- self.__coords = coords
- self.__nodes = nodes
- self.__height = height
- self.__line_id = line_id
- self.__bidirectional = bidirectional
- self.__kargs = kargs
- def getCoords(self):
- return self.__coords
- def getNodes(self):
- return self.__nodes
- def getHeight(self):
- return self.__height
- def getId(self):
- return self.__line_id
- def getBidirectional(self) -> bool:
- return self.__bidirectional
- def getKargs(self):
- return self.__kargs
- def getScheme(self, withHeight = True):
- s = ""
- s += " ".join([str(n.getNumber()) for n in self.__nodes])
- if withHeight:
- s += f" {self.__height}"
- if self.__bidirectional:
- s1 = s.split(" ")
- s1[0], s1[1] = s1[1], s1[0]
- s1 = " ".join(s1)
- s += f"\n{s1}"
- return s.strip()
- def getTotalInfo(self):
- s = self.__coords.copy()
- s.append({})
- if self.__bidirectional:
- s[-1]["BIDIREZIONALE"] = 1
- s[-1]["PESO"] = self.__height
- return s
- # ------------------------------------------ #
- def toggleBidirectional(self):
- self.__bidirectional = not self.__bidirectional
- def setBidirectional(self, value):
- self.__bidirectional = value
- def setHeight(self, value):
- self.__height = value
Questo è come vede il programma l'Arco, composto da:
- coords in cui specifica le coordinate dell'arco
- nodes è l'array formato da due nodi, quello di partenza e di destinazione
- height indica il peso dell'arco
- line_id è l'identificativo dell'arco nel canvas
- bidirectional di default impostato su False, indica se l'arco è bidirezionale o meno
- **kargs, sono degli argomenti aggiuntivi, marcati da una chiave (key)
-
Il metodo getScheme ritorna sotto forma di stringa il nodo di partenza, di fine e il peso.
Il parametro withHeight, impostato di default su True, mi aggiunge o meno il peso alla fine.
Su riga 31, s += " ".join([str(n.getNumber()) for n in self.__nodes]), prendo l'array dei Nodi e vado a richiamare il metodo getNumber in modo da ottenere il numero del nodo, e se bidirezionale mi va a copiare lo schema precedente e va aggiungere gli stessi nodi, ma invertiti.
Infine il return s.strip(), ritorna lo schema strippato, senza spazi in eccesso. - Il metodo getTotalInfo, ritorna un array composo dalle coordinate cartesiane dell'arco, seguito da un dizionario in cui si distinguono, il peso e il fatto di essere bidirezionale, che se impostato è 1, altrimenti non c'è.
Il metodo che uso è il toggleBidirectional in cui mi inverte il valore di self.__bidirectional.
- Rilevazione Errore
- def logFunzione(func):
- def wrapper(*args):
- try:
- func(*args)
- except:
- err = traceback.format_exc()
- ora = time.strftime("%d-%m-%Y | %H:%M", time.localtime())
- messagebox.showerror("Notifica di Servizio", "Si è verificato un errore. Chiedere più informazioni al proprietario del software")
- with open("log.txt", "a") as file:
- file.write(f"[{ora}] =>{err}\n{'-'*130}\n")
- file.close()
- return wrapper
- # ------------------------------------------ #
- class GUI(ctk.CTk):
- ....
- @logFunzione
- def setPoint(self, event):
- ...
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
- self.archi += 1
- temp = self.temp_coods.copy()
- pm = [(temp[0][0] + temp[1][0])/2, (temp[0][1] + temp[1][1])/2]
- try: angle = math.degrees(math.atan2((temp[0][1] - temp[1][1]), (temp[0][0] - temp[1][0])))
- except: angle = 90
- lenght = math.sqrt((temp[0][0] - temp[1][0])**2 + (temp[0][1] - temp[1][1])**2)
- lenght /= 2
- lenght -= 10
- temp = list(map(lambda x: list(x), temp))
- temp[0][0] = lenght*cos(angle) + pm[0] # x
- temp[0][1] = lenght*sin(angle) + pm[1] # y
- temp[1][0] = lenght*cos(angle+180) + pm[0] # x
- temp[1][1] = lenght*sin(angle+180) + pm[1] # y
- 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
- if self.permitted_coods[0][0]:
- self.nodi += 1
- nodo1 = self.canvas.create_oval((x1,y1),(x2,y2), fill="#202123", outline="white", activewidth=2)
- text_nodo1 = self.canvas.create_text(self.temp_coods[0], text=str(self.nodi), activefill="red", fill="white", state="disabled")
- ogg_nodo1 = Nodo(self.temp_coods[0], self.nodi, nodo1)
- self.canvas.itemconfig(nodo1, tags=([pickle.dumps(ogg_nodo1)]))
- if event.num == 2:
- x,y = self.temp_coods[0]
- self.dict_nodes[f"{x} {y}"] = [(x,y), pickle.dumps(ogg_nodo1)]
- else:
- 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:
- Coordinate del centro del nodo
- Numero del nodo (che il quel momento viene rappresentato da self.nodi)
- ID del disegno del cerchio (nodo1), nel suo tag sarà racchiuso l'oggetto interpretato da pickle
In un evento num è il numero del pulsante del mouse
- 1 tasto sinistro del mouse
- 2 tasto centrale del mouse
- 3 tasto destro del mouse
gabriele_melissano