4. Come Creare un Editor Testuale con Python Pt.3

In questa terza lezione concluderemo l’editor testuale che abbiamo creato utilizzando il modulo Tkinter della Standard Library di Python. Potete trovare il codice completo di questa lezione nel repository di GitHub.

Aggiungeremo una funzionalità che ci permetterà di mostrare un messaggio quando un file viene salvato, implementeremo le scorciatorie da tastiera per le funzionalità del sottomenù File e aggiungeremo un nuovo sottomenù che permetterà di leggere informazioni riguardo all’editor tramite pop up.


Come aggiungere le scorciatoie da tastiera

Iniziamo aggiungendo le scorciatoie da testiera aggiungendo nuovi comandi al widget di tipo Menu file_dropdown della classe Menubar.

Modifichiamo i parametri del metodo di Tkinter add_command che abbiamo utilizzato nel costruttore della classe Menubar per definire il comportamento delle voci del nostro menu a tendina. Per prima cosa aggiungiamo il parametro acceleretor per specificare una stringa da mostrare nel menù come suggerimento per le combinazione di tasti per cui faremo il binding. Assegniamo al parametro le consuete combinazioni di tasti.

class Menubar:

    def __init__(self, parent):
        # ...

        file_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        file_dropdown.add_command(label="Nuovo File",
                                  accelerator="Ctrl+N",
                                  command=parent.new_file)
        file_dropdown.add_command(label="Apri File",
                                  accelerator="Ctrl+O",
                                  command=parent.open_file)
        file_dropdown.add_command(label="Salva",
                                  accelerator="Ctrl+S",
                                  command=parent.save)
        file_dropdown.add_command(label="Salva con Nome",
                                  accelerator="Ctrl+Shift+S",
                                  command=parent.save_as)
        file_dropdown.add_separator()

Per effettuare il binding dobbiamo definire un nuovo metodo nella classe PyText: le scorciatoie da tastiera saranno infatti collegate ai suoi metodi. Definiamo in PyText il metodo bind_shortcuts e utilizziamo il metodo bind sul widget textarea passandogli come parametri la scorciatoria da tastiera e il metodo da richiamare:

class PyText:
    # ...

    def bind_shortcuts(self):
        self.textarea.bind('<Control-n>', self.new_file)
        self.textarea.bind('<Control-o>', self.open_file)
        self.textarea.bind('<Control-s>', self.save)
        self.textarea.bind('<Control-S>', self.save_as)
        self.textarea.bind('<Key>', self.statusbar.update_status)

Chiamiamo il nuovo metodo nell’inizializzatore init di PyText:

class PyText:

    def __init__(self, master):
        # ...

        self.bindshortcuts()

Se proviamo ad eseguire il codice adesso e proviamo ad utilizzare una scorciatoia da tastiera otteniamo un errore perché il metodo bind invia alla funzione di callback anche un oggetto con una serie di informazioni sul tasto che è stato premuto, quindi dobbiamo fare in modo che i nostri metodi accettino parametri extra aggiungendo agli argomenti la tupla *args, anche se in questo caso non ci servono (ma in un altro caso che vedremo a breve ci serviranno).

class PyText:
    # ...

    def new_file(self, *args):
        # ...

    def open_file(self, *args):
        # ...

    def save(self, *args):
        # ...

    def save_as(self, *args):
        # ...

Adesso se eseguiamo il codice vediamo che sarà possibile utilizzare le scorciatoie da tastiera.


Come aggiungere una barra di stato: il widget StringVar

Nell’ambito del web design quando si crea un interfaccia la visibilità dello stato del sistema è fondamentale: l’utente deve essere sempre informato su ciò che sta succedendo e se le operazioni che compie vanno a buon fine.

Per fare in modo che venga mostrato un messaggio quando un file viene salvato creiamo una nuova classe Statusbar, specifichiamo il font e definiamo un nuovo tipo di widget: StringVar.

Si tratta di una classe di Tkinter che rappresenta una variabile di tipo stringa che può essere utilizzata per aggiornare in modo dinamico il contenuto di un widget che accetta una stringa come valore, come Label, Entry o Button.

Inizializziamola con una stringa tramite il metodo set con il nome di questo editor, che chiameremo “0.1 Gutenberg”.

class Statusbar:

    def __init__(self, parent):
        font_specs = ('ubuntu', 12)

        self.status = tk.StringVar()
        self.status.set("PyText - 0.1 Gutenberg")

Definiamo anche un’etichetta con un widget di tipo Label, assegniamo un font, i colori per il font (fg) e per lo sfondo (bg) e definiamo un orientamento al testo ancorandola in basso a sinistra con il parametro anchor sul punto cardinale SW. Tramite il metodo pack facciamo in modo che l’etichetta si posizioni nella parte inferiore della schermata (side=tk.BOTTOM) espandendosi su entrambi i lati (fill=tk.BOTH).

class Statusbar:
    # ...

    label = tk.Label(parent.textarea, 
                    textvariable=self.status,
                    fg="black",
                    bg="lightgrey",
                    anchor='sw', 
                    font=font_specs)
    label.pack(side=tk.BOTTOM, fill=tk.BOTH)

Per aggiornare self.status dobbiamo definire un nuovo metodo. Il nostro obiettivo è fare in modo che la stringa "PyText - 0.1 Gutenberg" venga mostrata ogni qual volta si preme un qualsiasi tasto e "Il tuo File è stato salvato!" quando viene salvato un file. Creiamo quindi un nuovo metodo update_status da usare come callback per l’evento Key del widget TextArea, che verrà chiamato con un argomento che rappresenta il tipo di evento che è stato generato.

Come abbiamo visto prima, il metodo bind passa diversi argomenti alla funzione callback, quindi dobbiamo fare in modo che update_status li riceva come argomento usando la tupla *args.

Come facciamo a sfruttare il contenuto della tupla per capire se un file è stato salvato oppure è stato premuto un tasto qualsiasi? Quando viene premuto un tasto, viene generato l'evento <Key> del widget TextArea (tramite il binding che faremo tra poco) e l’argomento passato ad update_status sarà un oggetto di tipo tk.Event che contiene informazioni sull’evento, come il codice ASCII del tasto premuto, il nome del tasto, la coordinata del cursore, ecc. Se invece viene salvato un file, viene utilizzato il metodo save_as, che restituirà un valore booleano True se il salvataggio è andato a buon fine.

Quindi utilizziamo la funzione predefinita isinstance per controllare il tipo dell'argomento passato e decidere quale messaggio di stato visualizzare sulla barra di stato in base al contenuto della tupla *args. Se l'argomento è di tipo bool, viene visualizzato il messaggio "Il tuo File è stato salvato!" altrimenti viene visualizzato il messaggio "PyText - 0.1 Gutenberg".

class Statusbar:
    # ...

    def update_status(self, *args):
        if isinstance(args[0], bool):
            self.status.set("Il tuo File è stato salvato!")
        else:
            self.status.set("PyText - 0.1 Gutenberg")

Inanzitutto aggiungiamo al costruttore init di PyText la variabile self.statusbar per potervi accedere tramite i metodi di classe.

class PyText:

    def __init__(self, master):
        # ...

        self.statusbar = Statusbar(self)

Aggiungiamo la chiamata ad update_status nei metodi save e save_as:

def save(self, *args):
    # ...

    self.statusbar.update_status(True)


def save_as(self, *args):
    # ...

    self.statusbar.update_status(True)

Aggiungiamo il binding generico al metodo bind_shortcuts che chiami update_status alla pressione di un tasto qualsiasi:

class PyText:
    # ...

    def bind_shortcuts(self):
        # ...
        self.textarea.bind('<Key>', self.statusbar.update_status)

Ora la barra di stato è completa! Passiamo ai popup.


Come mostrare dei popup con le informazioni sull’Editor

Creiamo l’ultima componente del nostro editor: un’altra voce del menu a tendina per permetterci di visualizzare informazioni sull’editor. Aggiungiamo un widget di tipo Menu a Menubar, impostiamo il font, disabilitiamo tearoff e tramite add_command colleghiamo il metodo show_about_message che non esiste ancora ma che creeremo a breve.

class Menubar:

    def __init__(self, parent):
        # ...

        about_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        about_dropdown.add_command(label="About",
                                   command=self.show_about_message)

    def show_about_message(self):
        pass

Vogliamo che sopra il messaggio about vengano mostrate anche le Note di rilascio, quindi aggiungiamo al dunder init un ulteriore comando collegato al metodo show_release_notes (che creeremo a breve) e anche un separatore tra i due tramite il metodo add_separator del widget Menu:

class Menubar:

    def __init__(self, parent):
        # ...

        about_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        about_dropdown.add_command(label="Note di Rilascio",
                                   command=self.show_release_notes)
        about_dropdown.add_separator()
        about_dropdown.add_command(label="About",
                                   command=self.show_about_message)

    def show_about_message(self):
        pass

    def show_release_notes(self):
        pass

Tramite il metodo add_cascade (che abbiamo già visto nella prima parte del tutorial) creiamo i due sottomenu:

class Menubar:

    def __init__(self, parent):
        # ...

        about_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        
        # ...

        menubar.add_cascade(label="File", menu=file_dropdown)
        menubar.add_cascade(label="About", menu=about_dropdown)

Passiamo ora ai metodi show_about_message e show_release_notes. Per poter far apparire un popup dobbiamo aggiungere un nuovo import:

from tkinter import messagebox

Ora possiamo implementare i nostri metodi usando il metodo showinfo di messagebox che accetta due parametri: il titolo del popup e il messaggio da mostrare. Definiamo quindi titolo e messaggio per entrambe i nostri metodi e passiamoli come parametri a showinfo.

class Menubar:
    # ...

    def show_about_message(self):
        box_title = "Riguardo PyText"
        box_message = "Un semplice Editor Testuale creato con Python e TkInter!"
        messagebox.showinfo(box_title, box_message)

    def show_release_notes(self):
        box_title = "Note di Rilascio"
        box_message = "Versione 0.1 - Gutenberg"
        messagebox.showinfo(box_title, box_message)

A questo punto il nostro editor è concluso!

screenshot_editor_tk03

import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox


class Menubar: 

    def __init__(self, parent):       
        font_specs = ('ubuntu', 14)

        menubar = tk.Menu(parent.master, font=font_specs)
        parent.master.config(menu=menubar)

        file_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        file_dropdown.add_command(label="Nuovo File", 
                                  accelerator="Ctrl+N",
                                  command=parent.new_file)
        file_dropdown.add_command(label="Apri File", 
                                  accelerator="Ctrl+O",
                                  command=parent.open_file)
        file_dropdown.add_command(label="Salva", 
                                  accelerator="Ctrl+S",
                                  command=parent.save)
        file_dropdown.add_command(label="Salva con Nome", 
                                  accelerator="Ctrl+Shift+S",
                                  command=parent.save_as)
        file_dropdown.add_separator()
        file_dropdown.add_command(label="Esci", 
                                  command=parent.master.destroy)

        about_dropdown = tk.Menu(menubar, font=font_specs, tearoff=0)
        about_dropdown.add_command(label="Note di Rilascio",
                                   command=self.show_release_notes)
        about_dropdown.add_separator()
        about_dropdown.add_command(label="About",
                                   command=self.show_about_message)

        menubar.add_cascade(label="File", menu=file_dropdown)
        menubar.add_cascade(label="About", menu=about_dropdown)

    def show_about_message(self):
        box_title = "Riguardo PyText"
        box_message = "Un semplice Editor Testuale creato con Python e TkInter!"
        messagebox.showinfo(box_title, box_message)

    def show_release_notes(self):
        box_title = "Note di Rilascio"
        box_message = "Versione 0.1 - Gutenberg"
        messagebox.showinfo(box_title, box_message)


class Statusbar:

    def __init__(self, parent):
        font_specs = ('ubuntu', 12)

        self.status = tk.StringVar()
        self.status.set("PyText - 0.1 Gutenberg")

        label = tk.Label(parent.textarea, textvariable=self.status, fg="black",
                         bg="lightgrey", anchor='sw', font=font_specs)
        label.pack(side=tk.BOTTOM, fill=tk.BOTH)

    def update_status(self, *args):
        if isinstance(args[0], bool):
            self.status.set("Il tuo File è stato salvato!")
        else:
            self.status.set("PyText - 0.1 Gutenberg")

    
class PyText:
    """ PyText è un semplice Editor Testuale creato con TkInter.
    Lo scopo di PyText è quello di insegnarti l'utilizzo del modulo TkInter.
    
    Visita il file README.md per i link ai video tutorial in italiano dove
    viene spiegato nel dettaglio come creare questo programma, passo passo."""

    def __init__(self, master):
        master.title("Untitled - PyText")
        master.geometry("1200x700")

        font_specs = ('ubuntu', 18)

        self.master = master
        self.filename = None

        self.textarea = tk.Text(master, font=font_specs)
        self.scroll = tk.Scrollbar(master, command=self.textarea.yview)
        self.textarea.configure(yscrollcommand=self.scroll.set)
        self.textarea.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.scroll.pack(side=tk.RIGHT, fill=tk.Y)

        self.menubar = Menubar(self)
        self.statusbar = Statusbar(self)

        self.bind_shortcuts()   

    def set_window_title(self, name=None):
        if name:
            self.master.title(name + " - PyText")
        else:
            self.master.title("Untitled - PyText")

    def new_file(self, *args):
        self.textarea.delete(1.0, tk.END)
        self.filename = None
        self.set_window_title()

    def open_file(self, *args):
        self.filename = filedialog.askopenfilename(
            defaultextension=".txt",
            filetypes=[("Tutti i file", "*.*"),
                       ("File di Testo", "*.txt"),
                       ("Script Python", "*.py"),
                       ("Markdown Text", "*.md"),
                       ("File JavaScript", "*.js"),
                       ("Documenti HMTL", "*.html"),
                       ("Documenti CSS", "*.css")])
        if self.filename:
            self.textarea.delete(1.0, tk.END)
            with open(self.filename, "r") as f:
                self.textarea.insert(1.0, f.read())
            self.set_window_title(self.filename)

    def save(self, *args):
        if self.filename:
            try:
                textarea_content = self.textarea.get(1.0, tk.END)
                with open(self.filename, "w") as f:
                    f.write(textarea_content)
            except Exception as e:
                print(e)
        else:
            self.save_as()

    def save_as(self, *args):
        try:
            new_file = filedialog.asksaveasfilename(
                initialfile='Untitled.txt',
                defaultextension=".txt",
	            filetypes=[("Tutti i file", "*.*"),
	                       ("File di Testo", "*.txt"),
	                       ("Script Python", "*.py"),
	                       ("Markdown Text", "*.md"),
	                       ("File JavaScript", "*.js"),
	                       ("Documenti HMTL", "*.html"),
	                       ("Documenti CSS", "*.css")])
            with open(new_file, 'w') as f:
                textarea_content = self.textarea.get(1.0, tk.END)
                f.write(textarea_content)
            self.filename = new_file
            self.set_window_title(self.filename)
            self.statusbar.update_status(True)
        except Exception as e:
            print(e)

    def bind_shortcuts(self):
        self.textarea.bind('<Control-n>', self.new_file)
        self.textarea.bind('<Control-o>', self.open_file)
        self.textarea.bind('<Control-s>', self.save)
        self.textarea.bind('<Control-S>', self.save_as)
        self.textarea.bind('<Key>', self.statusbar.update_status)


if __name__ == "__main__":
    master = tk.Tk()
    pt = PyText(master)
    master.mainloop()