22nd Febbraio 2008

Programmare in Python/Qt - parte 3

posted in python, programmazione, qt |

Nella seconda parte abbiamo costruito un editor per poter immettere e modificare una sequenza di valori, utilizzando il framework model/view fornito dalle Qt4. In questo articolo sfrutteremo le funzioni definite nel modello applicativo per effettuare calcoli statistici sulle sequenze e visualizzarne i risultati sotto forma di tabella.

Tabelle

Per mostrare dei dati in forma tabulare, in un contesto model/view dobbiamo avere a disposizione due elementi: una view che mostri graficamente la tabella e un model che fornisca i dati alla view. Un widget per mostrare una tabella è un elemento molto comune, indispensabile praticamente in qualunque applicazione, ed è quindi naturale che sia già presente nelle librerie Qt il widget QTableView. L’unica cosa da fare per visualizzare dei dati in forma di tabella è quindi scrivere un model che implementi l’interfaccia attesa dalla view.

La classe QAbstractItemModel definisce l’interfaccia usata dalle view per accedere ai dati. Vale ancora la pena ricordare che i dati non devono necessariamente essere contenuti nel model stesso, ma esso può fungere da adapter per quella che è la vera fonte dati, sia essa un file, un database o altre classi che rappresentino un modello applicativo. L’interfaccia definita da QAbstractItemModel è in grado di fornire dati a tipi di view differenti, come tabelle, liste o alberi. Spesso però è più conveniente partire da un’implementazione parziale di questa interfaccia, più mirata ad una particolare view. Per questo costruiremo i nostri model a partire da QAbstractTableView.

Indici statistici

Nel modello applicativo abbiamo definito delle funzioni per calcolare indici statistici sui dati contenuti in una lista. In particolare possiamo calcolare 4 indici: media, errore semplice medio, varianza e deviazione standard. Un modo conveniente per mostrare questi indici all’utente è tramite una tabella che mostri nella prima colonna i nome degli indici e nella seconda i valori calcolati con le funzioni.

Tabella degli indici statistici

Il principio è lo stesso seguito nella seconda parte per implementare il modello per l’editor delle sequenze. Questa volta il compito è addirittura più agevole, perché si tratta di un modello di sola lettura: l’utente non deve essere in grado di modificare gli elementi del modello, in quanto calcolati. In sostanza dobbiamo implementare due metodi che forniscono metadati sul modello, rowCount() e columnCount(), che la view invoca per conoscere rispettivamente il numero di righe e di colonne della tabella; un metodo che fornisce le intestazioni, headerData(); ed infine il metodo più importante, quello che fornisce i dati veri e propri, data(). Prima di tutto è però indispensabile inizializzare il modello tramite costruttore.

class ModelloIndici(QAbstractTableModel):
    “”"Modello che rappresenta una tabella con gli indici statistici di una
    sequenza di float nel pattern model/view Qt.”"”
    
    def __init__(self, sequence):
        super(ModelloIndici, self).__init__()
        self._sequence = sequence
        
        # indici statistici che sono visualizzati nella tabella
        self._indici = {‘media’: media,
                                     ‘errore medio’: errore_semplice_medio,
                                     ‘varianza’: varianza,
                                     ‘dev. standard’: deviazione_standard}
        
        # le etichette per la tabella sono le chiavi assegnate agli indici
        self._labels = self._indici.keys()

Il costruttore prende come parametro la sequenza a cui applicare le funzioni di calcolo. Per rendere i dati in forma tabulare dobbiamo scegliere una rappresentazione interna. Un dizionario ci permette di associare facilmente un’etichetta ad una funzione, questo perché in Python tutto è oggetto, anche le funzioni. Perciò self._indici contiene i nomi degli indici statistici e le relative funzioni di calcolo, mentre in self._labels conserviamo una lista con le sole etichette (le chiavi del dizionario). Una simile struttura interna accentra in un solo punto - il dizionario - i dati che devono essere visualizzati, rendendo molto semplice l’aggiunta di nuovi indici qualora dovesse rendersi necessario: basterà aggiungere una coppia etichetta/funzione e automaticamente un nuovo indice apparirà in tabella. Vediamo come.

    def rowCount(self, parent):
        “”"Restituisce il numero di righe della tabella.”"”
        # il numero di righe e’ il numero di indici statistici
        return len(self._indici)
    
    def columnCount(self, parent):
        “”"Restituisce il numero di colonne della tabella.”"”
        # c’e’ una sola colonna, con il valore dell’indice
        return 1

Su questi due semplici metodi c’è poco da dire. Il numero di righe è dato dalla quantità di coppie etichetta/funzione contenute nel dizionario, mentre c’è una sola colonna, quella che mostra gli indici calcolati.

    def headerData(self, section, orientation, role):
        “”"Etichette per le intestazioni.”"”
        if role !=  Qt.DisplayRole:
            return QVariant()
        if orientation == Qt.Horizontal:
            # etichette orizzontali, in pratica la sola etichetta ‘valore’
            return QVariant(‘valore’)
        else:
            # etichette verticali, cioe’ i nomi assegnati agli indici statistici
            return QVariant(self._labels[section])

Il metodo headerData() vuole 3 parametri: section è l’indice numerico dell’intestazione; orientation dice se l’intestazione richiesta è orizzontale (Qt.Horizontal) o verticale (Qt.Vertical); role è il ruolo con cui la view accede ai dati, e poiché sviluppiamo un modello di sola lettura prendiamo in considerazione solo il DisplayRole. In orizzontale abbiamo la sola etichetta “valore”. In verticale ci sono i nomi degli indici statistici, quelli che abbiamo salvato in self._labels.

    def data(self, position, role):
        “”"Restituisce il valore dell’indice statistico.
        position - coordinate della cella, istanza di QModelIndex.”"”
        
        if not position.isValid():
            return QVariant()
        
        row = position.row()
        size = len(self._indici)
        
        if row >= size:
            return QVariant()
        
        if role == Qt.DisplayRole:
            # assegnamo ad index la funzione per calcolare l’indice statistico
            index = self._indici.values()[row]
            # l’indice statistico viene calcolato sulla sequenza
            return QVariant(“%.2f” % index(self._sequence))
        else:
            return QVariant()

Il metodo data() fornisce i valori, data la posizione e il ruolo (per il quale vale la considerazione precedente). Nel parametro position la view passerà un oggetto con due metodi, row() e column(), che forniscono le coordinate del dato da mostrare in tabella. Il dato viene calcolato dapprima individuando la funzione corrispondente, con la riga

            index = self._indici.values()[row]

che assegna ad index l’oggetto funzione selezionato dal dizionario; quindi la funzione viene applicata alla sequenza ed il risultato formattato con 2 cifre decimali restituito alla view sotto forma di QVariant. Da notare l’uso di index come se fosse una funzione: questo perché è una funzione. Le funzioni, in quanto oggetti a tutti gli effetti, possono essere conservate in contenitori (come il nostro dizionario), assegnate a variabili e invocate con la consueta notazione delle parentesi tonde.

Modello dei dettagli

Abbiamo quindi un modello con gli indici statistici di nostro interesse, che un’istanza di QTableView provvederà a mostrare all’utente. Ma non vogliamo fermarci qui. Dopo aver verificato quanto sia facile sviluppare un modello tabulare perché non crearne uno con un po’ di dettagli sui dati analizzati? In altre parole sviluppiamo un modello che contenga i valori unici nella sequenza, le frequenze con cui compaiono e altri indici che mostrino in dettaglio la struttura della collezione di valori.

Tabella dettagliata

Anche questa volta dobbiamo partire dal costruttore e da una struttura di rappresentazione interna.

class ModelloDettagliato(QAbstractTableModel):
    
    def __init__(self, sequence):
        super(ModelloDettagliato, self).__init__()
        self._sequence = sequence
        self._freq = frequenze(sequence)
        self._media = media(sequence)
        
        self._indici = (
                            (‘x’, lambda x, f: x),
                            (‘f’, lambda x, f: f),
                            (‘fx’, lambda x, f: x * f),
                            (‘x-M’, lambda x, f: x - self._media),
                            (‘(x-M)^2′, lambda x, f: (x - self._media)**2),
                            (‘f(x-M)^2′, lambda x, f: f * (x - self._media)**2)
                        )

        # le etichette per la tabella sono il primo elemento di ciascuna tupla
        self._labels = tuple([label for label, function in self._indici])
        # le funzioni per il calcolo dei valori sono il secondo elemento
        self._funzioni = tuple([function for label, function in self._indici])

Questa volta la scelta della struttura è ricaduta su una tupla di tuple. Come si è detto, la tupla è una lista immutabile, per cui non se ne possono modificare gli elementi, né aggiungerne o sottrarne; inoltre, come per una normale lista, l’ordine degli elementi è quello in cui sono stati inseriti. Al contrario un dizionario non garantisce l’ordine. Gli elementi della tupla sono a loro volta tuple con due valori: una stringa e una funzione. L’operatore lambda del Python consente di creare al volo semplici funzioni anonime. Noi lo utilizziamo per creare delle funzioni con due parametri, x e f, che sono rispettivamente un valore e la sua frequenza. Queste funzioni effettuano i calcoli che verranno mostrati nella tabella dei dettagli. Come in precedenza, una struttura interna centralizzata ci permette di aggiunere facilmente nuove funzioni, e poiché sono contenute in una tupla verrano mostrate nell’ordine in cui compaiono.

La tabella mostrerà su ciascuna riga un valore unico della sequenza, la frequenza con cui compare e altri indici calcolati mediante le funzioni lambda. I metodi rowCount(), columnCount() e headerData() sono del tutto analoghi ai precedenti.

    def data(self, position, role):
        “”"Restituisce il valore dell’indice di posizione position.”"”
        if not position.isValid():
            return QVariant()
        
        row_size, column_size = len(self._freq), len(self._indici)
        if position.row() >= row_size or position.column() >= column_size:
            return QVariant()
        
        if role == Qt.DisplayRole:
            return QVariant(self._elemento_tabella(position))
        else:
            return QVariant()

Il metodo principale è sempre data(), ma stavolta invoca un metodo privato _elemento_tabella() per individuare il dato da restituire. La collocazione in un altro metodo della logica per individuare un dato nella struttura interna rende i metodi più semplici e coesi.

    def _elemento_tabella(self, position):
        “”"Restituisce la cella della tabella individuata da position.
        position - la posizione della cella, di tipo QModelIndex.”"”
        
        idx_elemento, idx_funzione = position.row(), position.column()
        x = self._freq.keys()[idx_elemento]
        f = self._freq[x]
        funzione = self._funzioni[idx_funzione]
        return funzione(x, f)

Il metodo riceve il parametro position, che contiene le coordinate (riga, colonna) dell’elemento in tabella. La riga individua il valore unico della sequenza mentre la colonna la funzione da applicare. Il valore unico viene estratto dal dizionario delle frequenze ed assegnato a x, quindi ne viene calcolata la frequenza f, mentre a funzione viene assegnata la funzione da applicare ai due valori per calcolare il dato richiesto. Come si vede è essenziale che le funzioni lambda definite nella tupla prendano lo stesso numero di parametri nello stesso ordine, per poterle così invocare tutte allo stesso modo.

Conclusioni

L’architettura model/view implementata dalle Qt è estremamente flessibile. Attraverso model di facile implementazione, che fungono da connettori fra la view e la reale fonte dati, è possibile organizzare le informazioni in strutture assai diversificate che mostrano all’utente gli stessi dati con diverse prospettive. L’associazione fra view e model può essere modificata a runtime, così da poter cambiare prospettiva in risposta alle azioni dell’utente.

Nell’archivio trovate i sorgenti. Nel prossimo articolo costruiremo l’applicazione componendo i pezzi messi a punto fino a questo momento.

Leave a Reply