Questa serie di articoli sul pdp8 sta volgendo al termine. Come avete potuto notare, mi sono soffermato più sulle scelte e sui metodi utilizzati per realizzare il programma che sul funzionamento del processore, ma solo perché il mio obbiettivo è più generale e con queste ultime puntate spero di raggiungerlo. Infatti vedremo come contornare il nostro programma scritto in python con una adeguata interfaccia grafica, se pur molto spartana, per interagire con l’utente.
Tkinter ha tra i suoi pregi quello di essere leggera e semplice da utilizzare. Magari non ha tutte le funzionalità presenti in librerie grafiche come le Qt o le Gtk, ma per piccoli programmi non troppo complessi, può dare molte soddisfazioni, sopratutto perché, con poche righe di codice, si può dar vita alla nostra applicazione.
Prima di passare all’analisi del loop principale del programma, vorrei farvi vedere una parte fondamentale che ha risolto il problema della visualizzazione su schermi troppo piccoli (cioè con bassa risoluzione).
class AutoScrollbar(Scrollbar): """ Una scrollbar che si nasconde se non necessaria. Funziona solo se si usa la grid geometry. fonte : http://effbot.org/zone/tkinter-autoscrollbar.htm """ def set(self, lo, hi): if float(lo) <= 0.0 and float(hi) >= 1.0: # grid_remove è attualmente assente da Tkinter. # Il metodo tk.call viene infatti richiamato sull'oggetto # scrollbar self.tk.call("grid", "remove", self) else: self.grid() Scrollbar.set(self, lo, hi) def pack(self, **kw): raise TclError, "Non si può utilizzare pack con questo widget" def place(self, **kw): raise TclError, "Non si può utilizzare place con questo widget"
La classe AutoScrollbar permette di nascondere una barra di scorrimento se non è necessaria, ovvero quando la finestra è più piccola dello schermo che la visualizza. Questo piccolo trucco insieme ad un altro che vedremo nel main, ci permetterà di visualizzare agevolmente il nostro programma anche su diverse risoluzioni che non abbiamo previsto.
Questo problema si era inizialmente presentato poiché non avevo diviso l’editor dall’emulatore vero e proprio. Inoltre le proporzioni dei vari oggetti non erano le stesse presenti nell’ultima versione e quindi occupavano molto più spazio di quello che attualmente potete vedere nella versione finale del programma.
Ora vediamo il loop principale dell’applicazione, che si preoccuperà di renderizzare la GUI.
def run(): """ Esegue l'emulatore del pdp8 """ CD = pdp8() principale = Tk() principale.title("Pdp8 Emulator : Assembly Editor") emulatore = Toplevel() emulatore.title("Pdp8 Emulator") emulatore.geometry("1015x589") edit = Editor(principale, CD) scrollbar1 = AutoScrollbar(emulatore) scrollbar1.grid(row = 0, column = 1, sticky = N+S) scrollbar2 = AutoScrollbar(emulatore, orient = HORIZONTAL) scrollbar2.grid(row = 1, column = 0, sticky = E+W) finestra = Canvas (emulatore, yscrollcommand = scrollbar1.set, xscrollcommand = scrollbar2.set) finestra.grid(row = 0, column = 0, sticky = N+S+E+W) scrollbar1.config(command = finestra.yview) scrollbar2.config(command = finestra.xview) emulatore.grid_rowconfigure(0, weight = 1) emulatore.grid_columnconfigure(0, weight = 1) emul = Emulatore(finestra,edit,CD,emulatore) finestra.create_window(0, 0, anchor = NW, window = emul.master) emul.master.update_idletasks() finestra.config(scrollregion = finestra.bbox("all")) principale.protocol("WM_DELETE_WINDOW", edit.exit) emulatore.protocol("WM_DELETE_WINDOW", emul.exit) principale.mainloop() emulatore.mainloop() if __name__ == '__main__': exit(run())
- CD rappresenta il nostro “calcolatore virtuale”, creato con la classe pdp8 che abbiamo visto nei precedenti articoli.
- Principale è la finestra madre del programma e conterrà l’editor, come potete notare dal nome (title) che le diamo.
- Emulatore sarà una finestra che visualizzerà l’emulatore. Come potete vedere è stata creata con toplevel, così avrà le stesse proprietà di un Frame ma non sarà quello principale, che invece è Tk().
I Frame sono dei contenitori che racchiudono widgets in base a certe regole. Per avere un’idea del funzionamento non c’è esempio più lampante di una scatola (il nostro Frame) da riempire (con dei widgets). Pensate di inserire i vari componenti grafici che volete utilizzare per il vostro programma in questa scatola: potete decidere la posizione, l’orientamento, come sono collegati tra loro e alcune loro proprietà. Alcuni widgets possono essere anche dei frame per contenere altri oggetti, quindi si possono inserire scatole più piccole all’interno di quella principale.
Per unificare il tutto ci sono principalmente due metodi di “impacchettazione” (più precisamente geometry manager). Pack è il più semplice e non esce dalla logica con cui abbiamo ragionato prima: ci sono dei frame (contenitori) e noi scegliamo dove gli oggetti vengono messi. Non abbiamo però il pieno controllo di come verrano disposti sullo schermo, quindi è molto difficile raggiungere l’effetto desiderato tramite questo manager.
L’altra soluzione è grid: anche se all’inizio può sembrare un pò dispersivo, ora è il più utilizzato, poiché è molto più versatile di pack. Pensate infatti al vostro frame come una mappa in due dimensioni, dove ogni oggetto occupa una precisa coordinata. In questo modo si dividerà la finestra in celle che hanno per coordinate la colonna e la riga di quel settore e noi posizioneremo i vari oggetti in base a queste coordinate. Gli oggetti potranno occupare più celle in base alle indicazioni da noi date.
Potete vedere infatti come le scrollbar della finestra dell’emulatore (scrollbar1 e scrollbar2), sono state posizionate una nella prima riga e seconda colonna (gli indici partono come negli array, da 0), mentre l’altra nella seconda riga e prima colonna ed entrambe si estendono per tutta la lunghezza della finestra: la scrollbar verticale in verticale (N+S) e quella orizzontale in orizzontale (E+W).
Nella cella [0,0] di emulatore sarà presente “finestra”, che occupa tutto lo spazio (sticky = N+S+E+W) per rappresentare l’emulatore vero e proprio (con tutti i suoi widgets). Per rendere la finestra scorrevole si utilizza il widget Canvas, il quale ci permette di renderizzare l’emulatore al suo interno, così da poterlo scorrere senza spostare i widgets all’interno del frame. Per rendere poi la finestra sensibile al variare delle dimensioni si agirà tramite grid sul peso delle colonne e delle righe (rowconfigure e columnconfigure). Per concludere si dovranno “incatenare” gli eventi del Canvas con quelli del frame dell’emulatore (create_window e master.update_idletasks) e selezionare quindi la regione da aggiornare (finestra.config(scrollregion = …)), in questo caso tutta la finestra.
C’è da sottolineare che così facendo non avremo degli oggetti ridimensionabili, ma solo una finestra scorribile per visualizzare correttamente il programma su risoluzioni più basse di quelle previste. Non prevedo il ridimensionamento degli oggetti solo perché alcuni widgets (per come ho scritto il programma) funzionano solo con determinate grandezza (per esempio l’output incolonnato). Prevedere questo tipo di situazioni è più complicato e difficile da gestire con tkinter, ma dipende sempre da quello che state scrivendo.
Per concludere, leghiamo gli eventi di chiusura delle finestre (tramite i protocol, ovvero interazione tra finestra e applicazione) a dei metodi delle classi dell’emulatore e dell’editor. Infine richiamiamo i loop di entrambe le finestre per renderizzare il tutto.
Conclusioni
Per comprendere bene il funzionamento di grid basta fare qualche prova, sicuramente le cose saranno più chiare quando analizzeremo l’editor e l’emulatore. Per ora è bene prendere confidenza con questi concetti, poiché una volta acquisiti sarà molto semplice utilizzarli per i propri fini.