Vai al contenuto

GUI complete

In questo capitolo cercheremo di introdurre tutti quegli elementi grafici che implementano una GUI moderna:

  • le azioni
  • la barra dei menù
  • la barra degli strumenti
  • la barra di stato
  • il menù contestuale
  • le impostazioni delle applicazioni

Skeleton Full GUI

Lo Skeleton è un file che contiene la struttura di base dalla quale partire per costruire qualcosa.

Lo skeleton qui sotto descrive chiaramente una struttura da cui partire per alloggiare in maniera chiara e ben strutturata tutti gli elementi di una app Full GUI, ovvero comprensiva di azioni, menù, barre degli strumenti, etc...

Skeleton Full GUI
import wx

APP_NAME = "Il nome della vostra App"

class Finestra(wx.Frame):

    def __init__(self):
        super().__init__(None, title=APP_NAME)

        # Spazio per le variabili membro della classe


        # Chiamata alle funzioni che generano la UI
        self.creaMenubar()
        self.creaToolbar()

        # la statusbar è semplice...
        self.bar = self.CreateStatusBar()

        # Chiamata alla funzione che genera la MainView
        self.creaMainView()

        # le ultime cose, ad esempio, i Bind, le impostazioni iniziali, etc...
        self.Bind(wx.EVT_CLOSE, self.funzioneEsci)

        # chiamata alla funzione di caricamento impostazioni
        self.loadConfig()
        return

    # in questa funzione andremo a creare e popolare la menubar
    def creaMenubar(self):
        return

    # in questa funzione andremo a creare e popolare la toolbar
    def creaToolbar(self):
        return

    # questa funzione implementa la vista principale del programma
    def creaMainView(self):
        return

    def loadConfig(self):
        config = wx.FileConfig(APP_NAME)

        # dimensione
        w = int(config.Read("width", "800")) # 678 è la larghezza INIZIALE
        h = int(config.Read("height", "600")) # 432 è la altezza  INIZIALE
        self.SetSize(w,h)

        # posizione
        x = int(config.Read("x", "-1")) 
        y = int(config.Read("y", "-1")) 
        if (x,y) == (-1,-1):   # se la posizione è (-1,-1) lo metto al centro!
            self.Centre()
        else:
            self.Move(x,y)
        return

    def saveConfig(self):
        config = wx.FileConfig(APP_NAME)

        # dimensione
        (w,h) = self.GetSize()
        config.Write("width", str(w))
        config.Write("height", str(h))

        # posizione
        (x,y) = self.GetPosition()
        config.Write("x", str(x))
        config.Write("y", str(y))
        return

    def funzioneEsci(self,evt):
        self.saveConfig()
        self.Destroy()
        return

# ----------------------------------------
if __name__ == "__main__":
    app = wx.App()
    app.SetAppName(APP_NAME)
    window = Finestra()
    window.Show()
    app.MainLoop()

Ok... abbiamo una struttura di base come ottimo punto di partenza per creare una applicazione con tutti gli oggetti grafici di base!

Vediamo pezzo per pezzo come integrare cose in essa!

Azioni predefinite

In quanti modi diversi si può fare copia e incolla?

Con la combinazione CTRL + C... con l'icona copia sulla barra degli strumenti, con il menù del tasto destro, con la barra in alto... Mi vengono in mente almeno 4 metodi. Probabilmente ne esistono anche altri.

Adesso pensate un momento come foste veri programmatori:

Ma... per ognuna di queste modalità occorre reimplementare un oggetto grafico e una funzione evento???

Fortunatamente la risposta a quest'ultima domanda è no!

In questo contesto rientra un concetto tipico della programmazione grafica: il concetto di azione. E cosa è un'azione?

Una azione è un'astrazione di una funzionalità che il nostro programma vuole offrire all'utente.
Viene identificata univocamente tramite:

  • un nome (ad esempio: COPIA)
  • una descrizione (ad esempio, COPIA il CONTENUTO NEGLI APPUNTI)
  • un'icona (l'icona COPIA)
  • una scorciatoia (CTRL + C)

La libreria wxPython per assicurare uniformità nelle azioni più comuni ha pensato bene di identificarle tramite degli ID fissi: ad esempio l'ID per l'azione SALVA si chiama wx.ID_SAVE.

Ecco l'elenco completo degli ID delle azioni comuni in wxPython:

Elenco ACTION ID Disponibili
wx.ID_ABOUT wx.ID_EDIT wx.ID_JUMP_TO wx.ID_PRINT wx.ID_STRIKETHROUGH
wx.ID_ADD wx.ID_EXECUTE wx.ID_JUSTIFY_CENTER wx.ID_PROPERTIES
wx.ID_APPLY wx.ID_EXIT wx.ID_JUSTIFY_FILL wx.ID_TOP
wx.ID_JUSTIFY_LEFT wx.ID_REDO
wx.ID_BACKWARD wx.ID_FILE wx.ID_JUSTIFY_RIGHT wx.ID_REFRESH wx.ID_UNDELETE
wx.ID_BOLD wx.ID_FIND wx.ID_REMOVE wx.ID_UNDERLINE
wx.ID_BOTTOM wx.ID_FIRST wx.ID_LAST wx.ID_REPLACE wx.ID_UNDO
wx.ID_FLOPPY wx.ID_REVERT_TO_SAVED wx.ID_UNINDENT
wx.ID_CANCEL wx.ID_FORWARD wx.ID_NETWORK wx.ID_UP
wx.ID_CDROM wx.ID_NEW wx.ID_SAVE
wx.ID_CLEAR wx.ID_HARDDISK wx.ID_NO wx.ID_SAVEAS wx.ID_YES
wx.ID_CLOSE wx.ID_HELP wx.ID_SELECTALL
wx.ID_CONVERT wx.ID_HOME wx.ID_OK wx.ID_SELECT_COLOR wx.ID_ZOOM_100
wx.ID_COPY wx.ID_OPEN wx.ID_SELECT_FONT wx.ID_ZOOM_FIT
wx.ID_CUT wx.ID_INDENT wx.ID_SORT_ASCENDING wx.ID_ZOOM_IN
wx.ID_INDEX wx.ID_PASTE wx.ID_SORT_DESCENDING wx.ID_ZOOM_OUT
wx.ID_DELETE wx.ID_INFO wx.ID_PREFERENCES wx.ID_SPELL_CHECK
wx.ID_DOWN wx.ID_ITALIC wx.ID_PREVIEW wx.ID_STOP

A breve vedremo anche come utilizzarle in maniera più che semplice!!!

Documentazione ufficiale classi MenuBar, Menu, MenuItem

wx.MenuBar: Classe per implementare la barra dei Menù.
wx.Menu: Classe per implementare un Menù.
wx.MenuItem: Classe per implementare un elemento di Menù

I menù sono oggetti grafici che tutti conosciamo e a cui tutti siamo abituati, non c'è bisogno di fare grandi introduzioni!

Per aggiungere una menubar:

  1. lavoriamo nella funzione creaMenubar dello skeleton.
  2. prima creiamo un oggetto Menubar
  3. poi creiamo ogni menù che andremo ad aggiungervi (es: File, Modifica, Visualizza, etc...)
  4. aggiungiamo le azioni necessarie ai nostri menù
  5. aggiungiamo i menù creati alla menubar
  6. applichiamo la menubar alla nostra applicazione

Vediamo degli esempi di codice:

Esempi di inserimento voci di menù
# creiamo un oggetto Menubar
mb = wx.MenuBar()

# crea un menù...
fileMenu = wx.Menu()

# POI aggiungi alcune azioni...
fileMenu.Append(wx.ID_NEW)
fileMenu.Append(wx.ID_OPEN)

# altro esempio: inserimento DIRETTO di azione predefinita con TESTO e DESCRIZIONE personalizzati
fileMenu.Append(wx.ID_SAVE, "Salva bene :)", "Salva il documento corrente")

# riga di separazione: serve solo come abbellimento
fileMenu.AppendSeparator()

# creazione di un menuItem da azione predefinita, inserimento icona, aggiunta al menù
closeItem = wx.MenuItem(fileMenu, wx.ID_CLOSE)
closeItem.SetBitmap(wx.ArtProvider.GetBitmap(wx.ART_CLOSE))
fileMenu.Append(closeItem)

# altro menù...
toolsMenu = wx.Menu()

# creazione di una azione personalizzata con ID=35
customItem = wx.MenuItem(toolsMenu, 35, "Fai qualcosa")
toolsMenu.Append(customItem)

# CHECK ITEM
fullScreenItem = wx.MenuItem(toolsMenu, id=100, text="FullScreen", kind=wx.ITEM_CHECK)
toolsMenu.Append(fullScreenItem)

# PENULTIMA COSA: aggiungi i menù alla menubar
mb.Append(fileMenu, '&File') # (La & prima della F di File attiva la scorciatoia ALT + F)
mb.Append(toolsMenu, '&Tools')

# INFINE!!!
self.SetMenuBar(mb)

Se avete sistemato tutto al posto giusto, dovreste ottenere qualcosa di simile a questo:


image


Per collegare le azioni create ad una funzione (Binding) va intercettato l'evento wx.EVT_MENU abbinato all'ID della voce di menù in questione:

# per fare Bind dell'azione con ID = wx.ID_EXIT ad una funzione chiamata funzioneEsci
self.Bind(wx.EVT_MENU, self.funzioneEsci, id=wx.ID_EXIT)

# per fare Bind dell'azione con ID = 35 ad una funzione chiamata faiQualcosa
self.Bind(wx.EVT_MENU, self.faiQualcosa, id=35)

Toolbar

Documentazione ufficiale wx.ToolBar

Classe grafica per implementare una barra degli strumenti

Se le barre dei menù dovrebbero permettere l'accesso a tutte le funzioni disponibili in una applicazione, le barre degli strumenti dovrebbero permettere l'accesso alle azioni veloci, ovvero a quelle di maggior utilizzo per gli utenti.

Per aggiungere una Toolbar alla nostra Frame Widget dobbiamo utilizzare la funzione CreateToolBar a cui poi potremo aggiungere le azioni che ci interessano.

toolbar = self.CreateToolBar()

# toolbar.AddTool(...e qui si aggiungono le azioni una ad una...)
toolbar.AddTool(wx.ID_OPEN, "Apri",  wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN))
toolbar.AddTool(wx.ID_SAVE, "Salva", wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE))

# un separatore: come abbellimento e per vedere come si fa :)
toolbar.AddSeparator()

toolbar.AddTool(wx.ID_EXIT, "Esci",  wx.ArtProvider.GetBitmap(wx.ART_QUIT) )


# riempita la toobar, va eseguito il metodo Realize()
toolbar.Realize()

A questo punto, se volete collegare i pulsanti della Toolbar ad una funzione, basta eseguire il solito Bind() con l'evento wx.EVT_TOOL.

self.Bind(wx.EVT_TOOL, self.funzioneEsci, id = wx.ID_EXIT )
self.Bind(wx.EVT_TOOL, self.funzioneApri, id = wx.ID_OPEN )

Attenzione!

L'evento wx.EVT_TOOL è automaticamente abbinato ad un evento wx.EVT_MENU e viceversa. Questo significa che se, ad esempio, avete già implementato tutte le vostre azioni nella MenuBar e fatto il Binding con i loro ID con le opportune funzioni, tutte le azioni che aggiungerete alla toolbar con ID già utilizzati funzioneranno automaticamente senza bisogno di un ulteriore binding!!!

Vediamo una semplicissima dimostrazione di ciò con una finestra avente una sola azione, presente sia nella menubar che nella toolbar.

# Esempio banale con una sola azione su menu e toolbar, UN UNICO BINDING
import wx

class Esempio(wx.Frame):

    def __init__(self):
        super().__init__(None, title="Stessa azione: menu & toolbar")

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fileItem = fileMenu.Append(wx.ID_EXIT)
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.esci, id=wx.ID_EXIT)

        toolbar = self.CreateToolBar()
        toolbar.AddTool( wx.ID_EXIT, "ESCI", wx.ArtProvider.GetBitmap(wx.ART_QUIT) )
        toolbar.Realize()

    def esci(self, event):
        self.Close(True)
        return

# ----------------------------------------
if __name__ == "__main__":
    app = wx.App()
    window = Esempio()
    window.Show()
    app.MainLoop()

wx.StatusBar

Documentazione ufficiale wx.StatusBar

Classe grafica per implementare una barra di stato

La classe wx.StatusBar rappresenta una widget che implementa la barra di stato delle applicazioni.

image

E' possibile creare una barra di stato in due modi: o dichiarando un oggetto di tipo wx.StatusBar e poi inserendolo nella finestra tramite il metodo SetStatusBar() oppure chiamando direttamente dalla finestra il metodo CreateStatusBar(). Se dovete solo visualizzare informazioni il secondo metodo è una bomba! Se dovete modificare la StatusBar aggiungendovi widget e icone serve il primo metodo, eventualmente creando una classe derivata da wx.StatusBar.

Nell'esempio proposto si crea automaticamente una StatusBar e si visualizza la posizione del puntatore non appena questo entra nella finestra.

import wx

class Esempio(wx.Frame):

    def __init__(self):
        super().__init__(None, title="Muovi il mouse sopra la finestra")        
        self.bar = self.CreateStatusBar()
        self.Bind(wx.EVT_MOTION, self.controllaMouse)

    def controllaMouse(self, event):
        pos = event.GetPosition()
        info = "x: " + str(pos[0]) + " y: " + str(pos[1])
        self.bar.SetStatusText(info)
        return

# ----------------------------------------
app = wx.App()
window = Esempio()
window.Show()
app.MainLoop()

Context Menu

I Context Menus ovvero i menù contestuali sono quei menù che appaiono quando si fa click con il tasto destro in determinate posizione della nostra applicazione. Risulta chiaro a mio avviso che i Context Menu dipendono dalla widget sopra la quale si fa click con il tasto destro. Un immagine vale più di 1000 parole:

image

Per capire come si può implementare un Context Menu tramite la libreria wxPython proviamo ad implementarne uno su una widget con dentro un check item che abilita o disabilita la toolbar, rendendo possibile in quest'ultimo caso utilizzare i suoi strumenti.

import wx

class Esempio(wx.Frame):

    def __init__(self):
        super().__init__(None, title="Context Menu")

        self.toolbar = self.CreateToolBar()        
        exitTool = self.toolbar.AddTool( wx.ID_EXIT, "ESCI", wx.ArtProvider.GetBitmap(wx.ART_QUIT) )
        self.toolbar.Realize()

        self.Bind(wx.EVT_TOOL, self.esci, exitTool)

        # serve ad attivare il Context Menu
        self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)

        # Crea il menù, pronto per essere attivato!
        self.contextMenu = wx.Menu()
        self.fsItem = wx.MenuItem(self.contextMenu, id=100, text="Toolbar abilitata", kind=wx.ITEM_CHECK)
        self.contextMenu.Append(self.fsItem)
        self.contextMenu.Check(100, True)
        self.Bind(wx.EVT_MENU, self.ManageToolBar, self.fsItem)

    def esci(self, event):
        self.Close(True)
        return

    def OnRightDown(self, event):
        self.PopupMenu(self.contextMenu, event.GetPosition())

    def ManageToolBar(self,event):
        if self.fsItem.IsChecked():
            self.toolbar.Enable(True)
        else:
            self.toolbar.Enable(False)

# ----------------------------------------
app = wx.App()
window = Esempio()
window.Show()
app.MainLoop()

Il risultato di questo codice è il seguente:

image

Spero che osservare e riprodurre l'esempio sia sufficiente per capire il funzionamento :)

Impostazioni

Documentazione ufficiale wx.FileConfig

Classe per la gestione delle configurazioni utente di una applicazione

Può essere interessante e/o addirittura necessario per una applicazione salvare da qualche parte alcune impostazioni da ricaricare al prossimo riavvio. Ad esempio potrebbe essere interessante tracciare la posizione e la dimensione della finestra principale nello schermo, per riproporla identica... oppure nel caso di una applicazione che permette ad esempio di cambiare la dimensione del font, di memorizzare questa impostazione e riproporla anche nelle successive esecuzioni, senza dover ogni volta costringere l'utente a cambiarla di nuovo.

Certo siete già in grado di fare questo tipo di lavoro salvando ad esempio su file di testo queste informazioni... ma dove lo mettiamo questo file per essere ragionevolmente sicuri di ritrovarlo alla prossima esecuzione? A tutte queste domande risponde con semplicità disarmante la classe wx.FileConfig che deriva dalla classe astratta wx.ConfigBase (uhm... forse non sapete cosa significa astratta... pazienza!!!).

La classe wx.FileConfig richiede come informazioni solo il nome dell'applicazione, poi per ogni sistema operativo sceglie il posto riservato alle impostazioni delle applicazioni e scrive lì dentro un file di testo con una sequenza di coppie VARIABILE = VALORE.

Quindi riassumendo:

STEP 1

Dare un nome alla propria App. Questo si fa nella classe App, prima di creare una finestra, con la funzione SetAppName. Ad esempio:

app = wx.App()
app.SetAppName("Ciccio")
window = Esempio()
window.Show()
app.MainLoop()
STEP 2

Dove ti serve di salvare alcune impostazioni, crea un oggetto della classe wx.FileConfig e salva i tuoi dati con la funzione Write("variabile", valore). Nell'esempio sotto proviamo a salvare la variabile "colore" che contiene il valore "red" e la variabile "dimensioneFont" che contiene il valore 18. Attenzione ad assicurarsi di salvare sempre e solo stringhe:

config = wx.FileConfig("Ciccio")
config.Write( "colore" , "red" )

# l'int va convertito in stringa
config.Write( "dimensioneFont" , str(18) )

# basta... tutto qui!
STEP 3

Quando ti serve di sapere i valori abbinati alle variabili "colore" e "dimensioneFont", che evidentemente per il tuo programma sono importanti, ti basterà andare a controllarne il valore con le funzioni Read (che restituisce sempre stringhe, quindi converti i valori se ne hai bisogno).

config = wx.FileConfig("Ciccio")

# "black" è il valore di default, viene usato se non è stato salvato nulla
colore = config.Read( "colore" , "black" ) 

# trasformo in int. 12 default.
dimensioneFont = int( config.Read( "dimensioneFont" , "12" ) ) 

Come vedete salvare le configurazioni della propria App diventa semplicissimo, a patto di saper scegliere bene i valori da salvare come impostazioni e di fare in modo che i dati da salvare siano semplicemente convertibili in stringhe.