GtkTreeView: un breve tutorial
L’architettura di GtkTreeView è basata sul pattern noto in letteratura come Model/View/Controller (MVC, traducibile come Modello/Vista/Controllore). In base a questo approccio, il codice applicativo è suddiviso in tre moduli: il modello è la struttura dati che rappresenta i dati gestiti dall’applicazione e la logica di gestione (business rules); la vista implementa la visualizzazione dei dati e l’interazione con l’utente; il controllore implementa la logica che fa comunicare i due layer.
A prima vista tutto ciò può sembrare inutilmente complicato, ma in applicativi non banali questo approccio semplifica la struttura del programma. Pensiamo, per esempio, ad un editor per programmatori: lo stesso codice può essere visualizzato in più finestre contemporaneamente, e le modifiche effettuate in una finestra devono riflettersi nelle altre. Perciò, è opportuno separare la visualizzazione del testo dalla sua visualizzazione, anziché memorizzare il testo separatamente con ogni finestra. Un altro esempio lo troviamo in un qualsiasi file manager moderno: se ho due o più finestre aperte sulla stessa directory, ogni operazione di creazione, cancellazione o ridenominazione di file compiuta in una finestra si riflette immediatamente nell’altra.
Nel caso dei widget Gtk, il modello è implementato dalle classi che implementano l’interfaccia GtkTreeModel, la vista dal widget GtkTreeView ed il controllore dal codice applicativo scritto dallo sviluppatore.
Il modello: GtkTreeModel e GtkListStore
La classe GtkTreeModel definisce l’interfaccia per il componente modello dell’architettura di GtkTreeView. La libreria mette a disposizione le due strutture dati più comuni: una lista lineare implementata da GtkListStore, e un albero gerarchico implementato con GtkTreeStore. Non ho molta esperienza con la struttura ad albero, quindi nel seguito discuterò solo GtkListStore.
Alla classe principale che implementa la memorizzazione dei dati si affiancano classi di utilità per gestire operazioni come la ricerca, l’ordinamento e la scansione della lista.
Creare la struttura dati
Possiamo descrivere un GtkListStore come una tabella costituita da righe e colonne. La prima cosa da fare è creare la struttura dati richiamando il costruttore della classe, specificando il tipo di dati che ogni colonna dovrà contenere. Possiamo usare tutti i tipi predefiniti di Python (int
, str
, long
, float
e object
), e i tipi definiti dalle librerie Gtk+ e GObject. Ad esempio, il codice seguente definisce una lista nella quale ciascuna riga contiene due stringhe, un valore booleano, un oggetto Python (potrà essere una lista, un dizionario, o una classe definita dal programmatore) ed una immagine (gtk.gdk.Pixbuf
):
model = gtk.ListStore (str, str, bool, object, gtk.gdk.Pixbuf)
Una volta definita la struttura dati, non potrà più essere modificata.
Accesso ai dati del modello
Per identificare e manipolare le righe della lista abbiamo a disposizione due tipi di oggetti: percorsi (Tree Paths) e iteratori (GtkTreeIter).
Un percorso rappresenta in modo univoco una singola riga del modello sotto forma di intero, stringa o tupla. Nel caso di una lista lineare, per rappresentare una riga è sufficiente un singolo numero intero che identifica la posizione della riga nella lista: il numero 2
, la stringa '2'
e la tupla (2,)
sono equivalenti e identificano la terza riga della lista (il conteggio parte da zero). Nel caso degli alberi, invece, si usa una sequenza di numeri che riproducono la posizione gerarchica della riga: la tupla (0,3,1)
e la stringa '0:3:1'
identificano il secondo figlio del quarto figlio del nodo radice.
Un iteratore è un oggetto temporaneo che si comporta come una sorta di puntatore ad una riga del modello. Temporaneo significa che il riferimento fornito dall’oggetto perde di validità nel momento in cui il modello viene modificato aggiungendo o rimuovendo righe.
# Otteniamo un puntatore specificando il percorso della riga
# (in questo caso la quarta riga del modello)
# Se il path non è valido viene sollevata una eccezione
iter = model.get_iter (path=(3,))
# Otteniamo un puntatore alla prima riga del modello
# equivalente a iter = model.get_iter (path=(0,))
iter = model.get_iter_first()
# Scansione riga per riga di tutto il modello
iter = model.get_iter_first()
while iter:
# Facciamo qualcosa con l'iteratore...
iter=model.iter_next(iter) # Restituisce None quando arriva all'ultima riga
Per accedere ai dati contenuti nella riga si usa il metodo get_value
, che richiede l’indice della colonna da leggere (sempre partendo da 0). È pratica comune usare delle costanti per assegnare nomi significativi agli indici:
COL_NOME, COL_COGNOME, COL_ATTIVO, COL_DATI, COL_FOTO = (0, 1, 2, 3, 4)
# range(5) sarebbe stato equivalente
iter = model.get_iter_first ()
while iter:
nome = model.get_value (iter, COL_NOME)
cognome = model.get_value (iter, COL_COGNOME)
attivo = model.get_value (iter, COL_ATTIVO)
print nome, cognome, attivo
iter=model.iter_next(iter)
Aggiungere righe al modello
I metodi per aggiungere righe al modello ricevono in input una tupla o una lista (inteso come oggetto Python) che contiene i dati da inserire e restituiscono un iteratore che punta alla riga appena inserita. È anche possibile inserire una riga vuota e riempirla in seguito.
# Inserisce una riga in fondo alla lista
iter = model.append (['Antonio', 'Rossi', True, None, None])
# Inserisce una riga all'inizio della lista
iter = model.prepend (['Francesco', 'Bianchi', True, None, None])
# Inserisce una riga nella posizione specificata
# (la terza, in questo caso, ricordarsi che il conteggio parte da 0)
iter = model.insert (2, ['Silvio', 'Verdi', False, None, None])
# Inserisce una riga prima o dopo una riga data
# gli altri iteratori (other_iter e other_iter_1) vengono invalidati
# Nel secondo esempio inseriamo una riga vuota per modificarla in seguito
# col metodo set_value
iter = model.insert_before (other_iter, ['Fabrizio', 'Grigi', False, None, None])
iter = model.insert_after (other_iter_1, None)
model.set_value (iter, COL_NOME, 'Mario')
model.set_value (iter, COL_COGNOME, 'Gialli')
model.set_value (iter, COL_ATTIVO, True)
model.set_value (iter, COL_DATI, None)
model.set_value (iter, COL_FOTO, None)
# !!!ERRORE!!!
# L'iteratore other_iter è stato invalidato dalla precedente chiamata a insert_before
# Il risultato della riga successiva è indefinito.
model.set_value (other_iter, COL_NOME, 'Luca')
Rimuovere righe
Per rimuovere una riga dal modello si usa il metodo remove
; per svuotare completemente il modello il metodo clear
:
# Rimuoviamo la seconda riga
iter = model.get_iter (path = (1,))
model.remove (iter)
# Svuotiamo il modello
model.clear ()
Ordinamento
Con il metodo set_sort_column_id
possiamo determinare in che ordine avviene la scansione della lista (di default, la lista non è ordinata e la scansione avviene nell’ordine nel quale le righe sono state inserite).
# Impostiamo l'ordinamento crescente in base al valore della seconda colonna
model.set_sort_column_id (COL_COGNOME, gtk.SORT_ASCENDING)
# Impostiamo l'ordinamento decrescente in base al valore della prima colonna
model.set_sort_column_id (COL_NOME, gtk.SORT_DESCENDING)
La vista: GtkTreeView, GtkTreeViewColumn e GtkCellRenderer
Il widget GtkTreeView rappersenta la vista nell’approccio Modello/View/Controller. Si occupa, quindi, di mostrare all’utente i dati contenuti in un modello (GtkTreeStore o GtkListStore) e di gestire l’interazione con l’utente stesso. Si possono avere più viste per un singolo modello, ed i cambiamenti al modello si rifletteranno immediatamente in tutte.
GtkTreeView è, in effetti, un contenitore di oggetti GtkTreeViewColums, che implementano le colonne della vista, le quali a loro volta contengono uno o più oggetti GtkCellRenderer, che sono quelli che effettivamente visualizzano i dati.
Per creare la nostra vista dobbiamo, per prima cosa, creare il TreeView e associare al widget il nostro modello dati:
# Creazione di un oggetto GtkTreeView
treeview = gtk.TreeView(model)
# Oppure, se abbiamo sviluppato la GUI della nostra applicazione con glade,
# otteniamo il riferimento al widget dal file di glade e gli associamo il
# modello dati col metodo set_model
ui = gtk.glade.XML ('example.glade')
treeview = ui.get_widget ('treeview1')
treeview.set_model (model)
Successivamente, dobbiamo creare le “celle” (GtkCellRenderer) e impostare le relative proprietà (tra le quali, quella principale è il campo del modello dal quale leggere i dati da visualizzare). La libreria Gtk+ mette a disposizione diversi tipi di CellRenderer:
- GtkCellRendererText
- Utilizzabile per visualizzare stringhe di testo (i numeri vengono automaticamente convertiti). Le proprietà principali da collegare al modello sono `text` per visualizzare testo non formattato e `markup` per il testo formattato (si veda Pango Text Attribute Markup Language per informazioni sul markup).
- GtkCellRendererPixbuf
- Utilizzabile per visualizzare immagini o icone. Le proprietà principali sono `pixbuf` (da usare se l’immagine da mostrare è memorizzata in un oggetto `gtk.gdk.Pixbuf`) e `stock-id` (se l’immagine è una delle icone predefinite di Gtk+).
- GtkCellRendererToggle
- Utilizzabile per visualizzare un valore booleano sotto forma di pulsante di opzione. La proprietà principale è `active`.
- GtkCellRendererProgress
- Usata per visualizzare numeri percentuali sotto forma di progress bar (barra di avanzamento).
Nel TreeView della figura sopra, la prima colonna contiene un GtkCellRendererPixbuf, la seconda e la terza un GtkCellRendererText e la quarta e ultima un GtkCellRendererToggle.
Oltre alle proprietà relative ai dati sopra descritte, le celle possiedono altre proprietà tramite le quali impostare colori, tipi di carattere, dimensioni e altro. Si consulti la documentazione di riferimento per i dettagli.
Vediamo come creare le celle ed associarle alle colonne:
# Creazione del CellRenderer testuale
cell = gtk.CellRendererText ()
# Creazione della colonna del TreeView, impostando
# il titolo che compare nell'intestazione
column = gtk.TreeViewColumn (title="Nome")
# Inseriamo la cella nella colonna
column.pack_start (cell)
# Associamo la cella al campo dati da visualizzare
column.set_attributes (cell, text = COL_NOME)
# Aggiungiamo la colonna al TreeView
self.treeview.append_column (column)
# Le stesse operazioni si possono eseguire col
# seguente codice più compatto
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Cognome", cell, text = COL_COGNOME)
self.treeview.append_column (column)
cell = gtk.CellRendererToggle ()
column = gtk.TreeViewColumn ("Attivo", cell, active = COL_ATTIVO)
self.treeview.append_column (column)
cell = gtk.CellRendererPixbuf ()
column = gtk.TreeViewColumn ("Foto", cell, pixbuf = COL_FOTO)
self.treeview.append_column (column)
È possibile che la semplice associazione cella/campo dati non sia sufficiente per le specifiche del nostro programma. In questi casi, possiamo associare ad ogni cella una funzione callback che viene chiamata dalla libreria e nella quale possiamo modificare dinamicamente le proprietà della cella. La funzione si imposta col metodo set_cell_data_func
e riceve come parametri la colonna del TreeView che contiene la cella, la cella stessa, il modello dati, un iteratore che identifica la riga del modello interessata, ed eventuali dati custom.
Supponiamo di voler visualizzare in colore rosso ed in grassetto il campo Nome per le righe del modello che hanno il campo booleano Attivo impostato al valore True
:
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Nome", cell, text = COL_NOME)
column.set_cell_data_func (cell, cell_data_function, None)
self.treeview.append_column (column)
def cell_data_function (column, cell, model, iter, data=None):
active = model.get_value (iter, COL_ATTIVO)
if active:
nome = model.get_value (iter,0)
cell.set_property ('foreground', 'red')
cell.set_property ('markup', '<b>' + nome + '</b>')
else:
cell.set_property ('foreground', 'black')
Gestione della selezione
Un GtkTreeView può essere impostato per consentire di selezionare una (selezione singola) o più (selezione multipla) righe col mouse. La classe che gestisce le selezioni è GtkTreeSelection.
treeselection = treeview.get_selection()
#Impostazione della selezione singola ()
treeselection.set_mode (gtk.SELECTION_SINGLE)
#Impostazione della selezione multipla ()
treeselection.set_mode (gtk.SELECTION_MULTIPLE)
Se abbiamo impostato la selezione singola, il metodo get_selected
restituisce una tupla che contiene il modello dati ed un iteratore che identifica la riga selezionata.
# Esempio: cancelliamo dal modello la riga selezionata nella vista
model, iter = treeselection.get_selected ()
if iter:
model.remove (iter)
# Esempio aggiungiamo al modello una riga dopo quella selezionata
# oppure come ultima se nessuna riga è selezionata
model, iter = treeselection.get_selected ()
if iter:
newiter = model.insert_after (iter, newrow)
else:
newiter = self.model.append (newrow)
# Possiamo impostare da programma la selezione col metodo
# select_iter
treeselection.select_iter (newiter)
Se invece abbiamo impostato la selezione multipla, dobbiamo usare il metodo get_selected_rows
che restituisce il modello e la lista (intesa come struttura dati Python) dei Tree Paths che identificano le righe selezionate:
# Esempio: cancelliamo dal modello le righe selezionate nella vista
model, pathlist = treeselection.get_selected_rows()
if pathlist:
for rowpath in pathlist:
iter = model.get_iter (path=rowpath)
model.remove (iter)
Altre funzionalità del GtkTreeView
Nelle GUI alle quali siamo abituati è usuale avere la possibilità di ordinare una lista in base al valore di un campo, cliccando sull’intestazione della colonna. Per implementare questa funzionalità in GtkTreeView, è sufficiente usare il metodo set_sort_column_id
della colonna:
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Nome", cell, text = COL_NOME)
column.set_sort_column_id(COL_NOME)
self.treeview.append_column (column)
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Cognome", cell, text = COL_COGNOME)
column.set_sort_column_id(COL_COGNOME)
self.treeview.append_column (column)
Possiamo rendere ridimensionabili in larghezza le colonne col metodo set_resizable
:
column.set_resizable (True)
Una funzionalità piuttosto interessante è quella di poter rendere editabili le celle di testo, facendo in modo che le modifiche apportate si riflettano sul modello dati e di conseguenza sulle eventuali altre viste. Per fare ciò, dobbiamo impostare la proprietà editable
della cella e associare una funzione callback all’evento edited
della cella stessa.
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Nome", cell, text = COL_NOME)
cell.set_property('editable', True)
cell.connect('edited', edited_callback, (model, COL_NOME))
self.treeview.append_column (column)
cell = gtk.CellRendererText ()
column = gtk.TreeViewColumn ("Cognome", cell, text = COL_COGNOME)
cell.set_property('editable', True)
cell.connect('edited', edited_callback, (model, COL_COGNOME))
self.treeview.append_column (column)
def edited_callback (cell, rowpath, new_text, user_data):
model, col_id = user_data
iter = model.get_iter (rowpath)
model.set_value (iter, col_id, new_text)
Programma di esempio
Il programma di esempio mostra un programma PyGTK completo che mostra gran parte dei concetti descritti nel tutorial, aggiungendo la gestione del campo immagine qui tralasciata.