Appunti wxPython Documentation

Transcript

Appunti wxPython Documentation
Appunti wxPython Documentation
Release 1
Riccardo Polignieri
July 07, 2016
Contents
1
Novità e cambiamenti (man mano che ce ne sono...).
2
Introduzione.
2.1 Licenza. . . . . . . . . . . . . . .
2.2 Che cos’è wxPython? . . . . . . .
2.3 Convenzioni usate in questi appunti.
2.4 Piccolo glossario. . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
4
4
Documentazione wxPython.
3.1 La demo. . . . . . . . . . . . .
3.2 Gli altri esempi. . . . . . . . .
3.3 La documentazione wxPython.
3.4 La documentazione wxWidget.
3.5 Events in Style! . . . . . . . . .
3.6 Libri wxPython. . . . . . . . .
3.7 Siti wxPython. . . . . . . . . .
3.8 Un buon editor. . . . . . . . . .
3.9 La vecchia buona shell. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
7
8
8
8
8
8
9
9
4
Gli strumenti da non usare.
4.1 Boa Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 wxGlade. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3 XmlResource. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
11
11
12
5
Appunti wxPython - livello base
5.1 wx.App: le basi da sapere. . . . . . . . . . . . .
5.1.1
Che cosa è una wx.App e come lavorarci.
5.1.2
Il MainLoop: il motore della gui. . . . .
5.1.3
L’entry-point di un programma wxPython.
5.2 La catena dei “parent”. . . . . . . . . . . . . . . .
5.2.1
Dichiarare il “parent”. . . . . . . . . . .
5.2.2
Orientarsi nell’albero dei “parent”. . . . .
5.2.3
Le finestre top-level. . . . . . . . . . . .
5.3 Frame, dialoghi, panel: contenitori wxPython. . .
5.3.1 wx.Frame. . . . . . . . . . . . . . . . .
5.3.2 wx.Panel. . . . . . . . . . . . . . . . .
5.3.3 wx.Dialog. . . . . . . . . . . . . . . .
5.4 Gli Id in wxPython. . . . . . . . . . . . . . . . .
13
13
13
14
15
15
16
17
17
18
18
19
21
23
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
i
5.4.1
Assegnare gli Id. . . . . . . . . . . . . . . . . . . .
5.4.2
Lavorare con gli Id. . . . . . . . . . . . . . . . . . .
5.4.3
Quando gli Id possono tornare utili. . . . . . . . . .
5.5 I flag di stile. . . . . . . . . . . . . . . . . . . . . . . . . . .
5.5.1
Che cos’è una bitmask. . . . . . . . . . . . . . . . .
5.5.2
Conoscere i flag di stile di un widget. . . . . . . . .
5.5.3
Sapere quali stili sono stati applicati a un widget. . .
5.5.4
Settare gli stili dopo che il widget è stato creato. . . .
5.5.5
Che cosa sono gli extra-style. . . . . . . . . . . . . .
5.6 I sizer: le basi da sapere. . . . . . . . . . . . . . . . . . . . .
5.6.1
Non usate il posizionamento assoluto. . . . . . . . .
5.6.2
Usate i sizer, invece. . . . . . . . . . . . . . . . . .
5.6.3
Che cosa è un sizer. . . . . . . . . . . . . . . . . . .
5.6.4 wx.BoxSizer: il modello più semplice. . . . . . .
5.6.5 Add in dettaglio. . . . . . . . . . . . . . . . . . . .
5.7 Gli eventi: le basi da sapere. . . . . . . . . . . . . . . . . . .
5.7.1
Gli attori coinvolti. . . . . . . . . . . . . . . . . . .
5.7.2 Bind: collegare eventi e callback, in pratica. . . . .
5.7.3
Altri modi di usare Bind. . . . . . . . . . . . . . .
5.7.4
Sapere quali eventi possono originarsi da un widget.
5.7.5
Estrarre informazioni su un evento nel callback. . . .
5.7.6
Un esempio conclusivo. . . . . . . . . . . . . . . . .
5.8 I menu: le basi da sapere. . . . . . . . . . . . . . . . . . . .
5.8.1
Come creare una barra dei menu. . . . . . . . . . . .
5.8.2
Come creare i menu. . . . . . . . . . . . . . . . . .
5.8.3
Come creare le voci di menu. . . . . . . . . . . . . .
5.8.4
Come creare un separatore. . . . . . . . . . . . . . .
5.8.5
Come creare un sotto-menu. . . . . . . . . . . . . .
5.8.6
Collegare le voci di menu a eventi. . . . . . . . . . .
5.8.7
Conclusione. . . . . . . . . . . . . . . . . . . . . .
5.9 I menu: altri concetti di base. . . . . . . . . . . . . . . . . .
5.9.1
Scorciatoie da tastiera. . . . . . . . . . . . . . . . .
5.9.2
Disabilitare i menu. . . . . . . . . . . . . . . . . . .
5.9.3
Voci di menu spuntabili o selezionabili. . . . . . . .
5.9.4
Ranged events per i menu. . . . . . . . . . . . . . .
5.9.5
Conclusione. . . . . . . . . . . . . . . . . . . . . .
5.10 Questioni varie di stile. . . . . . . . . . . . . . . . . . . . . .
5.10.1 To self or not to self? . . . . . . . . . . . . . .
5.10.2 Costruire il layout nell’__init__ o no? . . . . . .
6
ii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
23
24
25
28
28
29
29
30
30
31
31
31
32
32
33
35
35
38
39
39
40
40
41
41
42
42
43
43
44
46
46
46
49
50
52
53
53
53
54
Appunti wxPython - livello intermedio
6.1 wx.App: concetti avanzati. . . . . . . . . . . . . . . . . . . . . .
6.1.1 wx.App.OnInit: il bootstrap della vostra applicazione.
6.1.2 wx.App.OnExit: gestire le operazioni di chiusura. . . .
6.1.3
Re-indirizzare lo standard output/error. . . . . . . . . . . .
6.2 Chiudere i frame e gli altri widget. . . . . . . . . . . . . . . . . . .
6.2.1
La chiusura di una finestra. . . . . . . . . . . . . . . . . .
6.2.2
Chiamare Veto() se non si vuole chiudere. . . . . . . .
6.2.3
Ignorare il Veto() se si vuole chiudere lo stesso. . . . . .
6.2.4
Essere sicuri che una finestra si chiuda davvero. . . . . . .
6.2.5
Distruggere un singolo widget. . . . . . . . . . . . . . . .
6.3 Terminare la wx.App. . . . . . . . . . . . . . . . . . . . . . . . .
6.3.1
La chiusura “normale”. . . . . . . . . . . . . . . . . . . .
6.3.2
Come mantenere in vita la wx.App. . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
57
57
57
59
60
64
64
66
67
67
69
70
70
72
6.4
6.5
6.6
6.7
6.8
6.9
7
6.3.3
Altri modi di terminare la wx.App. . . . . . . . . . . . . . . . . . . . . . . . . . .
6.3.4
Situazioni di emergenza. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Le dimensioni in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.4.1 wx.Size: la misura delle dimensioni. . . . . . . . . . . . . . . . . . . . . . . . . .
6.4.2
Gli strumenti per definire le dimensioni. . . . . . . . . . . . . . . . . . . . . . . . .
6.4.3 Fit e Layout: ricalcolare le dimensioni. . . . . . . . . . . . . . . . . . . . . . . .
I sizer: seconda parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.1 wx.GridSizer: una griglia rigida. . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.2 wx.FlexGridSizer: una griglia elastica. . . . . . . . . . . . . . . . . . . . . .
6.5.3 wx.GridBagSizer: una griglia ancora più flessibile. . . . . . . . . . . . . . . . .
6.5.4 wx.StaticBoxSizer: un sizer per raggruppamenti logici. . . . . . . . . . . . .
6.5.5 StdDialogButtonSizer e CreateButtonSizer: sizer per pulsanti generici.
6.5.6 wx.WrapSizer: un BoxSizer che sa quando “andare a capo”. . . . . . . . . . .
6.5.7
Esempi di utilizzo dei sizer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5.8 wx.SizerItem, e modificare il layout a runtime. . . . . . . . . . . . . . . . . . .
Gli eventi: concetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.1
La propagazione degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.2
Come un evento viene processato. . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.3
Riassunto dei passaggi importanti. . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.4
Come funziona Skip(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.5
Un esempio per Skip(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.6 Bind e la propagazione degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.7 Bind per gli eventi “non command”. . . . . . . . . . . . . . . . . . . . . . . . . .
6.6.8
Un esempio finale per la propagazione degli eventi. . . . . . . . . . . . . . . . . . .
I menu: concetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.7.1
Icone nelle voci di menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.7.2
Menu contestuali e popup. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.7.3
Manipolare dinamicamente i menu. . . . . . . . . . . . . . . . . . . . . . . . . . .
6.7.4
Come “fattorizzare” la creazione dei menu. . . . . . . . . . . . . . . . . . . . . . .
Validatori: prima parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.8.1
Come scrivere un validatore. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.8.2
Quando fallisce una validazione a cascata. . . . . . . . . . . . . . . . . . . . . . . .
6.8.3
La validazione ricorsiva. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.8.4 SetValidator: cambiare il validatore assegnato. . . . . . . . . . . . . . . . . . .
6.8.5
La validazione automatica dei dialoghi. . . . . . . . . . . . . . . . . . . . . . . . .
6.8.6
Consigli sulla validazione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Validatori: seconda parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.9.1
Trasferimento dati nei dialoghi con validazione automatica. . . . . . . . . . . . . . .
6.9.2
Trasferimento dati negli altri casi. . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.9.3
Conclusioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Appunti wxPython - livello avanzato
7.1 I constraints: un modo alternativo di organizzare il layout. . . . . . . . . . . . . . .
7.1.1 wx.IndividualLayoutConstraint e wx.LayoutConstraints.
7.1.2
Quando i constraints possono tornare utili. . . . . . . . . . . . . . . . . . .
7.2 Gli eventi: altre tecniche. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.1
Lambda binding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.2
Partial binding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.3
Event Manager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.4
Eventi personalizzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3 Gli eventi: altre tecniche (seconda parte). . . . . . . . . . . . . . . . . . . . . . . .
7.3.1
Filtri. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.2
Blocchi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.3
Categorie. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
74
74
75
75
75
76
77
77
78
78
79
79
80
81
81
82
83
83
86
86
87
89
91
91
92
92
93
96
97
99
100
101
103
103
103
106
109
109
113
114
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
115
115
115
117
119
119
119
120
120
125
125
127
128
iii
7.3.4
Handler personalizzati. . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.3.5
Un esempio finale per la propagazione degli eventi (aggiornato). . . . . .
7.4 Il loop degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.4.1
Il loop degli eventi e il main loop dell’applicazione. . . . . . . . . . . . .
7.4.2 Yield e i suoi compagni. . . . . . . . . . . . . . . . . . . . . . . . . .
7.4.3
Loop secondari. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.4.4
Creare loop degli eventi personalizzati. . . . . . . . . . . . . . . . . . . .
7.4.5
Perché manipolare il loop degli eventi? . . . . . . . . . . . . . . . . . . .
7.5 Come integrare event loop esterni in wxPython. . . . . . . . . . . . . . . . . . . .
7.5.1
Il problema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.5.2
Soluzione 1: usare Yield. . . . . . . . . . . . . . . . . . . . . . . . . .
7.5.3
Soluzione 2: catturare wx.EVT_IDLE. . . . . . . . . . . . . . . . . . .
7.5.4
Soluzione 3: usare un timer. . . . . . . . . . . . . . . . . . . . . . . . .
7.5.5
Soluzione 4: gestire manualmente gli eventi. . . . . . . . . . . . . . . . .
7.5.6
Soluzione 5: rovesciare il rapporto tra loop principale e ospite. . . . . . .
7.5.7
Soluzione 6: usare un thread separato. . . . . . . . . . . . . . . . . . . .
7.5.8
Integrare altri framework in wxPython. . . . . . . . . . . . . . . . . . . .
7.6 Chiudere i widget: aspetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . .
7.6.1
Distruzione di finestre a cascata. . . . . . . . . . . . . . . . . . . . . . .
7.6.2
Trappole legate alla distruzione dei widget. . . . . . . . . . . . . . . . .
7.7 Logging in wxPython (1a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.7.1
Logging con Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.7.2
Re-indirizzare il log verso la gui. . . . . . . . . . . . . . . . . . . . . . .
7.7.3
Loggare da thread differenti. . . . . . . . . . . . . . . . . . . . . . . . .
7.7.4
In conclusione... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.8 Logging in wxPython (2a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.8.1
Logging con wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.8.2
Cambiare il log target. . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.8.3
Scrivere un log target personalizzato. . . . . . . . . . . . . . . . . . . . .
7.8.4
Il conclusione: come loggare in wxPython. . . . . . . . . . . . . . . . .
7.9 Gestione delle eccezioni in wxPython (1a parte). . . . . . . . . . . . . . . . . . .
7.9.1
Il problema delle eccezioni Python non catturate. . . . . . . . . . . . . .
7.9.2 try/except in wxPython non funziona sempre come vi aspettate. . . .
7.9.3
Che cosa fare delle eccezioni Python non gestite. . . . . . . . . . . . . .
7.10 Gestione delle eccezioni in wxPython (2a parte). . . . . . . . . . . . . . . . . . .
7.10.1 wx.PyAssertionError: gli assert C++ tradotti in Python. . . . . . .
7.10.2 wx.PyDeadObjectError e il problema della distruzione dei widget. .
7.10.3 Le “%typemap” di SWIG e il type checking in wxPython. . . . . . . . .
7.10.4 Consigli conclusivi su logging e gestione delle eccezioni. . . . . . . . . .
7.11 Pattern: Publisher/Subscriber. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.11.1 Che cosa è pub/sub. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.11.2 wx.lib.pubsub: l’implementazione wxPython di pub/sub. . . . . . .
7.11.3 Un esempio di architettura pub/sub in wxPython. . . . . . . . . . . . . .
7.11.4 Messaggi pub/sub ed eventi wxPython. . . . . . . . . . . . . . . . . . .
7.11.5 Event Manager: a metà strada tra eventi e pub/sub. . . . . . . . . . . . .
7.11.6 In conclusione... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.12 Un tour delle funzioni globali di wxPython. . . . . . . . . . . . . . . . . . . . . .
7.12.1 Static method esposti come funzioni globali. . . . . . . . . . . . . . . . .
7.12.2 Funzioni Pre[WidgetName] per la two-step creation. . . . . . . . . .
7.12.3 Date e orari. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.12.4 Logging. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.12.5 Drag & Drop. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.12.6 Finding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.12.7 Scorciatoie per vari dialoghi. . . . . . . . . . . . . . . . . . . . . . . . .
iv
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
129
132
133
133
136
138
140
141
142
142
144
145
145
146
147
148
150
151
152
153
157
158
159
162
162
162
162
165
168
171
171
171
172
173
175
176
177
179
180
181
181
182
183
184
186
189
189
190
191
191
191
191
192
192
7.12.8
7.12.9
7.12.10
7.12.11
7.12.12
7.12.13
7.12.14
8
9
Costruttori di sizer item. . .
Font. . . . . . . . . . . . . .
Primitive per il disegno. . . .
Immagini e colori. . . . . .
Eventi, thread di esecuzione.
Informazioni sul sistema. . .
Varie. . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
192
193
193
194
194
195
195
Ricette wxPython
8.1 Catturare tutti gli eventi di un widget. . . . . . . . . . . . .
8.2 Un widget per selezionare periodi di tempo. . . . . . . . . .
8.3 Un pulsante che controlla le credenziali prima di procedere.
8.3.1
La prima versione. . . . . . . . . . . . . . . . . .
8.3.2
Approfondiamo il problema. . . . . . . . . . . . .
8.3.3
La seconda versione. . . . . . . . . . . . . . . . .
8.4 Convertire le date tra Python e wxPython. . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
197
197
198
200
200
201
202
203
TODO list.
9.1 Argomenti che vorrei trattare in futuro. . .
9.1.1
Grandi temi. . . . . . . . . . . . .
9.1.2
Argomenti più specifici. . . . . .
9.2 Rimandi ai “todo” nelle pagine già scritte. .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
205
205
205
205
205
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
v
vi
CHAPTER 1
Novità e cambiamenti (man mano che ce ne sono...).
Questi appunti sono stati scritti in una prima grande “infornata” nel novembre-dicembre 2012. Poi non ho più aggiunto
nulla per molto tempo.
In questa pagina trovate una cronologia delle successive aggiunte.
7 luglio 2016:
• due pagine sul logging;
• due pagine sulla gestione delle eccezioni;
• una pagina di considerazioni avanzate sulla chiusura dei widget;
• un paragrafo sul reindirizzamento dello standard output aggiunto alla pagina sulla wx.App.
9 giugno 2016:
• una precisazione da tempo dovuta su if __name__ == ’__main__’ nella pagina sulla wx.App, e
di conseguenza
• la clausola if __name__ == ’__main__’ è stata aggiunta ovunque, per non suggerire involontariamente cattive abitudini;
• una tour delle funzioni globali di wxPython, che a sua volta ha provocato piccole modifiche ad alcune altre
pagine, tra cui
• un nuovo paragrafo sulla chiusura dell’applicazione in situazioni di emergenza.
24 maggio 2016:
• una nuova pagina sui constraints;
• aggiunto un paragrago su wx.WrapSizer e uno su wx.SizerItem alla pagina sui sizer.
10 dicembre 2015:
• una pagina nuova sul pattern Publisher/Subscriber;
• indice ristrutturato.
29 novembre 2015:
• una pagina nuova di tecniche inconsuete per gli eventi (tra cui Yield);
• una pagina dedicata al loop degli eventi e alla sua manipolazione;
• una pagina che spiega tecniche avazate di integrazione di altri loop degli eventi in wxPython;
• aggiunto un esempio alla pagina degli argomenti avanzati sugli esempi;
1
Appunti wxPython Documentation, Release 1
• aggiornata la ricetta del pulsante che chiede la password, con una implementazione che fa uso delle
tecniche avanzate sugli eventi.
4 marzo 2015:
• tre nuove pagine sui menu.
27 febbraio 2015:
• questa pagina;
• nuova ricetta per la conversione delle date;
• nuova ricetta per un pulsante che chiede la password prima di procedere.
dicembre 2012:
• tutto quello che non è elencato nelle successive aggiunte.
2
Chapter 1. Novità e cambiamenti (man mano che ce ne sono...).
CHAPTER 2
Introduzione.
Benvenuti. Queste note non sono un manuale completo: però cerco di essere sistematico e andare per gradi, quindi
ecco... diciamo che aspirano a essere un manuale. Se non completo almeno buono, e comunque l’unico in italiano di
un certo “spessore” (almeno a mia conoscenza).
Questa prima pagina contiene le solite note introduttive... se non sapete nulla di wxPython, cominciate da qui. Altrimenti, leggete il glossario in fondo, e per il resto basta un’occhiata veloce.
2.1 Licenza.
Questi appunti sono distribuiti con licenza Creative Commons BY-NC-SA 3.0. Detto in breve, siete liberi di tagliare
e copiare e incollare e modificare... ma dovete sempre citarmi ((c) Riccardo Polignieri - 2012), e non potete fare uso
commerciale di queste cose.
2.2 Che cos’è wxPython?
wxPython è un GUI framework: un set organizzato di strumenti per scrivere l’interfaccia grafica delle vostre applicazioni. E’ il porting per Python di wxWidget, uno dei più “vecchi” e consolidati gui framework in circolazione,
essendo nato nel lontano 1992. wxWidgets è scritto in C++, e oltre che per Python, ne esistono bindings per Perl,
Ruby, Java, PHP, C#, Haskell, e molti altri linguaggi/ambienti.
wxWidgets/wxPython è “abbastanza” multipiattoforma: funziona sotto Windows (32 e 64 bit), MacOS, Linux, Solaris,
OS/2, e molti altri. Però manca ancora il supporto per Android e iOS, e quindi è (al momento) ancora ancorato al
mondo desktop.
wxWidgets non ha un toolkit grafico suo proprio, ma i vari port sono realizzati “appoggiandosi” a diversi toolkit
esterni: su Windows si utilizzano le Win API, su Mac Carbon o Cocoa, su Linux si usa GTK, etc. Questa è in generale
un’ottima idea: vuol dire che ogni utente ritroverà nella vostra applicazione il look-and-feel del suo desktop abituale.
Questo però vuole anche dire che, se i widget di base sono multipiattaforma in modo trasparente, ci si imbatte spesso
in feature che vengono “tradotte” in modo leggermente diverso sulle diverse piattaforme, o che sono specifiche solo di
una di esse.
L’implementazione più ampia è sicuramente quella per Windows. Questo significa che, se sviluppate in Windows ma
vi interessa mantenere la compatibilità sulle altre piattaforme, dovete prestare particolare attenzione a non introdurre
elementi “nativi” che non hanno corrispondenza altrove.
wxPython traduce wxWidget, permettendo di usarlo nelle applicazioni Python. Questo bindig per molti aspetti è
perfino “superiore” all’originale, perché introduce gran parte della flessibilità e dell’espressività tipiche di Python
rispetto a C++. Inoltre la comunità wxPython, negli anni, si è data molto da fare, e adesso molti widget aggiuntivi
3
Appunti wxPython Documentation, Release 1
sono disponibili solo in wxPython, e altri sono stati riscritti con un’interfaccia “pure python”. Tuttavia c’è un punto
critico importante da considerare per quanto riguarda wxPython: al momento, non è ancora supportato Python 3.
In realtà, wxPython è “antico” quasi quanto wxWidget stesso: le prime versioni risalgono alla metà degli anni ‘90 e,
per dire, giravano ancora con Window 3.1. Questo vuol dire che wxPython è più vecchio di molte parti di Python che
noi oggi diamo per scontate (datetime, per esempio). Nel corso degli anni, molti “wxPython regrets” si sono accumulati, e oggi Robin Dunn è al lavoro per una nuova riscrittura dell’intero framework, che prende il nome di “progetto
Phoenix”. Il supporto per Python 3 potrebbe arrivare con l’arrivo di Phoenix, oppure potrebbe essere anticipato se
Phoenix dovesse dimostrarsi un lavoro troppo lungo.
In ogni caso, oggi wxPython è ancora indietro rispetto allo sviluppo di Python. Oltre a questo, occorre tener presente
che wxPython è pur sempre una sottile buccia di codice Python sopra un framework C++. Per quanto wxPython faccia
un lavoro meraviglioso nell’adattare le cose dietro le quinte, per chi è abituato a scrivere in Python, l’API di wxPython
sembra ben poco... pytonica: getter e setter come se piovesse, costanti globali, eccetera eccetera.
Con questo, sembra che abbia elencato solo i difetti di wxWidgets/wxPython: chiaramente ci sono anche tutti i pregi,
e non sono pochi. Ma se state leggendo queste pagine, do per scontato che siate già convinti a usare wxPython: per
cui, sadicamente, non aggiungo altro.
2.3 Convenzioni usate in questi appunti.
Conviene dirlo subito: wxPython non rispetta la PEP8, e di conseguenza neppure io lo farò. Ecco fatto.
Il motivo è semplice: siccome wxPython traduce un framework C++, ha scelto di mantenere molte delle convenzioni
di wxWidgets. In particolare, non solo i nomi delle classi, ma anche i nomi dei metodi sono CamelCase in wxPython.
Per questi appunti, adotterò la strategia che uso di solito nel mio codice: i nomi wxPython restano ovviamente CamelCase, ma i nomi delle funzioni/metodi aggiunti rispettano la PEP8, e quindi sono minuscoli_con_underscore.
Quindi vi capiterà di leggere codice scritto così:
class MyWidget(wx.Button):
# le classi sono sempre CamelCase
def SetLabel(self, val): # metodo wxPython -> CamelCase
pass
def on_click(self, evt): # metodo aggiunto da me -> min_con_underscore
pass
Il motivo è che così si vede subito quali nomi fanno parte dell’API di wxPython, e quali invece sono aggiunti.
Per il resto, non c’è molto da dire. Aggiungo solo che i nomi degli identificatori sono in inglese (come dovrebbe
sempre essere), ma i commenti e le docstring sono in italiano (visto che questa è pur sempre una guida in italiano).
2.4 Piccolo glossario.
Può essere complicato orientarsi nella gergo di wxPython, e a maggior ragione tradurlo in italiano. Ecco un piccolo
glossario delle cose fondamentali:
• “widget” è praticamente qualsiasi cosa che si può vedere: un pulsante, un’etichetta, una finestra, un menu...
In inglese trovate anche “window”, perchè tutto ciò che si può vedere deriva dalla classe-madre wx.Window.
Inutile dire che questo genera confusione, per cui “widget” va molto meglio.
• “frame” è un wx.Frame o affine: la consueta “finestra” a cui siete abituati, con barra del titolo, bordi, caselle
di controllo etc.
4
Chapter 2. Introduzione.
Appunti wxPython Documentation, Release 1
• “dialogo” (cioè, finestra di dialogo) è un wx.Dialog o affine: l’aspetto esteriore può essere identico a quello
di un frame, ma il suo scopo e il suo funzionamento sono diversi.
• “panel” è un wx.Panel, un “pannello” che può contenere altri widget, e che di solito serve da “sfondo” a una
finestra. Non ha né bordi ne barra del titolo.
• “finestra” è usato genericamente per “frame”, “dialogo”, e talvolta anche per “panel”: in pratica, un contenitore.
• “contenitore” è un widget che può contenere altri widget: ossia un dialogo, un frame o un panel, essenzialmente.
• “parent”, “padre” (o “madre”!), “figlio”, si riferiscono al fatto che tutti i widget in wxPython stanno in una
relazione padre-figlio (che non ha niente a che vedere con la normale gerarchia delle classi). Ne parliamo
diffusamente in una pagina apposita.
• “pulsante” è un wx.Button o affine: il normale pulsante di tutte le gui...
• “sizer” è una delle sottoclassi di wx.Sizer: si tratta di un modo intelligente di organizzare il layout dei widget
dentro i contenitori.
• “evento”, in generale, è una azione che l’utente compie sulla vostra gui. In modo più specifico, può riferirsi a
una sotto-classe di wx.Event, o più frequentemente a un binder del tipo wx.EVT_*. Ma la terminologia qui
è complicata, e la spieghiamo meglio nelle pagine dedicate agli eventi.
2.4. Piccolo glossario.
5
Appunti wxPython Documentation, Release 1
6
Chapter 2. Introduzione.
CHAPTER 3
Documentazione wxPython.
wxPython è un framework vasto e complesso, e soprattutto all’inizio può disorientare. Questa sezione elenca una serie
di risorse che dovete assolutamente tenere a portata di mano quando scrivete codice wxPython.
Il problema con tutti questi materiali, ovviamente, è che sono in inglese. Non esiste nulla in italiano... che è poi la
ragione dell’esistenza di questi appunti. Tuttavia non potete prescindere da un po’ di fatica linguistica, se volete fare
qualche progresso con wxPython (e con Python, e con la programmazione in generale).
3.1 La demo.
La demo è il primo posto dove in genere vi conviene guardare. Se non c’è nella demo, le cose si complicano.
La demo è un pacchetto che si scarica e si installa a parte sul sito di wxPython (cercate “wxPython Demo” tra i vari
pacchetti). Una volta aperta, presenta un elenco di esempi organizzati in un albero che potete sfogliare. Ciascun
esempio mostra anche il suo codice sorgente e delle note utili. Potete anche fare delle modifiche direttamente nel
codice, e vedere subito il risultato.
L’unico problema della demo è che spesso gli esempi sono eccessivamente complessi: nello sforzo di dimostrare tutte
le possibilità dei vari widget, il codice si allunga a dismisura, e non è sempre facile orientarsi.
3.2 Gli altri esempi.
Scaricando il pacchetto della demo, installate anche alcune piccole applicazioni di esempio che trovate installate
in .../wxPython2.8 Docs and Demos/samples. Alcuni di questi esempi riguardano tecniche complesse
(“mainloop”, per esempio...), e altri sono applicativi molto estesi (“ide”, “PySketch”); però la maggior parte sono
interessanti e facili da capire. Forse i tre più semplici per iniziare sono:
• “simple” è un’applicazione basilare, a titolo di esempio;
• “doodle” illustra le basi delle tecniche di disegno (“PySketch” è molto più completo, ma è lungo da analizzare);
• “hangman” è una mini-applicazione ben strutturata;
Molto
utili
sono
anche
gli
snippet
raccolti
in
.../wxPython2.8 Docs and
Demos/wxPython/samples/wxPIA_book. Questi sono esempi tratti dal libro WxPython in Action.
L’unico problema di questi esempi è che, essendo tratti dal libro, sono organizzati secondo i capitoli, ed è un po’
noioso provarli tutti per vedere cosa fanno.
7
Appunti wxPython Documentation, Release 1
3.3 La documentazione wxPython.
La documentazione delle API di wxPython è solo online. Potete trovare la versione “ufficiale” generata con Epydoc
a questo indirizzo, ma vi consiglio di quardare prima quella “alternativa” mantenuta da Andrea Gavana qui. Questa
seconda documentazione non è ancora completa, ma è generata con Sphinx, ed è molto più chiara e ricca. Dovrebbe
diventare la documentazione ufficiale della prossima “reincarnazione” di wxPython, il cosiddetto “progetto Phoenix”
di cui si parla ormai da qualche anno.
Ovviamente la documentazione delle API è fondamentale, ma solo se sapete già che cosa state cercando...
3.4 La documentazione wxWidget.
wxPython è il porting per Python del framework C++ wxWidgets. La documentazione delle API di wxWidgets è
molto completa e ricca, ed è compresa nel pacchetto della demo, per cui la trovate installata in .../wxPython2.8
Docs and Demos/docs. In alternativa, potete consultare la versione on line qui.
Il problema qui è che tutta la sintassi, gli esempi, le convenzioni tipografiche etc., si riferiscono al mondo C++. Non
è troppo difficile, con un po’ di allenamento, tradurre al volo nel nostro linguaggio preferito; tuttavia non è neppure
proprio facilissimo.
Tuttavia la documentazione wxWidgets resta in certi casi l’unica vera risorsa. Tra l’altro, al suo interno si trovano
anche delle note specifiche per wxPython (e wxPerl!), nei punti in cui le API differiscono.
3.5 Events in Style!
“Events in Style” è un modulo del sempre geniale Andrea Gavana (disponibile alla pagina, semplicemente salvate la
pagina). E’ una piccola gui che punta alla documentazione online (va usata con una connessione internet) e scarica la
parte relativa agli eventi e agli stili (da cui il nome!) disponibili per ciascun widget.
E’ uno strumento molto comodo per farsi un’idea veloce di due aspetti (eventi e stili, appunto) che di solito nessuno
riesce mai a ricordarsi.
3.6 Libri wxPython.
Ce ne sono due che valgono la pena:
• WxPython in Action, già menzionato sopra, è scritto in collaborazione con Robin Dunn, l’autore di wxPython.
E’ un manuale ben fatto, di livello medio-base. Ci trovate i fondamentali ben spiegati, ma niente di particolarmente esotico.
• wxPython 2.8 Application Development Cookbook di Cody Precord (l’autore dell’IDE Editra, vedi sotto), è un
manuale più avanzato, che offre anche esempi di buone pratiche di programmazione. Leggetelo solo se avete
già un’idea di base di come funziona wxPython. Altrimenti può disorientare.
3.7 Siti wxPython.
Ce ne sono troppi.
8
Chapter 3. Documentazione wxPython.
Appunti wxPython Documentation, Release 1
Il problema qui è che wxPython è un framework anziano e popolare, il che significa che negli anni si è accumulata una
impressionante quantità di materiale, spesso vecchio (vedi alla voce “anziano”) e/o di scarsa qualità (vedi alla voce
“popolare”).
Todo
non riesco a consigliare nessun sito: fare una nuova indagine.
Per dovere di cronaca, devo citare almeno il wiki ufficiale, che però è poco sistematico, e talvolta presenta ancora degli
esempi superati. Tuttavia, molte pagine sono invece assolutamente ben scritte e aggiornate.
3.8 Un buon editor.
Sembra facile, ma se lavorate con un framework complesso come wxPython, scordatevi IDLE. Avete bisogno di un
editor che faccia almeno queste cose:
• code folding: il codice wxPython tende ad essere lungo. Senza il code folding, passerete la vita a fare scrolling
su e giù.
• autocompletion: come per tutti i framework complessi, il problema numero uno è orientarsi nella selva delle
classi e dei metodi. Il problema numero due, è rircordarsi come si scrivono esattamente. Senza l’autocompletion,
siete fritti.
• calltips: o come volete chiamarli, insomma, la docstring della funzione/metodo che appare automaticamente
quando scrivete il nome. Perché il problema numero tre è ricordarsi l’infinità di named arguments che può avere
un metodo wxPython (specialmente un costruttore). Senza i calltips, siete fritti.
Ora, tutti gli editor decenti hanno queste feature: scegliete quello che preferite. Tenete solo a mente che non è il caso
di ricorrere per forza a elefanti come Eclipse. Non è questa la sede per aprire l’eterna discussione su quale editor
utilizzare. Se non avete proprio nessuna idea, potete provare Editra: è un IDE abbastanza completo, scritto da Cody
Precord nientemeno che in wxPython. E’ diventato un po’ l’editor “ufficiale” di wxPython, e quindi è incluso nelle
distribuzioni che scaricate, ma vi conviene visitare il sito per avere la versione più aggiornata, scaricare i plugin, etc.
3.9 La vecchia buona shell.
E infine, non dimenticate di tenervi sempre accanto una shell aperta, quando programmate. Quando siete in dubbio,
dir è sempre vostro amico per un primo orientamento.
Per esempio:
>>> import wx
>>> [i for i in dir(wx.TextCtrl) if 'Background' in i]
vi rivela tutti i metodi disponibili in wx.TextCtrl che in qualche modo riguardano lo sfondo.
3.8. Un buon editor.
9
Appunti wxPython Documentation, Release 1
10
Chapter 3. Documentazione wxPython.
CHAPTER 4
Gli strumenti da non usare.
Avvertenza: questa pagina è “opinionated”, come si dice. La includo in questi appunti per mantenere una risposta
pronta alla domanda che sento spesso: che cosa ne pensi dei “gui builder”? Posso usarli? Semplificano il lavoro?
La risposta breve è no, non usateli, punto. Dopo di che, se volete continuare a leggere, tenete presente che comunque
questa è solo la mia opinione, e siete liberi di tenervi la vostra.
4.1 Boa Constructor.
Non usate Boa Constructor. Ecco.
Prima di tutto, è un progetto vecchio e ormai abbandonato: le ultime attività risalgono al 2007, per dire. Nel frattempo
wxPython è andato molto avanti, e (cosa ancora più importante) nessuno testa più Boa in modo sistematico da una
vita.
Detto questo, Boa cerca di essere un RAD per wxPython, e in quanto tale ha tutti i difetti tipici di un RAD. Produce
codice sporchissimo, sul quale comunque dovrete prima o poi mettere le mani, per qualunque applicazione appena un
po’ completa, perché ci sono molte cose che Boa non supporta o supporta male. Come in tutti i RAD, è difficilissimo
fattorizzare il codice e renderlo modulare e riutilizzabile.
Detto con franchezza, il motivo per cui viene ancora usato e se ne sente parlare ancora così tanto, è che molti si
avvicinano a wxPython con la speranza di trovare una specie di Visual Basic “free”, e Boa è la cosa che somiglia di
più ai RAD chiavi-in-mano.
Ora, non è il caso di ripetere la litania dei motivi per cui usare i RAD è Male. Chiunque programmi in Python dovrebbe
portarsi questa convinzione nel dna. Ma in ogni caso, Boa è poco soddisfacente anche come RAD.
4.2 wxGlade.
E non usate neppure wxGlade.
Devo dire che wxGlade mi sta molto più simpatico di Boa. Si propone come semplice “assemblatore di gui”, senza
avere la pretesa di essere un RAD completo, e questo è senz’altro un bene. Inoltre, di recente sembra aver ripreso un
po’ di attività, quindi non è abbandonato come Boa.
wxGlade ha un’api per generare widget “custom”, ossia non ancora supportati. Il che è un’ottima cosa, tranne naturalmente che ci si può chiedere se vale la pena di studiarsi le api di wxGlade in aggiunta a wxPython.
Anche wxGlade genera codice molto sporco e ripetitivo, e anche in questo caso è probabile che prima o poi finirete
per doverci mettere le mani comunque. Forse è un po’ più facile, rispetto a Boa, generare “porzioni” di interfaccia
riutilizzabili, ma comunque una vera fattorizzazione del codice resta un miraggio.
11
Appunti wxPython Documentation, Release 1
Forse wxGlade può essere adatto a costruire rapidamente interfacce non troppo complesse. Ma il problema è che, con
un po’ di esperienza, proprio le interfacce semplici sono quelle che fate prima a realizzare a mano, producendo codice
più compatto e pulito. Inoltre, se poi l’interfaccia cresce nel tempo e dovete metterci le mani, vi ritrovate a gestire il
solito polpettone di codice illeggibile.
Forse wxGlade può essere di aiuto all’inizio, se si ha paura di perdersi nel mare delle opzioni, metodi e proprietà di
ogni oggetto wxPython. Ma anche in questo caso, probabilmente si fa prima a imparare scrivendo il codice a mano.
4.3 XmlResource.
Molti sostengono che il codice per disegnare le gui sia boilerplate degradante da scrivere. Costoro in genere sostengono
che la gui dovrebbe essere definita da una risorsa esterna (tipicamente un file xml) e caricata dinamicamente dentro il
programma.
Gli fai notare che così bisogna sempre lottare per ficcare in uno schema xml tutte le sottigliezze espressive che puoi
molto più facilmente ottenere con qualche riga di codice. Loro ribattono che, se una gui è complicata al punto da non
poter essere espressa da uno schema xml, vuol dire che è troppo complicata, e andrebbe semplificata. Il che, a mio
avviso, è un po’ come non rispondere.
E quando gli fai notare che scrivere un file xml a mano è di gran lunga più degradante che scrivere codice boilerplate,
vacillano un po’, ma poi ribattono che: basta usare un editor xml! Magari visuale! Insomma, quasi quasi un RAD...
Un altro argomento spesso citato in favore degli schemi xml, è che aiuterebbero a separare la gui dal codice di controllo,
favorendo il pattern “model-controller-view”, autentico sacro graal della programmazione a oggetti (e su questo non
mi permetto di ironizzare, in effetti). Ma questo è un miraggio in wxPython, come in tutti i gui framework. O meglio,
come vedremo, si può in effetti applicare MCV a una applicazione con gui, ma è un processo che passa per strade
molto distanti dalla banale riduzione della gui a un xml. Nel frattempo, lo schema xml con la sua tragica staticità
toglie tutta l’espressività della programmazione dinamica, tutta la flessibilità di poter decidere le cose a runtime.
Todo
una pagina su MVC con collegamento a questa.
Detto questo, wxPython in effetti offre un modo di caricare dinamicamente schemi xml, grazie alla classe
wx.xrc.XmlResource (cercate “xml” nella demo per saperne di più). E se guardate nella directory della demo,
trovate anche .../wxPython2.8 Docs and Demos/scripts/xrced.pyw, che è un grazioso editor visuale di schemi xml correttamente formati per essere usati con wxPython. Potete provarlo: ricorda vagamente
wxGlade.
Ora intendiamoci, i sostenitori delle gui xml hanno le loro ragioni. Ma la mia opinione è che, a ben vedere, sono ragioni
“eterogenee” alla programmazione di interfacce grafiche. Voglio dire, se sei un sistemista, hai sempre scritto software
server-side, e un giorno ti chiedono una gui al volo perché anche i non addetti possano vedere un log di sistema (tu
ovviamente lo scorri con gli occhi, stile “Matrix”), allora capisco che uno schema xml ti sembra la soluzione più
naturale per sbrigare in fretta l’odioso compito.
Ma la programmazione di gui, credeteci o meno, è un’arte tanto quanto il resto della programmazione. Si possono fare
cose molto raffinate, e l’espressività di Python è di grande aiuto in questo.
Quindi in conclusione, no, non usate neppure xml.
Grazie.
12
Chapter 4. Gli strumenti da non usare.
CHAPTER 5
Appunti wxPython - livello base
5.1 wx.App: le basi da sapere.
Questa pagina racconta le pochissime cose assolutamente da sapere sulla wx.App, che è il cuore di ogni applicazione
wxPython.
5.1.1 Che cosa è una wx.App e come lavorarci.
La classe wx.App è il motore della nostra interfaccia grafica. Ogni applicazione wxPython deve avere una, e solo
una, wx.App istanziata e funzionante.
• La wx.App deve essere creata (istanziata) prima di istanziare ogni altra cosa, altrimenti wxPython darà errore;
• La wx.App dovrebbe essere “messa in moto” (chiamando il suo MainLoop come vedremo tra pochissimo)
immediatamente dopo aver mostrato l’interfaccia, altrimenti tutto resterà inerte.
Esaminiamo questo codice, che crea e mostra un frame con un bottone che cambia colore quando viene premuto:
1
2
from random import randint
import wx
3
4
5
6
7
8
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.button = wx.Button(self, -1, 'Hello word!')
self.button.Bind(wx.EVT_BUTTON, self.on_clic)
9
10
11
12
13
def on_clic(self, evt):
self.button.SetBackgroundColour((randint(0, 255),
randint(0, 255),
randint(0, 255)))
14
15
16
17
18
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()
Concentriamoci solo sulle ultime quattro righe. Tutto il resto è abbastanza intuitivo, ma in ogni caso non ci interessa:
basta dire che è il codice necessario per definire le caratteristiche del nostro frame con il pulsante cambia-colore.
La riga 15 crea una istanza della wx.App (non preoccupatevi per il momento di quel False nel costruttore). Solo a
questo punto è possibile creare (istanziare) qualsiasi altro elemento wxPython (se non ci credete, provate a scambiare
tra loro le righe 15 e 16...).
13
Appunti wxPython Documentation, Release 1
La riga successiva crea una istanza del nostro frame (di nuovo... non preoccupatevi del significato di quel None), e
la riga dopo lo mostra sullo schermo. Tutto sembra a posto, ma in realtà niente si è ancora messo in moto. Provate
a togliere l’ultima riga, e fate girare lo script: il frame si visualizza come prima, ma quando provate a cliccare sul
pulsante, nulla accade.
E’ solo quando, alla riga 18, invochiamo il MainLoop della nostra wx.App che le cose si mettono davvero in moto.
5.1.2 Il MainLoop: il motore della gui.
Il MainLoop della wx.App è il ciclo principale della nosta applicazione wxPython. Potete pensarlo come un grande
while True senza fine. A ogni iterazione del ciclo, wxPython controlla se gli elementi della gui si sono mossi, se
sono partiti degli eventi a cui bisogna rispondere, e insomma gestisce tutte le fasi della vita della nostra gui.
Note: Questa è in realtà una semplificazione: va bene nella quasi totalità degli scenari che incontrerete, ma se vi
trovate nella necessità di saperne di più, abbiamo scritto una pagina separata apposta.
Il ciclo termina solo quando l’ultimo elemento della gui viene distrutto (tipicamente, quando l’utente chiude l’ultimo
frame facendo clic sul pulsante di chiusura, con un menu, una scorciatoia da tastiera, o in qualsiasi altro modo).
Prima e dopo il MainLoop, il controllo è mantenuto normalmente dal modulo Python in cui vive il codice. Ma da
quando invocate il MainLoop, il controllo dell’applicazione passa a wxPython, che non lo restituisce fino a quando
non si è usciti dal MainLoop. Per rendervene conto, modificate le ultime righe dell’esempio precedente in questo
modo:
print 'qui siamo fuori dal MainLoop'
print "e il controllo non e' ancora passato a wxPython..."
raw_input('...premere <invio> per tuffarsi dentro wxPython!')
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()
print '...e adesso siamo usciti da wxPython:'
raw_input('premere <invio> per terminare lo script Python.')
Prima di entrare nel MainLoop, la vostra gui non funziona. Ma una volta che ci siete entrati, non è possibile eseguire
altro codice Python che risiede “fuori” da wxPython (a meno di non metterlo in un thread separato, si capisce... ma
questo per il momento è fuori portata per noi).
Todo
una pagina sui thread
Questo comportamento è tipico delle gui, e degli altri framework che devono rispondere a eventi (PyGame, per esempio). Devono stare in attesa delle interazioni dell’utente, e per questo “occupano” costantemente il flusso del
programma con il loro mainloop.
In sostanza, una volta entrati dentro wxPython, tutto deve essere pilotato da “dentro” wxPython. Questo rende più
complicato separare le funzioni delle varie parti del codice, per esempio applicando il pattern Model-View-Controller.
Vedremo in una lezione più avanzata come adattare MVC al contesto di wxPython (e dei gui framework in genere).
Todo
14
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
una pagina su MVC!
Anche se ci sarebbe molto altro da dire sul main loop, per iniziare non è poi molto quello che occorre sapere: il più
delle volte, basta ricordarsi di creare la wx.App e quindi invocare il suo MainLoop. Tutto il resto può essere pilotato
direttamente dalla finestra principale della vostra gui.
Per completare il quadro, abbiamo detto: si esce dal MainLoop quando l’ultimo elemento della gui viene distrutto.
Dovremmo specificare meglio: quando l’ultima finestra “top level” viene chiusa e distrutta. Ma per questo bisogna
prima spiegare meglio il concetto di “top level frame”, e, più in generale, della catena dei “parent”. Dedichiamo a
questo argomento una pagina separata.
5.1.3 L’entry-point di un programma wxPython.
In conclusione, per “ingranare” la nostra applicazione, bastano di solito le tre righe magiche:
app = wx.App(False)
MainFrame(None).Show() # dove MainFrame e' il frame principale dell'applicazione
app.MainLoop()
Il modulo Python che contiene queste righe è quindi l’entry-point del nostro programma wxPython: quello che l’utente
invocherà dalla shell o sui cui farà doppio clic per far partire il programma, insomma.
E’ opportuno ricordare qui che è buona pratica in Python non lasciare mai delle istruzioni “top-level” che comporterebbero dei side effect qualora il modulo dovesse venire importato. Di conseguenza, ricordatevi sempre di inserire il
bootstrap della vostra applicazione nel consueto blocco if __name__ == ’__main__’:
if __name__ == '__main__':
app = wx.App(False)
MainFrame(None).Show()
app.MainLoop()
Potreste chiedervi se questo è davvero necessario: dopo tutto, il modulo entry-point del programma non ha mai
bisogno, per definizione, di essere importato... giusto? In realtà ci sono alcuni scenari in cui questo potrebbe accadere:
per esempio, una suite di test automatizzati potrebbe dover importare anche questo modulo. In ogni caso, come per
tutte le buone pratiche: è sempre meglio seguirle.
Todo
una pagina sui test
Ci sono ancora parecchie cose da sapere sulla wx.App: ma sono argomenti più avanzati che per il momento non vi
servono.
5.2 La catena dei “parent”.
In wxPython, praticamente ogni widget che create deve stare in una relazione padre-figlio con altri widget. Come nella
vita reale, un widget può avere molti figli, ma un solo padre. Questa organizzazione, che è onnipresente, rispecchia
naturalmente lo stato delle cose in una normale interfaccia grafica. Per esempio, un wx.Frame (la finestra che fa da
cornice al resto) potrebbe contenere al suo interno un wx.Panel (lo “sfondo”) in cui potrebbero a loro volta essere
organizzati diversi wx.TextCtrl (caselle di testo), wx.Button (pulsanti), e così via.
Ma non è solo questo. Per esempio, se una finesta apre un wx.Dialog (una finestra di dialogo) per chiedere qualcosa
all’utente, è normale che questo dialogo sia “figlio” della finestra-madre. A loro volta, tutti i widget dentro il dialogo
saranno “figli” di questo, e così via.
5.2. La catena dei “parent”.
15
Appunti wxPython Documentation, Release 1
Queste catene di padri-figli possono essere anche molto lunghe. Il rapporto padre-figlio non è solo una questione di
organizzazione astratta, ma comporta anche delle conseguenze pratiche vistose.
Per esempio, se chiudete una finestra, tutti i suoi “figli” verranno distrutti automaticamente con essa. Questo di solito
è il comportamento che volete, perché altrimenti i vari widget della finestra resterebbero in vita con conseguenze
bizzarre. Ma se la vostra finestra aveva aperto una seconda finestra “figlia”, al momento di chiudere la prima si
chiuderà anche quest’ultima. Anche questo è logico: il principio è che nessun “padre” dovrebbe lasciare in giro figli
“orfani”. Se però non è il comportamento che desiderate, nessun problema: avete sempre il controllo delle relazioni
padre-figlio, quindi potete agganciare la seconda finestra a un nuovo padre, o anche dichiararla “top-level” come
vedremo.
Ma gli effetti delle relazioni padre-figlio si manifestano anche in altre occasioni. Alcuni widget possono trasmettere
automaticamente certe proprietà ai figli. Per esempio, se impostate un particolare font per un wx.Panel, questo verrà
automaticamente trasmesso a tutti i figli. Se chiamate Validate() su un wx.Dialog, tutti i suoi figli verranno
automaticamente “validati” (posto che abbiano un wx.Validator appropriato). E gli esempi potrebbero continuare
a lungo.
Forse però il caso più clamoroso è la propagazione degli eventi. Ce ne occupiamo in modo specifico in una pagina
separata, ma per il momento basta dire che un wx.CommandEvent si propaga dal widget che lo ha originato al
suo genitore, e poi al genitore del genitore, e così via fino all’ultimo genitore “top-level” e poi ancora da questo alla
wx.App. Così, per esempio, se avete un frame con dentro un panel con dentro un pulsante, e ci cliccate sopra, il
wx.EVT_BUTTON si trasmette all’indietro dal pulsante al panel, al frame, e finalmente alla wx.App, permettendovi
di intercettarlo a ogni successiva “fermata”.
La catena dei rapporti padre-figlio è quindi un concentto importante. In questa pagina cerchiamo di analizzare il
problema a fondo.
5.2.1 Dichiarare il “parent”.
Ci sono sostanzialmente due modi per dichiarare che un widget è “genitore” di un altro.
Il primo, di gran lunga il più comune, è al momento della creazione. Tutti i widget di wxPython, quando istanziate
la loro classe, vi chiedono di specificare il loro genitore. Il primo argomento (obbligatorio) che dovete passare al
costruttore è sempre il “parent”. Per esempio:
my_button = wx.Button(my_panel, -1, 'clic me!')
In questo esempio, my_panel (un panel già creato, supponiamo) sarà il “parent” di my_button. Per esempio, un
approccio tipico quando si definisce la classe di un frame, di un dialogo o di un panel, è di creare tutti i widget che
contiene in questo modo:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
a_button = wx.Button(self, -1, 'I am a button')
another_button = wx.Button(self, -1, '...me too...')
a_text = wx.TextCtrl(self, -1, 'I am a text box')
# etc. etc.
Notate quei self passati come primo argomento ai costruttori di ciascun widget: indicano che il “parent” del widget
dovrà essere lo stesso MyFrame (o meglio la sua istanza, appena sarà effettivamente creato).
Soltanto i wx.Frame e i wx.Dialog (insieme a qualche altro contenitore di minore importanza) possono essere
chiamati con parent=None. In questo caso non hanno genitori e sono considerati finestre “top-level”, come vedremo
tra poco. Chiaramente tutte le vostre applicazioni wxPython devono per forza avere almeno una finestra “top-level”,
ossia quella che create per prima:
16
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
app = wx.App(False)
my_first_frame = MyFrame(None, -1, title='Hello Word!') # parent=None !
my_first_frame.Show()
app.MainLoop()
Siete costretti, perché non esiste nessun possibile genitore già creato.
Il secondo modo di dichiare il “parent” di un widget è chiamare SetParent dopo che è stato creato:
my_button.SetParent(some_other_widget)
Questo può essere fatto su qualunque widget (posto che naturalmente non potete impostare SetParent(None) per
qualcosa che non sia un frame o un dialogo). Tuttavia è molto raro nella pratica, ed è sempre sorgente di confusione
ri-aggiustare la catena dei “parent” a runtime. Un caso in cui può essere giustificato è quando volete agganciare una
finestra figlia a un nuovo genitore (o renderla top-level) prima che il suo attuale “parent” venga distrutto.
5.2.2 Orientarsi nell’albero dei “parent”.
Le catene dei “parent” possono essere lunghe e complicate. wxPython mette a disposizione qualche strumento utile
per navigare in questo mare tempestoso.
• il più comune è GetParent (da usare così: my_button.GetParent()) che restituisce il genitore diretto
di un widget qualsiasi (oppure None, se lo chiamate su una finestra top-level).
• GetGrandParent è del tutto analogo, ma restituisce... beh, il nonno.
• GetTopLevelParent (disponibile anche come funzione globale: wx.GetTopLevelParent(widget))
è molto più utile, salta tutta la gerarchia e punta dritto al progenitore “top level”.
• GetChildren, chiamato su un genitore, restituisce l’elenco di tutti i suoi figli (solo i figli diretti: ma potete
chiamare ricorsivamente GetChildren per ricostruire tutta la discendenza di un widget, per esempio).
5.2.3 Le finestre top-level.
Come abbiamo detto, i wx.Frame e i wx.Dialog (e naturalmente tutte le loro sottoclassi) possono ammettere
parent=None. In questo caso sono dette “finestre top-level”, perché non hanno genitori.
In una applicazione possono esserci più finestre top-level contemporaneamente, e sicuramente deve essercene almeno
una. Quando l’ultima finestra top-level viene chiusa, questo è il segnale per wxPython di terminare la wx.App e
chiudere il programma, come analizziamo più approfonditamente altrove.
Proprio perché le finestre “top-level” possono essere diverse, wxPython permette anche di definire, tra queste, una
“finestra regina”, detta “top-window” (da non confondere con “top-level” window). Può esserci sono una “topwindow” aperta in ogni momento, e naturalmente deve trattarsi di una finestra “top-level”.
Di fatto, non c’è nessuna differenza particolare tra la “top-window” e le sue sorelle “top-level”. Per esempio, non è
vero che chiudendo la “top-window” si chiude automaticamente l’applicazione (perché questo avvenga, è necessario
che tutte le “top-level” siano chiuse). Si tratta semplicemente di una convenzione che permette, in presenza di più
“top-level” aperte, di puntare in fretta a una particolarmente importante.
wxPython considera automaticamente “top-window” il primo frame che create. Dopo di che, le varie finestre “toplevel” possono essere gestite con questi metodi e funzioni globali:
• wx.GetTopLevelWindows restituisce una lista delle finestre “top-level” aperte;
• wx.App.GetTopWindow restituisce la “top-window”;
• wx.App.SetTopWindow attribuisce a una “top-level” il ruolo di “top-window” (destituendo automaticamente l’attuale “top-window”);
5.2. La catena dei “parent”.
17
Appunti wxPython Documentation, Release 1
• infine, per promuovere a “top-level” una finestra normale basta chiamare su questa SetParent(None), come
abbiamo visto.
Detto questo, bisogna comunque specificare che, nel mondo reale, di rado c’è bisogno di tutto questo. La maggior
parte delle applicazioni wxPython hanno una sola “top-level”, che è il primo frame che create e mostrate, e che quindi
coincide con la “top-window”. Occasionalmente, potrebbero comparire per breve tempo altri dialoghi “top-level” (una
finestra di login, per esempio), ma si tratta di eccezioni temporanee. Nelle applicazioni di tutti i giorni, è buona norma
limitarsi a una sola “top-level”, anche per semplificare il processo di chiusura della wx.App..
5.3 Frame, dialoghi, panel: contenitori wxPython.
Tra i widget wxPython, alcuni hanno la funzione principale di contenere altri widget, organizzandoli sia graficamente
sia logicamente. I tre più importanti sono wx.Frame, wx.Dialog e wx.Panel.
Oltre a questi ce ne sono molti altri, per lo più ottenuti per sottoclassamento. Per esempio, wx.MiniFrame è un
normale frame, ma con la barra del titolo più stretta; wx.ScrolledPanel è un panel con barre di scorrimento
incorporate, etc. etc. Cercate sulla demo “frame”, “panel” e “dialog” per farvi un’idea.
E’ importante però conoscere bene i tre “fondamentali”, perché costituiscono l’intelaiatura di ogni applicazione wxPython.
5.3.1 wx.Frame.
I wx.Frame (frame per gli amici) sono quello che l’utente, guardando, dice “è la finestra”. In genere la “finestra
principale” della vostra applicazione è un frame, anche se non è necessario (potrebbe essere un dialogo). Molto spesso
i frame sono finestre “top-level”, ma nulla vieta di assegnare anche a loro dei genitori.
Non succede praticamente mai che si crei direttamente un wx.Frame: la procedura normale è invece definire delle
sottoclassi personalizzate, con tutte le caratteristiche volute (e già complete di tutti i widget da inserire nel frame), e
poi istanziare la sottoclasse.
Il costruttore di wx.Frame prevede naturalmente un parametro style, e ci sono moltissimi stili disponibili per
variare l’aspetto e le funzionalità di un frame (e anche alcuni extra-style). Potete consultare la documentazione per
conoscerli tutti. La raccomandazione in ogni caso è di non variare mai troppo l’aspetto base del frame, soprattutto la
finestra principale, per non disorientare (leggi: irritare) l’utente, che si aspetta la normale operatività delle finestre del
suo desktop. Se non passate nessun parametro style, verrà applicato il wx.DEFAULT_FRAME_STYLE.
Oltre alla cornice, nel frame è possibile inserire una wx.MenuBar (ossia una barra dei menu), una wx.ToolBar
(una barra dei pulsanti, in genere sotto i menu) e una wx.StatusBar (una barra di stato in basso).
Todo
una pagina per la toobar e la statusbar?
Tutto lo spazio che resta, ovviamente, deve essere riempito con altri widget “figli”. In genere i widget figli si creano
nell’__init__ del frame, in modo che quando il frame ha terminato il processo di istanziazione e verrà mostrato,
avrà già al suo interno tutti i widget figli che deve contenere. Per esempio:
1
2
3
4
5
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
a_text = wx.TextCtrl(self, pos=(10, 10))
a_button = wx.Button(self, -1, 'Hello Word', pos=(10, 50))
6
7
if __name__ == '__main__':
18
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
8
9
10
11
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()
Alla riga 1, definisco una sottoclasse personalizzata di wx.Frame. Alla riga 2, sovrascrivo l’__init__ per definire
al suo interno i diversi widget “figli” che il frame dovrà contenere. Alla riga 3, mi ricordo di richiamare l’__init__
della classe-madre: questo è sempre necessario al momento di sovrascrivere l’__init__, e non solo dei frame, ma di
qualsiasi widget. Infatti, nell’__init__ avvengono sempre importanti inizializzazioni sul lato C++ del framework,
e quindi è importante limitarsi ad estenderlo, senza sostituirlo del tutto.
Alle righe 4 e 5, creo due widget “figli” che saranno contenuti nel frame. Definisco il rapporto di parentela passando
self come primo argomento (il “parent”). In questo modo saranno figli della futura istanza di MyFrame, quando
verrà creata. Un discorso più ampio sulle catene di relazioni padre-figlio è affrontato in una pagina separata.
Note: in questi esempi minimali, usiamo il cosiddetto “posizionamento assoluto” dei widget, ovvero specifichiamo
la posizione in pixel. Questo è decisamente sconsigliato nel mondo reale. Usate i sizer, invece.
Alle righe 7 e 10, avvio la macchina della wx.App e del suo MainLoop. Di nuovo, potete trovare informazioni più
accurate su questo in un’altra sezione.
Alla riga 8, creo finalmente un’istanza della sottoclasse MyFrame che ho definito sopra. Con questo, wxPython invocherà tutti i procedimenti necessari per disegnare la mia finestra, compresi tutti i figli che ho creato nell’__init__.
Finalmente, alla riga 9, sono pronto a mostrare il mio frame, completo di tutti i widget che deve contenere.
Anche se dentro un frame è possibile mettere qualsiasi widget, in pratica conviene sempre “appoggiare” prima i widget
sopra un wx.Panel, e inserire direttamente nel frame soltanto il panel. In effetti il frame non è fatto per contenere
direttamente i widget. Un motivo potete già vederlo testando il codice dell’esempio qui sopra (almeno se siete in
Windows, è molto evidente). Il frame è “bucato”, nel senso che intorno al pulsante non si vede lo sfondo che ci
aspetteremmo, ma il brutale sfondo del frame (che è immodificabile). Ovviamente potete sistemare i widget in modo
da “tassellare” completamente il contenitore del frame senza lasciare nessun buco, ma questo non è pratico. Sulle
piattaforme diverse da Windows, il colore di sfondo del frame è ideantico al colore di sfondo degli altri widget, per
cui il buco non si vede (ma c’è sempre).
Ma non è solo un problema di estetica. Il fatto è che un frame manca di alcune funzionalità che probabilmente vi
interessano, e di cui invece dispone il panel. Ci arriviamo subito.
5.3.2 wx.Panel.
Se il frame è pensato per presentare la cornice della finestra, il panel ha la funzione di contenere i widget. Come
abbiamo notato qui sopra, anche se il frame può contenere direttamente i widget, in pratica si preferisce sempre
assegnarli a un panel, e poi inserire il panel dentro al frame.
Il panel ha delle funzionalità in più, interessanti. A livello estetico, ha uno sfondo “solido”, il cui colore può essere
modificato a piacere. Ha anche diverse tipologie di bordo, fissabili per mezzo degli stili.
Ma la cosa più interessante è che fornisce di default il comportamento wx.TAB_TRASVERSAL, ovvero la possibilità
di spostarsi tra i vari widget “figli” con il tasto di tabulazione.
Inoltre, un panel può avere tra i suoi figli un pulsante “di default” (chiamando su di esso il metodo SetDefault()),
che si attiva alla pressione del tasto <invio>.
Nel caso più semplice, per usare un panel dentro un frame basta creare un’instanza di wx.Panel nell’__init__
del frame, proprio come si farebbe per qualsiasi altro widget figlio. Dopo di che, tutti gli altri widget saranno assegnati
come figli del panel, e non del frame. Il nostro esempio di sopra diventa quindi:
5.3. Frame, dialoghi, panel: contenitori wxPython.
19
Appunti wxPython Documentation, Release 1
1
2
3
4
5
6
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = wx.Panel(self)
a_text = wx.TextCtrl(panel, pos=(10, 10))
a_button = wx.Button(panel, -1, 'Hello Word', pos=(10, 50))
7
8
9
10
11
12
if __name__ == '__main__':
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()
Notate, alla riga 4, che il il panel è figlio del frame (self), e gli altri widget sono invece figli del panel. Curiosamente
non abbiamo bisogno di specificare una posizione per il panel all’interno del frame. Infatti, quando un contenitore ha
un solo figlio, questo occupa naturalmente tutto lo spazio libero.
Naturalmente, con lo stesso metodo possiamo definire un secondo panel nell’__init__ del frame, e un altro gruppo
di widget da raggruppare. Possiamo inserire quanti panel vogliamo dentro un frame, basta specificare in qualche modo
il layout (con il posizionamento assoluto, oppure, molto meglio, con i sizer). E’ frequente anche l’inserimento di un
panel dentro un altro panel, per creare strutture più complesse.
I panel, nella pratica dello sviluppo di applicazioni efficienti, vengono utilizzati molto per organizzare i widget da
un punto di vista logico, raggruppando insieme quelli che concorrono alla stessa funzionalità del programma. Per
esempio, un panel potrebbe contenere tutti i campi necessari alla scheda anagrafica di una persona (nome, cognome,
indirizzo...). Un altro panel raggruppa invece i campi necessari a registrare la sua posizione nell’azienda (salario, data
di assunzione...). Il panel “anagrafico” potrebbe essere contenuto da solo in un frame “Dati personali”, e il panel
“aziendale” in un altro frame “Dati aziendali”. Ma entrambi i panel potrebbero essere riutilizzati e inseriti in un terzo
frame “Dati completi dell’impiegato”. Questa organizzazione favorisce il riutilizzo del codice e la separazione delle
varie funzioni (per esempio, ciascun panel potrebbe essere collegato a un diverso codice di controllo per il trattamento
dei dati immessi).
Il modo normale per implementare questi “cluster” riutilizzabili di widget consiste semplicemente nel creare sottoclassi personalizzte di wx.Panel che definiscono nel loro __init__ tutti i widget figli di cui hanno bisogno.
Successivamente, il panel personalizzato può essere inserito in un frame come al solito. Per esempio, riscriviamo
ancora una volta il nostro codice, separando il panel dal frame:
1
2
3
4
5
class MyPanel(wx.Panel):
def __init__(self, *a, **k):
wx.Panel.__init__(self, *a, **k)
a_text = wx.TextCtrl(self, pos=(10, 10))
a_button = wx.Button(self, -1, 'Hello Word', pos=(10, 50))
6
7
8
9
10
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = MyPanel(self)
11
12
13
14
15
16
if __name__ == '__main__':
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()
Si noti che adesso i due widget sono figli di self (ma self è il panel, beninteso), e si noti anche l’istanziazione di
MyPanel dentro il frame, alla riga 10.
Il risultato finale sembra identico, e anzi il codice si è allungato un po’. Ma il vantaggio nascosto è che questa volta
MyPanel è una classe separata, pronta a essere riutilizzata ovunque.
20
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
In conclusione, i panel sono un ottimo strumento per organizzare i widget, sia per il layout sia per la logica. Al contrario
di quello che ci si potrebbe aspettare, le applicazioni più estese tendono ad avere poche sottoclassi di wx.Frame,
piuttosto “leggere”, e molte sottoclassi di wx.Panel, ciascuna specializzata a gestire una funzionalità di base e a
esporla all’esterno in un’api coerente. I panel sono i veri e propri mattoni da costruzione di un’applicazione wxPython.
5.3.3 wx.Dialog.
I dialoghi sono delle finestre molto simili ai frame, ma con alcune limitazioni da un lato, e alcune aggiunte dall’altro.
E’ piuttosto facile confondere il comportamento dei frame con quello dei dialoghi, e (ab)usare di uno invece dell’altro.
Bisogna tener presente che la funzione dei dialoghi è di creare interfacce più semplici e “di rapido consumo”, per
chiedere qualche pezzo di informazione all’utente, e poi essere subito distrutti.
Anche se è possibile creare dialoghi molto complessi, è opportuno tenere a mente che wx.Dialog è progettato per
rispondere meglio a certe esigenze. E’ inutile “tirare la corda” e cercare di usare un dialogo per cose per cui sarebbe
più adatto un frame. Per esempio, un wx.Dialog non può avere una toolbar. E’ certamente possibile inserire una
fila orizzontale di piccoli pulsanti quadrati in alto, e mimare una toolbar... Ma a questo punto, perché non usare un
frame, piuttosto?
Ecco un elenco delle cose più importanti che dialoghi e frame hanno in comune:
• possono essere finestre “top-level”; tuttavia è più frequente che i dialoghi abbiano un frame genitore, da cui
sono gestiti (e soprattutto distrutti quando non servono più).
• condividono gli stili necessari per determinare i pulsanti della barra del titolo: in particolare, è possibile mostrare
o nascondere i pulsanti di riduzione a icona, chiusura, etc. E’ anche possibile determinare se sono ridimensionabili.
• possono naturalmente contenere un numero qualunque di altri widget, tra cui panel.
Ecco invece che cosa i dialoghi hanno in meno, rispetto ai frame:
• non possono avere barre dei menu, toolbar e barre di stato;
• non hanno alcuni stili specifici dei frame, per esempio wx.FRAME_TOOL_WINDOW
Ecco le funzionalità che i dialoghi hanno in più rispetto ai frame:
• hanno già le funzionalità dei panel. In pratica, potete pensare ai dialoghi come se avessero già un panel inserito
dentro. Quindi, quando create un widget “figlio” di un dialogo, è come inserire prima il widget dentro un panel,
e poi mettere il panel dentro il dialogo. I widget del dialogo quindi hanno già il “tab-trasversing”, e il default
widget.
• hanno un metodo ShowModal() in aggiunta al metodo Show(), per mostrare il dialogo in forma “modale”
(ossia, nessun’altra azione può essere compiuta se prima non si chiude il dialogo).
• possono fare uso di pulsanti con id predefiniti per chiudersi automaticamente e restituire un codice corrispondente al pulsante premuto.
• se usano pulsanti predefiniti, guadagnano la validazione automatica se il codice restituito è wx.ID_OK.
• ci sono molte sottoclassi predefinite e specializzate per facilitare casi d’uso tipici (chiedere brevi stringhe di
testo, password, selezionare file, colori, etc.: cercate “dialog” nella demo per farvi un’idea).
Ed ecco infine le cose che, semplicemente, sono diverse:
• hanno l’extra-sytle wx.WS_BLOCK_EVENTS settato per default. Il che significa che gli eventi generati dai
widget interni non possono propagarsi al di fuori del dialogo stesso. Questo è in linea con il principio che i
dialoghi dovrebbero sempre “sbrigarsi da soli le proprie faccende”, e limitarsi a restituire al mondo esterno un
codice di uscita.
5.3. Frame, dialoghi, panel: contenitori wxPython.
21
Appunti wxPython Documentation, Release 1
• rispondono diversamente al metodo Close(): un frame chiama automaticamente Destroy(), mentre un
dialogo non si distrugge subito, ma si limita a nascondersi restando ancora in vita. Questo perché è frequente
voler conservare il dialogo, dopo che l’utente lo ha “chiuso”, per raccogliere i suoi dati. Questo significa però
che dovete sempre preoccuparvi di chiamare voi stessi Destroy() quando il dialogo davvero non vi serve più.
La procedura comune per quanto riguarda l’utilizzo di dialoghi personalizzati per raccogliere e gestire dati, è più o
meno questa: si definisce una sottoclasse di wx.Dialog, con tutti i widget necessari (per esempio, caselle di testo,
etc.). Per evitare di dover accedere direttamente ai widget “figli”, conviene dotarla di una interfaccia GetValue che
raccoglie i dati e li presenta in una struttura-dati conveniente (per esempio, un dizionario). Infine, si inseriscono nel dialogo pulsanti di conferma o annullamento, magari con id predefiniti in modo da ottenere facilmente il comportamento
standard di chiusura ed eventuale validazione automatica. Quando l’utente chiude il dialogo, prima di distruggerlo si
accede alla interfaccia GetValue per raccogliere i dati inseriti.
Ecco un esempio minimo di un dialogo che chiede di inserire nome e cognome:
1
2
3
4
5
class YourNameDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
self.first_name = wx.TextCtrl(self)
self.family_name = wx.TextCtrl(self)
6
s = wx.FlexGridSizer(2, 2, 5, 5)
s.AddGrowableCol(1)
s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.first_name, 1, wx.EXPAND)
s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.family_name, 1, wx.EXPAND)
7
8
9
10
11
12
13
s1 = wx.BoxSizer()
s1.Add(wx.Button(self, wx.ID_OK, 'ok'), 1, wx.EXPAND|wx.ALL, 5)
s1.Add(wx.Button(self, wx.ID_CANCEL, 'cancella'), 1, wx.EXPAND|wx.ALL, 5)
14
15
16
17
s2 = wx.BoxSizer(wx.VERTICAL)
s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)
s2.Add(s1, 0, wx.EXPAND|wx.ALL, 5)
self.SetSizer(s2)
s2.Fit(self)
18
19
20
21
22
23
def GetValue(self):
return {'nome'
: self.first_name.GetValue(),
'cognome' : self.family_name.GetValue()}
24
25
26
27
28
29
30
31
32
33
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'inserisci il tuo nome')
b.Bind(wx.EVT_BUTTON, self.on_clic)
34
def on_clic(self, evt):
dlg = YourNameDialog(self, title='Nome e cognome, prego')
retcode = dlg.ShowModal()
if retcode == wx.ID_OK:
data = dlg.GetValue()
else:
data = {}
# print data
dlg.Destroy()
35
36
37
38
39
40
41
42
43
44
22
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
45
46
47
48
if __name__ == '__main__':
app = wx.App(False)
MyTopFrame(None, size=(150, 150)).Show()
app.MainLoop()
Le righe significative sono le 4-5, dove definiamo le caselle di testo in cui andranno inseriti i dati; le 15-16, dove
inseriamo i pulsanti con gli id predefiniti; 24-26, dove definiamo l’interfaccia GetValue che raccoglie di dati e li
presenta in una struttura conveniente.
Con queste premesse, il procedimento di creazione del dialogo e raccolta dei dati è molto lineare. Alla riga 36 creiamo
il dialogo, e alla riga 37 lo mostriamo. Non c’è stato bisogno di collegare esplicitamente i due pulsanti a degli eventi:
siccome hanno id predefiniti, wxPython sa già cosa fare. In entrambi i casi, il dialogo si chiude e ShowModal restituisce l’id del pulsante premuto, wx.ID_OK oppure wx.ID_CANCEL. Nel primo caso, raccogliamo i dati chiamando
GetValue(): non c’è bisogno di accedere direttamente agli elementi interni del dialogo, dal momento che abbiamo
definito un’interfaccia conveniente che si occupa di nascondere i dettagli e presentarci solo i dati che vogliamo. Infine,
distruggiamo esplicitamente il dialogo che ormai non serve più, alla riga 43.
Se l’utente fa clic sul pulsante contrassegnato da wx.ID_OK, avviene anche la validazione automatica, che in questo
caso passa sempre senza conseguenze, perché non abbiamo definito nessun validatore.
I validatori possono inoltre essere una valida alternativa per trasferire i dati dal dialogo alla finestra madre, alla chiusura
(e in senso contrario all’apertura). Parliamo di questo nella sezione apposita.
Rimando infine anche agli esempi della sezione dedicata alla chiusura delle finestre e di quella sui dialoghi con
pulsanti predefiniti.
5.4 Gli Id in wxPython.
In wxPython, ogni widget che create ha un numero (Id) che lo identifica univocamente. E’ una eredità del framework
C++ sottostante, e nel mondo Python questo uso pervasivo di id globali sembra goffo, per non dire altro. Ma non
dovete scordare che in C++ certe comodità di Python non ci sono. Per esempio, in Python voi potete passare il nome
di una funzione come argomento di un’altra funzione, perché le funzioni sono “first class object”. Quindi in wxPython
è normale scrivere cose come:
button.Bind(wx.EVT_BUTTON, self.on_clic)
dove appunto on_clic viene passata come argomento di Bind. Ma in C++ questo proprio non si può fare, e quindi
anche una cosa banale come collegare tra loro un oggetto, un evento e un callback diventa un complicato balletto
eseguito da una macro che può lavorare solo con riferimenti statici: e il riferimento agli oggetti è appunto il loro Id
univoco.
Quindi gli Id ci sono, e ci saranno sempre. wxPython potrebbe decidere di nasconderli completamente (e in effetti ci
sono piani per questo, in qualche punto nel futuro), ma per il momento invece li espone ancora.
In questa pagina diamo uno sguardo alle cose fondamentali da sapere sugli Id, e poi vedremo qualche raro caso in cui
possono ancora tornare comodi.
5.4.1 Assegnare gli Id.
L’Id viene attribuito obbligatoriamente a tutti i widget al momento della loro creazione. In genere, è il secondo
argomento che dovete passare al costruttore (il primo è il “parent”, come vediamo altrove).
Potete scegliere:
• l’opzione più comune è lasciare che wxPython assegni automaticamente l’Id, senza preoccuparvi di sapere qual
è. In questo caso, basta passare la costante wx.ID_ANY (o, più rapidamente, il valore -1) al costruttore:
5.4. Gli Id in wxPython.
23
Appunti wxPython Documentation, Release 1
button = wx.Button(self, -1, 'hello')
• se invece volete conoscere e conservare il valore dell’Id, lasciando comunque che sia wxPython a deciderlo,
potete generare un nuovo Id con la funzione globale wx.NewId():
button_id = wx.NewId()
button = wx.Button(self, button_id, 'hello')
• infine, potete assegnare voi stessi manualmente un numero:
button = wx.Button(self, 100, 'hello')
In quest’ultimo caso, però, dovete stare attenti a molte cose. Primo, wxPython conserva internamente molti Id già preassegnati, e dovete stare attenti a non sovrascriverli. Potete usare tutti i numeri che volete, purché non siano compresi
tra wx.ID_LOWEST e wx.ID_HIGHEST (vi lascio il piacere di scoprire quali sono).
Secondo, se impostate a mano certi Id, e lasciate a wxPython il compito settarne degli altri, dovete fare attenzione che
wxPython non sovrascriva i vostri (o viceversa). La cosa più sicura è determinare tutti gli Id che vi servono prima che
wxPython assegni il suo primo Id, e registrarli con la funzione wx.RegisterId. In questo modo dite a wxPython
di lasciar stare quei numeri. Per esempio, per riservare per il vostro uso cento numeri, potete fare:
# questo prima di ogni altra cosa
for x in range(100, 201):
wx.RegisterId(x)
# in futuro, posso usare gli Id dal 100 al 200
Tuttavia, questo è uno scrupolo inutile nella maggior parte dei casi. In genere wxPython assegna Id negativi progressivi
(a partire da -200, ma può essere variabile). Vi basta assegnare numeri positivi, e siete a posto.
Terzo, tenete conto che, tecnicamente, gli Id dovrebbero essere univoci nello stesso frame, ma tra diversi frame è
permesso duplicarli. Ovviamente il consiglio è cercare comunque di mantenere Id univoci in tutta l’applicazione.
Confesso di non aver mai controllato come si comporta wxPython con gli Id che genera lui: se ricomincia la numerazione a ogni nuovo frame, se si sente libero di riciclare gli Id quando voi distruggete un frame, e così via.
5.4.2 Lavorare con gli Id.
Una volta che il widget è stato creato, e quindi ha ricevuto il suo Id, ci sono pochi idiomi tipici che dovete conoscere.
• per sapere l’Id di un widget, usate GetId()
• per ri-assegnare un Id, potete usare SetId() (ma non dovreste mai averne bisogno)
• la funzione globlale wx.FindWindowById() restituisce un widget se conoscete il suo Id (o None se non
trova niente). Siccome gli Id possono essere ripetuti tra i diversi frame, potete anche passare il riferimento
al frame dentro cui volete cercare (per esempio, wx.FindWindowById(100, my_button) cerca solo
all’interno del frame dove vive my_button). Se non passate niente, la ricerca sarà globale, ma si arresta
appena trova il primo widget con l’Id corrispondente (e non è detto che ce ne siano altri, o che questo sia proprio
quello che vi serve). Se pensate che questo algoritmo sia un po’ bacato, avete trovato un’altra buona ragione per
non usare gli Id.
Lo abbiamo già notato: cose come wx.FindWindowById() possono far sorridere il programmatore Python, che
è abituato a passare in giro riferimenti alle istanze dei vari oggetti, come se fossero delle costanti qualunque. Ma
ricordate che in C++ vi trovate a passare cose statiche (gli Id, appunto), e allora una funzione di ricerca può tornare
utile.
24
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
5.4.3 Quando gli Id possono tornare utili.
Anche in wxPython, ci sono occasioni in cui lavorare direttamente con gli Id è comodo, o addirittura ancora necessario.
Vediamo alcuni casi tipici.
StockButtons.
Voi potete scegliere di non usare gli Id, ma wxWidgets li usa eccome. Ci sono molti Id predefiniti per compiti particolari. Un caso tipico sono gli “StockButtons” (cercate nella demo). In pratica, se create un wx.Button passandogli
come Id uno di quelli predefiniti del tipo wx.ID_*, wxPython aggiungerà la label corrispondente (e userà lo StockButton nativo sulle piattaforme che supportano questo concetto). Per esempio:
copy = wx.Button(parent, wx.ID_COPY)
produrrà un pulsante “copia”, e così via.
L’utilizzo di questo tipo di pulsanti può essere reso ancora più semplice dall’impiego di un sizer generato automaticamente.
Dialoghi con risposte predefinite.
Un utilizzo simile degli Id predefiniti avviene nei dialoghi. Ci sono molti dialoghi “standard” che non avete bisogno
di disegnare nel dettaglio; potete però impostarli perché abbiano certi pulsanti predefiniti. A seconda dei pulsanti che
inserite, il dialogo restituisce alla chiusura l’Id (predefinito) del pulsante premuto, come risultato del metodo Show o
ShowModal: questo vi consente di conoscere la decisione dell’utente, e regolarvi di conseguenza. Per esempio:
msg = wx.MessageDialog(None, -1, 'Vuoi il gelato?', 'Decisioni...',
# questo determina 3 pulsanti: si', no, annulla:
style = wx.YES|wx.NO|wx.CANCEL)
retcode = msg.ShowModal()
if retcode == wx.ID_YES:
# ha premuto si'
...
elif retcode == wx.ID_NO: # ha premuto no
...
else:
# ha premuto annulla (sarebbe wx.ID_CANCEL)
...
msg.Destroy() # dopo aver usato il dialogo, sempre ricordarsi...
Ovviamente l’uso di questi dialoghi (oltre a wx.MessageBox ne esistono altri simili: cercate “Dialog” nella demo
per avere un’idea) è possibile solo grazie all’uso dei vari Id predefiniti. Ci sono wx.ID_OK, wx.ID_CANCEL,
wx.ID_ABORT, wx.ID_YES, wx.ID_NO e altri ancora, che corrispondono alle scelte wx.OK, wx.CANCEL,
wx.ABORT, wx.YES, wx.NO (e la combinazione wx.YES_NO) del parametro style del dialogo.
Dialoghi personalizzati con pulsanti predefiniti.
Chiaramente potete usare questi pulsanti predefiniti (ossia questi Id predefiniti) anche nei dialoghi disegnati da voi.
Ecco un esempio:
1
2
3
4
5
6
7
class IceCreamDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
self.flavor = wx.ComboBox(self, -1, 'crema', style=wx.CB_READONLY,
choices=['crema', 'cioccolato', 'stracciatella'])
ok = wx.Button(self, wx.ID_OK, 'dammi subito il mio gelato!')
cancel = wx.Button(self, wx.ID_CANCEL, 'sono a dieta...')
5.4. Gli Id in wxPython.
25
Appunti wxPython Documentation, Release 1
8
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.flavor, 0, wx.EXPAND|wx.ALL, 15)
s1 = wx.BoxSizer()
s1.Add(ok, 1, wx.EXPAND|wx.ALL, 5)
s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)
s.Add(s1, 0, wx.EXPAND|wx.ALL, 10)
self.SetSizer(s)
s.Fit(self)
9
10
11
12
13
14
15
16
17
def GetValue(self): return self.flavor.GetStringSelection()
18
19
20
21
22
23
24
25
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'scelta gelati')
b.Bind(wx.EVT_BUTTON, self.on_clic)
26
def on_clic(self, evt):
msg = IceCreamDialog(self, title='gelati!')
retcode = msg.ShowModal()
if retcode == wx.ID_OK:
print 'gelato gusto %s in arrivo!' % msg.GetValue()
else:
print 'abbiamo i sorbetti al limone...'
27
28
29
30
31
32
33
34
35
36
37
38
if __name__ == '__main__':
app = wx.App(False)
MyTopFrame(None, size=(150, 150)).Show()
app.MainLoop()
Notate che non abbiamo bisogno di collegare esplicitamente i nostri due pulsanti a qualche evento. Basta assegnare
loro i corretti Id “predefiniti” (righe 6 e 7), e wxPython sa già cosa fare: chiude il dialogo e restituisce l’Id del pulsante
premuto.
Ovviamente questo funziona solo per il pulsanti con Id “predefiniti”: se aggiungete un pulsante con un Id qualsiasi,
per farlo funzionare dovrete collegarlo normalmente a un evento.
Validatori.
Ai validatori dedichiamo una sezione apposta, ma qui basta un appunto per ricordare un altro vantaggio dell’Id predefinito wx.ID_OK. Se nel vostro dialogo inserite un pulsante con questo Id, oltre ai benefici visti sopra, quando si
preme questo pulsante wxPython inserisce anche una validazione automatica del dialogo, prima di chiuderlo.
Ovviamente dovete impostare qualche validatore che faccia davvero un controllo. Per esempio, aggiungete al codice
del paragrafo precedente questo validatore che impedisce di selezionare il gusto “crema”:
1
2
3
4
5
class NoCreamValidator(wx.PyValidator):
def __init__(self): wx.PyValidator.__init__(self)
def Clone(self): return NoCreamValidator()
def TransferToWindow(self): return True
def TransferFromWindow(self): return True
6
def Validate(self, win):
if self.GetWindow().GetStringSelection() == 'crema':
wx.MessageBox('Gusto terminato!', 'Oh no!')
return False
7
8
9
10
26
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
else:
return True
11
12
e poi modificate la creazione di self.flavor aggiungendo il validatore:
self.flavor = wx.ComboBox(self, -1, 'crema', style=wx.CB_READONLY,
choices=['crema', 'cioccolato', 'stracciatella'],
validator=NoCreamValidator())
Come vedete, adesso quando premete il pulsante contrassegnato con wx.ID_OK, ottenete gratis una validazione del
dialogo.
Menu.
Lasciamo alla fine il caso di utilizzo più frequente per gli Id: i menu. Abbiamo dedicato una pagina separata per
approfondire l’uso dei menu. Qui ci limitiamo a qualche nota specifica sugli Id.
Intendiamoci, potete fare del tutto a meno degli Id quando lavorate con i menu. Se create ogni voce separatamente, e
collegate ogni voce a un callback separato, le cose procedono senza intoppi:
menu_item = my_menu.Append(-1, 'crema')
self.Bind(wx.EVT_MENU, self.crema_selected, menu_item)
menu_item = my_menu.Append(-1, 'cioccolato')
self.Bind(wx.EVT_MENU, self.cioccolato_selected, menu_item)
# etc. etc.
Notate l’Id -1 passato a tutte le voci aggiunte.
Capita spesso però che vogliate collegare più voci di menu a uno stesso callback, perché c’è anche un po’ di lavoro
in comune da fare, oppure perché si tratta di voci collegate tra loro (del tipo “check” o “radio”, per intenderci).
Tuttavia, prima o poi nel callback volete capire da quale voce esattamente è partito l’evento. E qui il classico modo
event.GetEventObject(), non funziona nel caso di un wx.EVT_MENU: in effetti, ma non fa altro che restituire
l’istanza del frame in cui appare il menu.
Tuttavia l’evento wx.EVT_MENU trasporta con sé l’Id (e solo quello) della voce che è stata selezionata, per cui se
invece chiedete event.GetId() ottenete un’informazione più precisa... a patto naturalmente di conoscere gli Id
delle singole voci di menu.
Ecco perché spesso si finisce per assegnare esplicitamente gli Id a tutte le voci del menu (a mano, o con wx.NewId();
i più minimalisti assegnano Id solo alle voci che effettivamente verranno raggruppate nei callback).
Oltretutto, se avete l’accortezza di assegnare Id consecutivi alle voci che volete raggruppare in un solo callback,
wxPython offre l’opportunità di collegarle tutte insieme usando wx.EVT_MENU_RANGE, che accetta soltanto Id
(appunto!) come parametri. Qualcosa del genere:
# al momento di creare il menu:
menu.Append(100, 'crema')
menu.Append(101, 'cioccolato')
menu.Append(102, 'stracciatella')
self.Bind(wx.EVT_MENU_RANGE, self.on_menu, id=100, id2=102)
# e poi, nel callback:
def on_menu(self, evt):
caller = evt.GetId()
# etc. etc.
wx.EVT_MENU_RANGE vi evita di collegare le voci una per una allo stesso callback. Naturalmente, un programmatore Python potrebbe semplicemente fare:
5.4. Gli Id in wxPython.
27
Appunti wxPython Documentation, Release 1
for id, label in enumerate(('crema', 'cioccolato', 'stracciatella')):
menu.Append(id+100, label)
self.Bind(wx.EVT_MENU, self.on_menu, id=id)
senza ricorrere a wx.EVT_MENU_RANGE... Ma di nuovo, dovete considerare che avete dalla vostra l’espressività e la
compattezza di Python...
E a proposito di espressività e compattezza, aggiungo che potete evitare del tutto l’uso degli Id con i menu (anche
quando intendete collegare più voci allo stesso callback), facendo uso del trucco del “lambda binding” per passare a
Bind un parametro in più:
# al momento di creare il menu:
for label in ('crema', 'cioccolato', 'stracciatella'):
item = menu.Append(-1, label)
self.Bind(wx.EVT_MENU,
lambda evt, label=label: self.on_menu(evt, label),
item)
# e poi, nel callback:
def on_menu(self, evt, label):
print label # -> restituisce la voce selezionata
5.5 I flag di stile.
I flag di stile sono frequenti in wxPython. Al momento della creazione di un widget qualsiasi, è comune aggiungere un
parametro style al costruttore. Ogni widget ha i suoi flag di stile predefiniti, che si possono combinare per ottenere
varie personalizzazioni.
5.5.1 Che cos’è una bitmask.
Prima di tutto, una piccola spiegazione sulle bitmask: non è un argomento specifico di wxPython, ma serve a capire
meglio il resto. Se avete familiarità con il concetto, saltate questo paragrafo.
Ciascuno stile in wxPython è definito da una costante globale. Per esempio:
>>> import wx
>>> wx.TE_READONLY # lo stile per i TextCtrl di sola lettura
16
Quello che alcuni notano a prima vista, è che tutte queste costanti sono scelte per essere potenze di 2. La cosa è utile
perché le potenze di 2, in notazione binaria, si possono sommare tra loro in modo che ciascun addendo contribuisca
a modificare solo un bit della somma finale. In altre parole, è sempre possibile ricostruire gli addendi a partire dal
risultato.
La comodità è che in questo modo potete combinare gli stili tra di loro, semplicemente sommandoli: dalla somma,
wxPython ricava l’elenco di quelli che avete scelto.
Note: Naturalmente un programmatore Python è abituato a usare strutture di più alto livello e maneggevoli, come le
liste o le tuple, per questi compiti. Ma in C++ queste cose non sono gratis, e le bitmask sono comode e veloci.
Per combinare gli stili, usate l’operatore bitwise OR Python:
wx.TextCtrl(parent, sytle=wx.TE_READONLY|wx.TE_MULTILINE)
28
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
produce una casella di testo multilinea e per sola lettura.
Alcuni stili, per comodità, sono già definiti come il risultato della combinazione di altri. Per esempio, il già citato
wx.DEFAULT_FRAME_STYLE è definito come:
wx.MAXIMIZE_BOX|wx.MINIMIZE_BOX|wx.RESIZE_BORDER|
wx.SYSTEM_MENU|wx.CAPTION|wx.CLOSE_BOX
E quindi vi risparmia un bel po’ di battute di tastiera. Quando lavorate con questi stili composti, talvolta può tornare
utile sottrarre qualche stile dalla lista, invece di aggiungerlo. Per fare questo, potete usare l’operatore XOR:
wx.Frame(parent, style=wx.DEFAULT_FRAME_STYLE^wx.RESIZE_BORDER)
produce un frame “normale” ma senza bordi trascinabili, e:
wx.Frame(parent, style=wx.DEFAULT_FRAME_STYLE^(wx.MAXIMIZE_BOX|
wx.MINIMIZE_BOX|
wx.RESIZE_BORDER))
elimina anche la possibilità di espansione e riduzione a icona.
5.5.2 Conoscere i flag di stile di un widget.
Ogni widget può avere un set di stili personalizzati tra cui scegliere, e non esiste un modo per sapere a runtime quali
sono. Inoltre, siccome gli stili sono solo delle costanti numeriche, non esistono docstring che spiegano rapidamente
che cosa fanno.
Tra l’altro, il nome stesso di “stile” è ingannevole. Alcuni stili effettivamente cambiano solo il look del widget a cui si
applicano, ma molti si riferiscono a caratteristiche strutturali più profonde: wx.LI_VIRTUAL cambia completamente
l’utilizzo di un wx.ListCtrl.
L’unica è affidarsi alla documentazione: per i widget più comuni, la documentazione wxWidget è la più completa.
Per i widget implementati solo in Python, sperimentali, etc., occorre anche provare la demo, o la documentazione
interna del widget (per esempio la docstring della classe o del modulo di solito sono molto chiare). L’utility esterna
EventsinStyle può essere molto comoda da usare, almeno per i widget che supporta.
Spesso è questione di pratica. Per esempio, tutti gli stili di un wx.TextCtrl iniziano con wx.TE_*, e tutti quelli
di un wx.ComboBox iniziano con wx.CB_*. Un buon editor con l’autocompletion in questi casi fa miracoli.
5.5.3 Sapere quali stili sono stati applicati a un widget.
Così come non è possibile sapere a runtime quali sono gli stili disponibili per un widget, non è neppure possibile
sapere quali stili avete applicato al widget una volta che è stato creato. Tuttavia molti widget implementano dei metodi
ausiliari con una funziona analoga.
Per esempio, wx.TextCtrl.IsMultiline() restituisce True se avete settato lo stile wx.TE_MULTILINE.
Ma non dovete farci troppo affidamento. Per esempio, in corrispondenza dello stile wx.TE_READONLY non esiste
nessun metodo IsReadOnly.
Conoscere quali sono questi metodi è ovviamente una questione di sfogliare con pazienza la documentazione, caso per
caso. Ovviamente, un po’ di mestiere aiuta... per esempio, prima di guardare alla cieca, iniziate a sfogliare i metodi
che iniziano con Is* e poi quelli con Get*.
5.5. I flag di stile.
29
Appunti wxPython Documentation, Release 1
5.5.4 Settare gli stili dopo che il widget è stato creato.
Per questo, potete usare il metodo SetWindowStyleFlag, che riceve come argomento una normale bitmask di
stili.
Tuttavia non è un’operazione da fare a cuor leggero. A seconda dei widget e degli stili che volete cambiare, potreste
causare incongruenze gravi. Certe operazioni potrebbero semplicemente fallire. Dovete fare esperimenti caso per
caso, ma in generale non è una pratica consigliabile.
In ogni caso, è molto probabile che dobbiate chiamare Refresh() sul widget, per vedere gli effetti delle modifiche.
5.5.5 Che cosa sono gli extra-style.
Definire gli stili come costanti numeriche che si possono combinare con le bitmask è comodo all’inizio, ma prima o
poi si arriva a un limite: non ci sono tante potenze di 2 in circolazione.
Se il widget ha bisogno di pochi stili, tutto va bene. Tuttavia, man mano che occorrono sempre più stili per le più
svariate necessità di un widget, ci si scontra con i limiti del tipo numerico (long) che C++ riserva per le costanti degli
stili.
Ed ecco che arrivano in soccorso gli “extended style” (o extra style). In pratica si tratta di stili aggiuntivi che non
possono stare nello spazio delle normali bitmask, e vanno quindi aggiunti a parte, con il metodo SetExtraStyle.
Questo metodo va chiamato ovviamente dopo che il widget è stato creato, ma prima di mostrarlo (chiamando Show()
sul widget stesso o sul suo parent contenitore).
Di nuovo, la documentazione è l’unico posto dove potete sapere se un certo widget prevede anche degli extra-style. In
ogni caso, è utile sapere che wx.Window ha alcuni extra-style definiti, e siccome wx.Window è la classe progenitrice
di tutti i widget, questi vengono ereditati da tutta la gerarchia (anche se naturalmente per la stragrande maggioranza
dei widget non hanno alcun significato). Inoltre, anche wx.Frame e wx.Dialog (e quindi le loro sottoclassi dirette)
ne aggiungono alcuni.
• gli extra-style di wx.Window iniziano tutti con wx.WS_EX_*
• gli extra-style di wx.Frame iniziano con wx.FRAME_EX_*
• gli extra-style di wx.Dialog iniziano con wx.DIALOG_EX_*
Questo dovrebbe aiutare un pochino.
Gli extra-style in genere hanno scopi abbastanza esotici, e servono di rado. Alcuni sono platform-specific, per esempio
wx.FRAME_EX_METAL ha effetto solo sul MacOS. Tuttavia ce ne sono alcuni che dovete tener presente:
• wx.WS_EX_VALIDATE_RECURSIVELY dice alla finestra di validare non solo tutti i suoi figli diretti (comportamento di default), ma anche i figli dei figli, etc. Utile quando si usano i validatori, e la finestra contiene
per esempio dei panel con dentro degli altri panel.
• wx.WS_EX_BLOCK_EVENTS dice alla finestra di bloccare la propagazione degli eventi che partono dai suoi
figli. Gli eventi arrivano fin qui, ma poi non si propagano oltre. Notare che i wx.Dialog, a differenza dei
frame, hanno questo flag impostato per default.
• wx.WS_EX_CONTEXTHELP, wx.DIALOG_EX_CONTEXTHELP, wx.FRAME_EX_CONTEXTHELP aggiungono il pulsante della guida rapida alla barra del titolo della finestra.
Infine, c’è un ultimo problema. Gli extra-style, come abbiamo detto, si possono aggiungere con SetExtraStyle
dopo aver creato il widget. Tuttavia ci sono casi in cui non è possibile aggiungere lo stile in un secondo momento,
perché la creazioe del widget determina la sua struttura in modo tale da non poter essere più modificato. E’ il caso
di *_EX_CONTEXTHELP (devo dire di non sapere se ci sono altri casi. Nel dubbio, la documetazione ovviamente
riporta il problema).
30
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
In questi casi, occorre intraprendere una strada ancora più complicata, nota come “two-step creation”, in cui si istanzia
per esempio un wx.PreFrame, gli si attribuiscono gli extra-style voluti, e poi lo si trasforma in wx.Frame aggiungendo gli stili normali. La “two-step creation” è un procedimento non difficile ma comunque avanzato, e può servire
in casi differenti (non solo per settare gli extra-style). Per questo motivo gli dedichiamo una pagina separata.
Todo
una pagina sulla two-step creation.
Note: Tutta questa complicazione degli extra-style a causa della limitazione delle bitmask, non denuncia forse un
problema di design? Risposta breve: sicuramente sì. Detto questo, non è per difendere wxWidgets, ma praticamente
qualsiasi grande framework con molta storia alle spalle accumula “regrets” dovuti a scarsa lungimiranza iniziale.
Quando wxWidgets è nato, le finestre non avevano pulsanti “context help”. Infine, va detto che gli extra-style sono
rari: la stragrande maggioranza dei widget ha 3-4 stili definiti, e le bitmask sono più che sufficienti, lasciando spazio
anche per aggiunte future.
5.6 I sizer: le basi da sapere.
Questa pagina tratta le cose fondamentali da sapere sui sizer, e descrive wx.BoxSizer, il più semplice dei sizer
disponibili in wxPython. I sizer più complessi sono descritti in una pagina separata. Inoltre, per usare i sizer occorre
anche avere un’idea di come si specificano le dimensioni dei widget, argomento che affrontiamo a parte.
5.6.1 Non usate il posizionamento assoluto.
In molti esempi brevi di queste note, per semplicità, facciamo uso del posizionamento assoluto, ossia specifichiamo
direttamente la posizione (in pixel) del widget rispetto al suo parent, al momento della creazione, passando il parametro
pos (ed eventualmente anche size per specificare la dimensione):
button = wx.Button(self, -1, 'clic me', pos=(10, 10))
Il posizionamento assoluto è più rapido, ma non dovrebbe mai essere usato in un programma “serio”. E questo per
almeno due valide ragioni:
• se l’utente vuole ridimensionare la finestra, i widget non scalano di conseguenza, e si crea dello spazio vuoto,
oppure appaiono le barre di scorrimento. Questo problema di usabilità generalmente non è più tollerato nelle
interfacce moderne;
• il posizionamento assoluto vi costringe a calcolare esattamente posizione e dimensione di ogni singolo elemento
dell’interfaccia. Finché lo fate la prima volta va ancora bene, ma se in seguito volete aggiungere o togliere
qualcosa, vi tocca ricalcolare tutto daccapo.
Questo secondo problema si “risolve” in genere usando i RAD. In effetti, l’accoppiata RAD-posizionamento assoluto
è un cavallo di battaglia di certi ambienti di programmazione... molto anni ‘90 di cui sicuramente avete sentito
parlare. Abbiamo già scritto che i RAD non vanno usati, e adesso possiamo aggiungere un motivo: incoraggiano il
posizionamento assoluto.
5.6.2 Usate i sizer, invece.
wxPython supporta, come abbiamo visto, anche il posizionamento assoluto. Ma il modo consigliato di organizzare il
layout di una finestra sono i sizer.
5.6. I sizer: le basi da sapere.
31
Appunti wxPython Documentation, Release 1
I sizer costituiscono ormai, e non solo in wxPython, la soluzione standard ai due problemi che abbiamo appena visto:
• ri-calcolano la dimensione di tutti i widget man mano che l’utente ridimensiona la finestra, in modo che il
contenuto sia sempre proporzionato al contenitore;
• inserire o togliere elementi in un secondo momento diventa banale: siccome è il sizer a occuparsi di calcolare
posizioni e dimensione, voi dovete solo dirgli che cosa c’è dentro.
Lo svantaggio dei sizer è che richiedono parecchie righe di codice in più (bisogna dire al sizer che cosa includere e in
che ordine), e che sono un po’ più difficili da imparare.
Tuttavia, con un po’ di pratica, i vantaggi superano ben presto gli svantaggi. Non a caso i sizer sono diventati praticamente onnipresenti nel mondo wxPython e non solo.
5.6.3 Che cosa è un sizer.
In wxPython esistono diversi tipi di sizer, tutti derivati dalla classe-madre wx.Sizer (che però è un genitore astratto
e non deve mai essere usato direttamente).
wx.Sizer non deriva da wx.Window perché, semplicemente, un sizer non si deve vedere; e non deriva da
wx.EvtHandler perché non ha bisogno di reagire agli eventi. Un sizer rappresenta semplicemente un algoritmo
per disporre i widget in un contenitore.
Si può applicare un sizer a un frame o a un dialogo/panel, anche se è molto più frequente vederlo applicato a un panel,
perché il panel è lo sfondo ideale su cui disporre i widget, come abbiamo visto parlando dei contenitori.
Il processo di lavoro è comune a tutti i tipi di sizer:
• prima si crea (istanzia) il sizer;
• poi si usa ripetutamente il metodo Add per aggiungere elementi al sizer. Possono essere aggiunti in questo modo
sia widget, sia altri sizer (che a loro volta contengono widget e/o sizer);
• infine, si usa il metodo SetSizer per attribuire il sizer così organizzato al suo contenitore;
• come opzione ulteriore, è possibile usare il metodo Fit per dire al sizer di adattare la sua dimensione a quella
degli elementi che contiene.
5.6.4 wx.BoxSizer: il modello più semplice.
Il più semplice sizer che potete usare è il wx.BoxSizer. Questo sizer organizza i widget in colonna, uno sotto
l’altro, oppure in riga, uno accanto all’altro.
Al momento di crearlo, dovete specificare la direzione lungo la quale si sviluppa il sizer. Se scrivete:
sizer = wx.BoxSizer(wx.HORIZONTAL) # default
il BoxSizer si svilupperà in senso orizzontale, allineando i suoi elementi uno accanto all’altro. Se invece scrivete:
sizer = wx.BoxSizer(wx.VERTICAL)
il sizer impilerà i suoi elementi uno sopra l’altro.
Una volta che il sizer è stato creato, usate Add per aggiungere un nuovo elemento sotto gli altri (se il sizer è verticale)
o a destra degli altri (se è orizzontale). Potete aggiungere quanti elementi desiderate. Per esempio, per aggiungere un
pulsante che avete creato in precedenza, scrivete:
sizer.Add(my_button)
32
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
Note: Inoltre, ci sono alcuni altri metodi che più raramente possono esservi utili. sizer.GetOrientation()
vi restituisce l’orientamento del sizer. AddMany permette di inserire più elementi alla volta. Prepend vi consente
di inserire un elemento all’inizio del sizer, invece che alla fine. Insert inserisce un elemento tra altri due. Remove
rimuove un elemento e lo distrugge, Detach lo rimuove senza distruggerlo, Replace lo sostituisce con un altro.
Potete consultare la documentazione per scoprire esattamente come funzionano. Non vi consigliamo di fare uso
frequente di queste tecniche, tuttavia.
5.6.5 Add in dettaglio.
Il metodo Add di un sizer richiede un argomento obbligatorio (il widget che bisogna aggiungere) e altri 3 facoltativi.
Esaminiamoli nel dettaglio.
L’argomento proportion di Add.
Il secondo argomento è proportion, un numero intero che indica la proporzione. La proporzione fa sempre riferimento alla direzione (orizzontale o verticale) del sizer. Se la proporzione è 0, allora il widget, lungo quella direzione,
occuperà solo lo spazio che gli compete (il suo “best size” naturale, oppure quello che avete impostato voi in qualche
modo). Tutti i widget con proporzione nulla occuperanno solo lo spazio di cui hanno effettivamente bisogno. Tutti
i widget con proporzione superiore a 0, invece, competeranno per occupare lo spazio eventualmente rimanente, in
maniera proporzionale alla loro... proporzione, appunto.
In altri termini, se un sizer contiene tre widget, con proporzione 0, 1, e 2 rispettivamente, allora il primo occuperà lo
spazio di cui ha bisogno, e lo spazio rimanente sarà diviso tra gli altri due: il secondo ne occuperà un terzo, e l’ultimo
si prenderà i due terzi restanti. Tutto questo, non dimentichiamolo, soltanto lungo la direzione “principale” del sizer.
Ecco il codice che illustra questo esempio:
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(wx.Button(p), 0)
sizer.Add(wx.Button(p), 1)
sizer.Add(wx.Button(p), 2)
p.SetSizer(sizer)
# la direzione e' verticale
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
Notate che tutte le volte che ridimensionate la finestra cambiano anche le dimensioni dei pulsanti, ma il secondo
e il terzo occuperanno sempre lo spazio restante in proporzione 2:1, mentre le dimensioni del primo pulsante non
cambieranno mai. Notate anche che i pulsanti si contendono soltanto lo spazio nella direzione verticale (ossia la
direzione del sizer), mentre in orizzontale ciascuno mantiene sempre lo stesso “best size”.
L’argomento flag di Add.
Il terzo argomento di wx.Sizer.Add è flag, ed è una bitmask come quelle che abbiamo già visto parlando degli
stili. In questa bitmask possono rientrare due indicazioni molto differenti tra loro:
• primo, come allineare i widget rispetto agli altri, e/o definirne le dimensioni;
5.6. I sizer: le basi da sapere.
33
Appunti wxPython Documentation, Release 1
• secondo, se lasciare dello spazio vuoto come bordo intorno al widget.
Il primo aspetto è complicato. Potete scegliere tra varie opzioni:
• uno dei possibili wx.ALIGN_* (*TOP, *BOTTOM, etc.) mantengono l’allineamento dei widget rispetto agli
altri del sizer. Questo in molti casi ha senso solo se il widget ha priorità nulla;
• wx.FIXED_MINSIZE mantiene sempre le dimensioni minime del widget (e si può abbinare con uno degli
allineamenti appena visti);
• wx.EXPAND o il suo sinonimo wx.GROW forzano il widget a occupare tutto lo spazio disponibile lungo la
dimensione “secondaria” del sizer (chiariremo meglio questo punto tra poco);
• wx.SHAPED è come wx.EXPAND, ma forza il widget a mantenere le proporzioni originarie.
Un chiarimento importante riguardo a wx.EXPAND. Questo flag forza il widget a espandersi lungo la direzione secondaria del sizer. Per contro, dare al widget una priorità superiore a 0 lo costringe a espandersi lungo la direzione
principale, come abbiamo visto. Quindi se il widget ha priorità superiore a 0 e il flag wx.EXPAND, riempirà lo spazio
disponibile in entrambe le direzioni.
In generale, dovete chiedervi in quale direzione ha senso far espandere i vostri widget. Per esempio, in un sizer
verticale, in genere i widget “multilinea” (liste, etc.) dovrebbero espandersi in entrambe le direzioni, mentre gli altri
(caselle di testo, combobox...) potrebbero espandersi solo nella direzione secondaria. Infine, altri ancora (pulsanti,
spin...) non dovrebbero espandersi per nulla:
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(wx.TextCtrl(...), 0, wx.EXPAND) # cresce solo in orizzontale
sizer.Add(wx.ListBox(...), 1, wx.EXPAND) # cresce in entrambe le direzioni
sizer.Add(wx.Button(...), 0, wx.ALIGN_CENTER_HORIZONTAL) # non cresce
Quando al secondo aspetto dell’argomento flag, ossia i bordi, potete indicare una combinazione qualsiasi di
wx.RIGHT, wx.LEFT, wx.TOP, wx.BOTTOM oppure wx.ALL (che li comprende tutti) per indicare su quali lati
volete che sia lasciato il bordo.
L’argomento border di Add.
Il quarto argomento di Add è anche il più semplice. Se nella bitmask del flag avete specificato che volete lasciare del
bordo, indicatene qui la dimensione, in pixel. Non è possibile specificare bordi di differente ampiezza su lati diversi.
Aggiungere uno spazio vuoto.
Add può essere usato anche per inserire uno spazio vuoto tra due widget. Basta passare il numero dei pixel da lasciare
vuoti in una tupla (in realtà, un’istanza di wx.Size, come vediamo nella pagina dedicata). Siccome in genere vi
interessa specificare solo lo spazio da lasciare lungo la direzione principale del sizer, potete passare -1 per l’altra
direzione. Per esempio:
sizer = wx.Sizer(wx.VERTICAL)
sizer.Add(wx.Button(...), 0, wx.ALL, 5)
sizer.Add((-1, 10))
# uno spazio di 10 pixel in verticale
sizer.Add(wx.Button(...), 0, wx.ALL, 5)
I due widget saranno così separati da 20 pixel di spazio (contando anche i bordi).
Utilizzare Add in questo modo è in realtà una scorciatoia per il metodo AddSpacer, che accetta gli stessi argomenti.
Notate infine che uno “spazio vuoto” si comporta esattamente come gli altri widget, e quindi può essere inserito con
un flag e una proporzione. In particolare, è molto frequente l’idioma Add((-1, -1), 1, wx.EXPAND), che
aggiunge uno spazio indeterminato che si allarga quando ridimensioniamo la finestra. Provate questo “trucco”, che
mantiene i widget nel centro della finestra:
34
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add((-1, -1), 1, wx.EXPAND)
sizer.Add(wx.Button(...), 0, wx.EXPAND)
sizer.Add(wx.Button(...), 0, wx.EXPAND)
sizer.Add((-1, -1), 1, wx.EXPAND)
L’idioma è abbastanza comune da aver meritato la creazione di un metodo apposito sizer.AddStretchSpacer
per riassumerlo.
(E non finisce qui...)
Abbiamo ancora alcune considerazioni da fare su Add (e i suoi fratelli AddSpacer e AddStretchSpacer):
riprendiamo il discorso nella prossima pagina che dedichiamo ai sizer.
5.7 Gli eventi: le basi da sapere.
wxPython, come tutti gli altri, è un gui framework “event-driven”: il suo MainLoop, una volta avviato, si mette in
ascolto degli eventi generati dall’utente, e risponde a essi nel modo che voi avete stabilito.
Ogni istante della vita di un’applicazione wxPython è affollata di eventi, visto che praticamente ogni cosa ne innesca
uno: non solo le interazioni “di alto livello” (premere un pulsante, scegliere una voce di menu, etc.), ma anche la
pressione dei tasti, lo spostamento del mouse, il cambiamento di dimensioni di una finestra, e molte molte altre cose
ancora, tuuo genera eventi. Perfino quando tutto il resto tace, viene generato di continuo un wx.EVT_IDLE, per
segnalare che il MainLoop in quel momento non ha niente da fare!
Come potete immaginare, wxPython offre moltissime possibilità di controllo sugli eventi. Il rovescio della medaglia
è che il sistema è molto difficile da comprendere nei dettagli. In questa pagina diamo uno sguardo a volo d’uccello
ai vari attori coinvolti, ma esaminiamo esclusivamente gli aspetti più semplici, che vi servono a gestire le situazioni
normali. Rimandiamo a una pagina separata l’analisi degli aspetti più complessi.
5.7.1 Gli attori coinvolti.
Che cosa è un evento.
Ecco, questo sarebbe già uno degli aspetti complessi. Per il momento vi basta sapere che gli eventi sono degli oggetti
(che altro?), istanze della classe wx.Event, o più probabilmete di una delle sue molteplici sottoclassi. Questi oggetti
sono in effetti dei segnali: vengono creati quando “succede qualcosa”, e viaggiano liberamente nello spazio (davvero:
non vi serve sapere altro). Se qualcuno è interessato al messaggio che portano, lo intercetta. Altrimenti, poco male. Per
esempio, quando l’utente fa clic su un pulsante, il pulsante lancia una istanza di wx.CommandEvent che contiene
informazioni sul pulsante che è stato premuto.
Tuttavia, a causa del complesso sistema con cui wxWidgets (il framework C++ su cui wxPython è basato) gestisce gli
eventi, voi non incontrerete mai di persona un oggetto-evento, almeno finché vi limitate alle basi.
E allora, che cosa incontrate nella vita di tutti i giorni? Ancora un po’ di pazienza, prego.
Che cosa è un callback.
Questo è facile. Un callback è un pezzo di codice che volete che sia eseguito in risposta a un dato evento. E’
semplicemente una funzione, o un metodo di una classe, che scrivete voi stessi. Per essere qualificato come callback,
è necessario che la funzione riceva uno e un solo argomento (oltre a self se è un metodo, naturalmente). E questo
argomento deve essere un riferimento all’oggetto-evento stesso. Quindi, qualcosa come:
5.7. Gli eventi: le basi da sapere.
35
Appunti wxPython Documentation, Release 1
def my_callback(event):
# etc etc
oppure:
def my_callback(self, event):
# etc etc
Ovviamente all’interno della vostra funzione potete anche non usare per nulla il riferimento all’evento.
I callback saranno chiamati automaticamente da wxPython quando sarà necessario. Tuttavia, non c’è niente di magico
in un callback: se talvolta volete chiamarlo manualmente (in assenza di qualunque evento), potete farlo passando per
esempio None al posto dell’evento:
my_callback(None) # esegue manualmente il callback
Addirittura, se pensate che questo modo di chiamare il callback avverrà spesso, potete far uso dei valori di default, e
definire il callback così:
def my_callback(self, event=None):
# etc etc
In questo modo, wxPython non avrà comunque problemi, e voi all’ccorrenza potete chiamarlo anche solo così:
my_callback() # esegue manualmente il callback
Resta inteso che, se il codice del callback fa davvero uso del riferimento all’evento, e quindi si trova disorientato
quando gli passate None, questo è un problema vostro.
Attenzione, ancora un po’ di confusione: spesso nei testi inglesi trovate “handler” per dire semplicemente “callback”.
Il significato è in genere ovvio dal contesto. E’ ancora più chiaro quando trovate “handler function” o “handler
method”: questo vuol dire senz’altro “callback”. Tuttavia, a rigore un “handler” è un’altra cosa, come vedremo subito.
Ora, come potete collegare effettivamente il callback all’evento che volete intercettare? Ancora un po’ di pazienza!
Che cosa è un handler.
All’estremo opposto degli oggetti-evento, ci sono gli “handler” (gestori). Gli handler sono classi (derivate dal genitore
astratto wx.EvtHandler) che conferiscono la capacità di gestire un evento, appunto. La cosa interessante è che
tutta la gerarchia dei widget wxPython deriva anche da wx.EvtHandler. Questo è come dire che, in wxPython,
ogni widget ha la capacità di gestire gli eventi provenienti da ogni altro widget.
Di nuovo, non incontrerete mai un handler nella vita di tutti i giorni. Ma questa volta il motivo è che gli handler “da
soli” non esistono proprio: invece, è corretto dire che tutti i widget (pulsanti, frame, menu, liste...) sono anche handler.
Quindi è corretto dire che, quando volete gestire un evento, a questo scopo usate le capacità di handler di un widget
(di solito proprio lo stesso che ha anche emesso l’oggetto-evento!).
E come fate a usare queste capacità di handler? Ancora un attimo di pazienza... ci siamo quasi.
Che cosa è un event type.
Semplicemente, una costante numerica univoca che rappresenta un evento specifico per un certo tipo di widget. Detto
più rapidamente: un certo tipo di evento. Qui occorre una precisazione. Le classi-evento (e i conseguenti oggettievento) sono poche, e molti widget possono innescare lo stesso evento. Per esempio, quando fate clic su un pulsante e
quando scegliete una voce di menu, in entrambi i casi si origina un wx.CommandEvent.
Un “event type”, d’altra parte, identifica univocamente il tipo di evento in relazione al tipo di widget che lo emette.
Per esempio:
36
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
>>> import wx
>>> wx.wxEVT_COMMAND_BUTTON_CLICKED
10008
è l’id per un wx.CommandEvent quando viene emesso da un wx.Button.
Gli event type sono costanti che vivono nel namespace wx nella forma wx.wxEVT_*. Ce ne sono molti, come potete
immaginare:
>>> len([i for i in dir(wx) if i.startswith('wxEVT_')])
219
Gli event type sono un’altra delle cose che non entreranno a far parte della vostra vita quotidiana di programmatori
wxPython. Ma wxPython in realtà li usa internamente per consentirvi di riferirvi, in modo trasparente, agli eventi non
in quanto tali, ma in quanto emessi da uno specifico widget. Il che è in genere quello che volete.
Ma per capire come, in pratica, potete riferirvi agli eventi... ehm, dovete pazientare un’ultima volta.
Che cosa è un binder.
Un binder è un oggetto usato per legare uno specifico event type, uno specifico handler e uno specifico callback. I
binder sono istanze della classe wx.PyEventBinder, e sono creature tipiche solo di wxPython. In effetti i binder
sono il modo in cui wxPython semplifica il processo di gestione degli eventi di wxWidget.
I binder sono gli oggetti che potete incontrare davvero (finalmente!) nella normale programmazione wxPython. Tuttavia, nella vita di tutti i giorni, non vi troverete mai a creare o manipolare direttamente un binder.
In effetti tutti i binder necessari sono già creati da wxPython nella fase di startup (quando viene eseguita l’istruzione
iniziale import wx), e vivono pronti all’uso nel namespace wx, sotto forma di simboli del tipo wx.EVT_*. La loro
nomenclatura mappa in effetti i nomi delle macro c++ che wxWidget utilizza dietro le quinte per fare i collegamenti.
Inoltre, dal momento che non dovete mai creare o modificare un binder, dal vostro punto di vista sono un po’ come
delle costanti, e quindi ha senso che abbiano nomi tutti maiuscoli. Tuttavia in realtà basta poco per capire che sono
oggetti a tutti gli effetti:
>>> import wx
>>> wx.EVT_BUTTON
<wx._core.PyEventBinder object at ....>
Notate anche che:
>>> wx.EVT_BUTTON.evtType
[10008]
ossia wx.EVT_BUTTON rappresenta l’event type wx.wxEVT_COMMAND_BUTTON_CLICKED che abbiamo visto
sopra, a testimoniare che un binder è legato a un event type specifico.
Il binder, in apparenza, porta con sé soltanto l’indicazione del suo event type. Tuttavia, non è solo una sovrastruttura
inutile intorno alla costante numerica dell’event type. Prima di tutto, un binder può riferirsi a più di un event type. Per
esempio, wx.EVT_MOUSE_EVENTS è un binder collettivo che raggruppa tutti gli event type del mouse (clic, clic a
destra, doppio clic, movimento, rotella...):
>>> wx.EVT_MOUSE_EVENTS.evtType
[10025, 10026, 10027, 10028, 10029, 10030, 10031, 10034, 10035,
10036, 10032, 10033, 10040]
Inoltre, come vedremo presto, il binder ha anche un metodo Bind, che è il motore che lega insieme eventi, handler e
callback.
Ma prima, ancora un pizzico di confusione, questa volta però comprensibile e sana. Proprio perché nella vita di
tutti i giorni non incontrare oggetti-eventi, nel linguaggio comune di wxPython è consueto riferirsi ai wx.EVT_*
5.7. Gli eventi: le basi da sapere.
37
Appunti wxPython Documentation, Release 1
come “eventi”, anche se sono più precisamente degli oggetti-binder. Tuttavia questa piccola licenza descrive la situazine più accuratamente, in un certo senso. Per esempio, quando premete un pulsante, questo innesca un generico
wx.CommandEvent (che, per dire, è la stessa cosa che si innesca anche quando selezionate un menu). D’altra parte,
il binder wx.EVT_BUTTON porta in sé non solo la nozione del wx.CommandEvent, ma anche quella di “generato
da un wx.Button” (ed è molto differente dal binder wx.EVT_MENU).
Note: Perché c’è bisogno dei binder (e degli event type)? Non basterebbero gli eventi da soli? In realtà la presenza
degli event type permette di mantenere ridotto il numero delle classi-evento, lasciando che la loro gerarchia si sviluppi
secondo le logiche proprie degli eventi, e senza star dietro alla proliferazione dei widget, sempre in corso.
Detto questo, finalmente siamo pronti per rispondere a tutte le domande!
5.7.2 Bind: collegare eventi e callback, in pratica.
E veniamo al dunque. Come faccio a collegare un evento a un callback?
Ricapitoliamo: quando faccio clic su un pulsante, viene creato un oggetto-evento. La prima cosa che devo fare è
scegliere un handler per quell’evento: siccome però tutti i widget in wxPython sono degli handler, in genere succede
che si sceglie il pulsante stesso come handler degli eventi che genera. Ovviamente un pulsante può generare diversi
eventi; e d’altra parte, un evento può essere generato da diversi widget oltre al pulsante. Per fortuna abbiamo anche a
disposizione un binder specifico, che identifica l’oggetto-evento che ci interessa in quanto emesso da un pulsante.
Tutto ciò che dobbiamo fare è chiamare il metodo Bind dell’handler che scegliamo (ossia, come abbiamo detto, il
pulsante stesso), e usarlo per connettere il binder e il nostro callback:
button = wx.Button(...)
button.Bind(wx.EVT_BUTTON, callback)
wx.EVT_BUTTON, ormai lo sappiamo, è il binder che identifica il particolare evento che si genera quando un pulsante
è premuto. callback è il nostro callback (di solito è un metodo della stessa classe in cui vivono le due righe
di codice che abbiamo appena scritto, per cui lo scrivete in genere nella forma self.callback). button è il
nostro pulsante, del quale però stiamo qui utilizzando le sue capacità di handler. Bind è il metodo implementato da
wx.EvtHandler (e pertanto ereditato anche da button) che compie la magia del collegamento.
Note: a prima vista sembra contradditorio. Non avevamo detto che erano i binder a collegare eventi, handler
e callback? E non abbiamo visto che i binder hanno anche loro un metodo Bind? E allora perché stiamo usanto wx.EvtHandler.Bind per fare il collegamento? In realtà wx.EvtHandler.Bind chiama semplicemnte
wx.PyEventBinder.Bind, quindi in definitiva sì, sono i binder a fare il collegamento dietro le quinte. Qui occorre una precisazione di carattere storico. I binder non solo hanno un loro Bind, ma implementano anche un metodo
__call__ che consente di chiamarli come una funzione, e che internamente chiama Bind. Nelle vecchie versioni
di wxPython, il collegamento era fatto in questo modo:
wx.EVT_BUTTON(button.GetId(), callback)
che era equivalente a:
wx.EVT_BUTTON.Bind(button.GetId(), callback)
e si vedeva chiaramente che era proprio il binder a lavorare. Tuttavia, questo sistema appariva poco “object-oriented”,
perché sembrava di chiamare direttamente un oggetto, e per di più un oggetto che sembra una costante. In effetti
però wx.PyEventBinder.__call__ è ancora lì per retrocompatibilità, e potete ancora vedere questo stile di
collegamento nel codice più vecchio (e questa è anche la ragione di questa nota un po’ pedante).
38
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
5.7.3 Altri modi di usare Bind.
Abbiamo visto che:
button = wx.Button(...)
button.Bind(wx.EVT_BUTTON, callback)
è il modo consueto di usare Bind, e presuppone di aver scelto il widget stesso come handler dell’evento che si origina
da esso.
Si può anche scegliere un altro handler, però. Per esempio, se il pulsante sta dentro un panel, o un frame, potete
scegliere il suo contenitore come handler, e scrivere:
button = wx.Button(self, ...) # 'self' e' un frame, dialog, panel...
self.Bind(wx.EVT_BUTTON, callback, button)
Notate che adesso Bind è stato chiamato passando button come terzo argomento. E’ come dare questo ordine:
handler self, devi collegare a callback tutti i wx.EVT_BUTTON che provengono da button.
Il terzo argomento è opzionale. Se però avessimo scritto soltanto:
self.Bind(wx.EVT_BUTTON, callback)
questo avrebbe voluto dire: handler self, devi collegare a callback tutti i wx.EVT_BUTTON che provengono da
qualsiasi tuo “figlio”. E naturalmente questa è una cosa utile talvolta, pericolosa di solito.
Che differenza c’è tra button.Bind(...) e self.Bind(..., button)? Talvolta possono esserci differenze
sottili, come vedremo nella seconda parte quando parleremo della propagazione degli eventi. Nella maggior parte dei
casi, però non c’è alcuna differenza pratica.
Ancora una domanda: abbiamo visto che è possibile collegare a un evento anche l’handler di un altro widget “genitore”
(o “progenitore” nella catena dei parent) del widget che lo ha emesso. E’ possibile invece collegare l’evento all’handler
di un widget “figlio”, o all’handler di un widget che non ha niente a che vedere con chi ha emesso l’evento (per
esempio, vive in un’altra finestra)? La risposta è no, perché l’evento non si propagherà mai in quella direzione. E’
un’altra cosa che vedremo meglio parlando di propagazione degli eventi.
5.7.4 Sapere quali eventi possono originarsi da un widget.
Ecco una domanda comune. Come faccio a sapere quali eventi (nel senso di binder wx.EVT_*) possono originarsi da
un certo widget? L’unica risposta è leggere la documentazione disponibile. Anche l’utility EventsInStyle può essere
molto utile.
In generale, un widget può originare alcuni eventi “caratteristici” suoi propri. Spesso questi iniziano con un prefisso comune, per esempio gli eventi tipici di un wx.ListCtrl iniziano con wx.EVT_LC_*, e quelli di un wx.ComboBox
con wx.EVT_CB_*, etc. Questi sono quelli che trovate nella documentazione del widget.
Tuttavia, ci sono molti altri eventi di livello più basso che il widget può generare, come quelli del mouse o della
tastiera.
Ispezionare i diversi binder non aiuta, perché un binder è indifferente alla riuscita del matrimonio che è chiamato a
celebrare: potete tranquillamente accoppiare un wx.EVT_BUTTON a una casella di testo, per dire. Semplicemente,
l’evento non si verificherà mai.
Un’altra strada è quella di esaminare la documentazione per le varie classi-evento (ossia quelle derivate da
wx.Event). Si possono elencare facilmente:
>>> import wx
>>> [i for i in dir(wx) if 'Event' in i]
5.7. Gli eventi: le basi da sapere.
39
Appunti wxPython Documentation, Release 1
Nella documentazione di ciascuna, ci sono i nomi dei vari binder che possono riferirsi a quell’evento.
In definitiva, è facile trovare subito gli eventi più comuni per un certo widget, ma occorre un po’ di esperienza per
scoprire gli altri.
Infine, ho scritto una ricetta apposta per cercare di risolvere questo problema: provatela, potrebbe tornarvi utile.
5.7.5 Estrarre informazioni su un evento nel callback.
Come abbiamo visto, i callback devono accettare come argomento un riferimento all’evento che li ha invocati:
def callback(self, event): # etc etc
L’argomento event non è altro che l’istanza dell’oggetto-evento che si è originata dal widget, è stata processata, e
adesso raggiunge finalmente il callback.
Questo oggetto può portare con sé molte informazioni utili: quali esattamente, dipende dall’evento. Consultate la
documentazione relativa a ciascuna sottoclasse di wx.Event per sapere che cosa potete recuperare.
Per esempio, un wx.ListCtrl emette vari tipi di eventi della classe wx.ListEvent. Sfogliando la documentazione, trovate per esempio il metodo wx.ListEvent.GetColumn, che vi dice, tra l’altro, la colonna che è stata
cliccata. Di conseguenza, nel vostro callback potete recuperarla scrivendo:
def callback(self, event):
clicked_column = event.GetColumn()
La stessa classe-madre astratta wx.Event ha dei metodi utili, che tutte le altre classi-evento ereditano. Per esempio,
GetEventObject() vi restituisce un riferimento al widget che ha emesso l’evento. GetEventType() vi dice
l’event type esatto.
Non è detto che un oggetto-evento contenga informazioni utili per ciascun metodo previsto dalla sua classe, naturalmente. Per esempio, wx.CommandEvent.IsChecked() è significativo quando il wx.CommandEvent è stato
emesso da una checkbox (o da una voce di menu che si può “flaggare”). Naturalmente, se il wx.CommandEvent
proviene da un pulsante, questo metodo non conterrà niente di utile.
Infine, se non siete sicuri di quale evento sta arrivando al callback, probabilmente siete ancora in fase di sviluppo.
Quindi, un bel print event (o un più raffinato print event.__class__.__name__, se preferite) basteranno a togliervi ogni dubbio.
5.7.6 Un esempio conclusivo.
Queste note non sono un tutorial su wxPython e suppongono che siate in grado di documentarvi da soli, vedere esempi
di codice in giro, etc. Tuttavia, in conclusione di questa lunga cavalcata sugli eventi, ecco un esempio minimo per far
vedere come si fa di solito. Molti altri esempi, naturalmente, si trovano nella demo e nella documentazione.
class TopFrame(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
button1 = wx.Button(self, -1, 'pulsante 1', pos=(10, 10), name='button 1')
button2 = wx.Button(self, -1, 'pulsante 2', pos=(10, 50), name='button 2')
button1.Bind(wx.EVT_BUTTON, self.on_button1)
button2.Bind(wx.EVT_BUTTON, self.on_button2)
def on_button1(self, evt):
print 'evento', evt.__class__.__name__
print 'oggetto', evt.GetEventObject().GetName()
def on_button2(self, evt):
40
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
print 'evento', evt.__class__.__name__
print 'oggetto', evt.GetEventObject().GetName()
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
5.8 I menu: le basi da sapere.
I menu non sono difficili, almeno per gli aspetti di base. Difficile è organizzarli in modo efficiente, compatto, intuitivo
per l’utente... quante volte avete perso tempo a cercare la funzionalità che vi serve tra le decine di voci di menu di una
gui? Ci sarebbe molto da parlare sul problema dell’usabilità dei menu... ma non qui, e non oggi!
In questa pagina ci limiteremo alle informazioni tecniche di base, rimandando a un’altra volta gli argomenti più
avanzati.
5.8.1 Come creare una barra dei menu.
Dei tre contenitori - base (finestre, dialoghi e panel), i menu possono essere usati solo con le finestre, ossia i
wx.Frame. In particolare, i dialoghi (wx.Dialog) non possono avere menu, e non avrebbe senso: se il vostro
dialogo è abbastanza complicato da aver bisogno di menu per orientare l’utente, scrivete un normale frame, invece.
I menu si creano tipicamente nell’__init__ della classe del vostro frame, e si mostrano già completi agli utenti
quando si mostra il frame. Aggiungere o modificare i menu in seguito è una pessima idea (piuttosto, potete abilitare e
disabilitare singole voci o interi menu).
I menu “vivono” nella barra dei menu, che come sicuramente sapete è la “striscia” subito sotto la barra del titolo della
vostra finestra. La barra dei menu è direttamente figlia del frame che la ospita, e non può essere figlia di niente altro
(per esempio, non del panel “principale” che farete per quella finestra).
Creare una barra dei menu è facilissimo, basta istanziare la classe wx.MenuBar e assegnarla successivamente al
frame usando il metodo SetMenuBar:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menubar = wx.MenuBar()
# qui creo i vari menu...
# e infine:
self.SetMenuBar(menubar)
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Se provate questo codice, vedrete un normale frame con una barra dei menu pronta, ma ancora vuota. Notate che il
nome della barra dei menu (menubar) serve solo localmente nell’__init__ della classe: per questo motivo non
abbiamo bisogno di chiamarla self.menubar.
5.8. I menu: le basi da sapere.
41
Appunti wxPython Documentation, Release 1
5.8.2 Come creare i menu.
Ogni menu è una istanza della classe wx.Menu. Dopo la sua creazione, un wx.Menu può essere popolato con le varie
voci che deve contenere. Infine, viene attaccato alla barra dei menu, usando il metodo wx.MenuBar.Append come
vedremo subito. Tuttavia, un menu può essere agganciato anche a un altro menu già esistente, formando in questo
modo un sotto-menu.
Espandiamo un poco l’esempio di sopra:
# ... come sopra...
menubar = wx.MenuBar()
mymenu = wx.Menu() # creo un menu
# qui aggiungo le voci del menu, e infine...
menubar.Append(mymenu, 'PrimoMenu')
mymenu = wx.Menu() # creo un altro menu
# qui aggiungo le voci del menu, e infine...
menubar.Append(mymenu, 'SecondoMenu')
self.SetMenuBar(menubar)
# ... segue come sopra...
Se adesso fate girare l’esempio di prima con quese aggiunte, vedrete che la barra dei menu si è popolata di due menu
(“PrimoMenu” e “SecondoMenu”), ancora senza nessun elemento. Noterete anche che il “titolo” dei menu è assegnato
solo al momento di agganciarlo alla barra dei menu.
Osservate poi che anche i nomi dei singoli menu servono solo all’interno dell’__init__, quindi non c’è bisogno di
nessun self davanti. Anzi, effettivamente il nome di un menu serve solo fino al momento di aggangiarlo alla barra
dei menu; per questo motivo possiamo riciclare senza scrupoli il nome generico mymenu per tutti quelli che stiamo
creando.
Infine, notate che i vari menu compaiono nella barra (da sinistra a destra) nell’ordine in cui li avete agganciati. A
dire il vero wx.MenuBar, oltre ad Append, offre anche un metodo Insert per inserire un menu in una posizione
arbitraria: tuttavia la cosa più facile e naturale è collocare i vari menu nell’ordine giusto usando solo Append.
5.8.3 Come creare le voci di menu.
Una voce di menu è semplicemente il risultato del metodo Append applicato a un wx.Menu. Per esempio:
menu = wx.Menu()
menu.Append(-1, "prima voce")
menu.Append(-1, "seconda voce")
menu.Append(-1, "terza voce")
inserisce tre voci di menu in un menu. Ma vediamo un po’ più da vicino come funziona questa magia.
Il secondo argomento di Append, come avrete capito, è l’etichetta che l’utente vedrà effettivamente nel menu. Il
primo argomento, invece, è un id univoco: abbiamo già visto che cosa sono gli id, e che passare -1 vuol dire lasciare
che wxPython gestisca da solo la creazione di un nuovo id.
Append ha altri due argomenti opzionali: il terzo è una stringa che, se inserita, appare come “help text” (di solito
come un tooltip, ma può dipendere dalle piattaforme). Il quarto è un flag che indica il tipo di voce di menu che stiamo
inserendo (il valore di default è wx.ITEM_NORMAL, ma ne parleremo un’altra volta).
Il metodo Append restituisce un oggetto che rappresenta la voce di menu appena inserita nel menu. In genere
dobbiamo conservare questo riferimento in una variabile, se vogliamo poi collegare questa voce di menu a un evento
(ossia, vogliamo fare qualcosa quando l’utente fa clic su di essa). Riscriviamo quindi il nostro esempio iniziale, fino a
popolare i nostri menu con qualche voce:
42
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menubar = wx.MenuBar()
mymenu = wx.Menu() # creo un menu, e lo popolo:
item1 = mymenu.Append(-1, 'voce uno')
item2 = mymenu.Append(-1, 'voce due')
menubar.Append(mymenu, 'PrimoMenu')
mymenu = wx.Menu() # creo un altro menu...
item3 = mymenu.Append(-1, 'voce tre')
item4 = mymenu.Append(-1, 'voce quattro')
item5 = mymenu.Append(-1, 'voce cinque')
menubar.Append(mymenu, 'SecondoMenu')
self.SetMenuBar(menubar)
# adesso non dobbiamo scordarci di collegare le voci di menu
# item1, item2, etc., a degli eventi!
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Se provate questo esempio, osserverete che i nostri menu si sono popolati con qualche voce. Ancora una volta, item1,
item2 etc. sono nomi che ci servono solo localmente, quindi non è il caso di farli precedere da un self.
Naturalmente le voci di menu sono ancora inerti: se ci fate clic sopra, non succede nulla. Manca ancora il collegamento
con gli eventi. Ci arriviamo subito: prima però, abbiamo ancora un paio di punti in sospeso.
5.8.4 Come creare un separatore.
Questo è davvero facile: basta usare AppendSeparator invece di Append. Per esempio:
mymenu = wx.Menu()
item3 = mymenu.Append(-1, 'voce tre')
item4 = mymenu.Append(-1, 'voce quattro')
mymenu.AppendSeparator() # un separatore
item5 = mymenu.Append(-1, 'voce cinque')
menubar.Append(mymenu, 'SecondoMenu')
5.8.5 Come creare un sotto-menu.
Come abbiamo già accennato, un sotto-menu non è altro che un normale wx.Menu agganciato a un altro menu, invece
che alla barra dei menu. wx.Menu dispone infatti di un metodo AppendMenu che fa proprio questo lavoro.
Lavoriamo ancora sul nostro esempio, e questa volta aggiungiamo un sotto-menu che inseriamo tra gli elementi del
secondo menu:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
5.8. I menu: le basi da sapere.
43
Appunti wxPython Documentation, Release 1
menubar = wx.MenuBar()
mymenu = wx.Menu() # creo un menu, e lo popolo:
item1 = mymenu.Append(-1, 'voce uno')
item2 = mymenu.Append(-1, 'voce due')
menubar.Append(mymenu, 'PrimoMenu')
submenu = wx.Menu() # ecco il sotto-menu!
item10 = submenu.Append(-1, 'voce uno del submenu')
item11 = submenu.Append(-1, 'voce due del submenu')
mymenu = wx.Menu() # adesso creo il secondo menu...
item3 = mymenu.Append(-1, 'voce tre')
item4 = mymenu.Append(-1, 'voce quattro')
# ... e aggancio qui il sotto-menu:
mymenu.AppendMenu(-1, "ecco un sotto-menu", submenu)
# quindi proseguo con le altre voci del menu
item5 = mymenu.Append(-1, 'voce cinque')
menubar.Append(mymenu, 'SecondoMenu')
self.SetMenuBar(menubar)
Adesso il secondo menu integra anche il nostro sotto-menu tra i suoi elementi. Notate che AppendMenu vuole
(naturalmente!) un argomento in più, rispetto al normale Append.
Notate anche che non abbiamo conservato in una variabile il riferimento al nodo di inserimento. Non abbiamo cioè
scritto, per esempio:
item6 = mymenu.AppendMenu(-1, "ecco un sotto-menu", submenu)
Questo è ciò che si fa in genere: non ci serve dargli un nome, perché non abbiamo bisogno di collegare questo nodo
a un evento. Quando l’utente fa clic qui, ci basta il comportamento di default gestito da wxPython (ovvero, aprire le
voci del sotto-menu). Tuttavia, se volessimo far succedere qualcosa di diverso, potremmo collegare anche questo nodo
a un evento, come qualsiasi altro elemento.
5.8.6 Collegare le voci di menu a eventi.
Ed eccoci al punto finale: dopo aver creato i vostri menu, bisogna fargli fare qualcosa!
Note: Quanto segue presuppone che sappiate già che cosa sono gli eventi, e come utilizzarli. In caso contrario, date
prima un’occhiata qui, e poi proseguite con questo.
Quando l’utente fa clic su una voce di menu, genera un wx.EVT_MENU, che è un CommandEvent intercettabile nel
frame “parent” (quello dove definite il menu, per intenderci).
La tecnica è quella solita che useremmo, per esempio, con un pulsante:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menubar = wx.MenuBar()
mymenu = wx.Menu()
item1 = mymenu.Append(-1, 'voce uno')
item2 = mymenu.Append(-1, 'voce due')
44
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
menubar.Append(mymenu, 'PrimoMenu')
submenu = wx.Menu()
item10 = submenu.Append(-1, 'voce uno del submenu')
item11 = submenu.Append(-1, 'voce due del submenu')
mymenu = wx.Menu()
item3 = mymenu.Append(-1, 'voce tre')
item4 = mymenu.Append(-1, 'voce quattro')
mymenu.AppendMenu(-1, "ecco un sotto-menu", submenu)
item5 = mymenu.Append(-1, 'voce cinque')
menubar.Append(mymenu, 'SecondoMenu')
self.SetMenuBar(menubar)
# collego ogni singola
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
self.Bind(wx.EVT_MENU,
# e
def
def
def
def
def
def
def
voce a un callback
self.on_clic_item1, item1)
self.on_clic_item2, item2)
self.on_clic_item3, item3)
self.on_clic_item4, item4)
self.on_clic_item5, item5)
self.on_clic_item10, item10)
self.on_clic_item11, item11)
scrivo i relativi callback
on_clic_item1(self, evt): print 'clic su voce uno'
on_clic_item2(self, evt): print 'clic su voce due'
on_clic_item3(self, evt): print 'clic su voce tre'
on_clic_item4(self, evt): print 'clic su voce quattro'
on_clic_item5(self, evt): print 'clic su voce cinque'
on_clic_item10(self, evt): print 'clic su voce uno del submenu'
on_clic_item11(self, evt): print 'clic su voce due del submenu'
E’ solo ordinaria amministrazione, se sapete come si gestiscono gli eventi. L’unica cosa interessante da notare, è che
abbiamo adottato il “secondo” stile di binding (dei tre che avevamo identificato). Non possiamo infatti scrivere:
item1.Bind(wx.EVT_MENU, self.on_clic_item1) # etc. etc.
perché in effetti una voce di menu non è un event handler di per sé, e quindi non ha un metodo Bind. Siamo quindi
obbligati a collegare il frame stesso (self.Bind), e specificare poi la voce di menu che desideriamo collagare
passandola come argomento di Bind.
Come già accennato, questo è l’unico momento in cui effettivamente utilizziamo i nomi che abbiamo assegnato alle
voci di menu (ossia le variabili item1, item2, etc.). Per tenere insieme il momento di creazione della voce di menu
e il suo collegamento a un callback, spesso è più comodo scrivere:
mymenu = wx.Menu()
item1 = mymenu.Append(-1, 'voce uno')
self.Bind(wx.EVT_MENU, self.on_clic_item1, item1)
item2 = mymenu.Append(-1, 'voce due')
self.Bind(wx.EVT_MENU, self.on_clic_item2, item2)
# etc etc
e questo ci porta a un passo dal compattare ulteriormente:
mymenu = wx.Menu()
self.Bind(wx.EVT_MENU, self.on_clic_item1, mymenu.Append(-1, 'voce uno'))
self.Bind(wx.EVT_MENU, self.on_clic_item2, mymenu.Append(-1, 'voce due'))
# etc etc
5.8. I menu: le basi da sapere.
45
Appunti wxPython Documentation, Release 1
E in questo modo eliminiamo completamente la necessità di mantenere i riferimenti a item1, item2 etc.
5.8.7 Conclusione.
Con queste informazioni dovreste essere in grado di creare e gestire almeno i casi più comuni. Ci sono tuttavia molte
altre cose da dire sui menu: continuate a leggere!
5.9 I menu: altri concetti di base.
Questa seconda parte sui menu segue direttamente la precedente. Dopo la panoramica sui concetti fondamentali,
raccogliamo qui “in ordine sparso” alcune altre tecniche che dovreste conoscere per essere pronti a usare i menu nel
mondo reale. Dedichiamo invece una pagina separata per le tecniche un po’ più avanzate.
5.9.1 Scorciatoie da tastiera.
Nessun vero menu potebbe dirsi completo senza le scorciatoie da tastiera! wxPython vi permette di utilizzare entrambe
le tecniche classiche: le scorciatoie e gli “acceleratori”.
Come creare una scorciatoia.
Le scorciatoie sono le lettere sottolineate nei menu, a cui si accede attraverso il tasto “alt”. Se volete “sottolineare”
una lettera in una voce di menu (ossia, creare una scorciatoia per quella lettera), basta premettere una &:
item1 = menu.Append(-1, 'Inc&olla')
Questo per esempio crea una scorciatoia per la “o” di “Incolla” (che apparirà sottolineata). Naturalmente, se avete
bisogno di scrivere una “&” nella vostra voce di menu, dovete fare l’escape e scrivere &&. Se avete bisogno di creare
una scorciatoia proprio per la “&” allora... non so, non ci ho mai provato e non dovreste neanche voi.
Una particolarità importante: se decidete di fornire scorciatoie per le vostre voci di menu, ricordatevi che ogni “nodo”
che porta a quella voce deve avere la sua scorciatoia: questo perché la prima pressione di “Alt+lettera” aprirà il menu,
poi si aprono gli eventuali sottomenu, fino a trovare la voce esatta. Se non è possibile fare il percorso completo con la
tastiera, non sarà possibile attivare la vostra scorciatoia. Quindi:
menu = wx.Menu()
item1 = menu.Append(-1, 'Inc&olla') # se creo questa scorciatoia...
# ...
menubar.Append(menu, "&Miomenu")
# ... devo farne una nel nodo precedente!
Infine, naturalmente, dovete fare attenzione a rendere le scorciatoie uniche (almeno nell’ambito di un menu). La
politica consueta è di “sottolineare” sempre la prima lettera, e passare alla seconda/terza lettera solo in caso di conflitto.
Una volta era considerato essenziale fornire le scorciatoie da tastiera, se non proprio per tutte le voci di tutti i menu,
almeno per quelle più importanti. Oggi non è più così imprescindibile: si è capito che gli utenti alla fine le usano poco,
e preferiscono piuttosto memorizzare gli “acceleratori” più comuni.
Come creare un acceleratore.
Gli acceleratori sono quelle combinazioni con due o tre tasti (“ctrl+alt+tasto”, e così via) che eseguono direttamente
un’azione senza la necessità di aprire un menu. Per esempio, in genere “Ctrl+S” serve per “salvare”, “Ctrl+O” serve
per “aprire”, “Ctrl+C” per “copiare” e “Ctrl+V” per “incollare” (qualsiasi cosa voglia dire nella logica della vostra
applicazione!). All’interno dei menu, gli acceleratori sono sempre scritti accanto alla voce corrispondente.
46
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
A dire il vero gli acceleratori sono definiti a livello globale nella vostra applicazione, e potete usarli anche al fuori dei
menu (e perfino se non usate nessun menu!). Tuttavia è molto comune usarli in accoppiata con i menu, e in effetti
c’è un modo facilissimo per integrare acceleratori e menu in wxPython: basta aggiungere all’etichetta della voce la
sequenza \tModificatore+Tasto. Per esempio:
menu = wx.Menu()
item1 = menu.Append(-1, 'Salva\tCtrl+s')
item1 = menu.Append(-1, 'Salva con nome\tCtrl+Shift+s')
item3 = menu.Append(-1, 'Chiudi\tAlt+F4') # ehm... in Windows ;-)
E’ perfettamente possibile usare insieme acceleratori e scorciatoie:
item1 = menu.Append(-1, '&Salva\tCtrl+s')
Occasionalmente potrebbe servirvi la funzione globale wx.StripMenuCodes per ottenere la voce di menu “depurata” dai vari simboli extra:
>>> wx.StripMenuCodes('&Salva\tCtrl+s')
u'Salva'
Ricordatevi che gli acceleratori sono globali in tutta la vostra applicazione, e quindi devono essere unici: non potete
definire lo stesso acceleratore per due azioni diverse, anche se si trovano in menu differenti.
Cercate di fornire acceleratori coerenti con le normali consuetudini: non definite “Ctrl+V” per “Valutare” un film,
anche se state scrivendo un software di recensioni cinematografiche. Se la vostra applicazione non ha bisogno di
azioni standard come aprire, salvare, copiare, incollare... non cercate comunque di riciclare questi acceleratori per i
vostri scopi: gli utenti sono abituati a usare “Ctrl+o” con il significato di “aprire”, e non vogliono dover memorizzare
una convenzione alternativa valida solo per il vostro programma.
Viceversa, se la vostra applicazione prevede azioni standard, ricordatevi di fornire gli accelerati standard corrispondenti: gli utenti se lo aspettano.
Un tocco di classe: fornite gli acceleratori più comuni nelle diverse piattaforme. Per esempio, “chiudere” un programma è “Alt+F4” in Windows, “Cmd+Q” nel Mac. Potete testare a runtime la piattaforma in uso, ricorrendo per
esempio a wx.Platform (o anche, naturalmente, a sys.platform della libreria standard di Python):
accel = {'__WXMSW__': '\tAlt+F4',
'__WXMAC__': '\tCtrl+Q'}[wx.Platform]
menu.Append(-1, 'Esci'+accel)
Notate che wxPython sul Mac traduce il “Ctrl” automaticamente in “Cmd” quindi non dovete preoccuparvi di questo
dettaglio. Con questa agevolazione, di fatto la stragrande maggioranza degli acceleratori sono identici tra le varie
piattaforme: potete regolarvi con questa pagina di Wikipedia. Diventa più complesso se volete tener conto delle
abitudini nazionali: per esempio, “trova” può diventare “Ctrl+T” in italiano.
Creare un acceleratore senza legarlo a una voce di menu.
Siccome qui stiamo parlando di menu, questa parte è un po’ fuori tema: la inseriamo ugualmente per completezza.
Come abbiamo già accennato, gli acceleratori possono essere definiti anche indipendentemente dai menu (o addirittura
in assenza di menu). La procedura però è un po’ più complicata.
Occorre prima di tutto istanziare una wx.AcceleratorTable, che è semplicemente un contenitore di uno o più
wx.AcceleratorEntry. Il codice da scrivere sarebbe quindi qualcosa come:
table = wx.AcceleratorTable([wx.AcceleratorEntry(......),
wx.AcceleratorEntry(......),
wx.AcceleratorEntry(......)])
5.9. I menu: altri concetti di base.
47
Appunti wxPython Documentation, Release 1
wxPython tuttavia ci permette di usare una più semplice lista di tuple (la conversione a oggetti
wx.AcceleratorEntry viene fatta in automatico). Possiamo quindi scrivere:
table = wx.AcceleratorTable([(......),
(......),
(......)])
Una volta descritte la tabella, è necessario infine assegnarla usando self.SetAcceleratorTable(table)
(dove self è la finestra corrente).
Un wx.AcceleratorEntry, a sua volta, deve essere costruito con tre parametri.
• Il primo è una bitmask che compone i tasti di controllo che devono essere premuti. La scelta è tra:
• wx.ACCEL_NORMAL (nessun modificatore)
• wx.ACCEL_ALT
• wx.ACCEL_SHIFT
• wx.ACCEL_CTRL (“Ctrl”, oppure “Cmd” sul Mac)
• wx.ACCEL_RAW_CTRL (“Ctrl” sempre, anche sul Mac)
• Il secondo è il keycode del tasto da associare (semplicemente ord(key))
• Il terzo è l’id del widget che emette il wx.CommandEvent che vogliamo innescare.
Questo ultimo parametro ci svela finalmente la vera natura degli acceleratori: si tratta semplicemente di un modo
rapido per simulare un clic su un widget. Basta che nell’interfaccia sia presente un widget in grado di emettere
un wx.CommandEvent (per esempio un pulsante), e possiamo simularne la pressione per innescare il callback
associato.
In questo esempio, che riassume tutto quello che abbiamo detto fin qui, troviamo due pulsanti collegati ad altrettanti
acceleratori:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
a_button = wx.Button(p, -1, 'pulsante a', pos=((10, 10)))
b_button = wx.Button(p, -1, 'pulsante b', pos=((10, 50)))
a_button.Bind(wx.EVT_BUTTON, self.on_a_button)
b_button.Bind(wx.EVT_BUTTON, self.on_b_button)
table = wx.AcceleratorTable(
[(wx.ACCEL_CTRL, ord('t'), a_button.GetId()),
(wx.ACCEL_CTRL|wx.ACCEL_SHIFT, ord('t'), b_button.GetId())]
)
self.SetAcceleratorTable(table)
def on_a_button(self, evt): print "evento a"
def on_b_button(self, evt): print "evento b"
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Certamente possiamo usare una wx.AcceleratorTable anche per creare acceleratori legati alle voci di menu,
all’occorrenza:
48
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
item1 = menu.Append(-1, 'Salva')
#...
table = wx.AcceleratorTable(
[(wx.ACCEL_CTRL, ord('s'), item1.GetId())]
)
self.SetAcceleratorTable(table)
Ma oltre a essere più complesso del modo facile visto prima, così non otteniamo automaticamente di inserire lo
shortcut dell’acceleratore accanto all’etichetta della voce del menu.
Infine, naturalmente, niente ci vieta di associare una voce di menu e (per esempio) un pulsante allo stesso callback, e
per buona misura usare un acceleratore per entrambi. In questo scenario conviene naturalmente associare l’acceleratore
alla voce di menu con il metodo rapido visto prima: quando l’utente digita la combinazione di tasti ottiene comunque
l’effetto desiderato, non importa se il clic è simulato sul menu o sul pulsante:
b = wx.Button(self, -1, 'Salva')
b.Bind(wx.EVT_BUTTON, self.on_clic)
# ...
item1 = menu.Append(-1, '&Salva\tCtrl+s') # acceleratore!
self.Bind(wx.EVT_MENU, self.on_clic, item1) # bind allo stesso callback
#...
def on_clic(self, evt): print 'stiamo salvando...'
5.9.2 Disabilitare i menu.
Come praticamente tutti i widget di wxPython, anche le voci di menu hanno un metodo Enable che consente di
abilitarli e disabilitarli, e un metodo IsEnabled per scoprire il loro stato attuale. Naturalmente, per fare questo
dovete accedere alle singole voci al di fuori dell’__init__, e pertanto dovete conservare un riferimento in una
variabile di istanza (con il self davanti, per capirci).
Disabilitare un intero menu è più faticoso, perché wx.Menu non dispone di un metodo Enable. Occorre passare per
wx.MenuBar.EnableTop. Questo metodo accetta due parametri:
• la posizione del menu che voglaimo (a partire da 0 per il primo);
• un boolean per dire se il menu deve essere abilitato o disabilitato.
Purtroppo quindi ci tocca conoscere la posizione del menu che ci interessa nella barra dei menu. Possiamo conservarla
in una variabile al momento della creazione, oppure scoprirla in seguito con wx.MenuBar.FindMenu che accetta
il nome del menu e restituisce il suo indice.
La controparte di wx.MenuBar.EnableTop per scoprire se un menu è attualmente abilitato,
wx.MenuBar.IsEnabledTop, con un costruttore analogo.
è
Ecco un esempio pratico che mette insieme tutto questo:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu_A = wx.Menu()
# teniamo un riferimento per questa voce di menu
self.voce_salva = menu_A.Append(-1, 'Salva')
menu_A.Append(-1, 'Apri')
menu_A.Append(-1, 'Chiudi')
menu_B = wx.Menu()
menu_B.Append(-1, 'Qui')
5.9. I menu: altri concetti di base.
49
Appunti wxPython Documentation, Release 1
menu_B.Append(-1, 'Quo')
menu_B.Append(-1, 'Qua')
menubar = wx.MenuBar()
menubar.Append(menu_A, 'File')
menubar.Append(menu_B, 'Paperi')
self.SetMenuBar(menubar)
# conserviamo un riferimento alla menubar:
self.menubar = menubar
# ricordiamo l'indice della posizione del menu_B nella menubar,
# se non preferiamo scoprirlo in seguito:
self.menu_B_position = 1
p = wx.Panel(self)
a_button = wx.Button(p, -1, '(dis)abilita Salva', pos=((10, 10)))
b_button = wx.Button(p, -1, '(dis)abilita menu Paperi', pos=((10, 50)))
a_button.Bind(wx.EVT_BUTTON, self.on_a_button)
b_button.Bind(wx.EVT_BUTTON, self.on_b_button)
def on_a_button(self, evt):
# dis/abilito la voce "salva" a seconda del suo stato corrente
self.voce_salva.Enable(not self.voce_salva.IsEnabled())
def on_b_button(self, evt):
# siccome abbiamo conservato l'indice della posizione del menu_B,
# possiamo usare direttamente quello.
# In alternativa, possiamo scoprirlo in questo modo:
# self.menu_B_position = self.menubar.FindMenu("Paperi")
is_enabled = self.menubar.IsEnabledTop(self.menu_B_position)
self.menubar.EnableTop(self.menu_B_position, not is_enabled)
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
5.9.3 Voci di menu spuntabili o selezionabili.
Ecco un’altra necessità piuttosto comune: i menu servono anche per fare delle scelte tra diverse opzioni, e per questo
ci sono tradizionalmente due possibilità:
• le voci di menu con la “spunta”, per possono essere de/selezionate individualmente;
• le voci di menu di tipo “radio”, presentate in gruppi all’interno dei quali è possibile selezionarne solo una alla
volta.
wxPython supporta entrambe le possibilità. Le voci “spuntabili” si ottengono aggiungendo il flag wx.ITEM_CHECK
al normale metodo Append. Le voci “radio” si ottengono aggiungendo il flag wx.ITEM_RADIO. Più voci “radio”
in successione si considerano parte di un gruppo. Un gruppo finisce quando si inserisce una voce non-radio (o un
separatore).
Ricordatevi che il flag wx.ITEM_* è il quarto argomento del metodo Append, come abbiamo già visto: il terzo è la
stringa di “help text” che spesso si lascia vuota).
50
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
Ecco un esempio per chiarire le cose dette fin qui:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu = wx.Menu()
menu.Append(-1, 'una voce normale')
menu.Append(-1, 'spunta uno', '', wx.ITEM_CHECK)
menu.Append(-1, 'spunta due', '', wx.ITEM_CHECK)
menu.Append(-1, 'spunta tre', '', wx.ITEM_CHECK)
# qui inizia un radio-group
menu.Append(-1, 'radio uno', '', wx.ITEM_RADIO)
menu.Append(-1, 'radio due', '', wx.ITEM_RADIO)
menu.Append(-1, 'radio tre', '', wx.ITEM_RADIO)
menu.AppendSeparator()
# qui inizia un nuovo radio-gruppo
menu.Append(-1, 'altro radio uno', '', wx.ITEM_RADIO)
menu.Append(-1, 'altro radio due', '', wx.ITEM_RADIO)
menubar = wx.MenuBar()
menubar.Append(menu, 'Menu')
self.SetMenuBar(menubar)
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Naturalmente potete collegare queste voci di menu “speciali” agli eventi come fareste di solito.
Il
wx.CommandEvent propagato da una voce di menu porta con sé un metodo IsChecked che potete interrogare per
sapere se l’utente ha appena spuntato la voce su cui ha fatto clic (questo in teoria funziona anche con le voci “radio”,
ma in pratica non serve a niente: se l’utente fa clic su una voce “radio”, questo vuol già dire che l’ha selezionata).
In alternativa, potete sapere in qualunque momento lo stato di una di queste voci interroganto il metodo IsChecked
del MenuItem. E naturalmente potete anche manipolare voi stessi lo stato di questi elementi, usando Check.
Ecco l’esempio di prima modificato per mostrare anche queste possibilità:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu = wx.Menu()
self.spunta_uno = menu.Append(-1, 'spunta uno', '', wx.ITEM_CHECK)
self.spunta_due = menu.Append(-1, 'spunta due', '', wx.ITEM_CHECK)
self.spunta_tre = menu.Append(-1, 'spunta tre', '', wx.ITEM_CHECK)
self.radio_uno = menu.Append(-1, 'radio uno', '', wx.ITEM_RADIO)
self.radio_due = menu.Append(-1, 'radio due', '', wx.ITEM_RADIO)
self.radio_tre = menu.Append(-1, 'radio tre', '', wx.ITEM_RADIO)
self.Bind(wx.EVT_MENU, self.on_spunta_due, self.spunta_due)
menubar = wx.MenuBar()
menubar.Append(menu, 'Menu')
self.SetMenuBar(menubar)
p = wx.Panel(self)
a_button = wx.Button(p, -1, 'manipola spunta', pos=((10, 10)))
b_button = wx.Button(p, -1, 'manipola radio', pos=((10, 50)))
5.9. I menu: altri concetti di base.
51
Appunti wxPython Documentation, Release 1
a_button.Bind(wx.EVT_BUTTON, self.on_a_button)
b_button.Bind(wx.EVT_BUTTON, self.on_b_button)
def on_a_button(self, evt):
print "spunta_uno adesso e' spuntato:", self.spunta_uno.IsChecked()
self.spunta_due.Check(not self.spunta_due.IsChecked())
print 'invertita la spunta di spunta_due'
def on_b_button(self, evt):
print "radio_uno adesso e' selezionato:", self.radio_uno.IsChecked()
self.radio_tre.Check(True)
print 'ho selezionato radio_tre'
def on_spunta_due(self, evt):
# Dimostra l'uso di evt.IsChecked.
# Qui avremmo potuto anche usare self.spunta_due.IsChecked()
print "adesso spunta_due e' spuntato:", evt.IsChecked()
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
5.9.4 Ranged events per i menu.
Quando abbiamo parlado degli id, ci siamo soffermati un po’ anche sul caso dei menu. E’ il caso di tornare a leggere
quel paragrafo, prima di procedere con la lettura qui.
Fatto? In quelle note si parlava di tecniche che qui finora non abbiamo mai incontrato, in particolare l’uso degli id
come scorciatoia per identificare la provenienza di un evento (grazie all’uso di evt.GetId() nel callback). Non
ripetiamo qui le cose già dette. Riassumendo:
• finché collegate ciascuna voce di menu a un callback separato, nessun problema;
• se volete collegare più voci a un singolo callback, potete farlo. Ma nel callback vi servirà rintracciare la voce da
cui è partito l’evento, e potete farlo con gli id;
• in particolare, se più voci hanno eventi consecutivi, potete collegarle in un colpo solo a un unico callback usando
wx.EVT_MENU_RANGE invece di wx.EVT_MENU.
Un classico esempio in cui di solito si fa in questo modo è proprio quando usate blocchi di voci “radio” (o anche,
sebbene più raramente, blocchi di voci spuntabili). In questi casi, in genere si preferisce raccogliere tutti gli eventi in
un solo callback, e smistare di qui la logica di decisione successiva.
Per esempio, sicuramente potreste anche fare così:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu = wx.Menu()
radio_uno = menu.Append(-1, 'radio uno', '', wx.ITEM_RADIO)
radio_due = menu.Append(-1, 'radio due', '', wx.ITEM_RADIO)
radio_tre = menu.Append(-1, 'radio tre', '', wx.ITEM_RADIO)
self.Bind(wx.EVT_MENU, self.on_radio_uno, radio_uno)
self.Bind(wx.EVT_MENU, self.on_radio_due, radio_due)
self.Bind(wx.EVT_MENU, self.on_radio_tre, radio_tre)
52
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
menubar = wx.MenuBar()
menubar.Append(menu, 'Menu')
self.SetMenuBar(menubar)
def on_radio_uno(self, evt): print 'hai selezionato radio_uno'
def on_radio_due(self, evt): print 'hai selezionato radio_due'
def on_radio_tre(self, evt): print 'hai selezionato radio_tre'
Ma così è molto prolisso. Una forma più compatta (notate l’uso degli id) sarebbe invece:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu = wx.Menu()
radio_uno = menu.Append(100, 'radio uno', '', wx.ITEM_RADIO)
radio_due = menu.Append(101, 'radio due', '', wx.ITEM_RADIO)
radio_tre = menu.Append(102, 'radio tre', '', wx.ITEM_RADIO)
self.Bind(wx.EVT_MENU_RANGE, self.on_radio, id=100, id2=102)
menubar = wx.MenuBar()
menubar.Append(menu, 'Menu')
self.SetMenuBar(menubar)
def on_radio(self, evt):
caller = evt.GetId()
if caller == 100:
print 'hai selezionato radio_uno'
elif caller == 101:
print 'hai selezionato radio_due'
elif caller == 102:
print 'hai selezionato radio_tre'
# etc. etc., o un qualsiasi metodo di dispatch che vi sembra opportuno
5.9.5 Conclusione.
Questa pagina e la precedente completano il panorama di ciò che vi serve sapere per usare i menu nella vita di tutti i
giorni. Ci sono tecniche più esotiche, di cui parleremo un’altra volta.
5.10 Questioni varie di stile.
Raccogliamo in questa pagina alcune questioni forse più di stile che di sostanza, che tuttavia possono confondere chi
è nuovo al mondo wxPython.
5.10.1 To self or not to self?
Quando si costruisce un contenitore (un panel, frame, dialogo), in genere nell’__init__ si elencano i widget che
deve contenere. E qui si pone un piccolo problema: è necessario che i loro nomi siano visibili in tutta l’istanza
del contenitore, oppure solo all’interno del metodo __init__ in cui li definiamo? Ovvero, è necessario sempre
premettere self. al nome, oppure no?
5.10. Questioni varie di stile.
53
Appunti wxPython Documentation, Release 1
La domanda si pone anche perché, all’inizio della propria esperienza con wxPython, molti purtroppo usano strumenti sbagliati come gui builder o RAD. Quando esaminate il codice prodotto da questi pessimi assistenti, trovate
interminabili elenchi del genere:
self.button_1 = wx.Button(...)
self.button_2 = wx.Button(...)
self.text_ctrl_1 = wx.TextCtrl(...)
self.label_1 = wx.StaticText(...)
self.label_2 = wx.StaticText(...)
e perfino con i sizer:
self.sizer_1 = wx.BoxSizer(...)
Il principiante ne ricava spesso la sensazione che il self. sia necessario. In realtà wxPython non richiede assolutamente niente del genere.
Regolatevi come fareste per un normale programma a oggetti: solo quando vi serve mantenere un riferimento visibile
anche negli altri metodi della classe, allora usate il self.. Altrimenti, potete benissimo farne a meno.
Considerate per esempio un normale pulsante: di solito avete bisogno del suo nome solo per collegarlo a un evento, e
per inserirlo in un sizer:
button = wx.Button(...)
button.Bind(wx.EVT_BUTTON, ...)
sizer.Add(button, ...)
Se tutte queste operazioni avvengono nell’__init__, non avete bisogno di chiamarlo self.button.
Un altro esempio: è rarissimo che vi serva il nome di un sizer al di fuori del metodo in cui lo avete creato. Premettere
self. ai nomi dei sizer è quasi sempre inutile.
E ancora: le label (wx.StaticText) vengono dichiarate e inserite nel layout, e mai più toccate. E’ un altro caso in
cui il self. non serve a nulla. Addirittura, nel caso delle label è frequente trovare questo idioma:
sizer.Add(wx.TextCtrl(...), ...)
ovvero, creare un label “anonima” solo nel momento in cui bisogna aggiungerla al layout. Il nome della label qui non
è specificato, perché non serve neppure all’interno dell’__init__.
In definitiva, è anche una questione di stile personale. Potete benissimo aggiungere self. ovunque: inquinerete il
namespace della vostra classe, ma non è grave. Per quel che vale, io preferisco regolarmi in modo diverso: uso self.
solo per i nomi che effettivamente utilizzo anche altrove, e mantengo locali tutti gli altri. Questo mi consente di vedere
a colpo d’occhio i widget più “importanti” nella mia gui.
5.10.2 Costruire il layout nell’__init__ o no?
Ovviamente il layout di un frame, di un dialogo o di un panel va specificato nell’__init__, prima di mostrarlo
all’utente. Tuttavia, alcuni preferiscono spostare il disegno “puro e semplice” del layout (creazione e popolamento dei
sizer) in un metodo separato _do_layout o qualcosa del genere, che viene richiamato dall’__init__.
Più o meno il pattern è questo:
def __init__(self, ...):
self.button_A = wx.Button(...)
self.text_A = wx.TextCtrl(...)
# etc. etc.
self._do_layout()
def _do_layout(self):
54
Chapter 5. Appunti wxPython - livello base
Appunti wxPython Documentation, Release 1
sizer = wx.BoxSizer(...)
sizer.Add(self.button_A, ...)
sizer.Add(self.text_A, ...)
# etc. etc.
Si tratta di una separazione puramente estetica: se nel codice eliminate le due righe self._do_layout() e def
_do_layout(self), tutto funziona esattamente come prima. E’ uno schema molto usato nel codice generato
automaticamente dagli stessi pessimi assistenti di cui sopra.
Lo schema può estendersi ulteriormente: per esempio, aggiungere un metodo _set_properties per impostare
colori, font, etc. dei vari widget; aggiungere un metodo _do_binding per raccogliere tutti i collegamenti degli
eventi ai callback.
Le ragioni addotte per queste separazioni sono sempre molto deboli: è vero, in questo modo l’__init__ è più snello
e contiene solo la definizione dei widget contenuti. Tuttavia è solo una suddivisione grafica, a beneficio dell’occhio.
Ma allora basta usare qualche divisore “grafico”, qualcosa del genere:
def __init__(self, ...)
button_A = wx.Button(...)
text_A = wx.TextCtrl(...)
# etc. etc.
# layout ----------------------------sizer = wx.BoxSizer(...)
sizer.Add(button_A, ...)
sizer.Add(text_A, ...)
# etc. etc.
# eventi ---------------------------button_A.Bind(wx.EVT_BUTTON, ...)
# etc. etc.
Naturalmente lo svantaggio immediato dei vari _do_layout etc., è la proliferazione dei self. (vedi paragrafo
precedente), perché ogni widget creato nell’__init__ deve essere visibile anche nel _do_layout.
Ma la cosa importante è capire che non si tratta di un reale processo di fattorizzazione: non una singola riga di codice
viene rielaborata e ridotta a pattern comuni.
Anzi, questa separazione artificiosa può addirittura ostacolare la fattorizzazione del codice. Considerate per esempio
questo modo di procedere molto compatto, che genera una serie di pulsanti, li collega a eventi e li inserisce in un sizer,
tutto in una volta:
for label in ('foo', 'bar', 'baz'):
b = wx.Button(self, -1, label)
b.Bind(wx.EVT_BUTTON, self.callback)
sizer.Add(b, 1, wx.EXPAND|wx.ALL, 5)
Chiaramente una cosa del genere non sarebbe più possibile con la divisione tra __init__ e _do_layout.
In conclusione, lasciate perdere i vari _do_layout e iniziate a scrivere tutto quanto nell’__init__. Dopo di che,
ponetevi il problema di una reale fattorizzazione del codice. Un buon esempio (forse troppo pignolo, a dire il vero) si
trova tra gli esempi della documentazione tratti dal capitolo 5 del libro “wxPython in Action”. Confrontate lo script
badExample.py con goodExample.py per avere un’idea di come si possa riformulare lo stesso layout in modo
più compatto e “astratto”.
5.10. Questioni varie di stile.
55
Appunti wxPython Documentation, Release 1
56
Chapter 5. Appunti wxPython - livello base
CHAPTER 6
Appunti wxPython - livello intermedio
6.1 wx.App: concetti avanzati.
Abbiamo già visto i concetti di base sulla wx.App, e come usarla per avviare la nostra applicazione wxPython.
In sostanza, si tratta di creare la wx.App, creare e mostrare il frame principale, e “avviare il motore” chiamando il
MainLoop della nosrta wx.App.
Questo in genere è più che sufficiente per i casi più semplici. Tuttavia, per le applicazioni più complesse, potreste
volere qualche tipo di controllo in più su questo processo di avviamento.
6.1.1 wx.App.OnInit: il bootstrap della vostra applicazione.
Per questo, dovete creare una sotto-classe personalizzata di wx.App, e sovrascrivere il metodo OnInit.
Il codice che scrivete in OnInit viene eseguito non appena la wx.App è stata istanziata, ma prima di entrare nel
MainLoop. E’ il momento giusto, tipicamente, per inizializzare alcune risorse esterne (connessioni al database, log
di sistema, file di configurazione, opzioni “globali”...). Infine, convine usare OnInit anche per istanziare e mostrare
il frame principale dell’applicazione: infatti, farlo in questo momento garantisce che la wx.App esiste già, e quindi la
creazione di frame e altri elementi può avvenire senza problemi.
Ecco un’idea di come potrebbero andare tipicamente le cose:
class MyApp(wx.App):
def OnInit(self):
# avvio la connessione al database
self.db = connect(my_db, ...)
# apro un log
self.log = open('logfile', 'a')
# leggo un file di configurazione
config = ConfigParser.ConfigParser(...)
# creo il frame principale
top_frame = MyFrame(None)
# lo mostro
top_frame.Show()
# esco da OnInit segnalando che tutto e' a posto
return True
if __name__ == '__main__':
app = MyApp(False)
# istanzio la mia app personalizzata
app.MainLoop()
# e invoco il MainLoop per avviarla
57
Appunti wxPython Documentation, Release 1
Questo modo di procedere presenta numerosi vantaggi. Per esempio, se una connessione al database viene aperta
in OnInit, “appartiene” alla wx.App, e quindi è disponibile globalmente per tutti gli elementi presenti e futuri
dell’applicazione. Da qualunque posto, per ottenere un riferimento al database basterà questo:
db = wx.GetApp().db
Un altro vantaggio è che OnInit deve restituire esplicitamente True alla fine delle sue operazioni: solo se restituisce
True la wx.App continuerà a vivere. Se invece restituisce False, la wx.App verrà chiusa, e la nostra applicazione
abortirà prematuramente.
Questo può essere sfruttato per chiudere subito tutto, quando per esempio una risorsa esterna non è disponibile. Per
esempio, potete fare questo:
class MyApp(wx.App):
def OnInit(self):
try:
self.db = connect(my_db, ...)
except:
return False
top_frame = MyFrame(None)
top_frame.Show()
return True
Se per qualche ragione il db non è raggiungibile, OnInit esce restituendo False, e tutto si ferma.
Inoltre, siccome al momento di OnInit la wx.App esiste già, è anche possibile mostrare all’utente dei messaggi
di emergenza per informarlo dello stato delle cose, e perfino chiedergli di prendere qualche decisione. Per esempio,
qualcosa del genere:
class MyApp(wx.App):
def OnInit(self):
try:
self.db = connect(my_db, ...)
except:
wx.MessageBox('Non trovo il db, addio.',
'Errore fatale', wx.ICON_STOP)
return False
try:
config = ConfigParser.ConfigParser(...)
except ConfigParser.Error:
msg = wx.MessageDialog(None,
'Non trovo il file di configurazione. Procedo lo stesso?',
'Errore trascurabile', wx.YES_NO|wx.ICON_EXCLAMATION)
if msg.ShowModal() == wx.ID_NO:
return False
top_frame = MyFrame(None)
top_frame.Show()
return True
Addirittura, potrebbe essere un buon momento per chiedere all’utente di effettuare il login:
class MyLoginDialog(wx.Dialog):
pass # etc etc
class MyApp(wx.App):
def OnInit(self):
try:
self.db = connect(my_db, ...)
except:
wx.MessageBox('Non trovo il db, addio.',
58
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
'Errore fatale', wx.ICON_STOP)
return False
login = MyLoginDialog(None)
if login.ShowModal() == wx.ID_OK:
user, psw = login.GetValue()
if self.db.get_user_psw(user) != psw:
wx.MessageBox('Password errata, addio.',
'Errore fatale', wx.ICON_STOP)
return False
else:
return False
# finalmente, se tutto va bene...
top_frame = MyFrame(None)
top_frame.Show()
return True
Come si vede, OnInit è molto flessibile, e consente di controllare un momento solitamente delicato come lo startup
dell’applicazione in modo preciso, compreso il feedback all’utente ed eventuali interazioni con lui. L’uso accorto di
OnInit permette anche di velocizzare i tempi: se qualcosa deve andar storto, si può fermare tutto prima di caricare
la parte più gravosa dell’interfaccia.
Infine, una piccola eleganza: avete notato che quando chiudiamo MyLoginDialog la nostra applicazione continua
a vivere (almeno fin quando, eventualmente, non decidiamo di return False), nonostante abbiamo appena chiuso
l’unica finestra “top level” presente? In effetti questa è un’eccezione alla regola: wxPython non inizia il processo di
chiusura, se non è mai entrato nel MainLoop. Questo ci consente di aprire e chiudere finestre a nostro piacimento in
OnInit senza paura di conseguenze spiacevoli.
Note: Potreste chiedervi perché c’è bisogno di un metodo separato OnInit per queste operazioni di apertura, quando
in genere in questi casi si lavora direttamente nell’__init__ della classe. Il punto è che l’__init__ è riservato
al bootstrap della stessa wx.App, e non è il posto giusto per metterci dentro anche il codice di inizializzazione della
vostra applicazione. Per esempio l’__init__ deve sempre restituire None, e quindi non è agevole gestire un errore di
inizializzazione differenziandolo con un diverso codice di uscita. Se ve la sentite, potete pasticciare con l’__init__
a vostro rischio e pericolo, naturalmente. Ma OnInit fornisce già un comodo aggancio per tutte le vostre necessità.
6.1.2 wx.App.OnExit: gestire le operazioni di chiusura.
In modo speculare, la wx.App fornisce anche un hook per il codice che volete eseguire subito prima della chiusura.
OnExit verrà eseguito dopo che l’ultima finestra top-level è stata chiusa, ma prima di distruggere la wx.App.
In OnExit potete inserire il vostro codice di chiusura personalizzato: chiudere le connessioni al database, chiudere i
log, salvare le configurazioni e le preferenze...
Proprio come avviene in OnInit, anche in OnExit potete approfittarne per chiedere ancora qualche decisione
all’utente. Potete ancora mostrare un wx.Dialog modale (ossia mostrato con ShowModal()).
Se però cercate di creare e mostrare un nuovo frame “top level” a questo punto, nella speranza di prevenire la chiusura
della wx.App, ormai è troppo tardi. Il frame verrà mostrato per un attimo, ma poi si chiuderà subito e tutto terminerà.
Note: Avvertenza per gli spericolati: non vale neppure cercare di prevenire la chiusura dell’applicazione settando
self.SetExitOnFrameDelete(False) prima di mostrare il nuovo frame top-level. Effettivamente il frame
resta visibile, ma l’applicazione si pianta. Questo codice, per esempio, non funziona:
class MyApp(wx.App):
def OnInit(self):
wx.Frame(None).Show()
6.1. wx.App: concetti avanzati.
59
Appunti wxPython Documentation, Release 1
return True
def OnExit(self):
self.SetExitOnFrameDelete(False)
wx.Frame(None).Show()
self.SetExitOnFrameDelete(True)
Parlare di OnExit ci porta naturalmente a parlare più nel dettaglio del processo di chiusura delle applicazioni wxPython... ma a questo argomento dedichiamo una pagina separata.
6.1.3 Re-indirizzare lo standard output/error.
Durante tutto il ciclo di vita di una applicazione wxPython, lo standard output e lo standard error sono normalmente
utilizzati, e per default restano come di consueto indirizzati verso la shell da cui avete invocato lo script Python del
vostro programma.
Il costruttore di wx.App accetta tuttavia un argomento redirect che determina se l’output dell’applicazione deve
essere re-indirizzato altrove:
• se redirect=False (default su Unix), l’output è inviato alla shell;
• se redirect=True (default su Windows) e l’argomento filename è impostato, l’output è inviato a un file;
• infine, se redirect=True e filename non è impostato, l’output è inviato a una apposita finestra
dell’interfaccia grafica.
Cominciamo a vedere un esempio di re-indirizzamento verso un file:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
print '\n\nEcco un esempio di standard output...\n'
print '... segue un esempio di standard ERROR: '
print 1/0
if __name__ == '__main__':
app = wx.App(redirect=True, filename='output.txt')
MyFrame(None).Show()
app.MainLoop()
Per provare invece il re-indirizzamento verso una finestra della gui, provate semplicemente a usare lo stesso frame con
questa wx.App:
if __name__ == '__main__':
app = wx.App(True) # re-indirizzamento verso una finestra
MyFrame(None).Show()
app.MainLoop()
Notate che all’inizio la finestra dell’output non è visibile: si apre la prima volta che viene scritto qualcosa su uno dei
due stream.
La finestra che ospita l’output, per default, è gestita da wx.PyOnDemandOutputWindow. Si tratta di una classe
che, non appena è necessario, crea, mostra e gestisce un semplice frame con un wx.TextCtrl multi-linea e di
60
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
sola lettura. Le scritture degli stream nel wx.TextCtrl sono thread-safe: se provengono da thread secondari, sono
chiamate attraverso wx.CallAfter.
La finestra di output, una volta creata, è una finestra top-level: questo è necessario, perché al momento di istanziare
la wx.App con redirect=True nessun frame principale è ancora stato creato. Tuttavia è una limitazione fastidiosa: anche quando l’utente ha chiuso la finestra principale dell’applicazione, la wx.App (e quindi in sostanza il
programma) resterà in vita, finché non viene chiusa anche la finestra dell’output. Se questo per voi è un problema,
potete chiamare wx.PyOnDemandOutputWindow.SetParent per assegnare un parent alla finestra di output.
Tuttavia questo va fatto in un momento preciso: dovete chiamare SetParent dopo aver creato almeno il frame principale (per forza: altrimenti non avete nessun parent da assegnare alla finestra di output); ma prima che la finestra sia
creata la prima volta (ovvero, prima che venga scritto qualcosa nell’output). Un esempio chiarirà forse meglio: usate
ancora una volta il frame dell’esempio precedente, ma avviate l’applicazione in questo modo:
if __name__ == '__main__':
app = wx.App(True) # re-indirizzamento verso una finestra
frame = MyFrame(None)
# questo e' il momento giusto per impostare il parent della stdioWin
app.stdioWin.SetParent(frame)
frame.Show()
app.MainLoop()
Come si vede dal questo esempio, quando creiamo una wx.App con il parametro redirect=True,
questa crea subito una istanza di wx.PyOnDemandOutputWindow e ne conserva un riferimento in
wx.App.stdioWin. Possiamo quindi usare questa variabile per impostare il parent della finestra di output prima
che wx.PyOnDemandOutputWindow abbia il tempo di crearla (cosa che avviene automaticamente la prima volta
che viene scritto qualcosa nello stream): per maggior sicurezza, in questo caso abbiamo preferito impostare il parent
prima ancora di mostare la finestra principale dell’applicazione.
Il re-indirizzamento dell’output può essere deciso anche a run-time, in un momento successivo alla creazione della
wx.App. Per questo basta chiamare il metodo wx.App.RedirectStdio, che accetta un argomento opzionale
filename (se impostato, l’output viene scritto su un file; altrimenti, viene mostrata la finestra). Analogamente,
wx.App.RestoreStdio ripristina il normale indirizzamento dell’output verso la shell:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
c = wx.CheckBox(p, -1, "re-indirizzamento output", pos=(20, 60))
c.Bind(wx.EVT_CHECKBOX, self.on_check)
def on_check(self, evt):
app = wx.GetApp()
if evt.IsChecked():
app.RedirectStdio()
# provate anche:
# app.RedirectStdio('output.txt')
else:
app.RestoreStdio()
# questo e' necessario perche' la finestra non si chiude da sola
app.stdioWin.close() # notare la "c" minuscola!
def on_clic(self, evt):
print '... uno standard output ...'
if __name__ == '__main__':
app = wx.App(False)
6.1. wx.App: concetti avanzati.
61
Appunti wxPython Documentation, Release 1
MyFrame(None).Show()
app.MainLoop()
Se poi wx.PyOnDemandOutputWindow vi sembra troppo spartana e volete mostrare l’output in un
modo più elegante, anche questo si può fare.
Il tipo di finestra impiegata è determinato dall’attributo
wx.App.outputWindowClass. Si tratta di un attributo di classe, e pertanto può essere impostato perfino prima
di istanziare la wx.App:
if __name__ == '__main__':
wx.App.outputWindowClass = MyOutputClass # una classe personalizzata
app = wx.App(True)
# etc etc
Oppure, se volete sotto-classare wx.App:
class MyApp(wx.App):
MyApp.outputWindowClass = MyOutputClass
def OnInit(self):
# etc etc
La vostra classe personalizzata può essere qualsiasi cosa: potete sotto-classare wx.App.outputWindowClass
oppure creare da zero. Dovete tuttavia impegnarvi a rispettare l’api di wx.App.outputWindowClass, mettendo
a disposizione i seguenti metodi:
• CreateOutputWindow(self, txt) dove create effettivamente la finestra di output, e ci scrivete il primo
messaggio ricevuto (txt);
• write(self, txt) dove scrivete il testo nella finestra di output (che se non c’è ancora, deve essere creata).
Ricordatevi che questo metodo potrebbe essere chiamato da thread secondari, e quindi dovrebbe essere threadsafe (e se non lo è, come minimo ricordatevi di non usare i thread!);
• close() dove chiudete la finestra di output;
• flush() che viene chiamato per i file e quindi dev’esserci, ma non dovrebbe essere necessario per una finestra
grafica (e infatti nell’implementazione di wx.App.outputWindowClass è una NOP).
Inoltre, dovreste come minimo ricordarvi di intercettare il wx.EVT_CLOSE che si genera quando l’utente chiude la
finestra per conto proprio (oppure, rendere la finestra non chiudibile in qualche modo).
Nell’esempio che segue proponiamo una semplice alternativa, che aggiunge un pulsante per pulire la finestra e uno per
salvare l’output su un file. Notate come abbiamo implementato tutti i metodi richiesti, e ci siamo anche assicurati che
le scritture siano thread-safe:
class MyOutputWindow(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.text = wx.TextCtrl(p, style=wx.TE_MULTILINE|wx.TE_READONLY)
clear = wx.Button(p, -1, 'cancella')
save = wx.Button(p, -1, 'salva...')
clear.Bind(wx.EVT_BUTTON, self.on_clear)
save.Bind(wx.EVT_BUTTON, self.on_save)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.text, 1, wx.EXPAND|wx.ALL, 5)
s1 = wx.BoxSizer(wx.HORIZONTAL)
s1.Add(clear, 1, wx.EXPAND|wx.ALL, 5)
s1.Add(save, 1, wx.EXPAND|wx.ALL, 5)
s.Add(s1, 0, wx.EXPAND)
p.SetSizer(s)
62
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
def on_clear(self, evt):
self.text.Clear()
def on_save(self, evt):
filename = wx.FileSelector('Salva output', wildcard='TXT files|*.txt',
flags=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT)
if filename.strip():
with open(filename, 'a') as f:
f.write(self.text.GetValue())
def write(self, txt):
self.text.AppendText(txt)
class MyOutputManager(object):
def __init__(self):
self.frame = None
self.parent = None
def SetParent(self, parent):
self.parent = parent
def CreateOutputWindow(self, txt):
self.frame = MyOutputWindow(self.parent, -1, title='stdout/stderr')
self.frame.write(txt)
self.frame.Show(True)
self.frame.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
def OnCloseWindow(self, event):
if self.frame is not None:
self.frame.Destroy()
self.frame = None
self.parent = None
def write(self, txt):
if self.frame is None:
if not wx.Thread_IsMain(): # le scritture sono thread-safe!
wx.CallAfter(self.CreateOutputWindow, txt)
else:
self.CreateOutputWindow(txt)
else:
if not wx.Thread_IsMain(): # le scritture sono thread-safe!
wx.CallAfter(self.frame.write, txt)
else:
self.frame.write(txt)
def close(self):
if self.frame is not None: # anche la chiusura deve essere thread-safe
wx.CallAfter(self.frame.Close)
def flush(self): pass
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=(20, 20))
6.1. wx.App: concetti avanzati.
63
Appunti wxPython Documentation, Release 1
b.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
print '... uno standard output ...'
if __name__ == '__main__':
# impostiamo MyOutputManager come finestra dell'output
wx.App.outputWindowClass = MyOutputManager
app = wx.App(True)
MyFrame(None).Show()
app.MainLoop()
Todo
una pagina sui thread.
6.2 Chiudere i frame e gli altri widget.
Il momento della chiusura in wxPython è delicato. In questa pagina esaminiamo il processo di chiusura dei frame e
dei dialoghi, e aggiungiamo qualche nota sulla chiusura dei widget in generale. Il discorso prosegue in una pagina
separata dove descriviamo il processo di chiusura della wx.App.
Prima di procedere, un avvertimento. Dopo aver letto questo capitolo, avrete l’impressione che chiudere le cose, in
wxPython, sia un procedimento tortuoso. Rispondo subito: è vero.
Ma non dovete dimenticare che wxPython è solo la superficie di un framework C++. Il punto è che in Python, quando
de-referenziate un oggetto, automaticamente tutte le risorse collegate che non servono più vegono pulite dalla memoria
dal garbage collector. Ma C++ non ha garbage collector, quindi la memoria va pulita “a mano”. Ora, quando chiudete
un frame, wxWidget naturalmente provvede da solo a eliminare anche tutti gli elementi di quel frame (pulsanti, caselle
di testo, etc.). Ma tutte le risorse “esterne” eventualmente collegate e che non servono più (connessioni al database,
strutture-dati presenti in memoria, etc.) devono essere cancellate a mano.
Ecco perché wxWidget, nel mondo C++, offre numerosi hook lungo il processo di chiusura, dove è possibile intervenire
per fare cleanup, o anche ripensarci e tornare indietro. wxPython eredita questo sistema, anche se, in effetti, è raro che
un programmatore Python lo utilizzi appieno.
6.2.1 La chiusura di una finestra.
La chiusura di una “finestra” (un frame o un dialogo) può capitare (di solito!) in due modi:
• perché l’utente fa clic sul pulsante di chiusura (quello con la X, per intenderci), o usa “Alt+F4”, “Mela-Q” o
altri equivalenti;
• perché voi chiamate Close() sul frame.
In entrambi i casi, si innesca un evento particolare, wx.EVT_CLOSE, che segnala al sistema che la finestra sta per
chiudersi. Quanto segue presuppone che voi abbiate un’idea di come funziona il meccanismo degli eventi.
Dunque, se voi non fate nulla, la parola passa al gestore di default, che si comporta in due modi differenti:
• per un wx.Frame, semplicemente chiama Destroy(), e questo pone fine alla vita del frame e ovviamente di
tutti i suoi figli;
64
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
• per un wx.Dialog, invece, lo nasconde alla vista, ma non lo distrugge. Questo comportamento è voluto, per
venire incontro alla situazione tipica in cui si vuole raccogliere dei dati dal dialogo, prima di distruggerlo. Resta
il fatto che, fin quando non chiamate esplicitamente Destroy(), il dialogo resta in vita.
Questo è quello che succede se voi non fate nulla. Ma chiaramente, come qualsiasi altro evento, anche
wx.EVT_CLOSE può essere catturato e gestito in un callback.
In questo caso, diciamo subito la cosa più importante. Se decidete di raccogliere wx.EVT_CLOSE e gestire da soli il
processo di chisura, allora dovete esplicitamente chiamare Destroy() anche per i frame, perché wxPython non farà
più nulla al posto vostro.
Potete anche decidere di non chiudere la finestra, dopo tutto. Questo, per esempio, è un procedimento tipico (anche
se, dal punto dell’usabilità, è una pessima pratica che vi sconsiglio):
1
2
3
4
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
5
6
7
8
9
10
11
def on_close(self, evt):
msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',
wx.ICON_QUESTION|wx.YES_NO)
if msg.ShowModal() == wx.ID_YES:
self.Destroy() # oppure, volendo: evt.Skip()
msg.Destroy()
Alla riga 10, chiamiamo esplicitamente self.Destroy() solo se l’utente ha risposto affermativamente. In caso
contrario, chiamiamo comunque Destroy() almeno sul MessageDialog che abbiamo creato, perché altrimenti
resterebbe in vita.
Invece di chiamare self.Destroy(), avremmo naturalmente anche potuto chiamare evt.Skip(), lasciando al
gestore predefinito dell’evento il compito di distruggere la finestra.
Come abbiamo detto, wx.EVT_CLOSE si innesca anche quando voi chiamate Close(). Per vederlo, possiamo
aggiungere un semplice pulsante che chiama Close nel suo callback:
1
2
3
4
5
6
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'chiudimi!')
b.Bind(wx.EVT_BUTTON, self.on_click)
self.Bind(wx.EVT_CLOSE, self.on_close)
7
8
9
def on_click(self, evt):
self.Close()
10
11
12
13
14
15
16
def on_close(self, evt):
msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',
wx.ICON_QUESTION|wx.YES_NO)
if msg.ShowModal() == wx.ID_YES:
self.Destroy()
msg.Destroy()
Anche quando agite sul pulsante, il self.Close() della riga 9 scatena comunque il wx.EVT_CLOSE, e di conseguenza viene eseguito il codice del callback on_close.
6.2. Chiudere i frame e gli altri widget.
65
Appunti wxPython Documentation, Release 1
6.2.2 Chiamare Veto() se non si vuole chiudere.
Se alla fine decidete di non chiudere la finestra, è buona norma chiamare sempre Veto() sull’evento
wx.EVT_CLOSE, per segnalare al resto del sistema che la richiesta di chiusura è stata respinta.
Per esempio, nel codice appena visto, dovreste aggiungere evt.Veto() alla fine del gestore on_close. Ora, in
questo specifico caso non vi serve comunque a nulla, perché nessun’altra parte del vostro codice è interessata alla sorte
di quella finestra.
Ma Veto() diventa utile, per esempio, quando chiamate Close() su una finestra da un’altra finestra: in questo
caso, la finestra che ordina la chiusura potrebbe essere interessata a sapere se l’ordine è stato eseguito o rifiutato.
Close() restituisce sempre True se la chiusura è andata a buon fine. Ma se voi chiamate Veto() (e non chiudete
la finestra, chiaramente), allora Close() restituisce False, e fa sapere in questo modo come sono andate le cose.
Ecco un esempio pratico:
1
2
3
4
5
6
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.do_child = wx.Button(self, -1, 'crea un frame figlio')
self.do_child.Bind(wx.EVT_BUTTON, self.on_child)
self.child = None
7
def on_child(self, evt):
if not self.child:
self.child = MyChildFrame(self, title='Figlio', size=(150, 150),
style=wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX)
self.child.Show()
self.do_child.SetLabel('CHIUDI il frame figlio')
else:
closed_successful = self.child.Close()
if closed_successful:
self.do_child.SetLabel('crea un frame figlio')
self.child = None
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyChildFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
24
def on_close(self, evt):
msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',
wx.ICON_QUESTION|wx.YES_NO)
if msg.ShowModal() == wx.ID_NO:
evt.Veto()
else:
self.Destroy()
msg.Destroy()
25
26
27
28
29
30
31
32
33
34
35
36
37
if __name__ == '__main__':
app = wx.App(False)
MyTopFrame(None).Show()
app.MainLoop()
In questo esempio, il frame principale crea e poi cerca di chiudere (alla riga 15) un frame figlio. Il frame figlio però
può decidere se chiudersi davvero, o rifiutare. Notate che, se decidiamo di non chiuderlo, chiamiamo Veto() (alla
riga 29) in modo che Close() restituisca False, e quindi il codice chiamante sappia come comportarsi (alle righe
15-18).
66
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Non chiudere un frame e “vietare” l’evento sono due cose indipendenti: se vietate ma poi chiudete lo stesso, Close()
restituisce comunque False, anche se la chiusura in effeti c’è stata. E viceversa. Quindi sta a voi non fare pasticci.
Questo, dite la verità, vi sembra un po’ cervellotico... ve l’avevo detto. E non è ancora finita.
6.2.3 Ignorare il Veto() se si vuole chiudere lo stesso.
E non è ancora finita, dicevamo. Chiamare semplicemente Veto() su un evento di chiusura potrebbe non essere
sicuro. Infatti, talvola l’evento non ha il potere di “vietare” la chiusura della finestra.
Attenzione! Se chiamate Veto() alla cieca, e l’evento in realtà non può “vietare” un bel niente, wxPython solleva
un’eccezione e tutto si pianta...
Quindi la cosa giusta è verificare sempre se l’evento effettivamente può “vietare”, prima di chiamare Veto(). La
verifica può essere fatta chiamando CanVeto() sull’evento stesso. Ecco come dovrebbe essere modificato il callback
dell’esempio precedente:
1
2
3
4
5
6
7
8
9
10
11
def on_close(self, evt):
if evt.CanVeto():
msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',
wx.ICON_QUESTION|wx.YES_NO)
if msg.ShowModal() == wx.ID_NO:
evt.Veto()
else:
self.Destroy()
msg.Destroy()
else: # se non possiamo vietare, dobbiamo distruggere per forza...
self.Destroy()
Ora, in verità l’annotazione della riga 10 non è del tutto corretta. Anche se non possiamo “vietare” l’evento, possiamo
sempre scegliere di non distruggere la finestra, e fare qualcos’altro (ricordiamo che “vietare” l’evento e chiudere effettivamente sono due cose separate). Ma questa sarebbe proprio una cosa da non fare. Primo perché ovviamente, se non
distruggiamo mai in risposta a un wx.EVT_CLOSE, la nostra finestra non si chiuderà mai (a meno di non distruggerla
esplicitamente chiamando Destroy() anziché Close()). Secondo, perché se non chiamiamo Veto() (perché
non possiamo) e non distruggiamo neppure la finestra, la chiamata a Close() restituirà comunque True (perché
l’evento non è stato “vietato”), anche se la finestra non è stata davvero chiusa. Quindi il codice chiamante potrebbe
avere problemi a regolarsi.
Resta solo una domanda: in quali casi un evento potrebbe non avere il potere di Veto?
Ebbene, le cose stanno così: di solito un wx.CLOSE_EVENT ha il potere di Veto. Questo, per esempio, accade
quando l’evento si innesca in seguito al clic sul pulsante di chiusura, alla combinazione “Alt+F4” nei sistemi Windows,
etc. oppure quando voi chiamate Close() su una finestra.
Tuttavia, se voi chiamate Close con l’opzione Close(force=True), allora il wx.EVT_CLOSE che si genera
non ha il potere di “vietare” un bel niente (o più precisamente, restituisce False quando testate per CanVeto()).
Questo, come vedete, può essere un bel problema per il codice che gestisce la chiusura: non potete sapere se verrà
eseguito in seguito a una chiamata Close() o a una chiamata Close(True). Per questo, l’unica soluzione è
appunto testare sempre se l’evento CanVeto() prima di chiamare eventualmente il Veto().
6.2.4 Essere sicuri che una finestra si chiuda davvero.
Ancora una precisazione. L’opzione force=True del metodo Close è un pochino ingannevole. Non significa
affatto, di per sé, che la chiusura della finestra verrà forzata e quindi garantita in ogni caso. Vuol dire solo che l’evento
non avrà il potere di “vietare” la chiusura. Ma, ricordiamolo ancora una volta, “vietare” l’evento e chiudere davvero
6.2. Chiudere i frame e gli altri widget.
67
Appunti wxPython Documentation, Release 1
la finestra sono due cose indipendenti. Se voi intercettate l’evento e nel callback finite per non chiudere la finestra,
ebbene la finestra resterà viva anche in seguito a un Close(force=True).
Ovviamente scrivere un callback che non chiude la finestra, nonostante l’evento non abbia il potere di Veto, deve
essere considerato una cattiva pratica, se non un errore di programmazione vero e proprio. Ma wxPython non ha
modo di rilevare una cosa del genere a runtime, e voi non potete sapere se state chiamando Close(True) su una
finestra con un callback scritto male (da qualcun altro, ovviamente!).
In definitiva, l’unico modo per essere certi che una finestra si chiuda davvero è chiamare direttamente Destroy(),
ma così facendo vi perdete l’eventuale gestione dell’evento di chiusura. In generale, non lo consiglio.
Questo lascia aperto il problema: come faccio a sapere se una finestra è stata davvero distrutta?
Ebbene, dopo che avete chiamato Close() (magari con l’aggiunta di force=True), l’unico modo di sapere se la
finestra è stata davvero distrutta, è... chiamarla, ovviamente! Sul “lato Python” di wxPython, il riferimento all’oggetto
resterà ancora nel namespace corrente. Ma sul “lato C++” di wxWidgets, quando una finestra è distrutta, semplicemente smetterà di funzionare. Quindi una chiamata successiva a un metodo qualsiasi dovrebbe sollevare un’eccezione
wx.PyDeadObjectError, che voi opportunamente intrappolerete in un try/except. Per andare sul sicuro,
scegliete un metodo che ogni widget deve avere per forza, per esempio GetId. Qualcosa come:
try:
my_widget.GetId()
except wx.PyDeadObjectError:
# siamo sicuri che e' davvero morto
Todo
una pagina su SWIG e l’oop Python/C++.
Ma ci sarebbe ancora un problema (ve lo aspettavate, dite la verità). Quando chiamate Close o addirittura Destroy,
questo impegna wxPython a distruggere la finestra... appena possibile, ma non necessariamente subito. Di sicuro la
distruzione avverrà entro il prossimo ciclo del MainLoop, ma se chiamate GetId su un frame immediatamente dopo
averlo distrutto, la chiamata per il momento andrà ancora a segno.
Provate questo codice, per esempio:
1
2
3
4
5
6
7
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
kill = wx.Button(self, -1, 'uccidi il figlio', pos=(10, 10))
kill.Bind(wx.EVT_BUTTON, self.on_kill)
autopsy = wx.Button(self, -1, "verifica se e' morto", pos=(10, 50))
autopsy.Bind(wx.EVT_BUTTON, self.on_autopsy)
8
self.child = wx.Frame(self, -1, 'FRAME FIGLIO')
self.child.Show()
9
10
11
def on_kill(self, evt):
self.child.Destroy() # andiamo sul sicuro...
self.child.GetId()
12
13
14
15
def on_autopsy(self, evt):
self.child.GetId()
16
17
18
19
20
21
22
if __name__ == '__main__':
app = wx.App(False)
MyTopFrame(None, size=(150, 150)).Show()
app.MainLoop()
68
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Sorprendentemente, la chiamata della riga 14 andrà ancora a segno, anche se avete appena distrutto il frame. Se
invece, dopo aver distrutto il frame, premete il pulsante “verifica”, la chiamata della riga 17 solleverà il tanto sospirato
wx.PyDeadObjectError.
Sull’eccezione wx.PyDeadObjectError resta ancora qualcosa da dire. Fin qui ne abbiamo parlato dal punto di
vista della sua desiderabilità (quando cioè lo usiamo come strumento per testare se un widget è stato distrutto). Nelle
pagine dedicate alle eccezioni ne parliamo invece dal punto di vista opposto.
Come abbiamo appena visto, la distruzione di un widget non può essere “istantanea”: questo talvolta porta a delle
complicazioni ulteriori, di cui parliamo in una pagina separata. Per il momento, ci limitiamo a concludere che non
c’è modo di sapere esattamente quando un widget verrà distrutto: tuttavia, dopo un ragionevole intervallo di tempo, è
molto facile capire se è stato distrutto.
6.2.5 Distruggere un singolo widget.
Praticamente tutti i widget in wxPython hanno un metodo Close e un metodo Destroy. Se volete distruggere un
pulsante, per esempio, potete regolarvi come abbiamo visto sopra.
In genere preferite chiamare direttamente Destroy, perché non avete bisogno di catturare il wx.EVT_CLOSE di un
widget qualsiasi. Tuttavia, nessuno vi vieta di sottoclassare un widget, e prescrivere un comportamento particolare da
tenere quando qualcuno cerca di chiuderlo.
Tuttavia, è raro distruggere un singolo widget. In genere si preferisce disabilitarlo, al limite nasconderlo: distruggerlo
lascia un “buco” nel layout sottostante, che bisogna riaggiustare.
Un caso limite sono i Panel, ovviamente. Questi contenitori sono “quasi” dei frame, e quindi talvolta potrebbe aver
senso distruggerli, e perfino gestire qualche raffinatezza con Close. Personalmente, io consiglio di non distruggere
mai neppure i Panel. Ovviamente, se distruggete un Panel (o un altro widget qualsiasi) anche tutti i suoi “figli”
verranno spazzati via.
Ecco un esempio di Panel “schizzinoso” che potrebbe opporsi alla sua distruzione:
1
2
3
4
5
class MyPanel(wx.Panel):
def __init__(self, *a, **k):
wx.Panel.__init__(self, *a, **k)
self.SetBackgroundColour(wx.RED) # per distinguerlo...
self.Bind(wx.EVT_CLOSE, self.on_close)
6
7
8
9
10
11
12
13
14
def on_close(self, evt):
msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Distruggi Panel',
wx.ICON_QUESTION|wx.YES_NO)
if msg.ShowModal() == wx.ID_NO:
evt.Veto()
else:
self.Destroy()
msg.Destroy()
15
16
17
18
19
20
21
22
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'distruggi panel')
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.b = b
23
24
25
26
self.mypanel = MyPanel(p)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(wx.TextCtrl(self.mypanel, -1, 'figlio di MyPanel'),
6.2. Chiudere i frame e gli altri widget.
69
Appunti wxPython Documentation, Release 1
0, wx.EXPAND|wx.ALL, 15)
self.mypanel.SetSizer(s)
27
28
29
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.mypanel, 1, wx.EXPAND)
s.Add(b, 0, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s)
30
31
32
33
34
def on_clic(self, evt):
ret = self.mypanel.Close()
if ret:
pass # etc. etc.
35
36
37
38
39
40
41
42
43
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
Come si vede, se il Panel si chiude davvero, resta un buco. Alla riga 38, bisognerà fare qualcosa: riempire il buco,
riaggustare il layout, etc.
Per finire, una menzione per DestroyChildren: quest’arma di distruzione di massa, usata su un widget qualsiasi,
lascia in vita lui ma distrugge automaticamente tutti i suoi “figli”. Naturalmente, la distruzione di ciascun figlio
comporta a catena la morte dei figli del figlio, e così via fino alla totale estinzione dell’albero dei discendenti. Può
tornare comodo, per esempio, per svuotare un wx.Panel senza però distruggerlo, e quindi ripopolarlo daccapo.
6.3 Terminare la wx.App.
In questa sezione analizziamo ciò che succede quando la wx.App termina, e con essa la nostra applicazione wxPython.
Il discorso prosegue direttamente da questa pagina, che forse vi conviene leggere prima.
6.3.1 La chiusura “normale”.
La storia semplice è questa: quando il MainLoop percepisce che anche l’ultima finestra “top level” è stata chiusa,
allora decide che il suo lavoro è terminato. Una volta usciti dal MainLoop, c’è ancora l’opportunità di fare qualche
operazione di pulizia nel wx.App.OnExit, come abbiamo già visto. Tuttavia non è più possibile, a questo punto,
creare una nuova finestra e tenere in vita la wx.App, il cui destino è ormai segnato. Terminato il wx.App.OnExit,
la nostra applicazione defunge definitivamente. Dentro il namespace del modulo Python resta ovviamente ancora un
riferimento nel nome app (o quello che avete usato per istanziare la wx.App), ma ormai non ha più alcuna utilità.
Questa è la storia semplice. Ma ovviamente avete ancora molti modi per complicarvi la vita.
Per prima cosa, ricordiamo che la wx.App termina una volta che tutte le finestre top-level sono state chiuse, ovvero
tutte le finestre che hanno parent=None, come abbiamo già visto. Quindi fate attenzione a non lasciare qualche
finestra top-level nascosta ma ancora viva. I casi tipici sono due: avete creato qualche dialogo top-level e non l’avete
mai distrutto esplicitamente (ricordiamo che chiamare Close() su un dialogo lo nasconde ma non lo distrugge).
Oppure, avete creato la wx.App con l’opzione redirect=True, e la finestra dello streaming output è ancora viva
ma nascosta per qualche ragione (forse perché, per tutta la durata della vostra applicazione, non c’è stato niente da
scrivere sullo streaming!). In questi casi, l’utente chiude il frame “principale”, ma la wx.App non termina davvero.
Forse pensate che questo non è un grande problema: l’utente ha comunque finito di interagire con la gui, e prima o
poi spegnerà il computer... Ma se invece riavvia e poi “chiude” un po’ di volte il programma, ben presto si troverà la
memoria intasata dalle vostre istanze fantasma.
E c’è dell’altro: se avete scritto qualcosa nel wx.App.OnExit, non verrà mai eseguito, perché non si esce mai dal
MainLoop. Inutile dire che, se questo codice comprende operazioni di assestamento dei dati nel database, o delle
70
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
scritture nel log, queste non verranno eseguite, e la prossima volta che si aprirà il programma ci si troverà con dati
inconsistenti.
Quindi fate sempre molta attenzione a non creare finestre top-level e poi lasciarle in giro senza sapere se ci sono
ancora o no. Una strategia di emergenza è catturare il wx.EVT_CLOSE della finestra “principale” e, prima di distruggerla, verificare che non ci siano altre finestre top-level ancora in vita (wx.GetTopLevelWindows torna utile),
ed eventualmente chiuderle. Anche in questo caso però fate attenzione perché non è detto che chiamare Close()
basti a garantire la distruzione (abbiamo già visto perché in un’altra pagina). La soluzione più brutale è chiamare
direttamente Destroy(), più o meno così:
# nel callback dell'EVT_CLOSE della "finestra principale"
def on_close(self, evt):
for window in wx.GetTopLevelWindows():
if window != self: # lascio me stesso per ultimo...
window.Destroy()
self.Destroy()
Questo funziona senz’altro, ma non è esente da altri rischi. Chiamare Destroy() sui dialoghi probabilmente va
ancora bene: se sono ancora vivi e nascosti, vuol dire che ve ne siete semplicemente dimenticati, ma ormai non
dovrebbero più avere nessuna funzione. Per i frame, d’altra parte, la situazione è più delicata. Forse prevedono del
codice da eseguire in risposta a un EVT_CLOSE, ma se chiamate Destroy() invece di Close() perderete quel
passaggio. Questo potrebbe portare a inconsistenze di vario tipo.
Nel dubbio, vi tocca controllare se si tratta di frame o di dialoghi, e agire con prudenza. Ma come faccio a sapere se
una finestra è un frame o un dialogo? Di colpo, siamo nel campo della magia nera di Python:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def on_close(self, evt):
ok_to_close = True
for window in wx.GetTopLevelWindows():
if window != self:
if wx._windows.Frame in window.__class__.__mro__:
# e' un frame, proviamo a chiuderlo gentilmente
ret = window.Close()
if not ret:
# evidentemente non vuole chiudersi!
ok_to_close = False
break
else:
# questo e' un dialogo: distruggiamolo senza pieta'
window.Destroy()
if ok_to_close:
self.Destroy()
else:
# c'e' in giro almeno una finestra che non vuole chiudersi
wx.MessageBox('Non posso chiudermi!')
evt.Veto()
return
Come vedete (riga 5), siamo piombati nel difficile, molto difficile. E non è detto che funzioni: per esempio, se una
delle finestre rifiuta di chiudersi, ma si “dimentica” di comunicare il suo Veto(), allora window.Close() (riga
7) restituirà True, e noi crederemo di averla chiusa quando invece è ancora in giro. Ci tocca aggiungere altri test per
essere davvero sicuri...
Ovviamente non sono ipotesi frequenti. Devo dire di non aver mai usato, in pratica, un metodo come questo per accertarmi che tutte le finestre top-level siano chiuse al momento di uscire dall’applicazione. E francamente vi sconsiglio
di provarci.
La soluzione corretta è invece tenere sempre traccia di tutte le finestre che aprite, soprattutto quelle top-level, e di
accertarvi sempre di chiuderle appena non servono più. In questo modo, quando arriva il momento di chiudere anche
6.3. Terminare la wx.App.
71
Appunti wxPython Documentation, Release 1
l’ultima finestra principale, siete sicuri che anche la wx.App terminerà la sua vita correttamente.
E’ opportuno ricordare che l’eventuale non-terminazione della wx.App non deve essere considerata come
un’eventualità da gestire, ma come un vero e proprio baco da correggere.
6.3.2 Come mantenere in vita la wx.App.
Ma c’è ancora dell’altro da sapere. Potrebbe capitarvi di non volere che la wx.App termini, ma che invece il suo
MainLoop resti attivo anche dopo che l’ultima finestra è stata chiusa.
Per fare questo, vi basta chiamare SetExitOnFrameDelete(False) sulla wx.App. Potete farlo proprio
all’inizio, in OnInit:
class MyApp(wx.App):
def OnInit(self):
self.SetExitOnFrameDelete(False)
return True
Oppure potete farlo successivamente, in un momento qualunque della vita del vostro programma, da dentro un frame
qualsiasi:
wx.GetApp().SetExitOnFrameDelete(False)
Potete farlo perfino, proprio all’ultimo, intercettando il wx.EVT_CLOSE dell’ultima finestra principale che sta per
chiudersi. L’unico momento in cui ormai è troppo tardi è nel wx.App.OnExit.
Con questa opzione, il MainLoop non termina quando l’ultima finesta muore. A questo punto, se volete, potete
andare avanti creando delle nuove finestre top-level. Ecco una possibile strategia:
1
2
3
4
5
6
class MyApp(wx.App):
def OnInit(self):
self.SetExitOnFrameDelete(False)
self.Bind(wx.EVT_IDLE, self.create_new_toplevel)
wx.Frame(None, title='PRIMA GENERAZIONE').Show()
return True
7
def create_new_toplevel(self, evt):
if not wx.GetTopLevelWindows():
wx.Frame(None, title='SECONDA GENERAZIONE!!').Show()
# dopo questa volta pero' basta...
self.SetExitOnFrameDelete(True)
8
9
10
11
12
13
14
15
16
if __name__ == '__main__':
app = MyApp(False)
app.MainLoop()
La procedura è chiara: all’inizio (riga 3) settiamo il flag a False, e quindi creiamo e mostriamo il primo frame toplevel. Tuttavia (riga 4) chiediamo anche alla wx.App di eseguire a ripetizione il metodo create_new_toplevel
nei momenti liberi del MainLoop. Questo metodo controlla se non sono più rimaste vive finestre top level (riga 9),
e in questo caso crea e mostra una “seconda generazione” di finestre. Contestualmente (riga 12) riportiamo il flag a
True, in modo che alla prossima chiusura il MainLoop questa volta termini davvero.
Ecco un altro possibile approccio:
1
2
3
4
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
5
72
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
6
7
8
def on_close(self, evt):
wx.CallLater(1, wx.GetApp().create_new_toplevel)
self.Destroy()
9
10
11
12
13
14
class MyApp(wx.App):
def OnInit(self):
self.SetExitOnFrameDelete(False)
MyFrame(None, title='PRIMA GENERAZIONE').Show()
return True
15
16
17
18
def create_new_toplevel(self):
MyFrame(None, title='SECONDA GENERAZIONE!!').Show()
self.SetExitOnFrameDelete(True)
19
20
21
22
if __name__ == '__main__':
app = MyApp(False)
app.MainLoop()
Qui invece è l’ultima finestra top-level che, al momento della sua chiusura (riga 7) utilizza wx.CallLater per
chiedere alla wx.App di creare una “seconda generazione” di frame immediatamente dopo la sua morte.
Notate l’utilizzo di wx.CallLater, che aspetta un certo periodo (in questo caso, 1 ms, il minimo possibile) e poi
chiama una funzione. Lo abbiamo scelto perché wx.CallLater non tiene impegnato il MainLoop, e quindi ci
serve a dimostrare che il MainLoop resta vivo comunque, per altri motivi (ossia, perché abbiamo settato il flag a
False).
Avremmo potuto invece usare wx.CallAfter, che è “quasi uguale”, nel senso che chiama una data funzione
dopo che tutti i gestori degli eventi correnti sono stati processati. Il punto però è che wx.CallAfter aggiunge
la sua funzione in coda ai compiti del MainLoop, e quindi lo tiene impegnato almeno fino a quel momento. E
siccome nel nostro caso la funzione chiamata è create_new_toplevel che appunto crea una nuova finestra toplevel, in sostanza il MainLoop non ha mai modo di terminare, indipendentemente da come è stato settato il flag
SetExitOnFrameDelete.
Provate a sostituire la riga 7 dell’esempio precedente con:
wx.CallAfter(wx.GetApp().create_new_toplevel)
Quando si distrugge la “prima generazione” compare la seconda, come previsto. Ma quando provate a distruggere
anche questa, la wx.App non termina come prima, anche se il flag è ormai impostato a True. Invece, ogni volta
appare una nuova “seconda generazione”, all’infinito. Questo perché wx.CallAfter tiene in vita il MainLoop
fino al momento di chiamare create_new_toplevel, dove però si crea una nuova finestra top-level, e quindi il
MainLoop trova un’altra ragione per proseguire la sua attività, all’infinito.
In altri termini wx.CallAfter, usato così, potrebbe essere un’altra strada per non far terminare il MainLoop,
senza dover usare SetExitOnFrameDelete. L’esempio di sopra potrebbe essere scritto anche così:
1
2
3
4
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
5
6
7
8
def on_close(self, evt):
wx.CallAfter(wx.GetApp().create_new_toplevel)
self.Destroy()
9
10
11
12
13
class MyApp(wx.App):
def OnInit(self):
MyFrame(None, title='PRIMA GENERAZIONE').Show()
return True
6.3. Terminare la wx.App.
73
Appunti wxPython Documentation, Release 1
14
def create_new_toplevel(self):
MyFrame(None, title='SECONDA GENERAZIONE!!').Show()
15
16
17
18
19
20
if __name__ == '__main__':
app = MyApp(False)
app.MainLoop()
Naturalmente questo lascia aperto il problema di capire come terminare, a un certo punto, la wx.App. Ma non è un
problema enorme. Si potrebbe aggiungere un test nel callback on_close, in modo da chiamare wx.CallAfter
una volta sola. Oppure si potrebbe chiamare wx.Exit()...
Ma questo è appunto l’argomento del prossimo paragrafo.
6.3.3 Altri modi di terminare la wx.App.
Ci sono almeno altri due modi per terminare una wx.App, entrambi sconsigliati nella pratica, ma utili da conoscere
come ultima risorsa.
Il primo è chiamare wx.GetApp().Exit() (oppure la scorciatoia equivalente wx.Exit()). Questo termina
immediatamente il MainLoop. Funziona, e lascia anche il tempo di eseguire il codice eventualmente contenuto in
wx.App.OnExit. Però chiude tutte le finestre top-level senza generare wx.EVT_CLOSE. Quindi, qualsiasi codice
di pulizia potevate aver scritto in risposta alla chiusura della finestra, verrà saltato.
Il secondo è chiamare wx.GetApp().ExitMainLoop(). Questo si comporta come Exit(), ma è un po’ più
gentile, perché aspetta che il ciclo corrente del MainLoop sia terminato prima di uscire. Da un lato, questo significa
la garanzia che gli eventi ancora pendenti saranno gestiti. Dall’altro, vuole anche dire che non c’è garanzia che il
programma sarà terminato proprio immediatamente.
6.3.4 Situazioni di emergenza.
In genere wx.Exit o wx.App.ExitMainLoop si usano in situazioni di emergenza, quando intercettate un errore
“di sistema” (un database o un’altra risorsa mancante, per esempio) e dovete staccare la spina in fretta, prima che
l’utente abbia il tempo di compromettere l’integrità dei dati, o peggiorare comunque la situazione.
Talvolta però il sistema diventa instabile per ragioni indipendenti dalla vostra applicazione. Per esempio l’utente
potrebbe per errore spegnere il computer prima di aver chiuso il vostro programma. Potreste comunque essere in grado
di salvare la situazione: wxPython emette un evento wx.EVT_QUERY_END_SESSION quando per qualche ragione
la sessione del sistema operativo è in procinto di terminare. Se lo intercettate, nel callback relativo potete gestire una
chiusura di emergenza della wx.App. Potreste anche provare a vietare l’evento chiamado wx.Event.Veto, in
certe condizioni (provate prima con wx.Event.CanVeto per verificare se è possibile).
In ogni caso, non è strettamente necessario prevedere wx.EVT_QUERY_END_SESSION. Se non lo intercettate, il
gestore di default chiama comunque wx.Window.Close su tutte le finestre top-level. Questo a sua volta, come
sappiamo, in condizioni normali vi permette di intercettare il conseguente wx.EVT_CLOSE per gestire le vostre
operazioni di chiusura. Se non intercettate neppure quello, la wx.App dovrebbe comunque avere il tempo di chiudersi
senza problemi, e quindi tutte le operazioni di chiusura “normale” che avete previsto in wx.App.OnExit dovrebbero
svolgersi regolarmente.
Infine, se vi trovate a dover gestire una chiusura di emergenza, può farvi comodo usare la funzione
wx.SafeShowMessage() per mostrare un ultimo messaggio all’utente in modo “sicuro” prima di spegnere la luce.
In Windows, questa funzione mostra il messaggio usando il dialogo nativo (senza quindi chiamare wx.MessageBox,
che potrebbe fallire); sulle altre piattaforme, scrive semplicemente il messaggio nello standard output. Potete usare
wx.SafeShowMessage anche in assenza di una wx.App funzionante, e quindi addirittura prima che la wx.App
sia stata correttamente avviata.
74
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
6.4 Le dimensioni in wxPython.
I questa pagina vediamo come si specificano le dimensioni di un widget in wxPython. Gran parte delle cose che
stiamo per dire hanno significato soprattutto quando sono applicate ai sizer. Quindi queste note sono un importante
complemento alle due pagine che dedichiamo ai sizer: in questa pagina diamo per scontato che le abbiate già lette.
6.4.1 wx.Size: la misura delle dimensioni.
In wxWidgets le dimensioni si indicano come istanze della classe wx.Size. Questo significa, per esempio, che per
definire le dimensioni di un pulsante bisogna scrivere:
button.SetSize(wx.Size(50, 50))
Tuttavia in wxWidgets trovate anche alcuni metodi “doppioni” che vi consentono più rapidamente di usare due argomenti (larghezza e altezza), senza bisogno di istanziare esplicitamente wx.Size. Questi doppioni si riconoscono
perché terminano in *WH. Per esempio, questo è equivalente alla precedente:
button.SetSizeWH(50, 50)
Talvolta invece accade il contrario. Il metodo “normale” richiede due argomenti, e tuttavia ne esiste anche una versione
“doppia” che consente l’uso di wx.Size. Questi doppioni si riconoscono perché terminano in *.Sz. Per esempio,
SetSizeHints ha un doppio SetSizeHintsSz.
In wxPython le cose sono più semplici da un lato, e involontariamente più complicate dall’altro. wxPython converte
automaticamente le istanze di wx.Size in più confortevoli tuple. Potete ancora usare esplicitamente wx.Size se
proprio volete, ma di solito si preferisce farne a meno:
button.SetSize((50, 50))
Notate che comuque SetSize vuole un solo argomento! Soltanto, abbiamo scritto una tupla al posto dell’istanza
di wx.Size. Naturalmente si può sempre usare la versione *WH del metodo, se si preferisce passare due argomenti
invece della tupla.
In aggiunta a questo, in wxPython alcuni metodi “getter” hanno dei “doppioni” che restituiscono una tupla al posto
di istanze di wx.Size. Si riconoscono perché terminano in *Tuple. Per esempio, GetSize ha il compagno
GetSizeTuple.
Note: Tutto questo è in effetti confuso e ridondante. E’ vero, ma dovete ricordare che in C++ non sono disponibili
le strutture-dati di alto livello di Python. Quindi wxWidgets definisce una lunga serie di tipi fondamentali, come
wx.Point, wx.Size, wx.Rect, wx.DateTime e molti altri. wxPython da un lato semplifica le cose, dall’altro
necessariamente le complica, perchè i nuovi oggetti Python devono coesistere con le classi preesistenti C++.
Un’osservazione conclusiva: passare il valore -1 a qualsiasi argomento di queste funzioni, è come dire “non mi
importa”. Per esempio, se scrivete:
button.SetSize((50, -1))
imponete che la larghezza del pulsante sia 50 pixel, ma lasciate libero wxPython di determinare l’altezza.
6.4.2 Gli strumenti per definire le dimensioni.
Ecco un rapido riassunto dei metodi “getter” e “setter” più significativi per quanto riguarda le dimensioni dei widget.
Quando esistono “doppioni” dei metodi, li indico in forma abbreviata. Per esempio, SetSize(WH) indica che esiste
anche la forma *WH di SetSize.
6.4. Le dimensioni in wxPython.
75
Appunti wxPython Documentation, Release 1
• GetSize(Tuple), SetSize(WH): specificano esattamente le dimensioni che deve avere il widget. Notate
che, se il widget è inserito in un sizer con flag wx.EXPAND e/o con proporzione superiore a 0, le sue dimensioni
potrebbero comunque variare.
• GetClientSize(Tuple), SetClientSize(WH): come i precedenti, ma meno platform-dependent se
usati con i frame e i dialoghi. Infatti calcolano solo l’area “effettiva” della finestra, lasciando fuori bordi e barra
del titolo, che possono avere dimensioni diverse su diversi sistemi. Chiaramente, se un widget non ha bordi, è
lo stesso che dire GetSize.
• SetInitialSize, come SetSize, ma se lasciate delle dimensioni libere (passando -1), le completa con il
“best size” del widget (vedi sotto). Notate che questo è esattamente il comportamento del paramentro size del
costruttore di tutti i widget. Quindi SetInitialSize è come un “costruttore differito” per quanto riguarda
le dimensioni (da cui lo “Initial” nel nome). In più, SetInitialSize imposta anche le dimensioni minime
(come chiamare SetMinSize, vedi sotto).
• GetMaxSize, SetMaxSize, GetMinSize, SetMinSize: specificano le dimensioni massime e minime
che può avere il widget.
• SetSizeHints(Sz): consente di specificare dimensioni massime e minime in un colpo solo, come dire
SetMaxSize seguito da SetMinSize.
• GetVirtualSize(Tuple), SetVirtualSize(WH), SetVirtualSizeHints(Sz): per le finestre
con scrolling incorporato (wx.ScrolledWindow, etc.) si riferisce alle dimensioni “vere”, e non quelle che
si vedono effettivamente.
• GetBestSize(Tuple): il “best size”, ossia le dimensioni minime per cui il widget si mantiene “presentabile” (per esempio, per un wx.StaticText questo dipende dalla lunghezza del testo che deve essere
visualizzato).
La cosa importante da capire qui è che potete indicare esattamente le dimensioni di un widget, fornire indicazioni
su minimi e/o massimi, o infine non indicarle affatto. Tenendo conto dei vincoli che imponete, l’algoritmo dei sizer
cercherà di distribuire lo spazio disponibile nel miglior modo possibile.
6.4.3 Fit e Layout: ricalcolare le dimensioni.
Esistono apparentemente due versioni di Fit, una come metodo di wx.Sizer (quindi di tutti i sizer derivati) e
un’altra come metodo di wx.Window (quindi di tutti i widget). In realtà il secondo finisce per chiamare il primo,
quindi alla fine è indifferente quale utilizzate.
wx.Sizer.Fit(window) (passando come argomento il contenitore che il sizer gestisce) dice al sizer di calcolare
le dimensioni della finestra basandosi su tutto quello che conosce riguardo agli elementi al suo interno.
wx.Window.Fit() (senza argomenti) dice alla finestra di calcolare le sue dimensioni, con strategie diverse a seconda dei casi. Se alla finestra è stato assegnato un sizer, chiama direttamente wx.Sizer.Fit(window) per fare il
lavoro. Altrimenti sceglie il “best size” per la finestra.
Anche Layout è disponibile sia come metodo dei sizer, sia dei contenitori (e ha effetti analoghi in entrambi i
casi). Chiamare Layout() forza il ricalcolo dell’algoritmo del sizer (e/o dei constraints). Notate che il gestore di default di un evento wx.EVT_SIZE non chiama Layout automaticamente per ridisegnare la finestra
ogni volta che l’utente la ridimensiona, e in genere questo non è necessario se il vostro layout è disegnato con i
sizer. Se invece usate i constraints, o se comunque volete che Layout sia chiamato ogni volta, potete impostare
wx.Window.SetAutoLayout(True) sul contenitore-parent di grado più alto (per esempio, il panel che contiene i widget che volete ri-disegnare). Ricordatevi anche che, se catturate voi stessi un wx.EVT_SIZE, dovreste
sempre ricordarvi di chiamare Skip nel vostro callback per consentire la gestione di default dell’evento. Se non vi
orientate in tutto questo, probabilmente non avete ancora letto la sezione dedicata agli eventi.
Ci sono due casi tipici (constraints a parte) in cui forzare il ricalcolo con Layout è utile:
76
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
• quando date delle dimensioni fisse a un frame, e poi lo riempite con dei widget, li organizzate in un sizer e infine
assegnate il sizer al frame, in effetti il frame non riceve alcun wx.EVT_SIZE dopo il primo dimensionamento,
e quindi verrà disegnato male. In questi casi, un self.Layout() proprio alla fine dell’__init__ risolve
le cose (ma un’altra soluzione, beninteso, è ricordarsi di impostare le dimensioni del frame come ultima cosa,
oppure non impostarle affatto);
• all’occorrenza, per ri-disegnare la finestra dopo che è stata mostrata, se per esempio sono stati aggiunti o nascosti
dei widget.
In casi particolari potrebbe essere necessario innescare programmaticamente un wx.EVT_SIZE, anche se la finestra
non viene ridimensionata. Per esempio, se nascondete/mostrate una toolbar, o un menu, o una status bar, allora
chiamare Layout da solo non basta, perché questi elementi non sono gestiti direttamente dai sizer. In casi del genere,
potete chiamare SendSizeEvent() sulla finestra per innescare programmaticamente un wx.EVT_SIZE.
6.5 I sizer: seconda parte.
Questa pagina riprende il discorso da dove lo avevamo interrotto nella pagina introduttiva sui sizer e sul
wx.BoxSizer in particolare. Per avere un quadro completo sui sizer, è utile anche leggere la pagina dedicata
alle dimensioni dei widget.
6.5.1 wx.GridSizer: una griglia rigida.
Se wx.BoxSizer è una colonna (o una riga) singola, wx.GridSizer è invece una griglia di celle. Al contrario
del wx.BoxSizer, al momento di creare un wx.GridSizer dovete specificare in anticipo il numero di righe e di
colonne. Per esempio:
sizer = wx.GridSizer(3, 2, 5, 5)
avrà 3 righe e 2 colonne. Il terzo e il quarto argomento specificano lo spazio (verticale e orizzontale) da lasciare tra le
celle, in pixel.
Una volta che avete creato il sizer, potete inserire i vari elementi usando Add come di consueto. Il sizer si riempirà da
sinistra a destra e dall’alto in basso.
Il wx.GridSizer è una struttura rigida: il widget più grande determina la dimensione di tutte le celle. Se gli altri
widget hanno flag wx.EXPAND e/o priorità superiore a 0, allora occuperanno l’intero spazio della cella. Altrimenti,
resteranno più piccoli della cella (con eventuale allineamento se hanno flag wx.ALIGN_*).
Una considerazione ulteriore sui bordi. Questo sizer permette di specificare uno spazio tra le righe e/o tra le colonne,
ma non uno spazio “di cornice”. Avete due opzioni per rimediare: la prima è non specificare gli spazi, e inserire ciascun
widget con il proprio bordo. La seconda è specificare gli spazi, e poi inserire il sizer completo in un wx.BoxSizer
di una sola cella, con il bordo adeguato. L’esempio che segue illustra questa seconda tecnica.
Il wx.GridSizer non è molto usato in pratica. La sua struttura rigida è limitante, e di solito lo rende utile solo
quando siete sicuri che tutti i widget abbiano le stesse dimensioni. Il caso da manuale sono i pulsanti di una calcolatrice:
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
sizer = wx.GridSizer(4, 4, 2, 2)
for lab in '789/456*123-0.=+':
b = wx.Button(p, -1, lab, size=(30, 30), name=lab)
b.Bind(wx.EVT_BUTTON, self.on_clic)
sizer.Add(b, 1, wx.EXPAND)
6.5. I sizer: seconda parte.
77
Appunti wxPython Documentation, Release 1
boxsizer = wx.BoxSizer()
boxsizer.Add(sizer, 1, wx.EXPAND|wx.ALL, 5) # un bordo di cornice
p.SetSizer(boxsizer)
sizer.Fit(self)
def on_clic(self, evt): print evt.GetEventObject().GetName(),
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
6.5.2 wx.FlexGridSizer: una griglia elastica.
wx.FlexGridSizer è una versione più flessibile di wx.GridSizer, ed è quella che si utilizza più spesso. E’
possibile infatti definire una o più righe (e/o colonne) che possono espandersi occupando lo spazio ancora disponibile
dopo che tutte quelle “normali” avranno occupato lo spazio minimo richiesto (basandosi sul widget più grande che
devono contenere, come per il wx.GridSizer).
Le righe/colonne “flessibili” si contendono lo spazio disponibile in base alla stessa regola delle priorità che abbiamo
visto per il wx.BoxSizer. Quindi, per esempio:
sizer = wx.FlexGridSizer(5, 5, 2, 2) # una griglia 5 x 5
sizer.AddGrowableCol(0, 1)
sizer.AddGrowableCol(1, 2)
sizer.AddGrowableRow(4) # e' sottinteso sizer.AddGrowableRow(4, 1)
vuol dire che la prima e la seconda colonna si spartiranno 1/3 e 2/3 dello spazio orizzontale disponibile, mentre la
quinta riga occuperà tutto lo spazio extra verticale.
Il FlexGridSizer è lo strumento più usato in tutte le situazioni in cui occorre creare una griglia dove, per esempio,
una colonna ha maggiore importanza. Il caso tipico è l’entry-form:
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
sizer = wx.FlexGridSizer(4, 2, 5, 5)
sizer.AddGrowableCol(1)
for lab in ('nome', 'cognome', 'indirizzo',
sizer.Add(wx.StaticText(p, -1, lab), 0,
sizer.Add(wx.TextCtrl(p, -1, name=lab),
boxsizer = wx.BoxSizer()
boxsizer.Add(sizer, 1, wx.EXPAND|wx.ALL, 5)
p.SetSizer(boxsizer)
'telefono'):
wx.ALIGN_CENTER_VERTICAL)
0, wx.EXPAND)
# un bordo di cornice
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
6.5.3 wx.GridBagSizer: una griglia ancora più flessibile.
Un wx.GridBagSizer è come un wx.FlexGridSizer, con due proprietà aggiuntive:
• è possibile specificare una cella precisa in cui inserire il widget;
78
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
• è possibile fare in modo che un widget si estenda in più celle adiacenti (come si comportano le tabelle HTML).
La prima proprietà può essere comoda in certi casi, ma se usate un wx.GridBagSizer solo per crearlo e riempirlo
una volta per sempre, allora è più ordinato utilizzare un semplice wx.(Flex)GridSizer‘. La seconda, d’altra parte, può
essere interessante.
Entrambe le proprietà sono ottenute modificando il metodo Add, che ora vuole due argomenti nuovi. Il primo (obbligatorio!) è pos, una tupla per specificare la posizione di inserimento. Il secondo (facoltativo) è span, per specificare
per quante righe (o colonne) adiacenti occorre estendere il widget, a partire dalla cella di inserimento.
Per esempio:
sizer.Add(widget, pos=(0, 0), span=(3, 2))
vuol dire che il widget, a partire dalla prima cella in alto a sinistra, si espande per tre righe e due colonne.
In compenso, Add perde l’argomento proportion, per cui dovete risolvere tutto con AddGrowableCol/Row e
specificando lo span.
Usare i wx.GridBagSizer può essere comodo da un lato, fonte di confusione dall’altro. Ovviamente tutto ciò che
potete fare con un wx.GridBagSizer potete farlo anche con la composizione di sizer più semplici. In generale,
quando il layout che avete in mente assomiglia a una griglia con forti irregolarità, potete prendere in considerazione il
wx.GridBagSizer. Questo, comunque, è il genere di layout che dovete disegnare prima su un foglio di carta, per
non confondervi troppo.
6.5.4 wx.StaticBoxSizer: un sizer per raggruppamenti logici.
Lasciamo per ultimo il wx.StaticBoxSizer, che è semplicemente un‘‘wx.BoxSizer‘‘ applicato a uno
wx.StaticBox.
L’aspetto grafico è quello di un consueto wx.StaticBox, ossia una linea rettangolare che circonda gli elementi
inclusi, con una label in alto.
Lo wx.StaticBoxSizer va usato solo in accoppiata con il suo wx.StaticBox, che va creato per primo. Infatti
il costruttore dello wx.StaticBoxSizer richiede un argomento in più rispetto al normale wx.BoxSizer, ossia
appunto un riferimento allo wx.StaticBox:
box = wx.StaticBox(parent, -1, 'opzioni')
sbs = wx.StaticBoxSizer(box, wx.VERTICAL)
Per il resto, l’uso di questo sizer è normalissimo:
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
box = wx.StaticBox(p, -1, 'opzioni')
sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
for i in range(3):
sizer.Add(wx.Button(p), 1, wx.EXPAND|wx.ALL, 5)
p.SetSizer(sizer)
6.5.5 StdDialogButtonSizer e CreateButtonSizer: sizer per pulsanti generici.
Abbiamo già incontrato il concetto di pulsanti con Id predefiniti, da usare tipicamente nei dialoghi. Per maggiore
comodità, è possibile inserirli automaticamente in un sizer orizzontale chiamato StdDialogButtonSizer.
6.5. I sizer: seconda parte.
79
Appunti wxPython Documentation, Release 1
Il metodo CreateButtonSizer,
chiamato su un dialogo,
restituisce
StdDialogButtonSizer già completo e pronto da inserire nel resto del layout.
automaticamente
un
Per esempio, questo:
btn_sizer = self.CreateButtonSizer(wx.OK|wx.CANCEL) # 'self' e' un dialogo
main_sizer.Add(btn_sizer, ...)
restituisce un sizer completo di due pulsanti con Id predefiniti (“ok” e “cancella”).
Gli Id predefiniti che è possibile utilizzare sono wx.ID_OK, wx.ID_CANCEL, wx.ID_YES, wx.ID_NO,
wx.ID_HELP.
6.5.6 wx.WrapSizer: un BoxSizer che sa quando “andare a capo”.
Concludiamo con quello che è probabilmente il più sconosciuto e meno documentato dei sizer di wxPython.
wx.WrapSizer si comporta in modo quasi identico a wx.BoxSizer, ma quando raggiunge il bordo del contenitore a cui è assegnato (per esempio, il bordo di una finestra) “va a capo” aggiungendo un’altra riga o un’altra
colonna, a seconda dell’orientamento.
Provate questo semplice esempio, per capire come si comporta:
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
s = wx.WrapSizer(wx.VERTICAL, 2)
for i in range(10):
s.Add(wx.Button(p, -1, str(i)), 0, wx.ALL, 5)
# s.Add((50, 50))
for i in range(10, 20):
s.Add(wx.Button(p, -1, str(i)), 0, wx.ALL, 5)
p.SetSizer(s)
Se provate a cambiare le dimensioni della finestra, vedrete che i pulsanti si distribuiscono su una o più colonne,
a seconda dello spazio che hanno. E’ facile immaginare che un wx.WrapSizer potrebbe tornare comodo, per
esempio, per visualizzare un elenco di piccole immagini (thumbnail) o in situazioni analoghe.
wx.WrapSizer ha poi alcune particolarità che meritano di essere ricordate. Purtroppo bisogna dire che wxPython
non implementa in modo completo questo sizer: di conseguenza, alcune delle proprietà che si possono leggere nella
documentazione di wxWidgets in realtà non funzionano in wxPython; può darsi che in futuro saranno implementate,
ma in ogni caso conviene sempre sperimentare direttamente.
La prima cosa che salta all’occhio, è che wx.WrapSizer si può istanziare con due argomenti: il primo è il consueto
flag di orientamento (come per wx.BoxSizer, può essere orizzontale o verticale). Il secondo è una serie di flag di
stile che possono essere:
• nessun flag settato (valore 1);
• flag EXTEND_LAST_ON_EACH_LINE (valore 0);
• flag REMOVE_LEADING_SPACES (valore 3);
• WRAPSIZER_DEFAULT_FLAGS (entrambi i flag settati, valore 2).
Purtroppo
però
wxPython
non
esporta
queste
costanti
(per
esempio,
non
esiste
wx.EXTEND_LAST_ON_EACH_LINE, come è facile controllare). Di conseguenza, occorre usare direttamente il loro valore numerico, come abbiamo fatto nell’esempio qui sopra. La documentazione di wxWidgets
sostiene che il comportamento di default è di avere entrambi i flag settati: in wxPython tuttavia sembra che il valore
di default sia al contrario di non avere nessun flag settato, e inoltre i valori numerici delle costanti sembrano differire
80
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
tra wxPython e wxWidgets (qui sopra abbiamo indicato quelli di wxPython, naturalmente). Quindi conviene sempre
dichiarare esplicitamente quali flag si desiderano attivi (di solito è più utile settare il secondo, o entrambi: potete
comunque sperimentare le alternative).
REMOVE_LEADING_SPACES serve se introducete degli spazi vuoti nel vostro sizer: in questo caso, il flag impedisce
che lo spazio vuoto finisca all’inizio di una riga o di una colonna. Nell’esempio qui sopra, de-commentate la riga
s.Add((50, 50)) per introdurre uno spazio a metà dei pulsanti: potrete verificare che effettivamente lo spazio
non finirà mai in una posizione anti-estetica. Di solito, quindi, è utile mantenere settato questo flag.
EXTEND_LAST_ON_EACH_LINE serve a estendere l’ultima riga o colonna, fino a occupare tutto lo spazio disponibile. E’ un comportamento che talvolta è desiderabile, talvolta no: vi conviene sperimentare, e verificare se fa al caso
vostro caso per caso.
Infine occorre segnalare che la documentazione di wxWidget menziona anche un metodo
wxWrapSizer::IsSpaceItem, che si può sovrascrivere per dire al sizer di considerare anche altri elementi specifici come se fossero degli spazi, ai fini del calcolo invocato dal flag REMOVE_LEADING_SPACES. In
wxPython però questo metodo non è presente, e quindi dobbiamo accontentarci del comportamento di default, che,
come abbiamo visto, considera “spazi” in un sizer solo gli elementi del tipo “Spacer” (ovvero quelli inseriti con
wx.Sizer.AddSpacer o wx.Sizer.AddStretchSpacer).
6.5.7 Esempi di utilizzo dei sizer.
Nella documentazione trovate vari esempi di layout realizzati con i sizer. In particolare, potete cercare “sizer” nella
demo. Inoltre, il capitolo 11 del libro “wxPython in action” è dedicato ai sizer, per cui tutti gli esempi della documentazione tratti da quel capitolo sono interessanti. In particolare, realworld.py mostra un tipico esempio di come i
sizer possono essere usati nel “mondo reale”.
6.5.8 wx.SizerItem, e modificare il layout a runtime.
Riprendiamo qui il discorso su wx.Sizer.Add che avevamo lasciato in sospeso nella precedente pagina sui sizer.
Finora infatti non abbiamo mai menzionato il fatto che wx.Sizer.Add, oltre ad aggiungere un widget (o uno spazio)
a un sizer, restituisce anche un valore di ritorno che occasionalmente può tornarci utile.
wx.Sizer.Add restituisce una istanza della classe wx.SizerItem che, come il nome suggerisce, incapsula il
concetto di “widget inserito in un sizer”. In genere non abbiamo bisogno di questo valore di ritorno, ma volendo
possiamo conservarlo assegnandolo a una variabile: qualcosa come:
s = wx.BoxSizer()
# in genere ci basta aggiungere i widget al sizer così:
s.Add(widget, 1, wx.EXPAND|wx.ALL, 5)
# ma talvolta è utile conservare il wx.SizerItem corrispondente:
self.sizer_item = s.Add(widget, 1, wx.EXPAND|wx.ALL, 5)
# etc. etc.
Un oggetto wx.SizerItem ha alcuni metodi che possono tornarci utili per manipolare il layout dopo che è stato
disegnato la prima volta: per esempio,
• SetDimension assegna posizione e dimensione del widget all’interno del sizer;
• SetBorder stabilisce il bordo da attribuire al widget;
• SetFlag attribuisce i flag del widget;
• SetProportion ridefinisce la dimensione relativa del widget in confronto agli altri.
Ecco un esempio che mostra qualche variazione di layout “al volo”:
6.5. I sizer: seconda parte.
81
Appunti wxPython Documentation, Release 1
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b1 = wx.Button(p, -1, '1')
b2 = wx.Button(p, -1, '2')
b3 = wx.Button(p, -1, '3')
b1.Bind(wx.EVT_BUTTON, self.on_clic_b1)
b2.Bind(wx.EVT_BUTTON, self.on_clic_b2)
b3.Bind(wx.EVT_BUTTON, self.on_clic_b3)
s = wx.BoxSizer(wx.VERTICAL)
self.sizer_item_b1 = s.Add(b1, 1, wx.EXPAND|wx.ALL, 5)
self.sizer_item_b2 = s.Add(b2, 1, wx.EXPAND|wx.ALL, 5)
self.sizer_item_b3 = s.Add(b3, 1, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s)
self.p = p
def on_clic_b1(self, evt):
self.sizer_item_b1.SetProportion(0)
self.p.SendSizeEvent()
def on_clic_b2(self, evt):
self.sizer_item_b2.SetFlag(0)
self.p.SendSizeEvent()
def on_clic_b3(self, evt):
self.sizer_item_b3.SetFlag(wx.EXPAND|wx.LEFT|wx.RIGHT)
self.sizer_item_b3.SetBorder(25)
self.p.SendSizeEvent()
Si noti l’uso di wx.Window.SendSizeEvent per invocare il ridisegno del layout anche quando la finestra non ha
effettivamente cambiato dimensioni.
In pratica, tuttavia, queste tecniche di manipolazione del layout non sono consigliabili. E’ buona norma non modificare
l’interfaccia in modo vistoso dopo averla mostrata la prima volta: l’utente ha bisogno di ambientarsi e ricordare la
posizione dei widget, farsi una mappa mentale degli aspetti più importanti della vostra gui e dei pattern di utilizzo per
lui più consueti. Se voi alterate profondamente il layout a runtime, aggiungendo e togliendo, spostando e modificando
i widget, l’utente ne ricaverà solo un senso di disordine e irritazione. Spesso i programmatori inesperti pensano che
sia utile, per esempio, far sparire i widget non necessari (o inattivi) in quel momento, e farli riapparire solo quando
servono: ma in realtà ci sono sempre modi migliori per organizzare il layout, e wxPython non è certo carente di
soluzioni intelligenti (per esempio, si possono organizzare i widget in “pagine” usando un wx.Notebook o altri
analoghi contenitori a schede).
6.6 Gli eventi: concetti avanzati.
In questa pagina affrontiamo alcuni aspetti degli eventi che non entrano quasi mai in gioco in un normale programma
wxPython. Tuttavia è opportuno conoscerli, e in certi casi conviene utilizzarli.
La lettura di questa pagina è consigliata solo se avete seguito la prima parte del discorso, che presenta la terminologia
e i concetti di base che qui daremo per scontati.
Per cominciare, riprendiamo alcune cose già viste. Un evento può essere collegato a un handler e a un callback, usando
il metodo Bind di un binder (in pratica si usa il Bind dell’handler, ma questo invoca subito il Bind del binder per
fare il lavoro). Lo stile che si usa in genere è questo:
82
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
button = wx.Button(...)
button.Bind(wx.EVT_BUTTON, callback)
Questo stile trae vantaggio dal fatto che tutti gli elementi visibili in wxPython (i vari widget) derivano anche da
wx.EvtHandler, e quindi hanno la capacità di fare da handler per i loro stessi eventi.
Abbiamo anche visto che si può scegliere anche un altro handler, e scrivere per esempio:
button = wx.Button(self, ...)
# dove self e' un dialogo, per esempio
self.Bind(wx.EVT_BUTTON, callback, button)
E abbiamo detto che quasi sempre i due stili sono equivalenti... ma “quasi sempre” non è “sempre”, appunto.
Per capire di più, dobbiamo parlare della propagazione degli eventi.
6.6.1 La propagazione degli eventi.
Abbiamo detto che tutti gli eventi derivano da wx.Event, ma questa in realtà è una classe-base che fa poco. Una
distinzione più importante avviene al livello della sottoclasse wx.CommandEvent: questi eventi (e tutti quelli delle
classi derivate) si chiamano in gergo “command event”, e si propagano. Tutti gli altri non si propagano.
Che cosa vuol dire “propagarsi”? Vuol dire che il segnale costituito dall’oggetto-evento, una volta creato, raggiunge
in primo luogo il widget stesso che lo ha originato. Se l’evento è di quelli che non si propagano, allora il segnale si
ferma lì, e nessun altro elemento della gui verrà mai a conoscenza che l’evento si è generato. In questo caso, in pratica,
soltanto l’handler del widget stesso che ha prodotto l’evento ha la possibilità di intercettarlo: in altre parole, l’unico
modo per intercettare questi eventi è usare lo stile widget.Bind(wx.EVT_*, callback) (ricordiamo ancora
che ogni widget è anche un handler).
Se invece l’evento si propaga, il segnale procede a visitare l’immediato “parent” del widget. Poi arriva al parent del
parent, e continua ad arrampicarsi per tutta la catena dei parent, fino a quando non raggiunge la finestra top-level. Di
qui, salta direttamente alla wx.App.
A ogni fermata di questa esplorazione, l’algoritmo di gestione dell’evento cerca se c’è un handler in grado di gestirlo,
ovvero se quell’handler è stato preventivamente collegato, grazie a un binder, a quel tipo di evento e a un callback.
Se sì, il callback viene eseguito e l’algoritmo si ferma. Se no, prosegue nel suo viaggio. Come ultima possibilità,
quando arriva alla wx.App, chiede anche al suo handler (sì, anche la wx.App deriva da wx.EvtHandler, e quindi
è possibile collegarla agli eventi). Se voi non avete previsto nessun collegamento neppure lì, allora la wx.App ha un
gestore di default, che semplicemente non fa nulla. E questo, finalmente, fa morire l’evento.
Dopo aver capito a grandi linee il meccanismo di propagazione, vediamo come funziona nel dettaglio.
6.6.2 Come un evento viene processato.
Fase 0
nasce l’evento.
L’evento si genera da un widget. Dunque l’handler del widget stesso se ne occupa, passando l’evento al suo algoritmo
interno wx.EvtHandler.ProcessEvent(). E si va alla fase 1.
FASE 1
l’handler è abilitato?
6.6. Gli eventi: concetti avanzati.
83
Appunti wxPython Documentation, Release 1
Qui la decisione che wxPython deve prendere è se questo handler è abilitato a processare eventi, oppure no.
In genere la risposta è sì. Tuttavia, è possibile chiamare manualmente SetEvtHandlerEnabled(False) su un
handler (su un widget, cioè) per impedirgli di processare eventi. Per ripristinare il comportamento normale, basta
chiamare widget.SetEvtHandlerEnabled(True).
Se la risposta è no, si passa direttamente alla fase 5. Se la risposta è sì, passare alla fase successiva.
FASE 2
l’handler può gestire l’evento?
Ovvero: avete collegato questo handler, per questo evento, a un callback, grazie a un binder?
Se la risposta è sì, l’algoritmo ProcessEvent esegue il vostro callback (bingo!). Quindi passa alla fase 3.
Se la risposta è no, ovviamente non c’è nessun callback da eseguire, e si procede con la fase 3.
FASE 3
l’evento dovrebbe propagarsi?
Se l’evento non è un “command event”, non ha la potenzialità di propagarsi.
Se invece l’evento è un “command event”, ha la potenzialità di propagarsi, ma non è detto che lo farà.
Prima di tutto, ci sono due dettagli che bisogna considerare:
• gli eventi non si propagano oltre i dialoghi. Abbiamo già accennato a questa cosa, parlando dell’extra-style
wx.WS_BLOCK_EVENTS che nei dialoghi è settato per default. Questo significa che un evento può passare da
un frame al parent (eventuale) del frame, ma non dal dialogo al parent del dialogo. Naturalmente è possibile
settare wx.WS_BLOCK_EVENTS anche su un frame, se si desidera.
• anche se un evento è “command”, potrebbe non propagarsi all’infinito. Infatti gli eventi hanno un “livello di propagazione” interno. L’unico modo per conoscerlo è chiamare event.StopPropagation(),
che interrompe la propagazione e restituisce il livello di propagazione. Non dimenticatevi di chimare
event.ResumePropagation() subito dopo. Se per esempio il livello è 1, l’evento non si propagherà
oltre il diretto genitore. Se il livello è 2, andrà fino al parent del parent, ma poi si fermerà. In pratica però
i normali “command event” hanno il livello di propagazione settato a sys.maxint, e quindi si propagano
effettivamente all’infinito. Ma potreste voler scrivere un classe-evento personalizzata che si propaga in modo
più limitato, se necessario.
Tenendo anche conto di queste cose, se l’evento non è ancora stato processato in precedenza, si propaga senz’altro.
Se invece l’evento è già stato processato, e quindi un callback è stato appena eseguito, di regola ProcessEvent termina e restituisce True, a meno che il callback non abbia chiamato Skip() sull’evento. Chiamare event.Skip()
è un segnale che si richiede di continuare il processamento degli eventi. Su Skip() parleremo in modo più approfondito in seguito.
Potete sapere se l’algoritmo ha deciso che l’evento può propagarsi chiamando event.ShouldPropagate.
Dopo che wxPython determina se l’evento dovrebbe propagarsi, con questa informazione si passa alla fase 4. Più
precisamente, se l’evento è un “command event”, fase 4A. Altrimenti, fase 4B.
FASE 4A
passare all’handler successivo (versione “command event”).
84
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
A questo punto l’algoritmo cerca l’handler successivo a cui bisogna rivolgersi. La ricerca avviene secondo le precedenze che elenchiamo qui sotto. In breve, ogni volta che viene trovato un handler, si torna alla fase 1, e si esegue il
ciclo 1-2-3. Se la fase 3 determina che occorre una ulteriore propagazione, si torna a questa fase 4A, e si riprende la
ricerca dal punto in cui era arrivata.
Ecco le regole per la ricerca degli handler:
• 4A.1: handler addizionali
Qui in genere non succede mai nulla. Comunque, un widget potrebbe avere uno stack di numerosi handler.
Ovviamente è una tecnica piuttosto avanzata, ma potreste scrivere un handler personalizzato (una sottoclasse di
wx.EvtHandler) e aggiungerlo allo stack chiamando widget.PushEventHandler(my_handler).
L’handler di cui abbiamo parlato finora nelle fasi 1 e 2, è in realtà il primo handler dello stack (e anche il solo, se non
ne avete aggiunti altri). Ma, se ci sono altri handler in coda, per ciascuno di essi si passa attraverso le fasi 1, 2, e 3.
Come sopra, se la fase 3, a un certo passaggio, determina che l’evento non può propagarsi ulteriormente, l’algoritmo
si ferma. Altrimenti, tutti gli handler addizionali vengono interrogati in seguenza. Quando sono esauriti, si procede
con la fase 4A.2.
• 4A.2: handler nelle sovraclassi
Prima si cerca nelle varie sovra-classi. Per ciascuna di esse, si interroga l’handler che si trova, passando per la fase 1
(è abilitato?), la fase 2 (può gestire l’evento?) e la fase 3 (potrebbe ancora propagarsi?). Se, a un certo passaggio, la
fase 3 determina che l’evento non si può propagare ulteriormente (tipicamente perché un callback è stato trovato ed
eseguito nella fase 2, ma non ha chiamato Skip) allora l’algoritmo si ferma, ProcessEvent termina e restituisce
True. Se invece a ogni passaggio la fase 3 determina che l’evento può ancora propagarsi, si passa alla sovra-classe
successiva fino a esaurirle. Quindi si procede alla fase 4A.3 qui sotto.
• 4A.3: handler del parent
Soltanto se, nell’ultima fase 3 attraversata, abbiamo stabilito che l’evento può ancora propagarsi, finalmente si passa
al parent del widget attuale. Si chiede prima al suo handler, e poi si continua a cercare nelle sovra-classi e tra gli
handler addizionali, percorrendo sempre le fasi 1-2-3 finché la fase 3 non determina che l’evento non può ulteriormente
propagarsi.
Quando alla fine l’handler trovato
• è un dialogo, oppure un frame con wx.WS_BLOCK_EVENTS settato, oppure
• è una finestra top-level,
si esegue il ciclo 1-2-3 un’ultima volta (compresa la fase 4 per la ricerca nelle sovra-classi, naturalmente), e poi, se
alla fase 3 si decide che l’evento dovrebbe ancora propagarsi, allora si passa alla fase 5.
FASE 4B
passare all’handler successivo (versione “non command”).
Questa versione della fase 4 è analoga a quella dei “command event”. Soltanto, l’evento non può propagarsi al suo
parent. Tuttavia, la ricerca nelle sovra-classi e negli handler addizionali avviene ancora. Quindi, ecco quello che
succede:
• 4B.1: handler nelle sovra-classi.
Per ciascuna sovra-classe si interroga l’handler e si passa per il ciclo 1-2-3. Se, a un certo passaggio, nella fase 3
troviamo che un callback è stato appena eseguito nella fase 2, ma non ha chiamato Skip, allora l’algoritmo si ferma.
Se invece il callback ha chiamato Skip, si passa alla sovra-classe successiva fino a esaurirle. Quindi si procede alla
fase 4B.2.
• 4B.2: handler addizionali
6.6. Gli eventi: concetti avanzati.
85
Appunti wxPython Documentation, Release 1
Se ci sono handler addizionali, per ciascuno di essi si passa per il ciclo 1-2-3. Come sopra, se la fase 3, a un certo
passaggio, trova che l’evento è stato processato ma il callback non ha chiamato Skip, l’algoritmo si ferma. Altrimenti,
tutti gli handler addizionali vengono interrogati in seguenza.
E poi non si procede oltre, perché l’evento non può comunque propagarsi al parent del widget.
Se l’evento non è stato ancora gestito, oppure se è stato gestito ma il callback ha chiamato Skip, si procede ancora
con la fase 5.
FASE 5
la ‘‘wx.App‘‘ come ultimo handler.
Se si arriva fino a questo punto e l’algoritmo non è ancora terminato (perché l’evento non è ancora stato processato,
oppure perché finora tutti i callback incontrati hanno sempre chiamato Skip), allora l’algoritmo chiede all’handler
della wx.App se è in grado di occuparsene.
In effetti è possibile collegare con un binder un evento a un callback anche nella wx.App, proprio come fareste di
solito.
Se perdete anche questa ultima occasione, il ProcessEvent dell’handler della wx.App ha comunque un comportamento predefinito, che semplicemente non fa nulla. In questo modo, l’algoritmo termina comunque e l’evento
muore.
6.6.3 Riassunto dei passaggi importanti.
Come vedete, il ciclo completo è piuttosto complicato, ma nel 99% dei casi si riduce a pochi semplici passaggi:
• se non è un “command event”, allora:
– o viene processato dall’handler del widget stesso che lo ha generato,
– oppure da qualche sua sovra-classe,
– oppure dall’handler della wx.App.
• se invece l’evento è un “command event”, allora:
– o viene processato dal widget che lo ha generato,
– oppure da qualche sua sovra-classe,
– oppure si cerca un collegamento in tutti i parent successivi,
– fino ad arrivare a un dialogo o a una finestra top-level,
– e quindi si conclude cercando un collegamento nell’handler della wx.App.
– Se in una di queste stazioni si trova un callback, la propagazione si ferma, a meno che il callback non
chiami Skip() sull’evento.
6.6.4 Come funziona Skip().
event.Skip() può essere chiamato sull’evento, dall’interno di un callback che lo gestisce. Non importa se viene
chiamato all’inizio o alla fine del codice del vostro callback: imposta comunque un flag interno all’evento, che segnala
all’algoritmo di gestione che dovrebbe continuare il processamento degli eventi in coda. Questo significa:
• continuare a propagare l’evento corrente (se può farlo), come se non fosse stato trovato nessun callback.
• processare gli eventi successivi che sono in coda.
86
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Entrambe queste cose sono importanti, e per quanto riguarda la seconda, bisogna ricordare che spesso una singola
azione dell’utente scatena più eventi in successione.
Per esempio, quando fate clic su un pulsante, producete un wx.EVT_LEFT_DOWN, un wx.EVT_LEFT_UP e un
wx.EVT_BUTTON in sequenza. Se voi intercettate il primo, e nel callback non chiamate Skip(), gli altri due non
verranno mai processati.
Voi direte: questo è grave solo se voglio intercettare anche un evento successivo; altrimenti, poco male. Ma non è
del tutto esatto, perché bloccando il processamento degli eventi potreste comunque impedire a wxPython di invocare
il comportamento di default di un widget. Per esempio, quando fate clic sul pulsante, wxPython deve comunque
preoccuparsi di cambiare per un istante il suo aspetto per farlo sembrare “abbassato”, e poi “rialzarlo”.
Il comportamento di default, quando occorre, si aggiunge a quello che voi eventualmente prescrivete nei vostri callback. Più precisamente, arriva dopo il vostro, perché è scritto nella sovra-classe madre da cui avete derivato il vostro
widget. A questo proposito, c’è un dettaglio (diabolico!) incluso nel nostro schema, che occorre comprendere bene:
l’algoritmo di processamento cerca gli handler nelle sovra-classi (fase 4.1) dopo aver determinato se l’evento deve
propagarsi (fase 3). Quindi, se intercettate un evento e non chiamate Skip() nel relativo callback, potreste impedire
la ricerca di eventuali meccanismi di gestione di default che si trovano nella classe-madre del vostro widget.
Torniamo all’esempio del clic sul pulsante, che genera wx.EVT_LEFT_DOWN, wx.EVT_LEFT_UP e
wx.EVT_BUTTON in sequenza. Se voi intercettate il primo e non chiamate Skip(), non solo impedite l’esecuzione
di ulteriori callback che potreste aver scritto in corrispondenza del secondo e del terzo; ma inoltre impedirete a wxPython di gestire correttamente lo stato del pulsante.
Per fortuna, i comportamenti di default di un pulsante sono codificati in risposta a wx.EVT_LEFT_DOWN e
wx.EVT_LEFT_UP, ossia gli eventi che in genere non vi interessano. L’evento che intercettate di solito è
wx.EVT_BUTTON, che parte solo dopo che tutta la gestione di default del pulsante è stata già completata (in particolare, wx.EVT_BUTTON è lanciato da wx.EVT_LEFT_UP alla fine del suo procedimento interno). Quindi potete
tranquillamente dimenticarvi di chiamare Skip() nel callback di un wx.EVT_BUTTON, e il vostro pulsante funzionerà come vi aspettate.
In genere, tutti i widget fanno partire in coda gli eventi “di più alto livello”, che sono quelli che in genere volete
intercettare. Così potete risparmiarvi di chiamare Skip() nel callback, perché wxPython ormai ha già fatto la sua
parte.
Una lezione che si può trarre da tutto questo è: non dovete intercettare wx.EVT_LEFT_UP su un pulsante, se potete
fare la stessa cosa intercettando wx.EVT_BUTTON.
Una seconda lezione è questa: se siete in dubbio, chiamate Skip().
6.6.5 Un esempio per Skip().
Ecco qualche riga di codice che illustra l’esempio del “clic su un pulsante”:
1
2
3
4
class SuperButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_BUTTON, self.on_clic)
5
6
7
8
def on_clic(self, event):
print 'clic su SuperButton'
event.Skip()
9
10
11
12
class MyButton(SuperButton):
def __init__(self, *a, **k):
SuperButton.__init__(self, *a, **k)
13
14
class TestEventFrame(wx.Frame):
6.6. Gli eventi: concetti avanzati.
87
Appunti wxPython Documentation, Release 1
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
button = MyButton(p, -1, 'clic!', pos=(50, 50))
button.Bind(wx.EVT_LEFT_DOWN, self.on_down)
button.Bind(wx.EVT_LEFT_UP, self.on_up)
button.Bind(wx.EVT_BUTTON, self.on_clic)
button.SetDefault()
15
16
17
18
19
20
21
22
23
def on_down(self, event):
print 'mouse giu'
event.Skip()
24
25
26
27
def on_up(self, event):
print 'mouse su'
event.Skip()
28
29
30
31
def on_clic(self, event):
print 'clic'
event.Skip()
32
33
34
35
36
37
38
39
if __name__ == '__main__':
app = wx.App(False)
TestEventFrame(None).Show()
app.MainLoop()
Come si vede, abbiamo creato una gerarchia di sotto-classi di wx.Button per testare anche la ricerca degli handler
nelle sovra-classi.
Stiamo intercettando contemporaneamente wx.EVT_LEFT_DOWN, wx.EVT_LEFT_UP e wx.EVT_BUTTON. Nella
configurazione di base, tutti i callback chiamano Skip(). Se provate a eseguire adesso lo script, trovate che l’ordine in
cui i callback sono chiamati rispecchia la normale ricerca degli handler: prima on_down, poi on_up, poi on_clic
e infine SuperButton.on_clic.
Avvertenza: abbiamo un piccolo problema terminologico. Da questo momento, quando dico “pulsante” intendo “pulsante sinistro del mouse”. Quando dico “bottone” mi riferisco invece al wx.Button disegnato sullo schermo.
Osserviamo più da vicino, con l’avvertenza che quanto segue potrebbe differire tra le varie piattaforme. Se abbassate il
pulsante del mouse, ma poi allontanate il puntatore dall’area del bottone prima di rilasciarlo, allora verranno catturati
il wx.EVT_LEFT_DOWN e anche il wx.EVT_LEFT_UP, tuttavia il wx.EVT_BUTTON non verrà emesso. wxPython
sa che il secondo evento “appartiene” ugualmente al bottone, anche se il puntatore si è spostato nel frattempo: lo sa
perché ha avuto modo di completare correttamente il processo interno del primo evento, e adesso si aspetta che il
prossimo wx.EVT_LEFT_UP sia da attribuire al bottone. Tuttavia, quando il wx.EVT_LEFT_UP effettivamente si
verifica, wxPython non innesca anche il wx.EVT_BUTTON, se il puntatore non è rimasto nell’area del bottone.
Specularmente, se abbassate il pulsante del mouse fuori dall’area del bottone, e poi lo rilasciate dopo averlo
spostato all’interno dell’area, vedrete comparire soltanto un wx.EVT_LEFT_UP “orfano” (e ovviamente nessun
wx.EVT_BUTTON).
Adesso, per prima cosa provate a eliminare lo Skip di on_clic (riga 34).
Il risultato è che
SuperButton.on_clic non verrà più eseguito. D’altra parte però il pulsante funzionerà correttamente, perché
non c’è nessuna particolare routine di default che wx.Button deve svolgere in risposta a un wx.EVT_BUTTON.
Invece, provate a togliere lo Skip di on_down (riga 26): il vostro callback verrà ovviamente ancora eseguito, ma ciò
che succede dopo comincia a diventare... strano. La ricerca di handler nelle sovra-classi si arresta, e pertanto wxPython
non è in grado di gestire il corretto funzionamento del bottone: notate infatti che non assume il caratteristico aspetto
“abbassato”.
Il wx.EVT_LEFT_UP (contrariamente a quando forse vi aspettate) viene ancora emesso quando sollevate il pulsante:
88
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
in realtà l’oggetto-evento del mouse (l’istanza della classe wx.MouseEvent) è creato da wxPython allo startup
dell’applicazione, e resta sempre in circolazione: assume di volta in volta differenti “event type” (e quindi può essere
collegato da differenti binder) a seconda dell’azione specifica del mouse in quel momento. Quindi non c’è niente di
strano che un wx.EVT_LEFT_UP venga ugualmente ricosciuto e catturato, se rilasciate il pulsante del mouse finché
il puntatore è ancora nell’area del bottone.
Notate però che, se prima di risollevare il mouse allontanate il puntatore dall’area del bottone, allora il
wx.EVT_LEFT_UP questa volta non verrà catturato: questo è spia di un cambiamento importante. A causa della
gestione non completa del precedente wx.EVT_LEFT_DOWN, adesso wxPython non è più in grado di capire che il
wx.EVT_LEFT_UP deve essere attribuito comunque al bottone. Inutile dire che, in queste circostanze, non c’è modo
per wx.EVT_LEFT_UP di chiudere in bellezza innescando il wx.EVT_BUTTON, anche rimanete con il puntatore
all’interno dell’area del bottone. Quando non avete eseguito il gestore di default del wx.EVT_LEFT_DOWN, avete
spezzato irrimediabilmente il meccanismo: una sequenza di “giù” e poi “su”, sia pure nell’area del bottone, non basta
più a far partire il wx.EVT_BUTTON.
Se infine togliete lo Skip del callback on_up (riga 30), le cose diventano se possibile ancora più strane. Chiaramente
i callback on_down e on_up vengono eseguiti, ma da quel momento tutto smette di funzionare correttamente.
wxPython non ha modo di completare la gestione di wx.EVT_LEFT_UP, e quindi nessun wx.EVT_BUTTON viene
innescato. Ma ciò che è peggio, il bottone resta costantemente “premuto” rifiutando di resettarsi (potete passarci
sopra il puntatore del mouse per convincervi del problema). Inoltre, adesso wxPython attribuisce ogni successivo clic
del mouse al bottone: fate clic al di fuori del bottone, e vedrete che i vostri callback continuano a essere chiamati
lo stesso. Ovviamente, siccome tutti i clic sono attribuiti al bottone, non potete nemmeno più chiudere la finestra
dell’applicazione!
Impressionante, vero? Ovviamente questa non è una conseguenza generale che avviene ogni volta che dimenticate
di chiamare Skip al momento giusto. In questo caso, molto dipende dal tipo di gestione interna che avviene nei
wx.Button.
Tuttavia, la regola generale resta quella: se siete in dubbio, chiamate Skip.
6.6.6 Bind e la propagazione degli eventi.
Finalmente siamo in grado di rispondere alla domanda da cui eravamo partiti:
widget.Bind(...) e self.Bind(..., button)?
che differenza c’è tra
Per la precisione, ci sono tre modi differenti di usare Bind. Per esempio:
1
# 'button' e' un pulsante, 'self' e' il panel/frame/dialog che lo contiene
2
3
4
5
button.Bind(wx.EVT_BUTTON, self.callback)
self.Bind(wx.EVT_BUTTON, self.callback, button)
self.Bind(wx.EVT_BUTTON, self.callback)
# (1)
# (2)
# (3)
Lo stile (1) collega l’evento generato da button direttamente all’handler button. Questo significa che l’handler
button sarà il primo a ricevere l’evento, e se ne occuperà eseguendo self.callback. Se al suo interno
self.callback non chiama Skip, l’evento non si propagherà oltre. Nove volte su dieci, questo è lo stile di
collegamento che vi serve davvero.
Lo stile (2) collega l’evento generato da button all’handler di self (che nel nostro esempio potrebbe essere un
panel, o un altro contenitore). Nove volte su dieci, questo stile ha lo stesso effetto del precedente. Tuttavia è importante
capire che in questo caso l’evento viene catturato solo dopo che si è propagato qualche volta. La catena dei parent da
button a self potrebbe anche essere lunga. Se nessun altro handler interviene a gestire l’evento prima di self,
allora effettivamente non c’è differenza tra lo stile (1) e lo stile (2). Lo stile (2) torna utile solo nei casi un cui è utile
inserire diversi handler lungo la catena di propagazione.
Ecco un esempio pratico:
6.6. Gli eventi: concetti avanzati.
89
Appunti wxPython Documentation, Release 1
1
from itertools import cycle
2
3
4
5
6
7
8
class ColoredButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_BUTTON, self.change_color)
self.color = cycle(('green', 'yellow', 'red'))
self.SetBackgroundColour(self.color.next())
9
def change_color(self, event):
self.SetBackgroundColour(self.color.next())
event.Skip()
10
11
12
13
14
15
16
17
18
19
20
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = wx.Panel(self)
button = ColoredButton(panel, -1, 'clic!', pos=(50, 50))
panel.Bind(wx.EVT_BUTTON, self.on_clic, button)
21
def on_clic(self, event):
print 'qui facciamo il vero lavoro...'
22
23
24
25
26
27
28
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
Abbiamo definito un pulsante personalizzato ColoredButton che cambia colore ogni volta che facciamo clic.
Questo comportamento è codificato dal callback change_color, che è collegato direttamente all’handler del pulsante stesso (riga 6: utilizziamo il primo stile). Notate che change_color chiama Skip, permettendo all’evento di
propagarsi per essere intercettato anche in seguito.
Infatti, quando vogliamo usare il nostro pulsante nel mondo reale, è necessario preservare il suo comportamento di
default (cambiare colore), e aggiungere il lavoro vero e proprio che vogliamo fargli fare nella nostra applicazione. Il
modo è semplice: basta aspettare che l’evento arrivi al contenitore superiore (in questo caso panel), e intercettarlo
di nuovo (riga 20: qui usiamo il secondo stile!).
Lo stile (3), infine, è incluso solo per maggiore chiarezza: infatti è identico allo stile (1) dal punto di vista della
semantica. In entrambi i casi, colleghiamo un handler a un evento. Significa che l’handler gestirà quell’evento, ogni
volta che passerà da lui, non importa da dove provenga. La differenza, chiaramente, è nel contesto. Nel caso dello stile
(1), l’handler è un wx.Button o un altro widget specifico. E’ altamente improbabile che un wx.Button sia parent
di qualche altro wx.Button, quindi gli unici wx.EVT_BUTTON che gli capiteranno mai sotto mano saranno quelli
che emette lui stesso. D’altra parte, nel caso dello stile (3), l’handler è un contenitore che potrebbe avere al suo interno
numerosi wx.Button. L’handler gestirà i wx.EVT_BUTTON di tutti i pulsanti che sono (anche indirettamente) suoi
figli.
Naturalmente, all’interno del callback potete chiamare event.GetEventObject() e risalire così al pulsante
specifico che ha emesso l’evento. Ecco un esempio:
1
2
3
4
5
6
7
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = wx.Panel(self)
button_A = wx.Button(panel, -1, 'A', pos=(50, 50))
button_B = wx.Button(panel, -1, 'B', pos=(50, 100))
panel.Bind(wx.EVT_BUTTON, self.on_clic)
90
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
8
9
10
def on_clic(self, event):
print 'premuto', event.GetEventObject().GetLabel()
11
12
13
14
15
if __name__ == '__main__':
app = wx.App(False)
TopFrame(None).Show()
app.MainLoop()
Ricapitolando: lo stile (1) e lo stile (3) dicono entrambi all’handler di gestire ogni evento di quel tipo, non importa da
dove è partito. Lo stile (2) dice all’handler di gestire solo gli eventi di quel tipo che sono partiti da un posto specifico.
Lo stile (1) e lo stile (3) sono in effetti identici nella semantica: lo stile (3) è semplicemente lo stile (1) applicato a un
contenitore.
Nella pratica, lo stile (1) è quello che va bene nella maggior parte dei casi. Lo stile (2) può aver senso se avete in mente
di intercettare più di una volta lo stesso evento. Lo stile (3) è usato raramente, perché ha il problema di intercettare più
di quanto in genere si vorrebbe.
6.6.7 Bind per gli eventi “non command”.
C’è un’altra ragione importante per cui lo stile (1) è quello più utilizzato.
Di fatto, è l’unico stile di collegamento che potete usare per gli eventi non “command”. Infatti, siccome questi non si
propagano, la vostra unica chance di intercettarli è rivolgengovi all’handler dello stesso widget che li ha generati.
Di conseguenza, lo stile (1) va bene per tutti gli eventi, “command” e no.
Ricordatevi comunque di chiamare Skip nel callback degli eventi “non command”, per permettere a wxPython di
ricercare il comportamento predefinito nelle sovra-classi.
6.6.8 Un esempio finale per la propagazione degli eventi.
Questo esempio riassume quello che abbiamo detto fin qui sulla propagazione degli eventi. Fate girare questo codice,
e osservate in che ordine vengono chiamati i callback:
1
2
3
4
class MyButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_BUTTON, self.onclic)
5
6
7
8
def onclic(self, evt):
print 'clic dalla classe Mybutton'
evt.Skip()
9
10
11
12
13
14
15
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = wx.Panel(self)
button = MyButton(panel, -1, 'clic', pos=((50,50)))
16
17
18
19
button.Bind(wx.EVT_BUTTON, self.onclic_button)
panel.Bind(wx.EVT_BUTTON, self.onclic_panel, button)
self.Bind(wx.EVT_BUTTON, self.onclic_frame, button)
20
21
22
def onclic_button(self, evt):
print 'clic dal button'
6.6. Gli eventi: concetti avanzati.
91
Appunti wxPython Documentation, Release 1
evt.Skip()
23
24
def onclic_panel(self, evt):
print 'clic dal panel'
evt.Skip()
25
26
27
28
def onclic_frame(self, evt):
print 'clic dal frame'
evt.Skip()
29
30
31
32
33
34
35
36
class MyApp(wx.App):
def OnInit(self):
self.Bind(wx.EVT_BUTTON, self.onclic)
return True
37
def onclic(self, evt):
print 'clic dalla wx.App'
evt.Skip()
38
39
40
41
42
43
44
45
if __name__ == '__main__':
app = MyApp(False)
Test(None).Show()
app.MainLoop()
Questo esempio copre i casi comuni e alcuni scenari più avanzati. Tuttavia, non è ancora completo: quando verrà il
momento di parlare degli handler personalizzati, ne scriveremo una versione più ampia.
6.7 I menu: concetti avanzati.
Nelle due pagine precedenti che abbiamo dedicato ai menu, abbiamo coperto le basi necessarie per l’uso di tutti i
giorni. In questa pagina copriamo invece altre tecniche, non necessariamente più “difficili”, ma semplicemente meno
consuete.
6.7.1 Icone nelle voci di menu.
Per cominciare, un tocco gentile: come inserire una piacevole icona nelle vostre voci di menu:
menu = wx.Menu()
item = wx.MenuItem(menu, -1, 'foo')
item.SetBitmap(wx.Bitmap('image.jpg'))
menu.AppendItem(item)
Niente di particolarmente difficile, solo che purtroppo l’icona deve essere attribuita prima di agganciare la voce al
menu. Di conseguenza non possiamo usare il solito pattern (menu.Append(...)), ma dobbiamo creare la voce di
menu separatamente. Per questo usiamo il costruttore wx.MenuItem(); poi aggiungiamo l’icona (SetBitmap),
e infine agganciamo la voce al menu usando il metodo alternativo AppendItem al posto del normale Append (la
differenza è che il primo accetta MenuItem già pronti all’uso, mentre il secondo li crea al volo).
Per quanto riguarda wx.Bitmap, probabilmente dovremo dedicare una pagina separata all’uso delle immagini in
wxPython. Per il momento, vi basta sapere che si fa così.
Todo
una pagina sulle immagini
92
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
6.7.2 Menu contestuali e popup.
Ecco invece qualcosa di più concreto. Finora abbiamo visto soltanto menu (wx.Menu) agganciati a una barra dei
menu (wx.MenuBar). Tuttavia i menu possono comparire anche in posti del tutto inaspettati: una tecnica consueta è
quella del “menu contestuale” (che si apre facendo clic col tasto destro), ma si può far comparire un menu in qualsiasi
punto in risposta a un qualsiasi evento, in teoria (in teoria! In pratica, però, è meglio non sorprendere troppo il povero
utente).
Di per sé queste tecniche non sono difficilissime, ma per essere usate in modo efficiente richiedono un briciolo di
strategia. Procediamo per gradi.
Una piccola nota terminologica: nel seguito, confondiamo volentieri “menu contestuale” e “menu popup”. Di base,
sono la stessa cosa (ovvero, menu popup). Un menu contestuale, come vedremo, a rigore è un menu popup che si apre
in risposta a un particolare evento wx.EVT_CONTEXT_MENU. Ma è puramente una distinzione di termini.
Fare prima il binding degli eventi.
Occorre tenere conto del fatto che questo genere di menu, per loro natura, appaiono e scompaiono di continuo. In
pratica, la strategia migliore è considerarli menu “usa-e-getta” che create e distruggete ogni volta (potete nascondere
il menu invece di distruggerlo: ma questo funziona solo se vi serve ogni volta lo stesso menu, e in genere è poco
pratico).
Se non ci sono problemi a creare e distruggere anche mille volte un menu, conviene però gestire gli eventi una volta
sola, in modo da non dover rifare tutte le volte il collegamento tra le varie voci e i callback.
Per assurdo che possa sembrare, una buona strategia è fare il binding degli eventi ancora prima di creare le voci
dei menu, e quindi i menu stessi. La cosa è perfettamente possibile: basta “prenotare” degli id, e usare gli id per
fare il binding. Ebbene sì: i menu contestuali sono uno dei pochi posti di wxPython in cui non è sbagliato usare
esplicitamente gli id. Come abbiamo visto, un altro caso potrebbero essere i ranged event.
Per esempio, va benissimo qualcosa come:
self.Bind(wx.EVT_MENU, self.my_callback_1, id=100)
self.Bind(wx.EVT_MENU, self.my_callback_2, id=101)
self.Bind(wx.EVT_MENU, self.my_callback_3, id=102)
# etc.
def my_callback_1(self, evt):
def my_callback_2(self, evt):
def my_callback_3(self, evt):
# etc etc
# etc etc
# etc etc
Per prima cosa collegate dei semplici id a dei callback specifici. Poi, quando arriverà il momento di creare le voci del
menu contestuale, basterà fare attenzione ad assegnare manualmente gli id giusti. Alla fine, potrete distruggere senza
problemi il menu contestuale: gli id resteranno sempre lì, già pronti e collegati ai callback.
Le cose potrebbero ulteriormente complicarsi, perché spesso nei menu contestuali compaiono delle voci che già esistono anche nel menu principale (salva, copia, incolla...), e che avete già collegato ai callback giusti al momento di
creare il menu principale. Anche in questo caso, la soluzione è di attribuire esplicitamente l’id di queste voci, e usare
lo stesso id anche nel menu contestuale. Per esempio:
menu = wx.Menu() # questo e' il menu principale
menu.Append(100, 'foo') # questa servira' anche nei menu contestuali
# ...
self.Bind(wx.EVT_MENU, self.my_callback, id=100)
6.7. I menu: concetti avanzati.
93
Appunti wxPython Documentation, Release 1
Creare e mostrare il menu popup.
Un menu contestuale si crea come un qualsiasi altro menu, e può contenere sottomenu, voci spuntabili o blocchi
“radio”, icone, etc. Potrebbe anche contenere shortcut e acceleratori, anche se raramente possono servire in questi
casi.
Una volta creato, il menu viene mostrato con il metodo self.PopupMenu() (dove self è la finestra corrente). Il
menu appare nel punto in cui si trova il cursore del mouse: siccome di solito voi mostrate il menu in risposta a un clic
dell’utente, il menu apparirà lì dove l’utente se lo aspetta (a meno che il menu appaia in risposta a un evento che non
comporta nessun clic, come vedremo: in questo caso sarà meglio specificare dove va fatto apparire il menu).
Non appena il menu appare, resta in attesa del prossimo clic dell’utente, ed eventualmente innesca un evento in
corrispondenza della sua scelta (eventualmente: perché l’utente potrebbe anche cliccare fuori dal menu, e in questo
caso niente succede). Quando l’evento è stato processato, il flusso del programma torna nelle vostre mani: la prima
cosa che dovete fare è ovviamente distruggere il menu, in modo da non lasciarlo in giro (il comportamento di default
si limiterebbe a nasconderlo).
Questo esempio chiarisce tutto quello che abbiamo detto fin qui:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
# prima, preparo i binding...
self.Bind(wx.EVT_MENU, self.my_callback_1, id=100)
self.Bind(wx.EVT_MENU, self.my_callback_2, id=101)
self.Bind(wx.EVT_MENU, self.my_callback_3, id=102)
p = wx.Panel(self)
wx.StaticText(p, -1, 'fai clic qui', pos=((50, 50)))
p.Bind(wx.EVT_LEFT_UP, self.on_clic)
def on_clic(self, evt):
# l'utente ha fatto clic: dobbiamo creare il menu popup...
menu = wx.Menu()
menu.Append(100, 'scelta uno') # notare gli id...
menu.Append(101, 'scelta due')
menu.Append(102, 'scelta tre')
# ... e adesso lo mostriamo:
self.PopupMenu(menu)
# adesso il menu popup resta a disposizione:
# quando l'utente ha finito di usarlo, il flusso del programma
# torna qui: subito distruggiamo il popup
menu.Destroy()
def my_callback_1(self, evt): print 'hai scelto la uno'
def my_callback_2(self, evt): print 'hai scelto la due'
def my_callback_3(self, evt): print 'hai scelto la tre'
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
In questo caso il menu popup appare in risposta a un clic in un punto qualsiasi del panel (abbiamo dovuto usare
wx.EVT_LEFT_UP perché naturalmente un panel non dispone di eventi specifici come wx.EVT_BUTTON).
Dopo che l’utente ha finito di usare il menu, lo distruggiamo e siamo pronti a ricrearlo di nuovo alla prossima occasione. Come si vede, il meccanismo di base è piuttosto semplice.
94
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Ecco invece l’esempio di prima modificato per mostrare come la stessa voce può apparire in un menu “normale” e in
un menu popup:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
# binding per i menu popup
self.Bind(wx.EVT_MENU, self.my_callback_1, id=100)
self.Bind(wx.EVT_MENU, self.my_callback_2, id=101)
menu = wx.Menu() # menu principale
menu.Append(-1, 'bla bla')
menu.Append(102, 'anche popup') # questo va anche nel popup
self.Bind(wx.EVT_MENU, self.my_callback_3, id=102)
menubar = wx.MenuBar()
menubar.Append(menu, 'Menu')
self.SetMenuBar(menubar)
p = wx.Panel(self)
wx.StaticText(p, -1, 'fai clic qui', pos=((50, 50)))
p.Bind(wx.EVT_LEFT_UP, self.on_clic)
def on_clic(self, evt):
menu = wx.Menu()
menu.Append(100, 'scelta uno')
menu.Append(101, 'scelta due')
menu.Append(102, 'anche popup') # c'e' anche nel menu principale
self.PopupMenu(menu)
menu.Destroy()
def my_callback_1(self, evt): print 'hai scelto la uno'
def my_callback_2(self, evt): print 'hai scelto la due'
def my_callback_3(self, evt): print 'la voce che sta in entrambi i menu'
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Un “autentico” menu contestuale.
Un menu contestuale, nell’uso comune del termine, è un menu popup che compare in risposta al clic col pulsante
destro del mouse. Nell’esempio di sopra, avremmo tranquillamente potuto scrivere:
p.Bind(wx.EVT_RIGHT_UP, self.on_clic)
e questo basta per creare un menu contestuale a tutti gli effetti... almeno a prima vista.
In realtà, tuttavia, se volete creare un menu contestuale “nel modo giusto” dovreste utilizzare l’evento
wx.EVT_CONTEXT_MENU per mostrare il vostro menu popup.
wxPython vi mette a disposizione questo evento proprio per questo specifico scopo. Che differenza c’è tra questo e un
banale wx.EVT_RIGHT_UP?
Prima di tutto, wx.EVT_CONTEXT_MENU si innesca anche quando l’utente chiede il menu contestuale con la tastiera
(c’è un tasto apposito, anche se non tutte le piattaforme lo usano!), e quindi garantisce l’esperienza nativa più completa.
6.7. I menu: concetti avanzati.
95
Appunti wxPython Documentation, Release 1
In secondo luogo, così wx.EVT_RIGHT_UP viene lasciato libero: potete usarlo separatamente per processare altre
cose, se vi serve. Attenti solo a non pasticciare con la catena degli eventi: quando l’utente rilascia il pulsante destro
del mouse, per prima cosa viene innescato il wx.EVT_RIGHT_UP. Se questo non viene processato, allora si innesca
il wx.EVT_CONTEXT_MENU. Quindi, se catturate l’evento del mouse, non dimenticatevi di chiamare Skip(), altrimenti l’evento per il menu contestuale non potrà mai partire.
Ancora una complicazione sulla posizione.
Se l’utente chiama il menu contestuale, lo vede apparire alla posizione corrente del puntatore. Questo comportamento
va benissimo (e non provate a modificarlo, se non volete farvi odiare), se il menu compare in seguito a un clic del
mouse.
Ma se il menu contestuale è chiamato con la tastiera, allora il comportamento di default non è più adatto, perché il
puntatore del mouse potrebbe trovarsi da tutt’altra parte in quel momento.
Potete scoprire la posizione corrente del puntatore in seguito a un wx.EVT_CONTEXT_MENU chiamando
GetPosition sull’evento nel callback. Se GetPosition vi restituisce wx.DefaultPosition invece di una
tupla, vuol dire che l’evento è stato chiamato dalla tastiera. In questo caso, prima di mostrare il menu contestuale, vi
conviene decidere una posizione adatta.
Nell’esempio che segue, vogliamo che una casella di testo abbia un menu contestuale: se l’utente lo richiama con il
mouse, tutto bene. Ma se lo chiama con la tastiera, allora dobbiamo fare un po’ di calcoli per assicurarci che compaia
in corrispondenza della posizione del cursore (e non dove si trova in quel momento il puntatore del mouse):
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.text = wx.TextCtrl(p, -1, 'fai clic qui '*10,
style=wx.TE_MULTILINE, pos=((50, 50)))
self.text.Bind(wx.EVT_CONTEXT_MENU, self.on_clic)
def on_clic(self, evt):
menu = wx.Menu()
menu.Append(-1, 'scelta uno') # i binding di queste voci
menu.Append(-1, 'scelta due') # sono omessi per brevita'
if evt.GetPosition() == wx.DefaultPosition:
ins_point = self.text.GetInsertionPoint()
correct_position = self.text.PositionToCoords(ins_point)
self.PopupMenu(menu, pos=correct_position)
else:
self.PopupMenu(menu)
menu.Destroy()
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
6.7.3 Manipolare dinamicamente i menu.
I menu sono strumenti complessi, e wxPython mette a disposizione molti metodi per maneggiarli. Potete
all’occorrenza far sparire voci di menu, aggiungerle, spostarle. E potete far sparire o cambiare allo stesso modo
interi menu.
96
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Tuttavia diciamo subito che queste non sono tecniche da adoperare a cuor leggero. Il menu, in tutte le applicazioni, è
una cosa sacra: il programmatore spende molte energie a progettarlo bene, l’utente investe molto tempo a orientarvisi;
in generale ci si aspetta che ogni possibile funzione del vostro programma corrisponda a una voce di menu, da qualche
parte.
Cambiare la struttura dei menu a runtime è probabilmente sempre una cattiva idea. Per esempio, se l’utente non
può accedere a certe voci, la cosa migliore è disabilitarle ma lasciarle visibili. Così l’utente può almeno capire che in
seguito a certe azioni (e magari con dei permessi aggiuntivi!) potrebbe accedere a quella sezione del vostro programma.
Se invece nascondete completamente la voce nel menu, l’utente potrebbe perdere un sacco di tempo a cercarla, se crede
che “da qualche parte ci deve pur essere”.
Proprio perché queste manovre non sono quasi mai una buona idea, non le descriveremo nel dettaglio. Potete senz’altro
riferirvi alla documentazione per scoprire qualcosa di più. In particolare, la demo (cercate “menu”) illustra qualche
esempio di voci di menu che appaiono, scompaiono e si spostano in questo modo.
Per rimuovere una voce di menu, chiamate menu.Remove(id), dove menu è il menu che contiene la voce,
e id è l’id della voce. Allo stesso modo potete rimuovere un intero menu dalla barra del menu chiamando
menubar.Remove(pos), dove pos è la posizione del menu nella barra. Notate che Remove non distrugge
l’oggetto MenuItem c++ sottostante. Questo è utile se volete re-inserire la voce di menu in un secondo tempo
(Remove restituisce un riferimento all’oggetto rimosso: basta conservarlo in una variabile, e poi riusarlo).
Per inserire una voce in mezzo ad altre esistenti, usate menu.InsertItem(pos, item), dove pos è la posizione
dell’elemento precedente a quello che volete inserire. Allo stesso modo potete inserire un menu nella barra dei menu
con menubar.Insert(pos, menu, title).
Se manipolate dinamicamente i menu, potrebbero servirvi anche le funzioni per cercare le varie voci o i vari menu.
Ce ne sono diversi: menu.FindItem(string) cerca una voce di menu per la sua etichetta; ma esistono anche
menu.FindItemById(id) e menu.FindItemByPos(position), con significato ovvio. Anziché rivolgersi
a un menu singolo, è possibile chiedere alla barra dei menu di cercare una determinata voce, ovunque sia: per questo
basta usare menubar.FindItemById(id).
Ma ci sono anche altre possibilità, che potete scoprire da soli guardando la documentazione.
Infine, anche sul fronte degli eventi, si può andare oltre il consueto wx.EVT_MENU. Esistono anche
wx.EVT_MENU_CLOSE (innescato dalla chiusura di un menu), wx.EVT_MENU_OPEN (quando si apre un menu),
wx.EVT_MENU_HIGHLIGHT (quando si passa col mouse sopra una voce di menu: il comportamento di default è
mostrare l“‘help text” del wx.MenuItem), e infine xw.EVT_MENU_HIGHLIGHT_ALL (come sopra, ma innescato
quando si passa col mouse sopra una voce qualsiasi: utile quando non vi interessa sapere quale voce in particolare sta
scorrendo l’utente).
6.7.4 Come “fattorizzare” la creazione dei menu.
La creazione dei menu comporta sempre la scrittura di codice prolisso e ripetitivo (un sacco di menu.Append e così
via). E’ naturale cercare modi di compattare un po’ queste procedure.
Prima di tutto, un avvertimento. Si tratta di tecniche non indispensabili, e anzi, a dirla tutta poco raccomandabili.
Compattare la creazione di un menu non è una vera “fattorizzazione”, perché dopo tutto il codice per creare i menu
serve una volta sola. Potete senz’altro dare una sforbiciata alle righe del vostro programma, se vi fa piacere. Ma non
ne guadagnate in ri-usabilità (che sarebbe il vero scopo della fattorizzazione), e probabilmente ci perdete in leggibilità.
Detto questo, chiaramente non è sbagliato trarre vantaggio dalla strumentazione standard di python. Per esempio,
dopo aver scritto dieci volte di seguito menu.Append, anche un programmatore python alle prime armi troverebbe
naturale usare un ciclo for:
for label in ('Topolino, 'Paperino', Qui', 'Quo', 'Qua'):
menu.Append(-1, label)
E siccome abbiamo visto che gli id possono essere importanti, meglio ancora:
6.7. I menu: concetti avanzati.
97
Appunti wxPython Documentation, Release 1
for n, lab in enumerate(('Topolino, 'Paperino', Qui', 'Quo', 'Qua')):
menu.Append(100+n, lab) # assegno id dal 100 in poi...
E perché fermarci qui? Se vogliamo offrire il servizio completo possiamo anche integrare il binding degli eventi:
labels = ('Qui', 'Quo', 'Qua')
events = (self.on_qui, self.on_quo, self.on_qua)
for lab, evt in zip(labels, events):
item = menu.Append(-1, lab)
self.Bind(wx.EVT_MENU, evt, item)
E potete andare avanti a personalizzare e rendere più smaliziato il vostro codice in mille modi diversi. Per esempio, vi
verrà in mente che invece della tupla di etichette potete anche scrivere:
labels = 'Topolino Paperino Qui Quo Qua'.split()
risparmiando qualche battuta e sentendovi in questo modo dei veri hacker. E così via.
Il gradino successivo è pensare di scrivere una funzione separata che riceva come argomento un po’ di etichette, e sputi
fuori un wx.Menu già bello pronto per essere attaccato alla sua wx.MenuBar. Se vi piace la terminologia dei design
pattern, questa si chiamerebbe una “funzione factory”. Ovviamente i menu possono contenere dei sotto-menu, e così
via: di conseguenza, progettare una factory di creazione dei menu può essere un piacevole esercizio per imparare le
funzioni ricorsive.
Ciascuno può divertirsi a scrivere la sua variante personalizzata. Ecco una traccia da cui partire:
def make_menu(items):
menu = wx.Menu()
for item in items:
if isinstance(item, list): # questo e' un sotto-menu
menu.AppendMenu(-1, item[0], make_menu(item[1:]))
elif item == '':
menu.AppendSeparator()
else:
menu.Append(-1, item)
return menu
Questa funzione prende come parametro una lista di elementi. Per ciascuno, se si tratta di una stringa aggiunge una
voce al menu; se si tratta invece di un’altra lista, aggiunge un sotto-menu con il nome del primo elemento, e procede
a chiamare se stessa ricorsivamente con gli altri elementi. Ecco un esempio di utilizzo:
menuitems = ['Voce 1', 'Voce 2', '', 'Voce 3',
['Sub-menu', 'Sub-voce 1', 'Sub-voce 2'], 'Voce 4']
menu = make_menu(menuitems)
menubar.Append(menu)
Naturalmente questa funzione, così com’è, non serve a molto: restituisce un menu pieno di voci di cui non conosciamo
l’id, e non abbiamo altri modi per collegare gli eventi.
Non è difficilissimo modificare questa prima versione per tener conto anche degli eventi: la funzione potrebbe ricevere
anche degli id, o addirittura già i nomi dei callback da collegare; oppure potrebbe assegnare gli id secondo un pattern
ricostruibile a posteriori.
Poi però la funzione sarebbe ancora molto limitata: non tiene conto di scorciatoie e acceleratori. Andrebbe arricchita.
E poi la funzione non tiene anche conto che alcune voci potrebbero essere inizialmente disabilitate. Bisognerebbe
modificarla.
E le voci spuntabili e i blocchi “radio”? Ehm, vanno calcolati anche loro.
98
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Dopo un po’, è facile perdere il filo. Tanto più cercate di generalizzare il problema, tanto più vi trovate a dover scrivere
un’intera libreria (con i suoi problemi di architettura, i bachi, i test...). E nel frattempo, il vostro programma iniziale
è sempre lì che aspetta di essere scritto. Inoltre, come abbiamo già detto, la fase di creazione dei menu avviene in
genere una volta sola nel vostro programma. Fino a che punto vale la pena di fattorizzare questo codice?
Ciascuno è libero di spingere questo esercizio fin dove crede.
Se volete un esempio più completo di “fattorizzazioni” eleganti ma di discutibile utilità pratica, potete guardare negli esempi tratti
dal libro “wxPython in Action” (che trovate nella documentazione: .../wxPython2.8 Docs and
Demos/wxPython/samples/wxPIA_book). Nella directory del Capitolo 5 trovate due file badExample.py
e goodExample.py che mostrano la stessa interfaccia prima e dopo la fattorizzazione (dei menu e non solo).
Alla fine della giornata, comunque, vi renderete conto che i veri problemi con i menu non vengono fuori al momento
della loro creazione, ma in seguito, durante il ciclo di vita della vostra applicazione. I menu crescono facilmente
fino a diventare sistemi complessi, e mantenere sempre aggiornato e coerente il loro stato è difficile. A seconda dei
casi, le varie voci vanno abilitate e disabilitate, spuntate, resettate... Spesso finite per costruire una rete intricata di
Enable(True) e Enable(False) nei vari callback, che diventa rapidamente ingestibile.
La vera sfida di “fattorizzazione” dei menu, quindi, è di trovare una forma pratica per separare la logica di business e
la logica di presentazione del vostro sistema di menu. Spesso la cosa migliore è costruire un “model” dei vostri menu
(una classe separata che incorpora una struttura ad albero, per esempio) capace di tener traccia dello status di ciascuna
voce, di calcolare gli aggiornamenti a seconda degli eventi che riceve, e di comunicare a sua volta questi cambiamenti
alla gui.
Anche in questo caso, tuttavia, non è il caso di perdere troppo tempo nello sforzo di generalizzare e prevedere tutte
le possibilità in anticipo. Partite da una soluzione rudimentale che si adatta alla vostra situazione, e poi apportate
miglioramenti man mano che vi servono.
Todo
una pagina su mvc.
6.8 Validatori: prima parte.
La validazione dei dati è un processo delicato. wxPython vi viene incontro con uno strumento apposito, la classe
wx.PyValidator (“validatore” per gli amici), che ha delle funzionalità interessanti, ma che non è facile comprendere a fondo.
I validatori in wxPython servono a due cose complementari:
• convalidano i dati;
• trasferiscono i dati dentro e fuori da un dialogo.
Potete usare i validatori anche solo per una di queste due funzioni, o per entrambe, a vostro gusto. Noi dedichiamo
questa sezione a spiegare la funzione di validazione, e un’altra pagina per il trasferimento dei dati.
Note: se avete un editor con l’autocompletion, avrete probabilmente scoperto che esiste anche la più “normale” classe
wx.Validator. Voi però dovete sempre usare la “versione python” wx.PyValidator. La ragione della presenza
di questi doppioni è complessa, e le dedicheremo una pagina separata. Affidatevi sempre a wx.PyValidator, è
tutto quello che vi serve sapere per usare bene i validatori.
Todo
6.8. Validatori: prima parte.
99
Appunti wxPython Documentation, Release 1
una pagina sui pycontrols
6.8.1 Come scrivere un validatore.
Occorre semplicemente sottoclassare wx.PyValidator. Ecco un esempio da manuale: questo è un validatore che
si può applicare a una casella di testo, e che garantisce che l’utente non la lasci vuota:
class NotEmptyValidator(wx.PyValidator):
def Clone(self): return NotEmptyValidator()
def TransferToWindow(self): return True
def TransferFromWindow(self): return True
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
return False
else:
return True
Le righe 2, 3, 4 sono (purtroppo) boilerplate necessario. Clone deve esserci necessariamente, e deve restituire una
istanza dello stesso validatore. I due Tranfer* servono solo se intendete usare il validatore per trasferire dati:
tuttavia dovete comunque sovrascriverli e restituire True, altrimenti wxPython emette un warning.
La parte interessante è Validate: sovrascrivete questo metodo per fare la vostra validazione. Validate deve
restituire True se la validazione ha successo, False altrimenti. Notate anche (riga 7) che all’interno del validatore,
potete risalire a un’istanza del widget che state validando, chiamando GetWindow. Forse pensate che il secondo,
necessario, parametro di Validate (ctl nel nostro esempio) contenga già un riferimento al widget validato, ma
questo potrebbe non essere vero in caso di validazioni “a cascata”, come il nostro esempio dimostrerà tra non molto.
Quindi il modo più sicuro è usare sempre GetWindow.
Il costruttore di un validatore, di norma, non prende argomenti. Tuttavia niente vi impedisce di passagli degli argomenti
extra, se necessario. Per esempio, questo validatore garantisce che il valore immesso non sia in una bad-list di parole
proibite:
1
2
3
4
class NotInBadListValidator(wx.PyValidator):
def __init__(self, badlist):
wx.PyValidator.__init__(self)
self._badlist=badlist
5
def Clone(self): return NotInBadListValidator(self._badlist)
def TransferToWindow(self): return True
def TransferFromWindow(self): return True
6
7
8
9
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val in self._badlist:
return False
else:
return True
10
11
12
13
14
15
16
Chiaramente, in questo caso volete sovrascrivere anche l’__init__ per gestire i paramentri aggiuntivi. Non dimenticatevi di riportare gli argomenti correttamente anche in Clone (riga 6)... anche il boiledplate richiede un minimo di
attenzione.
Una volta scritto, il validatore si applica al widget che intendete validare, al momento della sua creazione, passandolo
100
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
direttamente al costruttore (come parametro validator). Ovviamente potete usare lo stesso validatore (cioè: diverse
istanze dello stesso validatore) per validare più widget, se ne avete bisogno. Ecco un esempio:
1
2
3
4
5
class YourNamePanel(wx.Panel):
def __init__(self, *a, **k):
wx.Panel.__init__(self, *a, **k)
self.first_name = wx.TextCtrl(self, validator=NotEmptyValidator())
self.family_name = wx.TextCtrl(self, validator=NotEmptyValidator())
6
7
8
9
10
11
12
13
14
s = wx.FlexGridSizer(2, 2, 5, 5)
s.AddGrowableCol(1)
s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.first_name, 1, wx.EXPAND)
s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.family_name, 1, wx.EXPAND)
self.SetSizer(s)
s.Fit(self)
15
16
17
18
19
20
21
22
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.your_name = YourNamePanel(p)
validate = wx.Button(p, -1, 'valida')
validate.Bind(wx.EVT_BUTTON, self.on_validate)
23
24
25
26
27
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.your_name, 1, wx.EXPAND)
s.Add(validate, 0, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s)
28
29
30
31
32
def on_validate(self, evt):
ret = self.your_name.Validate()
if ret == False:
wx.MessageBox('Non valido')
33
34
35
36
37
if __name__ == '__main__':
app = wx.App(False)
MyTopFrame(None, size=(200, 200)).Show()
app.MainLoop()
Come si vede (righe 4 e 5) due caselle di testo sono legate al nostro validatore. Se preferite, potete testare anche l’altro
validatore. Cambiate la riga 4 con qualcosa come:
ugly_names = ('Cunegonda', 'Dagoberto', 'Emerenzio', 'Pancrazia')
self.first_name = wx.TextCtrl(self, validator=NotInBadListValidator(ugly_names))
Come vedete, abbiamo incorporato le caselle in un panel, in parte perché è buona pratica raggruppare le funzionalità
della gui in piccoli “mattoni” coerenti, come abbiamo già detto altrove. Però in questo caso il panel ci torna utile
anche per dimostrare la validazione “a cascata”: quando chiamiamo Validate sul panel (riga 33), in effetti vengono
validati tutti i widget figli del panel (purché abbiano un validatore associato, naturalmente). Validate chiamato sul
panel restituisce True solo se tutti i figli passano la validazione, False altrimenti.
6.8.2 Quando fallisce una validazione a cascata.
Nel caso di validazione a cascata, abbiamo però un problema aggiuntivo: il processo di validazione si ferma non
appena uno dei test fallisce, ma il valore restituito False non ci dice nulla su quale widget esattamente non ha
6.8. Validatori: prima parte.
101
Appunti wxPython Documentation, Release 1
superato la validazione.
Quando è necessario dare all’utente anche questa informazione, occorre far sì che sia il validatore stesso a occuparsene,
invece del codice chiamante (perché il codice chiamante si ritrova in mano solo un valore di ritorno False). Per
esempio, possiamo riscrivere il nostro NotEmptyValidator in questo modo:
class NotEmptyValidator(wx.PyValidator):
#Clone, TransferToWindow, TransferFromWindow... bla bla
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
wx.MessageBox('Bisogna inserire del testo')
return False
else:
return True
Questo però non è ancora sufficiente: se più caselle di testo hanno lo stesso validatore, talvolta si vuole sapere esattamente quale non funziona (in questo caso forse è banale per l’utente capire dove non c’è testo, ma pensate al caso
generale). Possiamo fare in molti modi, per esempio modificando anche il colore del widget incriminato:
1
2
class NotEmptyValidator(wx.PyValidator):
#Clone, TransferToWindow, TransferFromWindow... bla bla
3
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
win.SetBackgroundColour('yellow')
win.Refresh() # necessario...
wx.MessageBox('Bisogna inserire del testo')
return False
else:
# assicuriamoci di impostare il colore normale
win.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
win.Refresh()
return True
4
5
6
7
8
9
10
11
12
13
14
15
16
Notate che, facendo così, ci fidiamo che il nostro widget abbia un’interfaccia SetBackgroundColour. Questo per
un wx.TextCtrl è senz’altro vero, ma di nuovo, dovete pensare al caso generale.
Un’altra soluzione potrebbe essere per esempio recuperare il name del widget:
class NotEmptyValidator(wx.PyValidator):
#Clone, TransferToWindow, TransferFromWindow... bla bla
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
msg = '%s: manca del testo!' % win.GetName()
wx.MessageBox(msg)
return False
else:
return True
Anche questo sistema si basa sulla fiducia: confida nel fatto che noi abbiamo assegnato un parametro name significativo a ogni widget a cui attribuiamo il validatore. Nel nostro esempio sarebbe:
102
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
self.first_name = wx.TextCtrl(self, name='Nome', validator=NotEmptyValidator())
self.family_name = wx.TextCtrl(self, name='Cognome', validator=NotEmptyValidator())
Il paramentro name del costruttore non è di solito molto utile. In Python si passano gli oggetti stessi come parametri,
e questo rende superfluo contrassegnare ciascun widget con un identificativo statico da passare in giro tra le varie
funzioni (abbiamo fatto lo stesso discorso a proposito degli id). Tuttavia, in casi del genere, può essere un modo
veloce di aggiungere un “nickname” piacevole al widget, da presentare all’utente in caso di necessità.
Il paramentro name (e quindi l’interfaccia GetName) è sicuramente presente ovunque. Quindi, quale dei due sistemi
sulla fiducia è il meno rischioso? Affidarsi a un’interfaccia che potrebbe non esistere (SetBackgroundColour) o
a una che sicuramente esiste ma dipende da noi renderla significativa? La risposta sta al vostro stile, e alla dimensione
del vostro progetto. Nelle situazioni più semplici, non dovete preoccuparvi in nessun caso. Se però iniziate a scrivere
validatori “general purpose” e non sapete in anticipo a quali widget potrebbero essere assegnati, dovete muovervi con
più cautela.
6.8.3 La validazione ricorsiva.
La validazione a cascata si limita ai soli figli diretti, ma è possibile fare in modo che venga applicata ricorsivamente
anche ai figli dei figli, e così via. Per fare questo occorre settare lo stile wx.WX_EX_VALIDATE_RECURSIVELY.
Questo è un extra-style, e quindi va settato dopo la creazione, usando il metodo SetExtraStyle.
Facciamo degli esperimenti con il codice che abbiamo già scritto: per prima cosa, invece di validare il panel, proviamo
a validare direttamente il frame. Alla riga 33, sostituite così:
ret = self.Validate()
# era: ret = self.your_name.Validate()
Come previsto, la validazione non avviene. La catena dei parent in effetti è lunga: dopo il frame c’è il panel contenitore
(quello che istanziamo alla riga 22 e chiamiamo semplicemente p), quindi l’istanza di YourNamePanel, e finalmente
le caselle di testo che vogliamo validare.
Tuttavia, proviamo adesso ad aggiungere all’inizio dell’__init__ l’extra-style:
# nell'__init__ di MyTopFrame, subito all'inizio:
wx.Frame.__init__(self, *a, **k)
self.SetExtraStyle(wx.WS_EX_VALIDATE_RECURSIVELY)
Ecco che la validazione avviene di nuovo.
6.8.4 SetValidator: cambiare il validatore assegnato.
Anche dopo che il widget è stato creato, potete assegnarli un validatore, chiamando su di esso SetValidator
(attenzione: alcuni widget non dispongono di questo metodo). Se chiamate SetValidator su un widget che ha già
un validatore, ogni volta l’ultimo sostituisce il precedente.
6.8.5 La validazione automatica dei dialoghi.
Fin qui ci siamo limitati a chiamare Validate manualmente, per effettuare la validazione. L’unico automatismo
possibile è che, chiamandolo su un panel, si possono validare a cascata tutti i figli diretti (eventualmente anche i nipoti
etc., usando la validazione ricorsiva).
I dialoghi, tuttavia, hanno una marcia in più. E’ possible validare automaticamente un dialogo, quando è dotato di
un pulsante con id predefinito wx.ID_OK. In questo caso, quando l’utente fa clic sul pulsante wx.ID_OK, il dialogo
chiama automaticamente Validate su se stesso, prima di chiudersi. Se i widget contenuti nel dialogo hanno dei
validatori assegnati, entreranno in funzione.
6.8. Validatori: prima parte.
103
Appunti wxPython Documentation, Release 1
Abbiamo già parlato di questa feature dei dialoghi quando ci siamo occupati degli Id in wxPython: la sezione relativa
contiene degli esempi che vi invitiamo a rileggere.
Per quanto riguarda invece l’esempio che abbiamo seguito finora, ecco come diventa se lo trasportiamo in un dialogo
con validazione automatica:
1
2
3
4
class NotEmptyValidator(wx.PyValidator):
def Clone(self): return NotEmptyValidator()
def TransferToWindow(self): return True
def TransferFromWindow(self): return True
5
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
msg = '%s: manca del testo!' % win.GetName()
wx.MessageBox(msg)
return False
else:
return True
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NotInBadListValidator(wx.PyValidator):
def __init__(self, badlist):
wx.PyValidator.__init__(self)
self._badlist=badlist
20
def Clone(self): return NotInBadListValidator(self._badlist)
def TransferToWindow(self): return True
def TransferFromWindow(self): return True
21
22
23
24
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val in self._badlist:
msg = '%s: non valido!' % win.GetName()
wx.MessageBox(msg)
return False
else:
return True
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class NameDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
ugly_names = ('Cunegonda', 'Dagoberto', 'Emerenzio', 'Pancrazia')
self.first_name = wx.TextCtrl(self, name='Nome',
validator=NotInBadListValidator(ugly_names))
self.family_name = wx.TextCtrl(self, name='Cognome',
validator=NotEmptyValidator())
validate = wx.Button(self, wx.ID_OK, 'valida')
cancel = wx.Button(self, wx.ID_CANCEL, 'annulla')
45
s = wx.FlexGridSizer(2, 2, 5, 5)
s.AddGrowableCol(1)
s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.first_name, 1, wx.EXPAND)
s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(self.family_name, 1, wx.EXPAND)
46
47
48
49
50
51
52
s1 = wx.BoxSizer()
53
104
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
s1.Add(validate, 1, wx.EXPAND|wx.ALL, 5)
s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)
54
55
56
s2 = wx.BoxSizer(wx.VERTICAL)
s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)
s2.Add(s1, 0, wx.EXPAND)
self.SetSizer(s2)
s2.Fit(self)
57
58
59
60
61
62
63
64
65
66
67
class MyTopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'mostra dialogo')
b.Bind(wx.EVT_BUTTON, self.on_clic)
68
def on_clic(self, evt):
dlg = NameDialog(self)
ret = dlg.ShowModal()
if ret == wx.ID_OK:
print 'confermato'
else:
print 'annullato'
dlg.Destroy()
69
70
71
72
73
74
75
76
77
78
79
80
81
app = wx.App(False)
MyTopFrame(None, size=(200, 200)).Show()
app.MainLoop()
Alcune considerazioni preliminari. Ho scelto la tecnica di assegnare un name a ciascun widget (righe 39 e 41), e
di usare l’interfaccia GetName nei validatori per distinguerli (righe 10, 29). Il panel YourNamePanel è andato
via, e invece i widget da validare sono stati inseriti direttamente nel dialogo NameDialog. Questo perché un
dialogo ha già un suo panel predisposto e quindi appoggiargli sopra un altro frame avrebbe rischiesto l’uso di
wx.WX_EX_VALIDATE_RECURSIVELY per garantire la validazione automatica (che, ricordiamo, avviene chiamando Validate sul dialogo stesso). Infine, ho aggiunto un frame top-level MyTopFrame solo per esemplificare
il modo di chiamare il dialogo e poi distruggerlo.
Detto questo, passiamo alle cose più interessanti. Come abbiamo già visto parlando degli Id, i due pulsanti “valida” e
“annulla” (righe 43-44) sanno già che cosa fare, senza bisogno di collegarli a un evento. Entrambi tentano di chiudere
il dialogo, ma quello contrassegnato con wx.ID_OK, prima, esegue la validazione automatica. Tutti i widget nel
dialogo vengono validati, proprio come se avessimo chiamato Validate sul dialogo.
Notate che se la validazione fallisce il dialogo non si chiude. Questo vuol dire che, finché la validazione non ha
successo (o l’utente non preme “annulla”), il codice chiamante resta bloccato alla riga 71. E’ evidente che non c’è
proprio alcun modo di affidare al codice chiamante il compito di informare l’utente sul risultato della validazione: è
proprio necessario che siano i validatori stessi a pensarci.
Il codice chiamante prosegue la sua corsa quando la validazione ha successo, il dialogo si chiude e ShowModal
restituisce il codice corrispondente al pulsante premuto. Se adesso il codice è wx.ID_OK, si può stare sicuri che i
dati sono validi. Attenzione però: in caso di codice wx.ID_CANCEL, la validazione non è avvenuta e i dati non sono
sicuri.
Questo è importante: la validazione avviene solo in caso di wx.ID_OK. Se si desidera che i widget siano validati
sempre, qualunque pulsante sia stato premuto, allora bisogna tornare alla validazione manuale: collegare i pulsanti a
un evento, e chiamare Validate nel callback relativo.
Todo
6.8. Validatori: prima parte.
105
Appunti wxPython Documentation, Release 1
una pagina sulla validazione “in tempo reale” (avanzata? un’aggiunta a questa?)
6.8.6 Consigli sulla validazione.
Composizione di validatori.
A una prima impressione, i validatori sembrano oggetti limitati: per esempio, non possono essere combinati tra loro
per eseguire diversi test su un unico widget. Non è possibile chiamare diversi validatori uno dopo l’altro sullo stesso
widget. Così, ogni validatore deve avere, nel suo metodo Validate, tutti i test che servono per un dato widget in
una data circostanza. Questo limita il riutilizzo del validatore per diversi widget in condizioni differenti.
Tuttavia questa “limitazione” dipende spesso da un utilizzo errato dei validatori. Non dovete pensare che i validatori
siano il posto in cui scrivere effettivamente i test di validazione. Dovrebbero essere invece solo il punto di raccordo
finale tra la vostra suite di test il widget che dovete validare. Il codice effettivamente contenuto in Validate dovrebbe
essere breve, e avere solo quanto basta a gestire i dati in partenza e le risposte in arrivo.
Per esempio, io mi trovo spesso a scrivere cose come:
def Validate(self, win):
val = self.GetWindow().GetValue()
if all((test1(val), test2(val), test3(val))):
return True
else:
# informo l'utente che la validazione e' fallita
return False
Così posso scrivere separatamente i vari test1, test2, etc. in modo “atomico” e generale, e poi combinarli tra loro
a seconda dei casi (anche l’ordine di esecuzione si può naturalmente controllare). Così si può arrivare, nella peggiore
delle ipotesi a dover scrivere un breve validatore per ciascun widget da validare: ogni validatore rappresenta una catena
di test da eseguire.
Ma volendo si può fare di meglio, e scrivere un validatore “general purpose” con un numero variabile di test passati
come parametri:
class GroupTestValidator(wx.PyValidator):
def __init__(self, *tests):
wx.PyValidator.__init__(self)
self._tests = tests
#Clone, TransferToWindow, TransferFromWindow... bla bla
def Validate(self, win):
val = self.GetWindow().GetValue()
if all([test(val) for test in self._tests]):
return True
else:
return False
che poi può essere assegnato a diversi widget con diversi parametri:
text_1 = wx.TextCtrl(..., validator=GroupTestValidator(test1, test2))
text_2 = wx.TextCtrl(..., validator=GroupTestValidator(test1, test3, test4))
Naturalmente non bisogna esagerare: un singolo validatore “dinamico” non può certo bastare per tutte le esigenze
della vostra applicazione.
106
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
Validazione a cascata.
Sulla validazione a cascata, bisogna dire che è una grande comodità, tuttavia introduce dei limiti. Prima di tutto, la
validazione si ferma al primo widget che non va bene, ma questo impedisce all’utente di sapere se ci sono altri errori,
dopo il primo. E’ frustrante corregere un errore, premere di nuovo “invio”, e scoprire che c’era un errore anche nel
campo successivo. Se volete che tutti i widget siano validati comunque, non c’è niente da fare, dovete rinunciare alla
validazione a cascata (e a maggior ragione a quella ricorsiva, e a quella automatica), e validare a mano tutti i widget.
Fortunatamente in Python tutto diventa più semplice:
failed = []
for widget in (self.nome, self.cognome):
if not widget.Validate():
failed.append(widget)
# poi presento la lista degli errori, etc. etc.
Basta un’occhiata a questo banale ciclo for, e ci si chiede perchè perdere tempo con le validazioni a cascata, etc.
Ancora una volta, è merito della grande flessibilità di Python. Tuttavia i meccanismi di wxPython possono tornare
comodi per gestire i casi più consueti.
Validazione a seconda di un contesto.
Un altro limite dei validatori è che sono concepiti per validare un widget “di per sé stesso”, senza tenere conto del
contesto (per esempio, del valore di altri widget). Ora, naturalmente il “contesto” può essere calcolato e passato “a
mano” al validatore come argomento aggiuntivo:
class ContextValidator(wx.PyValidator):
#Clone, TransferToWindow, TransferFromWindow... bla bla
def Validate(self, win, context):
val = self.GetWindow().GetValue()
if all((test1(val, context), test2(val, context), ...)):
...
# e quindi, nel codice chiamante:
context = some_calculations() # per esempio, il valore di un altro widget
retcode = widget.Validate(context)
Questo ovviamente rende impossibile ogni tipo di validazione automatica, ma abbiamo visto che con Python in genere
non è un problema.
Ma c’è di più: sempre grazie alla flessibilità di Python, possiamo anche far calcolare il contesto dinamicamente al
validatore stesso. Possiamo spingerci a cose un po’ temerarie come questo esempio, dove un validatore ammette che
una casella di testo sia vuota solo se un’altra è piena:
class AlternateEmptyValidator(wx.PyValidator):
def __init__(self, context):
wx.PyValidator.__init__(self)
self.context = context
#Clone, TransferToWindow, TransferFromWindow... bla bla
def Validate(self, win):
val = self.GetWindow().GetValue()
context_val = self.context()
if context_val == '' and val == '': return False
if context_val != '' and val != '': return False
return True
E non si deve usare così naturalmente:
6.8. Validatori: prima parte.
107
Appunti wxPython Documentation, Release 1
text1 = wx.TextCtrl(..., validator=AlternateEmptyValidator(text2.GetValue))
text2 = wx.TextCtrl(..., validator=AlternateEmptyValidator(text1.GetValue))
perché al momento di assegnare il validatore a text1, text2 non esiste ancora! Tuttavia, può essere usato senza
problemi in questo modo:
text1 = wx.TextCtrl(...)
text2 = wx.TextCtrl(...)
text1.SetValidator(AlternateEmptyValidator(text2.GetValue))
text2.SetValidator(AlternateEmptyValidator(text1.GetValue))
La cosa importante è che, grazie a Python, passiamo direttamente il “callable” GetValue come argomento del
validatore, lasciando a lui il compito di... chiamarlo, appunto, quando necessario.
Problemi con i masked controls.
In ogni caso, altri problemi potrebbero spuntar fuori con i validatori. Per esempio, non giocano bene con i “masked
controls” (cercateli sulla demo), una famiglia di widget con un sistema di validazione interno, separato. Quando
un masked control non è valido, produce un suo comportamento di default (per esempio si colora di giallo): ma
siccome non ha un validatore vero e proprio attaccato, è difficile integrare questo suo comportamento in un processo
di validazione a cascata, per esempio.
Naturalmente si può argomentare che questo è colpa dei masked controls, e non dei validatori (che sono arrivati ben
prima). In ogni caso i masked controls sono utili da usare, ed è spiacevole dover gestire due flussi di validazione
separati.
Problemi con i controlli limitati.
Una situazione analoga è quella che capita con i numerosi widget che, in wxPython, hanno la possibilità di limitare
automaticamente i valori immessi. Per esempio, un wx.SpinCtrl può impostare un massimo e un minimo. Un
wx.ListBox o un wx.ComboBox si caricano con una lista di valori tra cui scegliere, e così via. In questi casi
la validazione, in un certo senso, è preventiva: una volta che il widget è mostrato, l’utente non può che inserire dati
validi.
Non è detto che i validatori siano completamente fuori gioco, neppure in questo caso. Potete lasciare che sia un validatore a caricare i dati in un wx.ComboBox, o impostare i limiti di un wx.SpinCtrl: è la funzione di trasferimento
dati che vedremo nella seconda parte di questa analisi.
In ogni caso, non è sempre facile gestire con disinvoltura questo doppio canale di validazione, per cui certi widget
sono controllati “a priori” e certi altri “a posteriori”.
Validazione ricorsiva.
Ancora qualche parola sulla validazione ricorsiva. In linea di principio, meglio non esagerare, specialmente se applicata alle finestre top-level che raggruppano (in vari panel) diverse macro-aree della vostra applicazione. Quando
chiamate Validate sul frame perché volete validare un certo settore, contemporaneamente validate anche tutti gli
altri. Nella migliore delle ipotesi è una perdita di tempo; nella peggiore un bel guaio, se in quel momento gli altri
settori sono in uno stato provvisoriamente inconsistente.
La cosa migliore è affidarsi al principio “ogni area, un panel” e validare i singoli panel, facendo affidamento sul
fatto che i loro figli diretti saranno i widget che davvero vi serve validare. Occasionalmente, quando uno di questi
panel-area ha una gerarchia più complessa (contiene altri panel, che contengono i widget), allora potete settare
wx.WX_EX_VALIDATE_RECURSIVELY solo per loro.
108
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
In conclusione: code smell?
In conclusione, i validatori sono strumenti utili, ma può essere difficile farli funzionare in modo armonico. Da un lato,
la loro praticità risalta soprattutto quando sono accoppiati ai dialoghi, con il meccanismo della validazione a cascata, e
automatica. Basta fare clic su wx.ID_OK, e ottieni gratis la validazione di tutto quanto. D’altra parte però con un ciclo
for in Python, anche la validazione manuale è molto agevole, e consente inoltre di personalizzare più accuratamente
che cosa e quando validare. Inoltre, sempre grazie a Python, è possibile scrivere validatori più generali e dinamici.
Anche dopo aver imparato a usare bene i validatori, resta comunque un vago “code smell”. C’è poco da fare: i
validatori entrano in gioco in un segmento difficile del flusso di gestione dell’applicazione, ovvero quando i dati
“sporchi” della gui si devono mescolare ai dati “puri” del modello sottostante. Vedremo anzi, nella seconda parte di
questa analisi, che il disagio può aumentare quando si usano i validatori per trasferire dati dal modello alla gui.
Alla fine, i validatori sono uno strumento. Vanno senz’altro bene per i casi più semplici, e possono essere usati
con successo anche in scenari più difficili: ma se non riuscite ad armonizzarli nel vostro framework di validazione
complessivo, potete tranquillamente rinunciarvi.
6.9 Validatori: seconda parte.
Todo
una pagina su MCV con molti riferimenti a questa.
Nella prima parte di questa analisi, abbiamo parlato dell’uso più naturale dei validatori: convalidare i dati immessi
dall’utente. Abbiamo visto che i validatori si inseriscono in un delicato momento della vita della vostra applicazione,
quando dovete trasformare i dati “grezzi” inseriti dall’utente nella gui, in dati “puri” (validi, consolidati) nel vostro
“model” e, in ultima analisi, nel vostro database o altro sistema di storage permanente.
I validatori cercano di offrire un servizio completo, per questa fase di lavoro. Non si limitano a convalidare i dati, ma,
se volete, si occupano anche di trasferirli avanti e indietro tra il “model” e l’interfaccia grafica.
6.9.1 Trasferimento dati nei dialoghi con validazione automatica.
Il modo più semplice per illustrare questa possibilità, è vederla applicata nel suo ambiente naturale, dove i validatori
danno il meglio di sé: i dialoghi con validazione automatica.
Per questo motivo abbiamo ampliato l’esempio dei “nomi e cognomi” che abbiamo seguito fin qui, fino a trasformarlo
in una applicazione vera e propria, anche se piccola. Il codice è più lungo, ma vale la pena di seguirlo per vedere come
i validatori si integrano nella vita di un’applicazione nel mondo reale (o quasi).
Il nostro programma consente di vedere, modificare e aggiungere dei nomi a un database. Quando fate doppio clic su
un nome della lista, il dialogo si apre in modalità “modifica”, e quando cliccate sul pulsante “nuovo”, lo stesso dialogo
vi consente di aggiungere un nuovo elemento.
1
2
3
4
5
class NotEmptyValidator(wx.PyValidator):
def __init__(self, person, key):
wx.PyValidator.__init__(self)
self._person = person
self._key = key
6
7
def Clone(self): return NotEmptyValidator(self._person, self._key)
8
9
10
def TransferToWindow(self):
win = self.GetWindow()
6.9. Validatori: seconda parte.
109
Appunti wxPython Documentation, Release 1
win.SetValue(self._person.get(self._key, ''))
return True
11
12
13
def TransferFromWindow(self):
win = self.GetWindow()
self._person[self._key] = win.GetValue()
return True
14
15
16
17
18
def Validate(self, ctl):
win = self.GetWindow()
val = win.GetValue().strip()
if val == '':
msg = '%s: non deve essere vuoto.' % win.GetName()
wx.MessageBox(msg)
return False
else:
return True
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AgeValidator(wx.PyValidator):
def __init__(self, person):
wx.PyValidator.__init__(self)
self._person = person
34
def Clone(self): return AgeValidator(self._person)
def Validate(self, win): return True # non facciamo validazione
35
36
37
def TransferToWindow(self):
win = self.GetWindow()
win.SetRange(0, 100) # minimo e massimo
win.SetValue(self._person.get('eta', 20)) # valore di default
return True
38
39
40
41
42
43
def TransferFromWindow(self):
win = self.GetWindow()
self._person['eta'] = win.GetValue()
return True
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class PersonDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
person = self.GetParent().current_person
self.first_name = wx.TextCtrl(self, name='Nome',
validator=NotEmptyValidator(person, 'nome'))
self.family_name = wx.TextCtrl(self, name='Cognome',
validator=NotEmptyValidator(person, 'cognome'))
self.year = wx.SpinCtrl(self, name="Eta'")
self.year.SetValidator(AgeValidator(person))
ok = wx.Button(self, wx.ID_OK, 'conferma')
cancel = wx.Button(self, wx.ID_CANCEL, 'annulla')
62
s = wx.FlexGridSizer(3, 2, 5, 5)
s.AddGrowableCol(1)
for ctl, lab in ((self.first_name, 'nome:'),
(self.family_name, 'cognome:'), (self.year, "eta':")):
s.Add(wx.StaticText(self, -1, lab), 0, wx.ALIGN_CENTER_VERTICAL)
s.Add(ctl, 1, wx.EXPAND)
63
64
65
66
67
68
110
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
69
70
71
72
s1 = wx.BoxSizer()
s1.Add(ok, 1, wx.EXPAND|wx.ALL, 5)
s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)
73
74
75
76
77
78
s2 = wx.BoxSizer(wx.VERTICAL)
s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)
s2.Add(s1, 0, wx.EXPAND)
self.SetSizer(s2)
s2.Fit(self)
79
80
81
82
83
84
85
86
87
88
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.people = wx.ListBox(p)
self.people.Bind(wx.EVT_LISTBOX_DCLICK, self.on_view_person)
new = wx.Button(p, -1, 'nuovo')
new.Bind(wx.EVT_BUTTON, self.on_new)
89
90
91
92
93
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.people, 1, wx.EXPAND|wx.ALL, 5)
s.Add(new, 0, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s)
94
95
96
self.current_person = {}
self.reload_people_list()
97
98
99
100
101
102
103
104
105
def reload_people_list(self):
self.people.Clear()
people = wx.GetApp().PEOPLE
for p in people:
s = '%i: %s %s %i' % (p, people[p]['nome'],
people[p]['cognome'], people[p]['eta'])
self.people.Append(s)
self.current_person = {}
106
107
108
109
110
111
112
113
114
115
116
117
118
def on_view_person(self, evt):
app = wx.GetApp()
selected = self.people.GetString(evt.GetSelection())
# questo e' molto brutto, ma e' per fare in fretta....
id = int(selected.split(':')[0])
self.current_person = app.PEOPLE[id]
dlg = PersonDialog(self, title='Vedi persona')
ret = dlg.ShowModal()
if ret == wx.ID_OK:
app.PEOPLE[id] = self.current_person
self.reload_people_list()
dlg.Destroy()
119
120
121
122
123
124
125
126
def on_new(self, evt):
self.current_person = {}
dlg = PersonDialog(self, title='Nuova persona')
ret = dlg.ShowModal()
if ret == wx.ID_OK:
app = wx.GetApp()
id = max(app.PEOPLE.keys()) + 1
6.9. Validatori: seconda parte.
111
Appunti wxPython Documentation, Release 1
app.PEOPLE[id] = self.current_person
self.reload_people_list()
dlg.Destroy()
127
128
129
130
131
132
133
134
135
136
137
138
class App(wx.App):
def OnInit(self):
self.PEOPLE = {1: {'nome':'Mario', 'cognome':'Rossi', 'eta':37},
2: {'nome':'Giuseppe', 'cognome':'Bianchi', 'eta':25},
3: {'nome':'Andrea', 'cognome':'Verdi', 'eta':42}}
TopFrame(None, title='Persone', size=(300, 300)).Show()
return True
139
140
141
142
app = App(False)
app.MainLoop()
Per cominciare, alcune spiegazioni che non riguardano i validatori. Per non scrivere troppo codice “fuori tema”,
abbiamo dovuto fare un bel po’ di semplificazioni: il “database” è in realtà un dizionario PEOPLE inizializzato alla riga
134. Abbiamo visto altrove che un posto intelligente per inizializzare la connessione al database è wx.App.OnInit,
e quindi seguiamo questa strada: immaginate soltanto che il dizionario PEOPLE sia in realtà una connessione aperta,
o un riferimento a un ORM.
La nostra finestra principale TopFrame ha semplicemente una lista di tutte le persone, e un pulsante per aggiungerne
di nuove. Il metodo reload_people_list (riga 98) si incarica di ricaricare la lista. Anche in questo caso,
abbiamo semplificato molto: dovete immaginare una richiesta a un database e un processo di adattamento dei dati al
formato di visualizzazione.
Quando l’utente fa doppio clic su un nome, la nostra semplificazione sconfina nell’errore vero e proprio (riga 111):
qui estraiamo l’id della persona selezionata direttamente dalla stringa di testo visualizzata nella lista. Che brutto!
Ovviamente non fate mai cose del genere nel mondo reale. Dovreste avere un “model” di qualche tipo collegato alla
“view” della lista, e quindi chiedere al “model” quale oggetto-persona corrisponde alla riga selezionata.
L’id così malamente ricavato ci serve per chiedere al “database” i dati necessari della persona selezionata (riga 112),
che vengono memorizzati nella variabile self.current_person. Questo è più simile a ciò che avverrebbe nel
mondo reale, in effetti.
Dopo di che, entra in gioco il PersonDialog che è il cuore della nostra applicazione, e che esaminiamo nel dettaglio
tra poco. Iniziamo però a notare che PersonDialog, in qualche modo per ora misterioso, ha la facoltà di modificare
il valore di self.current_person. E quindi, se l’utente ha chiuso il dialogo con wx.ID_OK, a noi non resta che
aggiornare il database con il self.current_person modificato, e quindi chiamare reload_people_list
per aggiornare la lista mostrata (righe 115-117).
Se invece l’utente fa clic sul pulsante “nuovo”, la procedura è identica, salvo che adesso self.current_person è
ovviamente un dizionario vuoto (riga 121). Di nuovo, quando l’utente chiude il dialogo, a noi non resta che aggiornare
il database e rinfrescare la lista. L’unica piccola variante è che questa volta dobbiamo calcolarci un nuovo id per il
database (riga 126: questo ovviamente nel mondo reale non sarebbe necessario... i database sanno regolarsi da soli).
Il dialogo PersonDialog, a prima vista sembra completamente magico. Ha solo un __init__ per disegnare la
gui, ma non si vede nessun codice per gestire tutte le operazioni di cui è incaricato. Parte della magia, ormai, dovrebbe
essere chiara: alle righe 60 e 61 creiamo due pulsanti con Id predefiniti, che si occupano di chiudere il dialogo, e (il
pulsante “conferma”) di validare i dati. Se non vi è chiaro perché, rileggete la prima parte di questa analisi, e anche
la pagina sugli Id.
Ma la vera magia sta nei validatori. Ciascun widget del nostro dialogo ha un validatore assegnato (righe 54-59).
Incidentalmente, notate che wx.SpinCtrl non prevede un paramentro validator nel suo costruttore, e quindi
dobbiamo assegnare il validatore in un secondo momento, usando SetValidator.
E finalmente esaminiamo i due validatori che abbiamo scritto. NotEmptyValidator si applica alle due caselle di
112
Chapter 6. Appunti wxPython - livello intermedio
Appunti wxPython Documentation, Release 1
testo, e il suo metodo Validate fa quello a cui siamo già abituati: blocca tutto se la casella è vuota.
La novità sono i due metodi per il trasferimento dei dati. Occorre prima di tutto capire che TransferToWindow
viene invocato automaticamente quando il dialogo si apre. TransferFromWindow invece viene invocato quando
il dialogo si chiude, ma solo in corrispondenza della pressione di wx.ID_OK (e naturalmente, solo a validazione
avvenuta).
Per poter trasferire i dati, il validatore deve avere qualche conoscenza del nostro “model”. Ecco perché passiamo come
argomenti person e key. Il primo è collagato alla self.current_person della nostra finestra-madre (vedi riga
53), il secondo è il “campo” esatto a cui il widget è collegato (vedi righe 55 e 57). In questo modo il validatore conosce
con precisione il valore su cui deve lavorare.
All’apertura del dialogo, TransferToWindow si occupa di prelevare il valore alle coordinate “person / key” e di
inserirlo nel widget (riga 11). Alla chiusura, TransferToWindow fa il lavoro opposto, prelevando il nuovo valore
e modificando il self.current_person della finestra-madre (riga 16). La cosa importante da notare che è che,
se il dato viene sovrascritto dal validatore, possiamo essere sicuri che è tutto è regolare, in quanto: primo, l’utente ha
premuto wx.ID_OK; secondo, la validazione è avvenuta con successo.
A questo punto il validatore ha terminato il suo lavoro, e per un attimo nella finestra madre si verifica una inconsistenza
tra il contenuto di self.current_person ormai modificato (il “controller”), e il valore riportato nella lista (la
“view”) e nel database (il “model”). E’ infatti, non appena chiuso il dialogo (riga 115), la finestra madre si occupa
immediatamente di aggiornare il database (riga 116) e la lista (riga 117).
Note: Non potrebbe occuparsi il validatore di aggiornare anche il database? Se il validatore avesse conoscenza diretta
del database (e non solo della current_person) potrebbe ricavare i valori che gli servono da questo, e modificarli
quando occorre. Nel nostro esempio, sarebbe naturalmente possibile. Ma nel mondo reale, c’è una ragione tecnica per
non farlo: se ogni validatore fa una richiesta separata per ottenere il suo pezzetto di informazione, potete scommettere
che l’amministratore del database avrà qualche obiezione (nel nostro esempio, sarebbero già 3 query in entrata e 3 in
uscita). Tuttavia, se usate un ORM e siete in grado di ottimizzare le richieste in qualche modo, allora nessun problema.
Comprendere questo flusso di gestione è fondamentale. Infatti vediamo come i validatori hanno favorito, sia pure
in modo ancora embrionale, la separazione delle diverse funzioni, fino ad arrivare a una sorta di “model-controllerview”. Nel nostro esempio, il model è ovviamente il database PEOPLE. La view è la lista mostrata nella finesta
madre. Il codice di controllo sta nel callback on_view_person (e in on_new ce n’è un’altro pressoché identico...
andrebbero fattorizzati, ma li ho lasciati separati per semplicità). Il “controller” si occupa di rispondere agli eventi
wxPython, e tenere sincronizzato il model con la view.
La terza casella (l’età della nostra persona) è un wx.SpinCtrl, e per quello abbiamo bisogno di un validatore
separato: da un lato non avrebbe senso controllare se il campo è vuoto (un wx.SpinCtrl non è mai vuoto), e
dall’altro abbiamo bisogno di controllare qualche dettaglio ulteriore.
Il validatore che abbiamo assegnato, AgeValidator, non fa in effetti nessuna validazione (riga 36), e si occupa solo
del trasferimento dei dati, in maniera del tutto analoga all’altro validatore. Osservate però che, al momento di aprire
il dialogo, si occupa anche di impostare minimo, massimo e valore di default per il wx.SpinCtrl affidato alla sua
sorveglianza (righe 40-41). Nel nostro piccolo esempio questo avviene in un brutto modo “statico”, ma non è difficile
passare questi valori dinamicamente come parametri. Questo è un esempio di come si possono usare i validatori anche
per lavorare con i controlli limitati, affrontando un problema che avevamo visto nella prima parte.
6.9.2 Trasferimento dati negli altri casi.
La capacità dei validatori di trasferire dati può essere usata anche in un contesto di validazione “manuale”. Se applicate
un validatore a un widget, TransferToWindow sarà invocato automaticamente ogni volta che il widget viene
mostrato. D’altra parte, dovrete invece chiamare direttamente TransferFromWindow per riottenere i dati indietro:
naturalmente prima dovete assicurarvi di aver chiamato Validate per controllare che i dati siano giusti.
6.9. Validatori: seconda parte.
113
Appunti wxPython Documentation, Release 1
6.9.3 Conclusioni.
La capacità dei validatori di gestire il flusso dei dati tra il “model” e l’interfaccia grafica, in aggiunta al loro utilizzo più
comune per validare i dati, li rende uno strumento potenzialmente centrale nella vostra applicazione. Con i validatori
potete automatizzare il flusso dei dati e separare le funzioni di controllo dal resto.
Come abbiamo visto nella prima parte, le difficoltà non mancano, ma spesso sono legate a una imperfetta comprensione di come funzionano davvero i validatori. Tuttavia, anche con le migliori intenzioni, talvota i validatori possono
essere semplicemente scomodi da usare. Va detto che specialmente nel mondo Python, le capacità di automatizzazione
dei validatori brillano di meno: spesso basta un ciclo for per applicare una routine di validazione a tutti i widget della
zona, senza bisogno di ulteriori sovrastrutture.
Tuttavia un utilizzo intelligente dei validatori indirizza verso la separazione tra “model”, “controller” e “view”, e
quindi, se non li volete usare, dovreste comunque fare attenzione che il sistema con cui li rimpiazzate mantenga questo
vantaggio.
Ancora una osservazione: i validatori sono uno dei punti in cui wxPython non si limita a essere un puro “gui framework”, ma sconfina nel campo di un “application framework”. Non c’è nulla di male in questo, finché ne siete
consapevoli. Se, per esempio, volete usare wxPython come front-end grafico “plugin” liberamente sostituibile con
altre interfacce, allora probabilmente non vi conviene legare ai validatori il vostro codice di controllo.
114
Chapter 6. Appunti wxPython - livello intermedio
CHAPTER 7
Appunti wxPython - livello avanzato
7.1 I constraints: un modo alternativo di organizzare il layout.
Il modo corretto di organizzare il layout della vostra interfaccia grafica è utilizzare i sizer (quello scorretto è naturalmente il posizionamento assoluto, come abbiamo già detto nella stessa pagina).
Prima che venissero introdotti i sizer, tuttavia, il layout delle finestre si organizzava con i constraints. I sizer sono obiettivamente più pratici ed eleganti, e negli anni sono stati sviluppati moltissimo. I constraints invece sono ufficialmente
deprecati da oltre 10 anni, e nessuno li usa più. Tuttavia non sono mai stati rimossi completamente da wxWidgets (e
anche wxPython continua a supportarli).
Anche se i constraints sono più macchinosi da usare rispetto ai sizer, ci sono alcuni casi particolari in cui potrebbero
ancora tornare utili. In generale dovreste sempre usare i sizer: se un problema di layout vi sembra insormontabile con
i sizer, nove su dieci vuol dire che avete ancora difficoltà a padroneggiarli. Tuttavia, occasionalmente potreste davvero
trovarvi in una situazione in cui i vecchi constraints hanno ancora qualche carta da giocare.
In questa pagina ci limiteremo a una breve presentazione dei constraints: se dovessero servirvi, la documentazione di
wxWidgets spiega tutti i dettagli. In ogni caso, dovreste usare i contraints solo come ultima risorsa, e solo nel modo
più semplice: se vi trovate a passare troppo tempo a studiare gli oscuri dettagli dei constraints, probabilmente state
sbagliando approccio e dovreste tornare ai sizer.
7.1.1 wx.IndividualLayoutConstraint e wx.LayoutConstraints.
Proprio come wx.Sizer e le sue sottoclassi incapsulano l’algoritmo di calcolo
wx.IndividualLayoutConstraint definisce un constraint da applicare a un widget.
di
un
sizer,
Un constraint è un vincolo che si impone a una caratteristica geometrica di un widget: si possono imporre fino a otto
diversi contraints, relativi a
• bordo destro,
• bordo sinistro,
• bordo superiore,
• bordo inferiore,
• altezza,
• larghezza,
• coordinata x del centro,
• coordinata y del centro.
115
Appunti wxPython Documentation, Release 1
I vincoli si specificano in relazione ad altri widget, che possono essere i container genitori (un wx.Panel, per
esempio), o i widget “fratelli” nello stesso container. Per esempio è possibile vincolare il bordo destro di un widget a
restare a 50 pixel dal bordo sinistro del vicino; si possono specificare anche vincoli come percentuali di altri vincoli, e
così via.
In realtà non c’è quasi mai ragione di istanziare direttamente un singolo constraint e poi applicarlo a un widget: si
preferisce usare la più comoda classe wx.LayoutConstraints, che in pratica è un contenitore che permette di
specificare fino a 8 vincoli insieme, e poi applicarli tutti contemporaneamente al widget interessato.
Un’istanza di wx.LayoutConstraints ha otto proprietà che mappano gli otto tipi di constraint visti sopra:
wx.LayoutConstraints.top impone un vincolo al bordo superiore, e così via (le altre sono .bottom, .left,
.right, .height, .width, .centreX e .centreY).
Un primo esempio chiarirà meglio la tecnica necessaria (iniziate a leggere dal codice della classe MainFrame, per
semplicità):
def my_constraints(relative_to):
lc = wx.LayoutConstraints()
lc.top.Below(relative_to, 20)
lc.width.PercentOf(relative_to, wx.Width, 50)
lc.height.AsIs()
# provate per esempio ad alternare le due righe qui sotto:
lc.left.SameAs(relative_to, wx.Left)
# lc.centreX.SameAs(relative_to, wx.CentreX)
return lc
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
p.SetAutoLayout(True)
b1 = wx.Button(p, -1, '1')
lc = wx.LayoutConstraints()
lc.top.SameAs(p, wx.Top, 20)
lc.left.SameAs(p, wx.Left, 40)
lc.width.PercentOf(p, wx.Width, 50)
lc.height.AsIs()
b1.SetConstraints(lc)
b2 = wx.Button(p, -1, '2')
b2.SetConstraints(my_constraints(relative_to=b1))
b3 = wx.Button(p, -1, '3')
b3.SetConstraints(my_constraints(relative_to=b2))
b4 = wx.Button(p, -1, '4')
b4.SetConstraints(my_constraints(relative_to=b3))
Abbiamo collocato un primo pulsante con dei constraint relativi al panel contenitore. Per gli altri pulsanti abbiamo
fattorizzato le regole dei constraint in una funzione separata, cosa che ci ha consentito di risparmiare un bel po’ di
spazio. Chiaramente, nel caso generale, avremmo dovuto specificare dei constraint differenti per ciascun widget, e
dopo un po’ di questa ginnastica capirete perché i sizer sono più comodi da usare.
La sintassi di wx.LayoutConstraints è articolata, ma tutto sommato facile da capire. Ciascuno degli otto
constraint può essere specificato in termini di:
• .SameAs, ovvero lo stesso bordo o dimensione di un riferimento (più un eventuale margine espresso in pixel);
• .PercentOf, ovvero una percentuale di un riferimento;
• .AsIs, ovvero “invariato”, oppure “dimensioni di default”;
116
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
• .Above, .Below, .LeftOf, .RightOf, ovvero sopra, sotto, a destra o a sinistra di un riferimento (più un
eventuale margine);
• .Absolute, ovvero il bordo o la dimensione sono espressi in valori assoluti;
• .Unconstrained, ovvero non ci sono vincoli, e il bordo e la dimensione sono calcolati dopo che tutti gli
altri vincoli sono stati rispettati (questo è il valore di default).
Il riferimento a cui si fa... riferimento (ehm) è espresso in termini delle costanti wx.Right, wx.Left, wx.Top,
wx.Bottom, wx.CentreX e wx.CentreY il cui significato è ovvio.
Per esempio, la riga lc.left.SameAs(p, wx.Left, 40) significa che il bordo sinistro (.left) del widget
a cui verranno assegnati questi constraint dovrà avere lo stesso valore (.SameAs) del bordo sinistro (wx.Left) del
widget di riferimento (il panel contenitore p), più un margine di 40 pixel.
I constraint si applicano al widget voluto invocando il metodo wx.Window.SetConstraints. Il calcolo
effettivo di tutti i constraint applicati avviene nel momento in cui wxPython esegue internamente il metodo
wx.Window.Layout del widget (ne abbiamo già parlato). Questo metodo però non è eseguito automaticamente:
per essere sicuri che sia davvero chiamato ogni volta che la finestra viene ri-dimensionata, possiamo fare tre cose:
• la più semplice, è chiamare wx.Window.SetAutoLayout sul parent dei widget a cui vogliamo assegnare
dei constraint: questo è possibile solo se il parent è un contenitore (che peraltro in pratica è la situazione più
frequente) ovvero un panel, un dialogo o un frame;
• sovrascrivere il callback wx.Window.OnSize che viene eseguito di default in risposta a un wx.EVT_SIZE,
e chiamare lì direttamente wx.Window.Layout;
• oppure, in modo equivalente, catturare wx.EVT_SIZE e chiamare wx.Window.Layout nel nostro callback.
7.1.2 Quando i constraints possono tornare utili.
Come avrete capito anche da questo primo semplice esempio, i constraint sono molto verbosi e farraginosi da usare,
in confronto ai sizer. In genere non vale la pena. Tuttavia, di tanto in tanto anche i sizer mostrano qualche limite.
Considerate per esempio il caso in cui volete assegnare dei bordi asimmetrici a un widget. Vediamo prima un layout
con i constraints:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel_base = wx.Panel(self)
panel_red = wx.Panel(panel_base)
panel_red.SetBackgroundColour(wx.RED) # per distinguerlo...
panel_base.SetAutoLayout(True)
lc = wx.LayoutConstraints()
lc.top.SameAs(panel_base, wx.Top, 20)
lc.left.SameAs(panel_base, wx.Left, 40)
lc.bottom.SameAs(panel_base, wx.Bottom, 60)
lc.right.SameAs(panel_base, wx.Right, 80)
panel_red.SetConstraints(lc)
b = wx.Button(panel_red, -1, 'clic', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.panel_base = panel_base
self.panel_red = panel_red
def on_clic(self, evt):
# dimostriamo come cambiare i margini di panel_red
lc = wx.LayoutConstraints()
7.1. I constraints: un modo alternativo di organizzare il layout.
117
Appunti wxPython Documentation, Release 1
lc.top.SameAs(self.panel_base, wx.Top, 70)
lc.left.SameAs(self.panel_base, wx.Left, 50)
lc.bottom.SameAs(self.panel_base, wx.Bottom, 30)
lc.right.SameAs(self.panel_base, wx.Right, 10)
self.panel_red.SetConstraints(lc)
self.panel_base.SendSizeEvent()
Come si vede, la logica dei constraint è facile da seguire, e non abbiamo nessuna difficoltà a impostare quattro margini
differenti per il nostro panel rosso. Anche modificare i margini successivamente è banale, come dimostriamo nel
callback del pulsante: basta ricreare e ri-assegnare un wx.LayoutConstraints (qui non ci siamo preoccupati
troppo di duplicare parecchie linee di codice. In un progetto reale, un po’ di fattorizzazione sarebbe consigliabile!).
L’unico accorgimento necessario, dal momento che la finestra non ha cambiato dimensioni, è ricordarsi di chiamare
wx.Window.SendSizeEvent per innescare il ricalcolo del layout.
I sizer d’altra parte hanno più difficoltà a gestire margini differenti. Se i margini fossero tutti uguali, non ci sarebbero
problemi a fare qualcosa come:
s = wx.BoxSizer()
s.Add(panel_red, 1, wx.EXPAND|wx.ALL, 20)
panel_base.SetSizer(s)
In caso di margini diversi, però, il layout si complica e bisogna ricorre ad artifici come gli spazi vuoti. Un equivalente
potrebbe essere questo:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel_base = wx.Panel(self)
panel_red = wx.Panel(panel_base)
panel_red.SetBackgroundColour(wx.RED)
s = wx.FlexGridSizer(3, 3) # una griglia 3x3
s.AddGrowableCol(1)
s.AddGrowableRow(1)
s.Add((40, 20)) # gli spacer d'angolo impongono i margini
s.Add((-1, -1))
s.Add((-1, -1))
s.Add((-1, -1))
s.Add(panel_red, 1, wx.EXPAND) # al centro, il panel rosso
s.Add((-1, -1))
s.Add((-1, -1))
s.Add((-1, -1))
s.Add((80, 60)) # gli spacer d'angolo impongono i margini
panel_base.SetSizer(s)
Qui per fortuna ci siamo fatti aiutare da un wx.FlexGridSizer con le sue proprietà AddGrowableCol e
AddGrowableRow, perché lo stesso layout realizzato esclusivamente con i wx.BoxSizer sarebbe stato più complicato (anche se invece un wx.GridBagSizer, a dire il vero, ci avrebbe risparmiato un po’ di linee di codice: ma
lo strumento migliore varia da progetto a progetto). Si nota comunque una buona dose di artificiosità per realizzare un
layout tutto sommato molto semplice.
Anche cambiare i margini a runtime, con i sizer è più complicato, e questo perfino nell’ipotesi che tutti i margini
siano uguali. Infatti i margini sono determinati da costanti e flag del metodo wx.Sizer.Add, in costrutti del
tipo s.Add(widget, 1, wx.BOTTOM|wx.TOP, 5) (che vuol dire, un margine di 5 pixel sopra e sotto).
L’unica soluzione per cambiare questi parametri in seguito, è conservare un riferimento al wx.SizerItem
restituito dalla chiamata a wx.Sizer.Add, e poi usare metodi come wx.SizerItem.SetFlag o
wx.SizerItem.SetProportion per cambiare le cose, come abbiamo visto. Oppure, si potrebbe in modo più
118
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
radicale staccare l’elemento dal sizer senza distruggerlo (con wx.Sizer.Detach) e poi re-inserirlo nello stesso
posto con parametri diversi.
Questo sono comunque scenari piuttosto rari nella pratica di tutti i giorni. Di solito non capita di dover modificare
margini già assegnati; in effetti, è raro anche voler assegnare margini asimmetrici. Non dovrebbe capitarvi spesso,
quindi, una situazione in cui vi viene voglia di usare i constraint invece dei sizer.
Se però decidete di usare i constraint per qualche motivo, ricordate infine che potete farli lavorare insieme ai sizer
senza particolari problemi. Nell’esempio qui sopra, per brevità abbiamo inserito un pulsante all’interno del panel
rosso con il posizionamento assoluto (anche se sappiamo bene che non bisognerebbe mai farlo). Avremmo invece
potuto creare un sizer assegnato al panel rosso, e usarlo come di consueto per disporre il pulsante e altri widget.
7.2 Gli eventi: altre tecniche.
Abbiamo già dedicato due pagine agli eventi. In questa sezione raccogliamo alcune note aggiuntive: non si tratta di
concetti più difficili dei precedenti, ma semplicemente di tecniche di utilizzo meno frequente.
La lettura di questa pagina presuppone la conoscenza delle due precedenti.
7.2.1 Lambda binding.
Abbiamo visto che un callback può, di regola, accettare un solo argomento: un riferimento all’evento che è stato
intercettato. Questa è una limitazione piuttosto fastidiosa del framework c++ sottostante. In realtà spesso l’oggettoevento porta con sé molte informazioni utili (GetEventObject restituisce un riferimento all’oggetto originario, per
esempio), ma ci sono casi in cui semplicemente si vorrebbe passare al callback qualcosa in più.
Una soluzione drastica sarebbe quella di creare un evento personalizzato (come mostriamo oltre in questa stessa
pagina) che porti dentro di sé tutte le informazioni che ci servono.
Una soluzione ancora più drastica potrebbe essere intercettare l’evento in uno stadio precedente della sua
propagazione, arricchirlo delle proprietà che ci servono, e lasciarlo proseguire.
Tuttavia le funzioni anonime lambda ci offrono una soluzione molto più snella. Una lambda conta pur sempre
come “uno” negli argomenti accettati da Bind, ma dentro possiamo metterci quello che vogliamo. Lo schema è molto
facile da capire:
button.Bind(wx.EVT_BUTTON,
lambda evt, foo=foo_val, bar=bar_val: self.callback(evt, foo, bar))
def callback(self, evt, foo, bar):
pass # ...
La nostra funzione lambda riceve ancora l’argomento consueto evt (il riferimento all’evento), ma ne aggiunge anche
altri a piacere. In questo modo possiamo passare a callback più informazioni di quelle contenute in evt.
Grazie alla consueta flessibilità di Python, possiamo passare come argomenti aggiuntivi sia valori statici (il testo di
una label, per esempio), sia riferimenti a funzioni da eseguire nel callback per ottenere valori dinamici.
7.2.2 Partial binding.
Il lambda-binding è un trucco molto vecchio. Nelle versioni più recenti di Python, si può ottenere la stessa cosa,
naturalmente, usando functools.partial. Si tratta di una tecnica molto meno utilizzata, ma solo per la maggiore
consuetudine con il lambda-binding.
Anche in questo caso, non c’è molto da spiegare:
7.2. Gli eventi: altre tecniche.
119
Appunti wxPython Documentation, Release 1
from functools import partial
button.Bind(wx.EVT_BUTTON, partial(self.callback, foo=foo_val, bar=bar_val))
def callback(self, evt, foo, bar):
pass # ...
In pratica, functools.partial è un wrapper del nostro callback che lascia fuori solo il primo argomento (il
consueto riferimento all’evento), e specifica quelli successivi.
7.2.3 Event Manager.
wx.lib.evtmgr è una piccola libreria che propone un modo alternativo e più “pitonico” di collegare gli eventi. E’
più o meno facile da usare come Bind, tuttavia è anche altrettanto facile scollegare gli eventi, e soprattutto si può
registrare un numero arbitrario di widget ad “ascoltare” il verificarsi di un certo evento (con Bind è possibile solo il
contrario: registrare un solo widget per catturare molti eventi).
Il modulo esporta una classe eventManager, che è un singleton. Il suo utilizzo è incredibilmente semplice:
from wx.lib.evtmgr import eventManager
# per registrare un callback all'ascolto di un evento proveniente da widget
eventManager.Register(callback, wx.EVT_*, widget)
# per de-registrare un callback
eventManager.DeregisterListener(callback)
# per de-registrare tutti i callback in ascolto degli eventi di widget
eventManager.DeregisterWindow(widget)
Come si può intuire dall’interfaccia, Event Manager utilizza il design pattern noto come Publisher/Subscriber. In
effetti, wxPython ha una sua implemetazione di pub/sub, molto ben fatta, di cui parliamo in una pagina separata.
Event Manager non è molto usato nella pratica perché il normale sistema di collegamento con Bind è in genere
sufficiente: il punto di forza di Event Manager (collegamento molti-a-molti tra sorgenti e ascoltatori) è in genere poco
utile nella struttura gerarchica dei gui-framework.
Tuttavia, Event Manager può essere preso in considerazione in molte situazioni dove pub/sub andrebbe impiegato.
Se il vostro design funzionerebbe meglio con pub/sub, allora date prima una possibilità anche a Event Manager. Vi
rimandiamo quindi alla pagina di pub/sub per un esame più approfondito di Event Manager e di quando conviene
usarlo.
7.2.4 Eventi personalizzati.
wxPython offre una grandissima varietà di eventi pronti all’uso, che coprono tutte le possibili interazioni con l’utente.
Tuttavia, è possibile anche creare nuovi eventi all’occorrenza.
Questo può essere utile in diverse occasioni, ma forse la più comune è quando si crea un nuovo widget (partendo da
zero, oppure assemblando cose già esistenti). Anche se al suo interno il widget può fare uso dei soliti eventi wxPython,
spesso si preferisce che propaghi verso l’esterno un evento nuovo, con un binder specifico apposta per lui. In questo
modo si nascondono i dettagli dell’implementazione interna, l’evento può trasportare le informazioni che desideriamo,
e l’event type “firma” l’evento rendendo evidente che è stato originato dal nostro widget.
Creare un evento, di per sé, non basta. Occorre anche creare un nuovo event type e un nuovo binder per collegarlo ai
callback. Esaminiamo questi passaggi, prendendo spunto da un esempio concreto: vogliamo creare un nuovo “widget”
che permetta di selezionare i trimestri di un anno.
120
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Note: L’esempio che segue è una semplificazione di un widget più elaborato che ho scritto per un altro progetto. La
versione completa, per chi è interessato, si trova qui.
Il widget è composto da un wx.ComboBox che elenca i trimestri, e uno wx.SpinCtrl per selezionare l’anno:
from datetime import datetime
class PeriodWidget(wx.Panel):
PERIODS = {'1 trimestre': ((1, 1), (3, 31)), '2 trimestre': ((4, 1), (6, 30)),
'3 trimestre': ((7, 1), (9, 30)), '4 trimestre': ((10, 1), (12, 31))}
def __init__(self, *a, **k):
wx.Panel.__init__(self, *a, **k)
self.period = wx.ComboBox(self, choices=self.PERIODS.keys(),
style=wx.CB_DROPDOWN|wx.CB_READONLY|wx.CB_SORT)
self.period.SetSelection(0)
self.year = wx.SpinCtrl(self, initial=2012, min=1950, max=2050)
s = wx.BoxSizer()
s.Add(self.period, 0, wx.EXPAND|wx.ALL, 5)
s.Add(self.year, 0, wx.EXPAND|wx.ALL, 5)
self.SetSizer(s)
s.Fit(self)
def GetValue(self):
start, end = self.PERIODS[self.period.GetStringSelection()]
year = self.year.GetValue()
return datetime(year, *start), datetime(year, *end)
Quando l’utente agisce sui due widget interni del nostro PeriodWidget, emette degli eventi che possono essere
intercettati. Noi vorremmo però presentare all’esterno un’interfaccia più coerente e pulita: il nostro widget dovrebbe
emettere un evento personalizzato ogni volta che l’utente cambia il periodo oppure l’anno.
Ecco quindi quello che dobbiamo fare.
Definire un event-type e un binder.
Prima ancora di scrivere la nostra classe-evento, conviene definire un nuovo event type, e di conseguenza un nuovo
binder per identificare il nostro evento. Per fortuna questa è la parte più facile di tutta l’operazione:
myEVT_PERIOD_MODIFIED = wx.NewEventType()
EVT_PERIOD_MODIFIED = wx.PyEventBinder(myEVT_PERIOD_MODIFIED, 1)
Come si vede, la cosa più difficile è la scelta del nome. In genere per l’event type si preferisce uno schema del tipo
myEVT_*, per mimare gli event type standard wx.wxEVT_*. Sempre per consuetudine, il binder ha lo stesso nome
dell’event type, tolto il prefisso my.
wx.NewEventType() restituisce semplicemente un nuovo identificatore non ancora usato per gli event type predefiniti. Ne abbiamo bisogno subito per definire il binder, e poi ne avremo ancora bisogno per istanziare l’oggettoevento, come vedremo.
Il nostro binder dovrà essere una istanza di wx.PyEventBinder. Gli argomenti richiesti sono due: il primo è l’event
type appena creato, e il secondo indica quanti Id ci si aspetta di ricevere al momento di creare l’evento. Questo sembra
strano a prima vista, ma in realtà possiamo anche creare eventi “range” (come per esempio wx.EVT_MENU_RANGE)
che accettano due Id. Naturalmente, nella stragrande maggioranza dei casi abbiamo invece bisogno di un solo Id,
quindi basta passare 1.
7.2. Gli eventi: altre tecniche.
121
Appunti wxPython Documentation, Release 1
Scrivere un evento personalizzato.
Si tratta adesso di derivare da wx.PyCommandEvent, la classe che wxPython mette a disposizione, al posto di
wx.CommandEvent, per sovrascrivere i metodi virtuali. Esiste anche una wx.PyEvent se si vuole scrivere un
evento “non command”, ma questo è naturalmente più inconsueto.
Todo
una pagina sui pycontrols
Nella migliore delle ipotesi, basterà dichiarare la nostra sotto-classe (ma se è questo il vostro caso, allora c’è un modo
ancora più facile di procedere, che vedremo oltre).
Nel nostro caso, ne approfittiamo invece per aggiungere delle informazioni ulteriori che l’evento trasporterà con sé.
Qui per esempio definiamo due proprietà per comunicare se l’utente ha modificato l’anno oppure il periodo (non dico
che sia una cosa molto utile, ma è solo un esempio!):
class PeriodEvent(wx.PyCommandEvent):
def __init__(self, evtType, id, mod_period=False, mod_year=False):
wx.PyCommandEvent.__init__(self, evtType, id)
self.mod_period = mod_period
self.mod_year = mod_year
Come si vede, wx.PyCommandEvent accetta due argomenti: evtType è l’event type, e id è l’Id dell’oggetto da
cui parte l’evento. Gli altri due argomenti sono una nostra aggiunta. Avremmo anche potuto aggiungere dei getter e
setter per queste due proprietà, naturalmente.
Abbiamo lasciata “aperta” la possibilità di settare il parametro evtType al momento della creazione dell’istanza: in
genere è quello che si preferisce fare, perché si potrebbero creare diversi event type per lo stesso evento. Tuttavia, se
sappiamo che esisterà solo un event type possibile per il nostro evento, possiamo anche impostarlo direttamente nella
nostra classe:
class PeriodEvent(wx.PyCommandEvent): # versione alternativa
def __init__(self, id, mod_period=False, mod_year=False):
wx.PyCommandEvent.__init__(self, myEVT_PERIOD_MODIFIED, id)
self.mod_period = mod_period
self.mod_year = mod_year
Lanciare l’evento personalizzato.
Adesso si tratta di scegliere il momento giusto per lanciare dal nostro widget l’evento che abbiamo scritto. Siccome
vogliamo che l’evento parta nel momento in cui l’utente agisce su uno dei due elementi del widget, colleghiamo
normalmente i due eventi corrispondenti, e quindi creiamo il nostro evento nei callback:
# nell'__init__ di PeriodWidget aggiungiamo:
self.period.Bind(wx.EVT_COMBOBOX, self.on_changed)
self.year.Bind(wx.EVT_SPINCTRL, self.on_changed)
def on_changed(self, evt):
changed = evt.GetEventObject()
my_event = PeriodEvent(myEVT_PERIOD_MODIFIED, self.GetId(),
changed==self.period, changed==self.year)
my_event.SetEventObject(self)
self.GetEventHandler().ProcessEvent(my_event)
Abbiamo collegato entrambi gli elementi allo stesso callback: ci fidiamo di GetEventObject per recuperare
l’elemento che è stato modificato. La parte più interessante è la creazione dell’istanza di PeriodEvent: come visto
122
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
sopra, richiede due argomenti “obbligatori” (l’event type e l’Id del widget che lo sta generando), ai quali aggiungiamo
i nostri due argomenti “personalizzati”.
E’ anche utile impostare alcune proprietà dell’evento appena creato, prima di emetterlo. Nel nostro esempio impostiamo SetEventObject, per permettere al futuro callback che lo intercetterà di usare GetEventObject se lo
desidera.
Quindi, dobbiamo emettere l’evento. Il modo più consueto è rivolgersi all’handler dello stesso widget che lo sta
generando (self.GetEventHandler()) e chiedergli di processare immediatamente l’evento invocando direttamente ProcessEvent.
Si noti anche che, siccome nel callback non chiamiamo Skip, i due eventi originari smettono di propagarsi, come
desideriamo: d’ora in poi saranno sostituiti dal nostro evento personalizzato.
C’è un altro modo di mettere in moto il nostro evento, ed è usare la funzione globale wx.PostEvent. Nel nostro
caso, sarebbe:
wx.PostEvent(self.GetEventHandler(), my_event)
C’è una differenza minima ma importante tra i due metodi. ProcessEvent fa partire immediatamente l’evento,
mentre PostEvent lo mette in coda allo stack di eventi pendenti dell’handler. Nel nostro esempio non fa nessuna differenza, ma supponiamo invece di dover chiamare Skip nel callback, per esempio per permettere la
ricerca di gestori nelle sovraclassi. In questo caso, PostEvent farebbe partire il nostro evento soltanto dopo che
wx.EVT_COMBOBOX (o wx.EVT_SPINCTRL) sono stati intercettati dalle sovra-classi, il che è in genere quello che
vogliamo. Invece ProcessEvent infilerebbe il nostro evento prima di terminare di processare quelli originali. Il
risultato è che, se qualcuno intercetta il nostro evento, quel callback verrà eseguito in mezzo al nostro processo interno,
e in genere non è il comportamento corretto.
Per testare la differenza tra i due metodi, ecco una versione leggermente modificata del nostro esempio, che introduce
una catena di sovra-classi del wx.ComboBox:
from datetime import datetime
myEVT_PERIOD_MODIFIED = wx.NewEventType()
EVT_PERIOD_MODIFIED = wx.PyEventBinder(myEVT_PERIOD_MODIFIED, 1)
class PeriodEvent(wx.PyCommandEvent):
def __init__(self, evtType, id, mod_period=False, mod_year=False):
wx.PyCommandEvent.__init__(self, evtType, id)
self.mod_period = mod_period
self.mod_year = mod_year
class SuperCombo(wx.ComboBox):
def __init__(self, *a, **k):
wx.ComboBox.__init__(self, *a, **k)
self.Bind(wx.EVT_COMBOBOX, self.oncombo)
def oncombo(self, evt):
print 'sto lavorando nella sovra-classe'
evt.Skip()
class MyCombo(SuperCombo):
def __init__(self, *a, **k): SuperCombo.__init__(self, *a, **k)
class PeriodWidget(wx.Panel):
PERIODS = {'1 trimestre': ((1, 1), (3, 31)), '2 trimestre': ((4, 1), (6, 30)),
'3 trimestre': ((7, 1), (9, 30)), '4 trimestre': ((10, 1), (12, 31))}
def __init__(self, *a, **k):
wx.Panel.__init__(self, *a, **k)
self.period = MyCombo(self, choices=self.PERIODS.keys(),
7.2. Gli eventi: altre tecniche.
123
Appunti wxPython Documentation, Release 1
style=wx.CB_DROPDOWN|wx.CB_READONLY|wx.CB_SORT)
self.period.SetSelection(0)
self.year = wx.SpinCtrl(self, initial=2012, min=1950, max=2050)
s = wx.BoxSizer()
s.Add(self.period, 0, wx.EXPAND|wx.ALL, 5)
s.Add(self.year, 0, wx.EXPAND|wx.ALL, 5)
self.SetSizer(s)
s.Fit(self)
self.period.Bind(wx.EVT_COMBOBOX, self.on_changed)
self.year.Bind(wx.EVT_SPINCTRL, self.on_changed)
def on_changed(self, evt):
evt.Skip()
changed = evt.GetEventObject()
my_event = PeriodEvent(myEVT_PERIOD_MODIFIED, self.GetId(),
changed==self.period, changed==self.year)
my_event.SetEventObject(self)
# alternate tra questi due metodi, e scoprite la differenza:
# wx.PostEvent(self.GetEventHandler(), my_event)
self.GetEventHandler().ProcessEvent(my_event)
def GetValue(self):
start, end = self.PERIODS[self.period.GetStringSelection()]
year = self.year.GetValue()
return datetime(year, *start), datetime(year, *end)
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.period = PeriodWidget(p)
self.period.Bind(EVT_PERIOD_MODIFIED, self.on_period)
def on_period(self, evt):
print 'mod. periodo:', evt.mod_period, 'mod. anno:', evt.mod_year
print evt.GetEventObject().GetValue()
if __name__ == '__main__':
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Intercettare l’evento personalizzato.
L’esempio che abbiamo appena riportato illustra anche come si intercetta il nostro evento personalizzato. Non c’è
nulla di speciale da dire al riguardo. Il codice cliente deve usare Bind(EVT_PERIOD_MODIFIED, ...) come
farebbe con un qualsiasi altro binder wx.EVT_*.
Un modo più rapido di creare un evento.
Se non avete bisogno di definire una classe per il vostro evento, allora wx.lib.newevent vi mette a disposizione
una comoda scorciatoia per scavalvare le altre operazioni di routine. Tutto quello che occorre fare è:
124
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
PeriodEvent, EVT_PERIOD_MODIFIED = wx.lib.newevent.NewCommandEvent()
Questo vi restituisce in un colpo solo una classe già costruita, e un binder. La classe è già predisposta con il type event
corretto (che quindi non avete bisogno di conoscere). Quando volete creare l’istanza dell’evento, dovete solo passare
un Id corretto al costruttore. Nel nostro esempio, sarebbe quindi:
my_event = PeriodEvent(self.GetId())
Ovviamente, siccome PeriodEvent non è più una classe che abbiamo scritto noi stessi, non ha nessun
metodo/proprietà aggiuntiva (o almeno, non dovrebbe averne... ma poi siamo pur sempre programmatori Python...
un po’ di monkey patching non ci spaventa di certo!).
Quando vogliamo intercettare il nostro evento, possiamo usare il binder EVT_PERIOD_MODIFIED proprio come
prima.
Oltre a wx.lib.newevent.NewCommandEvent() esiste anche wx.lib.newevent.NewEvent() per
creare un evento “non command”.
7.3 Gli eventi: altre tecniche (seconda parte).
Dopo aver dedicato due pagine agli eventi, e una pagina ad alcune tecniche più inconsuete, siamo ancora ben lontani
dall’aver esaurito l’argomento!
In questa sezione affrontiamo alcuni aspetti ancora più esotici, che molto probabilmente non avrete mai bisogno di
utilizzare. Ma possono sempre servire a vantarsi con gli amici, naturalmente.
Ancora una volta, non si tratta di tecniche difficili da comprendere... ma neppure particolarmente facili: leggete ciò
che segue solo dopo aver letto e capito tutto quello che abbiamo visto finora sugli eventi. Una raccomandazione:
siete incoraggiati a sperimentare per conto vostro a partire da quello che vedrete qui. Tuttavia, giocando con questi
strumenti, è facile ottenere interfacce che si bloccano, non rispondono, vanno in crash. Siate preparati a uccidere il
processo di python responsabile del vostro programma. E verificate periodicamente se non sono rimasti processi di
python ancora in vita. Se qualcosa può andar storto, lo farà.
7.3.1 Filtri.
La possibilità di applicare filtri globali è probabilmente l’aspetto meno documentato e usato di tutto il meccanismo
degli eventi di wxPython.
Diciamo subito che l’intera macchina dei filtri si può far partire e arrestare a comando, chiamando
wx.App.SetCallFilterEvent(True) e wx.App.SetCallFilterEvent(False).
Quando il meccanismo dei filtri è attivo, ogni volta che un event handler deve processare un evento, per prima cosa
chiamerà il metodo wx.App.FilterEvent. Nella sua implementazione di default, FilterEvent non fa nulla e
restituisce subito -1. Voi però avete la possibilità di sovrascrivere questo metodo, e fargli eseguire del codice. Inoltre,
se FilterEvent restituisce qualcosa di diverso da -1, l’evento non verrà più processato oltre.
Più precisamente: FilterEvent deve restituire uno tra:
• -1, per dire “prosegui a processare l’evento” (stessa cosa che chiamare Skip);
• 0 per dire “non processare l’evento”;
• +1 per dire “considera l’evento già processato, non andare oltre”.
Fate molta attenzione, se restituite qualcosa di diverso da -1: FilterEvent interviene su qualsiasi evento, compresi
quelli che disegnano l’interfaccia: se non fate bene i vostri conti, vi ritroverete con il programma bloccato e incapace
di rispondere agli eventi.
7.3. Gli eventi: altre tecniche (seconda parte).
125
Appunti wxPython Documentation, Release 1
La chiamata a FilterEvent avviene immediatamente all’inizio della propagazione, prima di qualsiasi altra cosa
(siamo all’inzio di quella che abbiamo chiamato “fase 0” nella catena della propagazione, se ricordate). E questa
chiamata avviene per qualsiasi evento emesso dal vostro programma. Ecco un esempio minimo per rendere l’idea:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=((50,50)))
b.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt): print '\n\nCLIC\n\n'
class myApp(wx.App):
def OnInit(self):
self.SetCallFilterEvent(True)
return True
def FilterEvent(self, evt):
print 'filter!', evt.__class__.__name__
return -1
if __name__ == '__main__':
app = myApp(False)
Test(None).Show()
app.MainLoop()
Prima di far girare questo codice, prendetevi un momento per indovinare come funzionerà. Abbiamo sottoclassato
wx.App per chiamare SetCallFilterEvent(True) nel suo OnInit: in questo modo ci assicuriamo che
il filtro degli eventi sia sempre attivo fin dall’inizio. Quindi, abbiamo implementato il suo metodo FilterEvent.
Restituiamo comunque -1 in modo che tutti gli eventi verranno processati come al solito, ma prima facciamo qualcosa
di speciale (per adesso ci limitiamo a scrivere nello standard output).
L’effetto del nostro programma è piuttosto vistoso: FilterEvent interviene proprio su tutti gli eventi, compreso il
wx.EVT_UPDATE_UI che viene emesso al semplice passaggio del mouse, senza contare il ridimensionamento e lo
spostamento delle finestre, e anche gli occasionali wx.EVT_IDLE.
Perfino la documentazione di wxWidgets (il framework c++ sottostante) ci consiglia di usare FilterEvent con
giudizio, per evitare rallentamenti. In python, dove ogni chiamata di funzione è particolarmente dispendiosa, è davvero
difficile consigliare l’uso di questa tecnica. E’ per questo che wxPython introduce SetCallFilterEvent (che non
esiste nelle wx). Di default, il meccanismo dei filtri è disabilitato, e siamo noi a doverlo attivare se proprio ci serve.
Come minimo, sarebbe meglio attivarlo solo al momento di utilizzarlo, e disattivarlo di nuovo appena possibile.
Note: In wxWidgets, le cose sono ancora più complicate. FilterEvent è un metodo originariamente messo a
disposizione da una apposita classe mix-in, che si chiama EventFilter. In questo modo, costruendo un widget
personalizzato che deriva anche da EventFilter è possibile dotarlo di un suo metodo FilterEvent da sovrascrivere, ed è quindi possibile attivare più filtri contemporaneamente. La wx.App dispone di un suo FilterEvent di
default, perché deriva già “per natura” da EventFilter. In wxPython tutto questo non è supportato: resta solo il
FilterEvent della wx.App.
E quindi? A che cosa potrebbe servirci questo filtro globale? In alcune circostanze è utile a monitorare l’attività
dell’utente in modo trasversale a tutta l’applicazione (detta così sa un po’ di spionaggio, vero?). Per esempio, considerate questa wx.App:
126
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
class myApp(wx.App):
def OnInit(self):
self.SetCallFilterEvent(True)
self.last_used = datetime.datetime.now()
return True
def FilterEvent(self, evt):
if evt.GetEventType() in (wx.EVT_LEFT_UP.typeId, wx.EVT_KEY_UP.typeId):
self.last_used = datetime.datetime.now()
return -1
Come vedete, tiene traccia dell’ultima volta che l’utente ha usato il mouse o la tastiera, ed è quindi possibile calcolare
da quanto tempo è inattivo.
Naturalmente sarebbe possibile ottenere lo stesso effetto collegando con Bind ogni singolo elemento dell’interfaccia
a un callback dedicato a fare questo lavoro (o anche, come vedremo tra poco, usando un handler personalizzato). Ma
chiaramente in questo modo si fa prima.
A partire da questa idea, non è difficile scrivere, per esempio, una wx.App che traccia in un apposito log tutti i tasti
premuti dall’utente... e così via.
7.3.2 Blocchi.
Quando abbiamo detto che i filtri sono l’aspetto meno conosciuto e usato del meccanismo degli eventi, volevamo dire:
a eccezione dei blocchi.
Un wx.EventBlocker è in grado di bloccare temporaneamente qualsiasi evento (o anche solo alcuni eventi selezionati) diretto a uno specifico widget.
Potete passare al costruttore -1 (ovvero wx.EVT_ANY) per dire “blocca tutti gli eventi”, oppure il typeId di un
evento specifico. Se vi serve aggiungere altri eventi da bloccare, potete farlo in seguito chiamando il suo metodo
Block.
Il blocco resta attivo fin quando l’istanza di wx.EventBlocker non viene fisicamente distrutta (in qualunque modo
possiate marcare un oggetto per essere reclamato dal garbage collector in python: uscendo dallo “scope” in cui è stata
definita la variabile, o in definitiva anche con del). A quel punto, gli eventi tornano a essere gestiti come di consueto.
Ecco un esempio pratico:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.ToggleButton(p, -1, 'blocca/sblocca', pos=((50,50)))
b.Bind(wx.EVT_TOGGLEBUTTON, self.onclic)
self.blockbutton = wx.Button(p, -1, 'posso bloccarmi!', pos=((50, 80)))
self.blockbutton.Bind(wx.EVT_BUTTON, self.on_blockbutton_clic)
def on_blockbutton_clic(self, evt):
print 'evidentemente adesso sto funzionando...'
def onclic(self, evt):
if evt.IsChecked():
self.block = wx.EventBlocker(self.blockbutton, -1)
else:
del self.block
if __name__ == '__main__':
7.3. Gli eventi: altre tecniche (seconda parte).
127
Appunti wxPython Documentation, Release 1
app = wx.App(False)
Test(None).Show()
app.MainLoop()
Il primo pulsante in alto attiva e disattiva un blocco che agisce sul secondo pulsante. Il blocco è totale: come vedete,
il pulsante smette di rispondere a tutti gli eventi (anche il mouseover, per esempio). Se modificate il blocco scrivendo:
self.block = wx.EventBlocker(self.blockbutton, wx.EVT_BUTTON.typeId)
vedrete che il pulsante blocca solo il wx.EVT_BUTTON, ma risponde ancora agli altri eventi.
Ancora una volta possiamo domandarci: a che cosa serve questo meccanismo? Ovviamente, se vogliamo solo disabilitare un widget, Enable() è tutto quel che serve. Un blocco, tuttavia, può essere all’occorrenza più selettivo,
fermando esattamente gli eventi che ci servono. Oppure: se vogliamo disattivare l’esecuzione di un segmento di codice
a seconda delle circostanze, potremmo mettere un po’ di logica in più nel callback. Tuttavia un blocco può aiutarci a
mantenere il codice più pulito.
7.3.3 Categorie.
Nella nostra rassegna dei concetti più oscuri e meno documentati sugli eventi, non potevano mancare le categorie.
Detto in breve, ogni evento appartiene a una categoria, a scelta tra:
• wx.wxEVT_CATEGORY_UI: questa categoria raggruppa gli eventi generati da aggiornamenti della gui (ridimensionamenti, spostamenti, etc.);
• wx.wxEVT_CATEGORY_USER_INPUT: questi sono gli eventi tipicamente generati dell’utente (pressione di
tasti, clic del mouse...);
• wx.wxEVT_CATEGORY_NATIVE_EVENTS:
definita
come
l’unione
(wx.wxEVT_CATEGORY_UI|wx.wxEVT_CATEGORY_USER_INPUT);
delle
due
precedenti
• wx.wxEVT_CATEGORY_TIMER: qui stanno i wx.TimerEvent;
• wx.wxEVT_CATEGORY_THREAD: i wx.ThreadEvent (gli eventi usati per comunicare tra i thread);
• wx.wxEVT_CATEGORY_SOCKET: a questa categoria appartengono solo i “socket event”, che wxPyhton non
supporta;
• wx.wxEVT_CATEGORY_CLIPBOARD: gli eventi della clipboard (copia e incolla, drag and drop);
• wx.wxEVT_CATEGORY_ALL: questa categoria raggruppa tutte le altre;
• wx.wxEVT_CATEGORY_UNKNOWN: aggiunta recentemente come fallback.
Todo
una pagina sui thread , una pagina sui timer , una pagina sulla clipboard
Potete sapere a quale categoria appartiene un evento con wx.Event.GetEventCategory. Per esempio, in un
callback:
def mycallback(self, evt):
print evt.GetEventCategory()
Se state creando un evento personalizzato e avete bisogno di impostare la sua categoria, potete sovrascrivere
GetEventCategory per restituire quello che vi sembra più opportuno.
128
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Il valore di queste costanti, come avrete probabilmente indovinato, è calibrato per poterle combinare in una bitmask,
per cui per esempio wx.wxEVT_CATEGORY_ALL^wx.wxEVT_CATEGORY_USER_INPUT vuol dire “tutto tranne
i comandi dell’utente”.
Le categorie degli eventi sono usate praticamente solo per filtrare che cosa processare in una chiamata a YieldFor,
e pertanto riprenderemo il discorso quando parleremo di questi argomenti. Siccome YieldFor è usato da wxWidget
in alcune occasioni, anche queste categorie hanno una funzione interna. Inoltre, naturalmente, potreste usarle per
implementare qualche filtro di vostro molto specializzato... se vi viene in mente un’idea.
7.3.4 Handler personalizzati.
Sappiamo già che wx.EvtHandler è la classe-base dedicata alla gestione degli eventi. E sappiamo anche che tutta
la gerarchia dei widget (a partire dalla classe madre wx.Window) deriva da wx.EvtHandler, e di conseguenza
tutti i widget hanno in sé la capacità di gestire gli eventi.
Questa architettura di default basta nella vita di tutti i giorni. Ma volendo, possiamo andare oltre...
Parlando della propagazione degli eventi, abbiamo fatto cenno alla possbilità che un widget abbia addirittura uno stack
di handler pronti intervenire uno dopo l’altro per gestire l’evento.
Todo
una pagina sui pycontrols (cfr paragrafo seguente)
In effetti, abbiamo la possibilità di creare handler personalizzati (derivando da wx.PyEvtHandler, la classe che
wxPython mette a disposizione per sovrascrivere i metodi virtuali), e aggiungerli allo stack degli handler di un determinato widget. In questa sezione vedremo come fare, ma prima un avvertimento: si tratta di strumenti che wxPython
mette a disposizione “traducendoli” dal framework c++ sottostante, ma che nel mondo python hanno meno utilità pratica. Leggete i paragrafi seguenti senza badare troppo all’utilità pratica: vedremo che tutto questo vi potrebbe tornare
utile, in certi scenari.
Per cominciare, non c’è nulla di magico in un handler personalizzato:
wx.PyEvtHandler. Per esempio:
è una semplice sotto-classe di
class MyEvtHandler(wx.PyEvtHandler):
def __init__(self):
wx.PyEvtHandler.__init__(self)
self.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
print "sono un clic gestito nell'handler personalizzato"
evt.Skip()
Questo è un handler che gestisce un wx.EVT_BUTTON nel modo che ormai vi è familiare (questo è un buon momento
per ricordarsi che, dopo tutto, Bind è un metodo di wx.EvtHandler). Per usarlo, non dobbiamo fare altro che
istanziarlo, e assegnarlo a un widget. In teoria potremmo assegnarlo a un widget qualsiasi, ma siccome il suo scopo è
gestire un wx.EVT_BUTTON, ha senso assegnarlo a un pulsante (o a un panel che contiene dei pulsanti, magari). Per
esempio:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=((50, 50)))
handler = MyEvtHandler()
b.PushEventHandler(handler)
7.3. Gli eventi: altre tecniche (seconda parte).
129
Appunti wxPython Documentation, Release 1
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
Tutto qui. PushEventHandler “spinge” il nostro handler personalizzato in cima allo stack degli handler del
pulsante. Il pulsante acquisisce il nostro handler, e quindi acquisisce la sua caratteristica risposta all’evento
wx.EVT_BUTTON.
Se adesso fate girare questo codice, vi accorgerete che avete ottenuto un comportamento del tutto analogo alla normale
gestione di un evento da parte di un pulsante. La differenza è che adesso il callback on_clic è definito nella classe
dell’handler, e non nella classe del frame come di consueto.
Note: questo è il motivo principale per cui esiste questo meccanismo nel framework c++. Il punto è che in wxWidgets
non si possono definire callback all’esterno della classe in cui risiedono i widget: quindi scrivere un handler separato
e agganciarlo a un widget è l’unico modo per intervenire “dal di fuori”. In python, ovviamente, questa feature non ci
impressiona più di tanto: le funzioni e i metodi sono “first class object”, e si possono passare a Bind come parametri
normali. In wxPython un callback può essere un metodo di un’altra classe, o una funzione esterna stand-alone, senza
alcuna difficoltà.
Naturalmente è possibile anche collegare un “normale” callback al pulsante:
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=((50, 50)))
b.PushEventHandler(MyEvtHandler())
b.Bind(wx.EVT_BUTTON, self.onclic)
def onclic(self, evt): print 'un clic "normale"'
Potete aggiungere diversi handler allo stack in successione, e in questo modo potete ottenere una riposta più “componibile”, “modulare” all’evento.
Note: questa è una possibilità effettivamente nuova, nel senso che invece non è possibile collegare con Bind diversi
callback per lo stesso evento ad un widget. Naturalmente però nessuno vieta di chiamare una serie di funzioni esterne
in successione dallo stesso callback, ottenendo lo stesso effetto di modularità.
Lo stack degli handler è appunto uno stack: l’ultimo handler inserito è il primo che si occuperà dell’evento. Notate che
l’handler predefinito (ovvero il widget stesso) in questo modo resta sempre in fondo allo stack, ed è quindi l’ultimo a
occuparsi dell’evento (prima cioè che l’evento si propaghi oltre il widget).
Questa caratteristica ci permette di determinare con precisione l’ordine in cui i callback devono intervenire. Può essere
importante, in alcuni scenari: esploriamo meglio uno di questi scenari in una ricetta separata.
Altre operazioni con gli handler.
L’operazione opposta a PushEventHandler è PopEventHandler, che toglie l’ultimo handler inserito nello
stack (e lo restituisce come risultato). Non potete togliere anche l’handler predefinito (cioè il widget stesso). Se
cercate di farlo, wxPython solleva una wx._core.PyAssertionError. Quindi tenete conto degli handler man
mano che li aggiungete, oppure preparatevi a intercettare questa eccezione:
130
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
while True: # svuoto completamente lo stack
try: mywidget.PopEventHandler()
except wx._core.PyAssertionError: break
PopEventHandler restituisce un riferimento all’handler rimosso: se lo conservate in una variabile potete riutilizzarlo in seguito, se volete.
Potete anche manipolare lo stack usando SetNextHandler e SetPreviousHandler. Ricordatevi però che
wxPython mantiene una lista doppia di riferimenti alla catena degli handler: di conseguenza, se impostate B come
successivo di A, dovete anche impostare A come precedente di B:
handler_A.SetNextHandler(handler_B)
handler_B.SetPreviousHandler(handler_A)
Impostare a None sia il precedente sia il successore di un handler, equivale a rimuoverlo dalla catena: dovete però
fare attenzione a “ripararla”. Per evitare complicazioni, se volete rimuovere un handler intermedio della catena, usate
piuttosto handler.Unlink(): questo ripara anche automaticamente la catena.
Potete conoscere il successore di un handler chiamando handler.GetNextHandler() (che restituisce None
se l’handler è l’ultimo della catena). Analogamente, handler.GetPreviousHandler vi restituisce l’handler
precedente. Se entrambi questi metodi restituiscono None, vuol dire che l’handler è “sganciato” dalla catena: potete
anche sapere se un handler è attualmente “sganciato” chiamando, più rapidamente, handler.IsUnlinked().
Inoltre, ricordatevi la possibilità di scollegare un evento da un handler con handler.Unbind() (che funziona proprio come Bind ma al contrario), e la possibilità di disconnettere completamente un handler chiamando
handler.SetEvtHandlerEnabled().
Infine, abbiamo già fatto cenno a handler.ProcessEvent() (e alla quasi equivalente funzione globale
wx.PostEvent()) che torna utile per far processare immediatamente a un handler un certo evento (tipicamente
un evento personalizzato creato sul momento, ma si può naturalmente usare anche con gli eventi “ordinari”). Questo
metodo, insieme con AddPendingEvent e QueueEvent, torna utile anche nel caso specifico in cui gli eventi
personalizzati sono creati, lanciati e processati come mezzo di comunicazione tra diversi thread.
Todo
una pagina sui thread.
A che cosa servono gli handler personalizzati?
E quindi, a che cosa servono questi oggetti? Come abbiamo già spiegato, sono importanti nel mondo c++, ma hanno
una utilità pratica ridotta in wxPython. Potete senz’altro usarli per aumentare la scomposizione e la fattorizzazione
del codice, se volete usare strumenti wxPython (invece delle normali tecniche python). Occasionalmente potreste
voler aggiungere degli handler “plug-in” a runtime (ma di solito potete ottenere lo stesso effetto chiamando Bind
e Unbind a runtime, o mantenendo un callback fisso e da quello applicando qualche tipo di “strategy pattern” per
selezionare le funzioni da chiamare a seconda dei casi).
Uno scenario in cui invece potrebbero tornarvi utili, è quando avete bisogno di controllare l’ordine esatto in cui vengono eseguiti i callback. Quando avete molteplici callback, l’ordine di esecuzione può dipendere da come intercettate
l’evento (potete collegarlo al widget, oppure a un suo parent). Se questa incertezza non va bene per quello che dovete
fare, allora una buona soluzione è far gestire l’evento da un handler personalizzato, e poi inserire l’handler nello stack
del widget (e ripetere all’occorrenza con altri callback in altri handler). In questo modo avete sempre il controllo dello
stack degli handler, e sapete con esattezza in che ordine verranno eseguiti i callback.
Per illustrare un esempio concreto di questo scenario, abbiamo scritto una ricetta in cui vogliamo che un pulsante,
quando viene premuto, per prima cosa chieda la password all’utente prima di procedere a elaborare ogni azione
successiva.
7.3. Gli eventi: altre tecniche (seconda parte).
131
Appunti wxPython Documentation, Release 1
7.3.5 Un esempio finale per la propagazione degli eventi (aggiornato).
Riprendiamo infine l’esempio riassuntivo che avevamo fatto al termine del discorso sulla propagazione degli eventi,
aggiornandolo con le tecniche viste in questa pagina. Di nuovo, fate girare il codice e osservate in che ordine sono
eseguiti i callback:
class MyEvtHandler(wx.PyEvtHandler):
def __init__(self, name):
wx.PyEvtHandler.__init__(self)
self.name = name
self.Bind(wx.EVT_BUTTON, self.onclic)
def onclic(self, evt):
print "clic dall'handler", self.name
evt.Skip()
class MyButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_BUTTON, self.onclic)
def onclic(self, evt):
print 'clic dalla classe Mybutton'
evt.Skip()
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
panel = wx.Panel(self)
button = MyButton(panel, -1, 'clic', pos=((50,50)))
button.PushEventHandler(MyEvtHandler('Alice'))
button.PushEventHandler(MyEvtHandler('Bob'))
button.Bind(wx.EVT_BUTTON, self.onclic_button)
panel.Bind(wx.EVT_BUTTON, self.onclic_panel, button)
self.Bind(wx.EVT_BUTTON, self.onclic_frame, button)
def onclic_button(self, evt):
print 'clic dal button'
evt.Skip()
def onclic_panel(self, evt):
print 'clic dal panel'
evt.Skip()
def onclic_frame(self, evt):
print 'clic dal frame'
evt.Skip()
class MyApp(wx.App):
def OnInit(self):
self.Bind(wx.EVT_BUTTON, self.onclic)
self.SetCallFilterEvent(True)
return True
132
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
def FilterEvent(self, evt):
evt.Skip()
if evt.GetEventType() == wx.EVT_BUTTON.typeId:
print 'clic dal filtro'
return -1
def onclic(self, evt):
print 'clic dalla wx.App'
evt.Skip()
if __name__ == '__main__':
app = MyApp(False)
Test(None).Show()
app.MainLoop()
Abbiamo finito di parlare degli eventi in wxPython? Naturalmente no! Come abbiamo accennato sopra, parliamo
di eventi anche nella pagina dedicata ai thread. Ma soprattutto, ci restano ancora molte cose da scoprire sugli event
loop... Ma sarà l’argomento di una pagina separata!
7.4 Il loop degli eventi.
Abbiamo già dedicato molte pagine agli eventi: ma non abbiamo ancora mai indagato sul cuore nascosto della nostra
applicazione wxPython: il luogo dove gli eventi arrivano e vengono accoppiati ai loro handler, che a loro volta si
incaricheranno di cercare i callback corrispondenti.
Stiamo parlando del loop degli eventi.
Note: Tutto ciò che leggerete in questa pagina è... molto sperimentale. Metteremo le mani sotto il cofano del modello
a eventi di wxPython, e finiremo per modificare comportamenti di default molto sensati, meditati e testati. Per giunta,
qui la documentazione scarseggia: anche in rete si trovano solo indicazioni vaghe e incomplete (ogni segnalazione
in contrario è benvenuta!). Alcune delle informazioni qui raccolte in pratica si trovano solo leggendo il (labirintico)
codice sorgente di wxWidgets. Se volete usare queste tecniche, vi raccomandiamo di prendere queste note solo come
orientamente iniziale, di sperimentare e approfondire molto per conto vostro, e di testare a lungo il codice che scrivete.
7.4.1 Il loop degli eventi e il main loop dell’applicazione.
Per prima cosa dobbiamo chiarire una possibile confusione di termini. Quando abbiamo parlato della wx.App,
abbiamo descritto il suo MainLoop come “un grande ciclo while True senza fine che si occupa di gestire gli
eventi”. In realtà, questa definizione confonde main loop e loop degli eventi (“event loop”, detto anche loop dei
messaggi, “message loop”): al momento era una piccola inesattezza senza conseguenze, ma adesso dobbiamo fare più
attenzione.
wx.App.MainLoop, in effetti, è semplicemente un metodo di wx.App: che viene invocato di solito una volta
sola, all’inizio della vita della vostra applicazione wxPython. Al suo interno, crea e avvia un event loop per gestire gli eventi man mano che appaiono. E quindi, che cosa è di preciso un event loop? E’ un’istanza della classe
wx.GUIEventLoop.
Note:
Una piccola ulteriore confusione terminologica: GUIEventLoop deriva da EventLoopBase, ma
senza estenderne le funzionalità. La documentazione di GUIEventLoop rimanda semplicemente a quella di
EventLoopBase. Nelle vecchie versioni di wxPython, GUIEventLoop era però chiamata EventLoop: per
questa ragione, vedete ancora moltissimo codice in giro che usa EventLoop. Nessun problema, però: EventLoop
7.4. Il loop degli eventi.
133
Appunti wxPython Documentation, Release 1
esiste ancora per retrocompatibilità, ed è ormai solo un alias di GUIEventLoop. In queste note useremo il nome
“moderno”.
Processare manualmente gli eventi.
Naturalmente voi potete sovrascrivere wx.App.MainLoop: ma dovete impegnarvi a creare voi stessi e “far girare”
un loop, altrimenti la vostra applicazione non potrà rispondere agli eventi. Il vostro compito minimo potrebbe essere:
class MyApp(wx.App):
def MainLoop(self):
loop = wx.GUIEventLoop()
loop.Run()
Questa però è solo una perdita di tempo: vi siete limitati a replicare quello che l’implementazione standard di
MainLoop farebbe in ogni caso. Anzi, in questo modo avete perso il meccanismo che esce da MainLoop e chiude la vostra applicazione quando non ci sono più finestre top level aperte: se provate a far girare una gui qualsiasi
con questa MyApp, vedrete che wxPython non termina mai (preparatevi a usare ctrl-c nella shell, o a terminare il
processo in qualche modo).
Tuttavia questo è almeno un inizio: abbiamo imparato a creare un event loop, e ad avviarlo con Run. A questo
proposito, va detto che Run si prende cura di fare il lavoro al posto vostro, ma è proprio l’opposto di quel che stiamo
cercando: noi vogliamo gestire gli eventi “manualmente”! Facciamo un passo avanti:
class MyApp(wx.App):
def MainLoop(self):
loop = wx.GUIEventLoop()
while True:
while loop.Pending():
loop.Dispatch()
loop.ProcessIdle()
Ecco che cominciamo a prendere il controllo: abbiamo abbandonato Run e facciamo tutto noi. Il segreto è chiamare Dispatch, metodo che attende l’arrivo di un evento, e si occupa di accoppiarlo al suo primo handler. Siccome Dispatch è bloccante (aspetta fin quando non c’è un evento da gestire), in genere conviene accoppiarlo con
Pending, che ci dice se ci sono eventi in coda in attesa di essere processati. Quando abbiamo finito di gestire gli
eventi in coda chiamiamo ProcessIdle, che emette un wx.EVT_IDLE per segnalare che il loop è attualmente
disoccupato (avremmo potuto ottenere lo stesso effetto con la funzione globale wx.WakeUpIdle). Emettere di tanto
in tanto un wx.EVT_IDLE è necessario, perché in wxPython ci sono dei gestori di default che intercettano questo
evento e ne approfittano per fare operazioni di servizio nei tempi morti.
Dobbiamo ancora occuparci del meccanismo di chiusura dell’applicazione: qui possiamo inventarci strategie diverse,
a seconda delle nostre esigenze specifiche. Ma anche un approccio brutale può bastare:
class MyApp(wx.App):
def MainLoop(self):
loop = wx.GUIEventLoop()
while True:
while loop.Pending():
loop.Dispatch()
if self.GetTopWindow() is None:
wx.Exit()
loop.ProcessIdle()
Chiamare wx.Exit è un modo raffinato abbastanza da permettere l’esecuzione di eventuale codice in
wx.App.OnExit, quindi le buone maniere sono salve. Ma a dire il vero, non ha comunque molta importanza.
Siccome stiamo facendo tutto “a mano”, alla peggio potremmo chiamare direttamente anche OnExit e/o qualsiasi
funzione di cleanup necessaria, prima di chiudere.
134
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Piuttosto, è il test GetTopWindow() is None che potrebbe essere fragile in certi corner-case. Abbiamo visto
mille modi in cui una finestra potrebbe non chiudersi davvero, e altri mille modi in cui si possono manipolare le
finestre top-level. Tuttavia, se mantenete un minimo di organizzazione nel vostro codice, non dovrebbe essere difficile
stabilire quando effettivamente è ora di spegnere le luci e chiudere il locale.
Infine, ancora una raffinatezza: abbiamo organizzato le nostre chiamate nell’ordine giusto, in modo che wx.Exit
possa intervenire solo quando non ci sono più eventi da processare: non si sa mai.
Un’altra tecnica per fare la stessa cosa, sarebbe naturalmente quella di usare un flag:
class MyApp(wx.App):
def OnInit(self):
self.time_to_quit = False
return True
def MainLoop(self):
loop = wx.GUIEventLoop()
while not self.time_to_quit:
while loop.Pending():
loop.Dispatch()
loop.ProcessIdle()
wx.Exit()
In questo modo evitiamo di chiamare GetTopWindow a ogni ciclo, e ci guadagnamo in velocità. Quando volete
uscire, dovete ricordarvi di settare il flag: per esempio, intercettando il wx.EVT_CLOSE della finestra principale:
def on_close(self, evt):
wx.GetApp().time_to_quit = True
Questo vi assicura di uscire dall’applicazione appena esaurita la coda corrente degli eventi da processare.
Infine, ancora un dettaglio di cui forse vi sarete già accorti, se avete... prestato orecchio alla ventola del vostro
computer! Il problema è che wxPython, quando è al comando, si preoccupa di dosare il consumo della vostra
cpu: ma il nostro while True senza alcuna moderazione finisce per occupare il processore quasi al 100% (solo
ProcessIdle rallenta un po’ le cose). Prima di prosciugare le risorse del nostro computer per niente, sarà meglio
correre ai ripari:
# se non volete importare time, usate wx.MilliSleep()
import time
class MyApp(wx.App):
def OnInit(self):
self.time_to_quit = False
return True
def MainLoop(self):
loop = wx.GUIEventLoop()
while not self.time_to_quit:
while loop.Pending():
loop.Dispatch()
loop.ProcessIdle()
time.sleep(0.1) # un po' di sollievo per la cpu
# wx.MilliSleep(10)
wx.Exit()
Altre cose da sapere sul loop degli eventi.
Un loop degli eventi (wx.GUIEventLoop) ha alcuni metodi che possono tornare utili, oltre a quelli che abbiamo già
visto. In primo luogo, IsRunning permette di sapere se il loop è al momento quello attivo (come vedremo presto,
7.4. Il loop degli eventi.
135
Appunti wxPython Documentation, Release 1
ci possono essere diversi event loop allo stesso tempo... complicazioni in vista!). Se avete avviato il loop con Run,
potete chiamare Exit per uscire dal loop (questo non distrugge l’istanza del loop, naturalmente): sarà meglio subito
avviare un altro loop, altrimenti la vostra applicazione resterà sospesa.
wx.App.GetMainLoop() restituisce un riferimento al loop degli eventi “principale”, ossia quello che è stato creato
da wxPython in wx.App.MainLoop. Va da sé che, se avete scritto un loop per conto vostro, allora GetMainLoop
resituirà None... poco male: basta conservare un riferimento all’istanza del vostro loop e recuperarla all’occorrenza.
Similmente, anche wx.GUIEventLoop.IsMain() restituisce True solo se il loop è stato creato da wxPyhton in
fase di inizializzazione.
Infine, anticipiamo qui il concetto di “attivazione” dei loop, che riprenderemo tra poco, parlando degli event loop
secondari (qualche ripetizione sarà inevitabile, a quel punto): i metodi che si occupano di questo aspetto sono
wx.GUIEventLoop.GetActive e wx.GUIEventLoop.SetActive. In realtà l’attivazione di un loop è una
questione poco più che simbolica. Quando chiamate SetActive, l’unico cambiamento che avviene è l’impostazione
di un flag interno.
Tuttavia, SetActive chiama contestualmente anche wx.App.OnEventLoopEnter, che è un altro degli hook
della wx.App che potete sovrascrivere. A differenza di OnInit che abbiamo già visto, OnEventLoopEnter può
essere sfruttato per eseguire codice che ha bisogno di un loop già funzionante (ovvero, che ha bisogno di postare degli
eventi nella coda). Si noti inoltre che OnEventLoopEnter viene chiamato ogni volta che si entra in un nuovo loop
degli eventi (come vedremo presto, possono esserci più loop nella vita di un’applicazione wxPython). Se vi serve
eseguire codice solo una volta all’inizio, potete testare se il loop è IsMain. Simmetricamente, quando uscite da un
loop degli eventi (chiamando Exit come vedremo tra poco), viene chiamato wx.App.OnEventLoopExit che
potete sovrascrivere.
In definitiva, “attivare” un loop può essere completamente inutile. Conviene però sempre farlo, per uniformità e
perché wxPython “se lo aspetta” (nel senso che altre parti del codice potrebbero testare GetActive e prendere delle
decisioni di conseguenza). Si può attivare un loop appena creato chiamando SetActive prima di avviarlo (Run).
Tuttavia la cosa migliore è servirsi dell’apposito helper wx.EventLoopActivator, della cui funzione parleremo
tra poco, a proposito dei loop secondari.
Per quanto riguarda GetActive, ricordiamo infine che si tratta di un metodo di classe, e che quindi va usato semplicemente così:
wx.GUIEventLoop.GetActive() # restituisce l'istanza del loop attivo
7.4.2 Yield e i suoi compagni.
A proposito di loop degli eventi, un discorso a parte merita Yield. Intanto diciamo che questo è un metodo di
wx.GUIEventLoop (la classe madre degli event loop), ma è anche gemello della funzione globale wx.Yield (che
però è ormai deprecata) e del metodo wx.App.Yield: potete usarli indifferentemente.
La funzione di Yield è di passare subito a processare i successivi eventi in coda, se ce ne sono. Questo è utile quando
la risposta a un evento (callback) rischia di metterci molto tempo e bloccare la gui.
Un esempio chiarirà meglio:
def long_op(): time.sleep(0.1)
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b1 = wx.Button(p, -1, 'clic', pos=((50, 50)))
b2 = wx.Button(p, -1, 'clic', pos=((50, 80)))
b1.Bind(wx.EVT_BUTTON, self.clic_b1)
b2.Bind(wx.EVT_BUTTON, self.clic_b2)
136
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
def clic_b1(self, evt):
evt.GetEventObject().Enable(False)
for i in xrange(100):
wx.GetApp().Yield()
long_op()
evt.GetEventObject().Enable(True)
def clic_b2(self, evt):
print 'clic'
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
L’efficacia di Yield dipende da quanto spesso riuscite a chiamarlo, ovvero da quanto riuscite a “spezzettare” la
vostra operazione bloccante. Nel nostro esempio, potete sperimentare con diverse durate di long_op per vedere fino
a quando la gui risponde in modo accettabile.
Se riuscite a segmentare adeguatamente l’operazione bloccante, Yield potrebbe essere un primo tentativo per integrare task secondari in modo “asincrono” (senza ricorrere a thread separati), o addirittura per integrare loop esterni
dentro wxPython (un problema di design piuttosto comune).
Todo
una pagina sui thread
Usando Yield, occorre ricordare che è vietato chiamarlo ricorsivamente: per questo, nel nostro esempio, abbiamo
dovuto disabilitare il pulsante, mentre l’operazione è in corso. Provate a eliminare questa precauzione, e cliccare due
volte in successione sul pulsante: otterrete un PyAssertionError. C’è anche un altro modo per evitare questo
problema: chiamare Yield con il parametro onlyIfNeeded=True (è False per default. Notate anche che
Yeld(True) è disponibile anche sotto forma della funzione globale wx.YeldIfNeeded()). Provate a togliere
le righe di codice che dis/abilitano il pulsante, e sostituire la chiamata con wx.GetApp().Yield(True). Non
otterrete più nessun errore, ma naturalmente questo non vuole ancora dire che siete a posto: nel nostro caso, chiamare
ricorsivamente l’operazione bloccante genera un sovraccarico sufficiente per bloccare comunque la gui, e Yield non
può farci nulla.
Questo ci insegna la lezione più importante: Yield può consentire di sbloccare la gui mentre un’operazione altrimenti bloccante viene processata in background: ma non è detto che l’utente farà buon uso di questa possibilità. E’
importante capire quali sono le attività che l’utente non può svolgere finché dura l’operazione lunga, e disabilitare
menu e pulsanti per evitare inconsistenze.
Per questa ragione, talvolta è preferibile usare invece wx.App.SafeYield (che è anche disponibile come funzione
globale wx.SafeYield, ma non come metodo di wx.GUIEventLoop). Questo metodo si comporta come Yield,
ma vuole due argomenti: il secondo è il già noto onlyIfNeeded (con la differenza che questa volta è obbligatorio).
Il primo argomento, invece, può essere None: in questo caso SafeYield blocca tutte le interazioni con l’interfaccia
prima di procedere con l’operazione, e le sblocca di nuovo alla fine. Se invece passate come primo argomento un
riferimento a un widget (un’intera finestra, se volete), allora solo le interazioni con questo widget resteranno attive,
permettendo quindi un utilizzo limitato finché dura l’operazione “bloccante”.
Se la protezione di SafeYield non vi basta, potete implementare una logica più raffinata per decidere se, cosa e
quando bloccare l’interfaccia, testando il metodo wx.GUIEventLoop.IsYielding. Questo metodo restituisce
True solo se è chiamato dall’interno di un Yield (o YieldFor, che discuteremo tra poco). Per rendervene conto,
nell’esempio di sopra provate a sostituire print ’clic’ nel callback del secondo pulsante con:
7.4. Il loop degli eventi.
137
Appunti wxPython Documentation, Release 1
print wx.GetApp().GetMainLoop().IsYielding()
Adesso, se cliccate sul secondo pulsante mentre il primo “sta lavorando”, otterrete True.
Un’altra implementazione raffinata di Yield è YieldFor, che si comporta come Yield con
onlyIfNeeded=True, e inoltre accetta come parametro una bitmask di categorie di eventi da processare
subito: quindi, solo gli eventi che non appartengono a quelle categorie verranno ritardati. E’ facile vederlo in azione
nel nostro esempio, basta sostituire la chiamata a Yield con:
wx.GetApp().GetMainLoop().YieldFor(wx.wxEVT_CATEGORY_NATIVE_EVENTS)
Questa soluzione (la più frequente) permette di processare subito gli eventi “locali” importanti, lasciando fuori quelli
che provengono da thread o altre fonti “ritardabili” (nel caso del nostro esempio non ci sarà ovviamente nessun effetto
visibile).
Ricordatevi
che
non
è
possibile
processare
separatamente
wx.wxEVT_CATEGORY_UI
e
wx.wxEVT_CATEGORY_USER_INPUT con YieldFor (si è visto che portava a troppe complicazioni):
bisogna per forza usare il raggruppamento wx.wxEVT_CATEGORY_NATIVE_EVENTS. Notate anche che
YieldFor(wx.wxEVT_CATEGORY_ALL) è equivalente semplicemente a Yield(onlyIfNeeded=True).
Ricordatevi infine che YieldFor è disponibile solo come metodo di wx.GUIEventLoop.
7.4.3 Loop secondari.
Finora abbiamo parlato sempre e solo di “un” event loop, ma la realtà è più complicata. Nella vita di un’applicazione
wxPython è possibile avere più loop compresenti: wxPython mantiene uno stack di loop degli eventi “innestati” uno
dentro l’altro: solo il loop in cima allo stack è attivo. Quando si esce da un loop, il controllo ritorna al loop precedente,
e così via.
Anche senza nessun intervento da parte vostra, questo avviene per esempio tutte le volte che mostrate un dialogo
“modale” (ossia un dialgo che disattiva tutti gli altri componenti della vostra applicazione finché non lo chiudete). Per
implementare un dialogo modale, wxPython crea e avvia un nuovo loop degli eventi, che finisce quindi in cima allo
stack. Quando il dialogo è distrutto, il nuovo loop termina e viene espulso dallo stack, facendo tornare il controllo al
loop precedente. Naturalmente nulla vieta che nel dialogo modale ci sia, per esempio, un pulsante che apre un nuovo
dialogo modale: lo stack dei loop può crescere in teoria all’infinito.
Facciamo una prova veloce:
class TestDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
b1 = wx.Button(self, -1, 'apri dialogo', pos=((50, 50)))
b1.Bind(wx.EVT_BUTTON, self.clic_b1)
b2 = wx.Button(self, -1, 'print evtloop', pos=((50, 80)))
b2.Bind(wx.EVT_BUTTON, self.clic_b2)
def clic_b1(self, evt): TestDialog(self).ShowModal()
def clic_b2(self, evt): print wx.GUIEventLoop.GetActive()
if __name__ == '__main__':
app = wx.App(False)
TestDialog(None).Show()
app.MainLoop()
Ogni volta che cliccate sul primo pulsante, aprite un nuovo dialogo modale “annidato”. Cliccando sul secondo pulsante, noterete che il loop attivo è di volta in volta diverso (confrontate gli indirizzi di memoria per vederlo).
138
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Tenete conto che, al di là della chiamata esplicita a ShowModal, wxPython potrebbe mostrarvi molti dialoghi modali
“di routine” durante la normale vita di un’applicazione. Di conseguenza, lo stack dei loop è uno scenario frequente
dietro le quinte.
In pratica, quanto è importante sapere queste cose? Dipende dal vostro scenario: di solito, anche quando sovrascrivete
wx.App.MainLoop e gestite gli eventi “a mano”, il comportamento standard dei dialoghi modali è comunque quello
che volete. Non vi importa se gli eventi prodotti dal dialogo tornano a essere gestiti in modo autonomo da wxPython
per un po’.
Se però lo ritenete opportuno, potete creare e distruggere anche i loop annidati “secondari”. In questo caso, dovreste
ricordarvi di ripristinare (riattivare) il loop precedente quando uscite da quello attuale. Per aiutarvi in questo compito,
vi conviene usare wx.EventLoopActivator: si tratta di una classe speciale che attiva un nuovo loop e mantiene
un riferimento a quello vecchio. Quando distruggete l’istanza di wx.EventLoopActivator, automaticamente
verrà ripristinato il loop precedente. Un esempio chiarirà forse meglio:
class TestDialog(wx.Dialog):
def __init__(self, *a, **k):
wx.Dialog.__init__(self, *a, **k)
self.loop = wx.GUIEventLoop()
self.active = wx.EventLoopActivator(self.loop)
self.Bind(wx.EVT_CLOSE, self.on_close)
print 'loop attivo nel dialogo:', wx.GUIEventLoop.GetActive()
def on_close(self, evt):
self.loop.Exit()
del self.active
self.Destroy()
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'apri dialogo', pos=((50, 50)))
b.Bind(wx.EVT_BUTTON, self.onclic)
def onclic(self, evt):
print 'loop attivo:', wx.GUIEventLoop.GetActive()
TestDialog(self).Show()
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
Notate prima di tutto che abbiamo rinunciato a ShowModal per mostrare il dialogo (altrimenti wxPython avrebbe
semplicemente aperto un altro loop dentro il nostro). Se volete disattivare il resto dell’interfaccia, dovete farlo a mano.
L’uso di wx.EventLoopActivator è mostrato nel nostro TestDialog: all’inizio apriamo un nuovo loop, e
quando il dialogo viene chiuso, distruggiamo anche l’istanza dell’attivatore, ripristinando il loop precedente. Notate
però che wx.EventLoopActivator, al momento della sua distruzione, non chiama Exit sul loop, quindi dobbiamo pensarci noi stessi (simmetricamente, chiamare Exit sul loop non basta a “disattivarlo”! Occorre distruggere il
wx.EventLoopActivator che lo ha attivato).
E’ importante uscire dal loop con Exit? Lo è abbastanza: come abbiamo già detto qui sopra, wx.App mette
a disposizione due hook specifici: OnEventLoopEnter e OnEventLoopExit. Il primo è chiamato da
wx.GUIEventLoop.SetActive, e il secondo da wx.GUIEventLoop.OnExit (che a sua volta dipende proprio da wx.GUIEventLoop.Exit). Potete sovrascrivere questi metodi per eseguire codice ogni volta che entrate e
uscite da un loop degli eventi. Per vedere come funzionano, potete sostituire l’App generica dell’esempio precedente
7.4. Il loop degli eventi.
139
Appunti wxPython Documentation, Release 1
con questa:
class MyApp(wx.App):
def OnEventLoopEnter(self):
print 'entro nel loop', wx.GUIEventLoop.GetActive()
def OnEventLoopExit(self):
print 'esco dal loop', wx.GUIEventLoop.GetActive()
if __name__ == '__main__':
app = MyApp(False)
Test(None).Show()
app.MainLoop()
Adesso notate che la “partita doppia” dei messaggi provenienti da MyApp e da TestDialog coincide. Ma se
togliete la chiamata a self.loop.Exit(), vedrete che MyApp.OnEventLoopExit non viene più eseguito
quando chiudete il dialogo. Naturalmente, potrebbe essere quello che volete in certe occasioni: l’importante è capire
che ri-attivare un loop non comporta automaticamente uscire dal loop precedente.
Infine, un suggerimento: se intendete usare sul serio queste tecniche, probabilmente vi conviene mantenere uno stack
(una semplice lista python) delle istanze di wx.EventLoopActivator man mano che le create, e poi distruggerle
semplicemente pop-andole fuori dallo stack.
7.4.4 Creare loop degli eventi personalizzati.
Negli ultimi esempi qui sopra, vi sarete accorti che, per giostrare tra diversi loop, abbiamo di nuovo rinunciato a
occuparci di gestire personalmente gli eventi. Tutto ciò che abbiamo fatto è stato istanziare dei wx.GUIEventLoop
e attivarli in successione, replicando peraltro quello che wxPython farebbe normalmente.
Se vi serve questa tecnica di gestire diversi loop, ma (ovviamente) volete anche personalizzare il modo in cui questi
loop gestiscono gli eventi, siete arrivati al punto in cui dovete sotto-classare wx.GUIEventLoop.
Il metodo che vi serve sovrascrivere è Run, all’interno del quale wxPython fa girare il ciclo infinito che già conosciamo. Ecco un esempio minimale, che dovrebbe ormai esservi familiare: abbiamo solo spostato il cuore delle operazioni dentro una sottoclasse di wx.GUIEventLoop.Run:
import time
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic')
b.Bind(wx.EVT_BUTTON, self.onclic)
self.Bind(wx.EVT_CLOSE, self.onclose)
def onclic(self, evt):
print 'la gui risponde agli eventi!'
def onclose(self, evt):
wx.GetApp().stop_app()
class MyEvtLoop(wx.GUIEventLoop):
def __init__(self):
self.time_to_quit = False
wx.GUIEventLoop.__init__(self)
140
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
def Run(self):
active = wx.EventLoopActivator(self)
while not self.time_to_quit:
while self.Pending():
self.Dispatch()
self.ProcessIdle()
time.sleep(0.1)
self.Exit()
del active
class MyApp(wx.App):
def OnInit(self):
self.time_to_quit = False
return True
def MainLoop(self):
self.loop = MyEvtLoop()
self.loop.Run()
wx.Exit()
def stop_app(self):
self.loop.time_to_quit = True
if __name__ == '__main__':
app = MyApp(False)
MyFrame(None).Show()
app.MainLoop()
In questo esempio abbiamo predisposto le cose nel wx.App.MainLoop per avere un solo loop per tutta la vita
dell’applicazione (chiamiamo wx.Exit() subito dopo che il loop ha smesso di funzionare). Ma naturalmente potete
organizzare le cose in modo da attivare più loop il successione, a seconda delle vostre esigenze.
7.4.5 Perché manipolare il loop degli eventi?
Abbiamo lavorato a lungo per comprendere il meccanismo dei loop degli eventi in wxPython, ma alla fine: quando è
necessario utilizzare queste tecniche?
Fortunatamente, quasi mai. Pochissime nozioni tra quelle contenute in questa pagina potrebbero trovar posto negli
scenari comuni: in pratica, vale la pena di tenere sottomano solo wx.Yield.
Altre idee possono tornarvi utili solo se sviluppate cose molto esotiche: certe “applicazioni dentro applicazioni” (un
editor visuale, per esempio) potrebbero aver bisogno di event loop gestiti separatamente per consentire all’applicazione
“figlia” di funzionare senza intaccare la “madre”. Questa è la tecnica, per esempio, che permette a una shell IPython
di integrare al suo interno una gui wxPython.
In teoria, come parte di un’architettura Model-Controller-View, si potrebbe voler “spacchettare” il loop degli eventi
per farlo gestire da un Controller esterno a wxPython. Ma è un approccio inutilmente complicato, almeno in linea
di principio: per fortuna wxPython offre degli agganci molto più comodi. Postare eventi personalizzati nella coda, o
perfino usare un handler personalizzato sono tecniche molto più pratiche e agevoli per stabilire una comunicazione
tra la gui e il Model sottostante (senza contare, naturalmente, la possibilità di un sistema di messaggistica estraneo a
wxPython, come Publisher/Subscriber).
Todo
una pagina su mcv
7.4. Il loop degli eventi.
141
Appunti wxPython Documentation, Release 1
In generale, prima di smontare il loop degli eventi, conviene provare tutte le altre soluzioni: quelle descritte in questa
pagina sono tecniche complicate e possono portare a errori difficili da scoprire, comportamenti non cross-compatibili,
etc.
Infine, un caso specifico e perfino abbastanza comune in cui potreste voler mettere le mani sotto il cofano, è quando
dovete affiancare a wxPython un altro loop degli eventi. La logica di molte applicazioni si fonda su qualche tipo di
ciclo infinito; anche molti grandi framework esistenti fanno uso di qualche tipo di event loop, da Twisted a Pygame,
da Gevent a Tornado. Effettivamente, per integrare wxPython in queste architetture, in certi casi potrebbe essere
necessario accedere direttamente al loop degli eventi. Ma non è l’unica strada, e anzi, talvolta non ce n’è proprio
bisogno: dedichiamo una pagina separata ad approfondire questi scenari.
7.5 Come integrare event loop esterni in wxPython.
Affrontiamo in questa pagina un argomento trasversale ad altri che abbiamo coperto in queste note: di conseguenza qui
ci limitiamo a fornire delle indicazioni specifiche ma sintetiche, e rimandiamo alle altre pagine per il quadro generale.
Il pre-requisito, naturalmente, è di conoscere tutto quello che abbiamo scritto sulla gestione degli eventi in wxPython.
7.5.1 Il problema.
wxPython, come tutti i gui framework, è organizzato intorno al suo “loop degli eventi”, in pratica un while True
infinito che resta attivo per tutto il ciclo di vita della vostra applicazione. Di conseguenza, in un’applicazione wxPython, wx.App.MainLoop è l’alfa e l’omega del vostro programma: per tutto il tempo restate bloccati lì dentro.
Questo naturalmente rende disagevole far funzionare wxPython insieme ad altri componenti che hanno un proprio
“main loop”.
Un esempio concreto.
Immaginate di aver scritto il motore di un gioco “in tempo reale”: al suo cuore, è un ciclo senza fine che controlla
lo stato del mondo, decide le mosse di giocatori governati dell’AI, e tra le altre cose accetta gli input del giocatore
umano... che che voi volete appunto gestire con un’interfaccia grafica wxPython. Questo scenario pone anche il
problema interessante di come scambiare informazioni avanti e indietro tra wxPython e il motore del gioco.
Ma, più alla base, il dilemma è come integrare un ciclo infinito del tipo:
class Game(object):
# etc. etc.
def run(self):
while True:
self.check_world_status()
self.update_resources()
for player in self.players:
player.move()
# etc. etc.
in una applicazione wxPython che viene pur sempre avviata con:
app = wx.App()
app.MainLoop() # e qui restiamo bloccati!
Dopo aver lanciato il suo main loop, wxPython “prende il comando”, e non è possibile far partire anche il main loop
del nostro gioco (non nello stesso thread, per lo meno).
142
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Una soluzione radicale: usare solo wxPython.
Una soluzione è re-implementare tutta la logica del gioco “dal punto di vista” di wxPython: l’utente invia gli input
agendo sull’interfaccia; i callback gestiscono la risposta appropriata; e in sostanza il “main loop” di tutta l’applicazione
diventa il wx.App.MainLoop gestito da wxPython.
Bisogna rinunciare del tutto al main loop Game.run del nostro gioco, e chiamare i singoli passaggi da dentro wxPython. Le azioni da compiere in risposta all’input dell’utente verranno eseguite nei callback. Le azioni da eseguire a
ciclo continuo potrebbero essere pianificate con dei timer, per esempio.
Bisogna dire che questo approccio, in generale, viola il principio di separazione tra le parti tipico per esempio del
pattern Model-Controller-View. E’ però una questione discutibile. In un certo senso, nessun gui framework rispetta
il pattern MCV in modo rigoroso: avete già rinunciato a MCV dalla riga import wx in testa al vostro codice. In
wxPython, come abbiamo visto tutti i widget derivano sia da wx.Window sia da wx.Event: e per questo recitano
allo stesso tempo la parte della View e quella del Controller (ovvero, sono capaci di rispondere agli eventi).
Usare un gui framework non significa rinunciare a un sano principio di separazione delle parti. Ma bisogna aver
presente che wxPython (al pari di altri framework analoghi) non riguarda solo la parte “view”, ma mette anche a
disposizione gli strumenti per gestire (almeno in parte) il “controller” dell’applicazione. Bisogna giocare secondo le
regole di wxPython, e adattare MCV di conseguenza. Nel nostro caso, come abbiamo detto, vorrebbe dire in pratica
lasciare inalterate le varie routine di Game (check_world_status etc.), e chiamarle dall’interno di wxPython.
Todo
una pagina su MCV.
Tuttavia, anche lasciando da parte le perplessità relative a MCV, questo approccio potrebbe essere sgradevole da usare
quando il main loop “ospite” è complesso, o è comunque già stato scritto e non intendiamo rinunciarvi.
E poi ci sono anche altri componenti già esistenti, “a eventi” o comunque dotati di qualche tipo di loop, che forniscono
servizi di gestione e coordinamento tra logica di business e logica di presentazione, e che potremmo dover integrare in
wxPython. Per esempio framework asincroni come Twisted, o librerie più leggere come Gevent, motori di rendering
come PyGame, e molti altri ancora. Naturalmente non possiamo metterci a “smontare” il main loop di questi grandi
framework: per integrarli, dobbiamo usare altre strategie.
Un approccio sbagliato: un ciclo while True.
Quello che non potete fare, naturalmente, è chiamare il vostro “main loop” ospite da qualche parte dentro
l’applicazione wxPython. Per esempio, immaginate questo scenario: voi avviate normalmente la wx.App, e presentate una finestra con un pulsante. Quando l’utente fa clic sul pulsante, parte il motore del gioco. Questo vorrebbe
dire, nel callback collegato al pulsante, scrivere quacosa come:
def on_clic(self, evt):
game = Game()
game.run()
In teoria, nessun problema. In pratica però, siccome run è un ciclo infinito, noi restiamo bloccati per sempre nel callback on_clic: wxPython aspetterà per sempre l’uscita da quel callback, il loop degli eventi si fermerà, e l’interfaccia
si bloccherà (in compenso però il motore di gioco, invisibile, continuerà a funzionare benissimo!)
L’approccio corretto: eseguire uno step alla volta.
Le strategie per risolvere questo problema sono molte, ma quasi tutte si fondano su una premessa indispensabile:
dovete essere in grado di spezzettare il vostro main loop ospite in operazioni di durata “abbastanza piccola” da poter
essere intercalate alle operazioni della gui, senza causare rallentamenti.
7.5. Come integrare event loop esterni in wxPython.
143
Appunti wxPython Documentation, Release 1
Il modo più consueto consiste nell’eseguire, ogni volta, solo un singolo ciclo (uno “step”) del vostro while True.
Ma se questo dovesse essere ancora troppo lungo, bisognerebbe frammentare lo step in operazioni più piccole.
Per riprendere l’esempio del nostro motore di gioco, dovreste poter effettuare questa piccola modifica:
class Game(object):
# etc. etc.
def make_step(self):
self.check_world_status()
self.update_resources()
for player in self.players:
player.move()
# etc. etc.
# e quindi (ma ormai non ci servirà più...)
def run(self):
while True:
self.make_step()
Nel mondo reale, forse, operazioni come check_world_status etc., saranno ancora troppo complesse e/o intercalate da azioni dell’utente: dovranno essere ulteriormente segmentate.
Ma se avete la possibilità di frammentare in qualche modo il vostro loop in operazioni abbastanza brevi, allora siete
pronti a implementare una delle soluzioni che seguono (altrimenti, forse potete ugualmente provare con la n. 5, che
rovescia i termini del problema e vi propone di segmentare il loop di wxPython, invece).
7.5.2 Soluzione 1: usare Yield.
Probabilmente il modo più semplice per venirne a capo è usare wx.App.Yield e i suoi cugini. Per esempio,
ricordate il ciclo while True che poco fa bloccava completamente wxPython? Ecco, basta modificarlo così:
def on_clic(self, evt):
game = Game()
while True:
wx.GetApp().Yield()
game.make_step()
In linea di principio, basta questo a togliervi il pensiero. A ogni ciclo, Yield raccoglie gli eventi che si sono accumulati nella coda, e li processa prima di cedere di nuovo la parola al vostro main loop ospite.
Nella vita reale forse non vorrete ospitare il main loop secondario proprio all’interno di un callback. Ma potete
inserirlo dove preferite, il principio non cambia: naturalmente dovete avviarlo quando il wx.App.MainLoop è
partito, altrimenti non funzionerà.
Vantaggi: è davvero molto facile da usare. Per i casi semplici, potrebbe davvero “funzionare e basta”.
Svantaggi: Yield e compagni godono in generale di cattiva fama tra i programmatori wxWidgets/wxPyhton. Yield
è fragile: può andar bene per sbloccare la gui in situazioni occasionali e temporanee, ma fargli tenere in piedi la
responsività della vostra applicazione lungo tutto il suo ciclo di vita... molti vi diranno che probabilmente è un po’
troppo. Intanto dovete cautelarvi contro la possibilità che l’utente faccia qualcosa di “illogico”, come già detto. Ma
quando le applicazioni diventano più complesse, e soprattutto quando i messaggi tra la gui e il “ciclo ospite” si
fanno più intrecciati ed è cruciali risolverli nell’ordine corretto... le cose potrebbero diventare difficili da gestire e
da debuggare. In essenza Yield scombina l’ordine naturale degli eventi nella coda. I problemi di “rientri” (ossia,
un evento chiamato una seconda volta prima che il loop abbia avuto la possibilità di gestire la prima chiamata) sono
sempre in agguato.
144
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
7.5.3 Soluzione 2: catturare wx.EVT_IDLE.
wx.EVT_IDLE è un evento che wxPython emette per segnalare che “non ha niente da fare” in quel momento (ovvero,
che la coda degli eventi da gestire è temporaneamente vuota). Abbiamo già detto che il sistema ne approfitta per
compiere operazioni di routine dietro le quinte.
Ma possiamo approfittarne anche noi. Basta intercettare l’evento, e far compiere al nostro loop ospite uno “step”.
Potete catturare il wx.EVT_IDLE dove volete, naturalmente: anche nella wx.App:
class MyApp(wx.App):
def OnInit(self):
self.Bind(wx.EVT_IDLE, self.on_idle)
self.game = Game()
def on_idle(self, evt):
self.game.make_step()
evt.RequestMore()
La chiamata finale a wx.IdleEvent.RequestMore è una finezza necessaria. Lo scenario che vogliamo evitare
è questo: la gui non ha niente da fare, e quindi emette un wx.EVT_IDLE; noi lo intercettiamo, ed eseguiamo uno
“step” del loop ospite. Finito lo step, può succedere che l’utente continui a non fare assolutamente nulla; ma ormai
wxPyhton ha già emesso il suo wx.EVT_IDLE, e dal suo punto di vista non è ancora cambiato niente... quindi tutto
si blocca senza emettere più segnali, fino a quando l’utente riprende in mano il mouse! Per evitare questo intoppo,
RequestMore segnala semplicemente la necessità di emettere un nuovo wx.EVT_IDLE (anche la funzione globale
wx.WakeUpIdle ha un effetto simile). Se nel frattempo l’utente è rimasto inattivo, l’evento sarà di nuovo intercettato
da noi. Se invece l’utente ha prodotto qualche segnale, allora prenderà la precedenza, e verrà processato.
Vantaggi: questo sistema è davvero molto performante (rispetto a un timer, per esempio). E’ probabilmente il sistema
migliore da tentare come prima cosa, se non ci sono ragioni particolari in contrario.
Svantaggi: non c’è garanzia che wx.EVT_IDLE venga emesso con regolarità. Se le attività della gui tengono il
sistema molto occupato, è possibile che la frequenza del ciclo ospite rallenti troppo per le vostre necessità. Per esempio,
non dovreste usare questo sistema e al contempo intercettare altri eventi molto frequenti (wx.EVT_UPDATE_UI...)
e impegnarli con callback “pesanti”. Se intercettate wx.EVT_IDLE anche per altri scopi, dovete stare attenti a
chiamare Skip() altrimenti il callback della wx.App non verrà mai attivato. Ma anche in casi apparentemente
normali, l’emissione di wx.EVT_IDLE potrebbe interrompersi per un po’: per esempio, se l’utente apre un menu o
un dialogo modale, la gui resta attiva finché non viene richiuso.
7.5.4 Soluzione 3: usare un timer.
Todo
una pagina sui timer.
Usare un timer è un’opzione tutto sommato facile da implementare. Basta avviarlo e catturare il relativo evento
periodico:
def __init__(....)
# .....
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
self.timer.Start(10) # millisecondi
def on_timer(self, evt):
# wx.GetApp().Yield()
7.5. Come integrare event loop esterni in wxPython.
145
Appunti wxPython Documentation, Release 1
self.game.make_step()
self.timer.Start(10)
Notate che forse dovremmo comunque chiamare Yield, se la gui reagisce troppo lentamente: ma conviene prima
testare il programma senza Yield, ed eventualmente inserirlo dopo.
Vantaggi: la frequenza dei timer è regolabile, e questo (insieme a un sano uso di Yield) permette di bilanciare
esattamente le esigenze di entrambi i loop. Inoltre i timer sono più affidabili dei wx.EVT_IDLE: non c’è il rischio
che si interrompano quando l’utente apre un menu, per esempio.
Svantaggi: i timer sono sempre più lenti dei wx.EVT_IDLE. Se entrambi i vostri loop tengono molto impegnata la cpu, potrebbe darvi noia il sovraccarico ulteriore dovuto al timer. In questo caso, forse vi convengono i
wx.EVT_IDLE.
7.5.5 Soluzione 4: gestire manualmente gli eventi.
E’ la soluzione più complessa, ma anche la più flessibile. Abbiamo parlato a lungo dei loop degli eventi di wxPython
e delle tecniche per manipolarlo. Molte cose si possono personalizzare se intervenite a quel livello. Ma la tecnica
standard che si può applicare al nostro esempio è questa:
class MyApp(wx.App):
def OnInit(self):
self.time_to_quit = False
self.game = Game()
def MainLoop(self):
loop = wx.GUIEventLoop()
active = wx.EventLoopActivator(loop)
while not self.time_to_quit:
self.game.make_step()
while loop.Pending():
loop.Dispatch()
# wx.MilliSleep(10)
loop.ProcessIdle()
wx.Exit()
Essenzialmente, si tratta di processare alternativamente la coda di eventi di wxPython e uno step del loop ospite, in uno
stesso ciclo. E’ possibile regolare il ritmo con cui vengono emessi wx.EVT_IDLE chiamando wx.MilliSleep
(anche per evitare un utilizzo eccessivo della cpu, in molti casi).
Una cosa importante da notare è che abbiamo il controllo completo dell’ordine in cui verranno gestiti tutti i segnali
provenienti da entrambi i loop.
Questo richiede un piccolo approfondimento. E’ normale infatti che i due loop interagiscano tra loro scambiandosi
“messaggi”: potrebbero essere oggetti di un sistema Publish/Subscriber, oppure banalmente degli eventi wxPython
(sappiamo già come emettere eventi personalizzati: ora possiamo usarli come sistema di messaggistica tra i due loop,
se vogliamo). Ebbene, con questa tecnica, ogni segnale emesso dal loop ospite in direzione di wxPython, verrà raccolto
e processato immediatamente dopo, nello stesso ciclo. Allo stesso modo, se la parte wxPython vuole inviare un segnale
al loop ospite, questo sarà processato nel ciclo immediatamente successivo, e non oltre.
Todo
una pagina su MVC | una pagina su pub/sub.
146
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
7.5.6 Soluzione 5: rovesciare il rapporto tra loop principale e ospite.
Invece di gestire il loop ospite dall’interno di wxPython, possiamo fare il contrario:
class Game(object):
def __init__(self):
# ....
self.bootstrap_wxPython()
def make_step(self):
self.check_world_status()
self.update_resources()
for player in self.players:
player.move()
# etc. etc.
def bootstrap_wxPython(self):
self.wxApp = MyApp(False)
self.wxLoop = wx.GUIEventLoop()
active = wx.EventLoopActivator(self.wxLoop)
def stop_wxPython(self):
wx.Exit()
def make_wxStep(self, loop):
while loop.Pending():
loop.Dispatch()
# wx.MilliSleep(10)
loop.ProcessIdle()
def run(self):
while True:
self.make_step()
self.make_wxStep(self.wxLoop)
game = Game()
game.run()
Se guardate questo esempio dopo molto tempo passato nella logica di wxPython, la sensazione sarà più o meno come
guidare in Inghilterra. E’ opportuna qualche spiegazione in più.
Prima di tutto, in una logica Model-Controller-View la nostra classe Game adesso fa un po’ la parte del Model e
un po’ quella del Controller. Forse vi converrà dividere le funzioni in due classi separate (ma non necessariamente!
MCV non è una religione, dopo tutto). In bootstrap_wxPython creiamo la wx.App e anche il loop degli eventi.
Qui stiamo sottintendendo che in MyApp.OnInit ci sia il codice necessario per mostrare la finestra principale
(altrimenti, poco male: basta crearla e mostrarla direttamente in bootstrap_wxPython). Quindi, alla fine di
bootstap_wxPython, l’utente vede l’interfaccia sullo schermo, ma niente è ancora attivo.
Fin qui siamo arrivati alla riga game = Game(). Immediatamente dopo, però, viene eseguito game.run() e tutto
si mette in moto. Il metodo run esegue alternativamente, in un ciclo infinito, uno step del nostro motore di gioco, e
uno step dell’interfaccia wxPython.
Più precisamente, uno step di wxPython (make_wxStep) corrisponde alla consueta sequenza di gestione degli eventi
a cui siamo abituati (emissione di wx.EVT_IDLE compresa).
Naturalmente a un certo punto bisognerà uscire dal programma. Potete scegliere la strategia che preferite: per esempio,
ci sarà un flag che provoca il break dal ciclo infinito di run. Una volta smesso di processare gli eventi wxPython,
tuttavia, l’interfaccia resterà sempre visibile ma inattiva. Ancora una volta dovrà essere la classe Game a intervenire,
chiamando al momento opportuno stop_wxPython.
7.5. Come integrare event loop esterni in wxPython.
147
Appunti wxPython Documentation, Release 1
7.5.7 Soluzione 6: usare un thread separato.
Le soluzioni viste fin qui sono asincrone: alternano i due loop nell’ambito di un solo thread di esecuzione. Ma
naturalmente potete anche provare a far vivere tutto il loop ospite in un thread separato.
Ai thread in wxPython dedicheremo una pagina apposita, e avremo modo di discutere pro e contro. Nell’attesa, eccovi
la versione breve. I thread sono sempre un argomento controverso: in generale tutti li sconsigliano, e con molte buone
ragioni. Nella pratica però spesso sono una soluzione comoda e a portata di mano... almeno finché non scappano di
mano. In python i thread sono particolarmente facili da usare, e in wxPython sono addirittura banali, finché vi tenete
sul sicuro e seguite una raccomandazione fondamentale.
Questa raccomandazione è di non eseguire mai chiamate che pprovengono da da un thread secondario e che modificano
lo stato dell’interfaccia. In wxPython è obbligatorio che tutto ciò che riguarda la gui sia eseguito nel thread principale
(quello in cui vive la wx.App).
Per “modificare lo stato dell’interfaccia” basta veramente poco: non potete, per esempio, impostare il valore di una
casella di testo da un thread secondario. Questo in pratica vi lascia con ben poche opzioni: per fortuna però, in nove
casi su dieci, l’opzione giusta è anche la più facile da capire e usare.
Basta far passare tutte le comunicazioni dal thread secondario al thread principale attraverso wx.CallAfter. Questa
funzione globale è semplicemente un wrapper thread-safe intorno alle chiamate di funzione wxPython. Detto in poche
parole, se da un thread secondario chiamate qualcosa come:
main_frame.some_textctrl.SetValue('hello')
la vostra applicazione rischia di disintegrarsi. Ma se invece chiamate:
wx.CallAfter(main_frame.some_textctrl.SetValue, 'hello')
tutto funzionerà come per incanto. Davvero: in nove casi su dieci vi basta sapere questo per fare multithreading in
wxPython. Se però inciampate senza saperlo nel decimo caso, allora siete nei guai.
Anche se in teoria è facile mettere il loop ospite in un processo separato, di rado questa è una buona idea. In genere,
a ben vedere usare i thread diventa solo una versione più complicata di qualche soluzione che abbiamo già visto.
Per esempio, molto spesso il loop ospite deve aggiornare la gui a intervalli regolari. Potete metterlo in un thread
secondario, e poi mandare messaggi al thread principale usando wx.CallAfter con regolarità (o postando eventi
nella coda con wx.PostEvent, l’altra tecnica standard in queste situazioni). Ma tutto questo si può fare lo stesso,
mantenendo i due loop nello stesso thread, e usando invece un timer. Inoltre, raramente framework complessi, che
hanno molte ramificate interazioni con il resto del vostro sistema operativo, funzionano bene in un thread secondario.
Fatte queste precisazioni, il nostro “motore di gioco” potrebbe in effetti essere avviato in un thread separato:
import threading
import time
import wx
class Game(object):
def __init__(self): self.players=('A', 'B', 'C')
def check_world_status(self): return 'controllo lo stato'
def update_resources(self): return 'aggiorno le risorse'
def move(self, player): return 'muovo il giocatore '+player
class ControllerThread(threading.Thread):
def __init__(self, gui_frame):
threading.Thread.__init__(self)
self.time_to_quit = False
self.gui_frame = gui_frame
self.game = Game()
def run(self):
148
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
while True:
if self.time_to_quit: break
wx.CallAfter(self.gui_frame.update_gui,
self.game.check_world_status())
time.sleep(2)
if self.time_to_quit: break
wx.CallAfter(self.gui_frame.update_gui,
self.game.update_resources())
time.sleep(2)
for player in self.game.players:
if self.time_to_quit: break
wx.CallAfter(self.gui_frame.update_gui,
self.game.move(player))
time.sleep(1)
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=((50, 50)))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.text = wx.TextCtrl(p, pos=((50, 80)), size=((200, -1)))
self.Bind(wx.EVT_CLOSE, self.on_close)
self.game_thread = ControllerThread(self)
self.game_thread.start()
def on_clic(self, evt):
self.text.SetValue('rispondo agli eventi!')
def on_close(self, evt):
self.game_thread.time_to_quit = True
dlg = wx.ProgressDialog("Chiusura", "Un attimo prego...",
maximum = 50, parent=self,
style = wx.PD_APP_MODAL)
for i in xrange(50):
time.sleep(.1)
dlg.Update(i)
dlg.Destroy()
evt.Skip()
def update_gui(self, msg):
self.text.SetValue(msg)
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
E’ un tentativo molto rozzo, perché abbiamo cercato di modificare il meno possibile l’esempio originario, ma può
bastare a rendere l’idea. Abbiamo spostato il main loop del gioco dentro una classe “controller” per comodità. Abbiamo usato time.sleep dentro il main loop per simulare una certa complessità, e soprattutto per lasciarvi il tempo
di vedere i successivi aggiornamenti della casella di testo.
Questo genera un problema imprevisto al momento di chiudere la finestra: il motore di gioco potrebbe trovarsi in un
punto in cui non ha ancora eseguito il controllo sul flag self.time_to_quit, e quindi cerca di scrivere in una
casella di testo che ormai è stata distrutta. Eventi come questo in wxPython sollevano un PyDeadObjectError,
7.5. Come integrare event loop esterni in wxPython.
149
Appunti wxPython Documentation, Release 1
che dobbiamo evitare. Per questo motivo abbiamo introdotto il check if self.time_to_quit: break ogni
volta che potevamo, ma restano sempre dei “buchi” di almeno 2 secondi. Abbiamo infine risolto ritardando la chiusura
del frame principale, mentre mostriamo un wx.ProgressDialog.
E’ una soluzione provvisoria a un problema che nel mondo reale, almeno in questi termini, non si presenterà: il vostro
main loop non resterà mai bloccato così a lungo. Tuttavia è un problema interessante, ed è un esempio dei grattacapi
che potreste dover affrontare: CallAfter programma un aggiornamento futuro della gui, che però nel frattempo
potrebbe assumere uno stato incompatibile con la situazione com’era al momento della chiamata a CallAfter.
In conclusione, è possibile in molti casi gestire un main loop secondario “alla pari” in un thread separato, anche se
difficilmente vale la pena. D’altro canto, i thread possono essere utili per altri compiti di routine, per esempio eseguire
operazioni di lunga durata in background senza bloccare la gui. Ma discuteremo meglio di questi aspetti quando
parleremo di thread.
Todo
una pagina sui thread: accorciare tutto questo paragrafo di conseguenza.
7.5.8 Integrare altri framework in wxPython.
Le tecniche viste fino a questo punto possono anche aiutarvi a usare wxPython insieme ad altri framework dotati di un
proprio main loop.
A dire il vero, alcuni framework mettono a disposizione delle apposite api per l’integrazione con wxPython. In altri
casi, ci sono comunque delle ricette già collaudate. Prima di mettersi a sperimentare soluzioni fatte in casa, conviene
sempre documentarsi. Vediamo di seguito qualche caso interessante.
wxPython e IPython.
IPython mette a disposizione degli hook per integrare nella sua shell gui fatte con wxPython o altri framework. La
documentazione relativa è molto chiara. In pratica, se in una shell IPython scrivete:
%gui wx
IPython avvia per voi una wx.App di cui gestisce il main loop, lasciandovi la libertà di disegnare interattivamente la
gui.
Queste capacità sono esposte anche sotto forma di api, cosa che vi permette di scrivere gui wxPython integrate in
IPython. Come ricorda anche la documentazione, in questo caso dovete però fare attenzione a non avviare voi stessi il
wx.App.MainLoop, perché questo è un compito da lasciare allo hook di IPython. Potete comunque sottoclassare e
istanziare wx.App, e quindi utilizzare i vari OnInit, OnExit etc. come di consueto.
Vale in ogni caso la pena di dare un’occhiata ai tre moduli che implementano questa funzionalità: al netto delle
necessarie astrazioni per supportare diversi gui framework da collegare e disconnettere a runtime, troverete alcuni
spunti interessanti che riprendono le tecniche viste finora.
wxPython e Pygame.
L’integrazione con Pygame è più problematica. Non esiste un’api “ufficiale”, e le ricette che si trovano in rete sono
vecchie e tutte piuttosto sperimentali. Far girare Pygame in un thread separato sembra non essere l’idea giusta: Pygame
ha bisogno di vivere nel main thread, proprio come wxPython. La soluzione corretta sembra essere una qualche
variante del pattern di sfruttare wx.EVT_IDLE o wx.EVT_TIMER per aggiornare il canvas di Pygame.
150
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Questa pagina del wiki di wxPython sono raccolti alcuni suggerimenti. Ma la strada più promettente sembra essere
quella indicata nel sito di Pygame.
wxPython e Twisted.
Twisted mette a disposizione un reactor specializzato per l’integrazione con wxPython. Potete trovare i dettagli nella
documentazione, e in questo esempio.
Purtroppo sia la documentazione sia l’esempio sono un po’ vecchi, e usano convenzioni wxPython ormai deprecate.
Non è difficile, comunque, tradurre l’esempio in un wxPython “moderno”. Eccolo ri-adattato:
from twisted.internet import wxreactor
# importante! prima installare wxreactor...
wxreactor.install()
# ... poi importare internet.reactor
from twisted.internet import reactor
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
menu = wx.Menu()
menu_item = menu.Append(-1, 'Exit')
menuBar = wx.MenuBar()
menuBar.Append(menu, 'File')
self.SetMenuBar(menuBar)
self.Bind(wx.EVT_MENU, self.DoExit, menu_item)
p = wx.Panel(self)
b = wx.Button(p, -1, 'reactor!', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.Bind(wx.EVT_CLOSE, self.DoExit)
def DoExit(self, evt):
# importante: fermiamo il reactor prima di chiudere
reactor.stop()
def on_clic(self, evt):
# programmiamo una chiamata con il reactor di Twisted
reactor.callLater(2, self.twoSecondsPassed)
def twoSecondsPassed(self):
print "two seconds passed"
app = wx.App(False)
# registriamo l'istanza della wx.App con Twisted...
reactor.registerWxApp(app)
MyFrame(None).Show()
# ... e lasciamo che Twisted pensi ad avviare il mainloop della wx.App
reactor.run()
7.6 Chiudere i widget: aspetti avanzati.
Questa pagina riprende il discorso sulla chiusura dei widget, esaminando alcuni scenari che si verificano di rado ma
che è meglio conoscere per evitare sorprese.
7.6. Chiudere i widget: aspetti avanzati.
151
Appunti wxPython Documentation, Release 1
Anche se in questa pagina non facciamo esempi espliciti, è chiaro che alcuni comportamenti che descriviamo potrebbero, in determinate circostanze, innescare dei wx.PyDeadObjectError: vi conviene quindi leggere anche la
pagina sulle eccezioni wxPython subito dopo o subito prima di questa.
7.6.1 Distruzione di finestre a cascata.
Quando chiudete una finestra, naturalmente wxPython distrugge a cascata anche tutti i widget “figli” nella catena dei
parent. Attenzione però: non solo i pulsanti, caselle di testo etc. dentro la finestra, ma anche eventuali altre finestre
figlie di quella che state chiudendo.
Ma qui c’è una trappola: la distruzione di tutti i widget figli (e quindi anche delle finestre figlie) avviene chiamando
direttamente wx.Window.Destroy, e non il più gentile wx.Window.Close. Dopo aver visto tutte le complicate sottigliezze legate a wx.Window.Close, probabilmente capirete perché wxPython sceglie di tagliar corto:
preferisce assicurarsi che, per esempio, quando l’utente chiude la finestra “top level” il programma termini davvero,
senza rimanere impigliato in qualche veto proveniente da finestre secondarie aperte magari solo per dimenticanza.
Tuttavia, questa procedura sbrigativa rischia di saltare qualche importante passaggio nelle vostre operazioni di
chiusura. Ecco un esempio pratico di quello che potrebbe succedere:
class ChildFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
def on_close(self, evt):
evt.Skip()
print 'sono ' + self.GetTitle() + ' e mi sto chiudendo!'
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.SetTitle('FINESTRA PRINCIPALE')
child = ChildFrame(self)
child.SetTitle('FINESTRA FIGLIA')
child.Show()
if __name__ == '__main__':
app = wx.App(False)
MainFrame(None).Show()
app.MainLoop()
Quando chiudete la finestra figlia, il wx.EVT_CLOSE viene generato e intercettato regolarmente. Ma quando invece chiudete direttamente la finestra principale, questo non succede perché la finestra figlia viene distrutta con
wx.Window.Destroy.
wxPython vi offre quindi un comportamento di default ragionevole ma sbrigativo: nella maggior parte dei casi è
sufficiente, ma quando invece avete la necessità di garantire la corretta procedura di chiusura delle finestre figlie,
non vi resta che intercettare il wx.EVT_CLOSE della finestra principale, e intervenire voi stessi. La strategia esatta
dipende dall’architettura del vostro programma. Ecco una traccia molto semplificata:
class ChildFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.Bind(wx.EVT_CLOSE, self.on_close)
self.SetTitle('FINESTRA ' + str(self.GetId()))
152
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
def on_close(self, evt):
evt.Skip()
print 'sono ' + self.GetTitle() + ' e mi sto chiudendo!'
self.GetParent().child_list.remove(self)
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'genera figlio', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.Bind(wx.EVT_CLOSE, self.on_close)
self.SetTitle('FINESTRA PRINCIPALE')
self.child_list = []
def on_clic(self, evt):
child = ChildFrame(self)
self.child_list.append(child)
child.Show()
def on_close(self, evt):
evt.Skip()
for child in self.child_list[:]:
child.Close()
Beninteso, in un’applicazione vera avrete bisogno di una strategia meno rudimentale (probabilmente un sistema di messaggistica pub/sub in un’architettura MVC). Ma qui il punto importante è che abbiamo intercettato il wx.EVT_CLOSE
della finestra principale per impedire a wxPython di distruggere sbrigativamente le finestre figlie. Invece, chiamiamo
manualmente wx.Window.Close su tutte le finestre figlie ancora aperte, garantendo così l’esecuzione dei callback
con le operazioni di chiusura.
7.6.2 Trappole legate alla distruzione dei widget.
Specialmente quando wxPython distrugge i widget a cascata, il momento esatto della distruzione di ciascuno non è
garantito, e in linea di principio non dovreste cercare di intervenire troppo nel processo di distruzione.
La differenza tra Close e Destroy.
La chiusura di una finestra è un evento importante, al quale spesso desiderate reagire in qualche modo. wxPython
mette a disposizione un hook ben preciso per questo scopo, ed è wx.Window.Close. La chiamata a questo
metodo (causata dal vostro codice, o dall’utente che fa clic sul pulsante di chiusura), provoca l’emissione di un
wx.EVT_CLOSE che potete intercettare: abbiamo anche visto che molti virtuosismi sono possibili durante questa
fase.
In ogni caso, finché si resta allo stadio di wx.Window.Close, la finestra è ancora “in vita”, stabile e pronta per
essere usata. Per esempio, se nel callback del wx.EVT_CLOSE volete prelevare il contenuto di una casella di testo
nella finestra, potete farlo senza timore che nel frattempo sia già stata distrutta.
Le cose cambiano non appena si chiama wx.Window.Destroy (cosa che avviene automaticamente nel gestore di
default del wx.EVT_CLOSE, se voi non stabilite diversamente). Da questo momento, la finestra entra in una fase
transitoria, e non ne viene garantito il normale funzionamento: wxPython procede a distruggere tutti i suoi figli chiamando direttamente wx.Window.Destroy su ciascuno di essi. In questa fase, di regola non viene emesso nessun
evento che possiate intercettare: wxPython fa tutto da solo, ed è saggio non cercare di intromettersi nel processo.
7.6. Chiudere i widget: aspetti avanzati.
153
Appunti wxPython Documentation, Release 1
Potete verificare se una finestra è sul punto di essere distrutta testando wx.Window.IsBeingDeleted: se ottenete
True, dovreste considerarla ormai perduta, anche se potreste ancora accedere a certe sue proprietà. In wxWidgets,
dove è possibile manipolare più da vicino il processo di distruzione, questo strumento è spesso necessario. In wxPython
la sua utilità è molto ridotta, ma vedremo alcuni esempi in cui può ancora servire.
Eventi da oggetti in fase di distruzione.
La distruzione di una finestra, in wxPython, non è un evento né immediato, né “atomico”. La chiamata a
wx.Window.Destroy imposta un flag testabile con wx.Window.IsBeingDeleted, e programma il widget
per la distruzione. Il processo di distruzione avviene in momenti successivi. wxPython utilizza le pause del suo main
loop (ovvero, i cicli di wx.EVT_IDLE successivi), in modo da essere certo che nessun evento resti in coda da processare quando procede con la distruzione. Ma la distruzione completa di una finestra può richiedere diversi cicli di
questo tipo.
Tra il momento in cui wx.Window.Destroy viene chiamato e il momento in cui anche l’ultimo “pezzo” della
finestra è effettivamente distrutto, è possibile avere il tempo per fare qualcosa di sbagliato? Di regola, no. Però dove
c’è una regola, c’è anche un’eccezione.
Per esempio immaginate che, nel corso della sua distruzione, il widget abbia il tempo di postare un evento nella
coda: l’evento farebbe riferimento a un oggetto la cui esistenza è... discutibile. Se un callback intercettasse l’evento
e chiamasse per esempio wx.Event.GetEventObject, non è chiaro che cosa potrebbe succedere. Forse state
pensando di scrivere voi stessi una trappola di questo tipo, per esempio estendendo wx.Window.Destroy in una
sotto-classe. Qualcosa come questo:
# questo NON funziona davvero come immaginate...
class MyButton(wx.Button):
def Destroy(self):
wx.PostEvent(.....)
# fatto il danno, procedo a distruggere il widget normalmente
wx.Button.Destroy(self)
Purtroppo o forse per fortuna questo codice non fa proprio quello che vi aspettate. Il fatto è che non potete semplicemente sovrascrivere il proxy Python (in wxPython) di un metodo virtuale C++ (in wxWidgets), e sperare di aver
sovrascritto anche l’originale. E così, se provate a implementare l’esempio qui sopra, otterrete due comportamenti
differenti:
1. quando chiamate MyButton.Destroy dal vostro codice Python, allora sicuramente la vostra versione di
Destroy verrà eseguita;
2. quando invece wxWidgets chiama internamente wxWindow::Destroy per distruggere il pulsante (magari
perché l’utente ha chiuso la finestra che lo contiene), allora verrà eseguito il codice C++ sottostante, e il vostro
MyButton.Destroy sarà completamente ignorato.
Todo
una pagina su SWIG e l’oop Python/C++.
Anche se wxPython vi impedisce di percorrere il sentiero più pericoloso, non è comunque difficile trovarsi in situazioni
imbarazzanti. Proviamo a implementare concretamente la nostra idea:
import wx.lib.newevent
TestEvent, EVT_TEST = wx.lib.newevent.NewCommandEvent()
class MyButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
154
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
self.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
event = TestEvent(self.GetId())
wx.PostEvent(self.GetEventHandler(), event)
def Destroy(self):
event = TestEvent(self.GetId())
wx.PostEvent(self.GetEventHandler(), event)
return wx.Button.Destroy(self)
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.test = MyButton(p, -1, 'test', pos=(20, 20))
b = wx.Button(p, -1, 'distruggi test', pos=(20, 60))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.Bind(EVT_TEST, self.on_evt_test)
def on_evt_test(self, evt):
print 'EVT_TEST emesso da ', evt.GetId()
def on_clic(self, evt):
self.test.Destroy()
Adesso MyButton emette l’evento anche in risposta al clic (solo per consentirci di testarlo), e il frame principale lo
intercetta. Abbiamo già capito che l’evento non verrà emesso quando wxWidgets distruggerà il pulsante al momento
di chiudere la finestra. Tuttavia ci aspettiamo di vedere l’evento quando siamo noi a distruggere manualmente il
pulsante chiamando self.test.Destroy() nel nostro codice. E invece no: sicuramente il nostro codice viene
eseguito, e possiamo vedere che in effetti il pulsante si distrugge; tuttavia non riusciamo a intercettare l’evento. Il
motivo, in questo caso, è che wx.PostEvent posta l’evento nella coda senza processarlo immediatamente. Subito
dopo il widget viene distrutto: siccome però il primo handler dell’evento è il widget stesso, quando viene il momento
di processare l’evento l’handler non si trova più. Volendo, possiamo assegnare all’evento un primo handler differente,
per esempio il suo parent (che nel nostro esempio è il panel della finestra principale):
wx.PostEvent(self.GetParent().GetEventHandler(), event)
Oppure possiamo usare wx.EvtHandler.ProcessEvent (invece di wx.PostEvent) per processare immediatamente l’evento:
self.GetEventHandler().ProcessEvent(event)
Entrambe le tecniche funzionano ma sono spericolate e potrebbero avere effetti collaterali indesiderati.
Emettere eventi da un widget in fase di distruzione è certamente uno scenario molto raro. In ogni caso il punto
dovrebbe essere chiaro: non è mai conveniente maneggiare direttamente wx.Window.Destroy. Quando volete
reagire alla chiusura di un widget, usate piuttosto wx.Window.Close e il relativo wx.EVT_CLOSE.
Esempi di trappole che potreste incontrare davvero.
In alcuni casi potreste davvero incontrare delle situazioni simili a quella descritta sopra: approfondiamo adesso un
paio di esempi rari ma relativamente ben conosciuti. L’importante è imparare a riconoscere il pattern, nel caso doveste
imbattervi in situazioni analoghe.
wx.TreeCtrl serve a visualizzare strutture ad albero. In ambiente Windows wxPython utilizza il widget nativo, mentre sulle altre piattaforme ripiega su un widget generico. wx.TreeCtrl emette, tra gli altri, un evento
7.6. Chiudere i widget: aspetti avanzati.
155
Appunti wxPython Documentation, Release 1
wx.EVT_TREE_SEL_CHANGED ogni volta che l’utente seleziona un diverso elemento dell’albero. Quando wxPython deve distruggere questo widget, per prima cosa procede a eliminare il contenuto, un elemento alla volta; fatto
questo, distrugge il widget vero e proprio. Ora, ecco il problema: il widget nativo di Windows, quando viene distrutto
un elemento dell’albero che in quel momento è selezionato, provvede a spostare la selezione sull’elemento più vicino,
e naturalmente per questo emette un wx.EVT_TREE_SEL_CHANGED. Quando wxPython svuota completamente
l’albero, nel processo di distruzione, questo fenomeno di solito passa inosservato: infatti, siccome in genere l’albero
ha una sola radice e lo svuotamento procede dalla radice alle foglie, una volta eliminata la radice non esiste più un
elemento valido su cui spostare la selezione.
Ma se invece il vostro albero ha più di una radice... Allora si verifica un fenomeno bizzarro: la distruzione del widget
provoca una raffica di wx.EVT_TREE_SEL_CHANGED, in numero variabile a seconda di quante radici ci sono, e
quale elemento era selezionato al momento della distruzione. Un esempio chiarirà forse meglio:
# ricordate, questo effetto si vede solo in Windows!
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.tree = wx.TreeCtrl(self,
# TR_HIDE_ROOT per avere molte radici
style=wx.TR_DEFAULT_STYLE|wx.TR_HIDE_ROOT)
root = self.tree.AddRoot('')
for i in range(10):
item = self.tree.AppendItem(root, 'nodo %d' % i)
self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_sel_changed)
# in alternativa, provate anche:
# self.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_sel_changed)
self.Refresh() # adatta il widget alla finestra
def on_sel_changed(self, evt):
print "Selezione cambiata: ", self.tree.GetItemText(evt.GetItem())
Quando chiudete la finestra, vedrete la traccia di una serie di eventi: noterete che lo svuotamento procede dell’alto
verso il basso, spingendo quindi verso il basso l’elemento selezionato che produce l’evento. Come risultato, avete più
eventi se selezionate un elemento alto della lista, e ne avete di meno se selezionate un elemento basso.
Una ulteriore complicazione: se invece di collegare il wx.TreeCtrl al suo evento, collegate la finestra-madre
(quindi: self.Bind(...) invece di self.tree.Bind(...)), il risultato cambia drasticamente. Questa volta,
al momento della chiusura, nessun evento sarà intercettato: questo perché l’event handler (ovvero la finestra stessa) si
trova in quel momento in una fase abbastanza avanzata della distruzione da non poter più operare.
Questo, naturalmente, è uno scenario abbastanza inconsueto: dovete essere su Windows; usare un wx.TreeCtrl;
avere un albero con più radici; intercettare wx.EVT_TREE_SEL_CHANGED; e infine, beninteso, dovete mettere nel
callback del codice “sensibile”, che può effettivamente combinare dei guai (scritture sul database, etc.) quando viene
eseguito a sproposito.
Se però siete proprio in questa condizione, niente panico. La soluzione è semplicissima: basta testare
wx.Window.IsBeingDeleted e non eseguire il callback proprio quando la finestra sta per essere distrutta:
def on_sel_changed(self, evt):
if not self.IsBeingDeleted():
print "Selezione cambiata: ", self.tree.GetItemText(evt.GetItem())
Vediamo un altro esempio molto simile, ma dalle conseguenze ancora più drammatiche. wx.GenericDirCtrl è
un pratico widget che visualizza automaticamente il vostro file system. Per fare questo, naturalmente al suo interno
utilizza un wx.TreeCtrl (ops). E naturalmente su Windows il file system può avere più di una radice (ops!)... Avete
già capito il problema:
# anche qui, l'effetto si vede solo in Windows
class MyFrame(wx.Frame):
156
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.dirctrl = wx.GenericDirCtrl(self)
self.dirctrl.Path = 'c:'
# "self.dirctrl.TreeCtrl" e' il wx.TreeCtrl interno
self.dirctrl.TreeCtrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_sel_changed)
def on_sel_changed(self, evt):
print self.dirctrl.Path
Se siete su una tipica macchina Windows, probabilmente avrete almeno due unità (c: e d:, in genere) che costituiscono altrettante radici del file system. Durante la fase di chiusura, viene emesso un wx.EVT_TREE_SEL_CHANGED
spurio: il callback relativo cerca di accedere alla proprietà wx.GenericDirCtrl.Path; questo provoca un tentativo di accesso a un elemento del wx.TreeCtrl interno, che però in quel momento non è accessibile; e questo, a
sua volta, provoca un clamoroso crash del programma!
Notate che non sempre il programma va in crash. Le due radici vengono distrutte in ordine (prima c:, poi d:), e se
voi avevate selezionato un elemento dell’albero d: al momento di chiudere la finestra, tutto fila liscio: quando d:
viene svuotato, non ci sono altri elementi su cui spostare la selezione. In ogni caso, anche questa volta la soluzione è
semplicissima:
def on_sel_changed(self, evt):
if not self.IsBeingDeleted():
print self.dirctrl.Path
Ci sono forse altri esempi del genere, nascosti nel gran numero di widget che wxPython mette a disposizione. Per esempio wx.Treebook emette un suo specifico evento wx.EVT_TREEBOOK_PAGE_CHANGED, che non è soggetto a
questo problema... almeno fin quando non avete bisogno di personalizzare il suo comportamento al punto da accedere
direttamente al suo wx.TreeCtrl interno... e allora, naturalmente, sapete che cosa potrebbe aspettarvi.
L’importante, in ogni caso, è rendersi conto del principio generale: la chiusura dei widget è un processo
delicato nel quale sarebbe meglio non intervenire. Se però vi trovate in circostanze eccezionali, nel dubbio
wx.Window.IsBeingDeleted è sempre pronto per voi.
7.7 Logging in wxPython (1a parte).
Questa è la prima di una serie di pagine che dedichiamo agli argomenti in qualche modo interconnessi del logging e
della gestione delle eccezioni. Non sono problemi specifici della programmazione di interfacce grafiche. In generale
si usano le stesse strategie di qualunque programma Python: in queste pagine supponiamo sempre che abbiate una
buona familiarità con le tecniche di base, e approfondiamo invece alcuni aspetti più specifici di wxPython.
Come è noto, il sistema di logging è una componente irrinunciabile di ogni applicazione web, dove è necessario poter
rintracciare l’origine di un problema tra migliaia di azioni provenienti da migliaia di connessioni contemporanee. Le
applicazioni desktop in genere lavorano in ambienti più piccoli e più protetti, ma non per questo potete permettervi
di trascurare il logging. Una volta che il programma ha abbandonato il vostro controllato ambiente di sviluppo per
entrare in produzione, solo l’analisi dei log può permettervi di capire che cosa è successo se qualcosa va storto.
Un’applicazione wxPython ha quindi bisogno di un sistema di logging esattamente come qualsiasi altro programma.
Non è questa la sede per un tutorial sull’argomento: ci limitiamo a qualche raccomandazione generale, prima di
affrontare alcuni aspetti più specifici relativi a wxPython.
1. Nel mondo del web, lo scopo del logging è anche aiutarvi a profilare l’efficienza della vostra applicazione man
mano che scala verso un numero di connessioni sempre più elevato. Per un’applicazione desktop, raramente
questo aspetto ha importanza. Lo scopo del logging è quindi soprattutto di aiutarvi a rintracciare le cause degli
errori.
7.7. Logging in wxPython (1a parte).
157
Appunti wxPython Documentation, Release 1
2. Di conseguenza, non abbiate paura di loggare molto. Nel web scegliere cosa loggare è un’arte, e un log alluvionato da milioni di registrazioni irrilevanti diventa inutile. Nel mondo desktop raramente questo è un problema,
e potete permettervi di abbondare con le registrazioni. L’importante però è scegliere livelli diversi di criticità,
e/o creare nomi e gerarchie di componenti, flag, o altre indicazioni che vi aiutano a filtrare successivamente il
registro.
3. Ma fate attenzione a non loggare dati sensibili! Questo può essere molto pericoloso, perché in genere i file di log
sono più vulnerabili del database dell’applicazione. In molti casi è comodo loggare, per esempio, l’ultima riga
inserita in una tabella, per avere idea precisa di che cosa ha innescato un errore. Ma dovreste attribuire a queste
registrazioni un livello di criticità più basso (“debug”, per esempio) di quello che impostate in produzione. In
ogni caso, concordate sempre una policy precisa con il committente/utente del vostro programma: fategli sapere
che cosa state loggando, e quali sono i potenziali pericoli.
4. Per lo stesso motivo, fate attenzione che il log non diventi un involontario sistema di tracciamento dell’attività
degli utenti. Se avete bisogno di legare ciascuna registrazione a chi l’ha originata, potrebbe essere necessario,
per esempio, generare un codice casuale al momento del login, da usare solo ai fini del log e da cambiare a ogni
nuova sessione.
5. Fornite all’utente un modo per cambiare temporaneamente il livello di criticità del logging, senza bisogno di un
vostro diretto intervento sui file di configurazione (per esempio, potrebbe essere una regolazione da includere in
un dialogo di opzioni del vostro programma).
6. Preparatevi all’eventualità che tutto possa andar bene! Se non intervenite per parecchio tempo, le dimensioni del
file di log cresceranno senza limiti, e questo è un problema. Se usate il modulo logging della libreria standard
di Python, vi conviene loggare su un RotatingFileHandler o un TimedRotatingFileHandler.
Create inoltre una routine automatica per cancellare o archiviare i vecchi log, nel caso.
7. Siate pronti ad analizzare i vostri log rapidamente. Potete usare dei tool, e/o scrivere delle routine ad-hoc: in
ogni caso, fate delle prove e testate in anticipo.
7.7.1 Logging con Python.
Il modulo logging della libreria standard di Python è un ottimo framework: se non avete esigenze particolari, vi
conviene senz’altro affidarvi a questo. Se non conoscete logging, la documentazione ufficiale è un modello di
chiarezza: in particolare, potete cominciare dal tutorial che è davvero semplice da capire.
Se siete già pratici dell’uso di logging, non dovreste avere problemi: le strategie da usare in un’applicazione wxPython sono del tutto analoghe a quelle di un qualsiasi altro programma Python.
Di solito, un buon momento per inizializzare un logger “principale” è in wx.App.OnInit:
import logging, logging.config
class MyApp(wx.App):
def OnInit(self):
self.logger = logging.getLogger(__name__)
logging.config.fileConfig('logging.ini')
return True
In questo modo, tutte le finestre della vostra applicazione potranno ottenere un riferimento allo stesso logger semplicemente con:
self.logger = wx.GetApp().logger
oppure,
naturalmente,
crearsi un proprio logger chiamando di nuovo self.logger =
logging.getLogger(__name__). Le strategie per dare un nome ai logger dipendono come di consueto
dall’organizzazione del vostro codice, dalle necessità e dai gusti personali. Se riuscite a mantenere funzionalità
158
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
separate in moduli separati, un semplice __name__ andrà bene. Altrimenti, ciascuna classe potrebbe dare al logger
il proprio nome, per esempio.
Molto spesso le registrazioni avvengono dall’interno di un callback collegato a un evento. In wxPython, ricordate,
certi eventi possono capitare molto frequentemente. Se registrate un log in risposta a eventi come wx.EVT_IDLE,
wx.EVT_SIZE, wx.EVT_MOVE, wx.EVT_PAINT e molti altri, la fluidità della vostra gui finirà per risentirne (e
vi ritroverete con un log sterminato). Se proprio vi serve loggare questi eventi, potete aiutarvi con strumenti come
wx.Yield.
7.7.2 Re-indirizzare il log verso la gui.
Specialmente in fase di sviluppo, può capitare di voler vedere il log “in tempo reale” sullo schermo. La cosa più facile,
e probabilmente anche la migliore, è dirigere l’output del log verso la shell (eventualmente duplicandolo su un file).
Potete usare le consuete strategie per differenziare l’ambiente di sviluppo da quello di produzione (per esempio, usare
file di configurazione diversi).
Se preferite, tuttavia, potete indirizzare il log verso la vostra gui. Questo potrebbe essere desiderabile in fase di
sviluppo, se per esempio avete timore che il buffer della vostra shell sia troppo piccolo, e/o volete un display più
maneggevole (in casi estremi, potreste scrivere una classe personalizzata per visualizzare il log all’interno di qualche
widget super-specializzato come RichTextCtrl, basato su Scintilla). In fase di produzione, d’altra parte, questo
non dovrebbe essere necessario: dopo tutto, mostrare un log all’utente finale dovrebbe andare contro l’intera idea di
“programma con interfaccia grafica”.
La soluzione più facile, ancora una volta, è indirizzare il log verso lo standard output e poi utilizzare una delle note
tecniche per visualizzare l’output nella gui. In questo modo, tuttavia, lo stream del log si mescolerebbe agli standard
output/error (che però in una applicazione grafica non dovrebbero comunque essere mai troppo impegnati). Se questo
per voi è inaccettabile, e volete comunque mantenere il flusso del log ben separato, magari dedicandogli uno spazio
ad-hoc nella vostra interfaccia, anche questo non è difficile.
Quello che dovete fare è crearvi un logging.Handler personalizzato. Ecco un approccio minimalistico al problema:
class MyLoggingHandler(logging.Handler):
def __init__(self, ctrl):
logging.Handler.__init__(self)
self.ctrl = ctrl
def emit(self, record):
record = self.format(record) + '\n'
self.ctrl.write(record)
class MyTextCtrl(wx.TextCtrl):
# un TextCtrl che espone l'api "write" richiesta da MyLoggingHandler.emit
write = wx.TextCtrl.AppendText
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.log_record = MyTextCtrl(p, style=wx.TE_MULTILINE|wx.TE_READONLY)
do_log = wx.Button(p, -1, 'emetti un log')
do_std = wx.Button(p, -1, 'emetti stdout/err')
do_log.Bind(wx.EVT_BUTTON, self.on_do_log)
do_std.Bind(wx.EVT_BUTTON, self.on_do_std)
7.7. Logging in wxPython (1a parte).
159
Appunti wxPython Documentation, Release 1
s = wx.BoxSizer(wx.VERTICAL)
s.Add(self.log_record, 1, wx.EXPAND|wx.ALL, 5)
s1 = wx.BoxSizer(wx.HORIZONTAL)
s1.Add(do_log, 1, wx.EXPAND|wx.ALL, 5)
s1.Add(do_std, 1, wx.EXPAND|wx.ALL, 5)
s.Add(s1, 0, wx.EXPAND)
p.SetSizer(s)
def on_do_log(self, evt):
wx.GetApp().logger.log(logging.WARNING, "questo e' un log")
def on_do_std(self, evt):
print "questo e' uno stdout; segue uno stderr:"
print 1/0
class MyApp(wx.App):
def OnInit(self):
self.logger = logging.getLogger(__name__)
# potete eventualmente aggiungere altri handler al logger
# self.logger.addHandler(logging.StreamHandler())
# self.logger.addHandler(logging.FileHandler(filename='log.txt'))
main_frame = MainFrame(None)
handler = MyLoggingHandler(main_frame.log_record)
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
self.logger.addHandler(handler)
main_frame.Show()
return True
if __name__ == '__main__':
app = MyApp(False)
app.MainLoop()
Come si vede, è piuttosto semplice. Per prima cosa creiamo un handler personalizzato MyLoggingHandler, che
si potrà poi abbinare a qualunque widget wxPython: l’unica richiesta, naturalmente, è che il widget implementi una
interfaccia per le scritture (qui l’abbiamo chiamata write).
Il guaio però è che non possiamo collegare l’handler finché il widget (e quindi tutta la finestra che gli sta intorno)
non è stato creato. Nel nostro esempio abbiamo organizzato il codice necessario in wx.App.OnInit, in modo
da essere sicuri di rispettare l’ordine giusto. Finché il widget che deve contenere il log è nella finestra principale
dell’applicazione, questa soluzione dovrebbe bastare. Ma se il log deve apparire in una finestra secondaria, che può
essere chiusa e magari riaperta dall’utente... allora siete nei guai: se il log viene scritto in un momento in cui il widget
non esiste, otterrete un wx.PyDeadObjectError o qualche eccezione analoga.
Se volete far vedere il log in una finestra diversa da quella principale, potete adottare diverse strategie per accertarvi
che il widget esista sempre per tutto il ciclo di vita della vostra applicazione. Per esempio potreste nascondere la
finestra, invece di chiuderla:
class MyLoggingHandler(logging.Handler):
def __init__(self, ctrl):
logging.Handler.__init__(self)
self.ctrl = ctrl
def emit(self, record):
record = self.format(record) + '\n'
self.ctrl.write(record)
160
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
class LogWindow(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
self.log_record = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY)
self.Bind(wx.EVT_CLOSE, self.OnClose)
def OnClose(self, evt):
self.Hide()
def write(self, record):
# implementiamo l'api richiesta da MyLoggingHandler
self.log_record.AppendText(record)
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
do_log = wx.Button(p, -1, 'emetti un log', pos=(20, 20))
do_std = wx.Button(p, -1, 'emetti stdout/err', pos=(20, 60))
show_log = wx.Button(p, -1, 'mostra log', pos=(20, 100))
do_log.Bind(wx.EVT_BUTTON, self.on_do_log)
do_std.Bind(wx.EVT_BUTTON, self.on_do_std)
show_log.Bind(wx.EVT_BUTTON, self.on_show_log)
self.Bind(wx.EVT_CLOSE, self.on_close)
def on_do_log(self, evt):
wx.GetApp().logger.log(logging.WARNING, "questo e' un log")
def on_do_std(self, evt):
print "questo e' uno stdout; segue uno stderr:"
print 1/0
def on_show_log(self, evt):
wx.GetApp().log_window.Show()
def on_close(self, evt):
wx.GetApp().log_window.Destroy()
evt.Skip()
class MyApp(wx.App):
def OnInit(self):
self.logger = logging.getLogger(__name__)
main_frame = MainFrame(None)
self.log_window = LogWindow(None, title='LOG')
handler = MyLoggingHandler(self.log_window)
self.logger.addHandler(handler)
main_frame.Show()
self.log_window.Show()
return True
if __name__ == '__main__':
app = MyApp(False)
app.MainLoop()
In questo esempio, la finestra che mostra il log viene creata all’inizio (in wx.App.OnInit) e distrutta solo al
momento di chiudere la finestra principale (in risposta al wx.EVT_CLOSE). Per tutto il ciclo di vita dell’applicazione,
7.7. Logging in wxPython (1a parte).
161
Appunti wxPython Documentation, Release 1
l’utente apre e chiude a piacere la finestra, ma in realtà non fa altro che mostrarla e nasconderla.
7.7.3 Loggare da thread differenti.
logging è thread-safe, quindi non dovreste poter tranquillamente loggare da thread differenti, come per qualsiasi
altro programma Python.
Il problema naturalmente si verifica quando, oltre a voler loggare da thread diversi, avete anche fatto qualcosa di
esotico come re-indirizzare l’output del log verso qualche widget della gui. Questo è vietato dalla prima regola aurea
dei thread in wxPython: mai modificare lo stato della gui da un thread secondario. Niente paura, però: la seconda
regola aurea dei thread in wxPython viene in soccorso: basta includere la chiamata pericolosa in wx.CallAfter e
tutto torna a funzionare come per magia.
Riprendendo l’esempio qui sopra, se volete usare usare l’handler personalizzato MyLoggingHandler in una situazione in cui è possibile che il log sia scritto anche da thread secondari, basterà modificarlo come segue:
class MyLoggingHandler(logging.Handler):
def __init__(self, ctrl):
logging.Handler.__init__(self)
self.ctrl = ctrl
def emit(self, record):
record = self.format(record) + '\n'
if wx.Thread_IsMain():
self.ctrl.write(record)
else:
wx.CallAfter(self.ctrl.write, record) # thread-safe!
Todo
una pagina sui thread.
7.7.4 In conclusione...
Quasi certamente l’impalcatura del vostro log si affiderà al modulo logging della libreria standard Python. Tuttavia,
anche wxPython mette a disposizione un suo framework di logging interno: difficilmente lo troverete più utile di
logging, e tuttavia dovete ugualmente sapere come funziona. Infatti wxPython lo utilizza anche senza il vostro
consenso, per alcune importanti funzioni: ne parliamo più in dettaglio nella prossima pagina che dedichiamo a questi
argomenti.
7.8 Logging in wxPython (2a parte).
Riprendiamo qui il discorso sul logging che abbiamo iniziato nella pagina dedicata alle strategie per integrare
logging in una applicazione wxPython. Ci occupiamo adesso del framework interno di logging di wxPython.
7.8.1 Logging con wxPython.
wxWidgets mette a disposizione un sistema completo di logging, e anche wxPython lo traduce, almeno in parte. A dire
il vero, da quando esiste logging nella libreria standard di Python, non c’è quasi più bisogno di ricorrere a wxPython
per loggare. Tuttavia è meglio avere almeno un’idea di come funziona il logging con wxPython, per due motivi:
perché in certi casi potrebbe ancora farvi comodo; e soprattutto perché, se non intervenite sui default, occasionalmente
162
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
wxPython usa il suo sistema di logging per comunicare direttamente all’utente alcuni errori di sistema, con dei pop-up.
In genere questo non è sbagliato: tuttavia è fuori dal vostro controllo, e se invece volete avere voce in capitolo, dovete
sapere che cosa sta succedendo dietro le quinte.
I concetti di base non sono complicati. Il sistema di log è imperniato sulla classe wx.Log, che però non è direttamente
necessaria per loggare con le impostazioni di default. Esistono diversi livelli di allerta, simboleggiati dalle costanti
wx.LOG_* (da wx.LOG_FatalError che vale 0 a wx.LOG_Trace che vale 7). Per loggare un messaggio
con il desiderato livello, si può ricorrere alle funzioni globali corrispondenti wx.Log* (da wx.LogFatalError
a wx.LogTrace). Questo specchietto riassuntivo dovrebbe aiutarvi a capire come funzionano le impostazioni di
default:
LOG_LABELS = ['Fatal Error', 'Error', 'Warning', 'Message', 'Status',
'Verbose', 'Debug', 'Trace', 'Sys Error']
LOG_FUNCTIONS = {
# significato
funzione di log
'Fatal Error': wx.LogFatalError,
'Error':
wx.LogError,
'Warning':
wx.LogWarning,
'Message':
wx.LogMessage,
'Status':
wx.LogStatus,
# cfr anche wx.LogStatusFrame
'Verbose':
wx.LogInfo,
# alias per wx.LogVerbose
'Debug':
wx.LogDebug,
'Trace':
wx.LogTrace,
'Sys Error':
wx.LogSysError}
LOG_LEVELS = {
# significato
costante x livello
valore
'Fatal Error': wx.LOG_FatalError,
# 0
'Error':
wx.LOG_Error,
# 1 - anche per wx.LogSysError
'Warning':
wx.LOG_Warning,
# 2
'Message':
wx.LOG_Message,
# 3
'Status':
wx.LOG_Status,
# 4
'Verbose':
wx.LOG_Info,
# 5
'Debug':
wx.LOG_Debug,
# 6
'Trace':
wx.LOG_Trace,
# 7
'MAX':
wx.LOG_Max,
# 10000
}
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
level_choices = LOG_LABELS[:-1] # a 'Sys Error' non corrisponde nessun livello
level_choices += ['MAX']
self.log_levels = wx.RadioBox(p, choices=level_choices, style=wx.VERTICAL)
self.log_levels.Bind(wx.EVT_RADIOBOX, self.on_set_level)
s = wx.BoxSizer(wx.HORIZONTAL)
s.Add(self.log_levels, 1, wx.EXPAND|wx.ALL, 5)
s1 = wx.BoxSizer(wx.VERTICAL)
for label in LOG_LABELS:
b = wx.Button(p, -1, 'provoca un '+label)
b.Bind(wx.EVT_BUTTON,
lambda evt, func=LOG_FUNCTIONS[label], label=label: self.do_log(evt, func, label))
s1.Add(b, 1, wx.ALL|wx.EXPAND, 5)
s.Add(s1, 1, wx.EXPAND)
p.SetSizer(s)
self.log_levels.SetSelection(4) # 'Status' -> livello di default
self.SetStatusBar(wx.StatusBar(self))
7.8. Logging in wxPython (2a parte).
163
Appunti wxPython Documentation, Release 1
def on_set_level(self, evt):
level = self.log_levels.GetItemLabel(evt.GetInt())
wx.Log.SetLogLevel(LOG_LEVELS[level])
def do_log(self, evt, func, label):
func("Ecco a voi un... " + label)
if __name__ == '__main__':
app = wx.App(False)
MainFrame(None).Show()
app.MainLoop()
Alcune osservazioni e spiegazioni:
• potete cambiare in ogni momento la soglia del logging con wx.Log.SetLogLevel (è un metodo statico, non
serve istanziare prima wx.Log);
• wx.LogVerbose e wx.LogInfo sono sinonimi: entrambe le funzioni loggano un messaggio con livello
wx.LOG_Info (ovvero, livello 5);
• i livelli fino a wx.LOG_Status, di default, emettono dei messaggi visibili all’utente, sotto forma di pop-up
differenziati a seconda della gravità del problema. Il messaggio di wx.LogStatus è visibile nella barra di
stato della finestra (nel nostro esempio qui sopra, sembra succedere solo la prima volta: poi la barra di stato non
viene pulita, e i successivi messaggi non si distinguono più). Sopra wx.LOG_Status, i messaggi non sono
più visibili all’utente (queste sono le impostazioni di default: si possono cambiare, vedremo).
• wx.LogStatus utilizza la barra di stato della finestra attiva al momento dell’errore: se si desidera mostrare
il messaggio in finestre diverse, esiste anche wx.LogStatusFrame. Per esempio questo, chiamato da una
finestra secondaria, scrive nella barra di stato della finestra principale:
wx.LogStatusFrame(self.GetTopLevelParent(), 'messaggio di log')
• wx.LogFatalError è un caso speciale: si comporta come wx.LogError, ma non può essere disabilitato,
e mostra il messaggio all’utente chiamando la funzione globale più sicura wx.SafeShowMessage invece del
normale wx.MessageBox. Infine, termina il programma (!) con exit code 3;
• wx.LogSysError si comporta come wx.LogError, ma è un caso speciale. Viene usato soprattutto internamente da wxPython, e restituisce anche l’ultimo errore di sistema occorso (errno su Unix, GetLastError
su Windows).
Sono informazioni disponibili anche attraverso le funzioni wx.SysErrorCode e
wx.SysErrorMsg, come vedremo meglio parlando di debugging;
Todo
una pagina sul debugging.
• in teoria è possibile definire livelli di log personalizzati (compresi tra wx.LOG_User e wx.LOG_Max). Il
problema è che siccome i livelli predefiniti vanno da 0 a 7 (e wx.LOG_User vale 100!) non è possibile
definire livelli intermedi;
• specie se si definiscono livelli personalizzati, sarà utile usare wx.LogGeneric che, oltre al messaggio,
richiede di specificare anche il livello con cui si intende registrarlo;
• wx.LOG_Verbose è un livello riservato ad eventuali messaggi più dettagliati da mostrare all’utente. Normalmente non è utilizzato, ma potrebbe essere definito da target personalizzati (vedi sotto);
• wx.LOG_Debug e wx.LOG_Trace sono livelli di log attivi solo in debug mode, come vedremo parlando di
debugging.
164
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Todo
una pagina sul debugging.
7.8.2 Cambiare il log target.
Il framework di logging wxPython ha il concetto di “log target” per indicare dove dovrebbero essere diretti i messaggi
di log, in quali casi, e così via.
Un log target è semplicemente una sotto-classe di wx.Log. wxPython mette a disposizione alcuni log target già
pronti, che soddisfano i casi d’uso più comuni. Il target di default, responsabile per tutti i comportamenti che abbiamo
visto fin qui, è wx.LogGui. Per esaminare gli altri, utilizziamo questa semplice finestra di lavoro:
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
do_log = wx.Button(p, -1, 'emetti log', pos=(20, 20))
do_log.Bind(wx.EVT_BUTTON, self.on_do_log)
suspend = wx.Button(p, -1, 'sospendi log', pos=(120, 20))
suspend.Bind(wx.EVT_BUTTON, self.on_suspend)
resume = wx.Button(p, -1, 'riprendi log', pos=(220, 20))
resume.Bind(wx.EVT_BUTTON, self.on_resume)
target = wx.Button(p, -1, 'cambia target', pos=(20, 60))
target.Bind(wx.EVT_BUTTON, self.on_target)
restore = wx.Button(p, -1, 'ripristina target', pos=(20, 100))
restore.Bind(wx.EVT_BUTTON, self.on_restore)
self.actual_log = wx.Log.GetActiveTarget()
def on_do_log(self, evt): wx.LogWarning('un messaggio di log')
def on_suspend(self, evt): self.actual_log.Suspend()
def on_resume(self, evt): self.actual_log.Resume()
def on_target(self, evt): pass
def on_restore(self, evt): pass
if __name__ == '__main__':
app = wx.App(False)
MainFrame(None).Show()
app.MainLoop()
Una osservazione importante, prima di procedere: alcuni log target implementano un buffer interno in cui accumulano
le scritture di log, che vengono mostrate all’utente quando periodicamente il buffer viene svuotato. Il log target di
default wx.LogGui è appunto tra questi. Il buffer viene svuotato dal gestore di default dell’evento wx.EVT_IDLE,
che come sappiamo viene emesso automaticamente quando il loop degli eventi del sistema è libero. Di conseguenza
il buffer viene svuotato sempre molto rapidamente, e l’impressione per l’utente è che che il messaggio di log sia
visualizzato non appena viene emesso.
Potete tuttavia interrompere manualmente lo svuotamento del buffer chiamando wx.Log.Suspend, e riprenderlo
con wx.Log.Resume (e in questo intervallo, se volete, potete usare wx.Log.Flush per svuotare il buffer in
momenti precisi). Nel nostro esempio, provate a cliccare sul pulsante “sospendi log”: le registrazioni successive si
accumulano nel buffer e vengono mostrate tutte insieme quando cliccate su “riprendi log”. Dovete fare attenzione,
tuttavia: ciascuna chiamata successiva a wx.Log.Suspend si accumula in uno stack: pertanto, se cliccate due
volte di seguito su “sospendi log”, dovete poi cliccare due volte su “riprendi log”. Questo comportamento sembra
7.8. Logging in wxPython (2a parte).
165
Appunti wxPython Documentation, Release 1
bizzarro, visto nell’interfaccia del nostro esempio: ma è il nostro esempio a essere anomalo. In realtà, sospendere lo
svuotamento del log dovrebbe essere considerata un’operazione temporanea (quindi, evitate di lasciare direttamente
in mano all’utente questa opzione!): ci si aspetta che il componente che chiama wx.Log.Suspend si preoccupi
anche di chiamare wx.Log.Resume quanto prima, per evitare che il buffer continui a riempirsi all’infinito. Lo
stack di wx.Log.Suspend serve appunto a permettere che ciascun componente possa sospendere e ripristinare lo
svuotamento del buffer senza preoccuparsi degli altri.
Detto questo, esaminiamo gli altri log target disponibili, in aggiunta al predefinito wx.LogGui.
wx.LogWindow, che apre una finestra separata e re-indirizza il log verso quella:
Il primo è
def on_target(self, evt):
self.actual_log = wx.LogWindow(pParent=self, szTitle='LOG',
bShow=True, bPassToOld=False)
def on_restore(self, evt):
self.actual_log = None
wx.LogWindow è un log target “evoluto” che deriva da wx.Log attraverso wxLogInterposer (una classe
wxWidgets che in wxPython non è tradotta). Questa aggiunta gli conferisce la capacità di mantenere un riferimento anche al precedente target, e di indirizzare i messaggi a entrambi: potete settare l’argomento bPassToOld a True per
verificare. Un altro effetto gradevole, dal punto di vista di un programmatore Python, è che per usare wx.LogWindow
occorre solo istanziarlo, e per tornare al log target precedente basta solo de-referenziarlo. I log target che vedremo in
seguito, derivano invece direttamente da wx.Log e sono pertanto più limitati e difficili da usare.
C’è però un difetto fastidioso in wx.LogWindow: se l’utente chiude la finestra di log, questo non basta a distruggere
il log target, con il risultato che non viene ripristinato il target di default. Purtroppo, impedire all’utente di chiudere la finestra non è immediato. wxWidgets mette a disposizione due hook, wxLogWindow::OnFrameClose e
wxLogWindow::OnFrameDelete, che sarebbero perfetti per questo scopo: tuttavia, wxPython non li esporta (è
senz’altro un baco) e quindi non possiamo utilizzarli. Siamo costretti a sotto-classare:
class MyLogWindow(wx.LogWindow):
def __init__(self, *a, **k):
wx.LogWindow.__init__(self, *a, **k)
self.GetFrame().Bind(wx.EVT_CLOSE, self.on_close_frame)
# qui in pratica _non_ chiamare Skip() impedisce la chiusura...
def on_close_frame(self, evt): return False
class MainFrame(wx.Frame):
# etc. etc. come sopra
def on_target(self, evt):
self.actual_log = MyLogWindow(pParent=self, szTitle='LOG',
bShow=True, bPassToOld=False)
def on_restore(self, evt):
# dobbiamo distruggere manualmente il frame
# perché il normale EVT_CLOSE è bloccato
self.actual_log.GetFrame().Destroy()
self.actual_log = None
Infine, ricordiamo che wx.LogWindow non usa un buffer interno: di conseguenza wx.Log.Suspend non ha
effetto.
Un altro log target utile è wx.LogTextCtrl: questo target non è documentato ma, come suggerisce il nome, reindirizza il log verso un wx.TextCtrl multilinea. Per usarlo, aggiungiamo quindi una casella di testo al nostro
frame di esempio:
166
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
class MainFrame(wx.Frame):
def __init__(self, *a, **k):
# etc. etc. come sopra
self.logtxt = wx.TextCtrl(p, pos=(20, 140), size=(300, 100),
style=wx.TE_MULTILINE|wx.TE_READONLY)
def on_target(self, evt):
self.actual_log = wx.LogTextCtrl(self.logtxt)
self.old_target = wx.Log.SetActiveTarget(self.actual_log)
def on_restore(self, evt):
wx.Log.SetActiveTarget(self.old_target)
Come preannunciato, wx.LogTextCtrl deriva direttamente da wx.Log, e quindi è un po’ più difficile da creare
e distruggere. Occorre passare da wx.Log.SetActiveTarget, un metodo che convenientemente restituisce un
riferimento al target precedente, che possiamo poi usare al momento di ripristinare il log di default. Ricordiamo poi
che neppure wx.LogTextCtrl fa uso di un buffer interno.
wx.LogStderr invia le scritture del log verso lo standard error, e il suo uso è del tutto analogo al precedente:
def on_target(self, evt):
self.actual_log = wx.LogStderr()
self.old_target = wx.Log.SetActiveTarget(self.actual_log)
def on_restore(self, evt):
wx.Log.SetActiveTarget(self.old_target)
In wxWidgets, wx.LogStderr è perfettamente in grado di indirizzare il log verso un qualsiasi file stream: basta
passare al costruttore il riferimento di un file aperto. Purtroppo però wxPython non offre questa possibilità (un altro
baco...), e questo ne limita parecchio l’utilità.
Infine, wx.LogBuffer invia semplicemente il log a un buffer interno, che si svuota a ogni wx.EVT_IDLE: le
scritture in coda vengono mostrate all’interno di un pop-up generico. L’effetto per l’utente è simile a quello del
normale wx.LogGui, ma senza i pop-up differenziati per livello di allarme. Siccome c’è un buffer dietro le quinte,
possiamo usare wx.Log.Suspend e wx.Log.Resume all’occorrenza:
def on_target(self, evt):
self.actual_log = wx.LogBuffer()
self.old_target = wx.Log.SetActiveTarget(self.actual_log)
def on_restore(self, evt):
wx.Log.SetActiveTarget(self.old_target)
Sopprimere il log con wx.LogNull.
wx.LogNull è un log target particolare, che ha l’effetto di sopprimere ogni tipo di log. Il suo utilizzo è elementare:
def on_target(self, evt):
self.actual_log = wx.LogNull()
def on_restore(self, evt):
self.actual_log = None
Come abbiamo già visto per wx.LogWindow, per ripristinare il precedente log target basta de-referenziare l’istanza
di wx.LogNull.
wx.LogNull va usato con cautela. A differenza di wx.Log.Suspend, che vi consente comunque di recuperare le
scritture sul log in un secondo momento, questa classe disabilita completamente ogni tipo di logging. Se per esempio
7.8. Logging in wxPython (2a parte).
167
Appunti wxPython Documentation, Release 1
un errore di sistema (lato wxWidgets) dovesse accadere nel periodo “scollegato”, wxPython non potrebbe mostrarlo
all’utente nel consueto pop-up, né registrarlo in alcun modo (a meno che l’errore non sia così grave da terminare il
programma, certo).
7.8.3 Scrivere un log target personalizzato.
Non è difficile scrivere un log target a partire da zero: è semplicemente una sotto-classe di wx.Log (che però voi
dovete sotto-classare nella versione Python-friendly wx.PyLog). Ci sono tre metodi che potete sovrascrivere:
• wx.PyLog.DoLogRecord, è la prima funzione, nell’ordine, che viene chiamata quando loggate, e si occupa
di formattare il messaggio. L’implementazione di default si limita ad aggiungere data e ora, e passare la stringa
a wx.PyLog.DoLogTextAtLevel;
• wx.PyLog.DoLogTextAtLevel, differenzia il comportamento a seconda dei livelli di allarme.
L’implementazione di default indirizza i livelli wx.LOG_Trace e wx.LOG_Debug all’output di debug del
sistema, e spedisce tutto il resto a wx.PyLog.DoLogText;
• wx.PyLog.DoLogText esegue effettivamente la scrittura di log.
Potete quindi sovrascrivere questi metodi, a seconda del tipo di personalizzazione di cui avete bisogno: nei casi più
semplici, re-implementare wx.PyLog.DoLogText potrebbe essere sufficiente. Volendo, tuttavia, potete intervenire
alla radice.
Unificare il log wxPython e il log Python.
Armati di questa conoscenza, possiamo scrivere un log target personalizzato che scrive direttamente su un log Python.
Ecco un approccio minimalistico:
WXLOG_TO_PYLOG = {
wx.LOG_FatalError:
wx.LOG_Error:
wx.LOG_Warning:
wx.LOG_Message:
wx.LOG_Status:
wx.LOG_Info:
wx.LOG_Debug:
}
logging.critical, # non funziona! vedi sotto...
logging.error,
logging.warning,
logging.info,
logging.info,
logging.info,
logging.debug,
class MyLogTarget(wx.PyLog):
def DoLogRecord(self, level, msg, info=None):
msg = '[da wxPython] ' + msg
WXLOG_TO_PYLOG[level](msg)
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'clic')
b.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
# provate anche altri livelli: wx.LogError, wx.LogWarning etc.
wx.LogMessage('prova di log')
class MyApp(wx.App):
def OnInit(self):
logging.basicConfig(filename='log.txt', level=logging.DEBUG,
format='%(asctime)s %(levelname)s: %(message)s')
168
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
wx.Log.SetActiveTarget(MyLogTarget())
return True
if __name__ == '__main__':
app = MyApp(False)
MyFrame(None).Show()
app.MainLoop()
Abbiamo scritto un semplice log target che traduce i livelli di log wxPython nelle corrispondenti funzioni del
modulo logging. La formattazione dei messaggi è curata solo da logging.basicConfig: siccome abbiamo sovrascritto wx.PyLog.DoLogRecord, la formattazione di wx.Log è completamente annullata. Potete
sperimentare diversi livelli di logging: notate in particolare che adesso potete anche loggare con wx.LogInfo
e wx.LogDebug; e notate che wx.LogSysError continua ad aggiungere al messaggio anche l’indicazione
dell’ultimo errore di sistema riscontrato.
Questo esempio è già un buon punto di partenza: tuttavia, in un programma vero la vostra strategia di logging dovrà
essere più complessa. Per esempio, potreste voler usare un logger Python separato per i messaggi che provengono da
wxPython.
Perché conviene sempre usare un log target personalizzato.
Scrivere e usare un log target personalizzato è probabilmente la cosa migliore da fare, in un’applicazione wxPython
“seria”. Non è soltanto una questione di display dei messaggi (dirottarli su un log Python invece che mostrarli
all’utente, per esempio). Il fatto è che, come spieghiamo meglio altrove, wxWidgets non usa le eccezioni (le eccezioni
C++, naturalmente) per segnalare condizioni di errore: molto spesso solleva degli “assert” interni, che wxPython cattura e trasforma in wx.PyAssertionError. Ma altrettanto spesso utilizza wx.LogSysError per notificare il
problema all’utente, e in questo caso noi non abbiamo nessuna opportunità di intervenire.
Di conseguenza, un log target personalizzato è l’unica strada per intercettare in qualche modo un wx.LogSysError.
Si tratta di uno strumento molto impreciso (difficile, per esempio, rintracciare il punto esatto in cui si è verificato
l’errore), ma è meglio di niente. Possiamo almeno inserire un po’ di logica Python nel nostro log target, per reagire in
modo speciale a un errore grave:
class MyLogTarget(wx.PyLog):
def DoLogRecord(self, level, msg, info=None):
msg = '[da wxPython] ' + msg
WXLOG_TO_PYLOG[level](msg)
if level == wx.LOG_Error:
# qui possiamo chiedere all'utente di salvare
# e chiudere, per esempio, o procedere direttamente
# con wx.Exit() o qualche routine personalizzata...
Anche così, ci sono almeno due limitazioni fastidiose:
• non c’è modo di distinguere tra wx.LogError e wx.LogSysError (entrambi usano lo stesso livello di log
wx.LOG_Error, purtroppo). Questo però non è grave: wx.LogError non è usato internamente da wxPython, e di sicuro non lo userete nemmeno voi (perché tutte le vostre scritture avverranno tramite il logging
di Python, chiaramente!). Quindi, se il vostro log target intercetta un log di livello wx.LOG_Error, sicuramente deve trattarsi di un grave wx.LogSysError proveniente da wxPython;
• purtroppo, wx.LogFatalError continua ad essere un caso a parte: lo abbiamo incluso nel nostro esempio
qui sopra, ma in realtà il comportamento di default non può essere cambiato (provate). Questo vuol dire che
wxPython continuerà a mostrare un messaggio all’utente e chiudere l’applicazione, che vi piaccia o no. Anche
questo non è così grave: se wxPython si imbatte in un problema tale da richiedere wx.LogFatalError,
allora vuol dire che chiudere l’applicazione è comunque la cosa migliore.
7.8. Logging in wxPython (2a parte).
169
Appunti wxPython Documentation, Release 1
Quando il log di wxPython è utile.
Scrivere un log target specializzato che dirotta i messaggi verso il log di Python è probabilmente la cosa migliore da
fare. Talvolta però il comportamento di default del log di wxPython (ovvero, il log target predefinito wx.LogGui)
potrebbe essere desiderabile.
Per esempio, sappiamo già che wxPython può utilizzare wx.LogSysError per mostrare all’utente un messaggio
quando si verifica una condizione di errore interno. Vediamo un esempio concreto: quando provate a costruire una
wx.Bitmap a partire da un file “sbagliato” (inesistente o corrotto), wxPython emette un messaggio di log che contiene utili informazioni sull’errore, e lo mostra all’utente (in realtà ci sono delle sottigliezze che qui non consideriamo:
approfondiremo ancora proprio questo esempio parlando di eccezioni). Provate a sostituire, nell’esempio di sopra:
# (...)
def on_clic(self, evt):
wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_ANY)
e commentate la riga wx.Log.SetActiveTarget(MyLogTarget()) per tornare al normale log di wxPython.
Quando fate clic sul pulsante, wxPython vi mostrerà un messaggio di errore. Se adesso ripristinate il nostro log
target personalizzato, le informazioni sull’errore finiranno nel log di Python, ma il messaggio di errore non verrà più
visualizzato.
Usare contemporaneamente due log target.
In casi del genere, non è difficile aggiungere ancora un po’ di logica nel nostro log target specializzato, per recuperare
il messaggio di wx.LogSysError e mostrarlo all’utente in un wx.MessageBox, ricostruendo in questo modo il
comportamento di wx.LogGui. Non è forse la soluzione più elegante, ma così non vi complicate troppo la vita.
In alternativa, il log di wxPython ha il supporto per i log target multipli, proprio come il modulo logging di Python.
Per inviare un messaggio a due target separati, dovete usare la classe apposita wx.LogChain. Purtroppo in wxPython l’uso di questo strumento è reso un po’ complicato dalla necessità di separare gli oggetti proxy Python dai
corrispondenti oggetti C++: si tratta probabilmente di un vero e proprio baco, per giunta non documentato. Senza
scendere troppo nel dettaglio, mostriamo un esempio funzionante di come si può usare wx.LogChain per dirigere
il log contemporaneamente a wx.LogGui e al nostro log target specializzato (che a sua volta lo indirizza al log di
Python):
class MyApp(wx.App):
def OnInit(self):
logging.basicConfig(filename='log.txt', level=logging.DEBUG,
format='%(asctime)s %(levelname)s: %(message)s')
# attiviamo prima il target wx.LogGui
wx.Log.SetActiveTarget(wx.LogGui())
# creiamo il nostro log target...
log = MyLogTarget()
# ... e lo aggiungiamo alla catena
log_chain = wx.LogChain(log)
# separiamo i proxy Python dagli oggetti C++ sottostanti
log.this.disown()
log_chain.this.disown()
# "log" e "log_chain" saranno subito reclamati dal garbage collector Python
# ma gli oggetti C++ saranno distrutti da wxWidgets separatamente
# al momento giusto
return True
Se provate questa versione di wx.App con il codice dell’esempio precedente, noterete che i messaggi di log sono
effettivamente indirizzati sia al log di Python (attraverso il log target personalizzato MyLogTarget), sia al log di
wxPython (che in questo caso è wx.LogGui, ma potete anche scegliere un log target diverso). La danza un po’
goffa delle due chiamate a this.disown è necessaria per risparmiarsi la fatica di dover capire quando esattamente
170
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
occorre distruggere gli oggetti proxy Python. this è un riferimento all’oggetto C++ collegato, e disown scollega il
proxy Python (che quindi può essere distrutto senza che l’oggetto C++ sia interessato).
Todo
una pagina su SWIG e l’oop Python/C++.
7.8.4 Il conclusione: come loggare in wxPython.
Se avete letto questa e la precedente pagina sul log, dovreste avere gli strumenti per implementare una strategia di
logging adatta alle vostre esigenze.
Sarebbe questo il momento di dare qualche consiglio pratico riassuntivo prima di abbandonare definitivamente
l’argomento. Siccome tuttavia il problema del logging in wxPython è strettamente intrecciato con il problema della
gestione delle eccezioni, preferiamo raccogliere queste indicazioni al termine del discorso sulle eccezioni.
7.9 Gestione delle eccezioni in wxPython (1a parte).
Questa è la prima di due pagine che affrontano il complesso problema della gestione delle eccezioni in un programma
wxPython, strettamente interconnesso con le osservazioni che abbiamo già fatto sul logging. Potreste pensare che
le eccezioni in wxPython funzionano come in un qualsiasi programma Python, ma come vedremo le cose non stanno
proprio così. Un programma wxPython è sempre il risultato dell’interazione di codice Python e codice C++ sottostante,
e anche le coppie meglio assortite non sempre vanno d’accordo.
Todo
una pagina su SWIG e l’oop Python/C++.
Nel caso specifico della gestione delle eccezioni, ci sono tre aree problematiche:
1. le eccezioni Python non sono completamente libere di propagarsi in un programma wxPython: ne derivano
alcune trappole insidiose che dovete saper evitare;
2. non esiste un meccanismo di traduzione tra eccezioni C++ ed eccezioni Python...
3. ...ma servirebbe comunque solo fino a un certo punto, perché wxWidgets non usa le eccezioni C++ per segnalare
condizioni di errori. wxWidgets fa uso di varie formule di “assert” e/o di allarmi emessi con il suo sistema di
log interno: esiste un meccanismo di traduzione degli “assert” in eccezioni Python, e si possono escogitare
soluzioni per gestire meglio anche le scritture di log. Ma in entrambi i casi ci sono dei limiti.
In questa prima pagina affrontiamo il problema specifico delle eccezioni Python. Dedichiamo una pagina separata
all’analisi delle condizioni di errore che possono originarsi dal codice C++ di wxWidgets.
7.9.1 Il problema delle eccezioni Python non catturate.
Probabilmente vi siete già accorti che in un programma wxPython le eccezioni si comportano in modo anomalo.
Un’eccezione non intercettata, in un normale programma Python, si propaga fino in cima allo stack senza trovare
nessun handler disposto a gestirla, finché il gestore di default non termina il programma, inviando il traceback
dell’eccezione allo standard error (e quindi, tipicamente, alla shell che ha invocato lo script).
In wxPython d’altra parte, un’eccezione non controllata non termina il programma:
7.9. Gestione delle eccezioni in wxPython (1a parte).
171
Appunti wxPython Documentation, Release 1
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self, -1, 'clic')
b.Bind(wx.EVT_BUTTON, lambda evt: 1/0) # ops!
In questo esempio, ogni volta che cliccate sul pulsante innescate una ZeroDivisionError non gestita. Il traceback
relativo compare in effetti nello standard error, ma l’applicazione wxPython resta perfettamente funzionante.
Per capire questo bizzarro comportamento, occorre tenere presente che, durante il ciclo di vita di un’applicazione
wxPython, il controllo dell’esecuzione passa di continuo da “strati” di codice Python a “strati” di codice wxWidgets
(quindi, codice C++). Quando l’utente fa clic su un pulsante, innesca come ben sappiamo il complesso meccanismo
dell’emissione di un evento e della ricerca di un gestore: questa fase è controllata da wxWidgets (C++). Quando
l’handler dell’evento è stato trovato, viene eseguito il callback relativo: questo in genere è codice che avete scritto voi
(Python). Quando il callback è stato eseguito, il controllo ritorna al wx.App.MainLoop: questo è di nuovo codice
C++. E così via.
Ora, ecco il problema: una eccezione Python non può propagarsi oltre lo “strato” in cui viene emessa. Non è possibile
propagare un’eccezione Python attraverso strati di codice C++. Questo è un problema di cui gli sviluppatori wxPython
sono ben consapevoli: in teoria dovrebbe essere possibile tradurre al volo una eccezione Python nella sua controparte
C++ e viceversa, in modo da permettere la propagazione libera tra i vari strati. In pratica però, non è affatto banale
implementare questo meccanismo: sono stati fatti dei tentativi, ma non si è mai approdati a nulla di definitivo.
E quindi? Che cosa succede quando l’eccezione Python si propaga fino al “confine” dello strato in cui è stata generata,
senza trovare nessun blocco try/except disposto a prendersene cura? Succede una cosa brutta ma inevitabile: il
codice C++ immediatamente successivo rileva che c’è una condizione di errore, chiama PyErr_Print per scriverlo
nello standard error, e resetta l’errore. Quindi l’eccezione termina lì, e non ha modo di propagarsi fino a raggiungere il
punto in cui l’interprete Python farebbe arrestare il programma. Voi potete vedere ugualmente il traceback nella shell
(o dovunque abbiate re-indirizzato lo standard error), ma solo perché è stato scritto lì da PyErr_Print (una delle
API C di Python).
Infine, tenete conto che esiste un caso interessante in cui questo comportamento non si applica. Se l’eccezione
Python non controllata avviene prima di aver chiamato wx.App.MainLoop, allora effettivamente il programma
si interromperà “come al solito”: questo perché, prima di entrare nel main loop, Python ha ancora il controllo
dell’applicazione. In pratica, ci sono due posti in cui un’eccezione Python può verificarsi prima di entrare nel main
loop: in wx.App.OnInit e nell’__init__ del frame principale dell’applicazione.
7.9.2 try/except in wxPython non funziona sempre come vi aspettate.
Se non siete ben consapevoli di questo comportamento, potreste trovarvi di fronte a situazioni paradossali. In Python
“puro”, potete intercettare un’eccezione anche molto lontano dal punto in cui si è generata:
>>>
...
...
>>>
...
...
>>>
...
...
>>>
...
...
...
...
...
172
def disastro():
return 1/0 # ops!
def produci_un_disastro():
return disastro()
def prepara_un_disastro():
return produci_un_disastro()
def salva_la_giornata():
try:
return prepara_un_disastro()
except ZeroDivisionError:
return "salvo per miracolo!"
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
>>> salva_la_giornata()
'salvo per miracolo!'
>>>
Questa non è una buona pratica di programmazione: ma potete comunque farlo. In wxPython, invece, intercettare
un’eccezione lontano dal punto di origine potrebbe non riuscire bene come immaginate:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self)
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.Bind(wx.EVT_SIZE, self.on_size)
def on_clic(self, evt):
try:
self.SendSizeEvent() # genera manualmente un wx.EVT_SIZE
except ZeroDivisionError:
print 'presa al volo!'
def on_size(self, evt):
evt.Skip()
1/0 # ops!
Prendetevi qualche minuto per immaginare come potrebbe funzionare questo esempio, prima di proseguire. Abbiamo
collegato wx.EVT_SIZE a un callback che produce una eccezione Python non gestita. Possiamo aspettarci, tutte
le volte che ridimensioniamo la finestra, di veder comparire il traceback nella shell (la finestra però si ridimensiona
correttamente. A wxWidgets non interessa il nostro codice problematico nel callback: fintanto che chiamiamo Skip,
l’handler di default dell’evento farà il suo mestiere). Per la precisione, un primo wx.EVT_SIZE viene emesso
automaticamente già al momento di creare la finestra: quindi dovremmo vedere un traceback nella shell proprio come
prima cosa.
Quando però facciamo clic sul pulsante, ci aspettiamo una cosa diversa. Nel callback del pulsante noi generiamo manualmente un wx.EVT_SIZE: quindi il callback difettoso verrà di nuovo eseguito, eccezione compresa. Questa volta
però ci siamo premuniti, e abbiamo incluso la chiamata che genera il wx.EVT_SIZE in un blocco try/except.
Dunque, ciò che vedremo questa volta sarà il messaggio “presa al volo!” nella shell, giusto?
Sbagliato, purtroppo. Il problema è che, tra lo strato di codice Python pronto a intercettare l’eccezione, e lo strato di
codice Python che innesca l’eccezione, c’è di mezzo un consistente strato di codice C++ (che si occupa del dispatch
del wx.EVT_SIZE). E attraverso questo strato la nostra eccezione non passa. Il risultato è che, anche quando il
wx.EVT_SIZE si genera in seguito al clic sul pulsante, noi vedremo comunque il traceback nella shell, perché il
ramo except che abbiamo predisposto non sarà mai raggiunto dall’eccezione.
Non esiste una soluzione generale di questo problema. La cosa migliore è attenersi al noto principio di buon senso: le
eccezioni dovrebbero essere intercettate più vicino possibile al punto di emissione. In Python è una buona pratica di
programmazione, ma in wxPython è un principio guida da seguire con il massimo scrupolo.
7.9.3 Che cosa fare delle eccezioni Python non gestite.
Abbiamo ormai capito che le eccezioni Python possono essere più difficili da intercettare in un programma wxPython,
e abbiamo visto che le eccezioni non gestite non terminano prematuramente il programma. Questo però ci lascia con
una domanda: come dovremmo comportarci con queste eccezioni non intercettate?
7.9. Gestione delle eccezioni in wxPython (1a parte).
173
Appunti wxPython Documentation, Release 1
Il problema.
Prima di tutto, non dovrebbe esserci bisogno di dirlo, ma insomma: in un programma con interfaccia grafica, pensato
per l’utente finale, le eccezioni non gestite sono bachi puri e semplici. Non dovrebbero esserci. Se vi capitano in fase
di sviluppo, poco male: tenete d’occhio il flusso dello standard error, osservate lo stacktrace, vi rimproverate perché
l’eccezione si è verificata “in vivo” passando tra le maglie della vostra suite di test, debuggate, scrivete altri test, li
eseguite, problema risolto.
Tuttavia, sappiamo che i bachi vengono fuori anche (no, soprattutto!) quando ormai il programma è in produzione. E’
a questo punto che wxPython vi fa rischiare grosso. In un normale programma Python, un baco in produzione significa
che l’eccezione non gestita ferma il programma. Certo, l’utente non sarà felice. Forse l’uscita anomala lascerà qualche
risorsa esterna non chiusa a dovere. Ma oltre a questo, il danno non ha modo di propagarsi.
In un programma wxPython, d’altra parte, l’utente finale non vede lo standard error e non ha modo di accorgersi di
nulla. Ecco uno scenario fin troppo comune (in pseudo-codice):
class Anagrafica(wx.Frame):
def __init__(...):
...
ok.Bind(wx.EVT_BUTTON, self.salva_dati)
def salva_dati(self, evt):
dati = self._raccogli_dati()
try:
db.orm.persisti(dati)
except db.QualcosaNonVa:
wx.MsgBox('Qualcosa non va')
return
wx.MsgBox('Dati salvati, tutto a posto')
self.Close()
Quando l’utente fa clic sul pulsante “Salva”, noi invochiamo una routine per salvare i dati nel database
(“db.orm.persisti()” potrebbe far riferimento a un ORM, o comunque a un layer separato in una logica MVC). La
nostra routine innesca un’eccezione custom “QualcosaNonVa” in caso di problemi, e noi correttamente la intercettiamo nel callback. Dunque, se qualcosa non va, l’utente vede un messaggio allarmante. Se invece tutto va bene,
l’utente viene rassicurato e la finestra si chiude. Questo pattern in sé non ha niente di sbagliato. Ma se c’è un baco
nel layer di gestione del database, “db.orm.persisti” innesca un’eccezione che non abbiamo previsto: siccome non la
intercettiamo, tutto sembra andare per il verso giusto (ricordate, wxPython non ferma il programma). Ma in realtà i
dati non sono stati salvati. Prima che l’utente abbia modo di accorgersi del problema, l’errore potrebbe ripetersi più
volte; i dati sbagliati saranno usati per ulteriori elaborazioni; e il baco originario potrebbe essere molto difficile da
individuare.
Ora, in Python un approccio come questo viene giustamente considerato una cattiva pratica:
try:
main()
except: # un "bare except" per ogni possibile imprevisto
# ...
Tuttavia possiamo chiederci se in wxPython non sia l’unico modo per risolvere il problema delle “eccezioni invisibili”.
Ci piacerebbe poter scrivere qualcosa come:
if __name__ == '__main__':
# questo non funziona davvero!
try:
app = wx.App(False)
MainFrame(None).Show()
app.MainLoop()
except:
174
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
wx.MessageBox('Qualcosa non va!!')
wx.Exit()
Purtroppo, come avrete già intuito, questo non funziona. A partire da quando invocate wx.App.MainLoop, ci
sono troppi strati di codice C++ perché una eccezione Python imprevista possa finire nella rete del “bare except” che
abbiamo messo al livello più alto dello stack.
Una soluzione accettabile.
Una soluzione accettabile è invece usare sys.excepthook dalla libreria standard di Python:
def my_excepthook (extype, value, tback):
wx.MessageBox('Questo non va proprio bene!', 'errore')
# non dimenticate di loggare... qualcosa come:
# logger.error('disastro fatale', exc_info=(extype, value, tback))
wx.Exit()
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
b = wx.Button(self)
b.Bind(wx.EVT_BUTTON, lambda evt: 1/0) # ops!
if __name__ == '__main__':
sys.excepthook = my_excepthook
app = wx.App(False)
MyFrame(None).Show()
app.MainLoop()
Nel nostro “except hook” personalizzato possiamo inserire una logica arbitrariamente complessa. Per esempio, sarebbe
perfettamente sicuro interagire con l’interfaccia grafica (aprire e chiudere finestre, postare eventi nella coda, etc.): la
nostra eccezione Python non impedisce al framework C++ sottostante di continuare a funzionare, come sappiamo.
Tuttavia, proprio perché stiamo affrontando un’eccezione imprevista (leggi: un baco) e non possiamo sapere che cosa
sta succedendo, conviene limitarsi al minimo indispensabile: avvertire l’utente che il programma sta per chiudersi;
fare il rollback di eventuali transazioni in corso; loggare il traceback dell’eccezione che altrimenti andrebbe perduto;
infine, chiudere l’applicazione nel modo che riteniamo migliore.
E’ possibile inserire il nostro except hook direttamente nel blocco if __name__=="__main__", come nel nostro
esempio. In alternativa, un buon momento è come sempre wx.App.OnInit.
7.10 Gestione delle eccezioni in wxPython (2a parte).
Riprendiamo il discorso sulle eccezioni da dove l’avevamo lasciato: dopo aver visto la difficile esistenza delle eccezioni Python nell’ecosistema wxPython, affrontiamo adesso lo stesso argomento dal punto di vista di wxPython
stesso (o meglio, dell’architettura wxWidgets sottostante).
La prima cosa da sapere al riguardo è che wxWidgets non utilizza le eccezioni per segnalare condizioni di errore.
Questo perché le eccezioni sono state introdotte nel linguaggio C++ solo quando ormai wxWidgets era già un framework molto esteso e complesso. Lo sforzo di adattare wxWidgets alla nuova realtà è arrivato fino al punto di supportare l’utilizzo delle eccezioni nel codice client (ovvero, le applicazioni wxWidgets scritte in C++ possono usare le
eccezioni). Ma al suo interno il framework ha continuato a utilizzare gli strumenti di cui si era nel frattempo dotato
per la gestione degli errori.
7.10. Gestione delle eccezioni in wxPython (2a parte).
175
Appunti wxPython Documentation, Release 1
7.10.1 wx.PyAssertionError: gli assert C++ tradotti in Python.
wxWidgets, per segnalare condizioni di errore, fa uso di una famiglia di macro di debugging, la cui più importante
è wxASSERT. Non è questa la sede per esaminare il loro complesso funzionamento: la cosa importante dal nostro
punto di vista è che ogni volta che il codice wxWidgets emette un assert, questo si trasmette al livello superiore
wxPython sotto forma dell’eccezione wx.PyAssertionError, che può quindi essere catturata in un normale
blocco try/except.
A livello globale, questo meccanismo si può controllare con il metodo wx.App.SetAssertMode (e il suo
corrispettivo wx.App.GetAssertMode), che accetta questi possibili valori: wx.PYAPP_ASSERT_DIALOG,
wx.PYAPP_ASSERT_EXCEPTION (il default), wx.PYAPP_ASSERT_LOG e wx.PYAPP_ASSERT_SUPPRESS,
tutti dal significato piuttosto intuitivo.
wx.PyAssertionError è, in un certo senso, quanto di meglio wxPython può metterci a disposizione per sbirciare
nel labirinto altrimenti inaccessibile del codice C++ sottostante. L’utilità di questo strumento, in ogni caso, è minore
di quanto si potrebbe sperare. In primo luogo, non è facile scoprire quando una particolare routine C++ potrebbe
innescare un assert: la documentazione di wxWidgets non è sempre così completa e accessibile. Ma più che altro, il
problema è che non esiste una policy uniforme trasversale a tutto il codice wxWidget, su come e quando si dovrebbero
usare gli assert. E’ facile trovare incongruenze bizzarre.
Facciamo un esempio concreto (uno tra i mille possibili) che abbiamo già introdotto parlando di logging: che cosa
succede quando proviamo a caricare una wx.Bitmap con un file inesistente?
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_ANY)
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)
# (1)
# (2)
La prima versione non produce un assert, ma una scrittura di log (con wx.LogSysError che mostra un messaggio
di errore all’utente). La seconda versione non produce né assert né scritture di log (!). In entrambi i casi la creazione
dell’oggetto wx.Bitmap va comunque a buon fine: potete scoprire solo a posteriori che c’è un problema, testando
wx.Bitmap.IsOk (che in entrambi i casi restituisce False).
Ma non è finita. Se il costruttore di wx.Bitmap non usa gli assert, d’altra parte ci sono molte api che invece testano
wx.Bitmap.IsOk ed emettono assert se qualcosa non va. Per esempio, proviamo a usare una wx.Bitmap per
produrre una wx.Mask:
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)
mask = wx.Mask(bmp, wx.RED)
Quando istanziamo la wx.Mask, il costruttore C++ emette un assert che viene tradotto in un
wx.PyAssertionError, che volendo possiamo intercettare.
D’altra parte, se invece usiamo la nostra wx.Bitmap malformata per creare un wx.BitmapButton, l’operazione
fallisce silenziosamente, senza che nessun assert venga emesso, e ci viene mostrato un normale pulsante senza nessuna
immagine:
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)
button = wx.BitmapButton(self, bitmap=bmp)
E ancora: proviamo adesso a usare la nostra wx.Bitmap per creare il pulsante di una wx.ToolBar. Al momento
di aggiungere il tool (con wx.ToolBar.AddTool), niente accade. Quando però finalizziamo la creazione della
toolbar (wx.ToolBar.Realize), allora viene emesso un assert se uno dei tool è “sbagliato”... ma naturalmente a
questo punto è impossibile dire esattamente quale:
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)
toolbar = self.CreateToolBar()
toolbar.AddLabelTool(-1, 'Ops!', bmp)
try:
toolbar.Realize()
176
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
except PyAssertionError:
print 'presa al volo!'
L’elenco delle eccentricità potrebbe continuare a lungo. wx.PyAssertionError è uno strumento indubbiamente
utile, che però non può essere usato con la stessa disinvoltura con cui un programatore Python è abituato a usare le sue
eccezioni. A noi verrebbe naturale scrivere codice più o meno così:
try:
bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)
except IOError:
...
Questo è l’approccio “Easier to Ask for Forgiveness Than Permission” tipico di Python. Ma quando avete a che fare
con wxWidgets dovete spesso adeguarvi al principio opposto “Look Before You Leap” del mondo C/C++. In questo
caso, tutto considerato, sarebbe probabilmente più saggio verificare se il file esiste davvero, prima di caricarlo nella
wx.Bitmap.
A parte queste considerazioni, vale infine la pena di ricordare che wx.PyAssertionError ha comunque gli stessi
limiti di tutte le eccezioni Python nell’ecosistema wxPython: in particolare, deve essere intercettato nello stesso
“strato” di codice Python in cui viene emesso.
7.10.2 wx.PyDeadObjectError e il problema della distruzione dei widget.
Nella pagina dedicata alla chiusura dei widget abbiamo già avuto modo di parlare di wx.PyDeadObjectError.
Si tratta di un’eccezione che wxPython innesca quando provate ad accedere (da Python) a un oggetto wxWidget
che non esiste più in quando è stato distrutto. Abbiamo già visto come, entro certi limiti, possa servire per testare
se la chiusura di una finestra è andata a buon fine. Ma si tratta di un’utilizzo “positivo” marginale. In genere
wx.PyDeadObjectError è un evento “negativo” nel contesto del vostro programma: vi segnala che, probabilmente, vi siete dimenticati un handler Python “orfano” in giro.
Riassumiamo brevemente la questione: wxPython è costruito a partire da wxWidgts usando SWIG. Quando istanziate
(da Python) un oggetto del framework wxWidget (C++), quello che fa davvero SWIG è creare due oggetti, quello
“reale” C++ e un oggetto proxy Python che vi permette di controllarlo:
>>> import wx
>>> app = wx.App()
>>> app
<wx._core.App; proxy of <Swig Object of type 'wxPyApp *' at 0x333bf00> >
>>> frame = wx.Frame(None)
>>> frame
<wx._windows.Frame; proxy of <Swig Object of type 'wxFrame *' at 0x22fb600> >
>>> button = wx.Button(frame)
>>> button
<wx._controls.Button; proxy of <Swig Object of type 'wxButton *' at 0x4121700> >
>>>
Come vedete, abbiamo sempre due oggetti: un oggetto Python (wx._core.App, wx._windows.Frame,
wx._controls.Button) che agisce da proxy per il corrispettivo oggetto C++ (wxPyApp, wxFrame,
wxButton) creato da SWIG. E’ facile perdere la contabilità di questa “partita doppia”, specialmente programmando
in un linguaggio dinamico come Python. In particolare, potrebbero esserci due problemi speculari:
1. come abbiamo visto, i widget (finestre, pulsanti, etc.) si distruggono usando wx.Window.Destroy (preferibilmente chiamato attraverso wx.Window.Close): questo finisce per invocare il distruttore dell’oggetto C++,
ma non ha nessun effetto sull’oggetto proxy Python, che resta normalmente in vita.
2. gli oggetti proxy, d’altra parte, si possono distruggere come qualsiasi oggetto Python: ri-assegnando la variabile
che ne conservava un riferimento; lasciando semplicemente che escano dallo “scope”; usando l’operatore del.
In tutti questi casi, naturalmente posto che non ci siano in giro altri riferimenti all’oggetto, il garbage collector di
7.10. Gestione delle eccezioni in wxPython (2a parte).
177
Appunti wxPython Documentation, Release 1
Python ne programma la distruzione. Ma distruggere il proxy Python non ha nessun effetto sul corrispondente
oggetto C++, che quindi resta in vita.
Approfondiamo ciascuno di questi due casi separatamente.
Todo
una pagina su SWIG e l’oop Python/C++.
Distruggere il proxy Python lasciando in vita l’oggetto C++.
In linea di principio, distruggere il proxy Python di un oggetto C++ ancora in vita sarebbe un “memory leak”: avete
un oggetto che resta in memoria senza che possiate più raggiungerlo da Python. In realtà, nel caso dei normali widget, di solito non è così grave: da un lato, esiste in genere la possibilità di rintracciarli tramite i normali meccanismi
di wxPython (per esempio gli id, o usando la catena dei parent); dall’altro, se il widget è visibile, l’utente ha sempre la possibilità di intervenire: per esempio chiudere una finestra lasciata aperta. Nell’esempio che segue, ogni
clic sul pulsante crea un nuovo wx.Frame (invisibile!): l’oggetto Python viene sempre cancellato, non appena la
variabile frame esce dallo “scope” del callback on_clic. Tuttavia gli oggetti C++ che restano in vita in questo
caso sono figli del frame principale, e pertanto possono essere recuperati da wxWidgets attraverso strumenti come
wx.Window.GetChildren:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
def on_clic(self, evt):
frame = wx.Frame(self)
print len(self.GetChildren())
Distruggere l’oggetto C++ lasciando in vita il proxy Python.
Il caso opposto di solito è più preoccupante. La distruzione di un oggetto wxWidgets può avvenire in qualsiasi
momento e anche indipendentemente da noi: basta che l’utente faccia clic sul pulsante di chisura di una finestra, e
non solo quella finestra ma anche tutti i widget che contiene saranno distrutti. Una volta che l’oggetto wxWidgts è
distrutto, qualunque ulteriore accesso al proxy Python innesca un wx.PyDeadObjectError, come sappiamo:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = wx.Button(p, -1, 'clic', pos=(20, 20))
b.Bind(wx.EVT_BUTTON, self.on_clic)
self.child_frame = wx.Frame(self )
self.child_frame.Show()
def on_clic(self, evt):
print self.child_frame.GetId() # per esempio
Nell’esempio qui sopra, ottenete un wx.PyDeadObjectError quando fate clic sul pulsante dopo aver chiuso il
frame figlio. Come abbiamo già detto nella pagina sulla chiusura dei widget potete sfruttare questa eccezione per
testare se un widget esiste ancora, catturandola in un blocco try/except.
178
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Come abbiamo visto, la distruzione a cascata di molti widget (per esempio, a seguito della chiusura di una finestra)
comporta delle complicazioni ulteriori. Se è possibile intromettersi nel processo di distruzione (ovvero, generare o intercettare un evento nell’intervallo compreso tra la prima chiamata a wx.Window.Destroy e l’effettiva distruzione
di tutti i widget coinvolti), potrebbero esserci delle conseguenze bizzarre.
Per esempio, non è difficile modificare uno degli esempi che abbiamo fatto in modo da ottenere un
wx.PyDeadObjectError:
class MyFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.txt = wx.TextCtrl(p, pos=(20, 20))
self.tree = wx.TreeCtrl(p, pos=(20, 60), size=(100, 300),
style=wx.TR_DEFAULT_STYLE|wx.TR_HIDE_ROOT)
root = self.tree.AddRoot('')
for i in range(10):
item = self.tree.AppendItem(root, 'nodo %d' % i)
self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_sel_changed)
def on_sel_changed(self, evt):
print self.txt.GetValue()
Se siete in Windows, questo codice genera una raffica di wx.PyDeadObjectError al momento di chiudere la
finestra, quando gli eventi spuri emessi dal wx.TreeCtrl provocano dei tentativi di accesso a un wx.TextCtrl
che nel frattempo è stato già distrutto (per l’analisi completa del motivo di tutto ciò, dovete leggervi la pagina dedicata).
In ogni caso, abbiamo già anche visto la soluzione: tutte le volte che un evento potrebbe essere intercettato nel mezzo
del processo di chiusura, potete evitare gli eventuali wx.PyDeadObjectError semplicemente testando la finestra
per wx.Window.IsBeingDeleted: se ottenete True, semplicemente non eseguite il codice del callback:
def on_sel_changed(self, evt):
if not self.IsBeingDeleted():
print self.txt.GetValue()
7.10.3 Le “%typemap” di SWIG e il type checking in wxPython.
Infine, un cenno meritano le eccezioni con cui wxPython sostituisce il type checking statico della controparte C++.
Avrete già notato che, se chiamate un metodo o una funzione con una signature diversa da quella prevista (parametri
del tipo sbagliato etc.), ottenete una eccezione Python (che si comporta come di consueto in wxPython: non ferma il
programma, etc.).
Queste eccezioni sono originate dalle “%typemap” di SWIG: in sostanza, meccanismi di traduzione che convertono il
type checking delle funzioni C++ di wxWidgets.
Le typemap di SWIG fanno in genere molto bene il loro lavoro: tuttavia si tratta pur sempre di meccanismi automatici
che, per quanto regolati e affinati negli anni da Robin Dunn, hanno pur sempre le loro idiosincrasie. Con la pratica,
non è difficile imbattersi in curiose bizzarrie di ogni tipo:
wx.Colour(100, -1, 100)
# restituisce ValueError
wx.Colour(100, 'ops', 100) # restituisce ValueError
wx.Colour(100, 500, 100)
# restituisce OverflowError... naturalmente!
Esperienze del genere scoraggiano un po’ chi è abituato alla comodità di try/except. Non c’è dubbio che programmando in wxPython bisogna adeguarsi all’approccio “Look Before You Leap” tipico del mondo C/C++. Tuttavia
anche le eccezioni Python si possono usare con successo: l’importante, come sempre, è non lesinare mai con gli unit
test.
7.10. Gestione delle eccezioni in wxPython (2a parte).
179
Appunti wxPython Documentation, Release 1
7.10.4 Consigli conclusivi su logging e gestione delle eccezioni.
Per finire, riprendiamo qui anche il discorso sul logging in wxPython, e riassumiamo alcune strategie tipiche.
• Per fare logging in wxPython, vi conviene utilizzare il modulo logging della libreria standard di Python.
• Ci sono pochi casi in cui forse vi conviene usare il framework di logging di wxPython (wx.Log etc.): quando
l’applicazione è così semplice da non avere una logica di business “pure-Python” separata; o magari quando
scegliete di usare il log in modo intensivo per mostrare messaggi all’utente. Eventualmente, valutate se usare
wx.LogChain per indirizzare i messaggi a diversi log target separati.
• Se decidete di usare logging, questo non basta comunque a liberarvi del tutto da wx.Log perché wxPython
ne fa uso in due modi:
• wxPython emette un wx.LogFatalError prima di chiudere l’applicazione, in caso di errore talmente grave
da compromettere il funzionamento del motore di wxWidget: questo comportamento non è modificabile in
nessun modo;
• wxPython emette un wx.LogSysError (e di default mostra un messaggio all’utente) quando incontra un
errore interno non fatale. Potete (e dovreste, in effetti) reagire a questi errori scrivendo un log target personalizzato: quando intercettate il messaggio, potete inviarlo al normale log Python; mostrare o meno un messaggio
all’utente; chiudere l’applicazione, e così via.
• Un log target personalizzato non è uno strumento molto preciso, ma è il meglio che potete fare per intercettare
gli errori di sistema che wxPython tratta con wx.LogSysError.
• Poi ci sono gli errori che wxWidgets gestisce internamente con gli assert C++, e che wxPython cattura e restituisce sotto forma di wx.PyAssertionError. Potete intercettare questa eccezione in un normale blocco
try/except, se volete. Ma spesso non è una buona idea, perché si tratta di un’eccezione molto generica.
Se siete sicuri che una determinata api emette wx.PyAssertionError solo in una circostanza ben precisa,
usate try/except. Altrimenti è preferibile l’approccio “Look Before You Leap”: verificate che le condizioni
siano tutte corrette, e soltanto allora chiamate l’api. Se fate così, tutte le eccezioni wx.PyAssertionError
che dovessero ancora verificarsi sarebbero bachi imprevisti: catturatele nel vostro “hook acchiappa-tutto” (vedi
sotto) e debuggate quanto prima.
• Per wx.PyDeadObjectError vale praticamente la stessa raccomandazione: intercettatelo in un
try/except solo quando siete veramente sicuri del motivo per cui viene emesso. Altrimenti, “Look Before
You Leap”: nei casi critici potete testare wx.Window.IsBeingDeleted prima di accedere a un widget.
I wx.PyDeadObjectError “liberi” sono naturalmente dei bachi: catturateli nel vostro “hook acchiappatutto” e debuggate.
• Siete invece liberi di usare try/except a piacere, per le eccezioni Python che provengono dal vostro codice:
anzi, è il normale approccio “Easier to Ask for Forgiveness Than Permission” di Python. Attenzione però:
le eccezioni Python vanno catturate quanto prima, perché in wxPython non possono propagarsi al di fuori del
segmento di codice Python da cui sono originate. Un’eccezione Python non catturata è, ancora una volta, un
baco: in wxPython però l’applicazione non termina come al solito, e questo è un problema grave. Il meglio che
potete fare è catturarle nell“‘hook acchiappa-tutto” e debuggare, debuggare quanto prima.
• Siccome le eccezioni Python non gestite (vostre, o i wx.Py*Error generati da wxPython) non terminano immediatamente il programma, potrebbero avere effetti nascosti molto gravi. Il meglio che potete fare è sovrascrivere sys.excepthook con un vostro “hook acchiappa-tutto”: quando catturate in questo modo un’eccezione
non gestita, dovreste senz’altro scriverla nel log. Potete eventualmente mostrare un messaggio all’utente, e
chiudere voi stessi l’applicazione.
• Tutte queste raccomandazioni (log target personalizzati, sys.excepthook sovrascritti, etc.) valgono solo
quando il vostro programma è in produzione. In fase di sviluppo, naturalmente, volete invece che gli errori
saltino fuori nel modo più appariscente possibile. Una buona strategia potrebbe essere scrivere due versioni
separate della vostra wx.App (o almeno, due versioni del suo wx.App.OnInit e wx.App.OnExit), da
usare in ambiente di sviluppo e in produzione.
180
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
7.11 Pattern: Publisher/Subscriber.
Affrontiamo in questa pagina un design pattern di grande importanza nel mondo delle applicazioni gui: Publisher/Subscriber (pub/sub, per brevità).
Questa pagina è un anello di congiunzione tra quelle, numerose, che abbiamo dedicato agli eventi, e quella sul pattern
Model/Controller/View. E’ consigliabile proseguire nella lettura solo se conoscete abbastanza bene il modello a eventi
di wxPython: in particolare, come si propagano.
Todo
una pagina su MCV con riferimenti a questa.
I design pattern in generale sono molto importanti e studiati nell’ambito della programmazione a oggetti. Non è questa
la sede per introdurre la filosofia dei pattern, spiegare cosa sono e quali problemi aiutano a risolvere. In rete e in libreria
si può trovare moltissimo: qui diamo per scontato che sappiate orientarvi già a sufficienza nella programmazione a
oggetti.
7.11.1 Che cosa è pub/sub.
Detto molto in breve, pub/sub è un pattern per implementare un sistema di messaggistica molti-a-molti tra un numero
arbitrario di oggetti. “Molti-a-molti” significa che ciascun oggetto può sia inviare messaggi a, sia ricevere messaggi
da, potenzialmente tutti gli altri oggetti.
Un “messaggio” è in definitiva la chiamata a una funzione (metodo) di un oggetto remoto. Di norma, affinché un
oggetto B possa chiamare un metodo di un altro oggetto A, è necessario che B “conosca” A: ovvero, che conservi un
riferimento interno a A. Per esempio:
>>> class A (object):
def foo(self):
return "sono A.foo"
>>> class B (object):
def __init__(self, reference_to_a):
self.a = reference_to_a # B adesso conosce A
def call_foo(self):
return self.a.foo()
>>> a = A()
>>> b = B(reference_to_a=a)
>>> b.call_foo() # restituisce "sono A.foo"
Questo funziona, ma così A e B sono “accoppiati”, come si dice. Il pattern pub/sub permette di scambiare messaggi
tra oggetti senza che abbiano necessità di “conoscersi” tra loro, aiutando a mantenere quel disaccoppiamento tra le
parti che è il cuore della programmazione a oggetti.
Per raggiungere questo scopo, tutti gli oggetti interessati si rivolgono a un unico oggetto intermediario dei messaggi.
L’intermediario mantiene al suo interno un elenco di tutti gli oggetti “iscritti” al sistema, e riceve i messaggi di ciascuno
di loro. Quando l’intermediario riceve un messaggio, lo inoltra a tutti gli oggetti del suo elenco. Se un oggetto vuole
partecipare al sistema dei messaggi, si iscrive presso l’intermediario. Tutto ciò che i partecipanti hanno bisogno di
conoscere, è l’intermediario.
Si possono trovare in rete molte implementazioni di pub/sub, anche in Python. Ecco un esempio molto rudimentale,
solo per aiutare a visualizzare quanto detto fin qui:
7.11. Pattern: Publisher/Subscriber.
181
Appunti wxPython Documentation, Release 1
1
2
3
class Publisher(object):
def __init__(self):
self._subscribers = set()
4
def subscribe(self, new_subscriber):
self._subscribers.add(new_subscriber)
5
6
7
def unsubscribe(self, old_subscriber):
self._subscribers.discard(old_subscriber)
8
9
10
def publish(self, message):
for subscriber in self._subscribers:
subscriber(message)
11
12
13
14
15
16
17
18
class Foo(object):
# un subscriber
def __init__(self, name, publisher):
self.name = name
self._publisher = publisher
19
def make_subscription(self, subscribe=True):
if subscribe:
self._publisher.subscribe(self.deal_with_incoming_message)
else:
self._publisher.unsubscribe(self.deal_with_incoming_message)
20
21
22
23
24
25
def say_hello(self):
self._publisher.publish('Hello world da... ' + self.name)
26
27
28
def deal_with_incoming_message(self, message):
print 'Sono', self.name, 'e ho ricevuto:', message
29
30
31
32
33
34
35
36
37
pub = Publisher()
andrea = Foo('Andrea', pub)
mario = Foo('Mario', pub)
andrea.make_subscription()
mario.make_subscription()
andrea.say_hello()
Potete fare un po’ di prove con questo giocattolo, aggiungendo altre istanze di Foo, o scrivendo altre classi da aggiungere al sistema di messaggistica. In realtà ci sono poche regole: la “sottoscrizione” consiste in sostanza nel fornire
all’intermediario un riferimento a un metodo da chiamare ogni volta che bisogna consegnare un messaggio (nel nostro caso, deal_with_incoming_message). Un dettaglio importante è la signature del metodo da usare per la
sottoscrizione: nella nostra implementazione, il metodo deve accettare esattamente un solo argomento (message),
perché l’intermediario lo chiamerà “alla cieca” fidandosi che l’interfaccia sia giusta (nel nostro esempio, la chiamata
subscriber(message)).
7.11.2 wx.lib.pubsub: l’implementazione wxPython di pub/sub.
Avere un sistema di messaggistica tra i componenti, implementato secondo la logica di pub/sub, è un aspetto molto
importante per un gui framework, per ragioni che saranno chiare tra poco.
Per questo wxPython mette a disposizione una sua versione di pub/sub: si tratta di una libreria completamente indipendente dal resto del framework, e quindi si può usare anche in progetti non legati al mondo wx (si può anche scaricare
e installare a parte: la documentazione completa si trova sul sito).
Per lavorare con questa versione di pub/sub, basta importare:
182
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
from wx.lib.pubsub import pub
wx.lib.pubsub si basa su una classe Publisher simile alla nostra, ma molto più raffinata. Prima di tutto,
Publisher è implementata come Singleton: solo una istanza di Publisher può vivere nel nostro programma. Questo
ci risparmia la noia di creare noi stessi una prima istanza, e poi passarla in giro (nel nostro esempio di sopra, in
Foo.__init__). Addirittura, l’api di wx.lib.pubsub nasconde completamente la classe Publisher: basta
importare pub per avere accesso, dietro le quinte, a una istanza unica di Publisher.
Note: le versioni precedenti di wx.lib.pubsub avevano un’api diversa, che esponeva direttamente Publisher.
La libreria si usava importando from wx.lib.pubsub import Publisher, in sostanza con le stesse funzionalità. La nuova api è stata inclusa in wxPython a partire dalla versione 2.9. Già a partire dalla versione
2.8.11.0, la nuova api poteva tuttavia essere abilitata con import wx.lib.pubsub.setupkwargs; from
wx.lib.pubsub import pub. Se avete una versione ancora più vecchia (o se trovate in giro degli esempi vecchi), potete comunque seguire questa pagina senza problemi, dal momento che la conversione è immediata.
In secondo luogo, Publisher conserva il suo “elenco degli abbonati” come una lista di weak references. Questo
ci risparmia il disturbo di cancellare la sottoscrizione di un oggetto, prima di distruggerlo (altrimenti il riferimento
esistente dentro la lista degli abbonati non ci permetterebbe di distruggerlo!). Ancora meglio, quando distruggiamo un
oggetto abbonato, Publisher se ne accorge e lo rimuove automaticamente dalla sua lista.
In terzo luogo, Publisher è in grado di differenziare i messaggi per “argomento” (topic): quando si pubblica un
messaggio, si deve specificare anche il suo topic. E d’altra parte, ci si può abbonare anche solo ad alcuni topic.
In questo modo è possibile separare le comunicazioni di oggetti diversi in ambiti diversi, senza obbligare ciascun
componente ad ascoltare tutto il traffico dei messaggi.
Ma c’è di più: è possibile creare delle gerarchie di topic, per esempio “notizie”, “notizie.sport”, “notizie.politica”,
“notizie.spettacolo”, etc. In questo modo, chi si abbona a “notizie.politica” riceverà solo i messaggi con questo topic.
Chi invece si abbona a “notizie” riceverà i messaggi del topic più generale, e quelli di tutti i sub-topic.
Per tutti i dettagli di wx.lib.pubsub vi rimandiamo alla documentazione on-line: quella di wxPython “classic”
purtroppo documenta solo la vecchia api, ed è pertanto superata. Ma la documentazione di Phoenix (la futura versione
di wxPython, ancora da completare) è invece aggiornata. La documentazione migliore, tuttavia, è nel sito stesso di
pubsub.
7.11.3 Un esempio di architettura pub/sub in wxPython.
Una situazione tipica dove pub/sub si può usare con successo in una gui, è quando volete mantenere sincronizzato lo
stato di molti diversi componenti: potrebbero essere dei widget all’interno di una finestra, o anche in finestre separate.
Questo esempio minimo dovrebbe aiutare a chiarire i termini del problema:
1
from wx.lib.pubsub import pub
2
3
TOPIC_VALUE_UPDATED = 'value-updated'
4
5
6
7
8
9
10
11
12
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.slider = wx.Slider(p, -1, 50, 0, 100,
style=wx.SL_VERTICAL)
check = wx.CheckBox(p, -1, 'connesso')
button = wx.Button(p, -1, 'nuovo')
13
14
15
self.slider.Bind(wx.EVT_SLIDER, self.on_slider)
check.Bind(wx.EVT_CHECKBOX, self.on_check)
7.11. Pattern: Publisher/Subscriber.
183
Appunti wxPython Documentation, Release 1
button.Bind(wx.EVT_BUTTON, self.on_clic)
16
17
s = wx.BoxSizer(wx.VERTICAL)
for ctl in (self.slider, check, button):
s.Add(ctl, 0, wx.ALIGN_CENTRE_HORIZONTAL|wx.ALL, 20)
p.SetSizer(s)
s.Fit(self)
18
19
20
21
22
23
pub.subscribe(self.update_value, TOPIC_VALUE_UPDATED)
check.SetValue(True)
24
25
26
def update_value(self, message):
self.slider.SetValue(message)
27
28
29
def on_slider(self, evt):
pub.sendMessage(TOPIC_VALUE_UPDATED,
message=self.slider.GetValue())
30
31
32
33
def on_check(self, evt):
if evt.IsChecked():
pub.subscribe(self.update_value, TOPIC_VALUE_UPDATED)
else:
pub.unsubscribe(self.update_value, TOPIC_VALUE_UPDATED)
34
35
36
37
38
39
def on_clic(self, evt):
Test(self).Show()
40
41
42
43
44
45
46
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
Per mantenere più compatto il codice, qui le finestre da sincronizzare sono in realtà istanze diverse dalla stessa classe
Test: ma potete naturalmente sperimentare per conto vostro esempi più elaborati.
La meccanica di base di wx.lib.pubsub, come si vede, è molto facile da capire. Un “messaggio” può essere in
realtà qualsiasi oggetto python (nel nostro caso trasmettiamo un semplice valore numerico, ma nulla vieta di passare
strutture dati più complesse). Un “topic” è una semplice stringa di testo: per praticità, conviene astrarre i topic in
variabili globali dichiarate all’inizio del modulo, soprattutto quando i topic cominciano a diventare numerosi. Una
gerarchia di topic si crea con la tipica sintassi “col punto”: “topic”, “topic.sub-topic”, “topic.sub-topic.sub-sub-topic”,
etc. wx.lib.pubsub offre alcune funzionalità più avanzate, che qui non descriviamo, rimandandovi alla documentazione.
Più interessante è capire come wx.lib.pubsub implementa in pratica il pattern pub/sub. Come si vede, ogni
componente può abbonarsi e cancellare l’abbonamento in qualsiasi momento, a run-time. Il sistema è completamente
indifferente a quali e quanti componenti sono abbonati. Ciascun componente è in grado di trasmettere e ricevere. Un
componente potrebbe trasmettere di volta in volta messaggi con topic diversi. Al limite, nulla vieta di abbonare lo
stesso metodo per l’ascolto di diversi topic (ma questa non è una buona pratica: conviene riservare metodi separati per
l’ascolto di topic separati. Oppure, se si è interessati all’ascolto di più topic, conviene raggrupparli in una gerarchia).
7.11.4 Messaggi pub/sub ed eventi wxPython.
Pub/sub è un pattern che permette di scambiare messaggi tra componenti. Questa è però anche la funzione svolta dal
sistema degli eventi, a cui abbiamo dedicato molte pagine. Un evento è simile a un messaggio pub/sub nel senso che si
origina in un componente, e provoca la chiamata a un metodo remoto (callback) precedentemente collegato mediante
una registrazione (è il compito di Bind, come sappiamo). Proprio l’esempio qui sopra ci permette di approfondire
184
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
alcune differenze importanti tra i messaggi di wx.lib.pubsub e gli eventi wxPython.
In primo luogo, si noti che i messaggi di wx.lib.pubsub si trasmettono senza nessun riguardo per la catena dei
parent. Nel nostro esempio, ogni nuova finestra è figlia di quella in cui abbiamo cliccato sul pulsante “nuovo”, e quindi
si possono facilmente creare anche finestre “cugine”: tuttavia, i messaggi si trasmettono indifferentemente non solo
lungo la linea genitori/figli, ma anche ai cugini più lontani. Per contrasto, si ricordi invece che gli eventi si propagano
solo lungo la catena dei parent (torneremo su questo tra poco).
In secondo luogo, wx.lib.pubsub è un sistema di messaggistica molti-a-molti: ogni componente può iscriversi
all’ascolto dei messaggi di tutti gli altri, e mandare a sua volta messaggi a tutti. Per contrasto, ricordiamo che gli
eventi possono essere collegati solo in modalità uno-a-uno o al massimo uno-a-molti. E’ possibile registrare lo stesso
“ascoltatore” (callback) a ricevere più eventi:
button.Bind(wx.EVT_BUTTON, self.listener)
another_button.Bind(wx.EVT_BUTTON, self.listener)
# self.listener ascolta gli eventi da due pulsanti diversi
Ma non è possibile registrare due ascoltatori diversi per intercettare lo stesso evento, per esempio:
button.Bind(wx.EVT_BUTTON, self.listener)
button.Bind(wx.EVT_BUTTON, self.another_listener)
# qui l'ultimo a registrarsi è quello che riceverà l'evento
A voler essere precisi, sappiamo già che questo non è del tutto vero: un evento si propaga, e diversi callback possono
intercettarlo in successione, ma solo se gli event handler da cui sono chiamati appartengono alla catena dei parent, e
solo se ogni callback ha cura di chiamare Skip.
In ogni caso, l’impressione complessiva è che pub/sub offra una soluzione più universale e al contempo elegante di
gestire i messaggi tra i componenti di un’applicazione gui. La domanda spontanea è: ma non sarebbe possibile fare
del tutto a meno degli eventi, e gestire ogni cosa attraverso wx.lib.pubsub?
Beh, no. Non sarebbe possibile, e forse neanche troppo conveniente.
Prima di tutto, wxPython si basa comunque sugli eventi: quando l’utente fa clic su un pulsante, è un
wx.CommandEvent che viene innescato, non un messaggio pub/sub. Anche nel nostro esempio qui sopra,
siamo comunque partiti da un wx.EVT_SLIDER, e solo nel callback collegato abbiamo avviato la macchina di
wx.lib.pubsub. Inoltre, wxPython usa gli eventi anche per tutti i suoi “messaggi di servizio”: per segnalare
che una porzione di interfaccia deve essere ridisegnata (wx.EVT_UPDATE_UI); per segnalare che le dimensioni
della finestra stanno cambiando (wx.EVT_SIZE); perfino per dire che non sta facendo nulla (wx.EVT_IDLE), e
molto altro ancora. Non è realistico pensare di sostituire integralmente la macchina degli eventi con qualcos’altro.
Ma c’è di più. Gli eventi (wx.Event e derivati) sono oggetti complessi, organizzati in gerarchie, fatti apposta
per conservare molte informazioni sul contesto che li ha generati. Un messaggio di wx.lib.pubsub, di per sé, non
conserva neppure il riferimento all’oggetto da cui è partito. Naturalmente anche con pub/sub si potrebbe implementare
una gerarchia di classi “messaggio”, istanziare la classe appropriata, riempirla delle necessarie informazioni, e infine
trasmetterla come messaggio - ma appunto, è una cosa che andrebbe implementata da zero.
Inoltre, proprio l’estrema libertà di propagazione dei messaggi pub/sub potrebbe non essere la cosa più desiderabile
nel contesto di una applicazione gui. C’è un motivo per cui solo i wx.CommandEvent si propagano, e si propagano
solo lungo la catena dei parent: nel 90% dei casi è proprio quello che vi serve. Le tipiche interfacce grafiche sono
organizzate in finestre che contengono panel che contengono pulsanti e altri widget: il rispetto di questa gerarchia vi
permette di evitare che componenti estranei possano essere disturbati da messaggi che non li riguardano. E’ facile e
comodo contenere i messaggi degli eventi in flussi separati. Naturalmente anche i topic di wx.lib.pubsub hanno
una funzione analoga: tuttavia, bisognerebbe costruire una gerarchia di topic elastica, che si adatti alla creazione e
distruzione di nuove finestre, alla disattivazione occasionale di parti dell’interfaccia, etc. E ancora una volta, tutto
questo andrebbe implementato a partire da zero.
Infine, vale la pena di ricordare che wx.lib.pubsub non dà nessuna garanzia di recapitare un messaggio ai suoi
destinatari in un ordine predefinito. Gli eventi, d’altra parte, vengono processati nell’ordine determinato dalla catena
dei parent; ed è possibile manipolare ulteriormente lo stack degli handler, come sappiamo.
7.11. Pattern: Publisher/Subscriber.
185
Appunti wxPython Documentation, Release 1
Qt e Wx: diversi approcci agli eventi (una digressione).
Ma questo non vuol dire che quello di wxPython sia l’unico approccio possibile per gli eventi in ambito di applicazioni
gui. Per esempio, Qt percorre una strada differente.
Qt è una delle principali alternative a wxWidgets per quanto riguarda la costruzione di interfacce grafiche desktop.
Come wxWidgets, Qt è un framework scritto in C++. Come wxWidgets, Qt è un framework vasto, robusto e anziano
(Qt è del 1991, Wx del 1992). PyQt e PySide sono due binding per Python di Qt (come wxPython è un binding di
wxWidgets).
La gestione degli eventi in Qt avviene interamente con un meccanismo di tipo pub/sub, che loro chiamano “Signals &
Slots”. L’implementazione di pub/sub di Qt ha avuto un enorme successo e si è estesa anche ad altri ambiti, al punto
che “signal/slot” è un sinonimo comune per “pub/sub”.
In questa pagina si legge, per esempio (traduzione mia, abbreviata e depurata dagli aspetti troppo legati a C++):
Il meccanismo signal/slot è forse la parte che più si differenzia dagli altri framework. (...) Gli altri framework implementano questo tipo di comunicazioni grazie ai callback. (...) I callback hanno due problemi
fondamentali: (...) In secondo luogo, il callback è fortemente accoppiato alla funzione chiamante, dal
momento che questa deve conoscere il callback da chiamare.
Il bersaglio principale, qui, è naturalmente wxWidgets. “Callback” è un termine generico, e inoltre è difficile tradurre
in Python questi concetti legati al mondo C++: tuttavia, il primissimo esempio che abbiamo fatto in questa pagina
(quello con class A e class B) potrebbe dare l’idea di un callback “classico”, con i relativi problemi di accoppiamento a cui alludono gli autori di Qt.
Questa pagina della documentazione Qt fa riferimento, in effetti, a un mondo che nel frattempo è cambiato parecchio.
wxWidgets ha abbandonato il suo originale modello rigido basato sui callback, e un fattore scatenante di questa
trasformazione è stato proprio wxPython con il suo Bind, che è stato introdotto da Robin Dunn e solo in seguito
implementato anche nella versione “madre” del framework (a partire da wxWidgets 2.9). In effetti, un binder svolge
una funzione un po’ simile a quella di un “Publisher” in pub/sub: è un oggetto mediatore che permette di disaccoppiare
eventi, event handler e callback.
In un certo senso, quindi, la gestione degli eventi in wxWidgets è diventata anch’essa più simile al modello pub/sub.
Da un lato, probabilmente, la versione “signal/slot” di Qt resta più elegante e coerente: per esempio, è possibile usare
lo stesso modello per gestire gli eventi nativi dell’interfaccia (clic sui pulsanti, etc.) e per qualsiasi altro messaggio
occorre scambiare tra i componenti. Dall’altro, wxWidgets mette a disposizione un sistema più strutturato e adeguato
alle normali esigenze degli eventi nativi. Quando però c’è bisogno di qualcosa di diverso, occorre integrare in altri
modi.
7.11.5 Event Manager: a metà strada tra eventi e pub/sub.
Abbiamo già introdotto wx.lib.evtmgr: si tratta di una piccola libreria che si appoggia internamente a
wx.lib.pubsub per offrire un modo più elegante di collegare gli eventi, e alcune funzionalità in più rispetto al
tradizionale Bind.
Event Manager non offre comunque tutta la libertà di pub/sub. In effetti, non è facile replicare esattamente la funzionalità del nostro esempio con gli slider sincronizzati, usando solo Event Manager. Una possibile approssimazione, che
ci mostra comunque degli aspetti interessanti, è questa:
1
from wx.lib.evtmgr import eventManager
2
3
4
5
6
7
8
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
self.slider = wx.Slider(p, -1, 50, 0, 100,
style=wx.SL_VERTICAL)
186
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
9
10
check = wx.CheckBox(p, -1, 'connesso')
button = wx.Button(p, -1, 'nuovo')
11
12
13
parent = self.GetParent()
self.target = parent.slider if parent else self.slider
14
15
16
17
18
19
eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)
# usiamo il vecchio Bind per gli altri eventi di routine
# ma potremmo usare Event Manager per tutti
check.Bind(wx.EVT_CHECKBOX, self.on_check)
button.Bind(wx.EVT_BUTTON, self.on_clic)
20
21
22
23
24
25
s = wx.BoxSizer(wx.VERTICAL)
for ctl in (self.slider, check, button):
s.Add(ctl, 0, wx.ALIGN_CENTRE_HORIZONTAL|wx.ALL, 20)
p.SetSizer(s)
s.Fit(self)
26
27
check.SetValue(True)
28
29
30
31
# confronto: EventMananger
|
pub/sub
def update_value(self, evt):
# def update_value(self, message):
self.slider.SetValue(evt.GetInt()) #
self.slider.SetValue(message)
32
33
34
35
36
37
def on_check(self, evt):
if evt.IsChecked():
eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)
else:
eventManager.DeregisterListener(self.update_value)
38
39
40
def on_clic(self, evt):
Test(self).Show()
41
42
43
44
45
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
E’ interessante confrontare il modo in cui comunichiamo il valore da assegnare allo slider: con pub/sub, il valore è il
contenuto del messaggio. Con Event Manager, d’altra parte, trasferiamo pur sempre degli eventi wxPython, e quindi
recuperiamo il valore che ci interessa direttamente dall’evento.
A parte questo, la limitazione di Event Manager è chiara: siccome stiamo comunque registrando eventi wxPython,
dobbiamo sapere quale event handler utilizzare. Nel nostro esempio, abbiamo scelto di sfruttare il fatto che ogni finestra “conosce” il suo parent diretto, e quindi il “target” è lo slider del parent (tranne per la finestra iniziale, che non ha
nessun parent). Questo però limita la trasmissione dell’evento alla catena dei parent: gli slider non restano sincronizzati tra finestre “cugine”. Per ottenere una sincronizzazione completa, dovremmo gestire noi stessi la contabilità delle
finestre aperte in qualche tipo di registro globale, magari da conservare nella wx.App - ma allora, tanto vale ricorrere
direttamente a pub/sub.
Anche se Event Manager non permette tutta la flessibilità di pub/sub, offre comunque qualcosa in più rispetto a
Bind: in particolare, con Event Manager è possibile registrare più ascoltatori per un singolo evento. Potete verificarlo
con l’esempio qui sopra: se generate diverse finestre figlie da una stessa finestra, noterete che un solo parent è in
grado di “comandare” tutti i figli contemporaneamente. Questo non sarebbe possibile collegando l’evento nel modo
tradizionale: potete verificarlo cambiando una riga nel codice dell’esempio qui sopra:
# sostituite questo...
eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)
7.11. Pattern: Publisher/Subscriber.
187
Appunti wxPython Documentation, Release 1
# ... con questo:
self.target.Bind(wx.EVT_SLIDER, self.update_value)
Adesso, se provate a generare più figli dallo stesso parent, noterete che il parent comanda solo l’ultimo della serie.
Note: se avete compreso fino in fondo come funzionano gli eventi in wxPython, forse vi sarà già venuta in mente una
scappatoia. Nel nostro caso specifico, siccome stiamo cercando di sincronizzare widget legati in una catena di parent,
potremmo ancora raggiungere l’effetto desiderato chiamando evt.Skip() nel callback update_value. Tuttavia
questa tecnica è valida solo per questo esempio particolare. Se volessimo registrare, per uno stesso evento, molti
ascoltatori non legati da nessuna particolare parentela, Bind non potrebbe più aiutarci, ed Event Manager sarebbe
l’unica soluzione (oltre a pub/sub, naturalmente).
A conti fatti, Event Manager non è probabilmente lo strumento giusto da usare quando volete notificare un evento ad
ascoltatori arbitrariamente lontani e generati dinamicamente. Se vi serve davvero questo tipo di flessibilità, probabilmente vi conviene usare direttamente wx.lib.pubsub.
Tuttavia, in una tipica applicazione gui, questo scenario non è così frequente. E’ più comune il caso in cui serve
notificare un evento da un “generatore” a un certo numero di “subordinati”: in casi del genere, Event Manager è
perfettamente a suo agio. Potete trovare un esempio del genere nella demo di wxPython (cercate “Event Manager”),
ma il codice è un po’ barocco e difficile da leggere. Ecco una versione estremamente semplificata della stessa idea:
1
from wx.lib.evtmgr import eventManager
2
3
4
5
6
7
class MySlider(wx.Slider):
def __init__(self, parent, id, value, minValue, maxValue, target):
wx.Slider.__init__(self, parent, id, value, minValue,
maxValue, style=wx.SL_VERTICAL)
eventManager.Register(self.update, wx.EVT_SLIDER, target)
8
def update(self, evt):
self.SetValue(evt.GetInt())
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
master_slider = wx.Slider(p, -1, 50, 0, 100,
style=wx.SL_VERTICAL)
s = wx.BoxSizer(wx.HORIZONTAL)
for i in range(10):
slider = MySlider(p, -1, 50, 0, 100, master_slider)
s.Add(slider, 1, wx.EXPAND|wx.ALIGN_CENTRE_HORIZONTAL, 10)
s1 = wx.BoxSizer(wx.VERTICAL)
s1.Add(master_slider, 1, wx.ALIGN_CENTRE_HORIZONTAL, 10)
s1.Add(s, 1, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s1)
26
27
28
29
30
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
188
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
7.11.6 In conclusione...
wxPython mette a disposizione un insieme articolato (anche se obiettivamente disarmonico) di strumenti per gestire la
comunicazione tra i componenti.
Nel caso più semplice (e frequente), gli eventi e Bind sono tutto ciò che vi serve.
Occasionalmente, potreste voler creare un evento personalizzato e postarlo nella coda degli eventi. Questo avviene
tipicamente quando sottoclassate un widget per personalizzarlo; ma è comune anche usare questo metodo per inviare
messaggi personalizzati non-nativi. Inoltre, wx.PostEvent è una delle due tecniche classiche per la comunicazione
inter-thread (l’altra è wrappare una semplice chiamata di funzione in wx.CallAfter).
Todo
una pagina sui thread.
Più raramente ancora, qualche trucco con gli event handler potrebbe aiutarvi, soprattutto a gestire l’ordine di esecuzione dei callback.
Se avete bisogno che diversi widget possano rispondere allo stesso evento, la semplice accoppiata di Bind e Skip
dovrebbe bastare, almeno fino a quando riuscite a organizzare tutti gli attori coinvolti nella stessa catena dei parent.
Ma se questo non fosse possibile (o se risultasse in un’organizzazione troppo innaturale; o se semplicemente non avete
voglia di complicarvi la vita con Skip), allora Event Manager molto probabilmente è ciò che vi serve.
Nei casi in cui neppure Event Manager può darvi una mano, non esitate a ricorrere direttamente a wx.lib.pubsub.
Vi conviene comunque usare pub/sub per tutte le notifiche che non nascono direttamente dagli eventi nativi
dell’interfaccia (oppure potete usare eventi personalizzati e postarli in coda: ma spesso è un’alternativa scomoda).
Ricordate che pub/sub non potrà mai sostituire completamente gli eventi, quindi la vostra applicazione sarà sempre
ibrida: spetta a voi capire fino a che punto usare pub/sub, e quando invece vi conviene lasciare le cose in mano alla
propagazione degli eventi. Inoltre, un messaggio pub/sub è di per sé meno strutturato di un evento wxPython: è possibile che dobbiate definire delle api precise per dare una struttura ai vostri messaggi. Anche la strategia dei topic di
wx.lib.pubsub non è sovrapponibile alla logica di propagazione degli eventi. Ricordate infine che pub/sub non è
thread-safe: se un messaggio pub/sub proveniente da un thread secondario ha come effetto di modificare la gui, dovete
sempre inserirlo in un wx.CallAfter (ma in ogni caso, usare pub/sub per comunicare tra i thread è una strategia
un po’ avventurosa).
In conclusione, l’importante è capire che tutti questi strumenti, usati in modo intelligente, vi aiutano nel fondamentale
compito di mantenere disaccoppiati i componenti, e separare gli ambiti di interesse delle varie sezioni della vostra
applicazione. Questo è il cuore della programmazione a oggetti, e in particolare del pattern Model/Controller/View di
cui parliamo in una pagina separata.
7.12 Un tour delle funzioni globali di wxPython.
wxPython è un framework anziano, vasto e intricato. Anche solo aprire una shell Python e chiedere len(dir(wx))
vi dà un’idea delle sue dimensioni. In quanto framework a oggetti, wxPython mette a disposizione parecchie centinaia
di classi organizzate in una vasta gerarchia. Ma nel namespace wx vivono anche alcune centinaia di funzioni globali,
che rispondono alle più diverse esigenze. Potete rendervi conto facilmente del loro numero:
>>> import wx, types
>>> len([i for i in dir(wx) if isinstance(getattr(wx, i), types.FunctionType)])
In questa pagina presentiamo velocemente queste funzioni, raggruppandole per temi. Naturalmente non ci soffermeremo nel dettaglio su ciascuna di esse: per questo basta ricorrere alla documentazione ufficiale.
7.12. Un tour delle funzioni globali di wxPython.
189
Appunti wxPython Documentation, Release 1
Note: Per non sovraccaricare l’indice di riferimenti inutili, non vi troverete le funzioni elencate in questa pagina.
7.12.1 Static method esposti come funzioni globali.
Quasi la metà delle funzioni globali che affollano il namespace wx sono in realtà degli static method di qualche classe
wxPython. E’ facile riconoscerli, perché il loro nome segue la convenzione [ClassName]_[MethodName]. Un
elenco completo è quindi:
>>> [i for i in dir(wx) if '_' in i]
Bisogna tener presente che wxPython è in circolazione da prima che Pyhton sviluppasse molte delle feature che noi
oggi diamo per scontate. Nei suoi primi anni di vita, wxPython “traduceva” gli occasionali static method delle classi
c++ di wxWidgets mettendoli a disposizione appunto come funzioni globali sciolte direttamente nel namespace wx.
Successivamente, quando Python ha incorporato il supporto per gli static method, anche wxPython li ha introdotti,
seguendo più fedelmente le api di wxWidgets. Di conseguenza, ormai da molti anni queste funzioni globali con
l’underscore esistono solo più per retro-compatibilità, e non dovreste usarle.
La regola di corrispondenza tra funzioni e static method è banale: in pratica, basta sostituire l’underscore con
il punto. Per esempio, al posto di chiamare la funzione wx.DateTime_Now(), dovreste usare il metodo
wx.DateTime.Now() (che, essendo appunto uno static method, può essere usato senza bisogno che la classe
wx.DateTime sia precedentemente istanziata).
L’unica eccezione a questa regola è wx.Thread_IsMain, che può essere usata solo come funzione globale perché la classe wxWidgets wxThread in wxPython non è stata tradotta (e quindi non esiste neppure il metodo
wx.Thread.IsMain).
Altre funzioni in via di dismissione.
wxPython, come è noto, è un porting in Python del framework c++ wxWidgets. La “traduzione” è fatta in larghissima
parte grazie a strumenti automatici, naturalmente: wxPython non esisterebbe senza SWIG. Uno dei limiti “storici”
della tecnica di traduzione adottata da wxPython, era l’impossibilità di convertire correttamente gli “overloaded methods” di c++.
wxWidgets fa uso pervasivo, infatti, di metodi costruttori (ma anche molti metodi setter) che possono accettare
parametri diversi. Molto spesso in wxPython questi “overloaded methods” sono tradotti con diversi metodi separati, ciascuno con un nome e una “signature” differente. Per esempio, come sappiamo, in wxPython abbiamo
wx.Window.SetSize e inoltre .SetDimensions, .SetRect, .SetSizeWH. Tutti questi metodi corrispondono all’unico “overloaded method” wxWindow::SetSize di wxWidgets.
Questa complicazione sta per essere finalmente superata nella nuova incarnazione di wxPython, il cosiddetto “progetto
Phoenix” che è già disponibile in beta e che dovrebbe uscire... in un futuro non ancora determinato. In Phoenix tutti i
“metodi doppioni” non saranno più necessari e scompariranno (metodi come wx.Window.SetSize, per esempio,
decideranno a runtime quale versione del metodo c++ sottostante invocare, a seconda della signature).
Un aspetto rilevante di questo problema è che, allo stato attuale, alcuni di questi “metodi alternativi” sono esposti
come funzioni globali nel namespace wx. Di conseguenza, anche queste funzioni sono in procinto di essere deprecate,
quando uscirà Phoenix. La situazione non è ancora definitiva (alcune cose resteranno, altre saranno deprecate, altre
spariranno proprio), e quindi non avrebbe senso fornire qui un elenco completo. Di seguito elenchiamo comunque
tutte le funzioni globali disponibili in wxPython “classico”, segnalando quelle che sono destinate a diventare obsolete
in Phoenix.
190
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
7.12.2 Funzioni Pre[WidgetName] per la two-step creation.
Un altro blocco molto numeroso è quello delle funzioni globali che iniziano con Pre. Queste sono funzioni di
convenienza che generano un “pre-widget” utilizzato per la cosiddetta “two-step creation”. Si tratta di una tecnica un
po’ convoluta di creazione di un widget, che si usa nei rari casi in cui è necessario settare un extra-style che non può
essere applicato dopo la creazione del widget.
Todo
una pagina sulla two-step creation.
Esiste una funzione globale Pre per ciascun widget di wxPython: abbiamo quindi wx.PreButton,
wx.PreFrame, etc. etc. La loro utilità è già oggi molto vicina a zero, naturalmente.
In Phoenix la “two-step creation” non sarà più necessaria, e tutte le funzioni Pre dovrebbero venire soppresse.
7.12.3 Date e orari.
wxPython mette a disposizione un sotto-sistema per rappresentare date e orari, incardinato sulle classi
wx.DateTime, wx.DateSpan e wx.TimeSpan. In questo ambito si collocano anche alcune scorciatoie esposte
come funzioni globali di varia utilità:
• DateTimeFromDateTime,
DateTimeFromDMY, DateTimeFromHMS, DateTimeFromJDN,
DateTimeFromTimeT,
GetCurrentTime,
GetLocalTime,
GetLocalTimeMillis,
GetUTCTime, Now
Ancora una volta, l’utilità di questo sotto-sistema è molto diminuita da quando esiste il modulo datetime nella
libreria standard di Python. Ma occorre sempre ricordare che wxPython è in circolazione da molto più tempo...
7.12.4 Logging.
wxPython mette a disposizione un sotto-sistema per gestire il logging, centrato sulla classe wx.Log. Per usare il
logging di wxPython con le impostazioni di default, non è necessario tuttavia accedere direttamente a wx.Log: è
sufficiente ricorrere a una delle più comode funzioni globali
• LogDebug, LogError, LogFatalError, LogGeneric, LogInfo, LogMessage, LogStatus,
LogStatusFrame, LogSysError, LogTrace, LogVerbose, LogWarning
Il sistema di logging di wxPython è usato ormai di rado, da quando esiste il modulo logging nella libreria standard
di Python. E’ vero però che wx.Log è più integrato nella logica wxWidgets sottostante a wxPython, e potrebbe fornire
messaggi di errore più completi per i problemi innescati strettamente all’interno del codice wxWidgets.
7.12.5 Drag & Drop.
Le operazioni di Drag & Drop (in sostanza, una forma particolare di copia e incolla) in wxPython sono affidate alle
classi wx.DropSource e wx.DropTarget e ai loro metodi. Alcune funzioni globali integrano delle funzionalità
in questo campo:
• CustomDataFormat,
IsDragResultOk
DragIcon,
DragListItem,
DragString,
DragTreeItem,
Le prime quattro sono funzioni-factory da usare come scorciatoie, e restituiscono una istanza della classe
wx.DragImage già preparata per trascinare diversi componenti (wx.DragImage è a sua volta una classe di convenienza ottimizzata per il trascinamento delle immagini, utile soprattutto in ambiente Windows). Queste funzioni, come
7.12. Un tour delle funzioni globali di wxPython.
191
Appunti wxPython Documentation, Release 1
si intuisce, corrispondono a un costruttore “overloaded” nella corrispondente classe c++. In Phoenix non dovrebbero
pertanto essere più necessarie.
Infine, IsDragResultOk restituisce True per indicare un trascinamento andato a buon fine.
Todo
una pagina sul copia e incolla e drag & drop.
7.12.6 Finding.
Alcune funzioni servono semplicemente per cercare un widget:
• FindWindowAtPoint, FindWindowAtPointer, FindWindowById,
FindWindowByName, GenericFindWindowAtPoint
FindWindowByLabel,
L’esistenza di queste funzioni è più facilmente spiegabile nell’ambito c++ di wxWidgets. In Python, dove i riferimenti agli oggetti possono essere passati liberamente come argomenti di funzioni, l’utilità di meccanismi del tipo
FindWindowBy... è praticamente nulla (è un discorso simile a quello che abbiamo già fatto per gli id). Occasionalmente potreste invece trovare qualche utilità nelle funzioni del tipo FindWindowAtPoint[er], per esempio se
lavorate direttamente con i canvas in applicazioni che disegnano dinamicamente oggetti sullo schermo.
Alcune funzioni globali restituiscono invece le finestre top-level, e la wx.App come sappiamo:
• GetApp, GetTopLevelParent, GetTopLevelWindows
A queste si può aggiungere infine GetActiveWindow, che restituisce il widget attualmente attivo.
7.12.7 Scorciatoie per vari dialoghi.
Alcune funzioni creano e restituiscono istanze già pronte di varie sotto-classi specializzate di wx.Dialog, oppure
usano queste per ottenere input dall’utente e restituiscono direttamente il risultato dopo aver chiuso e distrutto il
dialogo:
• AboutBox, DirSelector, FileSelector, GetColourFromUser, GetFontFromUser,
GetNumberFromUser, GetPasswordFromUser, GetSingleChoice, GetSingleChoiceIndex,
GetTextFromUser, LoadFileSelector, MessageBox, SaveFileSelector
Sono molto comode da usare nei casi più comuni, anziché istanziare direttamente i vari dialoghi specifici
(wx.FontDialog, wx.DirDialog, etc.) e poi chiuderli e distruggerli manualmente.
In questa categoria includiamo anche due funzioni per le wx.TipWindow:
• CreateFileTipProvider, ShowTip
Todo
una pagina sulle sottoclassi di wx.Dialog.
7.12.8 Costruttori di sizer item.
Come sappiamo, un wx.SizerItem è un elemento di un sizer. In genere otteniamo una istanza di questa classe come
valore di ritorno di wx.Sizer.Add, e nella pratica quotidiana non abbiamo mai bisogno di istanziare direttamente
né wx.SizerItem né wx.GBSizerItem (la sottoclasse specializzata per i wx.GridBagSizer).
192
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Ancor meno bisogno, quindi, abbiamo di queste funzioni globali che restituiscono un wx.[GB]SizerItem:
• GBSizerItemSizer,
GBSizerItemSpacer,
SizerItemSpacer, SizerItemWindow
GBSizerItemWindow,
SizerItemSizer,
Inoltre, come è facile intuire, si tratta di funzioni globali che corrispondono a costruttori “overloaded” delle corrispondenti classi c++. In Phoenix niente di tutto questo dovrebbe essere più necessario.
7.12.9 Font.
In wxWidgets e wxPython, la classe wx.Font serve a conservare informazioni relative a un font.
Alcune funzioni globali sono delle scorciatoie per creare una istanza di wx.Font. A parte la prima, le altre non
dovrebbero più servire in Phoenix:
• FFont, FFontFromPixelSize, Font2 (un alias
FontFromNativeInfoString, FontFromPixelSize
di
FFont),
FontFromNativeInfo,
Infine, GetNativeFontEncoding e TestFontEncoding sono relitti del vecchio sistema di supporto degli
encoding di wxWidgets, che in wxPython è completamente superato.
Todo
una pagina sui font
7.12.10 Primitive per il disegno.
Queste funzioni possono essere usate per ricavare informazioni sulla geometria del display (lo schermo o l’area di
lavoro):
• ClientDisplayRect, ColourDisplay, DisplayDepth, DisplaySize, DisplaySizeMM,
GetClientDisplayRect,
GetDisplayDepth,
GetDisplayPPI,
GetDisplaySize,
GetDisplaySizeMM, GetXDisplay
Altre funzioni riguardano differenti primitive per il disegno. Tre di esse permettono di istanziare un wx.Rect (e
probabilmente saranno soppresse in Phoenix per le consuete ragioni):
• RectPP, RectPS, RectS
Inoltre, IntersectRect calcola il rettangolo intersezione di altri due rettangoli.
Due funzioni creano un wx.Point2D (un wx.Point con coordinate float):
• Point2DCopy, Point2DFromPoint
Tre funzioni creano una wx.Region:
• RegionFromBitmap, RegionFromBitmapColour, RegionFromPoints
Due funzioni manipolano un wx.Cursor:
• SetCursor, StockCursor
Infine, in questa categoria includiamo anche due funzioni che creano un qualche tipo di DC:
• AutoBufferedPaintDCFactory, MemoryDCFromDC
Todo
7.12. Un tour delle funzioni globali di wxPython.
193
Appunti wxPython Documentation, Release 1
una pagina su come disegnare
7.12.11 Immagini e colori.
Molte funzioni globali lavorano con le immagini. Per iniziare, molte sono funzioni di conversione, e hanno la
forma [someClass]From[someClass]. In Phoenix dovrebbero essere tutte soppresse o quasi: in molti casi
basterà usare il costruttore “overloaded” (per esempio, wx.BitmapFromImage(image) sarà deprecata in favore di wx.Bitmap(image)); in altri casi, verranno introdotti degli static methods corrispondenti (per esempio,
wx.BitmapFromBuffer() diventerà wx.Bitmap.FromBuffer()):
• BitmapFromBits,
BitmapFromBuffer,
BitmapFromBufferRGBA, BitmapFromIcon,
BitmapFromImage,
BitmapFromXPMData,
BrushFromBitmap,
CursorFromImage,
IconBundleFromFile, IconBundleFromIcon, IconBundleFromStream, IconFromBitmap,
IconFromLocation,
IconFromXPMData,
ImageFromBitmap,
ImageFromBuffer,
ImageFromData,
ImageFromDataWithAlpha,
ImageFromMime,
ImageFromStream,
ImageFromStreamMime
Alcune funzioni restituiscono una classe “vuota” (anche queste dovrebbero sparire in Phoenix):
• EmptyBitmap, EmptyBitmapRGBA, EmptyIcon, EmptyImage
Tre funzioni costruiscono un wx.Colour (e non saranno più necessarie in Phoenix):
• ColourRGB, MacThemeColour, NamedColour
Infine, InitAllImageHandlers era usata per inizializzare gli handler dei tipi di immagini disponibili per
wx.Image (ormai da tempo questa funzione è una NOP lasciata per retro-compatibilità: l’inizializzazione avviene di
default quando si crea la wx.App).
Todo
una pagina sulle immagini: wx.Image, wx.Bitmap...
7.12.12 Eventi, thread di esecuzione.
Abbiamo dedicato ormai molte pagine agli eventi, e non c’è quindi più bisogno di spendere parole a proposito di
queste funzioni globali:
• CallAfter, NewEventType, PostEvent, SafeYield, Yield, YieldIfNeeded, WakeUpIdle
Alcune funzioni gestiscono processi esterni:
• Execute, GetProcessId, Kill, LaunchDefaultApplication, LaunchDefaultBrowser,
Shell, SysErrorCode, SysErrorMsg
Due funzioni globali hanno a che fare direttamente con i thread:
• Thread_IsMain, WakeUpMainThread
Così come wxPython non adotta il supporto per i thread di wxWidgets, preferendo affidarsi alla libreria standard di
Python, anche wxMutex di wxWidgets è assente in wxPython. Queste due funzioni globali sono ancora in giro per
retro-compatibilità:
• MutexGuiEnter, MutexGuiLeave
Infine, alcune funzioni per “dormire”:
• MicroSleep, MilliSleep, Sleep, Usleep
194
Chapter 7. Appunti wxPython - livello avanzato
Appunti wxPython Documentation, Release 1
Todo
una pagina sui thread
7.12.13 Informazioni sul sistema.
wxPython è dotato di alcuni strumenti per ottenere informazioni sull’ambiente in cui deve operare: per esempio la
classe wx.PlatformInfo, ma anche alcune funzioni globali che elenchiamo qui di seguito. Spesso si tratta di
strumenti che possono essere sostituiti con successo da moduli come sys e os nella libreria standard di Python. Ma
alcune funzioni più specifiche possono tornare utili.
Poche funzioni raccolgono informazioni sull’hardware:
• GetBatteryState, GetPowerType
Altre funzioni riguardano il sistema operativo:
• ExpandEnvVars,
GetFreeMemory,
GetFullHostName,
GetHostName,
GetLocale,
GetOsDescription, GetOsVersion, IsPlatform64Bit, IsPlatformLittleEndian,
Shutdown
Alcune ci fanno sapere qualcosa sull’utente loggato:
• GetEmailAddress, GetHomeDir, GetUserHome, GetUserId, GetUserName
Infine, due funzioni ci aiutano in particolare con il supporto Unicode di Python 2:
• GetDefaultPyEncoding, SetDefaultPyEncoding
7.12.14 Varie.
Raccogliamo qui alcune funzioni che non rientrano in nessuna delle categorie precendenti.
Due funzioni possono essere utilizzate per gestire la chiusura di emergenza della wx.App:
• Exit, SafeShowMessage
Trap solleva una eccezione nel debugger, ovvero il flusso di controllo passa al debugger (se avete un debugger
associato al processo Python in corso, naturalmente: se no il programma si limita a terminare in modo anomalo).
Alcune funzioni interrogano lo stato di mouse e tastiera, e impostano la “clessidra” del cursore:
• BeginBusyCursor, EndBusyCursor, GetKeyState, GetMousePosition, GetMouseState,
IsBusy
Tre funzioni manipolano gli id, come sappiamo:
• GetCurrentId, NewId, RegisterId
Alcune funzioni riguardano gli stock buttons:
• GetStockHelpString, GetStockLabel, IsStockID, IsStockLabel
Di StripMenuCodes abbiamo parlato a proposito dei menu; anche GetAccelFromString rientra nello stesso
ambito, ma sarà deprecata in Phoenix e peraltro non è mai stata particolarmente utile.
EnableTopLevelWindows può essere usata come valvola di sicurezza per congelare temporaneamente tutta la
gui. Per esempio, è usata internamente da wx.SafeYeld.
GetTranslation riguarda il supporto per il testo multilingue.
7.12. Un tour delle funzioni globali di wxPython.
195
Appunti wxPython Documentation, Release 1
FileTypeInfoSequence e NullFileTypeInfo creano un wx.FileTypeInfo (c’entra il supporto MIME
di wxPython).
Bell suona il system bell (!), deprecated può essere usato per emettere una deprecation warning personalizzata,
SoundFromData costruisce un wx.Sound, version restituisce la versione in uso di wxPython.
196
Chapter 7. Appunti wxPython - livello avanzato
CHAPTER 8
Ricette wxPython
8.1 Catturare tutti gli eventi di un widget.
Come abbiamo visto parlando degli eventi, non è facile in generale sapere quali eventi può emettere un determinato
widget.
Questa ricetta cerca di scoprirlo con un approccio naif: semplicemente collega a un widget tutti i binder disponibili
nel namespace wx. Chiaramente la maggior parte non sarà mai davvero emessa, ma poco importa.
Potete inserire il widget che desiderate testare, alla riga 8.
Ho eliminato gli eventi “collettivi” come wx.EVT_MOUSE_EVENTS (riga 31). Inoltre alcuni eventi sono innescati
di continuo, e producono “rumore di fondo”: vi conviene eliminarli. Alle righe 23-25 ne ho eliminati alcuni che trovo
particolarmente fastidiosi, ma potete regolarvi come preferite.
In ogni caso, per aiutare a rendere l’output più pulito, se l’evento è ripetuto viene stampato solo un trattino (righe
41-45).
1
import wx
2
3
4
5
6
7
8
class TopFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
# qui mettete il widget che volete testare, al posto del wx.Button
test_widget = wx.Button(p, -1, 'test me!')
9
10
11
12
13
14
self.output = wx.TextCtrl(p, -1, style=wx.TE_MULTILINE|wx.TE_READONLY)
s = wx.BoxSizer(wx.VERTICAL)
s.Add(test_widget, 1, wx.EXPAND|wx.ALL, 5)
s.Add(self.output, 1, wx.EXPAND|wx.ALL, 5)
p.SetSizer(s)
15
16
17
18
19
binder_names = [i for i in dir(wx) if i.startswith('EVT_')]
# questi due non si possono collegare con Bind:
binder_names.remove('EVT_COMMAND')
binder_names.remove('EVT_COMMAND_RANGE')
20
21
22
23
24
25
# qui rimuovete gli eventi che non volete registrare:
# per es. questi si ripetono di continuo e producono rumore di fondo
for b in ('EVT_IDLE', 'EVT_UPDATE_UI', 'EVT_UPDATE_UI_RANGE',
'EVT_MOTION', 'EVT_SET_CURSOR'):
binder_names.remove(b)
26
197
Appunti wxPython Documentation, Release 1
self.binder_dict = {}
27
28
for binder_name in binder_names:
obj_binder = getattr(wx, binder_name)
if len(obj_binder.evtType) == 1:
test_widget.Bind(obj_binder, self.callback)
binder_type = obj_binder.evtType[0]
self.binder_dict[binder_type] = binder_name
29
30
31
32
33
34
35
self.last_printed_event = ''
36
37
def callback(self, evt):
evt.Skip()
evt_type = self.binder_dict[evt.GetEventType()]
if evt_type != self.last_printed_event:
txt = '\n%s %s' % (evt.__class__.__name__, evt_type)
self.last_printed_event = evt_type
else:
txt = '-'
self.output.AppendText(txt)
38
39
40
41
42
43
44
45
46
47
48
49
50
51
app = wx.App(False)
TopFrame(None, title='Event Test').Show()
app.MainLoop()
52
Questa ricetta è uno script che avevo scritto per capire meglio come vengono generati gli eventi. In seguito mi sono
accorto che nella libreria di wxPython è già compresa una versione molto più “professionale” e completa della stessa
idea, nel modulo wx.lib.eventwatcher.py. Se lo eseguite, avvia una demo che mostra gli eventi di un frame
di prova. Ma potete usarlo per monitorare gli eventi di qualsiasi frame scritto da voi stessi. Il suo uso è semplicissimo:
from wx.lib.eventwatcher import EventWatcher
my_frame = MyFrame(...)
my_frame.Show()
ew = EventWatcher(my_frame)
ew.watch(my_frame)
ew.Show()
Per alcuni (pochi) aspetti preferisco ancora la mia versione: per esempio, EventWatcher cattura solo un
wx.EVT_BUTTON senza segnalare i contestuali wx.EVT_LEFT_DOWN e wx.EVT_LEFT_UP.
8.2 Un widget per selezionare periodi di tempo.
Ho usato questo widget (che avevo scritto in origine per un progetto “vero”) per esemplificare la scrittura di eventi
personalizzati.
Il suo funzionamento è semplice: permette di selezionare un periodo (per esempio un trimestre), e restituisce gli
estremi del periodo come datetime.date.
1
2
3
import datetime
import wx
import wx.lib.newevent
4
5
PeriodEvent, EVT_PERIOD_MODIFIED = wx.lib.newevent.NewCommandEvent()
198
Chapter 8. Ricette wxPython
Appunti wxPython Documentation, Release 1
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
class PeriodWidget(wx.Panel):
"""Un widget per selezionare periodi fissi. Chiamarlo con l'argomento
aggiuntivo "period" imposastato a 1 (per selezionare mesi) oppure
2 (bimestri), 3 (trimestri), 4 (quadrimestri), o 6 (semestri)."""
months = 'January February March April May June July August September October November December'.
spans = ',, bimester, trimester, quadrimester,, semester'.split(',')
def __init__(self, *a, **k):
self.period = k.pop('period')
wx.Panel.__init__(self, *a, **k)
if self.period == 1:
ch = self.months
else:
ch = [str(i)+self.spans[self.period] for i in range(1, (12/self.period)+1)]
self.choose_period = wx.ComboBox(self, -1, choices=ch,
style=wx.CB_DROPDOWN|wx.CB_READONLY)
self.choose_period.SetSelection(0)
self.choose_year = wx.SpinCtrl(self, initial=datetime.datetime.now().year,
min=1800, max=2200, size=((80, -1)))
s = wx.BoxSizer()
s.Add(self.choose_period, 1, wx.EXPAND|wx.RIGHT, 5)
s.Add(self.choose_year, 0, wx.FIXED_MINSIZE|wx.LEFT, 5)
self.SetSizer(s)
self.choose_period.Bind(wx.EVT_COMBOBOX, self.on_changed)
self.choose_year.Bind(wx.EVT_SPINCTRL, self.on_changed)
31
32
33
34
35
def on_changed(self, evt):
my_event = PeriodEvent(self.GetId())
my_event.SetEventObject(self)
self.GetEventHandler().ProcessEvent(my_event)
36
37
38
39
40
41
42
43
44
45
def GetValue(self):
start = datetime.date(
self.choose_year.GetValue(),
((self.period * self.choose_period.GetSelection()) + 1),
1)
end = ((start +
datetime.timedelta(days=(30*self.period)+15)).replace(day=1) datetime.timedelta(days=1))
return start, end
46
47
48
49
50
51
52
class TestFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
period = PeriodWidget(self, period=1)
period.Bind(EVT_PERIOD_MODIFIED, self.on_period)
53
54
55
def on_period(self, evt):
print evt.GetEventObject().GetValue()
56
57
58
59
60
app = wx.App(False)
TestFrame(None).Show()
app.MainLoop()
8.2. Un widget per selezionare periodi di tempo.
199
Appunti wxPython Documentation, Release 1
8.3 Un pulsante che controlla le credenziali prima di procedere.
Questo pulsante può essere usato al posto di un normale wx.Button, ed è uguale in tutto e per tutto. La sola
differenza è che, in risposta a un EVT_BUTTON (un normale clic), chiede all’utente di inserire una password, e solo
in caso positivo passa a eseguire il callback associato.
8.3.1 La prima versione.
1
2
import wx
3
4
5
6
7
8
# una funzione dummy per verificare la password
def check_psw(psw):
if psw == 'secret':
return True
return False
9
10
11
12
13
14
15
16
class CheckPermissionButton(wx.Button):
"""Un wx.Button che chiede la password all'utente
prima di procedere."""
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_LEFT_UP, self.on_leftup)
17
def on_leftup(self, evt):
msg = 'Inserire la password per procedere:'
cpt = 'Password richiesta.'
if check_psw(wx.GetPasswordFromUser(msg, cpt)):
wx.PostEvent(
self.GetEventHandler(),
wx.PyCommandEvent(wx.EVT_BUTTON.typeId, self.GetId()))
else:
wx.MessageBox('Password errata!', 'Password errata',
wx.ICON_ERROR|wx.OK)
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class TestFrame(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = CheckPermissionButton(p, -1, 'clic me', pos=((50, 50)))
b.Bind(wx.EVT_BUTTON, self.onclic)
36
def onclic(self, evt):
wx.MessageBox(
"Password corretta, procedo con l'azione prevista.")
# e qui il codice previsto del callback, come di consueto
37
38
39
40
41
42
43
44
45
app = wx.App(False)
TestFrame(None).Show()
app.MainLoop()
46
Ecco come si può manipolare la catena degli eventi per ottenere effetti un po’ più concreti degli esempi presentati nei
capitoli dedicati agli eventi. Nel mondo reale, chiaramente, la funzione check_psw potrebbe essere sostituita da un
200
Chapter 8. Ricette wxPython
Appunti wxPython Documentation, Release 1
controllo su nome utente e password (confrontando con un database), o con un più avanzato sistema di permessi.
E’ utile ripeterlo: non è necessario intervenire sulla propagazione degli eventi per ottenere un effetto del genere. Per
esempio, si potrebbe facilmente scrivere un decoratore da applicare ai singoli callback che necessitano di un controllo
sulle credenziali (a-la-Django, per intenderci). E questo potrebbe essere un metodo più “corretto” da usare, perché
non ricorre a una specificità della gui per la logica di business dell’applicazione.
Tuttavia è utile anche sapere che queste cose, quando serve, si possono fare restando all’interno della logica di wxPython.
8.3.2 Approfondiamo il problema.
Ciò detto, questa ricetta richiede qualche parola in più per spiegare lo strano giro che abbiamo dovuto fare, per ricevere
un primo evento ed emetterne subito dopo un secondo.
Il problema generale, qui, è che è molto difficile organizzare una catena “ordinata” di callback in wxPython. Come
regola generale, se avete bisogno di inserire più di un callback in risposta a un evento, è meglio scrivere il codice in
modo tale che non sia importante l’ordine in cui i callback sono eseguiti.
Se però l’ordine di esecuzione è importante (ed è il nostro caso: vogliamo che il controllo della password avvenga
prima del resto), allora siate pronti a fare salti mortali.
Questo esempio minimo riproduce il problema che incontriamo nel nostro caso:
class MyButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.Bind(wx.EVT_BUTTON, self.check_psw)
def check_psw(self, evt):
print 'controllo della password!'
evt.Skip()
class Test(wx.Frame):
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = MyButton(p, -1, 'clic', pos=((50, 50)))
b.Bind(wx.EVT_BUTTON, self.on_clic)
# self.Bind(wx.EVT_BUTTON, self.on_clic, b)
def on_clic(self, evt):
print 'qualche operazione con permessi privilegiati'
evt.Skip()
if __name__ == '__main__':
app = wx.App(False)
Test(None).Show()
app.MainLoop()
Se adesso fate girare questo esempio, vi accorgete che l’ordine dei callback è tragicamente invertito: prima viene
eseguita l’operazione critica, e poi si chiede la password all’utente!
Questo avviene perché il callback collegato dinamicamente con Bind ha la precedenza su quello definito nella classe
madre. Una soluzione sarebbe intercettare l’evento non direttamente nel pulsante che lo genera, ma nel suo parent,
utilizzando il “secodo stile” di binding che abbiamo visto. Provate a far girare il codice sostituendo b.Bind...
con self.Bind..., e vedrete che adesso “funziona”. Infatti l’evento, prima di arrivare all’handler del panel (che
8.3. Un pulsante che controlla le credenziali prima di procedere.
201
Appunti wxPython Documentation, Release 1
provoca l’esecuzione del codice privilegiato) ha il tempo di passare per l’handler della classe madre, che innesca il
controllo della password.
Tuttavia questa soluzione non è molto sicura: ci fidiamo del fatto che il codice cliente utilizzi lo stile “giusto” di
binding per collegare il suo evento, in modo da metterlo educatamente in coda dietro al nostro. In molti casi possiamo
conviverci serenamente: basta documentare bene come il nostro pulsante deve essere usato.
Ma se non vogliamo correre questo rischio, dobbiamo fare un po’ fatica. Nella versione iniziale di questa ricetta,
abbiamo sfruttato il fatto che un wx.Button, come abbiamo visto, emette prima un wx.EVT_LEFT_DOWN e un
wx.EVT_LEFT_UP, e solo allora il wx.EVT_BUTTON che in genere viene usato dal codice cliente. Di conseguenza,
nella nostra classe madre, abbiamo intercettato il wx.EVT_LEFT_UP che sicuramente viene innescato prima, e ne
abbiamo approfittato per fare il nostro controllo di sicurezza.
Purtroppo però, una volta intercettato, quell’evento è “consumato”. Possiamo chiamare Skip, ma anche questo si
riferisce solo al wx.EVT_LEFT_UP: non c’è modo di “far tornare al via” il successivo wx.EVT_BUTTON, per farlo
intercettare dal pulsante nel codice cliente. Ormai quell’handler è stato oltrepassato.
Ecco perché abbiamo dovuto diramare un wx.EVT_BUTTON fresco, pronto a essere usato dal pulsante.
Se questa soluzione vi sembra troppo macchinosa... c’è un aspetto anche peggiore! Il nostro accrocchio funziona
come dovrebbe solo fintanto che il codice cliente si limita a intercettare wx.EVT_BUTTON. Ma se per qualche ragione
volesse intercettare anche lui wx.EVT_LEFT_UP (o peggio ancora, wx.EVT_LEFT_DOWN), saremmo di nuovo al
problema della “gara degli handler” che abbiamo visto prima.
E non solo: se vogliamo dirla tutta, il nostro approccio funziona solo perché stiamo parlando un wx.Button, che
fortunatamente ha la caratteristica di emettere tre eventi in successione. Ma se volessimo generalizzare il problema
con un altro widget, che emette un solo evento per volta, saremmo punto e a capo.
8.3.3 La seconda versione.
Se questo trucco vi sembra un po’ troppo sporco, in effetti esiste una soluzione più definitiva: basta scrivere un handler
personalizzato, e spingerlo in cima allo stack degli handler. Nel nostro handler gestiremo l’evento con l’operazione
prioritaria che ci sta a cuore.
Un esempio vale più di mille parole, come sempre:
1
2
3
4
5
class MyEvtHandler(wx.PyEvtHandler):
def __init__(self):
wx.PyEvtHandler.__init__(self)
self.Bind(wx.EVT_BUTTON, self.onclic)
6
def onclic(self, evt):
msg = 'Inserire la password per procedere:'
cpt = 'Password richiesta.'
if check_psw(wx.GetPasswordFromUser(msg, cpt)):
evt.Skip()
else:
wx.MessageBox('Password errata!', 'Password errata',
wx.ICON_ERROR|wx.OK)
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyButton(wx.Button):
def __init__(self, *a, **k):
wx.Button.__init__(self, *a, **k)
self.PushEventHandler(MyEvtHandler())
21
22
23
class Test(wx.Frame):
202
Chapter 8. Ricette wxPython
Appunti wxPython Documentation, Release 1
24
25
26
27
def __init__(self, *a, **k):
wx.Frame.__init__(self, *a, **k)
p = wx.Panel(self)
b = MyButton(p, -1, 'clic', pos=((50,50)))
28
29
30
# self.Bind(wx.EVT_BUTTON, self.onclic, b)
b.Bind(wx.EVT_BUTTON, self.onclic)
31
32
33
34
35
def onclic(self, evt):
wx.MessageBox(
"Password corretta, procedo con l'azione prevista.")
# e qui il codice previsto del callback, come di consueto
36
Abbiamo semplicemente spostato la logica del controllo della password da MyButton a MyEvtHandler. La vera
magia è, naturalmente, chiamare PushEventHandler per portare il nostro handler in cima alla lista degli handler
del pulsante. Una volta che ci siamo assicurati che il nostro codice sarà eseguito per primo, il resto è un gioco da
ragazzi: se la password è corretta chiamiamo Skip e lasciamo propagare l’evento. Se no, tutto si ferma lì. Notate
anche che adesso il codice fa quello che vogliamo indipendentemente dallo “stile” di binding preferito dal codice
sorgente.
8.4 Convertire le date tra Python e wxPython.
Questa ricetta è un vero e proprio classico, e ne trovate diverse versioni in rete. Vale la pena comunque di riportarla, e
avercela sottomano.
Python utilizza per le date la comoda struttura datetime.datetime, e in genere vogliamo lavorare con
questa. Purtroppo wxPython deve ereditare il formato del framework c++ sottostante, e quindi utilizza la classe
wx.DateTime, che però è più scomoda da usare, e soprattutto incompatibile con qualsiasi cosa al di fuori di wxPython.
Niente paura, basta usare queste pratiche funzioni di conversione tra i due formati (la cosa migliore sarebbe tenerle in
un qualche “utils.py” da dove importarle alla bisogna):
import wx, datetime
def pydate2wxdate(pydate):
'Accetta una datetime.datetime e restutuisce una wx.DateTime'
tt = pydate.timetuple()
dmy = (tt[2], tt[1]-1, tt[0])
return wx.DateTimeFromDMY(*dmy)
def wxdate2pydate(wxdate):
'Accetta una wx.DateTime e restutuisce una datetime.datetime'
ymd = map(int, wxdate.FormatISODate().split('-'))
return datetime.datetime(*ymd)
In realtà, col passare del tempo queste funzioni di conversione diventano sempre meno utili, perché i diversi widget
di wxPython si sono arricchiti nel frattempo di metodi “pythonici” che accettano e restituiscono oggetti datetime.
Per esempio, CalendarCtrl accanto ai vecchi metodi GetDate e SetDate ha anche le versioni PyGetDate
e PySetDate, e così molti altri widget. Prima di utilizzare questa ricetta, controllate quindi sempre che il vostro
widget non sia già pronto a fare il lavoro sporco dietro le quinte.
8.4. Convertire le date tra Python e wxPython.
203
Appunti wxPython Documentation, Release 1
204
Chapter 8. Ricette wxPython
CHAPTER 9
TODO list.
9.1 Argomenti che vorrei trattare in futuro.
9.1.1 Grandi temi.
Questi sono temi impegnativi: varie pagine, complesse.
• DISEGNARE. I wx.DC, e tutti gli strumenti per il disegno.
• Pattern MCV
• Unit test
9.1.2 Argomenti più specifici.
Questi sono argomenti più contenuti, anche se talvolta molto tecnici.
• La creazione di widget personalizzati, a partire da PyControl.
• Le immagini.
• wx.ListCtrl
• Timers
• Threads
9.2 Rimandi ai “todo” nelle pagine già scritte.
Todo
una pagina su SWIG e l’oop Python/C++.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/chiusura.rst, line 219.)
Todo
una pagina su SWIG e l’oop Python/C++.
205
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/chiusuraavanzata.rst, line 137.)
Todo
una pagina per la toobar e la statusbar?
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/contenitori.rst, line 38.)
Todo
non riesco a consigliare nessun sito: fare una nuova indagine.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/documentarsi.rst, line 92.)
Todo
una pagina su SWIG e l’oop Python/C++.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eccezioni.rst, line 11.)
Todo
una pagina su SWIG e l’oop Python/C++.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eccezioni2.rst, line 110.)
Todo
una pagina sui pycontrols
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventi_tecniche.rst, line 165.)
Todo
una pagina sui thread , una pagina sui timer , una pagina sulla clipboard
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventi_tecniche2.rst, line 167.)
Todo
una pagina sui pycontrols (cfr paragrafo seguente)
206
Chapter 9. TODO list.
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventi_tecniche2.rst, line 198.)
Todo
una pagina sui thread.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventi_tecniche2.rst, line 300.)
Todo
una pagina sui thread
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventloop.rst, line 234.)
Todo
una pagina su mcv
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/eventloop.rst, line 437.)
Todo
una pagina sulla two-step creation.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 50.)
Todo
una pagina sul copia e incolla e drag & drop.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 88.)
Todo
una pagina sulle sottoclassi di wx.Dialog.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 120.)
Todo
una pagina sui font
9.2. Rimandi ai “todo” nelle pagine già scritte.
207
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 146.)
Todo
una pagina su come disegnare
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 179.)
Todo
una pagina sulle immagini: wx.Image, wx.Bitmap...
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 199.)
Todo
una pagina sui thread
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/funzioni.rst, line 225.)
Todo
una pagina su MCV.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/integrazione_event_loop.rst, line 55.)
Todo
una pagina sui timer.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/integrazione_event_loop.rst, line 158.)
Todo
una pagina su MVC | una pagina su pub/sub.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/integrazione_event_loop.rst, line 206.)
Todo
una pagina sui thread: accorciare tutto questo paragrafo di conseguenza.
208
Chapter 9. TODO list.
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/integrazione_event_loop.rst, line 361.)
Todo
una pagina sui thread.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/logging.rst, line 221.)
Todo
una pagina sul debugging.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/logging2.rst, line 104.)
Todo
una pagina sul debugging.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/logging2.rst, line 114.)
Todo
una pagina su SWIG e l’oop Python/C++.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/logging2.rst, line 412.)
Todo
una pagina sulle immagini
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/menu_avanzate.rst, line 31.)
Todo
una pagina su mvc.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/menu_avanzate.rst, line 326.)
Todo
una pagina su MVC con collegamento a questa.
9.2. Rimandi ai “todo” nelle pagine già scritte.
209
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/nonusare.rst, line 59.)
Todo
una pagina su MCV con riferimenti a questa.
(The
original
entry
is
located
in
wxpython/checkouts/latest/source/pubsub.rst, line 16.)
/home/docs/checkouts/readthedocs.org/user_builds/appunti-
Todo
una pagina sui thread.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/pubsub.rst, line 351.)
Todo
una pagina sulla two-step creation.
(The
original
entry
is
located
in
wxpython/checkouts/latest/source/stili.rst, line 137.)
/home/docs/checkouts/readthedocs.org/user_builds/appunti-
Todo
una pagina sui pycontrols
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/validatori.rst, line 23.)
Todo
una pagina sulla validazione “in tempo reale” (avanzata? un’aggiunta a questa?)
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/validatori.rst, line 260.)
Todo
una pagina su MCV con molti riferimenti a questa.
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/validatoridue.rst, line 17.)
Todo
una pagina sui thread.
210
Chapter 9. TODO list.
Appunti wxPython Documentation, Release 1
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/wxapp_avanzata.rst, line 368.)
Todo
una pagina sui thread
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/wxapp_basi.rst, line 84.)
Todo
una pagina su MVC!
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/wxapp_basi.rst, line 90.)
Todo
una pagina sui test
(The
original
entry
is
located
in
/home/docs/checkouts/readthedocs.org/user_builds/appuntiwxpython/checkouts/latest/source/wxapp_basi.rst, line 117.)
• genindex
9.2. Rimandi ai “todo” nelle pagine già scritte.
211
Appunti wxPython Documentation, Release 1
212
Chapter 9. TODO list.
Index
B
bitmask, 28
C
child window, 15
chiusura
di finestre a cascata, 151
di un dialogo, 21, 64
di un frame, 64
di una wx.App, 70
wx.App.ExitMainLoop, 74
wx.App.SetExitOnFrameDelete, 72
wx.Event.CanVeto, 67, 74
wx.Event.Veto, 65, 74
wx.EVT_QUERY_END_SESSION, 74
wx.Exit, 74
wx.PyDeadObjectError, 67, 177
wx.SafeShowMessage, 74
wx.Window.Close, 64, 69, 151
wx.Window.Destroy, 64, 69, 151
wx.Window.DestroyChildren, 69
wx.Window.IsBeingDeleted, 153
constraints, 115
wx.IndividualLayoutConstraint, 115
wx.LayoutConstraints, 115
wx.Window.Layout, 115
wx.Window.SetAutoLayout, 115
wx.Window.SetConstraints, 115
D
date, 203
wx.DateTime, 203
demo di wxPython, 7
dialogo, 18
chiusura, 21, 64
con pulsanti predefiniti, 21, 25
con risposte predefinite, 25
con validazione automatica, 21, 26, 103
wx.Dialog, 21, 25, 26
wx.Dialog.ShowModal, 21
wx.MessageDialog, 25
wx.Window.Destroy, 21
dimensioni in wxPython, 74
wx.EVT_SIZE, 76
wx.Size, 75
wx.Sizer.Fit, 76
wx.Sizer.Layout, 76
wx.Window.Fit, 76
wx.Window.GetBestSize(Tuple), 75
wx.Window.GetClientSize(Tuple), 75
wx.Window.GetMaxSize, 75
wx.Window.GetMinSize, 75
wx.Window.GetSize(Tuple), 75
wx.Window.GetVirtualSize(Tuple), 75
wx.Window.Layout, 76
wx.Window.SendSizeEvent, 76
wx.Window.SetAutoLayout, 76
wx.Window.SetClientSize(WH), 75
wx.Window.SetInitialSize, 75
wx.Window.SetMaxSize, 75
wx.Window.SetMinSize, 75
wx.Window.SetSize(WH), 75
wx.Window.SetSize*, 75
wx.Window.SetSizeHints(Sz), 75
wx.Window.SetVirtualSize(WH), 75
wx.Window.SetVirtualSizeHints(Sz), 75
documentazione, 7
EventsinStyle, 8
libri, 8
wiki, 8
E
eccezioni
eccezioni Python, 171
eccezioni wxPython, 175
sys.excepthook, 175
type checking, 179
wx.App.GetAssertMode, 175
wx.App.SetAssertMode, 175
wx.PYAPP_ASSERT_*, 175
wx.PyAssertionError, 175
213
Appunti wxPython Documentation, Release 1
wx.PyDeadObjectError, 67, 177
eventi, 35, 82, 119, 125
Bind, 38, 89
binder, 37
binding con ’partial’, 119
blocchi, 127
callback, 35
categorie, 128
command event, 83
Event Manager, 120, 186
event type, 36
eventi personalizzati, 120, 198
filtri, 125
handler, 36, 130
handler personalizzati, 129, 202
lambda binding, 27, 119
loop degli eventi, 133, 142
metodi e proprietà, 40
processamento, 83
propagazione, 83, 89, 184
Skip, 83, 86
wx.App.FilterEvent, 125
wx.App.SafeYield, 136
wx.App.SetCallFilterEvent, 125
wx.App.Yield, 136, 144
wx.CommandEvent, 83
wx.Event, 35
wx.Event.CanVeto, 67, 74
wx.Event.GetEventCategory, 128
wx.Event.SetEventObject, 198
wx.Event.Skip, 83, 86
wx.Event.Veto, 65, 74
wx.EventBlocker, 127
wx.EVT_*, 37
wx.EVT_CLOSE, 64, 70, 151
wx.EVT_CONTEXT_MENU, 92
wx.EVT_IDLE, 134, 144
wx.EVT_MENU, 44
wx.EVT_MENU_RANGE, 52
wx.EVT_QUERY_END_SESSION, 74
wx.EVT_SIZE, 76
wx.EvtHandler, 36
wx.EvtHandler.Bind, 38, 89
wx.EvtHandler.GetNextHandler, 130
wx.EvtHandler.GetPreviousHandler, 130
wx.EvtHandler.IsUnlinked, 130
wx.EvtHandler.ProcessEvent, 83, 122
wx.EvtHandler.SetEvtHandlerEnabled, 83, 130
wx.EvtHandler.SetNextHandler, 130
wx.EvtHandler.SetPreviousHandler, 130
wx.EvtHandler.Unbind, 130
wx.EvtHandler.Unlink, 130
wx.GUIEventLoop.IsYielding, 136
wx.GUIEventLoop.Yield, 136, 144
214
wx.GUIEventLoop.YieldFor, 136
wx.IdleEvent.RequestMore, 144
wx.lib.newevent.NewCommandEvent, 124, 198
wx.lib.newevent.NewEvent, 124
wx.NewEventType, 121
wx.PostEvent, 122, 200
wx.PyCommandEvent, 121, 200
wx.PyEvent, 121
wx.PyEventBinder, 37, 121
wx.PyEventBinder.Bind, 38
wx.PyEvtHandler, 129, 202
wx.PyEvtHandler.ProcessEvent, 198
wx.SafeYield, 136
wx.WakeUpIdle, 134, 144
wx.Window.GetEventHandler, 198
wx.Window.PopEventHandler, 130
wx.Window.PushEventHandler, 129, 202
wx.Window.SendSizeEvent, 76
wx.wxEVT_*, 36
wx.wxEVT_CATEGORY_*, 128
wx.YeldIfNeeded, 136
wx.Yield (deprecato, usare wx.App.Yield), 136
F
frame, 18
chiusura, 64
stili, 18
wx.Frame, 18
funzioni globali, 189
I
icone
nei menu, 92
id in wxPython, 23
dialogo con pulsanti predefiniti, 25
dialogo con risposte predefinite, 25
dialogo con validazione automatica, 26
stock buttons, 25
uso nei menu, 27
wx.FindWindowById, 24
wx.ID_ANY, 23
wx.ID_CANCEL, 25
wx.ID_HIGHEST, 23
wx.ID_LOWEST, 23
wx.ID_NO, 25
wx.ID_OK, 25, 103
wx.ID_YES, 25
wx.NewId, 23
wx.RegisterId, 23
wx.Window.GetId, 24
wx.Window.SetId, 24
IPython, 150
Index
Appunti wxPython Documentation, Release 1
L
logging
con Python, 157
con wxPython, 162
wx.Log, 162
wx.Log*, 162
wx.Log.Flush, 165
wx.Log.Resume, 165
wx.Log.SetActiveTarget, 165
wx.Log.SetLogLevel, 162
wx.Log.Suspend, 165
wx.LOG_*, 162
wx.LogBuffer, 165
wx.LogChain, 170
wx.LogGui, 165
wx.LogNull, 167
wx.LogStderr, 165
wx.LogTextCtrl, 165
wx.LogWindow, 165
wx.PyLog, 168
wx.PyLog.DoLogRecord, 168
wx.PyLog.DoLogRecordAtLevel, 168
wx.PyLog.DoLogText, 168
loop degli eventi, 133
integrare loop esterni, 142
personalizzati, 140
stack dei loop, 138
wx.App.GetMainLoop, 135
wx.App.OnEventLoopEnter, 135, 138
wx.App.OnEventLoopExit, 135, 138
wx.EventLoop (alias per GUIEventLoop), 133
wx.EventLoopActivator, 138
wx.EventLoopBase, 133
wx.EVT_IDLE, 134
wx.GUIEventLoop, 133
wx.GUIEventLoop.Dispatch, 134
wx.GUIEventLoop.Exit, 135, 138
wx.GUIEventLoop.GetActive, 135, 138
wx.GUIEventLoop.IsMain, 135
wx.GUIEventLoop.IsRunning, 135
wx.GUIEventLoop.IsYielding, 136
wx.GUIEventLoop.Pending, 134, 135
wx.GUIEventLoop.ProcessIdle, 134
wx.GUIEventLoop.Run, 134, 140
wx.GUIEventLoop.SetActive, 135, 138
wx.GUIEventLoop.Yield, 138
wx.GUIEventLoop.YieldFor, 136
M
menu, 41, 46, 92
abilitare e disabilitare, 49
acceleratori, 46
contestuali, popup, 92
icone, 92
Index
scorciatoie, 46
sottomenu, 43
spuntabili e selezionabili, 50
tecniche di fattorizzazione, 97
tecniche di manipolazione, 96
uso degli id, 27
wx.AcceleratorEntry, 47
wx.AcceleratorTable, 47
wx.EVT_CONTEXT_MENU, 92
wx.EVT_MENU, 27, 44
wx.EVT_MENU_RANGE, 27, 52
wx.Frame
PopupMenu, 92
wx.ITEM_*, 50
wx.Menu, 41
wx.Menu.Append, 42
wx.Menu.AppendMenu, 43
wx.Menu.AppendSeparator, 43
wx.MenuBar, 41
wx.MenuBar.EnableTop, 49
wx.MenuBar.IsEnabledTop, 49
wx.MenuItem, 42
wx.MenuItem.Check, 50
wx.MenuItem.IsChecked, 50
wx.MenuItem.SetBitmap, 92
wx.StripMenuCodes, 46
P
panel, 18
tab trasversing, 19
wx.Panel, 19
wx.TAB_TRASVERSAL, 19
parent window, 15
parent, catena dei, 15, 184
wx.App.GetTopWindow, 17
wx.App.SetTopWindow, 17
wx.GetTopLevelParent, 17
wx.GetTopLevelWindows, 17
wx.Window.GetChildren, 17
wx.Window.GetGrandParent, 17
wx.Window.GetTopLevelParent, 17
wx.Window.SetParent, 16
posizionamento assoluto, 31
pub/sub, 180
confronto con gli eventi, 184
confronto con signal/slot, 185
wx.lib.pubsub, 182
Pygame, 150
Q
Qt, 185
S
sizer, 31, 77
215
Appunti wxPython Documentation, Release 1
dimensioni dei widget, 74
SendSizeEvent, 76
wx.BoxSizer, 32
wx.EVT_SIZE, 76
wx.FlexGridSizer, 78
wx.GridBagSizer, 78
wx.GridSizer, 77
wx.HORIZONTAL, 32
wx.Sizer, 32
wx.Sizer.Add, 33, 81
wx.Sizer.AddSpacer, 34
wx.Sizer.AddStretchSpacer, 34
wx.Sizer.Fit, 76
wx.Sizer.Layout, 76
wx.SizerItem, 81
wx.StaticBoxSizer, 79
wx.StdDialogButtonSizer, 79
wx.VERTICAL, 32
wx.Window.CreateButtonSizer, 79
wx.Window.Fit, 76
wx.Window.Layout, 76
wx.Window.SendSizeEvent, 81
wx.Window.SetSizer, 32
wx.WrapSizer, 80
stdout/err
wx.App.outputWindowClass, 60
wx.App.RedirectStdio, 60
wx.PyOnDemandOutputWindow, 60
stili, 28
di un frame, 18
extra-style, 30, 103
two-step creation, 30
wx.PreFrame, 30
wx.Window.SetExtraStyle, 30, 103
wx.Window.SetWindowStyleFlag, 29
stock buttons, 25, 79
strumenti
Boa Constructor, 11
Editra, 9
EventsinStyle, 8, 29
wxGlade, 11
XmlResource, 12
T
top-level window, 17
Twisted, 151
two-step creation, 30
V
validatore, 99
composizione, 106
trasferimento dati, 109
validazione a cascata, 100, 101, 106
validazione automatica, 21, 26, 103, 109
216
validazione ricorsiva, 103, 108
wx.PyValidator, 26, 100
wx.PyValidator.Clone, 100
wx.PyValidator.TransferFromWindow, 109
wx.PyValidator.TransferToWindow, 109
wx.PyValidator.Validate, 100
wx.Window.SetValidator, 103
wx.WX_EX_VALIDATE_RECURSIVELY, 103
W
wx
PyDeadObjectError, 67, 177
wx.AcceleratorEntry, 47
wx.AcceleratorTable, 47
wx.App, 13, 57, 70
chiusura, 70
ExitMainLoop, 74
FilterEvent, 125
GetAssertMode, 175
GetMainLoop, 135
GetTopWindow, 17
MainLoop, 14, 57, 70, 133
OnEventLoopEnter, 135, 138
OnEventLoopExit, 135, 138
OnExit, 59, 70
OnInit, 57
outputWindowClass, 60
RedirectStdio, 60
SafeYield, 136
SetAssertMode, 175
SetCallFilterEvent, 125
SetExitOnFrameDelete, 72
SetTopWindow, 17
Yield, 136, 144
wx.BoxSizer, 32
wx.Button
SetDefault, 19
wx.CallAfter, 72, 147
wx.CallLater, 72
wx.CommandEvent, 83
wx.DateTime, 203
wx.Dialog, 21, 25, 26, 64
ShowModal, 21, 138
wx.Event, 35
CanVeto, 67, 74
GetEventCategory, 128
SetEventObject, 198
Skip, 83, 86
Veto, 65, 74
wx.EventBlocker, 127
wx.EventLoop (alias per GUIEventLoop), 133
wx.EventLoopActivator, 138
wx.EventLoopBase, 133
wx.EVT_*, 37
Index
Appunti wxPython Documentation, Release 1
wx.EVT_CLOSE, 64, 70, 151
wx.EVT_CONTEXT_MENU, 92
wx.EVT_IDLE, 134, 144
wx.EVT_MENU, 27, 44
wx.EVT_MENU_RANGE, 27, 52
wx.EVT_QUERY_END_SESSION, 74
wx.EVT_SIZE, 76
wx.EvtHandler, 36
Bind, 38
GetNextHandler, 130
GetPreviousHandler, 130
IsUnlinked, 130
ProcessEvent, 83, 122
SetEvtHandlerEnabled, 83, 130
SetNextHandler, 130
SetPreviousHandler, 130
Unbind, 130
Unlink, 130
wx.Exit, 74
wx.FindWindowById, 24
wx.FlexGridSizer, 78
wx.Frame, 18, 64
PopupMenu, 92
wx.GetTopLevelParent, 17
wx.GetTopLevelWindows, 17
wx.GridBagSizer, 78
wx.GridSizer, 77
wx.GUIEventLoop, 133
Dispatch, 134
Exit, 135, 138
GetActive, 135, 138
IsMain, 135
IsRunning, 135
IsYielding, 136
Pending, 134, 135
ProcessIdle, 134
Run, 134, 140
SetActive, 135, 138
Yield, 136, 138, 144
YieldFor, 136
wx.HORIZONTAL, 32
wx.ID_ANY, 23
wx.ID_CANCEL, 25
wx.ID_HIGHEST, 23
wx.ID_LOWEST, 23
wx.ID_NO, 25
wx.ID_OK, 25, 103
wx.ID_YES, 25
wx.IdleEvent
RequestMore, 144
wx.IndividualLayoutConstraint, 115
wx.ITEM_* (nei menu), 50
wx.LayoutConstraints, 115
wx.lib
Index
pubsub, 182
wx.lib.evtmgr
eventManager, 120, 186
wx.lib.newevent
NewCommandEvent, 124, 198
NewEvent, 124
wx.Log, 162
Flush, 165
Resume, 165
SetActiveTarget, 165
SetLogLevel, 162
Suspend, 165
wx.Log*, 162
wx.LOG_*, 162
wx.LogBuffer, 165
wx.LogChain, 170
wx.LogGui, 165
wx.LogNull, 167
wx.LogStderr, 165
wx.LogTextCtrl, 165
wx.LogWindow, 165
wx.Menu, 41
Append, 42
AppendMenu, 43
AppendSeparator, 43
wx.MenuBar, 41
EnableTop, 49
IsEnabledTop, 49
wx.MenuItem, 42
Check, 50
IsChecked, 50
SetBitmap, 92
wx.MessageDialog, 25
wx.MilliSleep, 134
wx.NewEventType, 121
wx.NewId, 23
wx.Panel, 19
wx.PostEvent, 122, 200
wx.PreFrame, 30
wx.PYAPP_ASSERT_*, 175
wx.PyAssertionError, 175
wx.PyCommandEvent, 121, 200
wx.PyEvent, 121
wx.PyEventBinder, 37, 121
Bind, 38
wx.PyEvtHandler, 129, 202
ProcessEvent, 198
wx.PyLog, 168
DoLogRecord, 168
DoLogRecordAtLevel, 168
DoLogText, 168
wx.PyOnDemandOutputWindow, 60
wx.PyValidator, 26, 100
Clone, 100
217
Appunti wxPython Documentation, Release 1
TransferFromWindow, 109
TransferToWindow, 109
Validate, 100
wx.RegisterId, 23
wx.SafeShowMessage, 74
wx.SafeYield, 136
wx.Size, 75
wx.Sizer, 32
Add, 33, 81
AddSpacer, 34
AddStretchSpacer, 34
Fit, 76
Layout, 76
wx.SizerItem, 81
wx.StaticBoxSizer, 79
wx.StdDialogButtonSizer, 79
wx.StripMenuCodes, 46
wx.TAB_TRASVERSAL, 19
wx.VERTICAL, 32
wx.WakeUpIdle, 134, 144
wx.Window
Close, 64, 69, 70, 151
CreateButtonSizer, 79
Destroy, 21, 64, 69, 151
DestroyChildren, 69
Fit, 76
GetBestSize(Tuple), 75
GetChildren, 17
GetClientSize(Tuple), 75
GetEventHandler, 198
GetGrandParent, 17
GetId, 24
GetMaxSize, 75
GetMinSize, 75
GetSize(Tuple), 75
GetTopLevelParent, 17
GetVirtualSize(Tuple), 75
IsBeingDeleted, 153
Layout, 76, 115
PopEventHandler, 130
PushEventHandler, 129, 202
SendSizeEvent, 76, 81
SetAutoLayout, 76, 115
SetClientSize(WH), 75
SetConstraints, 115
SetExtraStyle, 30, 103
SetId, 24
SetInitialSize, 75
SetMaxSize, 75
SetMinSize, 75
SetParent, 16
SetSize(WH), 75
SetSize*, 75
SetSizeHints(Sz), 75
218
SetSizer, 32
SetValidator, 103
SetVirtualSize(WH), 75
SetVirtualSizeHints(Sz), 75
SetWindowStyleFlag, 29
wx.WrapSizer, 80
wx.WX_EX_VALIDATE_RECURSIVELY, 103
wx.wxEVT_*, 36
wx.wxEVT_CATEGORY_*, 128
wx.xrc
XmlResource, 12
wx.YeldIfNeeded, 136
wx.Yield (deprecato, usare wx.App.Yield), 136
Index