Friday, February 28, 2014

Creazione di oggetti poligonali complessi in Tkinter.

Continuando la riflessione di cui al post precedente, in cui abbiamo visto come recuperare le istruzioni fondamentali per la realizzazione di spostamenti sulla canvas (istruzione M in SVG), realizzazione di linee (istruzione L) e curve (istruzione C), siamo in grado di iniziare a tradurre le istruzioni SVG in Tkinter. Dobbiamo considerare pero' che, mentre in SVG possiamo tracciare il percorso <path> d=' nell'ambito della stessa istruzione, in Tkinter abbiamo delle primitive per la creazione di curve o figure che devono essere realizzate separatamente. Per avere un criterio unificatore, possiamo considerare l'attributo generico 'tag' dei widget, già visto in precedenza, che ci permetterà di usare più oggetti canvas simultaneamente, come fossero un solo oggetto.

E' necessario, prima di andare avanti, chiarire due aspetti. Uno riguarda la differenza tra le istruzioni SVG M, L, C e le corrispondenti minuscole. Le prime indicano riferimenti assoluti, le seconde riferimenti relativi. Nel primo caso vengono indicate le coordinate esatte, nel secondo gli spostamenti.

Altro chiarimento che vi devo riguarda il metodo usato nel post precedente per estrarre le informazioni dal path; la ricerca fatta con l'uso delle espressioni regolari avviene in tutta la stringa, prescindendo dall'ordine.

Pertanto la ricostruzione del tracciato è sicuramente affetta da errori, se non si tiene conto del punto corretto di inizio tracciato della nuova linea o curva.

Ciò detto, riprendiamo a lavorare sul file di estrazione, apportando le correzioni necessarie al codice.


#|/usr/bin/env python
import re

class SVG2Tk:

    import xml.etree.ElementTree as ET
    
    def __init__(self, file_s, file_out):

        self.file_s = open(file_s, 'r')
        self.file_out= open(file_out,'w')

        tree = SVG2Tk.ET.parse(file_s)
        root = tree.getroot()
        self.xmlrecur(root)
        self.file_out.close()
        
        file_in = open(file_out)
        self.tk_out = open('tk_out.txt','w')
        try:
            width=file_in.next()
            height=file_in.next()
            self.tk_out.write('width %sheight %s' % (width, height))
        except Exception:
            print Exception

        while True:
            
            try:
                id_ = file_in.next()
                style = file_in.next()
                d = file_in.next()
                self.tk_out.write('id,' + id_)
                while len(d)>0:
                    pattern1 = r'([M]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]?[-\d]*[\.]?[\d]*[\s]*)'
                    matchObj = re.match(pattern1, d)
                    if matchObj:
                        string = matchObj.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')
    
                    pattern2 = r'([L]{1}[\s]*[-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*)'
                    matchOb2 = re.match(pattern2, d)
                    if matchOb2:
                        string = matchOb2.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')

                    pattern3 = r'([C]{1}[\s]*)(([-\d]*[\.]?[\d]*[\,\s]{1}[-\d]*[\.]?[\d]*[\s]*){3})'
                    matchOb3 = re.match(pattern3, d)
                    if matchOb3:
                        string = matchOb3.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')

                    pattern4 = r'([z]{1}[\s]*)'
                    matchOb4 = re.match(pattern4, d)
                    if matchOb4:
                        string = matchOb4.group()
                        d=d.replace(string, "")
                        s_string = re.split(r'[;\s]*',string)
                        string = ','.join(s_string[:-1])
                        self.tk_out.write(string+'\n')
                        
            except Exception:
                print Exception
                self.tk_out.close()
                break
                

    def xmlrecur(self,x):
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                    self.file_out.write(i.attrib['id'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['style'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['d'])
                    self.file_out.write('\n')
            elif set(['width','height']).issubset(i.attrib):
                self.file_out.write(i.attrib['width'])
                self.file_out.write('\n')
                self.file_out.write(i.attrib['height'])
                self.file_out.write('\n')
                
            else:
                self.xmlrecur(i)

if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg','out_xml.txt')

Le correzioni maggiori apportate al codice del post precedente riguardano:

  1. L'uso del solo metodo 'match' delle regular expression, perchè in questo particolare contesto vogliamo che la stringa corrispondente all'attributo 'd' sia consumata man mano che si trova la corrispondenza tra il modello e la prima parte della stringa. Ogni volta che si verifica l'abbinamento, la parte iniziale corrispondente della stringa viene cancellata e la procedura viene iterata fino a consumare tutta la stringa.
  2. Il file SVG sorgente usa solo i codici path M C e L maiuscoli e z minuscolo, per cui la ricerca della lettera iniziale si limita a queste consonanti. Per inciso la lettera z rappresenta il codice di chiusura della poligonale sul punto iniziale.
  3. In questa fase, e sempre in modo temporaneo, creiamo un nuovo file di appoggio, denominato tk_out.txt, in cui scriviamo le stringhe risultanti dalla rilettura del file out_file.txt che viene generato all'inizio. Le stringhe contengono elementi separati da ';' per maggiore agio nella successiva suddivisione. Tutto questo, bene inteso, fino alla stesura finale del codice.

Ci stiamo progressivamente avvicinando al momento della traduzione. Portate pazienza ancora un po'

Tuesday, February 25, 2014

Dal file SVG all'istruzione create_polygon in Tkinter. Uso delle espressioni regolari

Non c'è strumento più importante, per la manipolazione di testi, delle espressioni regolari. Costituiscono uno strumento scomodo, per la loro scarsa leggibilità, ma nello stesso tempo potente perchè consentono di leggere stringhe di testo in cui la disposizione e il numero di caratteri puo' essere estremamente variabile, ma presenta delle regolarità di modello, o pattern.

Prendiamo, per esempio, una stringa in cui compaiono il cognome e il nome di un certo numero di persone, una per stringa. Supponiamo che il software che ha generato le stringhe abbia assegnato caratteri tutti maiuscoli al cognome e solo l'iniziale maiuscola al nome.

Il pattern, scritto in forma verbosa, sarà pertanto il seguente:

Leggi da inizio riga una o più stringhe formate da soli caratteri maiuscoli, eventualmente intervallati e seguiti da spazi.

Leggi quindi una o più stringhe formate da 1 carattere alfabetico maiuscolo seguito da un certo numero di caratteri minuscoli e spazi.

Il pattern così descritto potrà essere tradotto in un'espressione regolare, che potremo cimentare contro ognuna delle stringhe formanti il cognome e nome delle persone in elenco.

In Python si usano pattern identici a quelli usati in linguaggio Perl e simili a quelli che troviamo in quasi tutti i linguaggi di programmazione, da C++ a Java, perfino a VBA per Excel.

Il pattern sopra descritto puo' essere reso dalla seguente espressione regolare:

([A-Z]+[\s]+)+([A-Z]{1}[a-z]+[\s]?)+

che, ovviamente, è pressochè illeggibile.

Non c'è alternativa allo studio analitico dei simboli utilizzati nelle espressioni regolari. Un buon tester/editor per collaudare le vostre regexp è il seguente: pythex

Con il suo aiuto, e con molta pazienza, potete voi stessi comporre espressioni regolari anche molto complesse e sperimentarle su stringhe test.


Veniamo alla traduzione SVG->Tkinter. Qualche post fa abbiamo estratto dai file SVG un intero set di stringhe di coordinate associate al tag <path> come attributi. Ora si tratta di ripescarle dai file (più avanti elimineremo il passaggio intermedio, costruendo un parser) e convertirle in istruzioni datate di senso nell'equivalente Tkinter di che è create_polygon.

Come ricorderete, ho salvato le stringhe in gruppi di tre, 'id','style' e 'd': la prima stringa è l'attribuzione identificativa fatta dal software grafico per riconoscere, nel blocco path, a cosa si riferisce il blocco stesso. Poi c'è la stringa style, che rappresenta il complesso delle informazioni di formato che permettono di personalizzare la poligonale. Infine la stringa d contiene le coordinate di percorso. Non abbiamo incertezze sul fatto che i tre gruppi di informazioni esistano tutti, per cui possiamo procedere.

Una piccola modifica alle stringhe di ricerca mi permette di catturare un'informazione utile per il futuro dimensionamento dell'immagine. Cerco di intercettare gli attrib che identificano la larghezza (width) e l'altezza (height) dell'immagine. Trattandosi di informazioni generali, verranno intercettate molto presto dalla procedura ricorsiva e inserite nel file in uscita per prime.

Il programma seguente, molto preliminare e non necessariamente funzionante in tutti i contesti e con tutti i file SVG, ricerca ricorsivamente gli attributi corrispondenti alle variabili di interesse e le salva in un file in modo da poter essere rilette con molta tranquillità in una fase successiva. Il salvataggio dei dati in un file non è un passaggio obbligatorio, ma ci aiuta a costruire il programma intercettando gli errori.

Per le prove ho utilizzato il file SVG: EU-Italy.svg.

Di seguito invece il programma che sto elaborando per la manipolazione di questo file.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#|/usr/bin/env python
import re

class SVG2Tk:

    import xml.etree.ElementTree as ET
    
    def __init__(self, file_s, file_out):

        self.file_s = open(file_s, 'r')
        self.file_out= open(file_out,'w')

        tree = SVG2Tk.ET.parse(file_s)
        root = tree.getroot()
        self.xmlrecur(root)
        self.file_out.close()
        
        file_in = open(file_out)
        try:
            width=file_in.next()
            height=file_in.next()
            print 'width %sheight %s' % (width, height)
        except Exception:
            print Exception

        while True:
            
            try:
                id_ = file_in.next()
                style = file_in.next()
                d = file_in.next()

                pattern1 = r'([M|m]{1}[\s]*)([\d]*[\.]?[\d]*[\,]?[\d]*[\.]?[\d]*)'
                matchObj = re.search(pattern1, d)
                if matchObj:
                    print matchObj.groups()
                pattern2 = r'([L|l]{1}[\s]*)([\d]*[\.]?[\d]*[\,]{1}[\d]*[\.]?[\d]*)'
                for matchOb2 in re.finditer(pattern2, d):
                        print matchOb2.groups()
                pattern3 = r'([C|c]{1}[\s]*)(([\d]*[\.]?[\d]*[\,]{1}[\d]*[\.]?[\d]*[\s]+){3})'
                matchOb3 = re.search(pattern3, d)
                if matchOb3:
                    print matchOb3.group(1,2)
                    
            except Exception:
                print 'exception'
                break
                

    def xmlrecur(self,x):
        for i in x:
            if set(['id','style','d']).issubset(i.attrib):
                    self.file_out.write(i.attrib['id'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['style'])
                    self.file_out.write('\n')
                    self.file_out.write(i.attrib['d'])
                    self.file_out.write('\n')
            elif set(['width','height']).issubset(i.attrib):
                self.file_out.write(i.attrib['width'])
                self.file_out.write('\n')
                self.file_out.write(i.attrib['height'])
                self.file_out.write('\n')
                
            else:
                self.xmlrecur(i)

if __name__ == '__main__':
    app = SVG2Tk('EU-Italy.svg','out_xml.txt')

Il codice presenta alcune modalità di utilizzo delle espressioni regolari in Python:

  • La libreria di riferimento è re, che viene importata in testa al programma
  • con la opzione re.search vengono cimentati dei pattern con le stringhe che, con metodo iterativo, sono estratte dal file salvato.
  • La variabile matchObj e analoghe (nomi di fantasia, come sempre) sono oggetti delle libreria re, non sono utilizzabili in modo diretto. I metodi group() e groups() servono per estrarre le sottostringhe corrispondenti ai pattern. Al momento viene fatta esclusivamente una stampa su console dei risultati, come si vede dalla stampa seguente.
Python 2.7.5+ (default, Sep 19 2013, 13:48:49) 
[GCC 4.8.1] on linux2
Type "copyright", "credits" or "license()" for more information.
==== No Subprocess ====
>>> 
width 207036
height 173880

('M ', '168720,114651')
('C ', '170405.84,114660.61 171877.76,115218.21 173517.99,115465.97\n')
('M ', '790.46875,0.875')
('L ', '426.9375,0.90625')
('L ', '426.46875,1.5')
('L ', '423.84375,2.25')
('L ', '418.375,7')
('L ', '416.65625,8.28125')
('L ', '414.53125,8.625')
('L ', '413,6.90625')
('L ', '411.75,4.5')
('L ', '410.96875,2.15625')
('L ', '409.15625,1.5625')
('L ', '408.1875,1.96875')
('L ', '406.875,3.96875')
('L ', '404.25,4.96875')
('L ', '402.25,7.15625')
('L ', '401.375,9.875')
('L ', '400.09375,11.1875')
('L ', '398.46875,11.3125')
('L ', '397.09375,10.53125')
('L ', '395.6875,10.90625')
('L ', '395.34375,12.28125')
('L ', '396,13.25')
('L ', '396.125,14.875')
('L ', '395.125,15.75')
('L ', '390,15.4375')
('L ', '388.65625,14.65625')
('L ', '387.03125,14.3125')
('L ', '385.09375,15.5625')
('L ', '383.5,18.71875')
('L ', '380.96875,21.5625')
('L ', '380.75,25.03125')
('L ', '384.3125,31.5')
('L ', '386.3125,33.03125')
('L ', '389.3125,33.65625')
('L ', '391.4375,33.34375')
('L ', '396.53125,30.65625')
('L ', '400.21875,31.34375')

In un prossimo post faro' un'ulteriore elaborazione del codice, eliminando il file di appoggio e iniziando la traduzione in codice Tkinter. Come penso possiate apprezzare, la combinazione di strumenti raffinati come le librerie di Python consente, con pochissimo codice, di affrontare problemi di notevole complessità. Sono interessato a qualche vostro commento, anche per migliorare il codice e pulirlo un po' dalla grezzezza del mio stile. A presto.

Friday, February 14, 2014

Terzo intermezzo - duemila magnifiche biglie

Interrompo ancora una volta il lento processo di avvicinamento verso la realizzazione di un software astrologico per parlare, almeno una volta, di classi.

Fino ad ora abbiamo usato le classi come contenitori generali per i widget di Tkinter, situazioni in cui la programmazione per oggetti non era poi così indispensabile, se non per mantenere ordinato il codice e l'accesso alle funzioni.

Ora però vorrei mostrarvi, ancora in ambiente Tkinter, cosa succede quando si generano molte istanze della stessa classe. Nel successivo programma verranno generati, in una Canvas, duemila oggetti circolari, che si muoveranno in modo pseudocasuale, abbastanza browniano, in uno sfondo di colore nero.

Se pensate che Python, essendo un linguaggio interpretato, sia troppo lento, forse questo piccolo programma vi farà ricredere. Nella maggior parte delle situazioni, la velocità non è un problema. Se avete sperimentato la velocità di scansione di un file SVG, potrete forse aver giudicato 'ragionevole' un tempo esecutivo di qualche frazione di secondo.

A proposito di questo programma, voglio farvi solo notare come si usa una stringa 'numerata' per tenere traccia dell'elenco delle sfere, come si genera casualmente il colore di ogni sfera, come viene fatto l'aggiornamento ogni 50 (diciamo almeno ogni 50) millisecondi della canvas, e come è facile usare il metodo 'move' di Canvas per spostare di lunghezze determinate ogni singolo oggetto. Il refresh è automatico, Tkinter/Tcl non è poi' così male, anche qualora volessimo usarlo per produrre un videogame o una simulazione fisica.


import Tkinter
import random

class MyApp:

    def __init__(self):
        self.root = Tkinter.Tk()
        self.frame= Tkinter.Frame()
        self.frame.pack()
        self.width = 1200
        self.height= 1200
        self.canvas = Tkinter.Canvas(self.frame, width=self.width,
                                     height=self.height, background = "black")
        self.canvas.pack()

        for i in range(2000):
            name='myBall'+str(i)
            x1 = random.randrange(self.width)
            y1 = random.randrange(self.height)
            color = "#%02x%02x%02x" % (random.randrange(256),random.randrange(256),random.randrange(256))
            x2 = x1+10
            y2 = y1+10
            self.canvas.create_oval(x1,y1,x2,y2,fill=color, tag = name)
        self.root.after(50,self.move)

    def move(self):
        for i in range(2000):
            xp = random.randrange(-10,11)
            yp = random.randrange(-10,11)
            self.canvas.move('myBall'+str(i),xp,yp)
        self.root.after(50,self.move)


            
    def mainloop(self):
        self.root.mainloop()



app=MyApp()
app.mainloop()

Mappe cartografiche e file SVG

I file con estensione SVG sono utilizzati per la grafica vettoriale, quella, per intenderci, che non sgrana quando si ingrandisce perchè l'immagine viene disegnata con linee e curve al momento della visualizzazione, anzichè, come per le immagini raster tipo jpg, png, bmp ecc., essere realizzata una volte per tutte all'atto della creazione (è il formato di base di applicazioni come Inkscape e il suo precursore Sodipodi in ambiente open source o Adobe Illustrator e Corel Draw in campo commerciale).

Il vantaggio della grafica vettoriale è che consente una rappresentazione di dettaglio che è utile per visualizzare, con molta fedeltà, dei tracciati e dei confini. Prima di riprendere lo studio del tempo in astrologia e quindi delle ore locali e solari, dei fusi orari e dell'ora legale, consentitemi una digressione in questo ambito della grafica, che ci tornerà molto utile nel seguito.

I file SVG (Scalar Vector Graphic) sono dei file XML. l'XML è uno standard nella conservazione delle informazioni su file, che si è affermato negli ultimi decenni grazie alla sua portabilità e alla sua semplicità di lettura e manipolazione, tanto per gli esseri umani che per i computer.

Un file XML, in estrema sintesi, racchiude i contenuti all'interno di tag di apertura e chiusura, un po' come HTML per il web:

<?xml version="1.0" encoding="UTF-8"?>
<utenti>
    <utente>
        <nome>Luca</nome>
        <cognome>Cicci</cognome>
        <indirizzo>Milano</indirizzo>
    </utente>
    <utente>
        <nome>Max</nome>
        <cognome>Rossi</cognome>
        <indirizzo>Roma</indirizzo>
    </utente>
</utenti>

I tag non sono vincolati, possono essere concepiti come i nomi dei campi in un database, con molta libertà. La complessità variabile fa sì che si possa arrivare a discreti livelli di nidificazione, e non è sempre facile manipolare questi file con le normali operazioni su file.

Python fornisce una libreria e diverse metodologie per l'accesso alle informazioni di un file XML e le useremo subito per estrarre dati cartografici da file SVG pubblicati sul web.

Sfortunatamente non posso allegare immagini vettoriali, ma un breve ricerca sul web ve ne mette a disposizione un numero notevole. Quella che vedete qui è stata ottenuta da un file SVG, tipicamente una mappa con chiari confini regionali, che permette una agevole rappresentazione grafica anche di dati socio-politici ed economici.

Come vedremo fra un attimo, il file SVG contiene le coordinate del tracciato delle linee che vanno a comporre l'immagine, insieme ad un certo numero di dati di stile, che permettono di variare spessore delle linee, colori, e altre caratteristiche. A differenza dei normali file XML, che, come nell'esempio sopra riportato, racchiudono in tag concentriche i dati interessanti, nei file SVG i dati sono contenuti nel corpo dei tag, e la loro estrazione richiede di valutare i tag stessi, cosa che faremo subito.

Il mio interesse per i file SVG non è fine a se stesso: nei post precedenti abbiamo utilizzato le canvas di Tkinter, e abbiamo visto che alcune figure si compongono dando come parametri delle posizioni x e y nella canvas stessa. Se riuscissimo a estrarre le informazioni che costituiscono il tracciato delle linee di un file SVG, potremmo tentare di trasformarle con un semplice script in Python, nelle informazioni equivalenti per creare le stesse linee in una canvas. Vediamo quindi, per prima cosa, come è formato un file SVG e quali sono le istruzioni per noi interessanti.

Il tag che identifica l'insieme dei punti che compongono la curva di confine di uno stato è il <path>. al suo interno, con una simbologia semplice, si scrivono le coordinate delle linee complesse che formano la curva.

Se esploriamo uno di questi file con un editor di testo, anzichè con il browser o il visualizzatore di immagini (io uso gedit su ubuntu, in ambiente Windows suggerirei Notepad++), all'interno di path troviamo il subtag d= seguito da lettere singole e numeri.

Un esempio tratto da una cartografia dell'Italia:

d="M61.759,101.27c-5.313,1.354  -9.802,6.955  -15.825,5.334c-5.442,2.572 -11.99,4.529 -17.632,1.63 c-2.721,1.465 -4.626,4.26 -8.239,3.793c-3.933,0.437  -7.61,4.156  -5.245,8.238c2.784,7.396,10.431,12.128,11.943,20.122  c1.742,2.854,6.42,3.213,7.201 -0.83c2.369,1.082,2.18,5.512,5.364,2.786c6.318 -1.545,12.173 -4.757,17.484 -8.09  c7.049  -1.133,14.792,4.075,21.011  -1.274c4.678  -3.889,1.782  -10.284,0.267  -14.995c-0.883  -4.695  -0.146  -11.463  -6.075  -12.743  C68.255,104.604,65.624,100.895,61.759,101.27z" />

la lettera m o M (moveto) identifica le coordinate del punto di partenza, la c o C (curveto) produce una curva di Bezier tra il punto precedente e un nuovo punto utilizzando due punti come punti di controllo. La l o L (lineto) definisce un segmento congiungente, per la via più breve, il punto precedente con il nuovo, infine la z o Z (closepath) chiude la poligonale iniziata con il primo punto.

Per i nostri scopi è sufficiente scandire il file SVG alla ricerca del tag path e recuperare le informazioni contenute nel subtag d. l'operazione puo' non essere agevole, per via dei differenti livelli di profondità che sono ottenuti dai software con cui si realizzano le immagini vettoriali. In questo caso ci aiuta una libreria di Python, la xml.etree.ElementTree, e un piccolo codice ricorsivo scritto da me.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#|/usr/bin/env python
from xml.etree import ElementTree as ET

def xmlrecur(x):
    for i in x:
        if set(['id','style','d']).issubset(i.attrib):
                file_out.write(i.attrib['id'])
                file_out.write('\n')
                file_out.write(i.attrib['style'])
                file_out.write('\n')
                file_out.write(i.attrib['d'])
                file_out.write('\n')
        else:
            xmlrecur(i)

file_out=open('provaxml.txt','w')
tree = ET.parse('Europe_regions.svg')
root = tree.getroot()
xmlrecur(root)
file_out.close()

Il programma importa la libreria di cui sopra, che permette di esplorare i tag e i loro attribution in un file esterno. Se chiediamo di recuperare le informazioni relative ad un particolare tag, è probabile che siamo costretti a cercare a profondità variabili, da cui la necessità di una funzione ricorsiva, che chiama se stessa proponendo come input il nuovo livello di tag, fino a concludere il lavoro e stamparlo su un file esterno. Ho imposto la verifica simultanea su tre subtag (id, style e d) per essere certo di avere un numero di linee multiplo di tre. Il file che si ottiene è composto esclusivamente dai contenuti di questi tre subtag, nell'ordine, e funziona, ovviamente, solo se il file SVG li contiene tutti e tre, altrimenti occorre un adattamento. Se volete sapere cosa sono i set e il metodo issubset, vi rimando alla documentazione ufficiale di Python.

Nel prossimo post proveremo a fare la conversione tra il nuovo file txt e il metodo create_polygon di canvas scrivendo un piccolo parser. A presto.

Thursday, February 13, 2014

Ancora su widget ed eventi

Prima di riprendere la riflessione sull'uso astrologico della programmazione in Python, vi presento un'altra piccola applicazione che mostra come si puo' creare un widget e distruggerlo in funzione degli eventi del mouse (ma potrebbero essere anche altri tipi di eventi).

Gli eventi sono generati di continuo, il sistema li produce da sè o li acquisisce dall'utente. Alcuni possono essere intercettati, come il movimento del mouse su un widget (mouseover) o la sua uscita dal widget. In ambedue i casi, come si vede nel programma allegato, tali eventi possono essere intercettati per la chiamata di funzioni di callback.

Il metodo tag_bind si applica alla canvas. Vengono passati come parametri il tipo di evento (Motion o Leave tra segni di minore e maggiore) e la funzione di callback che si desidera attivare. Nelle funzioni, quando il widget viene creato riceve un tag che lo contraddistingue (potrebbe essere applicato a più oggetti, volendo), quando il widget viene distrutto viene identificato con il proprio tag.

Nella finestra di cui avete una riproduzione qui a fianco, il widget è una stringa di testo, che compare, con diverso contenuto, quando si entra col mouse nell'area del widget scelto (cerchio o quadrato). Non appena si allontana il mouse, la stringa di testo, generata in occasione del primo evento, viene cancellata. La sintassi per la cancellazione del widget è, nel caso specifico:
self.canvas.delete(tag)
che di fatto rimuove il widget 'child' dell'applicazione 'parent' che è la canvas.

La manipolazione intenzionale dei widget è una delle tecniche di composizione grafica di cui avremo bisogno in futuro, quando inizieremo a scrivere l'applicazione per lo sviluppo e la composizione grafica dei temi astrologici.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import Tkinter

class MyApp:

  def __init__(self):
    
    self.root=Tkinter.Tk()
    self.root.title('Creare e cancellare i widget')

    self.frame=Tkinter.Frame()
    self.frame.pack()

    self.canvas = Tkinter.Canvas(self.frame, width=300, height = 300)
    self.canvas.pack()
    self.canvas.create_oval(25,25,75,75, fill="green", tag = 'cerchio')
    self.canvas.create_rectangle(100,100,150,150, fill='red', tag = 'quadrato')

    self.canvas.tag_bind('cerchio', '<Motion>',self.motion_in)
    self.canvas.tag_bind('cerchio','<Leave>', self.motion_out)
    self.canvas.tag_bind('quadrato','<Motion>',self.message)
    self.canvas.tag_bind('quadrato','<Leave>', self.message_destroy)

  def motion_in(self, event):
    self.canvas.create_text(200,200, text="Sei entrato nel cerchio", tag='mouse_str')
    
  def motion_out(self, event):
    self.canvas.delete('mouse_str')
    
  def message(self, event):
    self.canvas.create_text(200, 200, text="Sei entrato nel quadrato", tag='entrato')
    
  def message_destroy(self, event):
    self.canvas.delete('entrato')
    
  def mainloop(self):
    self.root.mainloop()

  
master = MyApp()
master.mainloop()  

Wednesday, February 12, 2014

La demo di Indianapolis

Nel post precedente ho allegato la demo di un circuito automobilistico, realizzato con lo strumento Canvas di Tkinter, Alcune tecniche utilizzate nel programma possono farvi risparmiare qualche ora di ricerca su Google.

Vi ho anticipato che per usare immagini fotografiche serve la libreria pillow. Nel giochino viene ridimensionata l'immagine fotografica della Porsche Carrera, ruotata verso l'alto in posizione di partenza.

La classe Carrera fornisce due metodi: il primo

def __init__
crea l'immagine come oggetto di canvas (istanza di Canvas, widget di Tkinter) e l'altro
def update
consente di sostituire l'immagine con una nuova, ruotata diversamente, per seguire logicamente il cambio di direzione dell'automobile quando percorre il circuito.

L'altra classe è la solita class myApp che contiene la parte grafica e quello logica del software.

Quando create una variabile preceduta da self. nel costruttore della classe, la sua visibilità (scope) si estende automaticamente ai metodi, cioè alle funzioni; il valore di default di self.eccentricità, per esempio, compare molto precocemente e puo' essere utilizzato ovunque nel codice.

La creazione dei widget comuni come Label e Button, compresa l'istruzione command, dovrebbe esservi familiare.

Per Canvas vediamo le cose più importanti:

  • i parametri width e height fissano rispettivamente la larghezza e l'altezza. Qui sono stati inizializzati come variabili di istanza, perchè vengono usati spesso nel programma
  • per disegnare gli ovali che costituiscono i bordi della pista di Indianapolis nel giochino, ho usato un piccolo espediente: dato che, a differenza di linee e punti, la funzione create_oval richiede le coordinate del box che li contiene, ho creato la funzione coord_ell. Questa funzione, con l'aiuto della geometria analitica, passati come parametri il valore di eccentricità (diciamo il grado di schiacciamento dell'ellisse, compreso tra 0 e 1), i limiti dimensionali della Canvas, un valore angolare e percentuale, restituisce una tupla contenente gli estremi del box (x1,y1,x2,y2) e le coordinate x,y di un punto dell'ellisse dato il valore dell'angolo al centro, utile se devo tracciare punti o posizionare, come in questo caso, un'immagine. Il valore percentuale stabilisce un correttivo per creare ellissi a varia distanza dal centro. Il bordo interno della pista, per esempio, è all'80% con una eccentricità di 0.8, il bordo esterno al 100% sempre con eccentricità 0.8.
  • La traiettoria della Carrera, invece ha una eccentricità del 90%, per cui risulta un ellisse più schiacciata, dando l'illusione di un derapage agli estremi e di un percorso sul bordo interno nel tratto 'rettilineo'. Ovviamente è solo un espediente geometrico, non c'è "Car Physics", niente attriti, niente resistenza dell'aria, nessuna inclinazione della pista. Se volete divertirvi un po', provate a fissare l'eccentricità al valore 1.0, vedrete l'auto percorrere il diametro maggiore, avanti e indietro, ruotando in modo abbastanza divertente durante il tragitto.
  • Nella sequenza preceduta da #Car, potete vedere come si carica un'immagine dal disco fisso, e come si ridimensiona a 50x50 pixel. Potete modificare la grandezza, se volete. Nel 'resize' è stata aggiunta l'opzione ANTIALIAS, che serve a limitare i danni sull'immagine che inevitabilmente la trasformazione produce. mantenendo un buon effetto fotorealistico.
  • Il timer non esiste come widget in Tkinter, volendo si può usare il timing della libreria thread, ma tutto sommato può bastare il metodo .after, applicato all'istanza di root, che attiva una funzione di callback (in questo caso 'muovi') dopo un certo numero di millisecondi. Questo metodo funziona una volta sola, per cui è necessario rilanciarlo dall'interno della funzione chiamata, indicando, come già si fa per il command dei button, solo il nome della funzione, non la sua istanza, cioè evitando di usare le parentesi () dopo il nome: il programma andrebbe in crash velocemente per superamento della profondità di ricorsione.

Dal prossimo post ritorniamo ad occuparci un po' di software astrologico, partendo dalle variabili di tempo e da come si possono integrare con l'uso dell'Olsen database, il database delle ore legali in vigore in varie epoche in molti, se non tutti i paesi del mondo e il database delle località che abbiamo creato per SQLite. Alla prossima

La Canvas, ovvero la 'tela' per disegnare in Tkinter

Quando lavorate in linea di comando, potete sostituire le Entry con le funzioni di input, e le Label con le istruzioni print. Quello che non potete sostituire è il disegno di grafici, disegni o schemi, per cui il widget Canvas risulta insostituibile.

Nella sua forma più semplice una canvas non è che un riquadro di dimensioni scelte in fase di configurazione, che permette di tracciare punti, linee, ovali, rettangoli e anche di renderizzare un testo. Unitamente alla libreria PIL (Python Image Library), consente di importare immagini di vario formato, che possono essere collocate staticamente nella canvas o fatte muovere con tastiera e mouse o cun un algoritmo.

Piuttosto di installare PIL, su Ubuntu, ho preferito un suo fork, pillow, che promette di essere maggiormente user friendly; perchè funzioni correttamente è pero' necessario preliminarmente (re)installare le librerie tk e tcl:

sudo apt-get install tk8.5-dev tcl8.5-dev
sudo pip install pillow

Per esemplificare l'uso della Canvas creiamo una piccola applicazione test con due Frame, la prima contiene una Canvas, la seconda due Button e due Label.

Nella Canvas simuliamo un circuito automobilistico con una Porsche Carrera, ripresa dall'alto, che gira in un'orbita fissa ma di cui possiamo variare la velocità. Non è il gioco più divertente dell'anno ma credo che abbia una discreta efficacia didattica. Rimando le spiegazioni al prossimo post.








  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# -*- coding: utf-8 -*-
import Tkinter
from PIL import Image, ImageTk
import math

class Carrera:
        """
        Classe utile per creare un'oggetto immagine della Porsche Carrera e ruotarla
        in relazione alla posizione angolare sulla traiettoria ellittica
        """
        
        def __init__(self, canvas, image, x, y, angolo):
                self.image = image
                self.image = self.image.rotate(angolo)
                self.car = ImageTk.PhotoImage(self.image)
                canvas.create_image(x, y, image=self.car, tag='myCarrera')

        def update(self, canvas, image, angolo):
                self.image = image
                self.image = self.image.rotate(angolo)
                self.car = ImageTk.PhotoImage(self.image)
                canvas.itemconfig('myCarrera', image=self.car)

                
      
class MyApp:

    def __init__(self):

        self.root = Tkinter.Tk()
        self.root.title('Indianapolis')
        self.coefficiente=1.0
        
        #Frame 1
        self.frame1 = Tkinter.Frame()
        self.frame1.pack(side='left')
        self.width = 600.0
        self.height= 600.0

        #Canvas
        self.canvas = Tkinter.Canvas(self.frame1, background = 'green',
                                     width=self.width, height=self.height)
        self.canvas.pack()
        
        self.eccentricita = .8
        self.angolo = 0
        # disegna il bordo esterno della pista
        coord=self.coord_ell(self.eccentricita, self.width, self.height, self.angolo, 100)
        self.circ1 = self.canvas.create_oval(coord[0],coord[1],coord[2],coord[3],
                                             dash = 10, fill='white', width=2)
        # disegna il bordo interno della pista
        coord=self.coord_ell(self.eccentricita, self.width, self.height, self.angolo, 60)
        self.circ2 = self.canvas.create_oval(coord[0],coord[1],coord[2],coord[3],
                                             dash = 10, fill='grey', width=1)

        # Car
        # usare l'istruzione seguente dopo aver salvato l'immagine della
        # Porsche Carrera che compare nella pagina del blog
        # nella stessa directory del file .py
        photo=Image.open('CarreraTop1.png') 
        self.carrera_photo=photo.resize((50,50),Image.ANTIALIAS)
        # traiettoria dell'auto
        coord=self.coord_ell(self.eccentricita, self.width, self.height, self.angolo, 90)
        self.carrera = Carrera(self.canvas, self.carrera_photo, coord[4], coord[5], self.angolo)
        # inizializza il timer
        self.root.after(50, self.muovi)

        #Frame2
        self.frame2 = Tkinter.Frame()
        self.frame2.pack(side='left')
        self.label1 = Tkinter.Label(self.frame2,
                                   text = 'Indianapolis\nclicca + per accelerare\n- per decelerare',
                                   font = 'Helvetica 14', width=20)
        self.label1.pack()
        # acceleratore
        self.button_acc=Tkinter.Button(self.frame2, text = '+',
                                       font = 'Courier 34', command=self.accelera)
        self.button_acc.pack(fill= 'x')
        # deceleratore
        self.button_dec=Tkinter.Button(self.frame2, text = '-',
                                       font = 'Courier 34', command=self.decelera)
        self.button_dec.pack(fill= 'x')
        # indica la velocità
        self.label2 = Tkinter.Label(self.frame2,
                                    text = 'velocità: 1.0', width=20)
        self.label2.pack()


    def coord_ell(self, e, width, height, angolo, perc):
        """ Funzione che, data l'eccentricità di un'ellisse, le dimensioni x e y della canvas,
        l'angolo al centro e la percentuale dimensionale, restituisce una tupla, con le coordinate
        x1 y1 x2 y2 (bbox) per ovali ed eventualmente rettangoli, le coordinate x e y dell'ellisse
        dato l'angolo al centro    
        """
        a=width/2*perc/100
        c=a*e
        b=math.sqrt(a*a-c*c)
        x1=width/2-a
        y1=(height/2-b)
        x2=width/2+a
        y2=(height/2+b)
        return (x1,y1,x2,y2,a*math.cos(angolo)+self.width/2,b*math.sin(angolo)+self.height/2)
        
    def muovi(self):
        """
        Posizionamento assoluto di un oggetto self.carrera in un orbita ellittica al 90% dell'ampiezza
        """
        self.angolo -= self.coefficiente*math.pi/180
        self.eccentricita = 0.9
        coord= self.coord_ell(self.eccentricita, self.width, self.height, self.angolo, 90)
        self.carrera.update(self.canvas, self.carrera_photo, -self.angolo*180/math.pi)
        self.canvas.coords('myCarrera',coord[4],coord[5])
        self.timer = self.root.after(50, self.muovi)

    def accelera(self):
        self.coefficiente*=1.1
        self.label2.config(text = 'velocità : + %2.2f' % (self.coefficiente))

    def decelera(self):
        self.coefficiente*=0.9
        self.label2.config(text = 'velocità : + %2.2f' % (self.coefficiente))


    def mainloop(self):
        self.root.mainloop()

 


app = MyApp()
app.mainloop()

Tuesday, February 11, 2014

I widget di Tkinter. Metodi comuni.

Tutti i widget istanziabili in Tkinter possiedono alcuni metodi base, utilizzabili per definire o modificare il comportamento base del widget stesso.

Abbiamo già incontrato il metodo config o configure che serve a settare le variabili interne del widget, i colori, le variabili, la grandezza, il binding e, come abbiamo visto nella piccola applicazione che creava un semaforo simulato, permette anche di recuperare le informazioni di settaggio, se richiesto senza parametri, restituendo una tupla.

Un metodo più rapido è cget:

widget.cget('opzione').

L'uso di cget, eventualmente combinata con config, permette di ricavare i valori di istanza associati ad un determinato widget.

Altro metodo che abbiamo già incontrato è mainloop, che mantiene in ciclo indefinito l'istanza di Tk() mantenendo l'applicazione in uno stato di ascolto di eventi.

Un metodo senza dubbio indispensabile è .after, che consente la temporizzazione di chiamate a funzioni di callback dopo un numero predefinito di millisecondi. Questo metodo fornisce un timer che svincola il programma da interventi dell'utente, come gli eventi del mouse o della tastiera e provvede quindi una modalità di animazione del software utile per scandire cambiamenti di stato delle variabili o altro tipo di segnali.

Per darvi modo di apprezzare l'uso di questi metodi ho riscritto l'applicazione 'semaforo' in modo da includerne alcuni. In particolare il metodo after, che temporizza la durata del segnale luminoso, avviando quindi il ciclo, e l'istruzione cget per il recupero della variabile [foreground] del widget button. Ho pensato anche di migliorare la classe che crea l'applicazione, spostando all'interno della class la creazione dell'istanza di Tk e definendo un metodo apposito per il mainloop.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- coding: utf-8 -*-
import Tkinter

class MyApp:

    def __init__(self):
        
        self.root = Tkinter.Tk()
        self.frame1 = Tkinter.Frame()
        self.frame1.pack()
        
        self.button1 = Tkinter.Button(self.frame1,font="Courier 36",
                                      text=u"\u25cf",
                                      foreground="grey",
                                      activeforeground="grey",
                                      background="grey",
                                      )
        self.button1.pack(side='left')
        self.label1 = Tkinter.Label(self.frame1, text="spento")
        self.label1.pack(side='left')

        self.frame2 = Tkinter.Frame()
        self.frame2.pack()
        self.button2 = Tkinter.Button(self.frame2, font="Courier 36",
                                      text=u"\u25cf",
                                      foreground="grey",
                                      activeforeground="grey",
                                      background="grey",
                                      )
        self.button2.pack(side='left')
        self.label2 = Tkinter.Label(self.frame2, text="spento")
        self.label2.pack(side='left')

        self.frame3 = Tkinter.Frame()
        self.frame3.pack()
        self.button3 = Tkinter.Button(self.frame3,font="Courier 36",
                                      text=u"\u25cf",
                                      foreground="green",
                                      activeforeground="green",
                                      background="grey",
                                      )
        self.button3.pack(side='left')
        self.label3 = Tkinter.Label(self.frame3, text="acceso")
        self.label3.pack(side='left')

        self.root.after(5000,self.change)

    def mainloop(self):
        self.root.mainloop()

 
    def change(self):

        self.label1.config(text='spento')
        self.label2.config(text='spento')
        self.label3.config(text='spento')
        b1_c=self.button1.cget('foreground')
        b2_c=self.button2.cget('foreground')
        b3_c=self.button3.cget('foreground')
        
        if b3_c=='green':
            if b2_c=='grey':
                self.button3.config(foreground='green', activeforeground='green')
                self.button2.config(foreground='yellow',activeforeground='yellow')
                self.label2.config(text='acceso')
                self.label3.config(text='acceso')
                self.button1.config(foreground='grey')
                self.root.after(2000,self.change)
                
            else:
                self.button3.config(foreground='grey', activeforeground='grey')
                self.button2.config(foreground='grey', activeforeground='grey')
                self.button1.config(foreground='red',activeforeground='red')
                self.label2.config(text='spento')
                self.label1.config(text='acceso')
                self.root.after(5000,self.change)
        else:
            self.button3.config(foreground='green', activeforeground='green')
            self.label3.config(text='acceso')
            self.button2.config(foreground='grey', activeforeground='grey')
            self.button1.config(foreground='grey', activeforeground='grey')
            self.root.after(5000,self.change)


app = MyApp()
app.mainloop()

How to create a virtual linux machine with qemu under Debian or Ubuntu with near native graphics performance

It's been a long time since my latest post. I know, I'm lazy. But every now and then I like to publish something that other people c...