Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano...

234
Appunti wxPython Documentation Release 1 Riccardo Polignieri Sep 27, 2017

Transcript of Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano...

Page 1: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython DocumentationRelease 1

Riccardo Polignieri

Sep 27, 2017

Page 2: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”
Page 3: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Contents

1 Novità e cambiamenti (man mano che ce ne sono...). 1

2 Introduzione. 32.1 Licenza. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.2 Che cos’è wxPython? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.3 Convenzioni usate in questi appunti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42.4 Piccolo glossario. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

3 Documentazione wxPython. 73.1 La demo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73.2 Gli altri esempi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73.3 La documentazione wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83.4 La documentazione wxWidget. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83.5 Events in Style! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83.6 Libri wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83.7 Siti wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93.8 Un buon editor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93.9 La vecchia buona shell. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

4 Gli strumenti da non usare. 114.1 Boa Constructor. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114.2 wxGlade. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114.3 XmlResource. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

5 Appunti wxPython - livello base 155.1 wx.App: le basi da sapere. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

5.1.1 Che cosa è una wx.App e come lavorarci. . . . . . . . . . . . . . . . . . . . . . . . . . . . 155.1.2 Il MainLoop: il motore della gui. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165.1.3 L’entry-point di un programma wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

5.2 La catena dei “parent”. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185.2.1 Dichiarare il “parent”. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185.2.2 Orientarsi nell’albero dei “parent”. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195.2.3 Le finestre top-level. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

5.3 Frame, dialoghi, panel: contenitori wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205.3.1 wx.Frame. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205.3.2 wx.Panel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225.3.3 wx.Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

i

Page 4: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

5.4 Gli Id in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255.4.1 Assegnare gli Id. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265.4.2 Lavorare con gli Id. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275.4.3 Quando gli Id possono tornare utili. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

5.5 I flag di stile. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305.5.1 Che cos’è una bitmask. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315.5.2 Conoscere i flag di stile di un widget. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315.5.3 Sapere quali stili sono stati applicati a un widget. . . . . . . . . . . . . . . . . . . . . . . . 325.5.4 Settare gli stili dopo che il widget è stato creato. . . . . . . . . . . . . . . . . . . . . . . . . 325.5.5 Che cosa sono gli extra-style. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

5.6 I sizer: le basi da sapere. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335.6.1 Non usate il posizionamento assoluto. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335.6.2 Usate i sizer, invece. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345.6.3 Che cosa è un sizer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345.6.4 wx.BoxSizer: il modello più semplice. . . . . . . . . . . . . . . . . . . . . . . . . . . . 355.6.5 Add in dettaglio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

5.7 Gli eventi: le basi da sapere. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375.7.1 Gli attori coinvolti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385.7.2 Bind: collegare eventi e callback, in pratica. . . . . . . . . . . . . . . . . . . . . . . . . . 415.7.3 Altri modi di usare Bind. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415.7.4 Sapere quali eventi possono originarsi da un widget. . . . . . . . . . . . . . . . . . . . . . 425.7.5 Estrarre informazioni su un evento nel callback. . . . . . . . . . . . . . . . . . . . . . . . . 425.7.6 Un esempio conclusivo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

5.8 I menu: le basi da sapere. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445.8.1 Come creare una barra dei menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445.8.2 Come creare i menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445.8.3 Come creare le voci di menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 455.8.4 Come creare un separatore. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465.8.5 Come creare un sotto-menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465.8.6 Collegare le voci di menu a eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475.8.7 Conclusione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

5.9 I menu: altri concetti di base. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495.9.1 Scorciatoie da tastiera. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495.9.2 Disabilitare i menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525.9.3 Voci di menu spuntabili o selezionabili. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535.9.4 Ranged events per i menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 555.9.5 Conclusione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

5.10 Questioni varie di stile. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565.10.1 To self or not to self? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 575.10.2 Costruire il layout nell’__init__ o no? . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

6 Appunti wxPython - livello intermedio 616.1 wx.App: concetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

6.1.1 wx.App.OnInit: il bootstrap della vostra applicazione. . . . . . . . . . . . . . . . . . . 616.1.2 wx.App.OnExit: gestire le operazioni di chiusura. . . . . . . . . . . . . . . . . . . . . . 636.1.3 Re-indirizzare lo standard output/error. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

6.2 Chiudere i frame e gli altri widget. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 686.2.1 La chiusura di una finestra. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 686.2.2 Chiamare Veto() se non si vuole chiudere. . . . . . . . . . . . . . . . . . . . . . . . . . 706.2.3 Ignorare il Veto() se si vuole chiudere lo stesso. . . . . . . . . . . . . . . . . . . . . . . . 716.2.4 Essere sicuri che una finestra si chiuda davvero. . . . . . . . . . . . . . . . . . . . . . . . . 726.2.5 Distruggere un singolo widget. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

6.3 Terminare la wx.App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 746.3.1 La chiusura “normale”. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

ii

Page 5: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

6.3.2 Come mantenere in vita la wx.App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 766.3.3 Altri modi di terminare la wx.App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 786.3.4 Situazioni di emergenza. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

6.4 Le dimensioni in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 796.4.1 wx.Size: la misura delle dimensioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 796.4.2 Gli strumenti per definire le dimensioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 806.4.3 Fit e Layout: ricalcolare le dimensioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

6.5 I sizer: seconda parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 816.5.1 wx.GridSizer: una griglia rigida. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 816.5.2 wx.FlexGridSizer: una griglia elastica. . . . . . . . . . . . . . . . . . . . . . . . . . 826.5.3 wx.GridBagSizer: una griglia ancora più flessibile. . . . . . . . . . . . . . . . . . . . . 836.5.4 wx.StaticBoxSizer: un sizer per raggruppamenti logici. . . . . . . . . . . . . . . . . 836.5.5 StdDialogButtonSizer e CreateButtonSizer: sizer per pulsanti generici. . . . . 846.5.6 wx.WrapSizer: un BoxSizer che sa quando “andare a capo”. . . . . . . . . . . . . . . 846.5.7 Esempi di utilizzo dei sizer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 856.5.8 wx.SizerItem, e modificare il layout a runtime. . . . . . . . . . . . . . . . . . . . . . . 86

6.6 Gli eventi: concetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 876.6.1 La propagazione degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 876.6.2 Come un evento viene processato. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 886.6.3 Riassunto dei passaggi importanti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 916.6.4 Come funziona Skip(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 916.6.5 Un esempio per Skip(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 926.6.6 Bind e la propagazione degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 946.6.7 Bind per gli eventi “non command”. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 956.6.8 Un esempio finale per la propagazione degli eventi. . . . . . . . . . . . . . . . . . . . . . . 96

6.7 I menu: concetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 976.7.1 Icone nelle voci di menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 976.7.2 Menu contestuali e popup. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 976.7.3 Manipolare dinamicamente i menu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1016.7.4 Come “fattorizzare” la creazione dei menu. . . . . . . . . . . . . . . . . . . . . . . . . . . 102

6.8 Validatori: prima parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1046.8.1 Come scrivere un validatore. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1046.8.2 Quando fallisce una validazione a cascata. . . . . . . . . . . . . . . . . . . . . . . . . . . . 1066.8.3 La validazione ricorsiva. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1086.8.4 SetValidator: cambiare il validatore assegnato. . . . . . . . . . . . . . . . . . . . . . . 1086.8.5 La validazione automatica dei dialoghi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1086.8.6 Consigli sulla validazione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

6.9 Validatori: seconda parte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1146.9.1 Trasferimento dati nei dialoghi con validazione automatica. . . . . . . . . . . . . . . . . . . 1146.9.2 Trasferimento dati negli altri casi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1186.9.3 Conclusioni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

7 Appunti wxPython - livello avanzato 1217.1 I constraints: un modo alternativo di organizzare il layout. . . . . . . . . . . . . . . . . . . . . . . . 121

7.1.1 wx.IndividualLayoutConstraint e wx.LayoutConstraints. . . . . . . . . . 1217.1.2 Quando i constraints possono tornare utili. . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

7.2 Gli eventi: altre tecniche. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1257.2.1 Lambda binding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1257.2.2 Partial binding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1267.2.3 Event Manager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1267.2.4 Eventi personalizzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

7.3 Gli eventi: altre tecniche (seconda parte). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1317.3.1 Filtri. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1327.3.2 Blocchi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

iii

Page 6: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

7.3.3 Categorie. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1347.3.4 Handler personalizzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1357.3.5 Un esempio finale per la propagazione degli eventi (aggiornato). . . . . . . . . . . . . . . . 138

7.4 Il loop degli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1397.4.1 Il loop degli eventi e il main loop dell’applicazione. . . . . . . . . . . . . . . . . . . . . . . 1407.4.2 Yield e i suoi compagni. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1437.4.3 Loop secondari. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1447.4.4 Creare loop degli eventi personalizzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1467.4.5 Perché manipolare il loop degli eventi? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

7.5 Come integrare event loop esterni in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1487.5.1 Il problema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1487.5.2 Soluzione 1: usare Yield. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1517.5.3 Soluzione 2: catturare wx.EVT_IDLE. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1517.5.4 Soluzione 3: usare un timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1527.5.5 Soluzione 4: gestire manualmente gli eventi. . . . . . . . . . . . . . . . . . . . . . . . . . . 1527.5.6 Soluzione 5: rovesciare il rapporto tra loop principale e ospite. . . . . . . . . . . . . . . . . 1537.5.7 Soluzione 6: usare un thread separato. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1547.5.8 Integrare altri framework in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

7.6 Chiudere i widget: aspetti avanzati. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1587.6.1 Distruzione di finestre a cascata. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1587.6.2 Trappole legate alla distruzione dei widget. . . . . . . . . . . . . . . . . . . . . . . . . . . 160

7.7 Logging in wxPython (1a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1647.7.1 Logging con Python. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1657.7.2 Re-indirizzare il log verso la gui. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1657.7.3 Loggare da thread differenti. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1687.7.4 In conclusione... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169

7.8 Logging in wxPython (2a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1697.8.1 Logging con wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1697.8.2 Cambiare il log target. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1717.8.3 Scrivere un log target personalizzato. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1747.8.4 Il conclusione: come loggare in wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . 177

7.9 Gestione delle eccezioni in wxPython (1a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1787.9.1 Il problema delle eccezioni Python non catturate. . . . . . . . . . . . . . . . . . . . . . . . 1787.9.2 try/except in wxPython non funziona sempre come vi aspettate. . . . . . . . . . . . . . 1797.9.3 Che cosa fare delle eccezioni Python non gestite. . . . . . . . . . . . . . . . . . . . . . . . 180

7.10 Gestione delle eccezioni in wxPython (2a parte). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1827.10.1 wx.PyAssertionError: gli assert C++ tradotti in Python. . . . . . . . . . . . . . . . . 1827.10.2 wx.PyDeadObjectError e il problema della distruzione dei widget. . . . . . . . . . . . 1847.10.3 Le “%typemap” di SWIG e il type checking in wxPython. . . . . . . . . . . . . . . . . . . 1867.10.4 Consigli conclusivi su logging e gestione delle eccezioni. . . . . . . . . . . . . . . . . . . . 186

7.11 Pattern: Publisher/Subscriber. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1887.11.1 Che cosa è pub/sub. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1887.11.2 wx.lib.pubsub: l’implementazione wxPython di pub/sub. . . . . . . . . . . . . . . . . 1897.11.3 Un esempio di architettura pub/sub in wxPython. . . . . . . . . . . . . . . . . . . . . . . . 1907.11.4 Messaggi pub/sub ed eventi wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1917.11.5 Event Manager: a metà strada tra eventi e pub/sub. . . . . . . . . . . . . . . . . . . . . . . 1937.11.6 In conclusione... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196

7.12 Un tour delle funzioni globali di wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1967.12.1 Static method esposti come funzioni globali. . . . . . . . . . . . . . . . . . . . . . . . . . . 1977.12.2 Funzioni Pre[WidgetName] per la two-step creation. . . . . . . . . . . . . . . . . . . . 1987.12.3 Date e orari. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1987.12.4 Logging. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1987.12.5 Drag & Drop. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1987.12.6 Finding. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199

iv

Page 7: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

7.12.7 Scorciatoie per vari dialoghi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1997.12.8 Costruttori di sizer item. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1997.12.9 Font. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2007.12.10 Primitive per il disegno. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2007.12.11 Immagini e colori. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2017.12.12 Eventi, thread di esecuzione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2017.12.13 Informazioni sul sistema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2027.12.14 Varie. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202

8 Ricette wxPython 2058.1 Catturare tutti gli eventi di un widget. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2058.2 Un widget per selezionare periodi di tempo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2078.3 Un pulsante che controlla le credenziali prima di procedere. . . . . . . . . . . . . . . . . . . . . . . 208

8.3.1 La prima versione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2088.3.2 Approfondiamo il problema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2098.3.3 La seconda versione. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210

8.4 Convertire le date tra Python e wxPython. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211

9 TODO list. 2139.1 Argomenti che vorrei trattare in futuro. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213

9.1.1 Grandi temi. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2139.1.2 Argomenti più specifici. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213

9.2 Rimandi ai “todo” nelle pagine già scritte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213

v

Page 8: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

vi

Page 9: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

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ù aggiuntonulla 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, edi conseguenza

• la clausola if __name__ == '__main__' è stata aggiunta ovunque, per non suggerire involontari-amente cattive abitudini;

• una tour delle funzioni globali di wxPython, che a sua volta ha provocato piccole modifiche ad alcune altrepagine, 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:

1

Page 10: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• 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;

• aggiornata la ricetta del pulsante che chiede la password, con una implementazione che fa uso delletecniche 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...).

Page 11: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 2

Introduzione.

Benvenuti. Queste note non sono un manuale completo: però cerco di essere sistematico e andare per gradi, quindiecco... diciamo che aspirano a essere un manuale. Se non completo almeno buono, e comunque l’unico in italiano diun certo “spessore” (almeno a mia conoscenza).

Questa prima pagina contiene le solite note introduttive... se non sapete nulla di wxPython, cominciate da qui. Altri-menti, leggete il glossario in fondo, e per il resto basta un’occhiata veloce.

Licenza.

Questi appunti sono distribuiti con licenza Creative Commons BY-NC-SA 3.0. Detto in breve, siete liberi di tagliaree copiare e incollare e modificare... ma dovete sempre citarmi ((c) Riccardo Polignieri - 2012), e non potete fare usocommerciale di queste cose.

Che cos’è wxPython?

wxPython è un GUI framework: un set organizzato di strumenti per scrivere l’interfaccia grafica delle vostre appli-cazioni. 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 almondo desktop.

wxWidgets non ha un toolkit grafico suo proprio, ma i vari port sono realizzati “appoggiandosi” a diversi toolkitesterni: su Windows si utilizzano le Win API, su Mac Carbon o Cocoa, su Linux si usa GTK, etc. Questa è in generaleun’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 spessoin feature che vengono “tradotte” in modo leggermente diverso sulle diverse piattaforme, o che sono specifiche solo diuna di esse.

3

Page 12: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

L’implementazione più ampia è sicuramente quella per Windows. Questo significa che, se sviluppate in Windows mavi interessa mantenere la compatibilità sulle altre piattaforme, dovete prestare particolare attenzione a non introdurreelementi “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 Pythonrispetto a C++. Inoltre la comunità wxPython, negli anni, si è data molto da fare, e adesso molti widget aggiuntivisono disponibili solo in wxPython, e altri sono stati riscritti con un’interfaccia “pure python”. Tuttavia c’è un puntocritico 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 chenoi oggi diamo per scontate (datetime, per esempio). Nel corso degli anni, molti “wxPython regrets” si sono accu-mulati, e oggi Robin Dunn è al lavoro per una nuova riscrittura dell’intero framework, che prende il nome di “progettoPhoenix”. Il supporto per Python 3 potrebbe arrivare con l’arrivo di Phoenix, oppure potrebbe essere anticipato sePhoenix dovesse dimostrarsi un lavoro troppo lungo.

In ogni caso, oggi wxPython è ancora indietro rispetto allo sviluppo di Python. Oltre a questo, occorre tener presenteche wxPython è pur sempre una sottile buccia di codice Python sopra un framework C++. Per quanto wxPython facciaun lavoro meraviglioso nell’adattare le cose dietro le quinte, per chi è abituato a scrivere in Python, l’API di wxPythonsembra 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: percui, sadicamente, non aggiungo altro.

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 convenzionidi 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 Camel-Case, 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 -> CamelCasepass

def on_click(self, evt): # metodo aggiunto da me -> min_con_underscorepass

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 dovrebbesempre essere), ma i commenti e le docstring sono in italiano (visto che questa è pur sempre una guida in italiano).

Piccolo glossario.

Può essere complicato orientarsi nella gergo di wxPython, e a maggior ragione tradurlo in italiano. Ecco un piccologlossario delle cose fondamentali:

4 Chapter 2. Introduzione.

Page 13: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• “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, caselledi controllo etc.

• “dialogo” (cioè, finestra di dialogo) è un wx.Dialog o affine: l’aspetto esteriore può essere identico a quellodi 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 unafinestra. 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 unarelazione padre-figlio (che non ha niente a che vedere con la normale gerarchia delle classi). Ne parliamodiffusamente 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 widgetdentro i contenitori.

• “evento”, in generale, è una azione che l’utente compie sulla vostra gui. In modo più specifico, può riferirsi auna 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

Page 14: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

6 Chapter 2. Introduzione.

Page 15: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 3

Documentazione wxPython.

wxPython è un framework vasto e complesso, e soprattutto all’inizio può disorientare. Questa sezione elenca una seriedi 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 laragione dell’esistenza di questi appunti. Tuttavia non potete prescindere da un po’ di fatica linguistica, se volete farequalche progresso con wxPython (e con Python, e con la programmazione in generale).

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 varipacchetti). Una volta aperta, presenta un elenco di esempi organizzati in un albero che potete sfogliare. Ciascunesempio mostra anche il suo codice sorgente e delle note utili. Potete anche fare delle modifiche direttamente nelcodice, e vedere subito il risultato.

L’unico problema della demo è che spesso gli esempi sono eccessivamente complessi: nello sforzo di dimostrare tuttele possibilità dei vari widget, il codice si allunga a dismisura, e non è sempre facile orientarsi.

Gli altri esempi.

Scaricando il pacchetto della demo, installate anche alcune piccole applicazioni di esempio che trovate installatein .../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 sonointeressanti 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;

7

Page 16: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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.

La documentazione wxPython.

La documentazione delle API di wxPython è solo online. Potete trovare la versione “ufficiale” generata con Epydoca questo indirizzo, ma vi consiglio di quardare prima quella “alternativa” mantenuta da Andrea Gavana qui. Questaseconda documentazione non è ancora completa, ma è generata con Sphinx, ed è molto più chiara e ricca. Dovrebbediventare 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...

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.8Docs 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 è neppureproprio facilissimo.

Tuttavia la documentazione wxWidgets resta in certi casi l’unica vera risorsa. Tra l’altro, al suo interno si trovanoanche delle note specifiche per wxPython (e wxPerl!), nei punti in cui le API differiscono.

Events in Style!

“Events in Style” è un modulo del sempre geniale Andrea Gavana (disponibile alla pagina, semplicemente salvate lapagina). E’ una piccola gui che punta alla documentazione online (va usata con una connessione internet) e scarica laparte 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 nessunoriesce mai a ricordarsi.

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 particolar-mente esotico.

• wxPython 2.8 Application Development Cookbook di Cody Precord (l’autore dell’IDE Editra, vedi sotto), è unmanuale più avanzato, che offre anche esempi di buone pratiche di programmazione. Leggetelo solo se avetegià un’idea di base di come funziona wxPython. Altrimenti può disorientare.

8 Chapter 3. Documentazione wxPython.

Page 17: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Siti wxPython.

Ce ne sono troppi.

Il problema qui è che wxPython è un framework anziano e popolare, il che significa che negli anni si è accumulata unaimpressionante 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 degliesempi superati. Tuttavia, molte pagine sono invece assolutamente ben scritte e aggiornate.

Un buon editor.

Sembra facile, ma se lavorate con un framework complesso come wxPython, scordatevi IDLE. Avete bisogno di uneditor che faccia almeno queste cose:

• code folding: il codice wxPython tende ad essere lungo. Senza il code folding, passerete la vita a fare scrollingsu e giù.

• autocompletion: come per tutti i framework complessi, il problema numero uno è orientarsi nella selva delleclassi 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 automaticamentequando scrivete il nome. Perché il problema numero tre è ricordarsi l’infinità di named arguments che può avereun 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 casodi ricorrere per forza a elefanti come Eclipse. Non è questa la sede per aprire l’eterna discussione su quale editorutilizzare. Se non avete proprio nessuna idea, potete provare Editra: è un IDE abbastanza completo, scritto da CodyPrecord nientemeno che in wxPython. E’ diventato un po’ l’editor “ufficiale” di wxPython, e quindi è incluso nelledistribuzioni che scaricate, ma vi conviene visitare il sito per avere la versione più aggiornata, scaricare i plugin, etc.

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.7. Siti wxPython. 9

Page 18: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

10 Chapter 3. Documentazione wxPython.

Page 19: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 4

Gli strumenti da non usare.

Avvertenza: questa pagina è “opinionated”, come si dice. La includo in questi appunti per mantenere una rispostapronta 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 comunquequesta è solo la mia opinione, e siete liberi di tenervi la vostra.

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 frattempowxPython è andato molto avanti, e (cosa ancora più importante) nessuno testa più Boa in modo sistematico da unavita.

Detto questo, Boa cerca di essere un RAD per wxPython, e in quanto tale ha tutti i difetti tipici di un RAD. Producecodice sporchissimo, sul quale comunque dovrete prima o poi mettere le mani, per qualunque applicazione appena unpo’ completa, perché ci sono molte cose che Boa non supporta o supporta male. Come in tutti i RAD, è difficilissimofattorizzare 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 siavvicinano a wxPython con la speranza di trovare una specie di Visual Basic “free”, e Boa è la cosa che somiglia dipiù 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 dovrebbeportarsi questa convinzione nel dna. Ma in ogni caso, Boa è poco soddisfacente anche come RAD.

wxGlade.

E non usate neppure wxGlade.

11

Page 20: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Devo dire che wxGlade mi sta molto più simpatico di Boa. Si propone come semplice “assemblatore di gui”, senzaavere la pretesa di essere un RAD completo, e questo è senz’altro un bene. Inoltre, di recente sembra aver ripreso unpo’ 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 natu-ralmente 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 finireteper doverci mettere le mani comunque. Forse è un po’ più facile, rispetto a Boa, generare “porzioni” di interfacciariutilizzabili, ma comunque una vera fattorizzazione del codice resta un miraggio.

Forse wxGlade può essere adatto a costruire rapidamente interfacce non troppo complesse. Ma il problema è che, conun po’ di esperienza, proprio le interfacce semplici sono quelle che fate prima a realizzare a mano, producendo codicepiù compatto e pulito. Inoltre, se poi l’interfaccia cresce nel tempo e dovete metterci le mani, vi ritrovate a gestire ilsolito 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à diogni oggetto wxPython. Ma anche in questo caso, probabilmente si fa prima a imparare scrivendo il codice a mano.

XmlResource.

Molti sostengono che il codice per disegnare le gui sia boilerplate degradante da scrivere. Costoro in genere sostengonoche la gui dovrebbe essere definita da una risorsa esterna (tipicamente un file xml) e caricata dinamicamente dentro ilprogramma.

Gli fai notare che così bisogna sempre lottare per ficcare in uno schema xml tutte le sottigliezze espressive che puoimolto più facilmente ottenere con qualche riga di codice. Loro ribattono che, se una gui è complicata al punto da nonpoter essere espressa da uno schema xml, vuol dire che è troppo complicata, e andrebbe semplificata. Il che, a mioavviso, è 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 nonmi 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 strademolto 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 schemixml 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 softwareserver-side, e un giorno ti chiedono una gui al volo perché anche i non addetti possano vedere un log di sistema (tuovviamente 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.

12 Chapter 4. Gli strumenti da non usare.

Page 21: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Ma la programmazione di gui, credeteci o meno, è un’arte tanto quanto il resto della programmazione. Si possono farecose molto raffinate, e l’espressività di Python è di grande aiuto in questo.

Quindi in conclusione, no, non usate neppure xml.

Grazie.

4.3. XmlResource. 13

Page 22: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

14 Chapter 4. Gli strumenti da non usare.

Page 23: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 5

Appunti wxPython - livello base

wx.App: le basi da sapere.

Questa pagina racconta le pochissime cose assolutamente da sapere sulla wx.App, che è il cuore di ogni applicazionewxPython.

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 solouna, 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 from random import randint2 import wx3

4 class MyFrame(wx.Frame):5 def __init__(self, *a, **k):6 wx.Frame.__init__(self, *a, **k)7 self.button = wx.Button(self, -1, 'Hello word!')8 self.button.Bind(wx.EVT_BUTTON, self.on_clic)9

10 def on_clic(self, evt):11 self.button.SetBackgroundColour((randint(0, 255),12 randint(0, 255),13 randint(0, 255)))14

15 app = wx.App(False)16 frame = MyFrame(None)

15

Page 24: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

17 frame.Show()18 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 aquesto punto è possibile creare (istanziare) qualsiasi altro elemento wxPython (se non ci credete, provate a scambiaretra loro le righe 15 e 16...).

La riga successiva crea una istanza del nostro frame (di nuovo... non preoccupatevi del significato di quel None), ela riga dopo lo mostra sullo schermo. Tutto sembra a posto, ma in realtà niente si è ancora messo in moto. Provatea togliere l’ultima riga, e fate girare lo script: il frame si visualizza come prima, ma quando provate a cliccare sulpulsante, nulla accade.

E’ solo quando, alla riga 18, invochiamo il MainLoop della nostra wx.App che le cose si mettono davvero in moto.

Il MainLoop: il motore della gui.

Il MainLoop della wx.App è il ciclo principale della nosta applicazione wxPython. Potete pensarlo come un grandewhile True senza fine. A ogni iterazione del ciclo, wxPython controlla se gli elementi della gui si sono mossi, sesono 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 vitrovate 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’ultimoframe 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 daquando invocate il MainLoop, il controllo dell’applicazione passa a wxPython, che non lo restituisce fino a quandonon si è usciti dal MainLoop. Per rendervene conto, modificate le ultime righe dell’esempio precedente in questomodo:

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 eseguirealtro codice Python che risiede “fuori” da wxPython (a meno di non metterlo in un thread separato, si capisce... maquesto per il momento è fuori portata per noi).

Todo

una pagina sui thread

16 Chapter 5. Appunti wxPython - livello base

Page 25: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Questo comportamento è tipico delle gui, e degli altri framework che devono rispondere a eventi (PyGame, per es-empio). Devono stare in attesa delle interazioni dell’utente, e per questo “occupano” costantemente il flusso delprogramma 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

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 pilotatodirettamente 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 bisognaprima spiegare meglio il concetto di “top level frame”, e, più in generale, della catena dei “parent”. Dedichiamo aquesto argomento una pagina separata.

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'applicazioneapp.MainLoop()

Il modulo Python che contiene queste righe è quindi l’entry-point del nostro programma wxPython: quello che l’utenteinvocherà 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 comportereb-bero dei side effect qualora il modulo dovesse venire importato. Di conseguenza, ricordatevi sempre di inserire ilbootstrap 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 maibisogno, 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 pertutte 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 viservono.

5.1. wx.App: le basi da sapere. 17

Page 26: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

La catena dei “parent”.

In wxPython, praticamente ogni widget che create deve stare in una relazione padre-figlio con altri widget. Come nellavita reale, un widget può avere molti figli, ma un solo padre. Questa organizzazione, che è onnipresente, rispecchianaturalmente lo stato delle cose in una normale interfaccia grafica. Per esempio, un wx.Frame (la finestra che fa dacornice al resto) potrebbe contenere al suo interno un wx.Panel (lo “sfondo”) in cui potrebbero a loro volta essereorganizzati 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 qualcosaall’utente, è normale che questo dialogo sia “figlio” della finestra-madre. A loro volta, tutti i widget dentro il dialogosaranno “figli” di questo, e così via.

Queste catene di padri-figli possono essere anche molto lunghe. Il rapporto padre-figlio non è solo una questione diorganizzazione 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 conseguenzebizzarre. Ma se la vostra finestra aveva aperto una seconda finestra “figlia”, al momento di chiudere la prima sichiuderà 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 relazionipadre-figlio, quindi potete agganciare la seconda finestra a un nuovo padre, o anche dichiararla “top-level” comevedremo.

Ma gli effetti delle relazioni padre-figlio si manifestano anche in altre occasioni. Alcuni widget possono trasmettereautomaticamente 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 verrannoautomaticamente “validati” (posto che abbiano un wx.Validator appropriato). E gli esempi potrebbero continuarea lungo.

Forse però il caso più clamoroso è la propagazione degli eventi. Ce ne occupiamo in modo specifico in una paginaseparata, ma per il momento basta dire che un wx.CommandEvent si propaga dal widget che lo ha originato alsuo genitore, e poi al genitore del genitore, e così via fino all’ultimo genitore “top-level” e poi ancora da questo allawx.App. Così, per esempio, se avete un frame con dentro un panel con dentro un pulsante, e ci cliccate sopra, ilwx.EVT_BUTTON si trasmette all’indietro dal pulsante al panel, al frame, e finalmente alla wx.App, permettendovidi intercettarlo a ogni successiva “fermata”.

La catena dei rapporti padre-figlio è quindi un concentto importante. In questa pagina cerchiamo di analizzare ilproblema a fondo.

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 istanziatela loro classe, vi chiedono di specificare il loro genitore. Il primo argomento (obbligatorio) che dovete passare alcostruttore è 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, unapproccio tipico quando si definisce la classe di un frame, di un dialogo o di un panel, è di creare tutti i widget checontiene in questo modo:

class MyFrame(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)

18 Chapter 5. Appunti wxPython - livello base

Page 27: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 widgetdovrà 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 esserechiamati con parent=None. In questo caso non hanno genitori e sono considerati finestre “top-level”, come vedremotra poco. Chiaramente tutte le vostre applicazioni wxPython devono per forza avere almeno una finestra “top-level”,ossia quella che create per prima:

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) perqualcosa che non sia un frame o un dialogo). Tuttavia è molto raro nella pratica, ed è sempre sorgente di confusioneri-aggiustare la catena dei “parent” a runtime. Un caso in cui può essere giustificato è quando volete agganciare unafinestra figlia a un nuovo genitore (o renderla top-level) prima che il suo attuale “parent” venga distrutto.

Orientarsi nell’albero dei “parent”.

Le catene dei “parent” possono essere lunghe e complicate. wxPython mette a disposizione qualche strumento utileper navigare in questo mare tempestoso.

• il più comune è GetParent (da usare così: my_button.GetParent()) che restituisce il genitore direttodi 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 potetechiamare ricorsivamente GetChildren per ricostruire tutta la discendenza di un widget, per esempio).

Le finestre top-level.

Come abbiamo detto, i wx.Frame e i wx.Dialog (e naturalmente tutte le loro sottoclassi) possono ammettereparent=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 almenouna. Quando l’ultima finestra top-level viene chiusa, questo è il segnale per wxPython di terminare la wx.App echiudere il programma, come analizziamo più approfonditamente altrove.

5.2. La catena dei “parent”. 19

Page 28: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 “top-window” 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, è necessarioche 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 “top-level” 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 automatica-mente l’attuale “top-window”);

• infine, per promuovere a “top-level” una finestra normale basta chiamare su questa SetParent(None), comeabbiamo visto.

Detto questo, bisogna comunque specificare che, nel mondo reale, di rado c’è bisogno di tutto questo. La maggiorparte delle applicazioni wxPython hanno una sola “top-level”, che è il primo frame che create e mostrate, e che quindicoincide con la “top-window”. Occasionalmente, potrebbero comparire per breve tempo altri dialoghi “top-level” (unafinestra di login, per esempio), ma si tratta di eccezioni temporanee. Nelle applicazioni di tutti i giorni, è buona normalimitarsi a una sola “top-level”, anche per semplificare il processo di chiusura della wx.App..

Frame, dialoghi, panel: contenitori wxPython.

Tra i widget wxPython, alcuni hanno la funzione principale di contenere altri widget, organizzandoli sia graficamentesia 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 è unnormale frame, ma con la barra del titolo più stretta; wx.ScrolledPanel è un panel con barre di scorrimentoincorporate, 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 wx-Python.

wx.Frame.

I wx.Frame (frame per gli amici) sono quello che l’utente, guardando, dice “è la finestra”. In genere la “finestraprincipale” della vostra applicazione è un frame, anche se non è necessario (potrebbe essere un dialogo). Molto spessoi 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 dellesottoclassi personalizzate, con tutte le caratteristiche volute (e già complete di tutti i widget da inserire nel frame), epoi istanziare la sottoclasse.

Il costruttore di wx.Frame prevede naturalmente un parametro style, e ci sono moltissimi stili disponibili pervariare l’aspetto e le funzionalità di un frame (e anche alcuni extra-style). Potete consultare la documentazione perconoscerli tutti. La raccomandazione in ogni caso è di non variare mai troppo l’aspetto base del frame, soprattutto lafinestra principale, per non disorientare (leggi: irritare) l’utente, che si aspetta la normale operatività delle finestre delsuo desktop. Se non passate nessun parametro style, verrà applicato il wx.DEFAULT_FRAME_STYLE.

20 Chapter 5. Appunti wxPython - livello base

Page 29: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 creanonell’__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 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 a_text = wx.TextCtrl(self, pos=(10, 10))5 a_button = wx.Button(self, -1, 'Hello Word', pos=(10, 50))6

7 if __name__ == '__main__':8 app = wx.App(False)9 frame = MyFrame(None)

10 frame.Show()11 app.MainLoop()

Alla riga 1, definisco una sottoclasse personalizzata di wx.Frame. Alla riga 2, sovrascrivo l’__init__ per definireal 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 diqualsiasi 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 passandoself come primo argomento (il “parent”). In questo modo saranno figli della futura istanza di MyFrame, quandoverrà 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 specifichiamola 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 in-vocherà 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 widgetsopra un wx.Panel, e inserire direttamente nel frame soltanto il panel. In effetti il frame non è fatto per conteneredirettamente i widget. Un motivo potete già vederlo testando il codice dell’esempio qui sopra (almeno se siete inWindows, è molto evidente). Il frame è “bucato”, nel senso che intorno al pulsante non si vede lo sfondo che ciaspetteremmo, ma il brutale sfondo del frame (che è immodificabile). Ovviamente potete sistemare i widget in mododa “tassellare” completamente il contenitore del frame senza lasciare nessun buco, ma questo non è pratico. Sullepiattaforme diverse da Windows, il colore di sfondo del frame è ideantico al colore di sfondo degli altri widget, percui 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 viinteressano, e di cui invece dispone il panel. Ci arriviamo subito.

5.3. Frame, dialoghi, panel: contenitori wxPython. 21

Page 30: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

wx.Panel.

Se il frame è pensato per presentare la cornice della finestra, il panel ha la funzione di contenere i widget. Comeabbiamo notato qui sopra, anche se il frame può contenere direttamente i widget, in pratica si preferisce sempreassegnarli 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ò esseremodificato 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 assegnaticome figli del panel, e non del frame. Il nostro esempio di sopra diventa quindi:

1 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 panel = wx.Panel(self)5 a_text = wx.TextCtrl(panel, pos=(10, 10))6 a_button = wx.Button(panel, -1, 'Hello Word', pos=(10, 50))7

8 if __name__ == '__main__':9 app = wx.App(False)

10 frame = MyFrame(None)11 frame.Show()12 app.MainLoop()

Notate, alla riga 4, che il il panel è figlio del frame (self), e gli altri widget sono invece figli del panel. Curiosamentenon abbiamo bisogno di specificare una posizione per il panel all’interno del frame. Infatti, quando un contenitore haun 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 gruppodi widget da raggruppare. Possiamo inserire quanti panel vogliamo dentro un frame, basta specificare in qualche modoil layout (con il posizionamento assoluto, oppure, molto meglio, con i sizer). E’ frequente anche l’inserimento di unpanel 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 daun punto di vista logico, raggruppando insieme quelli che concorrono alla stessa funzionalità del programma. Peresempio, 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, datadi 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 terzoframe “Dati completi dell’impiegato”. Questa organizzazione favorisce il riutilizzo del codice e la separazione dellevarie funzioni (per esempio, ciascun panel potrebbe essere collegato a un diverso codice di controllo per il trattamentodei dati immessi).

Il modo normale per implementare questi “cluster” riutilizzabili di widget consiste semplicemente nel creare sot-toclassi 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, riscriviamoancora una volta il nostro codice, separando il panel dal frame:

1 class MyPanel(wx.Panel):2 def __init__(self, *a, **k):3 wx.Panel.__init__(self, *a, **k)

22 Chapter 5. Appunti wxPython - livello base

Page 31: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

4 a_text = wx.TextCtrl(self, pos=(10, 10))5 a_button = wx.Button(self, -1, 'Hello Word', pos=(10, 50))6

7 class MyFrame(wx.Frame):8 def __init__(self, *a, **k):9 wx.Frame.__init__(self, *a, **k)

10 panel = MyPanel(self)11

12 if __name__ == '__main__':13 app = wx.App(False)14 frame = MyFrame(None)15 frame.Show()16 app.MainLoop()

Si noti che adesso i due widget sono figli di self (ma self è il panel, beninteso), e si noti anche l’istanziazione diMyPanel 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 voltaMyPanel è una classe separata, pronta a essere riutilizzata ovunque.

In conclusione, i panel sono un ottimo strumento per organizzare i widget, sia per il layout sia per la logica. Alcontrario 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à dibase e a esporla all’esterno in un’api coerente. I panel sono i veri e propri mattoni da costruzione di un’applicazionewxPython.

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”, perchiedere 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 perrispondere meglio a certe esigenze. E’ inutile “tirare la corda” e cercare di usare un dialogo per cose per cui sarebbepiù adatto un frame. Per esempio, un wx.Dialog non può avere una toolbar. E’ certamente possibile inserire unafila orizzontale di piccoli pulsanti quadrati in alto, e mimare una toolbar... Ma a questo punto, perché non usare unframe, 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 cuisono gestiti (e soprattutto distrutti quando non servono più).

• condividono gli stili necessari per determinare i pulsanti della barra del titolo: in particolare, è possibile mostrareo nascondere i pulsanti di riduzione a icona, chiusura, etc. E’ anche possibile determinare se sono ridimension-abili.

• 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 inseritodentro. Quindi, quando create un widget “figlio” di un dialogo, è come inserire prima il widget dentro un panel,

5.3. Frame, dialoghi, panel: contenitori wxPython. 23

Page 32: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

e poi mettere il panel dentro il dialogo. I widget del dialogo quindi hanno già il “tab-trasversing”, e il defaultwidget.

• 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 corrispon-dente 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 ditesto, 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 daiwidget interni non possono propagarsi al di fuori del dialogo stesso. Questo è in linea con il principio che idialoghi dovrebbero sempre “sbrigarsi da soli le proprie faccende”, e limitarsi a restituire al mondo esterno uncodice di uscita.

• rispondono diversamente al metodo Close(): un frame chiama automaticamente Destroy(), mentre undialogo non si distrugge subito, ma si limita a nascondersi restando ancora in vita. Questo perché è frequentevoler 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ù omeno 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 cheraccoglie i dati e li presenta in una struttura-dati conveniente (per esempio, un dizionario). Infine, si inseriscono nel di-alogo pulsanti di conferma o annullamento, magari con id predefiniti in modo da ottenere facilmente il comportamentostandard di chiusura ed eventuale validazione automatica. Quando l’utente chiude il dialogo, prima di distruggerlo siaccede alla interfaccia GetValue per raccogliere i dati inseriti.

Ecco un esempio minimo di un dialogo che chiede di inserire nome e cognome:

1 class YourNameDialog(wx.Dialog):2 def __init__(self, *a, **k):3 wx.Dialog.__init__(self, *a, **k)4 self.first_name = wx.TextCtrl(self)5 self.family_name = wx.TextCtrl(self)6

7 s = wx.FlexGridSizer(2, 2, 5, 5)8 s.AddGrowableCol(1)9 s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)

10 s.Add(self.first_name, 1, wx.EXPAND)11 s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)12 s.Add(self.family_name, 1, wx.EXPAND)13

14 s1 = wx.BoxSizer()15 s1.Add(wx.Button(self, wx.ID_OK, 'ok'), 1, wx.EXPAND|wx.ALL, 5)16 s1.Add(wx.Button(self, wx.ID_CANCEL, 'cancella'), 1, wx.EXPAND|wx.ALL, 5)17

18 s2 = wx.BoxSizer(wx.VERTICAL)19 s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)20 s2.Add(s1, 0, wx.EXPAND|wx.ALL, 5)21 self.SetSizer(s2)22 s2.Fit(self)23

24 def GetValue(self):

24 Chapter 5. Appunti wxPython - livello base

Page 33: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

25 return {'nome' : self.first_name.GetValue(),26 'cognome' : self.family_name.GetValue()}27

28

29 class MyTopFrame(wx.Frame):30 def __init__(self, *a, **k):31 wx.Frame.__init__(self, *a, **k)32 b = wx.Button(self, -1, 'inserisci il tuo nome')33 b.Bind(wx.EVT_BUTTON, self.on_clic)34

35 def on_clic(self, evt):36 dlg = YourNameDialog(self, title='Nome e cognome, prego')37 retcode = dlg.ShowModal()38 if retcode == wx.ID_OK:39 data = dlg.GetValue()40 else:41 data = {}42 # print data43 dlg.Destroy()44

45 if __name__ == '__main__':46 app = wx.App(False)47 MyTopFrame(None, size=(150, 150)).Show()48 app.MainLoop()

Le righe significative sono le 4-5, dove definiamo le caselle di testo in cui andranno inseriti i dati; le 15-16, doveinseriamo i pulsanti con gli id predefiniti; 24-26, dove definiamo l’interfaccia GetValue che raccoglie di dati e lipresenta in una struttura conveniente.

Con queste premesse, il procedimento di creazione del dialogo e raccolta dei dati è molto lineare. Alla riga 36 creiamoil 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 restitu-isce l’id del pulsante premuto, wx.ID_OK oppure wx.ID_CANCEL. Nel primo caso, raccogliamo i dati chiamandoGetValue(): non c’è bisogno di accedere direttamente agli elementi interni del dialogo, dal momento che abbiamodefinito 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 questocaso 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 conpulsanti predefiniti.

Gli Id in wxPython.

In wxPython, ogni widget che create ha un numero (Id) che lo identifica univocamente. E’ una eredità del frameworkC++ sottostante, e nel mondo Python questo uso pervasivo di id globali sembra goffo, per non dire altro. Ma nondovete scordare che in C++ certe comodità di Python non ci sono. Per esempio, in Python voi potete passare il nomedi 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)

5.4. Gli Id in wxPython. 25

Page 34: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

dove appunto on_clic viene passata come argomento di Bind. Ma in C++ questo proprio non si può fare, e quindianche una cosa banale come collegare tra loro un oggetto, un evento e un callback diventa un complicato ballettoeseguito da una macro che può lavorare solo con riferimenti statici: e il riferimento agli oggetti è appunto il loro Idunivoco.

Quindi gli Id ci sono, e ci saranno sempre. wxPython potrebbe decidere di nasconderli completamente (e in effetti cisono 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 cuipossono ancora tornare comodi.

Assegnare gli Id.

L’Id viene attribuito obbligatoriamente a tutti i widget al momento della loro creazione. In genere, è il secondoargomento 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:

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à pre-assegnati, e dovete stare attenti a non sovrascriverli. Potete usare tutti i numeri che volete, purché non siano compresitra 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 chewxPython non sovrascriva i vostri (o viceversa). La cosa più sicura è determinare tutti gli Id che vi servono prima chewxPython assegni il suo primo Id, e registrarli con la funzione wx.RegisterId. In questo modo dite a wxPythondi lasciar stare quei numeri. Per esempio, per riservare per il vostro uso cento numeri, potete fare:

# questo prima di ogni altra cosafor 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 numer-azione a ogni nuovo frame, se si sente libero di riciclare gli Id quando voi distruggete un frame, e così via.

26 Chapter 5. Appunti wxPython - livello base

Page 35: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 nontrova niente). Siccome gli Id possono essere ripetuti tra i diversi frame, potete anche passare il riferimentoal frame dentro cui volete cercare (per esempio, wx.FindWindowById(100, my_button) cerca soloall’interno del frame dove vive my_button). Se non passate niente, la ricerca sarà globale, ma si arrestaappena trova il primo widget con l’Id corrispondente (e non è detto che ce ne siano altri, o che questo sia proprioquello che vi serve). Se pensate che questo algoritmo sia un po’ bacato, avete trovato un’altra buona ragione pernon 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. Maricordate che in C++ vi trovate a passare cose statiche (gli Id, appunto), e allora una funzione di ricerca può tornareutile.

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 partico-lari. Un caso tipico sono gli “StockButtons” (cercate nella demo). In pratica, se create un wx.Button passandoglicome Id uno di quelli predefiniti del tipo wx.ID_*, wxPython aggiungerà la label corrispondente (e userà lo Stock-Button 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 automatica-mente.

Dialoghi con risposte predefinite.

Un utilizzo simile degli Id predefiniti avviene nei dialoghi. Ci sono molti dialoghi “standard” che non avete bisognodi disegnare nel dettaglio; potete però impostarli perché abbiano certi pulsanti predefiniti. A seconda dei pulsanti cheinserite, il dialogo restituisce alla chiusura l’Id (predefinito) del pulsante premuto, come risultato del metodo Show oShowModal: 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

...

5.4. Gli Id in wxPython. 27

Page 36: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 demoper 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 class IceCreamDialog(wx.Dialog):2 def __init__(self, *a, **k):3 wx.Dialog.__init__(self, *a, **k)4 self.flavor = wx.ComboBox(self, -1, 'crema', style=wx.CB_READONLY,5 choices=['crema', 'cioccolato', 'stracciatella'])6 ok = wx.Button(self, wx.ID_OK, 'dammi subito il mio gelato!')7 cancel = wx.Button(self, wx.ID_CANCEL, 'sono a dieta...')8

9 s = wx.BoxSizer(wx.VERTICAL)10 s.Add(self.flavor, 0, wx.EXPAND|wx.ALL, 15)11 s1 = wx.BoxSizer()12 s1.Add(ok, 1, wx.EXPAND|wx.ALL, 5)13 s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)14 s.Add(s1, 0, wx.EXPAND|wx.ALL, 10)15 self.SetSizer(s)16 s.Fit(self)17

18 def GetValue(self): return self.flavor.GetStringSelection()19

20

21 class MyTopFrame(wx.Frame):22 def __init__(self, *a, **k):23 wx.Frame.__init__(self, *a, **k)24 b = wx.Button(self, -1, 'scelta gelati')25 b.Bind(wx.EVT_BUTTON, self.on_clic)26

27 def on_clic(self, evt):28 msg = IceCreamDialog(self, title='gelati!')29 retcode = msg.ShowModal()30 if retcode == wx.ID_OK:31 print 'gelato gusto %s in arrivo!' % msg.GetValue()32 else:33 print 'abbiamo i sorbetti al limone...'34

35 if __name__ == '__main__':36 app = wx.App(False)37 MyTopFrame(None, size=(150, 150)).Show()38 app.MainLoop()

Notate che non abbiamo bisogno di collegare esplicitamente i nostri due pulsanti a qualche evento. Basta assegnareloro i corretti Id “predefiniti” (righe 6 e 7), e wxPython sa già cosa fare: chiude il dialogo e restituisce l’Id del pulsantepremuto.

28 Chapter 5. Appunti wxPython - livello base

Page 37: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 pre-definito wx.ID_OK. Se nel vostro dialogo inserite un pulsante con questo Id, oltre ai benefici visti sopra, quando sipreme 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 codicedel paragrafo precedente questo validatore che impedisce di selezionare il gusto “crema”:

1 class NoCreamValidator(wx.PyValidator):2 def __init__(self): wx.PyValidator.__init__(self)3 def Clone(self): return NoCreamValidator()4 def TransferToWindow(self): return True5 def TransferFromWindow(self): return True6

7 def Validate(self, win):8 if self.GetWindow().GetStringSelection() == 'crema':9 wx.MessageBox('Gusto terminato!', 'Oh no!')

10 return False11 else:12 return True

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 deldialogo.

Menu.

Lasciamo alla fine il caso di utilizzo più frequente per gli Id: i menu. Abbiamo dedicato una pagina separata perapprofondire 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, ecollegate 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 incomune 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’istanzadel frame in cui appare il menu.

5.4. Gli Id in wxPython. 29

Page 38: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Tuttavia l’evento wx.EVT_MENU trasporta con sé l’Id (e solo quello) della voce che è stata selezionata, per cui seinvece chiedete event.GetId() ottenete un’informazione più precisa... a patto naturalmente di conoscere gli Iddelle 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 programma-tore Python potrebbe semplicemente fare:

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 lacompattezza di Python...

E a proposito di espressività e compattezza, aggiungo che potete evitare del tutto l’uso degli Id con i menu (anchequando intendete collegare più voci allo stesso callback), facendo uso del trucco del “lambda binding” per passare aBind 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

I flag di stile.

I flag di stile sono frequenti in wxPython. Al momento della creazione di un widget qualsiasi, è comune aggiungere unparametro style al costruttore. Ogni widget ha i suoi flag di stile predefiniti, che si possono combinare per ottenerevarie personalizzazioni.

30 Chapter 5. Appunti wxPython - livello base

Page 39: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Che cos’è una bitmask.

Prima di tutto, una piccola spiegazione sulle bitmask: non è un argomento specifico di wxPython, ma serve a capiremeglio 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 lettura16

Quello che alcuni notano a prima vista, è che tutte queste costanti sono scelte per essere potenze di 2. La cosa è utileperché le potenze di 2, in notazione binaria, si possono sommare tra loro in modo che ciascun addendo contribuiscaa modificare solo un bit della somma finale. In altre parole, è sempre possibile ricostruire gli addendi a partire dalrisultato.

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 leliste 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)

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à citatowx.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ò tornareutile 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.

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 qualisono. Inoltre, siccome gli stili sono solo delle costanti numeriche, non esistono docstring che spiegano rapidamenteche cosa fanno.

Tra l’altro, il nome stesso di “stile” è ingannevole. Alcuni stili effettivamente cambiano solo il look del widget a cui siapplicano, ma molti si riferiscono a caratteristiche strutturali più profonde: wx.LI_VIRTUAL cambia completamentel’utilizzo di un wx.ListCtrl.

5.5. I flag di stile. 31

Page 40: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 documentazioneinterna del widget (per esempio la docstring della classe o del modulo di solito sono molto chiare). L’utility esternaEventsinStyle 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 quellidi un wx.ComboBox iniziano con wx.CB_*. Un buon editor con l’autocompletion in questi casi fa miracoli.

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 possibilesapere quali stili avete applicato al widget una volta che è stato creato. Tuttavia molti widget implementano dei metodiausiliari 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 esistenessun metodo IsReadOnly.

Conoscere quali sono questi metodi è ovviamente una questione di sfogliare con pazienza la documentazione, caso percaso. Ovviamente, un po’ di mestiere aiuta... per esempio, prima di guardare alla cieca, iniziate a sfogliare i metodiche iniziano con Is* e poi quelli con Get*.

Settare gli stili dopo che il widget è stato creato.

Per questo, potete usare il metodo SetWindowStyleFlag, che riceve come argomento una normale bitmask distili.

Tuttavia non è un’operazione da fare a cuor leggero. A seconda dei widget e degli stili che volete cambiare, potrestecausare incongruenze gravi. Certe operazioni potrebbero semplicemente fallire. Dovete fare esperimenti caso percaso, ma in generale non è una pratica consigliabile.

In ogni caso, è molto probabile che dobbiate chiamare Refresh() sul widget, per vedere gli effetti delle modifiche.

Che cosa sono gli extra-style.

Definire gli stili come costanti numeriche che si possono combinare con le bitmask è comodo all’inizio, ma prima opoi 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 deglistili.

Ed ecco che arrivano in soccorso gli “extended style” (o extra style). In pratica si tratta di stili aggiuntivi che nonpossono 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. Inogni caso, è utile sapere che wx.Window ha alcuni extra-style definiti, e siccome wx.Window è la classe progenitricedi tutti i widget, questi vengono ereditati da tutta la gerarchia (anche se naturalmente per la stragrande maggioranzadei 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_*

32 Chapter 5. Appunti wxPython - livello base

Page 41: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• 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 esempiowx.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 (com-portamento di default), ma anche i figli dei figli, etc. Utile quando si usano i validatori, e la finestra contieneper 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 suoifigli. Gli eventi arrivano fin qui, ma poi non si propagano oltre. Notare che i wx.Dialog, a differenza deiframe, hanno questo flag impostato per default.

• wx.WS_EX_CONTEXTHELP, wx.DIALOG_EX_CONTEXTHELP, wx.FRAME_EX_CONTEXTHELP aggiun-gono 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 SetExtraStyledopo 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 casodi *_EX_CONTEXTHELP (devo dire di non sapere se ci sono altri casi. Nel dubbio, la documetazione ovviamenteriporta il problema).

In questi casi, occorre intraprendere una strada ancora più complicata, nota come “two-step creation”, in cui si istanziaper esempio un wx.PreFrame, gli si attribuiscono gli extra-style voluti, e poi lo si trasforma in wx.Frame aggiun-gendo gli stili normali. La “two-step creation” è un procedimento non difficile ma comunque avanzato, e può servirein 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 unproblema di design? Risposta breve: sicuramente sì. Detto questo, non è per difendere wxWidgets, ma praticamentequalsiasi 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 sonorari: la stragrande maggioranza dei widget ha 3-4 stili definiti, e le bitmask sono più che sufficienti, lasciando spazioanche per aggiunte future.

I sizer: le basi da sapere.

Questa pagina tratta le cose fondamentali da sapere sui sizer, e descrive wx.BoxSizer, il più semplice dei sizerdisponibili in wxPython. I sizer più complessi sono descritti in una pagina separata. Inoltre, per usare i sizer occorreanche avere un’idea di come si specificano le dimensioni dei widget, argomento che affrontiamo a parte.

Non usate il posizionamento assoluto.

In molti esempi brevi di queste note, per semplicità, facciamo uso del posizionamento assoluto, ossia specifichiamodirettamente la posizione (in pixel) del widget rispetto al suo parent, al momento della creazione, passando il parametropos (ed eventualmente anche size per specificare la dimensione):

5.6. I sizer: le basi da sapere. 33

Page 42: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 peralmeno 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 nelleinterfacce moderne;

• il posizionamento assoluto vi costringe a calcolare esattamente posizione e dimensione di ogni singolo elementodell’interfaccia. Finché lo fate la prima volta va ancora bene, ma se in seguito volete aggiungere o toglierequalcosa, 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 sentitoparlare. Abbiamo già scritto che i RAD non vanno usati, e adesso possiamo aggiungere un motivo: incoraggiano ilposizionamento assoluto.

Usate i sizer, invece.

wxPython supporta, come abbiamo visto, anche il posizionamento assoluto. Ma il modo consigliato di organizzare illayout di una finestra sono i sizer.

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 ilcontenuto sia sempre proporzionato al contenitore;

• inserire o togliere elementi in un secondo momento diventa banale: siccome è il sizer a occuparsi di calcolareposizioni 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 inche 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 pratica-mente onnipresenti nel mondo wxPython e non solo.

Che cosa è un sizer.

In wxPython esistono diversi tipi di sizer, tutti derivati dalla classe-madre wx.Sizer (che però è un genitore astrattoe 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 perdisporre 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 modosia 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;

34 Chapter 5. Appunti wxPython - livello base

Page 43: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• come opzione ulteriore, è possibile usare il metodo Fit per dire al sizer di adattare la sua dimensione a quelladegli elementi che contiene.

wx.BoxSizer: il modello più semplice.

Il più semplice sizer che potete usare è il wx.BoxSizer. Questo sizer organizza i widget in colonna, uno sottol’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 unpulsante che avete creato in precedenza, scrivete:

sizer.Add(my_button)

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 consentedi inserire un elemento all’inizio del sizer, invece che alla fine. Insert inserisce un elemento tra altri due. Removerimuove 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 usofrequente di queste tecniche, tuttavia.

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 riferi-mento 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 qualchemodo). Tutti i widget con proporzione nulla occuperanno solo lo spazio di cui hanno effettivamente bisogno. Tuttii widget con proporzione superiore a 0, invece, competeranno per occupare lo spazio eventualmente rimanente, inmaniera 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à lospazio di cui ha bisogno, e lo spazio rimanente sarà diviso tra gli altri due: il secondo ne occuperà un terzo, e l’ultimosi prenderà i due terzi restanti. Tutto questo, non dimentichiamolo, soltanto lungo la direzione “principale” del sizer.Ecco il codice che illustra questo esempio:

5.6. I sizer: le basi da sapere. 35

Page 44: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

class TopFrame(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)p = wx.Panel(self)sizer = wx.BoxSizer(wx.VERTICAL) # la direzione e' verticalesizer.Add(wx.Button(p), 0)sizer.Add(wx.Button(p), 1)sizer.Add(wx.Button(p), 2)p.SetSizer(sizer)

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 secondoe il terzo occuperanno sempre lo spazio restante in proporzione 2:1, mentre le dimensioni del primo pulsante noncambieranno mai. Notate anche che i pulsanti si contendono soltanto lo spazio nella direzione verticale (ossia ladirezione 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 deglistili. In questa bitmask possono rientrare due indicazioni molto differenti tra loro:

• primo, come allineare i widget rispetto agli altri, e/o definirne le dimensioni;

• 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 aglialtri 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 degliallineamenti appena visti);

• wx.EXPAND o il suo sinonimo wx.GROW forzano il widget a occupare tutto lo spazio disponibile lungo ladimensione “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 sec-ondaria del sizer. Per contro, dare al widget una priorità superiore a 0 lo costringe a espandersi lungo la direzioneprincipale, come abbiamo visto. Quindi se il widget ha priorità superiore a 0 e il flag wx.EXPAND, riempirà lo spaziodisponibile in entrambe le direzioni.

In generale, dovete chiedervi in quale direzione ha senso far espandere i vostri widget. Per esempio, in un sizerverticale, 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 orizzontalesizer.Add(wx.ListBox(...), 1, wx.EXPAND) # cresce in entrambe le direzionisizer.Add(wx.Button(...), 0, wx.ALIGN_CENTER_HORIZONTAL) # non cresce

36 Chapter 5. Appunti wxPython - livello base

Page 45: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 voleteche 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 delbordo, 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 lasciarevuoti in una tupla (in realtà, un’istanza di wx.Size, come vediamo nella pagina dedicata). Siccome in genere viinteressa specificare solo lo spazio da lasciare lungo la direzione principale del sizer, potete passare -1 per l’altradirezione. 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 verticalesizer.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 conun flag e una proporzione. In particolare, è molto frequente l’idioma Add((-1, -1), 1, wx.EXPAND), cheaggiunge uno spazio indeterminato che si allarga quando ridimensioniamo la finestra. Provate questo “trucco”, chemantiene i widget nel centro della finestra:

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.AddStretchSpacerper 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.

Gli eventi: le basi da sapere.

wxPython, come tutti gli altri, è un gui framework “event-driven”: il suo MainLoop, una volta avviato, si mette inascolto 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 innescauno: non solo le interazioni “di alto livello” (premere un pulsante, scegliere una voce di menu, etc.), ma anche lapressione dei tasti, lo spostamento del mouse, il cambiamento di dimensioni di una finestra, e molte molte altre cose

5.7. Gli eventi: le basi da sapere. 37

Page 46: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

ancora, tuuo genera eventi. Perfino quando tutto il resto tace, viene generato di continuo un wx.EVT_IDLE, persegnalare 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’uccelloai vari attori coinvolti, ma esaminiamo esclusivamente gli aspetti più semplici, che vi servono a gestire le situazioninormali. Rimandiamo a una pagina separata l’analisi degli aspetti più complessi.

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 oggettisono 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. Peresempio, quando l’utente fa clic su un pulsante, il pulsante lancia una istanza di wx.CommandEvent che contieneinformazioni sul pulsante che è stato premuto.

Tuttavia, a causa del complesso sistema con cui wxWidgets (il framework C++ su cui wxPython è basato) gestisce glieventi, 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 questoargomento deve essere un riferimento all’oggetto-evento stesso. Quindi, qualcosa come:

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 magicoin un callback: se talvolta volete chiamarlo manualmente (in assenza di qualunque evento), potete farlo passando peresempio 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, edefinire 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ì:

38 Chapter 5. Appunti wxPython - livello base

Page 47: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 disorientatoquando 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 “handlermethod”: 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 genitoreastratto wx.EvtHandler) che conferiscono la capacità di gestire un evento, appunto. La cosa interessante è chetutta 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 “dasoli” 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. Dettopiù rapidamente: un certo tipo di evento. Qui occorre una precisazione. Le classi-evento (e i conseguenti oggetti-evento) sono poche, e molti widget possono innescare lo stesso evento. Per esempio, quando fate clic su un pulsante equando 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:

>>> import wx>>> wx.wxEVT_COMMAND_BUTTON_CLICKED10008

è 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 poteteimmaginare:

>>> 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 programmatoriwxPython. Ma wxPython in realtà li usa internamente per consentirvi di riferirvi, in modo trasparente, agli eventi nonin 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.

5.7. Gli eventi: le basi da sapere. 39

Page 48: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Che cosa è un binder.

Un binder è un oggetto usato per legare uno specifico event type, uno specifico handler e uno specifico callback. Ibinder sono istanze della classe wx.PyEventBinder, e sono creature tipiche solo di wxPython. In effetti i bindersono 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. Tut-tavia, 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’istruzioneiniziale import wx), e vivono pronti all’uso nel namespace wx, sotto forma di simboli del tipo wx.EVT_*. La loronomenclatura 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’ comedelle costanti, e quindi ha senso che abbiano nomi tutti maiuscoli. Tuttavia in realtà basta poco per capire che sonooggetti 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 vistosopra, 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 sovrastrutturainutile intorno alla costante numerica dell’event type. Prima di tutto, un binder può riferirsi a più di un event type. Peresempio, wx.EVT_MOUSE_EVENTS è un binder collettivo che raggruppa tutti gli event type del mouse (clic, clic adestra, 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 ecallback.

Ma prima, ancora un pizzico di confusione, questa volta però comprensibile e sana. Proprio perché nella vita di tuttii giorni non incontrare oggetti-eventi, nel linguaggio comune di wxPython è consueto riferirsi ai wx.EVT_* come“eventi”, anche se sono più precisamente degli oggetti-binder. Tuttavia questa piccola licenza descrive la situazinepiù 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, ilbinder wx.EVT_BUTTON porta in sé non solo la nozione del wx.CommandEvent, ma anche quella di “generato daun 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 presenzadegli event type permette di mantenere ridotto il numero delle classi-evento, lasciando che la loro gerarchia si sviluppisecondo 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!

40 Chapter 5. Appunti wxPython - livello base

Page 49: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 succedeche si sceglie il pulsante stesso come handler degli eventi che genera. Ovviamente un pulsante può generare diversieventi; e d’altra parte, un evento può essere generato da diversi widget oltre al pulsante. Per fortuna abbiamo anche adisposizione 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, ilpulsante 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 righedi codice che abbiamo appena scritto, per cui lo scrivete in genere nella forma self.callback). button è ilnostro pulsante, del quale però stiamo qui utilizzando le sue capacità di handler. Bind è il metodo implementato dawx.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 ecallback? E non abbiamo visto che i binder hanno anche loro un metodo Bind? E allora perché stiamo usantowx.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 occorreuna 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 versionidi 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 effettiperò wx.PyEventBinder.__call__ è ancora lì per retrocompatibilità, e potete ancora vedere questo stile dicollegamento nel codice più vecchio (e questa è anche la ragione di questa nota un po’ pedante).

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 originada esso.

Si può anche scegliere un altro handler, però. Per esempio, se il pulsante sta dentro un panel, o un frame, potetescegliere il suo contenitore come handler, e scrivere:

5.7. Gli eventi: le basi da sapere. 41

Page 50: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 daqualsiasi 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 differenzesottili, come vedremo nella seconda parte quando parleremo della propagazione degli eventi. Nella maggior parte deicasi, 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’handlerdi un widget “figlio”, o all’handler di un widget che non ha niente a che vedere con chi ha emesso l’evento (peresempio, 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.

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 daun certo widget? L’unica risposta è leggere la documentazione disponibile. Anche l’utility EventsInStyle può esseremolto utile.

In generale, un widget può originare alcuni eventi “caratteristici” suoi propri. Spesso questi iniziano con un prefisso co-mune, per esempio gli eventi tipici di un wx.ListCtrl iniziano con wx.EVT_LC_*, e quelli di un wx.ComboBoxcon 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 dellatastiera.

Ispezionare i diversi binder non aiuta, perché un binder è indifferente alla riuscita del matrimonio che è chiamato acelebrare: 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]

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 perscoprire gli altri.

Infine, ho scritto una ricetta apposta per cercare di risolvere questo problema: provatela, potrebbe tornarvi utile.

Estrarre informazioni su un evento nel callback.

Come abbiamo visto, i callback devono accettare come argomento un riferimento all’evento che li ha invocati:

42 Chapter 5. Appunti wxPython - livello base

Page 51: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

def callback(self, event): # etc etc

L’argomento event non è altro che l’istanza dell’oggetto-evento che si è originata dal widget, è stata processata, eadesso raggiunge finalmente il callback.

Questo oggetto può portare con sé molte informazioni utili: quali esattamente, dipende dall’evento. Consultate ladocumentazione 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 documen-tazione, trovate per esempio il metodo wx.ListEvent.GetColumn, che vi dice, tra l’altro, la colonna che è statacliccata. 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 dicel’event type esatto.

Non è detto che un oggetto-evento contenga informazioni utili per ciascun metodo previsto dalla sua classe, natural-mente. Per esempio, wx.CommandEvent.IsChecked() è significativo quando il wx.CommandEvent è statoemesso da una checkbox (o da una voce di menu che si può “flaggare”). Naturalmente, se il wx.CommandEventproviene 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) baster-anno a togliervi ogni dubbio.

Un esempio conclusivo.

Queste note non sono un tutorial su wxPython e suppongono che siate in grado di documentarvi da soli, vedere esempidi codice in giro, etc. Tuttavia, in conclusione di questa lunga cavalcata sugli eventi, ecco un esempio minimo per farvedere 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):print 'evento', evt.__class__.__name__print 'oggetto', evt.GetEventObject().GetName()

if __name__ == '__main__':app = wx.App(False)TopFrame(None).Show()app.MainLoop()

5.7. Gli eventi: le basi da sapere. 43

Page 52: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

I menu: le basi da sapere.

I menu non sono difficili, almeno per gli aspetti di base. Difficile è organizzarli in modo efficiente, compatto, intuitivoper l’utente... quante volte avete perso tempo a cercare la funzionalità che vi serve tra le decine di voci di menu di unagui? 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.

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 utentiquando si mostra il frame. Aggiungere o modificare i menu in seguito è una pessima idea (piuttosto, potete abilitare edisabilitare singole voci o interi menu).

I menu “vivono” nella barra dei menu, che come sicuramente sapete è la “striscia” subito sotto la barra del titolo dellavostra 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 alframe 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 ilnome della barra dei menu (menubar) serve solo localmente nell’__init__ della classe: per questo motivo nonabbiamo bisogno di chiamarla self.menubar.

Come creare i menu.

Ogni menu è una istanza della classe wx.Menu. Dopo la sua creazione, un wx.Menu può essere popolato con le varievoci che deve contenere. Infine, viene attaccato alla barra dei menu, usando il metodo wx.MenuBar.Append comevedremo subito. Tuttavia, un menu può essere agganciato anche a un altro menu già esistente, formando in questomodo un sotto-menu.

Espandiamo un poco l’esempio di sopra:

# ... come sopra...menubar = wx.MenuBar()

44 Chapter 5. Appunti wxPython - livello base

Page 53: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 è assegnatosolo 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 dinessun self davanti. Anzi, effettivamente il nome di un menu serve solo fino al momento di aggangiarlo alla barradei menu; per questo motivo possiamo riciclare senza scrupoli il nome generico mymenu per tutti quelli che stiamocreando.

Infine, notate che i vari menu compaiono nella barra (da sinistra a destra) nell’ordine in cui li avete agganciati. Adire il vero wx.MenuBar, oltre ad Append, offre anche un metodo Insert per inserire un menu in una posizionearbitraria: tuttavia la cosa più facile e naturale è collocare i vari menu nell’ordine giusto usando solo Append.

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. Ilprimo argomento, invece, è un id univoco: abbiamo già visto che cosa sono gli id, e che passare -1 vuol dire lasciareche 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 solitocome un tooltip, ma può dipendere dalle piattaforme). Il quarto è un flag che indica il tipo di voce di menu che stiamoinserendo (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 generedobbiamo 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 apopolare i nostri menu con qualche voce:

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')

5.8. I menu: le basi da sapere. 45

Page 54: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 collegamentocon gli eventi. Ci arriviamo subito: prima però, abbiamo ancora un paio di punti in sospeso.

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 separatoreitem5 = mymenu.Append(-1, 'voce cinque')menubar.Append(mymenu, 'SecondoMenu')

Come creare un sotto-menu.

Come abbiamo già accennato, un sotto-menu non è altro che un normale wx.Menu agganciato a un altro menu, inveceche 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 delsecondo menu:

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')

submenu = wx.Menu() # ecco il sotto-menu!

46 Chapter 5. Appunti wxPython - livello base

Page 55: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 menuitem5 = 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 nodoa un evento. Quando l’utente fa clic qui, ci basta il comportamento di default gestito da wxPython (ovvero, aprire levoci del sotto-menu). Tuttavia, se volessimo far succedere qualcosa di diverso, potremmo collegare anche questo nodoa un evento, come qualsiasi altro elemento.

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, dateprima 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 nelframe “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')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()

5.8. I menu: le basi da sapere. 47

Page 56: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 voce a un callbackself.Bind(wx.EVT_MENU, self.on_clic_item1, item1)self.Bind(wx.EVT_MENU, self.on_clic_item2, item2)self.Bind(wx.EVT_MENU, self.on_clic_item3, item3)self.Bind(wx.EVT_MENU, self.on_clic_item4, item4)self.Bind(wx.EVT_MENU, self.on_clic_item5, item5)self.Bind(wx.EVT_MENU, self.on_clic_item10, item10)self.Bind(wx.EVT_MENU, self.on_clic_item11, item11)

# e scrivo i relativi callbackdef on_clic_item1(self, evt): print 'clic su voce uno'def on_clic_item2(self, evt): print 'clic su voce due'def on_clic_item3(self, evt): print 'clic su voce tre'def on_clic_item4(self, evt): print 'clic su voce quattro'def on_clic_item5(self, evt): print 'clic su voce cinque'def on_clic_item10(self, evt): print 'clic su voce uno del submenu'def 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, è cheabbiamo 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 quindiobbligati a collegare il frame stesso (self.Bind), e specificare poi la voce di menu che desideriamo collagarepassandola come argomento di Bind.

Come già accennato, questo è l’unico momento in cui effettivamente utilizziamo i nomi che abbiamo assegnato allevoci di menu (ossia le variabili item1, item2, etc.). Per tenere insieme il momento di creazione della voce di menue 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

E in questo modo eliminiamo completamente la necessità di mantenere i riferimenti a item1, item2 etc.

48 Chapter 5. Appunti wxPython - livello base

Page 57: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Conclusione.

Con queste informazioni dovreste essere in grado di creare e gestire almeno i casi più comuni. Ci sono tuttavia moltealtre cose da dire sui menu: continuate a leggere!

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 nelmondo reale. Dedichiamo invece una pagina separata per le tecniche un po’ più avanzate.

Scorciatoie da tastiera.

Nessun vero menu potebbe dirsi completo senza le scorciatoie da tastiera! wxPython vi permette di utilizzare entrambele 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 avetebisogno di scrivere una “&” nella vostra voce di menu, dovete fare l’escape e scrivere &&. Se avete bisogno di creareuna 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 latastiera, 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). Lapolitica 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 direttamenteun’azione senza la necessità di aprire un menu. Per esempio, in genere “Ctrl+S” serve per “salvare”, “Ctrl+O” serveper “aprire”, “Ctrl+C” per “copiare” e “Ctrl+V” per “incollare” (qualsiasi cosa voglia dire nella logica della vostraapplicazione!). All’interno dei menu, gli acceleratori sono sempre scritti accanto alla voce corrispondente.

5.9. I menu: altri concetti di base. 49

Page 58: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

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 deimenu (e perfino se non usate nessun menu!). Tuttavia è molto comune usarli in accoppiata con i menu, e in effettic’è un modo facilissimo per integrare acceleratori e menu in wxPython: basta aggiungere all’etichetta della voce lasequenza \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 “depu-rata” 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 potetedefinire 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 diazioni standard come aprire, salvare, copiare, incollare... non cercate comunque di riciclare questi acceleratori per ivostri scopi: gli utenti sono abituati a usare “Ctrl+o” con il significato di “aprire”, e non vogliono dover memorizzareuna convenzione alternativa valida solo per il vostro programma.

Viceversa, se la vostra applicazione prevede azioni standard, ricordatevi di fornire gli accelerati standard corrispon-denti: gli utenti se lo aspettano.

Un tocco di classe: fornite gli acceleratori più comuni nelle diverse piattaforme. Per esempio, “chiudere” un pro-gramma è “Alt+F4” in Windows, “Cmd+Q” nel Mac. Potete testare a runtime la piattaforma in uso, ricorrendo peresempio 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 questodettaglio. Con questa agevolazione, di fatto la stragrande maggioranza degli acceleratori sono identici tra le variepiattaforme: potete regolarvi con questa pagina di Wikipedia. Diventa più complesso se volete tener conto delleabitudini 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 addiritturain 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(......)])

50 Chapter 5. Appunti wxPython - livello base

Page 59: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

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 modorapido 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 altrettantiacceleratori:

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:

5.9. I menu: altri concetti di base. 51

Page 60: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

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 loshortcut 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, eper buona misura usare un acceleratore per entrambi. In questo scenario conviene naturalmente associare l’acceleratorealla voce di menu con il metodo rapido visto prima: quando l’utente digita la combinazione di tasti ottiene comunquel’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...'

Disabilitare i menu.

Come praticamente tutti i widget di wxPython, anche le voci di menu hanno un metodo Enable che consente diabilitarli e disabilitarli, e un metodo IsEnabled per scoprire il loro stato attuale. Naturalmente, per fare questodovete accedere alle singole voci al di fuori dell’__init__, e pertanto dovete conservare un riferimento in unavariabile 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 perwx.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 conservarlain una variabile al momento della creazione, oppure scoprirla in seguito con wx.MenuBar.FindMenu che accettail 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 menuself.voce_salva = menu_A.Append(-1, 'Salva')menu_A.Append(-1, 'Apri')menu_A.Append(-1, 'Chiudi')

menu_B = wx.Menu()

52 Chapter 5. Appunti wxPython - livello base

Page 61: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

menu_B.Append(-1, 'Qui')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 correnteself.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()

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 questoci 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 allavolta.

wxPython supporta entrambe le possibilità. Le voci “spuntabili” si ottengono aggiungendo il flag wx.ITEM_CHECKal 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 unseparatore).

Ricordatevi che il flag wx.ITEM_* è il quarto argomento del metodo Append, come abbiamo già visto: il terzo è la

5.9. I menu: altri concetti di base. 53

Page 62: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

stringa di “help text” che spesso si lascia vuota).

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-groupmenu.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-gruppomenu.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 persapere 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 IsCheckeddel 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)

54 Chapter 5. Appunti wxPython - livello base

Page 63: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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)))

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()

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 leggerequel 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 idcome scorciatoia per identificare la provenienza di un evento (grazie all’uso di evt.GetId() nel callback). Nonripetiamo 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 dacui è 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 usandowx.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 inun 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)

5.9. I menu: altri concetti di base. 55

Page 64: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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)

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

Conclusione.

Questa pagina e la precedente completano il panorama di ciò che vi serve sapere per usare i menu nella vita di tutti igiorni. Ci sono tecniche più esotiche, di cui parleremo un’altra volta.

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.

56 Chapter 5. Appunti wxPython - livello base

Page 65: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

To self or not to self?

Quando si costruisce un contenitore (un panel, frame, dialogo), in genere nell’__init__ si elencano i widget chedeve contenere. E qui si pone un piccolo problema: è necessario che i loro nomi siano visibili in tutta l’istanzadel contenitore, oppure solo all’interno del metodo __init__ in cui li definiamo? Ovvero, è necessario semprepremettere self. al nome, oppure no?

La domanda si pone anche perché, all’inizio della propria esperienza con wxPython, molti purtroppo usano stru-menti sbagliati come gui builder o RAD. Quando esaminate il codice prodotto da questi pessimi assistenti, trovateinterminabili 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 assoluta-mente niente del genere.

Regolatevi come fareste per un normale programma a oggetti: solo quando vi serve mantenere un riferimento visibileanche 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, eper 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. Premettereself. 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 incui 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 ilnamespace 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 vederea colpo d’occhio i widget più “importanti” nella mia gui.

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 mostrarloall’utente. Tuttavia, alcuni preferiscono spostare il disegno “puro e semplice” del layout (creazione e popolamento deisizer) in un metodo separato _do_layout o qualcosa del genere, che viene richiamato dall’__init__.

5.10. Questioni varie di stile. 57

Page 66: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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):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 generatoautomaticamente dagli stessi pessimi assistenti di cui sopra.

Lo schema può estendersi ulteriormente: per esempio, aggiungere un metodo _set_properties per impostarecolori, font, etc. dei vari widget; aggiungere un metodo _do_binding per raccogliere tutti i collegamenti deglieventi ai callback.

Le ragioni addotte per queste separazioni sono sempre molto deboli: è vero, in questo modo l’__init__ è più snelloe 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 paragrafoprecedente), 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 codiceviene rielaborata e ridotta a pattern comuni.

Anzi, questa separazione artificiosa può addirittura ostacolare la fattorizzazione del codice. Considerate per esempioquesto 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,

58 Chapter 5. Appunti wxPython - livello base

Page 67: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

ponetevi il problema di una reale fattorizzazione del codice. Un buon esempio (forse troppo pignolo, a dire il vero) sitrova tra gli esempi della documentazione tratti dal capitolo 5 del libro “wxPython in Action”. Confrontate lo scriptbadExample.py con goodExample.py per avere un’idea di come si possa riformulare lo stesso layout in modopiù compatto e “astratto”.

5.10. Questioni varie di stile. 59

Page 68: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

60 Chapter 5. Appunti wxPython - livello base

Page 69: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 6

Appunti wxPython - livello intermedio

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 ilMainLoop della nosrta wx.App.

Questo in genere è più che sufficiente per i casi più semplici. Tuttavia, per le applicazioni più complesse, potrestevolere qualche tipo di controllo in più su questo processo di avviamento.

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 nelMainLoop. E’ il momento giusto, tipicamente, per inizializzare alcune risorse esterne (connessioni al database, logdi sistema, file di configurazione, opzioni “globali”...). Infine, convine usare OnInit anche per istanziare e mostrareil frame principale dell’applicazione: infatti, farlo in questo momento garantisce che la wx.App esiste già, e quindi lacreazione 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 databaseself.db = connect(my_db, ...)# apro un logself.log = open('logfile', 'a')# leggo un file di configurazioneconfig = ConfigParser.ConfigParser(...)# creo il frame principaletop_frame = MyFrame(None)# lo mostro

61

Page 70: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

top_frame.Show()# esco da OnInit segnalando che tutto e' a postoreturn True

if __name__ == '__main__':app = MyApp(False) # istanzio la mia app personalizzataapp.MainLoop() # e invoco il MainLoop per avviarla

Questo modo di procedere presenta numerosi vantaggi. Per esempio, se una connessione al database viene apertain OnInit, “appartiene” alla wx.App, e quindi è disponibile globalmente per tutti gli elementi presenti e futuridell’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 restituisceTrue la wx.App continuerà a vivere. Se invece restituisce False, la wx.App verrà chiusa, e la nostra applicazioneabortirà prematuramente.

Questo può essere sfruttato per chiudere subito tutto, quando per esempio una risorsa esterna non è disponibile. Peresempio, 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 messaggidi 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:

62 Chapter 6. Appunti wxPython - livello intermedio

Page 71: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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.',

'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 Falseelse:

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 startupdell’applicazione in modo preciso, compreso il feedback all’utente ed eventuali interazioni con lui. L’uso accorto diOnInit permette anche di velocizzare i tempi: se qualcosa deve andar storto, si può fermare tutto prima di caricarela parte più gravosa dell’interfaccia.

Infine, una piccola eleganza: avete notato che quando chiudiamo MyLoginDialog la nostra applicazione continuaa vivere (almeno fin quando, eventualmente, non decidiamo di return False), nonostante abbiamo appena chiusol’unica finestra “top level” presente? In effetti questa è un’eccezione alla regola: wxPython non inizia il processo dichiusura, se non è mai entrato nel MainLoop. Questo ci consente di aprire e chiudere finestre a nostro piacimento inOnInit senza paura di conseguenze spiacevoli.

Note: Potreste chiedervi perché c’è bisogno di un metodo separato OnInit per queste operazioni di apertura, quandoin genere in questi casi si lavora direttamente nell’__init__ della classe. Il punto è che l’__init__ è riservatoal bootstrap della stessa wx.App, e non è il posto giusto per metterci dentro anche il codice di inizializzazione dellavostra applicazione. Per esempio l’__init__ deve sempre restituire None, e quindi non è agevole gestire un errore diinizializzazione 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à.

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 ilog, salvare le configurazioni e le preferenze...

Proprio come avviene in OnInit, anche in OnExit potete approfittarne per chiedere ancora qualche decisioneall’utente. Potete ancora mostrare un wx.Dialog modale (ossia mostrato con ShowModal()).

6.1. wx.App: concetti avanzati. 63

Page 72: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Se però cercate di creare e mostrare un nuovo frame “top level” a questo punto, nella speranza di prevenire la chiusuradella 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 settandoself.SetExitOnFrameDelete(False) prima di mostrare il nuovo frame top-level. Effettivamente il frameresta visibile, ma l’applicazione si pianta. Questo codice, per esempio, non funziona:

class MyApp(wx.App):def OnInit(self):

wx.Frame(None).Show()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 wx-Python... ma a questo argomento dedichiamo una pagina separata.

Re-indirizzare lo standard output/error.

Durante tutto il ciclo di vita di una applicazione wxPython, lo standard output e lo standard error sono normalmenteutilizzati, e per default restano come di consueto indirizzati verso la shell da cui avete invocato lo script Python delvostro programma.

Il costruttore di wx.App accetta tuttavia un argomento redirect che determina se l’output dell’applicazione deveessere 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 finestradell’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 conquesta wx.App:

64 Chapter 6. Appunti wxPython - livello intermedio

Page 73: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

if __name__ == '__main__':app = wx.App(True) # re-indirizzamento verso una finestraMyFrame(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 deidue stream.

La finestra che ospita l’output, per default, è gestita da wx.PyOnDemandOutputWindow. Si tratta di una classeche, non appena è necessario, crea, mostra e gestisce un semplice frame con un wx.TextCtrl multi-linea e disola lettura. Le scritture degli stream nel wx.TextCtrl sono thread-safe: se provengono da thread secondari, sonochiamate attraverso wx.CallAfter.

La finestra di output, una volta creata, è una finestra top-level: questo è necessario, perché al momento di istanziarela wx.App con redirect=True nessun frame principale è ancora stato creato. Tuttavia è una limitazione fas-tidiosa: anche quando l’utente ha chiuso la finestra principale dell’applicazione, la wx.App (e quindi in sostanza ilprogramma) 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 prin-cipale (per forza: altrimenti non avete nessun parent da assegnare alla finestra di output); ma prima che la finestra siacreata la prima volta (ovvero, prima che venga scritto qualcosa nell’output). Un esempio chiarirà forse meglio: usateancora 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 finestraframe = MyFrame(None)# questo e' il momento giusto per impostare il parent della stdioWinapp.stdioWin.SetParent(frame)frame.Show()app.MainLoop()

Come si vede dal questo esempio, quando creiamo una wx.App con il parametro redirect=True, questacrea 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 vienescritto qualcosa nello stream): per maggior sicurezza, in questo caso abbiamo preferito impostare il parent prima an-cora 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 dellawx.App. Per questo basta chiamare il metodo wx.App.RedirectStdio, che accetta un argomento opzionalefilename (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:

6.1. wx.App: concetti avanzati. 65

Page 74: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

# app.RedirectStdio('output.txt')else:

app.RestoreStdio()# questo e' necessario perche' la finestra non si chiude da solaapp.stdioWin.close() # notare la "c" minuscola!

def on_clic(self, evt):print '... uno standard output ...'

if __name__ == '__main__':app = wx.App(False)MyFrame(None).Show()app.MainLoop()

Se poi wx.PyOnDemandOutputWindow vi sembra troppo spartana e volete mostrare l’output in un modopiù 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 istanziarela wx.App:

if __name__ == '__main__':wx.App.outputWindowClass = MyOutputClass # una classe personalizzataapp = wx.App(True)# etc etc

Oppure, se volete sotto-classare wx.App:

class MyApp(wx.App):MyApp.outputWindowClass = MyOutputClassdef OnInit(self):

# etc etc

La vostra classe personalizzata può essere qualsiasi cosa: potete sotto-classare wx.App.outputWindowClassoppure creare da zero. Dovete tuttavia impegnarvi a rispettare l’api di wx.App.outputWindowClass, mettendoa disposizione i seguenti metodi:

• CreateOutputWindow(self, txt) dove create effettivamente la finestra di output, e ci scrivete il primomessaggio 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 thread-safe (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 finestragrafica (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 lafinestra 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 persalvare l’output su un file. Notate come abbiamo implementato tutti i metodi richiesti, e ci siamo anche assicurati chele 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)

66 Chapter 6. Appunti wxPython - livello intermedio

Page 75: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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)

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 = Noneself.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 = Noneself.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)

6.1. wx.App: concetti avanzati. 67

Page 76: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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))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'outputwx.App.outputWindowClass = MyOutputManagerapp = wx.App(True)MyFrame(None).Show()app.MainLoop()

Todo

una pagina sui thread.

Chiudere i frame e gli altri widget.

Il momento della chiusura in wxPython è delicato. In questa pagina esaminiamo il processo di chiusura dei frame edei dialoghi, e aggiungiamo qualche nota sulla chiusura dei widget in generale. Il discorso prosegue in una paginaseparata 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, inwxPython, 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, quandode-referenziate un oggetto, automaticamente tutte le risorse collegate che non servono più vegono pulite dalla memoriadal garbage collector. Ma C++ non ha garbage collector, quindi la memoria va pulita “a mano”. Ora, quando chiudeteun frame, wxWidget naturalmente provvede da solo a eliminare anche tutti gli elementi di quel frame (pulsanti, caselledi 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 intervenireper fare cleanup, o anche ripensarci e tornare indietro. wxPython eredita questo sistema, anche se, in effetti, è raro cheun programmatore Python lo utilizzi appieno.

La chiusura di una finestra.

La chiusura di una “finestra” (un frame o un dialogo) può capitare (di solito!) in due modi:

68 Chapter 6. Appunti wxPython - livello intermedio

Page 77: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• perché l’utente fa clic sul pulsante di chiusura (quello con la X, per intenderci), o usa “Alt+F4”, “Mela-Q” oaltri 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 perchiudersi. 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 ditutti i suoi figli;

• per un wx.Dialog, invece, lo nasconde alla vista, ma non lo distrugge. Questo comportamento è voluto, pervenire incontro alla situazione tipica in cui si vuole raccogliere dei dati dal dialogo, prima di distruggerlo. Restail 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 ilprocesso 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 (anchese, dal punto dell’usabilità, è una pessima pratica che vi sconsiglio):

1 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 self.Bind(wx.EVT_CLOSE, self.on_close)5

6 def on_close(self, evt):7 msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',8 wx.ICON_QUESTION|wx.YES_NO)9 if msg.ShowModal() == wx.ID_YES:

10 self.Destroy() # oppure, volendo: evt.Skip()11 msg.Destroy()

Alla riga 10, chiamiamo esplicitamente self.Destroy() solo se l’utente ha risposto affermativamente. In casocontrario, chiamiamo comunque Destroy() almeno sul MessageDialog che abbiamo creato, perché altrimentiresterebbe in vita.

Invece di chiamare self.Destroy(), avremmo naturalmente anche potuto chiamare evt.Skip(), lasciando algestore predefinito dell’evento il compito di distruggere la finestra.

Come abbiamo detto, wx.EVT_CLOSE si innesca anche quando voi chiamate Close(). Per vederlo, possiamoaggiungere un semplice pulsante che chiama Close nel suo callback:

1 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 b = wx.Button(self, -1, 'chiudimi!')5 b.Bind(wx.EVT_BUTTON, self.on_click)6 self.Bind(wx.EVT_CLOSE, self.on_close)7

8 def on_click(self, evt):9 self.Close()

10

11 def on_close(self, evt):12 msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',

6.2. Chiudere i frame e gli altri widget. 69

Page 78: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

13 wx.ICON_QUESTION|wx.YES_NO)14 if msg.ShowModal() == wx.ID_YES:15 self.Destroy()16 msg.Destroy()

Anche quando agite sul pulsante, il self.Close() della riga 9 scatena comunque il wx.EVT_CLOSE, e di con-seguenza viene eseguito il codice del callback on_close.

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, inquesto specifico caso non vi serve comunque a nulla, perché nessun’altra parte del vostro codice è interessata alla sortedi quella finestra.

Ma Veto() diventa utile, per esempio, quando chiamate Close() su una finestra da un’altra finestra: in questocaso, 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 chiudetela finestra, chiaramente), allora Close() restituisce False, e fa sapere in questo modo come sono andate le cose.

Ecco un esempio pratico:

1 class MyTopFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 self.do_child = wx.Button(self, -1, 'crea un frame figlio')5 self.do_child.Bind(wx.EVT_BUTTON, self.on_child)6 self.child = None7

8 def on_child(self, evt):9 if not self.child:

10 self.child = MyChildFrame(self, title='Figlio', size=(150, 150),11 style=wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX)12 self.child.Show()13 self.do_child.SetLabel('CHIUDI il frame figlio')14 else:15 closed_successful = self.child.Close()16 if closed_successful:17 self.do_child.SetLabel('crea un frame figlio')18 self.child = None19

20 class MyChildFrame(wx.Frame):21 def __init__(self, *a, **k):22 wx.Frame.__init__(self, *a, **k)23 self.Bind(wx.EVT_CLOSE, self.on_close)24

25 def on_close(self, evt):26 msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',27 wx.ICON_QUESTION|wx.YES_NO)28 if msg.ShowModal() == wx.ID_NO:29 evt.Veto()30 else:31 self.Destroy()32 msg.Destroy()33

70 Chapter 6. Appunti wxPython - livello intermedio

Page 79: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

34 if __name__ == '__main__':35 app = wx.App(False)36 MyTopFrame(None).Show()37 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() (allariga 29) in modo che Close() restituisca False, e quindi il codice chiamante sappia come comportarsi (alle righe15-18).

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.

Ignorare il Veto() se si vuole chiudere lo stesso.

E non è ancora finita, dicevamo. Chiamare semplicemente Veto() su un evento di chiusura potrebbe non esseresicuro. 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 sollevaun’eccezione e tutto si pianta...

Quindi la cosa giusta è verificare sempre se l’evento effettivamente può “vietare”, prima di chiamare Veto(). Laverifica può essere fatta chiamando CanVeto() sull’evento stesso. Ecco come dovrebbe essere modificato il callbackdell’esempio precedente:

1 def on_close(self, evt):2 if evt.CanVeto():3 msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Uscita',4 wx.ICON_QUESTION|wx.YES_NO)5 if msg.ShowModal() == wx.ID_NO:6 evt.Veto()7 else:8 self.Destroy()9 msg.Destroy()

10 else: # se non possiamo vietare, dobbiamo distruggere per forza...11 self.Destroy()

Ora, in verità l’annotazione della riga 10 non è del tutto corretta. Anche se non possiamo “vietare” l’evento, possiamosempre scegliere di non distruggere la finestra, e fare qualcos’altro (ricordiamo che “vietare” l’evento e chiudere effet-tivamente sono due cose separate). Ma questa sarebbe proprio una cosa da non fare. Primo perché ovviamente, se nondistruggiamo mai in risposta a un wx.EVT_CLOSE, la nostra finestra non si chiuderà mai (a meno di non distruggerlaesplicitamente 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 potrebbeavere 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, accadequando 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 generanon ha il potere di “vietare” un bel niente (o più precisamente, restituisce False quando testate per CanVeto()).

6.2. Chiudere i frame e gli altri widget. 71

Page 80: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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().

Essere sicuri che una finestra si chiuda davvero.

Ancora una precisazione. L’opzione force=True del metodo Close è un pochino ingannevole. Non significaaffatto, di per sé, che la chiusura della finestra verrà forzata e quindi garantita in ogni caso. Vuol dire solo che l’eventonon avrà il potere di “vietare” la chiusura. Ma, ricordiamolo ancora una volta, “vietare” l’evento e chiudere davverola 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, deveessere considerato una cattiva pratica, se non un errore di programmazione vero e proprio. Ma wxPython non hamodo di rilevare una cosa del genere a runtime, e voi non potete sapere se state chiamando Close(True) su unafinestra 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 lafinestra è stata davvero distrutta, è... chiamarla, ovviamente! Sul “lato Python” di wxPython, il riferimento all’oggettoresterà ancora nel namespace corrente. Ma sul “lato C++” di wxWidgets, quando una finestra è distrutta, semplice-mente smetterà di funzionare. Quindi una chiamata successiva a un metodo qualsiasi dovrebbe sollevare un’eccezionewx.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 ladistruzione avverrà entro il prossimo ciclo del MainLoop, ma se chiamate GetId su un frame immediatamente dopoaverlo distrutto, la chiamata per il momento andrà ancora a segno.

Provate questo codice, per esempio:

1 class MyTopFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 kill = wx.Button(self, -1, 'uccidi il figlio', pos=(10, 10))5 kill.Bind(wx.EVT_BUTTON, self.on_kill)6 autopsy = wx.Button(self, -1, "verifica se e' morto", pos=(10, 50))7 autopsy.Bind(wx.EVT_BUTTON, self.on_autopsy)8

9 self.child = wx.Frame(self, -1, 'FRAME FIGLIO')10 self.child.Show()11

72 Chapter 6. Appunti wxPython - livello intermedio

Page 81: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

12 def on_kill(self, evt):13 self.child.Destroy() # andiamo sul sicuro...14 self.child.GetId()15

16 def on_autopsy(self, evt):17 self.child.GetId()18

19 if __name__ == '__main__':20 app = wx.App(False)21 MyTopFrame(None, size=(150, 150)).Show()22 app.MainLoop()

Sorprendentemente, la chiamata della riga 14 andrà ancora a segno, anche se avete appena distrutto il frame. Seinvece, dopo aver distrutto il frame, premete il pulsante “verifica”, la chiamata della riga 17 solleverà il tanto sospiratowx.PyDeadObjectError.

Sull’eccezione wx.PyDeadObjectError resta ancora qualcosa da dire. Fin qui ne abbiamo parlato dal punto divista della sua desiderabilità (quando cioè lo usiamo come strumento per testare se un widget è stato distrutto). Nellepagine 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 dellecomplicazioni ulteriori, di cui parliamo in una pagina separata. Per il momento, ci limitiamo a concludere che nonc’è modo di sapere esattamente quando un widget verrà distrutto: tuttavia, dopo un ragionevole intervallo di tempo, èmolto facile capire se è stato distrutto.

Distruggere un singolo widget.

Praticamente tutti i widget in wxPython hanno un metodo Close e un metodo Destroy. Se volete distruggere unpulsante, 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 unwidget qualsiasi. Tuttavia, nessuno vi vieta di sottoclassare un widget, e prescrivere un comportamento particolare datenere quando qualcuno cerca di chiuderlo.

Tuttavia, è raro distruggere un singolo widget. In genere si preferisce disabilitarlo, al limite nasconderlo: distruggerlolascia 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 aversenso distruggerli, e perfino gestire qualche raffinatezza con Close. Personalmente, io consiglio di non distruggeremai 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 class MyPanel(wx.Panel):2 def __init__(self, *a, **k):3 wx.Panel.__init__(self, *a, **k)4 self.SetBackgroundColour(wx.RED) # per distinguerlo...5 self.Bind(wx.EVT_CLOSE, self.on_close)6

7 def on_close(self, evt):8 msg = wx.MessageDialog(self, 'Sei proprio sicuro?', 'Distruggi Panel',9 wx.ICON_QUESTION|wx.YES_NO)

10 if msg.ShowModal() == wx.ID_NO:11 evt.Veto()12 else:13 self.Destroy()14 msg.Destroy()

6.2. Chiudere i frame e gli altri widget. 73

Page 82: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

15

16 class TopFrame(wx.Frame):17 def __init__(self, *a, **k):18 wx.Frame.__init__(self, *a, **k)19 p = wx.Panel(self)20 b = wx.Button(p, -1, 'distruggi panel')21 b.Bind(wx.EVT_BUTTON, self.on_clic)22 self.b = b23

24 self.mypanel = MyPanel(p)25 s = wx.BoxSizer(wx.VERTICAL)26 s.Add(wx.TextCtrl(self.mypanel, -1, 'figlio di MyPanel'),27 0, wx.EXPAND|wx.ALL, 15)28 self.mypanel.SetSizer(s)29

30 s = wx.BoxSizer(wx.VERTICAL)31 s.Add(self.mypanel, 1, wx.EXPAND)32 s.Add(b, 0, wx.EXPAND|wx.ALL, 5)33 p.SetSizer(s)34

35 def on_clic(self, evt):36 ret = self.mypanel.Close()37 if ret:38 pass # etc. etc.39

40 if __name__ == '__main__':41 app = wx.App(False)42 TopFrame(None).Show()43 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 figliocomporta 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.

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.

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 qualcheoperazione 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 unriferimento 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.

74 Chapter 6. Appunti wxPython - livello intermedio

Page 83: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Per prima cosa, ricordiamo che la wx.App termina una volta che tutte le finestre top-level sono state chiuse, ovverotutte le finestre che hanno parent=None, come abbiamo già visto. Quindi fate attenzione a non lasciare qualchefinestra top-level nascosta ma ancora viva. I casi tipici sono due: avete creato qualche dialogo top-level e non l’avetemai 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 vivama nascosta per qualche ragione (forse perché, per tutta la durata della vostra applicazione, non c’è stato niente dascrivere 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 opoi spegnerà il computer... Ma se invece riavvia e poi “chiude” un po’ di volte il programma, ben presto si troverà lamemoria 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 dalMainLoop. Inutile dire che, se questo codice comprende operazioni di assestamento dei dati nel database, o dellescritture nel log, queste non verranno eseguite, e la prossima volta che si aprirà il programma ci si troverà con datiinconsistenti.

Quindi fate sempre molta attenzione a non creare finestre top-level e poi lasciarle in giro senza sapere se ci sonoancora o no. Una strategia di emergenza è catturare il wx.EVT_CLOSE della finestra “principale” e, prima di dis-truggerla, 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 è chiamaredirettamente 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 vaancora bene: se sono ancora vivi e nascosti, vuol dire che ve ne siete semplicemente dimenticati, ma ormai nondovrebbero più avere nessuna funzione. Per i frame, d’altra parte, la situazione è più delicata. Forse prevedono delcodice da eseguire in risposta a un EVT_CLOSE, ma se chiamate Destroy() invece di Close() perderete quelpassaggio. 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 seuna finestra è un frame o un dialogo? Di colpo, siamo nel campo della magia nera di Python:

1 def on_close(self, evt):2 ok_to_close = True3 for window in wx.GetTopLevelWindows():4 if window != self:5 if wx._windows.Frame in window.__class__.__mro__:6 # e' un frame, proviamo a chiuderlo gentilmente7 ret = window.Close()8 if not ret:9 # evidentemente non vuole chiudersi!

10 ok_to_close = False11 break12 else:13 # questo e' un dialogo: distruggiamolo senza pieta'14 window.Destroy()15 if ok_to_close:16 self.Destroy()17 else:18 # c'e' in giro almeno una finestra che non vuole chiudersi19 wx.MessageBox('Non posso chiudermi!')

6.3. Terminare la wx.App. 75

Page 84: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

20 evt.Veto()21 return

Come vedete (riga 5), siamo piombati nel difficile, molto difficile. E non è detto che funzioni: per esempio, se unadelle finestre rifiuta di chiudersi, ma si “dimentica” di comunicare il suo Veto(), allora window.Close() (riga7) restituirà True, e noi crederemo di averla chiusa quando invece è ancora in giro. Ci tocca aggiungere altri test peressere davvero sicuri...

Ovviamente non sono ipotesi frequenti. Devo dire di non aver mai usato, in pratica, un metodo come questo per accer-tarmi che tutte le finestre top-level siano chiuse al momento di uscire dall’applicazione. E francamente vi sconsigliodi provarci.

La soluzione corretta è invece tenere sempre traccia di tutte le finestre che aprite, soprattutto quelle top-level, e diaccertarvi sempre di chiuderle appena non servono più. In questo modo, quando arriva il momento di chiudere anchel’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 comeun’eventualità da gestire, ma come un vero e proprio baco da correggere.

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 suoMainLoop resti attivo anche dopo che l’ultima finestra è stata chiusa.

Per fare questo, vi basta chiamare SetExitOnFrameDelete(False) sulla wx.App. Potete farlo proprioall’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 framequalsiasi:

wx.GetApp().SetExitOnFrameDelete(False)

Potete farlo perfino, proprio all’ultimo, intercettando il wx.EVT_CLOSE dell’ultima finestra principale che sta perchiudersi. 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, poteteandare avanti creando delle nuove finestre top-level. Ecco una possibile strategia:

1 class MyApp(wx.App):2 def OnInit(self):3 self.SetExitOnFrameDelete(False)4 self.Bind(wx.EVT_IDLE, self.create_new_toplevel)5 wx.Frame(None, title='PRIMA GENERAZIONE').Show()6 return True7

8 def create_new_toplevel(self, evt):9 if not wx.GetTopLevelWindows():

10 wx.Frame(None, title='SECONDA GENERAZIONE!!').Show()11 # dopo questa volta pero' basta...12 self.SetExitOnFrameDelete(True)13

14 if __name__ == '__main__':

76 Chapter 6. Appunti wxPython - livello intermedio

Page 85: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

15 app = MyApp(False)16 app.MainLoop()

La procedura è chiara: all’inizio (riga 3) settiamo il flag a False, e quindi creiamo e mostriamo il primo frame top-level. Tuttavia (riga 4) chiediamo anche alla wx.App di eseguire a ripetizione il metodo create_new_toplevelnei 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 aTrue, in modo che alla prossima chiusura il MainLoop questa volta termini davvero.

Ecco un altro possibile approccio:

1 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 self.Bind(wx.EVT_CLOSE, self.on_close)5

6 def on_close(self, evt):7 wx.CallLater(1, wx.GetApp().create_new_toplevel)8 self.Destroy()9

10 class MyApp(wx.App):11 def OnInit(self):12 self.SetExitOnFrameDelete(False)13 MyFrame(None, title='PRIMA GENERAZIONE').Show()14 return True15

16 def create_new_toplevel(self):17 MyFrame(None, title='SECONDA GENERAZIONE!!').Show()18 self.SetExitOnFrameDelete(True)19

20 if __name__ == '__main__':21 app = MyApp(False)22 app.MainLoop()

Qui invece è l’ultima finestra top-level che, al momento della sua chiusura (riga 7) utilizza wx.CallLater perchiedere 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 poichiama una funzione. Lo abbiamo scelto perché wx.CallLater non tiene impegnato il MainLoop, e quindi ciserve a dimostrare che il MainLoop resta vivo comunque, per altri motivi (ossia, perché abbiamo settato il flag aFalse).

Avremmo potuto invece usare wx.CallAfter, che è “quasi uguale”, nel senso che chiama una data funzionedopo che tutti i gestori degli eventi correnti sono stati processati. Il punto però è che wx.CallAfter aggiungela sua funzione in coda ai compiti del MainLoop, e quindi lo tiene impegnato almeno fino a quel momento. Esiccome nel nostro caso la funzione chiamata è create_new_toplevel che appunto crea una nuova finestra top-level, in sostanza il MainLoop non ha mai modo di terminare, indipendentemente da come è stato settato il flagSetExitOnFrameDelete.

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 distruggereanche questa, la wx.App non termina come prima, anche se il flag è ormai impostato a True. Invece, ogni voltaappare una nuova “seconda generazione”, all’infinito. Questo perché wx.CallAfter tiene in vita il MainLoopfino al momento di chiamare create_new_toplevel, dove però si crea una nuova finestra top-level, e quindi ilMainLoop trova un’altra ragione per proseguire la sua attività, all’infinito.

6.3. Terminare la wx.App. 77

Page 86: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 class MyFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 self.Bind(wx.EVT_CLOSE, self.on_close)5

6 def on_close(self, evt):7 wx.CallAfter(wx.GetApp().create_new_toplevel)8 self.Destroy()9

10 class MyApp(wx.App):11 def OnInit(self):12 MyFrame(None, title='PRIMA GENERAZIONE').Show()13 return True14

15 def create_new_toplevel(self):16 MyFrame(None, title='SECONDA GENERAZIONE!!').Show()17

18 if __name__ == '__main__':19 app = MyApp(False)20 app.MainLoop()

Naturalmente questo lascia aperto il problema di capire come terminare, a un certo punto, la wx.App. Ma non è unproblema enorme. Si potrebbe aggiungere un test nel callback on_close, in modo da chiamare wx.CallAfteruna volta sola. Oppure si potrebbe chiamare wx.Exit()...

Ma questo è appunto l’argomento del prossimo paragrafo.

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 conoscerecome ultima risorsa.

Il primo è chiamare wx.GetApp().Exit() (oppure la scorciatoia equivalente wx.Exit()). Questo terminaimmediatamente il MainLoop. Funziona, e lascia anche il tempo di eseguire il codice eventualmente contenuto inwx.App.OnExit. Però chiude tutte le finestre top-level senza generare wx.EVT_CLOSE. Quindi, qualsiasi codicedi 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 significala garanzia che gli eventi ancora pendenti saranno gestiti. Dall’altro, vuole anche dire che non c’è garanzia che ilprogramma sarà terminato proprio immediatamente.

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 chel’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’utentepotrebbe per errore spegnere il computer prima di aver chiuso il vostro programma. Potreste comunque essere ingrado di salvare la situazione: wxPython emette un evento wx.EVT_QUERY_END_SESSION quando per qualcheragione la sessione del sistema operativo è in procinto di terminare. Se lo intercettate, nel callback relativo potete

78 Chapter 6. Appunti wxPython - livello intermedio

Page 87: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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, ilgestore di default chiama comunque wx.Window.Close su tutte le finestre top-level. Questo a sua volta, comesappiamo, in condizioni normali vi permette di intercettare il conseguente wx.EVT_CLOSE per gestire le vostreoperazioni di chiusura. Se non intercettate neppure quello, la wx.App dovrebbe comunque avere il tempo di chiudersisenza problemi, e quindi tutte le operazioni di chiusura “normale” che avete previsto in wx.App.OnExit dovrebberosvolgersi 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. InWindows, 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 usarewx.SafeShowMessage anche in assenza di una wx.App funzionante, e quindi addirittura prima che la wx.Appsia stata correttamente avviata.

Le dimensioni in wxPython.

I questa pagina vediamo come si specificano le dimensioni di un widget in wxPython. Gran parte delle cose chestiamo per dire hanno significato soprattutto quando sono applicate ai sizer. Quindi queste note sono un importantecomplemento alle due pagine che dedichiamo ai sizer: in questa pagina diamo per scontato che le abbiate già lette.

wx.Size: la misura delle dimensioni.

In wxWidgets le dimensioni si indicano come istanze della classe wx.Size. Questo significa, per esempio, che perdefinire 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 ar-gomenti (larghezza e altezza), senza bisogno di istanziare esplicitamente wx.Size. Questi doppioni si riconosconoperché 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 converteautomaticamente le istanze di wx.Size in più confortevoli tuple. Potete ancora usare esplicitamente wx.Size seproprio 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’istanzadi wx.Size. Naturalmente si può sempre usare la versione *WH del metodo, se si preferisce passare due argomentiinvece della tupla.

In aggiunta a questo, in wxPython alcuni metodi “getter” hanno dei “doppioni” che restituiscono una tupla al postodi istanze di wx.Size. Si riconoscono perché terminano in *Tuple. Per esempio, GetSize ha il compagnoGetSizeTuple.

6.4. Le dimensioni in wxPython. 79

Page 88: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Note: Tutto questo è in effetti confuso e ridondante. E’ vero, ma dovete ricordare che in C++ non sono disponibilile strutture-dati di alto livello di Python. Quindi wxWidgets definisce una lunga serie di tipi fondamentali, comewx.Point, wx.Size, wx.Rect, wx.DateTime e molti altri. wxPython da un lato semplifica le cose, dall’altronecessariamente 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 miimporta”. 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.

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 esisteanche la forma *WH di SetSize.

• GetSize(Tuple), SetSize(WH): specificano esattamente le dimensioni che deve avere il widget. Notateche, se il widget è inserito in un sizer con flag wx.EXPAND e/o con proporzione superiore a 0, le sue dimensionipotrebbero comunque variare.

• GetClientSize(Tuple), SetClientSize(WH): come i precedenti, ma meno platform-dependent seusati con i frame e i dialoghi. Infatti calcolano solo l’area “effettiva” della finestra, lasciando fuori bordi e barradel 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 delcostruttore di tutti i widget. Quindi SetInitialSize è come un “costruttore differito” per quanto riguardale 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 minimeche può avere il widget.

• SetSizeHints(Sz): consente di specificare dimensioni massime e minime in un colpo solo, come direSetMaxSize seguito da SetMinSize.

• GetVirtualSize(Tuple), SetVirtualSize(WH), SetVirtualSizeHints(Sz): per le finestrecon scrolling incorporato (wx.ScrolledWindow, etc.) si riferisce alle dimensioni “vere”, e non quelle chesi vedono effettivamente.

• GetBestSize(Tuple): il “best size”, ossia le dimensioni minime per cui il widget si mantiene “pre-sentabile” (per esempio, per un wx.StaticText questo dipende dalla lunghezza del testo che deve esserevisualizzato).

La cosa importante da capire qui è che potete indicare esattamente le dimensioni di un widget, fornire indicazionisu minimi e/o massimi, o infine non indicarle affatto. Tenendo conto dei vincoli che imponete, l’algoritmo dei sizercercherà di distribuire lo spazio disponibile nel miglior modo possibile.

Fit e Layout: ricalcolare le dimensioni.

Esistono apparentemente due versioni di Fit, una come metodo di wx.Sizer (quindi di tutti i sizer derivati) eun’altra come metodo di wx.Window (quindi di tutti i widget). In realtà il secondo finisce per chiamare il primo,

80 Chapter 6. Appunti wxPython - livello intermedio

Page 89: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

quindi alla fine è indifferente quale utilizzate.

wx.Sizer.Fit(window) (passando come argomento il contenitore che il sizer gestisce) dice al sizer di calcolarele 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 sec-onda dei casi. Se alla finestra è stato assegnato un sizer, chiama direttamente wx.Sizer.Fit(window) per fare illavoro. 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 de-fault di un evento wx.EVT_SIZE non chiama Layout automaticamente per ridisegnare la finestra ogni volta chel’utente la ridimensiona, e in genere questo non è necessario se il vostro layout è disegnato con i sizer. Se inveceusate 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 chevolete ri-disegnare). Ricordatevi anche che, se catturate voi stessi un wx.EVT_SIZE, dovreste sempre ricordarvi dichiamare 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:

• quando date delle dimensioni fisse a un frame, e poi lo riempite con dei widget, li organizzate in un sizer e infineassegnate 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__ risolvele 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 nascostidei widget.

In casi particolari potrebbe essere necessario innescare programmaticamente un wx.EVT_SIZE, anche se la finestranon viene ridimensionata. Per esempio, se nascondete/mostrate una toolbar, o un menu, o una status bar, allorachiamare 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.

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 alledimensioni dei widget.

wx.GridSizer: una griglia rigida.

Se wx.BoxSizer è una colonna (o una riga) singola, wx.GridSizer è invece una griglia di celle. Al contrariodel wx.BoxSizer, al momento di creare un wx.GridSizer dovete specificare in anticipo il numero di righe e dicolonne. 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 lecelle, in pixel.

Una volta che avete creato il sizer, potete inserire i vari elementi usando Add come di consueto. Il sizer si riempirà dasinistra a destra e dall’alto in basso.

6.5. I sizer: seconda parte. 81

Page 90: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Il wx.GridSizer è una struttura rigida: il widget più grande determina la dimensione di tutte le celle. Se gli altriwidget 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 ciascunwidget con il proprio bordo. La seconda è specificare gli spazi, e poi inserire il sizer completo in un wx.BoxSizerdi 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 soloquando siete sicuri che tutti i widget abbiano le stesse dimensioni. Il caso da manuale sono i pulsanti di una calcola-trice:

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)

boxsizer = wx.BoxSizer()boxsizer.Add(sizer, 1, wx.EXPAND|wx.ALL, 5) # un bordo di cornicep.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()

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 disponibiledopo che tutte quelle “normali” avranno occupato lo spazio minimo richiesto (basandosi sul widget più grande chedevono contenere, come per il wx.GridSizer).

Le righe/colonne “flessibili” si contendono lo spazio disponibile in base alla stessa regola delle priorità che abbiamovisto per il wx.BoxSizer. Quindi, per esempio:

sizer = wx.FlexGridSizer(5, 5, 2, 2) # una griglia 5 x 5sizer.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 laquinta 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):

82 Chapter 6. Appunti wxPython - livello intermedio

Page 91: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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', 'telefono'):

sizer.Add(wx.StaticText(p, -1, lab), 0, wx.ALIGN_CENTER_VERTICAL)sizer.Add(wx.TextCtrl(p, -1, name=lab), 0, wx.EXPAND)

boxsizer = wx.BoxSizer()boxsizer.Add(sizer, 1, wx.EXPAND|wx.ALL, 5) # un bordo di cornicep.SetSizer(boxsizer)

if __name__ == '__main__':app = wx.App(False)TopFrame(None).Show()app.MainLoop()

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;

• è 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 riempirlouna 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 (obbli-gatorio!) è pos, una tupla per specificare la posizione di inserimento. Il secondo (facoltativo) è span, per specificareper 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 especificando lo span.

Usare i wx.GridBagSizer può essere comodo da un lato, fonte di confusione dall’altro. Ovviamente tutto ciò chepotete 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 ilwx.GridBagSizer. Questo, comunque, è il genere di layout che dovete disegnare prima su un foglio di carta, pernon confondervi troppo.

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 elementiinclusi, con una label in alto.

6.5. I sizer: seconda parte. 83

Page 92: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Lo wx.StaticBoxSizer va usato solo in accoppiata con il suo wx.StaticBox, che va creato per primo. Infattiil costruttore dello wx.StaticBoxSizer richiede un argomento in più rispetto al normale wx.BoxSizer, ossiaappunto 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)

StdDialogButtonSizer e CreateButtonSizer: sizer per pulsanti generici.

Abbiamo già incontrato il concetto di pulsanti con Id predefiniti, da usare tipicamente nei dialoghi. Per maggiorecomodità, è possibile inserirli automaticamente in un sizer orizzontale chiamato StdDialogButtonSizer.

Il metodo CreateButtonSizer, chiamato su un dialogo, restituisce automaticamente unStdDialogButtonSizer già completo e pronto da inserire nel resto del layout.

Per esempio, questo:

btn_sizer = self.CreateButtonSizer(wx.OK|wx.CANCEL) # 'self' e' un dialogomain_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.

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 contenitorea cui è assegnato (per esempio, il bordo di una finestra) “va a capo” aggiungendo un’altra riga o un’altra colonna, aseconda 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):

84 Chapter 6. Appunti wxPython - livello intermedio

Page 93: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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, peresempio, 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 wxPythonnon implementa in modo completo questo sizer: di conseguenza, alcune delle proprietà che si possono leggere nelladocumentazione 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 consuetoflag di orientamento (come per wx.BoxSizer, può essere orizzontale o verticale). Il secondo è una serie di flag distile 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 illoro valore numerico, come abbiamo fatto nell’esempio qui sopra. La documentazione di wxWidgets sostiene che ilcomportamento di default è di avere entrambi i flag settati: in wxPython tuttavia sembra che il valore di default sia alcontrario di non avere nessun flag settato, e inoltre i valori numerici delle costanti sembrano differire tra wxPythone wxWidgets (qui sopra abbiamo indicato quelli di wxPython, naturalmente). Quindi conviene sempre dichiarareesplicitamente quali flag si desiderano attivi (di solito è più utile settare il secondo, o entrambi: potete comunquesperimentare le alternative).

REMOVE_LEADING_SPACES serve se introducete degli spazi vuoti nel vostro sizer: in questo caso, il flag impedisceche lo spazio vuoto finisca all’inizio di una riga o di una colonna. Nell’esempio qui sopra, de-commentate la rigas.Add((50, 50)) per introdurre uno spazio a metà dei pulsanti: potrete verificare che effettivamente lo spazionon 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 disponi-bile. E’ un comportamento che talvolta è desiderabile, talvolta no: vi conviene sperimentare, e verificare se fa al casovostro caso per caso.

Infine occorre segnalare che la documentazione di wxWidget menziona anche un metodowxWrapSizer::IsSpaceItem, che si può sovrascrivere per dire al sizer di considerare anche altri ele-menti specifici come se fossero degli spazi, ai fini del calcolo invocato dal flag REMOVE_LEADING_SPACES. InwxPython 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 conwx.Sizer.AddSpacer o wx.Sizer.AddStretchSpacer).

Esempi di utilizzo dei sizer.

Nella documentazione trovate vari esempi di layout realizzati con i sizer. In particolare, potete cercare “sizer” nellademo. Inoltre, il capitolo 11 del libro “wxPython in action” è dedicato ai sizer, per cui tutti gli esempi della documen-tazione tratti da quel capitolo sono interessanti. In particolare, realworld.py mostra un tipico esempio di come isizer possono essere usati nel “mondo reale”.

6.5. I sizer: seconda parte. 85

Page 94: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 ilconcetto di “widget inserito in un sizer”. In genere non abbiamo bisogno di questo valore di ritorno, ma volendopossiamo 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 è statodisegnato 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”:

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()

86 Chapter 6. Appunti wxPython - livello intermedio

Page 95: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Si noti l’uso di wx.Window.SendSizeEvent per invocare il ridisegno del layout anche quando la finestra non haeffettivamente cambiato dimensioni.

In pratica, tuttavia, queste tecniche di manipolazione del layout non sono consigliabili. E’ buona norma non modificarel’interfaccia in modo vistoso dopo averla mostrata la prima volta: l’utente ha bisogno di ambientarsi e ricordare laposizione dei widget, farsi una mappa mentale degli aspetti più importanti della vostra gui e dei pattern di utilizzo perlui più consueti. Se voi alterate profondamente il layout a runtime, aggiungendo e togliendo, spostando e modificandoi widget, l’utente ne ricaverà solo un senso di disordine e irritazione. Spesso i programmatori inesperti pensano chesia utile, per esempio, far sparire i widget non necessari (o inattivi) in quel momento, e farli riapparire solo quandoservono: ma in realtà ci sono sempre modi migliori per organizzare il layout, e wxPython non è certo carente disoluzioni intelligenti (per esempio, si possono organizzare i widget in “pagine” usando un wx.Notebook o altrianaloghi contenitori a schede).

Gli eventi: concetti avanzati.

In questa pagina affrontiamo alcuni aspetti degli eventi che non entrano quasi mai in gioco in un normale programmawxPython. 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 terminologiae 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, usandoil metodo Bind di un binder (in pratica si usa il Bind dell’handler, ma questo invoca subito il Bind del binder perfare il lavoro). Lo stile che si usa in genere è questo:

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 dawx.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 esempioself.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.

La propagazione degli eventi.

Abbiamo detto che tutti gli eventi derivano da wx.Event, ma questa in realtà è una classe-base che fa poco. Unadistinzione più importante avviene al livello della sottoclasse wx.CommandEvent: questi eventi (e tutti quelli delleclassi 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, raggiungein primo luogo il widget stesso che lo ha originato. Se l’evento è di quelli che non si propagano, allora il segnale siferma 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’unicomodo per intercettare questi eventi è usare lo stile widget.Bind(wx.EVT_*, callback) (ricordiamo ancorache 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 delparent, e continua ad arrampicarsi per tutta la catena dei parent, fino a quando non raggiunge la finestra top-level. Diqui, salta direttamente alla wx.App.

6.6. Gli eventi: concetti avanzati. 87

Page 96: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 ungestore 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.

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 algoritmointerno wx.EvtHandler.ProcessEvent(). E si va alla fase 1.

FASE 1

l’handler è abilitato?

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 unhandler (su un widget, cioè) per impedirgli di processare eventi. Per ripristinare il comportamento normale, bastachiamare 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-stylewx.WS_BLOCK_EVENTS che nei dialoghi è settato per default. Questo significa che un evento può passare da

88 Chapter 6. Appunti wxPython - livello intermedio

Page 97: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

un frame al parent (eventuale) del frame, ma non dal dialogo al parent del dialogo. Naturalmente è possibilesettare 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 “livellodi propagazione” interno. L’unico modo per conoscerlo è chiamare event.StopPropagation(), cheinterrompe 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 ildiretto 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 effettivamenteall’infinito. Ma potreste voler scrivere un classe-evento personalizzata che si propaga in modo più limitato, senecessario.

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 ProcessEventtermina 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”).

A questo punto l’algoritmo cerca l’handler successivo a cui bisogna rivolgersi. La ricerca avviene secondo le prece-denze che elenchiamo qui sotto. In breve, ogni volta che viene trovato un handler, si torna alla fase 1, e si esegue ilciclo 1-2-3. Se la fase 3 determina che occorre una ulteriore propagazione, si torna a questa fase 4A, e si riprende laricerca 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. Ovvi-amente è 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 nonne 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’algoritmosi ferma. Altrimenti, tutti gli handler addizionali vengono interrogati in seguenza. Quando sono esauriti, si procedecon 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, lafase 3 determina che l’evento non si può propagare ulteriormente (tipicamente perché un callback è stato trovato edeseguito nella fase 2, ma non ha chiamato Skip) allora l’algoritmo si ferma, ProcessEvent termina e restituisceTrue. Se invece a ogni passaggio la fase 3 determina che l’evento può ancora propagarsi, si passa alla sovra-classesuccessiva fino a esaurirle. Quindi si procede alla fase 4A.3 qui sotto.

• 4A.3: handler del parent

6.6. Gli eventi: concetti avanzati. 89

Page 98: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Soltanto se, nell’ultima fase 3 attraversata, abbiamo stabilito che l’evento può ancora propagarsi, finalmente si passaal parent del widget attuale. Si chiede prima al suo handler, e poi si continua a cercare nelle sovra-classi e tra glihandler addizionali, percorrendo sempre le fasi 1-2-3 finché la fase 3 non determina che l’evento non può ulteriormentepropagarsi.

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, sealla 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 suoparent. Tuttavia, la ricerca nelle sovra-classi e negli handler addizionali avviene ancora. Quindi, ecco quello chesuccede:

• 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 3troviamo 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 allafase 4B.2.

• 4B.2: handler addizionali

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 certopassaggio, 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 ancoracon 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’handlerdella 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 disolito.

Se perdete anche questa ultima occasione, il ProcessEvent dell’handler della wx.App ha comunque un com-portamento predefinito, che semplicemente non fa nulla. In questo modo, l’algoritmo termina comunque e l’eventomuore.

90 Chapter 6. Appunti wxPython - livello intermedio

Page 99: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 nonchiami Skip() sull’evento.

Come funziona Skip().

event.Skip() può essere chiamato sull’evento, dall’interno di un callback che lo gestisce. Non importa se vienechiamato all’inizio o alla fine del codice del vostro callback: imposta comunque un flag interno all’evento, che segnalaall’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.

Entrambe queste cose sono importanti, e per quanto riguarda la seconda, bisogna ricordare che spesso una singolaazione 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 unwx.EVT_BUTTON in sequenza. Se voi intercettate il primo, e nel callback non chiamate Skip(), gli altri due nonverranno 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 invocareil comportamento di default di un widget. Per esempio, quando fate clic sul pulsante, wxPython deve comunquepreoccuparsi 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 call-back. Più precisamente, arriva dopo il vostro, perché è scritto nella sovra-classe madre da cui avete derivato il vostrowidget. 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 devepropagarsi (fase 3). Quindi, se intercettate un evento e non chiamate Skip() nel relativo callback, potreste impedirela 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’esecuzionedi ulteriori callback che potreste aver scritto in corrispondenza del secondo e del terzo; ma inoltre impedirete a wx-Python di gestire correttamente lo stato del pulsante.

6.6. Gli eventi: concetti avanzati. 91

Page 100: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 tran-quillamente 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 voleteintercettare. Così potete risparmiarvi di chiamare Skip() nel callback, perché wxPython ormai ha già fatto la suaparte.

Una lezione che si può trarre da tutto questo è: non dovete intercettare wx.EVT_LEFT_UP su un pulsante, se potetefare la stessa cosa intercettando wx.EVT_BUTTON.

Una seconda lezione è questa: se siete in dubbio, chiamate Skip().

Un esempio per Skip().

Ecco qualche riga di codice che illustra l’esempio del “clic su un pulsante”:

1 class SuperButton(wx.Button):2 def __init__(self, *a, **k):3 wx.Button.__init__(self, *a, **k)4 self.Bind(wx.EVT_BUTTON, self.on_clic)5

6 def on_clic(self, event):7 print 'clic su SuperButton'8 event.Skip()9

10 class MyButton(SuperButton):11 def __init__(self, *a, **k):12 SuperButton.__init__(self, *a, **k)13

14 class TestEventFrame(wx.Frame):15 def __init__(self, *a, **k):16 wx.Frame.__init__(self, *a, **k)17 p = wx.Panel(self)18 button = MyButton(p, -1, 'clic!', pos=(50, 50))19 button.Bind(wx.EVT_LEFT_DOWN, self.on_down)20 button.Bind(wx.EVT_LEFT_UP, self.on_up)21 button.Bind(wx.EVT_BUTTON, self.on_clic)22 button.SetDefault()23

24 def on_down(self, event):25 print 'mouse giu'26 event.Skip()27

28 def on_up(self, event):29 print 'mouse su'30 event.Skip()31

32 def on_clic(self, event):33 print 'clic'34 event.Skip()35

36 if __name__ == '__main__':37 app = wx.App(False)

92 Chapter 6. Appunti wxPython - livello intermedio

Page 101: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

38 TestEventFrame(None).Show()39 app.MainLoop()

Come si vede, abbiamo creato una gerarchia di sotto-classi di wx.Button per testare anche la ricerca degli handlernelle sovra-classi.

Stiamo intercettando contemporaneamente wx.EVT_LEFT_DOWN, wx.EVT_LEFT_UP e wx.EVT_BUTTON. Nellaconfigurazione di base, tutti i callback chiamano Skip(). Se provate a eseguire adesso lo script, trovate che l’ordine incui i callback sono chiamati rispecchia la normale ricerca degli handler: prima on_down, poi on_up, poi on_clice infine SuperButton.on_clic.

Avvertenza: abbiamo un piccolo problema terminologico. Da questo momento, quando dico “pulsante” intendo “pul-sante 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 ilpulsante del mouse, ma poi allontanate il puntatore dall’area del bottone prima di rilasciarlo, allora verranno catturatiil wx.EVT_LEFT_DOWN e anche il wx.EVT_LEFT_UP, tuttavia il wx.EVT_BUTTON non verrà emesso. wxPythonsa che il secondo evento “appartiene” ugualmente al bottone, anche se il puntatore si è spostato nel frattempo: lo saperché ha avuto modo di completare correttamente il processo interno del primo evento, e adesso si aspetta che ilprossimo wx.EVT_LEFT_UP sia da attribuire al bottone. Tuttavia, quando il wx.EVT_LEFT_UP effettivamente siverifica, 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 averlospostato 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’è nessunaparticolare 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 wxPythonnon è 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:in realtà l’oggetto-evento del mouse (l’istanza della classe wx.MouseEvent) è creato da wxPython allo startupdell’applicazione, e resta sempre in circolazione: assume di volta in volta differenti “event type” (e quindi può esserecollegato da differenti binder) a seconda dell’azione specifica del mouse in quel momento. Quindi non c’è niente distrano 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 ges-tione non completa del precedente wx.EVT_LEFT_DOWN, adesso wxPython non è più in grado di capire che ilwx.EVT_LEFT_UP deve essere attribuito comunque al bottone. Inutile dire che, in queste circostanze, non c’è modoper wx.EVT_LEFT_UP di chiudere in bellezza innescando il wx.EVT_BUTTON, anche rimanete con il puntatoreall’interno dell’area del bottone. Quando non avete eseguito il gestore di default del wx.EVT_LEFT_DOWN, avetespezzato irrimediabilmente il meccanismo: una sequenza di “giù” e poi “su”, sia pure nell’area del bottone, non bastapiù 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. Chiaramentei 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 vieneinnescato. Ma ciò che è peggio, il bottone resta costantemente “premuto” rifiutando di resettarsi (potete passarcisopra il puntatore del mouse per convincervi del problema). Inoltre, adesso wxPython attribuisce ogni successivo clicdel mouse al bottone: fate clic al di fuori del bottone, e vedrete che i vostri callback continuano a essere chiamati

6.6. Gli eventi: concetti avanzati. 93

Page 102: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

lo stesso. Ovviamente, siccome tutti i clic sono attribuiti al bottone, non potete nemmeno più chiudere la finestradell’applicazione!

Impressionante, vero? Ovviamente questa non è una conseguenza generale che avviene ogni volta che dimenticatedi chiamare Skip al momento giusto. In questo caso, molto dipende dal tipo di gestione interna che avviene neiwx.Button.

Tuttavia, la regola generale resta quella: se siete in dubbio, chiamate Skip.

Bind e la propagazione degli eventi.

Finalmente siamo in grado di rispondere alla domanda da cui eravamo partiti: che differenza c’è tra widget.Bind(...) e self.Bind(..., button)?

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 contiene2

3 button.Bind(wx.EVT_BUTTON, self.callback) # (1)4 self.Bind(wx.EVT_BUTTON, self.callback, button) # (2)5 self.Bind(wx.EVT_BUTTON, self.callback) # (3)

Lo stile (1) collega l’evento generato da button direttamente all’handler button. Questo significa che l’handlerbutton 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 collegamentoche vi serve davvero.

Lo stile (2) collega l’evento generato da button all’handler di self (che nel nostro esempio potrebbe essere unpanel, o un altro contenitore). Nove volte su dieci, questo stile ha lo stesso effetto del precedente. Tuttavia è importantecapire che in questo caso l’evento viene catturato solo dopo che si è propagato qualche volta. La catena dei parent dabutton 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 è utileinserire diversi handler lungo la catena di propagazione.

Ecco un esempio pratico:

1 from itertools import cycle2

3 class ColoredButton(wx.Button):4 def __init__(self, *a, **k):5 wx.Button.__init__(self, *a, **k)6 self.Bind(wx.EVT_BUTTON, self.change_color)7 self.color = cycle(('green', 'yellow', 'red'))8 self.SetBackgroundColour(self.color.next())9

10 def change_color(self, event):11 self.SetBackgroundColour(self.color.next())12 event.Skip()13

14

15 class TopFrame(wx.Frame):16 def __init__(self, *a, **k):17 wx.Frame.__init__(self, *a, **k)18 panel = wx.Panel(self)19 button = ColoredButton(panel, -1, 'clic!', pos=(50, 50))20 panel.Bind(wx.EVT_BUTTON, self.on_clic, button)21

22 def on_clic(self, event):

94 Chapter 6. Appunti wxPython - livello intermedio

Page 103: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

23 print 'qui facciamo il vero lavoro...'24

25 if __name__ == '__main__':26 app = wx.App(False)27 TopFrame(None).Show()28 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 pul-sante stesso (riga 6: utilizziamo il primo stile). Notate che change_color chiama Skip, permettendo all’evento dipropagarsi per essere intercettato anche in seguito.

Infatti, quando vogliamo usare il nostro pulsante nel mondo reale, è necessario preservare il suo comportamento didefault (cambiare colore), e aggiungere il lavoro vero e proprio che vogliamo fargli fare nella nostra applicazione. Ilmodo è semplice: basta aspettare che l’evento arrivi al contenitore superiore (in questo caso panel), e intercettarlodi 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 dellasemantica. In entrambi i casi, colleghiamo un handler a un evento. Significa che l’handler gestirà quell’evento, ognivolta 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 parentdi qualche altro wx.Button, quindi gli unici wx.EVT_BUTTON che gli capiteranno mai sotto mano saranno quelliche emette lui stesso. D’altra parte, nel caso dello stile (3), l’handler è un contenitore che potrebbe avere al suo internonumerosi wx.Button. L’handler gestirà i wx.EVT_BUTTON di tutti i pulsanti che sono (anche indirettamente) suoifigli.

Naturalmente, all’interno del callback potete chiamare event.GetEventObject() e risalire così al pulsantespecifico che ha emesso l’evento. Ecco un esempio:

1 class TopFrame(wx.Frame):2 def __init__(self, *a, **k):3 wx.Frame.__init__(self, *a, **k)4 panel = wx.Panel(self)5 button_A = wx.Button(panel, -1, 'A', pos=(50, 50))6 button_B = wx.Button(panel, -1, 'B', pos=(50, 100))7 panel.Bind(wx.EVT_BUTTON, self.on_clic)8

9 def on_clic(self, event):10 print 'premuto', event.GetEventObject().GetLabel()11

12 if __name__ == '__main__':13 app = wx.App(False)14 TopFrame(None).Show()15 app.MainLoop()

Ricapitolando: lo stile (1) e lo stile (3) dicono entrambi all’handler di gestire ogni evento di quel tipo, non importa dadove è 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 uncontenitore.

Nella pratica, lo stile (1) è quello che va bene nella maggior parte dei casi. Lo stile (2) può aver senso se avete in mentedi 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.

Bind per gli eventi “non command”.

C’è un’altra ragione importante per cui lo stile (1) è quello più utilizzato.

6.6. Gli eventi: concetti avanzati. 95

Page 104: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Di fatto, è l’unico stile di collegamento che potete usare per gli eventi non “command”. Infatti, siccome questi non sipropagano, 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 diricercare il comportamento predefinito nelle sovra-classi.

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 class MyButton(wx.Button):2 def __init__(self, *a, **k):3 wx.Button.__init__(self, *a, **k)4 self.Bind(wx.EVT_BUTTON, self.onclic)5

6 def onclic(self, evt):7 print 'clic dalla classe Mybutton'8 evt.Skip()9

10

11 class Test(wx.Frame):12 def __init__(self, *a, **k):13 wx.Frame.__init__(self, *a, **k)14 panel = wx.Panel(self)15 button = MyButton(panel, -1, 'clic', pos=((50,50)))16

17 button.Bind(wx.EVT_BUTTON, self.onclic_button)18 panel.Bind(wx.EVT_BUTTON, self.onclic_panel, button)19 self.Bind(wx.EVT_BUTTON, self.onclic_frame, button)20

21 def onclic_button(self, evt):22 print 'clic dal button'23 evt.Skip()24

25 def onclic_panel(self, evt):26 print 'clic dal panel'27 evt.Skip()28

29 def onclic_frame(self, evt):30 print 'clic dal frame'31 evt.Skip()32

33 class MyApp(wx.App):34 def OnInit(self):35 self.Bind(wx.EVT_BUTTON, self.onclic)36 return True37

38 def onclic(self, evt):39 print 'clic dalla wx.App'40 evt.Skip()41

42 if __name__ == '__main__':43 app = MyApp(False)44 Test(None).Show()45 app.MainLoop()

96 Chapter 6. Appunti wxPython - livello intermedio

Page 105: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Questo esempio copre i casi comuni e alcuni scenari più avanzati. Tuttavia, non è ancora completo: quando verrà ilmomento di parlare degli handler personalizzati, ne scriveremo una versione più ampia.

I menu: concetti avanzati.

Nelle due pagine precedenti che abbiamo dedicato ai menu, abbiamo coperto le basi necessarie per l’uso di tutti igiorni. In questa pagina copriamo invece altre tecniche, non necessariamente più “difficili”, ma semplicemente menoconsuete.

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 almenu. Di conseguenza non possiamo usare il solito pattern (menu.Append(...)), ma dobbiamo creare la voce dimenu 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 (ladifferenza è 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 inwxPython. Per il momento, vi basta sapere che si fa così.

Todo

una pagina sulle immagini

Menu contestuali e popup.

Ecco invece qualcosa di più concreto. Finora abbiamo visto soltanto menu (wx.Menu) agganciati a una barra deimenu (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 qualsiasipunto in risposta a un qualsiasi evento, in teoria (in teoria! In pratica, però, è meglio non sorprendere troppo il poveroutente).

Di per sé queste tecniche non sono difficilissime, ma per essere usate in modo efficiente richiedono un briciolo distrategia. 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 aprein 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. Inpratica, la strategia migliore è considerarli menu “usa-e-getta” che create e distruggete ogni volta (potete nascondere

6.7. I menu: concetti avanzati. 97

Page 106: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

il menu invece di distruggerlo: ma questo funziona solo se vi serve ogni volta lo stesso menu, e in genere è pocopratico).

Se non ci sono problemi a creare e distruggere anche mille volte un menu, conviene però gestire gli eventi una voltasola, 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 vocidei menu, e quindi i menu stessi. La cosa è perfettamente possibile: basta “prenotare” degli id, e usare gli id perfare il binding. Ebbene sì: i menu contestuali sono uno dei pochi posti di wxPython in cui non è sbagliato usareesplicitamente 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): # etc etcdef my_callback_2(self, evt): # etc etcdef my_callback_3(self, evt): # etc etc

Per prima cosa collegate dei semplici id a dei callback specifici. Poi, quando arriverà il momento di creare le voci delmenu contestuale, basterà fare attenzione ad assegnare manualmente gli id giusti. Alla fine, potrete distruggere senzaproblemi 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à es-istono anche nel menu principale (salva, copia, incolla...), e che avete già collegato ai callback giusti al momento dicreare il menu principale. Anche in questo caso, la soluzione è di attribuire esplicitamente l’id di queste voci, e usarelo stesso id anche nel menu contestuale. Per esempio:

menu = wx.Menu() # questo e' il menu principalemenu.Append(100, 'foo') # questa servira' anche nei menu contestuali# ...self.Bind(wx.EVT_MENU, self.my_callback, id=100)

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 questicasi.

Una volta creato, il menu viene mostrato con il metodo self.PopupMenu() (dove self è la finestra corrente). Ilmenu appare nel punto in cui si trova il cursore del mouse: siccome di solito voi mostrate il menu in risposta a un clicdell’utente, il menu apparirà lì dove l’utente se lo aspetta (a meno che il menu appaia in risposta a un evento che noncomporta 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 incorrispondenza della sua scelta (eventualmente: perché l’utente potrebbe anche cliccare fuori dal menu, e in questocaso niente succede). Quando l’evento è stato processato, il flusso del programma torna nelle vostre mani: la primacosa che dovete fare è ovviamente distruggere il menu, in modo da non lasciarlo in giro (il comportamento di defaultsi 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)

98 Chapter 6. Appunti wxPython - livello intermedio

Page 107: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

# 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 popupmenu.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 usarewx.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 occa-sione. Come si vede, il meccanismo di base è piuttosto semplice.

Ecco invece l’esempio di prima modificato per mostrare come la stessa voce può apparire in un menu “normale” e inun menu popup:

class MyFrame(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)

# binding per i menu popupself.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 principalemenu.Append(-1, 'bla bla')menu.Append(102, 'anche popup') # questo va anche nel popupself.Bind(wx.EVT_MENU, self.my_callback_3, id=102)

menubar = wx.MenuBar()menubar.Append(menu, 'Menu')self.SetMenuBar(menubar)

6.7. I menu: concetti avanzati. 99

Page 108: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 principaleself.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 pulsantedestro 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 unbanale 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.

In secondo luogo, così wx.EVT_RIGHT_UP viene lasciato libero: potete usarlo separatamente per processare altrecose, se vi serve. Attenti solo a non pasticciare con la catena degli eventi: quando l’utente rilascia il pulsante destrodel mouse, per prima cosa viene innescato il wx.EVT_RIGHT_UP. Se questo non viene processato, allora si innescail wx.EVT_CONTEXT_MENU. Quindi, se catturate l’evento del mouse, non dimenticatevi di chiamare Skip(), al-trimenti 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 comportamentova benissimo (e non provate a modificarlo, se non volete farvi odiare), se il menu compare in seguito a un clic delmouse.

Ma se il menu contestuale è chiamato con la tastiera, allora il comportamento di default non è più adatto, perché ilpuntatore 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 chiamandoGetPosition sull’evento nel callback. Se GetPosition vi restituisce wx.DefaultPosition invece di una

100 Chapter 6. Appunti wxPython - livello intermedio

Page 109: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

tupla, vuol dire che l’evento è stato chiamato dalla tastiera. In questo caso, prima di mostrare il menu contestuale, viconviene decidere una posizione adatta.

Nell’esempio che segue, vogliamo che una casella di testo abbia un menu contestuale: se l’utente lo richiama con ilmouse, tutto bene. Ma se lo chiama con la tastiera, allora dobbiamo fare un po’ di calcoli per assicurarci che compaiain 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 vocimenu.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()

Manipolare dinamicamente i menu.

I menu sono strumenti complessi, e wxPython mette a disposizione molti metodi per maneggiarli. Poteteall’occorrenza far sparire voci di menu, aggiungerle, spostarle. E potete far sparire o cambiare allo stesso modointeri menu.

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 qualcheparte.

Cambiare la struttura dei menu a runtime è probabilmente sempre una cattiva idea. Per esempio, se l’utente nonpuò accedere a certe voci, la cosa migliore è disabilitarle ma lasciarle visibili. Così l’utente può almeno capire che inseguito 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 credeche “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’altroriferirvi alla documentazione per scoprire qualcosa di più. In particolare, la demo (cercate “menu”) illustra qualcheesempio 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

6.7. I menu: concetti avanzati. 101

Page 110: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

MenuItem c++ sottostante. Questo è utile se volete re-inserire la voce di menu in un secondo tempo (Removerestituisce 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 posizionedell’elemento precedente a quello che volete inserire. Allo stesso modo potete inserire un menu nella barra dei menucon 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 anchemenu.FindItemById(id) e menu.FindItemByPos(position), con significato ovvio. Anziché rivolgersia un menu singolo, è possibile chiedere alla barra dei menu di cercare una determinata voce, ovunque sia: per questobasta 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 è mostrarel“‘help text” del wx.MenuItem), e infine xw.EVT_MENU_HIGHLIGHT_ALL (come sopra, ma innescato quando sipassa col mouse sopra una voce qualsiasi: utile quando non vi interessa sapere quale voce in particolare sta scorrendol’utente).

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 menuserve una volta sola. Potete senz’altro dare una sforbiciata alle righe del vostro programma, se vi fa piacere. Ma nonne 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 troverebbenaturale 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:

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, viverrà in mente che invece della tupla di etichette potete anche scrivere:

labels = 'Topolino Paperino Qui Quo Qua'.split()

102 Chapter 6. Appunti wxPython - livello intermedio

Page 111: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 sputifuori un wx.Menu già bello pronto per essere attaccato alla sua wx.MenuBar. Se vi piace la terminologia dei designpattern, 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 lefunzioni 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-menumenu.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 unavoce al menu; se si tratta invece di un’altra lista, aggiunge un sotto-menu con il nome del primo elemento, e procedea 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 conosciamol’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 ricevereanche degli id, o addirittura già i nomi dei callback da collegare; oppure potrebbe assegnare gli id secondo un patternricostruibile 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. Bisognerebbemodificarla.

E le voci spuntabili e i blocchi “radio”? Ehm, vanno calcolati anche loro.

Dopo un po’, è facile perdere il filo. Tanto più cercate di generalizzare il problema, tanto più vi trovate a dover scrivereun’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 ingenere 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 “fattor-izzazioni” eleganti ma di discutibile utilità pratica, potete guardare negli esempi tratti dal libro “wxPython inAction” (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 chemostrano 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 momentodella loro creazione, ma in seguito, durante il ciclo di vita della vostra applicazione. I menu crescono facilmentefino a diventare sistemi complessi, e mantenere sempre aggiornato e coerente il loro stato è difficile. A seconda deicasi, le varie voci vanno abilitate e disabilitate, spuntate, resettate... Spesso finite per costruire una rete intricata diEnable(True) e Enable(False) nei vari callback, che diventa rapidamente ingestibile.

6.7. I menu: concetti avanzati. 103

Page 112: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

La vera sfida di “fattorizzazione” dei menu, quindi, è di trovare una forma pratica per separare la logica di business ela 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 ciascunavoce, di calcolare gli aggiornamenti a seconda degli eventi che riceve, e di comunicare a sua volta questi cambiamentialla gui.

Anche in questo caso, tuttavia, non è il caso di perdere troppo tempo nello sforzo di generalizzare e prevedere tuttele possibilità in anticipo. Partite da una soluzione rudimentale che si adatta alla vostra situazione, e poi apportatemiglioramenti man mano che vi servono.

Todo

una pagina su mvc.

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 comprenderea 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 dedichiamoquesta 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” classewx.Validator. Voi però dovete sempre usare la “versione python” wx.PyValidator. La ragione della presenzadi 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

una pagina sui pycontrols

Come scrivere un validatore.

Occorre semplicemente sottoclassare wx.PyValidator. Ecco un esempio da manuale: questo è un validatore chesi 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 Truedef TransferFromWindow(self): return True

def Validate(self, ctl):win = self.GetWindow()val = win.GetValue().strip()

104 Chapter 6. Appunti wxPython - livello intermedio

Page 113: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

if val == '':return False

else:return True

Le righe 2, 3, 4 sono (purtroppo) boilerplate necessario. Clone deve esserci necessariamente, e deve restituire unaistanza 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 deverestituire 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, maquesto 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 argomentiextra, se necessario. Per esempio, questo validatore garantisce che il valore immesso non sia in una bad-list di paroleproibite:

1 class NotInBadListValidator(wx.PyValidator):2 def __init__(self, badlist):3 wx.PyValidator.__init__(self)4 self._badlist=badlist5

6 def Clone(self): return NotInBadListValidator(self._badlist)7 def TransferToWindow(self): return True8 def TransferFromWindow(self): return True9

10 def Validate(self, ctl):11 win = self.GetWindow()12 val = win.GetValue().strip()13 if val in self._badlist:14 return False15 else:16 return True

Chiaramente, in questo caso volete sovrascrivere anche l’__init__ per gestire i paramentri aggiuntivi. Non dimen-ticatevi di riportare gli argomenti correttamente anche in Clone (riga 6)... anche il boiledplate richiede un minimo diattenzione.

Una volta scritto, il validatore si applica al widget che intendete validare, al momento della sua creazione, passandolodirettamente al costruttore (come parametro validator). Ovviamente potete usare lo stesso validatore (cioè: diverseistanze dello stesso validatore) per validare più widget, se ne avete bisogno. Ecco un esempio:

1 class YourNamePanel(wx.Panel):2 def __init__(self, *a, **k):3 wx.Panel.__init__(self, *a, **k)4 self.first_name = wx.TextCtrl(self, validator=NotEmptyValidator())5 self.family_name = wx.TextCtrl(self, validator=NotEmptyValidator())6

7 s = wx.FlexGridSizer(2, 2, 5, 5)8 s.AddGrowableCol(1)9 s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)

10 s.Add(self.first_name, 1, wx.EXPAND)11 s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)12 s.Add(self.family_name, 1, wx.EXPAND)13 self.SetSizer(s)

6.8. Validatori: prima parte. 105

Page 114: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

14 s.Fit(self)15

16 class MyTopFrame(wx.Frame):17 def __init__(self, *a, **k):18 wx.Frame.__init__(self, *a, **k)19 p = wx.Panel(self)20 self.your_name = YourNamePanel(p)21 validate = wx.Button(p, -1, 'valida')22 validate.Bind(wx.EVT_BUTTON, self.on_validate)23

24 s = wx.BoxSizer(wx.VERTICAL)25 s.Add(self.your_name, 1, wx.EXPAND)26 s.Add(validate, 0, wx.EXPAND|wx.ALL, 5)27 p.SetSizer(s)28

29 def on_validate(self, evt):30 ret = self.your_name.Validate()31 if ret == False:32 wx.MessageBox('Non valido')33

34 if __name__ == '__main__':35 app = wx.App(False)36 MyTopFrame(None, size=(200, 200)).Show()37 app.MainLoop()

Come si vede (righe 4 e 5) due caselle di testo sono legate al nostro validatore. Se preferite, potete testare anche l’altrovalidatore. 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 utileanche per dimostrare la validazione “a cascata”: quando chiamiamo Validate sul panel (riga 33), in effetti vengonovalidati tutti i widget figli del panel (purché abbiano un validatore associato, naturalmente). Validate chiamato sulpanel restituisce True solo se tutti i figli passano la validazione, False altrimenti.

Quando fallisce una validazione a cascata.

Nel caso di validazione a cascata, abbiamo però un problema aggiuntivo: il processo di validazione si ferma nonappena uno dei test fallisce, ma il valore restituito False non ci dice nulla su quale widget esattamente non hasuperato 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). Peresempio, 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

106 Chapter 6. Appunti wxPython - livello intermedio

Page 115: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

else:return True

Questo però non è ancora sufficiente: se più caselle di testo hanno lo stesso validatore, talvolta si vuole sapere esat-tamente quale non funziona (in questo caso forse è banale per l’utente capire dove non c’è testo, ma pensate al casogenerale). Possiamo fare in molti modi, per esempio modificando anche il colore del widget incriminato:

1 class NotEmptyValidator(wx.PyValidator):2 #Clone, TransferToWindow, TransferFromWindow... bla bla3

4 def Validate(self, ctl):5 win = self.GetWindow()6 val = win.GetValue().strip()7 if val == '':8 win.SetBackgroundColour('yellow')9 win.Refresh() # necessario...

10 wx.MessageBox('Bisogna inserire del testo')11 return False12 else:13 # assicuriamoci di impostare il colore normale14 win.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))15 win.Refresh()16 return True

Notate che, facendo così, ci fidiamo che il nostro widget abbia un’interfaccia SetBackgroundColour. Questo perun 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 significa-tivo a ogni widget a cui attribuiamo il validatore. Nel nostro esempio sarebbe:

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 variefunzioni (abbiamo fatto lo stesso discorso a proposito degli id). Tuttavia, in casi del genere, può essere un modoveloce 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 sistemisulla fiducia è il meno rischioso? Affidarsi a un’interfaccia che potrebbe non esistere (SetBackgroundColour) oa una che sicuramente esiste ma dipende da noi renderla significativa? La risposta sta al vostro stile, e alla dimensionedel vostro progetto. Nelle situazioni più semplici, non dovete preoccuparvi in nessun caso. Se però iniziate a scrivere

6.8. Validatori: prima parte. 107

Page 116: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

validatori “general purpose” e non sapete in anticipo a quali widget potrebbero essere assegnati, dovete muovervi conpiù cautela.

La validazione ricorsiva.

La validazione a cascata si limita ai soli figli diretti, ma è possibile fare in modo che venga applicata ricorsivamenteanche 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, proviamoa 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 finalmentele 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.

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.

La validazione automatica dei dialoghi.

Fin qui ci siamo limitati a chiamare Validate manualmente, per effettuare la validazione. L’unico automatismopossibile è che, chiamandolo su un panel, si possono validare a cascata tutti i figli diretti (eventualmente anche i nipotietc., usando la validazione ricorsiva).

I dialoghi, tuttavia, hanno una marcia in più. E’ possible validare automaticamente un dialogo, quando è dotato diun pulsante con id predefinito wx.ID_OK. In questo caso, quando l’utente fa clic sul pulsante wx.ID_OK, il dialogochiama automaticamente Validate su se stesso, prima di chiudersi. Se i widget contenuti nel dialogo hanno deivalidatori assegnati, entreranno in funzione.

Abbiamo già parlato di questa feature dei dialoghi quando ci siamo occupati degli Id in wxPython: la sezione relativacontiene 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 dialogocon validazione automatica:

1 class NotEmptyValidator(wx.PyValidator):2 def Clone(self): return NotEmptyValidator()3 def TransferToWindow(self): return True4 def TransferFromWindow(self): return True5

6 def Validate(self, ctl):

108 Chapter 6. Appunti wxPython - livello intermedio

Page 117: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

7 win = self.GetWindow()8 val = win.GetValue().strip()9 if val == '':

10 msg = '%s: manca del testo!' % win.GetName()11 wx.MessageBox(msg)12 return False13 else:14 return True15

16 class NotInBadListValidator(wx.PyValidator):17 def __init__(self, badlist):18 wx.PyValidator.__init__(self)19 self._badlist=badlist20

21 def Clone(self): return NotInBadListValidator(self._badlist)22 def TransferToWindow(self): return True23 def TransferFromWindow(self): return True24

25 def Validate(self, ctl):26 win = self.GetWindow()27 val = win.GetValue().strip()28 if val in self._badlist:29 msg = '%s: non valido!' % win.GetName()30 wx.MessageBox(msg)31 return False32 else:33 return True34

35 class NameDialog(wx.Dialog):36 def __init__(self, *a, **k):37 wx.Dialog.__init__(self, *a, **k)38 ugly_names = ('Cunegonda', 'Dagoberto', 'Emerenzio', 'Pancrazia')39 self.first_name = wx.TextCtrl(self, name='Nome',40 validator=NotInBadListValidator(ugly_names))41 self.family_name = wx.TextCtrl(self, name='Cognome',42 validator=NotEmptyValidator())43 validate = wx.Button(self, wx.ID_OK, 'valida')44 cancel = wx.Button(self, wx.ID_CANCEL, 'annulla')45

46 s = wx.FlexGridSizer(2, 2, 5, 5)47 s.AddGrowableCol(1)48 s.Add(wx.StaticText(self, -1, 'nome:'), 0, wx.ALIGN_CENTER_VERTICAL)49 s.Add(self.first_name, 1, wx.EXPAND)50 s.Add(wx.StaticText(self, -1, 'cognome:'), 0, wx.ALIGN_CENTER_VERTICAL)51 s.Add(self.family_name, 1, wx.EXPAND)52

53 s1 = wx.BoxSizer()54 s1.Add(validate, 1, wx.EXPAND|wx.ALL, 5)55 s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)56

57 s2 = wx.BoxSizer(wx.VERTICAL)58 s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)59 s2.Add(s1, 0, wx.EXPAND)60 self.SetSizer(s2)61 s2.Fit(self)62

63 class MyTopFrame(wx.Frame):64 def __init__(self, *a, **k):

6.8. Validatori: prima parte. 109

Page 118: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

65 wx.Frame.__init__(self, *a, **k)66 b = wx.Button(self, -1, 'mostra dialogo')67 b.Bind(wx.EVT_BUTTON, self.on_clic)68

69 def on_clic(self, evt):70 dlg = NameDialog(self)71 ret = dlg.ShowModal()72 if ret == wx.ID_OK:73 print 'confermato'74 else:75 print 'annullato'76 dlg.Destroy()77

78

79 app = wx.App(False)80 MyTopFrame(None, size=(200, 200)).Show()81 app.MainLoop()

Alcune considerazioni preliminari. Ho scelto la tecnica di assegnare un name a ciascun widget (righe 39 e 41), edi usare l’interfaccia GetName nei validatori per distinguerli (righe 10, 29). Il panel YourNamePanel è andatovia, e invece i widget da validare sono stati inseriti direttamente nel dialogo NameDialog. Questo perché un di-alogo 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 chiamandoValidate sul dialogo stesso). Infine, ho aggiunto un frame top-level MyTopFrame solo per esemplificare il mododi 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 chiudereil dialogo, ma quello contrassegnato con wx.ID_OK, prima, esegue la validazione automatica. Tutti i widget neldialogo 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 hasuccesso (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 ShowModalrestituisce il codice corrispondente al pulsante premuto. Se adesso il codice è wx.ID_OK, si può stare sicuri che idati sono validi. Attenzione però: in caso di codice wx.ID_CANCEL, la validazione non è avvenuta e i dati non sonosicuri.

Questo è importante: la validazione avviene solo in caso di wx.ID_OK. Se si desidera che i widget siano validatisempre, qualunque pulsante sia stato premuto, allora bisogna tornare alla validazione manuale: collegare i pulsanti aun evento, e chiamare Validate nel callback relativo.

Todo

una pagina sulla validazione “in tempo reale” (avanzata? un’aggiunta a questa?)

Consigli sulla validazione.

Composizione di validatori.

A una prima impressione, i validatori sembrano oggetti limitati: per esempio, non possono essere combinati tra loroper eseguire diversi test su un unico widget. Non è possibile chiamare diversi validatori uno dopo l’altro sullo stesso

110 Chapter 6. Appunti wxPython - livello intermedio

Page 119: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

widget. Così, ogni validatore deve avere, nel suo metodo Validate, tutti i test che servono per un dato widget inuna 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 validatorisiano il posto in cui scrivere effettivamente i test di validazione. Dovrebbero essere invece solo il punto di raccordofinale tra la vostra suite di test il widget che dovete validare. Il codice effettivamente contenuto in Validate dovrebbeessere 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 Trueelse:

# informo l'utente che la validazione e' fallitareturn False

Così posso scrivere separatamente i vari test1, test2, etc. in modo “atomico” e generale, e poi combinarli tra loroa seconda dei casi (anche l’ordine di esecuzione si può naturalmente controllare). Così si può arrivare, nella peggioredelle ipotesi a dover scrivere un breve validatore per ciascun widget da validare: ogni validatore rappresenta una catenadi test da eseguire.

Ma volendo si può fare di meglio, e scrivere un validatore “general purpose” con un numero variabile di test passaticome 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 Trueelse:

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 esigenzedella vostra applicazione.

Validazione a cascata.

Sulla validazione a cascata, bisogna dire che è una grande comodità, tuttavia introduce dei limiti. Prima di tutto, lavalidazione 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 nelcampo successivo. Se volete che tutti i widget siano validati comunque, non c’è niente da fare, dovete rinunciare allavalidazione 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:

6.8. Validatori: prima parte. 111

Page 120: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 tornarecomodi 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 delcontesto (per esempio, del valore di altri widget). Ora, naturalmente il “contesto” può essere calcolato e passato “amano” 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 widgetretcode = widget.Validate(context)

Questo ovviamente rende impossibile ogni tipo di validazione automatica, ma abbiamo visto che con Python in generenon è un problema.

Ma c’è di più: sempre grazie alla flessibilità di Python, possiamo anche far calcolare il contesto dinamicamente alvalidatore stesso. Possiamo spingerci a cose un po’ temerarie come questo esempio, dove un validatore ammette cheuna 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 Falseif context_val != '' and val != '': return Falsereturn True

E non si deve usare così naturalmente:

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 senzaproblemi in questo modo:

112 Chapter 6. Appunti wxPython - livello intermedio

Page 121: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 delvalidatore, 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 “maskedcontrols” (cercateli sulla demo), una famiglia di widget con un sistema di validazione interno, separato. Quandoun masked control non è valido, produce un suo comportamento di default (per esempio si colora di giallo): masiccome non ha un validatore vero e proprio attaccato, è difficile integrare questo suo comportamento in un processodi validazione a cascata, per esempio.

Naturalmente si può argomentare che questo è colpa dei masked controls, e non dei validatori (che sono arrivati benprima). In ogni caso i masked controls sono utili da usare, ed è spiacevole dover gestire due flussi di validazioneseparati.

Problemi con i controlli limitati.

Una situazione analoga è quella che capita con i numerosi widget che, in wxPython, hanno la possibilità di limitareautomaticamente i valori immessi. Per esempio, un wx.SpinCtrl può impostare un massimo e un minimo. Unwx.ListBox o un wx.ComboBox si caricano con una lista di valori tra cui scegliere, e così via. In questi casila validazione, in un certo senso, è preventiva: una volta che il widget è mostrato, l’utente non può che inserire dativalidi.

Non è detto che i validatori siano completamente fuori gioco, neppure in questo caso. Potete lasciare che sia un valida-tore a caricare i dati in un wx.ComboBox, o impostare i limiti di un wx.SpinCtrl: è la funzione di trasferimentodati 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 widgetsono 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 ap-plicata alle finestre top-level che raggruppano (in vari panel) diverse macro-aree della vostra applicazione. Quandochiamate Validate sul frame perché volete validare un certo settore, contemporaneamente validate anche tutti glialtri. Nella migliore delle ipotesi è una perdita di tempo; nella peggiore un bel guaio, se in quel momento gli altrisettori sono in uno stato provvisoriamente inconsistente.

La cosa migliore è affidarsi al principio “ogni area, un panel” e validare i singoli panel, facendo affidamento sulfatto che i loro figli diretti saranno i widget che davvero vi serve validare. Occasionalmente, quando uno di questipanel-area ha una gerarchia più complessa (contiene altri panel, che contengono i widget), allora potete settare wx.WX_EX_VALIDATE_RECURSIVELY solo per loro.

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, eautomatica. Basta fare clic su wx.ID_OK, e ottieni gratis la validazione di tutto quanto. D’altra parte però con un ciclo

6.8. Validatori: prima parte. 113

Page 122: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

for in Python, anche la validazione manuale è molto agevole, e consente inoltre di personalizzare più accuratamenteche 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: ivalidatori 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 diquesta 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 usaticon successo anche in scenari più difficili: ma se non riuscite ad armonizzarli nel vostro framework di validazionecomplessivo, potete tranquillamente rinunciarvi.

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 immessidall’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.

Trasferimento dati nei dialoghi con validazione automatica.

Il modo più semplice per illustrare questa possibilità, è vederla applicata nel suo ambiente naturale, dove i validatoridanno 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 trasformarloin una applicazione vera e propria, anche se piccola. Il codice è più lungo, ma vale la pena di seguirlo per vedere comei 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 suun nome della lista, il dialogo si apre in modalità “modifica”, e quando cliccate sul pulsante “nuovo”, lo stesso dialogovi consente di aggiungere un nuovo elemento.

1 class NotEmptyValidator(wx.PyValidator):2 def __init__(self, person, key):3 wx.PyValidator.__init__(self)4 self._person = person5 self._key = key6

7 def Clone(self): return NotEmptyValidator(self._person, self._key)8

9 def TransferToWindow(self):10 win = self.GetWindow()11 win.SetValue(self._person.get(self._key, ''))12 return True13

14 def TransferFromWindow(self):15 win = self.GetWindow()

114 Chapter 6. Appunti wxPython - livello intermedio

Page 123: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

16 self._person[self._key] = win.GetValue()17 return True18

19 def Validate(self, ctl):20 win = self.GetWindow()21 val = win.GetValue().strip()22 if val == '':23 msg = '%s: non deve essere vuoto.' % win.GetName()24 wx.MessageBox(msg)25 return False26 else:27 return True28

29

30 class AgeValidator(wx.PyValidator):31 def __init__(self, person):32 wx.PyValidator.__init__(self)33 self._person = person34

35 def Clone(self): return AgeValidator(self._person)36 def Validate(self, win): return True # non facciamo validazione37

38 def TransferToWindow(self):39 win = self.GetWindow()40 win.SetRange(0, 100) # minimo e massimo41 win.SetValue(self._person.get('eta', 20)) # valore di default42 return True43

44 def TransferFromWindow(self):45 win = self.GetWindow()46 self._person['eta'] = win.GetValue()47 return True48

49

50 class PersonDialog(wx.Dialog):51 def __init__(self, *a, **k):52 wx.Dialog.__init__(self, *a, **k)53 person = self.GetParent().current_person54 self.first_name = wx.TextCtrl(self, name='Nome',55 validator=NotEmptyValidator(person, 'nome'))56 self.family_name = wx.TextCtrl(self, name='Cognome',57 validator=NotEmptyValidator(person, 'cognome'))58 self.year = wx.SpinCtrl(self, name="Eta'")59 self.year.SetValidator(AgeValidator(person))60 ok = wx.Button(self, wx.ID_OK, 'conferma')61 cancel = wx.Button(self, wx.ID_CANCEL, 'annulla')62

63 s = wx.FlexGridSizer(3, 2, 5, 5)64 s.AddGrowableCol(1)65 for ctl, lab in ((self.first_name, 'nome:'),66 (self.family_name, 'cognome:'), (self.year, "eta':")):67 s.Add(wx.StaticText(self, -1, lab), 0, wx.ALIGN_CENTER_VERTICAL)68 s.Add(ctl, 1, wx.EXPAND)69

70 s1 = wx.BoxSizer()71 s1.Add(ok, 1, wx.EXPAND|wx.ALL, 5)72 s1.Add(cancel, 1, wx.EXPAND|wx.ALL, 5)73

6.9. Validatori: seconda parte. 115

Page 124: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

74 s2 = wx.BoxSizer(wx.VERTICAL)75 s2.Add(s, 1, wx.EXPAND|wx.ALL, 5)76 s2.Add(s1, 0, wx.EXPAND)77 self.SetSizer(s2)78 s2.Fit(self)79

80

81 class TopFrame(wx.Frame):82 def __init__(self, *a, **k):83 wx.Frame.__init__(self, *a, **k)84 p = wx.Panel(self)85 self.people = wx.ListBox(p)86 self.people.Bind(wx.EVT_LISTBOX_DCLICK, self.on_view_person)87 new = wx.Button(p, -1, 'nuovo')88 new.Bind(wx.EVT_BUTTON, self.on_new)89

90 s = wx.BoxSizer(wx.VERTICAL)91 s.Add(self.people, 1, wx.EXPAND|wx.ALL, 5)92 s.Add(new, 0, wx.EXPAND|wx.ALL, 5)93 p.SetSizer(s)94

95 self.current_person = {}96 self.reload_people_list()97

98 def reload_people_list(self):99 self.people.Clear()

100 people = wx.GetApp().PEOPLE101 for p in people:102 s = '%i: %s %s %i' % (p, people[p]['nome'],103 people[p]['cognome'], people[p]['eta'])104 self.people.Append(s)105 self.current_person = {}106

107 def on_view_person(self, evt):108 app = wx.GetApp()109 selected = self.people.GetString(evt.GetSelection())110 # questo e' molto brutto, ma e' per fare in fretta....111 id = int(selected.split(':')[0])112 self.current_person = app.PEOPLE[id]113 dlg = PersonDialog(self, title='Vedi persona')114 ret = dlg.ShowModal()115 if ret == wx.ID_OK:116 app.PEOPLE[id] = self.current_person117 self.reload_people_list()118 dlg.Destroy()119

120 def on_new(self, evt):121 self.current_person = {}122 dlg = PersonDialog(self, title='Nuova persona')123 ret = dlg.ShowModal()124 if ret == wx.ID_OK:125 app = wx.GetApp()126 id = max(app.PEOPLE.keys()) + 1127 app.PEOPLE[id] = self.current_person128 self.reload_people_list()129 dlg.Destroy()130

131

116 Chapter 6. Appunti wxPython - livello intermedio

Page 125: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

132 class App(wx.App):133 def OnInit(self):134 self.PEOPLE = {1: {'nome':'Mario', 'cognome':'Rossi', 'eta':37},135 2: {'nome':'Giuseppe', 'cognome':'Bianchi', 'eta':25},136 3: {'nome':'Andrea', 'cognome':'Verdi', 'eta':42}}137 TopFrame(None, title='Persone', size=(300, 300)).Show()138 return True139

140

141 app = App(False)142 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 riga134. 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 aggiungernedi 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 alformato 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 nelmondo reale, in effetti.

Dopo di che, entra in gioco il PersonDialog che è il cuore della nostra applicazione, e che esaminiamo nel dettagliotra poco. Iniziamo però a notare che PersonDialog, in qualche modo per ora misterioso, ha la facoltà di modificareil valore di self.current_person. E quindi, se l’utente ha chiuso il dialogo con wx.ID_OK, a noi non resta cheaggiornare il database con il self.current_person modificato, e quindi chiamare reload_people_listper 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 aggiornareil database e rinfrescare la lista. L’unica piccola variante è che questa volta dobbiamo calcolarci un nuovo id per ildatabase (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 lagui, ma non si vede nessun codice per gestire tutte le operazioni di cui è incaricato. Parte della magia, ormai, dovrebbeessere chiara: alle righe 60 e 61 creiamo due pulsanti con Id predefiniti, che si occupano di chiudere il dialogo, e (ilpulsante “conferma”) di validare i dati. Se non vi è chiaro perché, rileggete la prima parte di questa analisi, e anchela 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 quindidobbiamo assegnare il validatore in un secondo momento, usando SetValidator.

E finalmente esaminiamo i due validatori che abbiamo scritto. NotEmptyValidator si applica alle due caselle ditesto, 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 TransferToWindowviene invocato automaticamente quando il dialogo si apre. TransferFromWindow invece viene invocato quando

6.9. Validatori: seconda parte. 117

Page 126: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

il dialogo si chiude, ma solo in corrispondenza della pressione di wx.ID_OK (e naturalmente, solo a validazioneavvenuta).

Per poter trasferire i dati, il validatore deve avere qualche conoscenza del nostro “model”. Ecco perché passiamo comeargomenti person e key. Il primo è collagato alla self.current_person della nostra finestra-madre (vedi riga53), il secondo è il “campo” esatto a cui il widget è collegato (vedi righe 55 e 57). In questo modo il validatore conoscecon precisione il valore su cui deve lavorare.

All’apertura del dialogo, TransferToWindow si occupa di prelevare il valore alle coordinate “person / key” e diinserirlo nel widget (riga 11). Alla chiusura, TransferToWindow fa il lavoro opposto, prelevando il nuovo valoree 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 hapremuto 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 inconsistenzatra 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 occupaimmediatamente 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 direttadel database (e non solo della current_person) potrebbe ricavare i valori che gli servono da questo, e modificarliquando occorre. Nel nostro esempio, sarebbe naturalmente possibile. Ma nel mondo reale, c’è una ragione tecnica pernon farlo: se ogni validatore fa una richiesta separata per ottenere il suo pezzetto di informazione, potete scommettereche l’amministratore del database avrà qualche obiezione (nel nostro esempio, sarebbero già 3 query in entrata e 3 inuscita). 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 purein modo ancora embrionale, la separazione delle diverse funzioni, fino ad arrivare a una sorta di “model-controller-view”. Nel nostro esempio, il model è ovviamente il database PEOPLE. La view è la lista mostrata nella finestamadre. 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 eventiwxPython, 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 validatoreseparato: da un lato non avrebbe senso controllare se il campo è vuoto (un wx.SpinCtrl non è mai vuoto), edall’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 solodel trasferimento dei dati, in maniera del tutto analoga all’altro validatore. Osservate però che, al momento di aprireil dialogo, si occupa anche di impostare minimo, massimo e valore di default per il wx.SpinCtrl affidato alla suasorveglianza (righe 40-41). Nel nostro piccolo esempio questo avviene in un brutto modo “statico”, ma non è difficilepassare questi valori dinamicamente come parametri. Questo è un esempio di come si possono usare i validatori ancheper lavorare con i controlli limitati, affrontando un problema che avevamo visto nella prima parte.

Trasferimento dati negli altri casi.

La capacità dei validatori di trasferire dati può essere usata anche in un contesto di validazione “manuale”. Se applicateun validatore a un widget, TransferToWindow sarà invocato automaticamente ogni volta che il widget vienemostrato. 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.

118 Chapter 6. Appunti wxPython - livello intermedio

Page 127: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 validatoripotete 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 compren-sione di come funzionano davvero i validatori. Tuttavia, anche con le migliori intenzioni, talvota i validatori possonoessere semplicemente scomodi da usare. Va detto che specialmente nel mondo Python, le capacità di automatizzazionedei validatori brillano di meno: spesso basta un ciclo for per applicare una routine di validazione a tutti i widget dellazona, senza bisogno di ulteriori sovrastrutture.

Tuttavia un utilizzo intelligente dei validatori indirizza verso la separazione tra “model”, “controller” e “view”, equindi, se non li volete usare, dovreste comunque fare attenzione che il sistema con cui li rimpiazzate mantenga questovantaggio.

Ancora una osservazione: i validatori sono uno dei punti in cui wxPython non si limita a essere un puro “gui frame-work”, ma sconfina nel campo di un “application framework”. Non c’è nulla di male in questo, finché ne sieteconsapevoli. Se, per esempio, volete usare wxPython come front-end grafico “plugin” liberamente sostituibile conaltre interfacce, allora probabilmente non vi conviene legare ai validatori il vostro codice di controllo.

6.9. Validatori: seconda parte. 119

Page 128: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

120 Chapter 6. Appunti wxPython - livello intermedio

Page 129: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 7

Appunti wxPython - livello avanzato

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 è natural-mente 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 obi-ettivamente più pratici ed eleganti, e negli anni sono stati sviluppati moltissimo. I constraints invece sono ufficialmentedeprecati da oltre 10 anni, e nessuno li usa più. Tuttavia non sono mai stati rimossi completamente da wxWidgets (eanche wxPython continua a supportarli).

Anche se i constraints sono più macchinosi da usare rispetto ai sizer, ci sono alcuni casi particolari in cui potrebberoancora tornare utili. In generale dovreste sempre usare i sizer: se un problema di layout vi sembra insormontabile coni sizer, nove su dieci vuol dire che avete ancora difficoltà a padroneggiarli. Tuttavia, occasionalmente potreste davverotrovarvi 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 diwxWidgets spiega tutti i dettagli. In ogni caso, dovreste usare i contraints solo come ultima risorsa, e solo nel modopiù semplice: se vi trovate a passare troppo tempo a studiare gli oscuri dettagli dei constraints, probabilmente statesbagliando approccio e dovreste tornare ai sizer.

wx.IndividualLayoutConstraint e wx.LayoutConstraints.

Proprio come wx.Sizer e le sue sottoclassi incapsulano l’algoritmo di calcolo di un sizer, wx.IndividualLayoutConstraint definisce un constraint da applicare a un widget.

Un constraint è un vincolo che si impone a una caratteristica geometrica di un widget: si possono imporre fino a ottodiversi contraints, relativi a

• bordo destro,

• bordo sinistro,

• bordo superiore,

121

Page 130: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• bordo inferiore,

• altezza,

• larghezza,

• coordinata x del centro,

• coordinata y del centro.

I vincoli si specificano in relazione ad altri widget, che possono essere i container genitori (un wx.Panel, peresempio), o i widget “fratelli” nello stesso container. Per esempio è possibile vincolare il bordo destro di un widget arestare a 50 pixel dal bordo sinistro del vicino; si possono specificare anche vincoli come percentuali di altri vincoli, ecosì via.

In realtà non c’è quasi mai ragione di istanziare direttamente un singolo constraint e poi applicarlo a un widget: sipreferisce usare la più comoda classe wx.LayoutConstraints, che in pratica è un contenitore che permette dispecificare 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, persemplicità):

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 abbiamofattorizzato le regole dei constraint in una funzione separata, cosa che ci ha consentito di risparmiare un bel po’ dispazio. Chiaramente, nel caso generale, avremmo dovuto specificare dei constraint differenti per ciascun widget, edopo un po’ di questa ginnastica capirete perché i sizer sono più comodi da usare.

122 Chapter 7. Appunti wxPython - livello avanzato

Page 131: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

La sintassi di wx.LayoutConstraints è articolata, ma tutto sommato facile da capire. Ciascuno degli ottoconstraint 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”;

• .Above, .Below, .LeftOf, .RightOf, ovvero sopra, sotto, a destra o a sinistra di un riferimento (più uneventuale 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 glialtri 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 widgeta cui verranno assegnati questi constraint dovrà avere lo stesso valore (.SameAs) del bordo sinistro (wx.Left) delwidget 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 effettivodi 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 sicuriche 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 assegnaredei 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.

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 layoutcon 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)

7.1. I constraints: un modo alternativo di organizzare il layout. 123

Page 132: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

b = wx.Button(panel_red, -1, 'clic', pos=(20, 20))b.Bind(wx.EVT_BUTTON, self.on_clic)self.panel_base = panel_baseself.panel_red = panel_red

def on_clic(self, evt):# dimostriamo come cambiare i margini di panel_redlc = wx.LayoutConstraints()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 marginidifferenti per il nostro panel rosso. Anche modificare i margini successivamente è banale, come dimostriamo nelcallback del pulsante: basta ricreare e ri-assegnare un wx.LayoutConstraints (qui non ci siamo preoccupatitroppo 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 chiamarewx.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 sarebberoproblemi 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 equivalentepotrebbe 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 3x3s.AddGrowableCol(1)s.AddGrowableRow(1)s.Add((40, 20)) # gli spacer d'angolo impongono i marginis.Add((-1, -1))s.Add((-1, -1))s.Add((-1, -1))s.Add(panel_red, 1, wx.EXPAND) # al centro, il panel rossos.Add((-1, -1))s.Add((-1, -1))s.Add((-1, -1))s.Add((80, 60)) # gli spacer d'angolo impongono i marginipanel_base.SetSizer(s)

Qui per fortuna ci siamo fatti aiutare da un wx.FlexGridSizer con le sue proprietà AddGrowableCol eAddGrowableRow, perché lo stesso layout realizzato esclusivamente con i wx.BoxSizer sarebbe stato più com-plicato (anche se invece un wx.GridBagSizer, a dire il vero, ci avrebbe risparmiato un po’ di linee di codice: ma

124 Chapter 7. Appunti wxPython - livello avanzato

Page 133: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

lo strumento migliore varia da progetto a progetto). Si nota comunque una buona dose di artificiosità per realizzare unlayout tutto sommato molto semplice.

Anche cambiare i margini a runtime, con i sizer è più complicato, e questo perfino nell’ipotesi che tutti i marginisiano uguali. Infatti i margini sono determinati da costanti e flag del metodo wx.Sizer.Add, in costrutti deltipo 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 restitu-ito 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ù radicale staccarel’elemento dal sizer senza distruggerlo (con wx.Sizer.Detach) e poi re-inserirlo nello stesso posto con parametridiversi.

Questo sono comunque scenari piuttosto rari nella pratica di tutti i giorni. Di solito non capita di dover modificaremargini 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 sizersenza particolari problemi. Nell’esempio qui sopra, per brevità abbiamo inserito un pulsante all’interno del panelrosso con il posizionamento assoluto (anche se sappiamo bene che non bisognerebbe mai farlo). Avremmo invecepotuto creare un sizer assegnato al panel rosso, e usarlo come di consueto per disporre il pulsante e altri widget.

Gli eventi: altre tecniche.

Abbiamo già dedicato due pagine agli eventi. In questa sezione raccogliamo alcune note aggiuntive: non si tratta diconcetti più difficili dei precedenti, ma semplicemente di tecniche di utilizzo meno frequente.

La lettura di questa pagina presuppone la conoscenza delle due precedenti.

Lambda binding.

Abbiamo visto che un callback può, di regola, accettare un solo argomento: un riferimento all’evento che è statointercettato. Questa è una limitazione piuttosto fastidiosa del framework c++ sottostante. In realtà spesso l’oggetto-evento porta con sé molte informazioni utili (GetEventObject restituisce un riferimento all’oggetto originario, peresempio), 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 stessapagina) 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 suapropagazione, 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 semprecome “uno” negli argomenti accettati da Bind, ma dentro possiamo metterci quello che vogliamo. Lo schema è moltofacile 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 anchealtri a piacere. In questo modo possiamo passare a callback più informazioni di quelle contenute in evt.

7.2. Gli eventi: altre tecniche. 125

Page 134: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Grazie alla consueta flessibilità di Python, possiamo passare come argomenti aggiuntivi sia valori statici (il testo diuna label, per esempio), sia riferimenti a funzioni da eseguire nel callback per ottenere valori dinamici.

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 maggioreconsuetudine con il lambda-binding.

Anche in questo caso, non c’è molto da spiegare:

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 (ilconsueto riferimento all’evento), e specifica quelli successivi.

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 ilcontrario: 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 widgeteventManager.Register(callback, wx.EVT_*, widget)

# per de-registrare un callbackeventManager.DeregisterListener(callback)

# per de-registrare tutti i callback in ascolto degli eventi di widgeteventManager.DeregisterWindow(widget)

Come si può intuire dall’interfaccia, Event Manager utilizza il design pattern noto come Publisher/Subscriber. Ineffetti, 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 generesufficiente: il punto di forza di Event Manager (collegamento molti-a-molti tra sorgenti e ascoltatori) è in genere pocoutile 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. Virimandiamo quindi alla pagina di pub/sub per un esame più approfondito di Event Manager e di quando convieneusarlo.

126 Chapter 7. Appunti wxPython - livello avanzato

Page 135: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 dazero, 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 questomodo 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 aicallback. Esaminiamo questi passaggi, prendendo spunto da un esempio concreto: vogliamo creare un nuovo “widget”che permetta di selezionare i trimestri di un anno.

Note: L’esempio che segue è una semplificazione di un widget più elaborato che ho scritto per un altro progetto. Laversione 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 essereintercettati. Noi vorremmo però presentare all’esterno un’interfaccia più coerente e pulita: il nostro widget dovrebbeemettere 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 nuovobinder 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)

7.2. Gli eventi: altre tecniche. 127

Page 136: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Come si vede, la cosa più difficile è la scelta del nome. In genere per l’event type si preferisce uno schema del tipomyEVT_*, per mimare gli event type standard wx.wxEVT_*. Sempre per consuetudine, il binder ha lo stesso nomedell’event type, tolto il prefisso my.

wx.NewEventType() restituisce semplicemente un nuovo identificatore non ancora usato per gli event type pre-definiti. Ne abbiamo bisogno subito per definire il binder, e poi ne avremo ancora bisogno per istanziare l’oggetto-evento, come vedremo.

Il nostro binder dovrà essere una istanza di wx.PyEventBinder. Gli argomenti richiesti sono due: il primo è l’eventtype appena creato, e il secondo indica quanti Id ci si aspetta di ricevere al momento di creare l’evento. Questo sembrastrano 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.

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 modoancora 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 dicoche 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_periodself.mod_year = mod_year

Come si vede, wx.PyCommandEvent accetta due argomenti: evtType è l’event type, e id è l’Id dell’oggetto dacui parte l’evento. Gli altri due argomenti sono una nostra aggiunta. Avremmo anche potuto aggiungere dei getter esetter per queste due proprietà, naturalmente.

Abbiamo lasciata “aperta” la possibilità di settare il parametro evtType al momento della creazione dell’istanza: ingenere è quello che si preferisce fare, perché si potrebbero creare diversi event type per lo stesso evento. Tuttavia, sesappiamo che esisterà solo un event type possibile per il nostro evento, possiamo anche impostarlo direttamente nellanostra classe:

class PeriodEvent(wx.PyCommandEvent): # versione alternativadef __init__(self, id, mod_period=False, mod_year=False):

wx.PyCommandEvent.__init__(self, myEVT_PERIOD_MODIFIED, id)self.mod_period = mod_periodself.mod_year = mod_year

128 Chapter 7. Appunti wxPython - livello avanzato

Page 137: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Lanciare l’evento personalizzato.

Adesso si tratta di scegliere il momento giusto per lanciare dal nostro widget l’evento che abbiamo scritto. Siccomevogliamo che l’evento parta nel momento in cui l’utente agisce su uno dei due elementi del widget, colleghiamonormalmente 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 recuperarel’elemento che è stato modificato. La parte più interessante è la creazione dell’istanza di PeriodEvent: come vistosopra, richiede due argomenti “obbligatori” (l’event type e l’Id del widget che lo sta generando), ai quali aggiungiamoi nostri due argomenti “personalizzati”.

E’ anche utile impostare alcune proprietà dell’evento appena creato, prima di emetterlo. Nel nostro esempio impos-tiamo SetEventObject, per permettere al futuro callback che lo intercetterà di usare GetEventObject se lodesidera.

Quindi, dobbiamo emettere l’evento. Il modo più consueto è rivolgersi all’handler dello stesso widget che lo stagenerando (self.GetEventHandler()) e chiedergli di processare immediatamente l’evento invocando diretta-mente ProcessEvent.

Si noti anche che, siccome nel callback non chiamiamo Skip, i due eventi originari smettono di propagarsi, comedesideriamo: 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 nostrocaso, 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 nes-suna differenza, ma supponiamo invece di dover chiamare Skip nel callback, per esempio per permettere la ricercadi 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 chevogliamo. Invece ProcessEvent infilerebbe il nostro evento prima di terminare di processare quelli originali. Ilrisultato è 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 introduceuna 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)

7.2. Gli eventi: altre tecniche. 129

Page 138: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

self.mod_period = mod_periodself.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(),

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

130 Chapter 7. Appunti wxPython - livello avanzato

Page 139: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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, ...) comefarebbe 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 disposizioneuna comoda scorciatoia per scavalvare le altre operazioni di routine. Tutto quello che occorre fare è:

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 eventcorretto (che quindi non avete bisogno di conoscere). Quando volete creare l’istanza dell’evento, dovete solo passareun 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 nessunmetodo/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 comeprima.

Oltre a wx.lib.newevent.NewCommandEvent() esiste anche wx.lib.newevent.NewEvent() percreare un evento “non command”.

Gli eventi: altre tecniche (seconda parte).

Dopo aver dedicato due pagine agli eventi, e una pagina ad alcune tecniche più inconsuete, siamo ancora ben lontanidall’aver esaurito l’argomento!

In questa sezione affrontiamo alcuni aspetti ancora più esotici, che molto probabilmente non avrete mai bisogno diutilizzare. 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 questistrumenti, è facile ottenere interfacce che si bloccano, non rispondono, vanno in crash. Siate preparati a uccidere ilprocesso di python responsabile del vostro programma. E verificate periodicamente se non sono rimasti processi dipython ancora in vita. Se qualcosa può andar storto, lo farà.

7.3. Gli eventi: altre tecniche (seconda parte). 131

Page 140: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Filtri.

La possibilità di applicare filtri globali è probabilmente l’aspetto meno documentato e usato di tutto il meccanismodegli 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 cosachiamerà il metodo wx.App.FilterEvent. Nella sua implementazione di default, FilterEvent non fa nulla erestituisce 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, compresiquelli che disegnano l’interfaccia: se non fate bene i vostri conti, vi ritroverete con il programma bloccato e incapacedi rispondere agli eventi.

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 questachiamata 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 sottoclassatowx.App per chiamare SetCallFilterEvent(True) nel suo OnInit: in questo modo ci assicuriamo cheil 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 qualcosadi 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

132 Chapter 7. Appunti wxPython - livello avanzato

Page 141: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

wx.EVT_UPDATE_UI che viene emesso al semplice passaggio del mouse, senza contare il ridimensionamento e lospostamento delle finestre, e anche gli occasionali wx.EVT_IDLE.

Perfino la documentazione di wxWidgets (il framework c++ sottostante) ci consiglia di usare FilterEvent congiudizio, per evitare rallentamenti. In python, dove ogni chiamata di funzione è particolarmente dispendiosa, è davverodifficile consigliare l’uso di questa tecnica. E’ per questo che wxPython introduce SetCallFilterEvent (che nonesiste 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 adisposizione da una apposita classe mix-in, che si chiama EventFilter. In questo modo, costruendo un widgetpersonalizzato che deriva anche da EventFilter è possibile dotarlo di un suo metodo FilterEvent da sovrascri-vere, ed è quindi possibile attivare più filtri contemporaneamente. La wx.App dispone di un suo FilterEvent didefault, perché deriva già “per natura” da EventFilter. In wxPython tutto questo non è supportato: resta solo ilFilterEvent 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, consid-erate questa wx.App:

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 calcolareda quanto tempo è inattivo.

Naturalmente sarebbe possibile ottenere lo stesso effetto collegando con Bind ogni singolo elemento dell’interfacciaa un callback dedicato a fare questo lavoro (o anche, come vedremo tra poco, usando un handler personalizzato). Machiaramente 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 tastipremuti dall’utente... e così via.

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 se-lezionati) diretto a uno specifico widget.

Potete passare al costruttore -1 (ovvero wx.EVT_ANY) per dire “blocca tutti gli eventi”, oppure il typeId di unevento specifico. Se vi serve aggiungere altri eventi da bloccare, potete farlo in seguito chiamando il suo metodoBlock.

Il blocco resta attivo fin quando l’istanza di wx.EventBlocker non viene fisicamente distrutta (in qualunque modopossiate marcare un oggetto per essere reclamato dal garbage collector in python: uscendo dallo “scope” in cui è statadefinita la variabile, o in definitiva anche con del). A quel punto, gli eventi tornano a essere gestiti come di consueto.

7.3. Gli eventi: altre tecniche (seconda parte). 133

Page 142: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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__':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 dis-abilitare 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 codicea seconda delle circostanze, potremmo mettere un po’ di logica in più nel callback. Tuttavia un blocco può aiutarci amantenere il codice più pulito.

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 (ridi-mensionamenti, spostamenti, etc.);

• wx.wxEVT_CATEGORY_USER_INPUT: questi sono gli eventi tipicamente generati dell’utente (pressione ditasti, clic del mouse...);

• wx.wxEVT_CATEGORY_NATIVE_EVENTS: definita come l’unione delle due precedenti (wx.wxEVT_CATEGORY_UI|wx.wxEVT_CATEGORY_USER_INPUT);

• 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 nonsupporta;

134 Chapter 7. Appunti wxPython - livello avanzato

Page 143: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• 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 uncallback:

def mycallback(self, evt):print evt.GetEventCategory()

Se state creando un evento personalizzato e avete bisogno di impostare la sua categoria, potete sovrascrivereGetEventCategory per restituire quello che vi sembra più opportuno.

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 trannei 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 wxWidgetin alcune occasioni, anche queste categorie hanno una funzione interna. Inoltre, naturalmente, potreste usarle perimplementare qualche filtro di vostro molto specializzato... se vi viene in mente un’idea.

Handler personalizzati.

Sappiamo già che wx.EvtHandler è la classe-base dedicata alla gestione degli eventi. E sappiamo anche che tuttala gerarchia dei widget (a partire dalla classe madre wx.Window) deriva da wx.EvtHandler, e di conseguenzatutti 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 stackdi 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 chewxPython mette a disposizione per sovrascrivere i metodi virtuali), e aggiungerli allo stack degli handler di un deter-minato widget. In questa sezione vedremo come fare, ma prima un avvertimento: si tratta di strumenti che wxPythonmette a disposizione “traducendoli” dal framework c++ sottostante, ma che nel mondo python hanno meno utilità prat-ica. Leggete i paragrafi seguenti senza badare troppo all’utilità pratica: vedremo che tutto questo vi potrebbe tornareutile, in certi scenari.

Per cominciare, non c’è nulla di magico in un handler personalizzato: è una semplice sotto-classe di wx.PyEvtHandler. Per esempio:

class MyEvtHandler(wx.PyEvtHandler):def __init__(self):

wx.PyEvtHandler.__init__(self)

7.3. Gli eventi: altre tecniche (seconda parte). 135

Page 144: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 momentoper ricordarsi che, dopo tutto, Bind è un metodo di wx.EvtHandler). Per usarlo, non dobbiamo fare altro cheistanziarlo, 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). Peresempio:

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)

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 delpulsante. 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 normalegestione di un evento da parte di un pulsante. La differenza è che adesso il callback on_clic è definito nella classedell’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 wxWidgetsnon si possono definire callback all’esterno della classe in cui risiedono i widget: quindi scrivere un handler separatoe agganciarlo a un widget è l’unico modo per intervenire “dal di fuori”. In python, ovviamente, questa feature non ciimpressiona più di tanto: le funzioni e i metodi sono “first class object”, e si possono passare a Bind come parametrinormali. In wxPython un callback può essere un metodo di un’altra classe, o una funzione esterna stand-alone, senzaalcuna 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ù “com-ponibile”, “modulare” all’evento.

136 Chapter 7. Appunti wxPython - livello avanzato

Page 145: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Note: questa è una possibilità effettivamente nuova, nel senso che invece non è possibile collegare con Bind diversicallback per lo stesso evento ad un widget. Naturalmente però nessuno vieta di chiamare una serie di funzioni esternein 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 chel’handler predefinito (ovvero il widget stesso) in questo modo resta sempre in fondo allo stack, ed è quindi l’ultimo aoccuparsi 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ò essereimportante, 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 nellostack (e lo restituisce come risultato). Non potete togliere anche l’handler predefinito (cioè il widget stesso). Secercate di farlo, wxPython solleva una wx._core.PyAssertionError. Quindi tenete conto degli handler manmano che li aggiungete, oppure preparatevi a intercettare questa eccezione:

while True: # svuoto completamente lo stacktry: mywidget.PopEventHandler()except wx._core.PyAssertionError: break

PopEventHandler restituisce un riferimento all’handler rimosso: se lo conservate in una variabile potete riutiliz-zarlo in seguito, se volete.

Potete anche manipolare lo stack usando SetNextHandler e SetPreviousHandler. Ricordatevi però chewxPython mantiene una lista doppia di riferimenti alla catena degli handler: di conseguenza, se impostate B comesuccessivo 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, usatepiuttosto handler.Unlink(): questo ripara anche automaticamente la catena.

Potete conoscere il successore di un handler chiamando handler.GetNextHandler() (che restituisce Nonese l’handler è l’ultimo della catena). Analogamente, handler.GetPreviousHandler vi restituisce l’handlerprecedente. Se entrambi questi metodi restituiscono None, vuol dire che l’handler è “sganciato” dalla catena: poteteanche 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 pro-prio 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 unevento personalizzato creato sul momento, ma si può naturalmente usare anche con gli eventi “ordinari”). Questometodo, insieme con AddPendingEvent e QueueEvent, torna utile anche nel caso specifico in cui gli eventipersonalizzati sono creati, lanciati e processati come mezzo di comunicazione tra diversi thread.

Todo

una pagina sui thread.

7.3. Gli eventi: altre tecniche (seconda parte). 137

Page 146: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 hannouna utilità pratica ridotta in wxPython. Potete senz’altro usarli per aumentare la scomposizione e la fattorizzazionedel codice, se volete usare strumenti wxPython (invece delle normali tecniche python). Occasionalmente potrestevoler aggiungere degli handler “plug-in” a runtime (ma di solito potete ottenere lo stesso effetto chiamando Binde Unbind a runtime, o mantenendo un callback fisso e da quello applicando qualche tipo di “strategy pattern” perselezionare 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 ven-gono eseguiti i callback. Quando avete molteplici callback, l’ordine di esecuzione può dipendere da come intercettatel’evento (potete collegarlo al widget, oppure a un suo parent). Se questa incertezza non va bene per quello che dovetefare, allora una buona soluzione è far gestire l’evento da un handler personalizzato, e poi inserire l’handler nello stackdel widget (e ripetere all’occorrenza con altri callback in altri handler). In questo modo avete sempre il controllo dellostack 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 azionesuccessiva.

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 sonoeseguiti i callback:

class MyEvtHandler(wx.PyEvtHandler):def __init__(self, name):

wx.PyEvtHandler.__init__(self)self.name = nameself.Bind(wx.EVT_BUTTON, self.onclic)

def onclic(self, evt):print "clic dall'handler", self.nameevt.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)

138 Chapter 7. Appunti wxPython - livello avanzato

Page 147: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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

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, parliamodi eventi anche nella pagina dedicata ai thread. Ma soprattutto, ci restano ancora molte cose da scoprire sugli eventloop... Ma sarà l’argomento di una pagina separata!

Il loop degli eventi.

Abbiamo già dedicato molte pagine agli eventi: ma non abbiamo ancora mai indagato sul cuore nascosto della nostraapplicazione wxPython: il luogo dove gli eventi arrivano e vengono accoppiati ai loro handler, che a loro volta siincaricheranno 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 modelloa 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 segnalazionein 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

7.4. Il loop degli eventi. 139

Page 148: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

orientamente iniziale, di sperimentare e approfondire molto per conto vostro, e di testare a lungo il codice che scrivete.

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 glieventi”. In realtà, questa definizione confonde main loop e loop degli eventi (“event loop”, detto anche loop deimessaggi, “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 voltasola, all’inizio della vita della vostra applicazione wxPython. Al suo interno, crea e avvia un event loop per gestiregli 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, masenza estenderne le funzionalità. La documentazione di GUIEventLoop rimanda semplicemente a quella diEventLoopBase. Nelle vecchie versioni di wxPython, GUIEventLoop era però chiamata EventLoop: perquesta ragione, vedete ancora moltissimo codice in giro che usa EventLoop. Nessun problema, però: EventLoopesiste 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 diMainLoop farebbe in ogni caso. Anzi, in questo modo avete perso il meccanismo che esce da MainLoop e chi-ude la vostra applicazione quando non ci sono più finestre top level aperte: se provate a far girare una gui qualsiasicon questa MyApp, vedrete che wxPython non termina mai (preparatevi a usare ctrl-c nella shell, o a terminare ilprocesso in qualche modo).

Tuttavia questo è almeno un inizio: abbiamo imparato a creare un event loop, e ad avviarlo con Run. A questoproposito, va detto che Run si prende cura di fare il lavoro al posto vostro, ma è proprio l’opposto di quel che stiamocercando: 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 è chia-mare Dispatch, metodo che attende l’arrivo di un evento, e si occupa di accoppiarlo al suo primo handler. Sic-

140 Chapter 7. Appunti wxPython - livello avanzato

Page 149: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

come Dispatch è bloccante (aspetta fin quando non c’è un evento da gestire), in genere conviene accoppiarlo conPending, che ci dice se ci sono eventi in coda in attesa di essere processati. Quando abbiamo finito di gestire glieventi in coda chiamiamo ProcessIdle, che emette un wx.EVT_IDLE per segnalare che il loop è attualmentedisoccupato (avremmo potuto ottenere lo stesso effetto con la funzione globale wx.WakeUpIdle). Emettere di tantoin tanto un wx.EVT_IDLE è necessario, perché in wxPython ci sono dei gestori di default che intercettano questoevento 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 stiamofacendo tutto “a mano”, alla peggio potremmo chiamare direttamente anche OnExit e/o qualsiasi funzione di cleanupnecessaria, prima di chiudere.

Piuttosto, è il test GetTopWindow() is None che potrebbe essere fragile in certi corner-case. Abbiamo vistomille modi in cui una finestra potrebbe non chiudersi davvero, e altri mille modi in cui si possono manipolare lefinestre top-level. Tuttavia, se mantenete un minimo di organizzazione nel vostro codice, non dovrebbe essere difficilestabilire 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.Exitpossa 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 = Falsereturn 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 voleteuscire, 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 vostrocomputer! Il problema è che wxPython, quando è al comando, si preoccupa di dosare il consumo della vostracpu: ma il nostro while True senza alcuna moderazione finisce per occupare il processore quasi al 100% (solo

7.4. Il loop degli eventi. 141

Page 150: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

ProcessIdle rallenta un po’ le cose). Prima di prosciugare le risorse del nostro computer per niente, sarà megliocorrere ai ripari:

# se non volete importare time, usate wx.MilliSleep()import time

class MyApp(wx.App):def OnInit(self):

self.time_to_quit = Falsereturn 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,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 subitoavviare un altro loop, altrimenti la vostra applicazione resterà sospesa.

wx.App.GetMainLoop() restituisce un riferimento al loop degli eventi “principale”, ossia quello che è stato creatoda wxPython in wx.App.MainLoop. Va da sé che, se avete scritto un loop per conto vostro, allora GetMainLoopresituirà 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 infase di inizializzazione.

Infine, anticipiamo qui il concetto di “attivazione” dei loop, che riprenderemo tra poco, parlando degli event loopsecondari (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 ques-tione poco più che simbolica. Quando chiamate SetActive, l’unico cambiamento che avviene è l’impostazione diun flag interno.

Tuttavia, SetActive chiama contestualmente anche wx.App.OnEventLoopEnter, che è un altro degli hookdella 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 deglieventi nella coda). Si noti inoltre che OnEventLoopEnter viene chiamato ogni volta che si entra in un nuovo loopdegli eventi (come vedremo presto, possono esserci più loop nella vita di un’applicazione wxPython). Se vi serveeseguire codice solo una volta all’inizio, potete testare se il loop è IsMain. Simmetricamente, quando uscite da unloop degli eventi (chiamando Exit come vedremo tra poco), viene chiamato wx.App.OnEventLoopExit chepotete sovrascrivere.

In definitiva, “attivare” un loop può essere completamente inutile. Conviene però sempre farlo, per uniformità eperché wxPython “se lo aspetta” (nel senso che altre parti del codice potrebbero testare GetActive e prendere delledecisioni 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 parleremotra poco, a proposito dei loop secondari.

142 Chapter 7. Appunti wxPython - livello avanzato

Page 151: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Per quanto riguarda GetActive, ricordiamo infine che si tratta di un metodo di classe, e che quindi va usato sem-plicemente così:

wx.GUIEventLoop.GetActive() # restituisce l'istanza del loop attivo

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 quandola 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)

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” lavostra operazione bloccante. Nel nostro esempio, potete sperimentare con diverse durate di long_op per vedere finoa quando la gui risponde in modo accettabile.

Se riuscite a segmentare adeguatamente l’operazione bloccante, Yield potrebbe essere un primo tentativo per inte-grare task secondari in modo “asincrono” (senza ricorrere a thread separati), o addirittura per integrare loop esternidentro 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, abbiamodovuto disabilitare il pulsante, mentre l’operazione è in corso. Provate a eliminare questa precauzione, e cliccare due

7.4. Il loop degli eventi. 143

Page 152: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

volte in successione sul pulsante: otterrete un PyAssertionError. C’è anche un altro modo per evitare questoproblema: chiamare Yield con il parametro onlyIfNeeded=True (è False per default. Notate anche cheYeld(True) è disponibile anche sotto forma della funzione globale wx.YeldIfNeeded()). Provate a toglierele righe di codice che dis/abilitano il pulsante, e sostituire la chiamata con wx.GetApp().Yield(True). Nonotterrete più nessun errore, ma naturalmente questo non vuole ancora dire che siete a posto: nel nostro caso, chiamarericorsivamente l’operazione bloccante genera un sovraccarico sufficiente per bloccare comunque la gui, e Yield nonpuò farci nulla.

Questo ci insegna la lezione più importante: Yield può consentire di sbloccare la gui mentre un’operazione altri-menti 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 disabilitaremenu e pulsanti per evitare inconsistenze.

Per questa ragione, talvolta è preferibile usare invece wx.App.SafeYield (che è anche disponibile come funzioneglobale 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’interfacciaprima di procedere con l’operazione, e le sblocca di nuovo alla fine. Se invece passate come primo argomento unriferimento 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 equando bloccare l’interfaccia, testando il metodo wx.GUIEventLoop.IsYielding. Questo metodo restituisceTrue 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:

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 cononlyIfNeeded=True, e inoltre accetta come parametro una bitmask di categorie di eventi da processaresubito: quindi, solo gli eventi che non appartengono a quelle categorie verranno ritardati. E’ facile vederlo in azionenel 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 quelliche provengono da thread o altre fonti “ritardabili” (nel caso del nostro esempio non ci sarà ovviamente nessun effettovisibile).

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): bisognaper 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.

Loop secondari.

Finora abbiamo parlato sempre e solo di “un” event loop, ma la realtà è più complicata. Nella vita di un’applicazionewxPython è possibile avere più loop compresenti: wxPython mantiene uno stack di loop degli eventi “innestati” unodentro 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

144 Chapter 7. Appunti wxPython - livello avanzato

Page 153: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

implementare un dialogo modale, wxPython crea e avvia un nuovo loop degli eventi, che finisce quindi in cima allostack. Quando il dialogo è distrutto, il nuovo loop termina e viene espulso dallo stack, facendo tornare il controllo alloop precedente. Naturalmente nulla vieta che nel dialogo modale ci sia, per esempio, un pulsante che apre un nuovodialogo 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 pul-sante, noterete che il loop attivo è di volta in volta diverso (confrontate gli indirizzi di memoria per vederlo).

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 frequentedietro le quinte.

In pratica, quanto è importante sapere queste cose? Dipende dal vostro scenario: di solito, anche quando sovrascrivetewx.App.MainLoop e gestite gli eventi “a mano”, il comportamento standard dei dialoghi modali è comunque quelloche volete. Non vi importa se gli eventi prodotti dal dialogo tornano a essere gestiti in modo autonomo da wxPythonper un po’.

Se però lo ritenete opportuno, potete creare e distruggere anche i loop annidati “secondari”. In questo caso, dovrestericordarvi 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 mantieneun riferimento a quello vecchio. Quando distruggete l’istanza di wx.EventLoopActivator, automaticamenteverrà 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.activeself.Destroy()

class Test(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)p = wx.Panel(self)

7.4. Il loop degli eventi. 145

Page 154: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 avrebbesemplicemente 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, equando il dialogo viene chiuso, distruggiamo anche l’istanza dell’attivatore, ripristinando il loop precedente. Notateperò che wx.EventLoopActivator, al momento della sua distruzione, non chiama Exit sul loop, quindi dobbi-amo pensarci noi stessi (simmetricamente, chiamare Exit sul loop non basta a “disattivarlo”! Occorre distruggere ilwx.EventLoopActivator che lo ha attivato).

E’ importante uscire dal loop con Exit? Lo è abbastanza: come abbiamo già detto qui sopra, wx.App mettea 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 proprioda wx.GUIEventLoop.Exit). Potete sovrascrivere questi metodi per eseguire codice ogni volta che entrate euscite da un loop degli eventi. Per vedere come funzionano, potete sostituire l’App generica dell’esempio precedentecon 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 setogliete la chiamata a self.loop.Exit(), vedrete che MyApp.OnEventLoopExit non viene più eseguitoquando chiudete il dialogo. Naturalmente, potrebbe essere quello che volete in certe occasioni: l’importante è capireche 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 distruggerlesemplicemente pop-andole fuori dallo stack.

Creare loop degli eventi personalizzati.

Negli ultimi esempi qui sopra, vi sarete accorti che, per giostrare tra diversi loop, abbiamo di nuovo rinunciato aoccuparci di gestire personalmente gli eventi. Tutto ciò che abbiamo fatto è stato istanziare dei wx.GUIEventLoope 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 questiloop gestiscono gli eventi, siete arrivati al punto in cui dovete sotto-classare wx.GUIEventLoop.

146 Chapter 7. Appunti wxPython - livello avanzato

Page 155: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Il metodo che vi serve sovrascrivere è Run, all’interno del quale wxPython fa girare il ciclo infinito che già conos-ciamo. Ecco un esempio minimale, che dovrebbe ormai esservi familiare: abbiamo solo spostato il cuore delle oper-azioni 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 = Falsewx.GUIEventLoop.__init__(self)

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 = Falsereturn 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 vitadell’applicazione (chiamiamo wx.Exit() subito dopo che il loop ha smesso di funzionare). Ma naturalmente poteteorganizzare le cose in modo da attivare più loop il successione, a seconda delle vostre esigenze.

7.4. Il loop degli eventi. 147

Page 156: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 negliscenari 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” (uneditor 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 IPythondi integrare al suo interno una gui wxPython.

In teoria, come parte di un’architettura Model-Controller-View, si potrebbe voler “spacchettare” il loop degli eventiper farlo gestire da un Controller esterno a wxPython. Ma è un approccio inutilmente complicato, almeno in lineadi principio: per fortuna wxPython offre degli agganci molto più comodi. Postare eventi personalizzati nella coda, operfino usare un handler personalizzato sono tecniche molto più pratiche e agevoli per stabilire una comunicazionetra la gui e il Model sottostante (senza contare, naturalmente, la possibilità di un sistema di messaggistica estraneo awxPython, come Publisher/Subscriber).

Todo

una pagina su mcv

In generale, prima di smontare il loop degli eventi, conviene provare tutte le altre soluzioni: quelle descritte in questapagina 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, è quandodovete affiancare a wxPython un altro loop degli eventi. La logica di molte applicazioni si fonda su qualche tipo diciclo 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 esserenecessario accedere direttamente al loop degli eventi. Ma non è l’unica strada, e anzi, talvolta non ce n’è propriobisogno: dedichiamo una pagina separata ad approfondire questi scenari.

Come integrare event loop esterni in wxPython.

Affrontiamo in questa pagina un argomento trasversale ad altri che abbiamo coperto in queste note: di conseguenza quici 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.

Il problema.

wxPython, come tutti i gui framework, è organizzato intorno al suo “loop degli eventi”, in pratica un while Trueinfinito che resta attivo per tutto il ciclo di vita della vostra applicazione. Di conseguenza, in un’applicazione wx-Python, 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”.

148 Chapter 7. Appunti wxPython - livello avanzato

Page 157: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Un esempio concreto.

Immaginate di aver scritto il motore di un gioco “in tempo reale”: al suo cuore, è un ciclo senza fine che controllalo stato del mondo, decide le mosse di giocatori governati dell’AI, e tra le altre cose accetta gli input del giocatoreumano... che che voi volete appunto gestire con un’interfaccia grafica wxPython. Questo scenario pone anche ilproblema 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 loopdel nostro gioco (non nello stesso thread, per lo meno).

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 inputagendo sull’interfaccia; i callback gestiscono la risposta appropriata; e in sostanza il “main loop” di tutta l’applicazionediventa 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 wx-Python. Le azioni da compiere in risposta all’input dell’utente verranno eseguite nei callback. Le azioni da eseguire aciclo 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 delpattern Model-Controller-View. E’ però una questione discutibile. In un certo senso, nessun gui framework rispettail pattern MCV in modo rigoroso: avete già rinunciato a MCV dalla riga import wx in testa al vostro codice. InwxPython, come abbiamo visto tutti i widget derivano sia da wx.Window sia da wx.Event: e per questo recitanoallo 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 averpresente che wxPython (al pari di altri framework analoghi) non riguarda solo la parte “view”, ma mette anche adisposizione gli strumenti per gestire (almeno in parte) il “controller” dell’applicazione. Bisogna giocare secondo leregole di wxPython, e adattare MCV di conseguenza. Nel nostro caso, come abbiamo detto, vorrebbe dire in praticalasciare 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 usarequando il main loop “ospite” è complesso, o è comunque già stato scritto e non intendiamo rinunciarvi.

7.5. Come integrare event loop esterni in wxPython. 149

Page 158: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

E poi ci sono anche altri componenti già esistenti, “a eventi” o comunque dotati di qualche tipo di loop, che fornisconoservizi di gestione e coordinamento tra logica di business e logica di presentazione, e che potremmo dover integrare inwxPython. Per esempio framework asincroni come Twisted, o librerie più leggere come Gevent, motori di renderingcome PyGame, e molti altri ancora. Naturalmente non possiamo metterci a “smontare” il main loop di questi grandiframework: 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 dentrol’applicazione wxPython. Per esempio, immaginate questo scenario: voi avviate normalmente la wx.App, e pre-sentate una finestra con un pulsante. Quando l’utente fa clic sul pulsante, parte il motore del gioco. Questo vorrebbedire, 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 call-back on_clic: wxPython aspetterà per sempre l’uscita da quel callback, il loop degli eventi si fermerà, e l’interfacciasi 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 poteressere intercalate alle operazioni della gui, senza causare rallentamenti.

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 inter-calate 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 sietepronti a implementare una delle soluzioni che seguono (altrimenti, forse potete ugualmente provare con la n. 5, cherovescia i termini del problema e vi propone di segmentare il loop di wxPython, invece).

150 Chapter 7. Appunti wxPython - livello avanzato

Page 159: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 accu-mulati 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 poteteinserirlo 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 laresponsività 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. Maquando le applicazioni diventano più complesse, e soprattutto quando i messaggi tra la gui e il “ciclo ospite” sifanno più intrecciati ed è cruciali risolverli nell’ordine corretto... le cose potrebbero diventare difficili da gestire eda 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) sonosempre in agguato.

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 percompiere 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 ormaiwxPyhton ha già emesso il suo wx.EVT_IDLE, e dal suo punto di vista non è ancora cambiato niente... quindi tuttosi 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 globalewx.WakeUpIdle ha un effetto simile). Se nel frattempo l’utente è rimasto inattivo, l’evento sarà di nuovo intercettatoda noi. Se invece l’utente ha prodotto qualche segnale, allora prenderà la precedenza, e verrà processato.

7.5. Come integrare event loop esterni in wxPython. 151

Page 160: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Vantaggi: questo sistema è davvero molto performante (rispetto a un timer, per esempio). E’ probabilmente il sistemamigliore 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 ilsistema 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 achiamare Skip() altrimenti il callback della wx.App non verrà mai attivato. Ma anche in casi apparentementenormali, l’emissione di wx.EVT_IDLE potrebbe interrompersi per un po’: per esempio, se l’utente apre un menu oun dialogo modale, la gui resta attiva finché non viene richiuso.

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 eventoperiodico:

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()self.game.make_step()self.timer.Start(10)

Notate che forse dovremmo comunque chiamare Yield, se la gui reagisce troppo lentamente: ma conviene primatestare 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 bilanciareesattamente le esigenze di entrambi i loop. Inoltre i timer sono più affidabili dei wx.EVT_IDLE: non c’è il rischioche 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 impegnatala cpu, potrebbe darvi noia il sovraccarico ulteriore dovuto al timer. In questo caso, forse vi convengono i wx.EVT_IDLE.

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 wxPythone delle tecniche per manipolarlo. Molte cose si possono personalizzare se intervenite a quel livello. Ma la tecnicastandard che si può applicare al nostro esempio è questa:

class MyApp(wx.App):def OnInit(self):

self.time_to_quit = Falseself.game = Game()

def MainLoop(self):

152 Chapter 7. Appunti wxPython - livello avanzato

Page 161: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 unostesso 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 segnaliprovenienti 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à raccoltoe processato immediatamente dopo, nello stesso ciclo. Allo stesso modo, se la parte wxPython vuole inviare un segnaleal loop ospite, questo sarà processato nel ciclo immediatamente successivo, e non oltre.

Todo

una pagina su MVC | una pagina su pub/sub.

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()

7.5. Come integrare event loop esterni in wxPython. 153

Page 162: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

# 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 comeguidare 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 eun 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 dibootstap_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 tuttosi mette in moto. Il metodo run esegue alternativamente, in un ciclo infinito, uno step del nostro motore di gioco, euno step dell’interfaccia wxPython.

Più precisamente, uno step di wxPython (make_wxStep) corrisponde alla consueta sequenza di gestione degli eventia 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.

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. Manaturalmente 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, eccovila versione breve. I thread sono sempre un argomento controverso: in generale tutti li sconsigliano, e con molte buoneragioni. Nella pratica però spesso sono una soluzione comoda e a portata di mano... almeno finché non scappano dimano. In python i thread sono particolarmente facili da usare, e in wxPython sono addirittura banali, finché vi tenetesul sicuro e seguite una raccomandazione fondamentale.

Questa raccomandazione è di non eseguire mai chiamate che pprovengono da da un thread secondario e che modificanolo 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 unacasella di testo da un thread secondario. Questo in pratica vi lascia con ben poche opzioni: per fortuna però, in novecasi 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. Questafunzione globale è semplicemente un wrapper thread-safe intorno alle chiamate di funzione wxPython. Detto in pocheparole, se da un thread secondario chiamate qualcosa come:

154 Chapter 7. Appunti wxPython - livello avanzato

Page 163: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 inwxPython. 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 threadsecondario, e poi mandare messaggi al thread principale usando wx.CallAfter con regolarità (o postando eventinella 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, chehanno 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 threadingimport timeimport 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 = Falseself.gui_frame = gui_frameself.game = Game()

def run(self):while True:

if self.time_to_quit: breakwx.CallAfter(self.gui_frame.update_gui,

self.game.check_world_status())time.sleep(2)if self.time_to_quit: breakwx.CallAfter(self.gui_frame.update_gui,

self.game.update_resources())time.sleep(2)for player in self.game.players:

if self.time_to_quit: breakwx.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)))

7.5. Come integrare event loop esterni in wxPython. 155

Page 164: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 = Truedlg = 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à. Abbi-amo usato time.sleep dentro il main loop per simulare una certa complessità, e soprattutto per lasciarvi il tempodi 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 unpunto in cui non ha ancora eseguito il controllo sul flag self.time_to_quit, e quindi cerca di scrivere in unacasella di testo che ormai è stata distrutta. Eventi come questo in wxPython sollevano un PyDeadObjectError,che dobbiamo evitare. Per questo motivo abbiamo introdotto il check if self.time_to_quit: break ognivolta che potevamo, ma restano sempre dei “buchi” di almeno 2 secondi. Abbiamo infine risolto ritardando la chiusuradel 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 vostromain loop non resterà mai bloccato così a lungo. Tuttavia è un problema interessante, ed è un esempio dei grattacapiche potreste dover affrontare: CallAfter programma un aggiornamento futuro della gui, che però nel frattempopotrebbe 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 sedifficilmente vale la pena. D’altro canto, i thread possono essere utili per altri compiti di routine, per esempio eseguireoperazioni di lunga durata in background senza bloccare la gui. Ma discuteremo meglio di questi aspetti quandoparleremo di thread.

Todo

una pagina sui thread: accorciare tutto questo paragrafo di conseguenza.

156 Chapter 7. Appunti wxPython - livello avanzato

Page 165: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Integrare altri framework in wxPython.

Le tecniche viste fino a questo punto possono anche aiutarvi a usare wxPython insieme ad altri framework dotati di unproprio main loop.

A dire il vero, alcuni framework mettono a disposizione delle apposite api per l’integrazione con wxPython. In altricasi, ci sono comunque delle ricette già collaudate. Prima di mettersi a sperimentare soluzioni fatte in casa, convienesempre 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. Ladocumentazione 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 lagui.

Queste capacità sono esposte anche sotto forma di api, cosa che vi permette di scrivere gui wxPython integrate inIPython. Come ricorda anche la documentazione, in questo caso dovete però fare attenzione a non avviare voi stessi ilwx.App.MainLoop, perché questo è un compito da lasciare allo hook di IPython. Potete comunque sottoclassare eistanziare 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 dellenecessarie astrazioni per supportare diversi gui framework da collegare e disconnettere a runtime, troverete alcunispunti 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 sonovecchie e tutte piuttosto sperimentali. Far girare Pygame in un thread separato sembra non essere l’idea giusta: Pygameha bisogno di vivere nel main thread, proprio come wxPython. La soluzione corretta sembra essere una qualchevariante del pattern di sfruttare wx.EVT_IDLE o wx.EVT_TIMER per aggiornare il canvas di Pygame.

Questa pagina del wiki di wxPython sono raccolti alcuni suggerimenti. Ma la strada più promettente sembra esserequella indicata nel sito di Pygame.

wxPython e Twisted.

Twisted mette a disposizione un reactor specializzato per l’integrazione con wxPython. Potete trovare i dettagli nelladocumentazione, 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.reactorfrom twisted.internet import reactor

class MyFrame(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)

7.5. Come integrare event loop esterni in wxPython. 157

Page 166: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 chiuderereactor.stop()

def on_clic(self, evt):# programmiamo una chiamata con il reactor di Twistedreactor.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.Appreactor.run()

Chiudere i widget: aspetti avanzati.

Questa pagina riprende il discorso sulla chiusura dei widget, esaminando alcuni scenari che si verificano di rado mache è meglio conoscere per evitare sorprese.

Anche se in questa pagina non facciamo esempi espliciti, è chiaro che alcuni comportamenti che descriviamo potreb-bero, in determinate circostanze, innescare dei wx.PyDeadObjectError: vi conviene quindi leggere anche lapagina sulle eccezioni wxPython subito dopo o subito prima di questa.

Distruzione di finestre a cascata.

Quando chiudete una finestra, naturalmente wxPython distrugge a cascata anche tutti i widget “figli” nella catena deiparent. Attenzione però: non solo i pulsanti, caselle di testo etc. dentro la finestra, ma anche eventuali altre finestrefiglie di quella che state chiudendo.

Ma qui c’è una trappola: la distruzione di tutti i widget figli (e quindi anche delle finestre figlie) avviene chiamandodirettamente wx.Window.Destroy, e non il più gentile wx.Window.Close. Dopo aver visto tutte le com-plicate 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 dichiusura. Ecco un esempio pratico di quello che potrebbe succedere:

158 Chapter 7. Appunti wxPython - livello avanzato

Page 167: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 in-vece 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 esattadipende 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()))

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)

7.6. Chiudere i widget: aspetti avanzati. 159

Page 168: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 mes-saggistica pub/sub in un’architettura MVC). Ma qui il punto importante è che abbiamo intercettato il wx.EVT_CLOSEdella finestra principale per impedire a wxPython di distruggere sbrigativamente le finestre figlie. Invece, chiamiamomanualmente wx.Window.Close su tutte le finestre figlie ancora aperte, garantendo così l’esecuzione dei callbackcon le operazioni di chiusura.

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. wxPythonmette a disposizione un hook ben preciso per questo scopo, ed è wx.Window.Close. La chiamata a questometodo (causata dal vostro codice, o dall’utente che fa clic sul pulsante di chiusura), provoca l’emissione di unwx.EVT_CLOSE che potete intercettare: abbiamo anche visto che molti virtuosismi sono possibili durante questafase.

In ogni caso, finché si resta allo stadio di wx.Window.Close, la finestra è ancora “in vita”, stabile e pronta peressere usata. Per esempio, se nel callback del wx.EVT_CLOSE volete prelevare il contenuto di una casella di testonella 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 didefault del wx.EVT_CLOSE, se voi non stabilite diversamente). Da questo momento, la finestra entra in una fasetransitoria, e non ne viene garantito il normale funzionamento: wxPython procede a distruggere tutti i suoi figli chia-mando direttamente wx.Window.Destroy su ciascuno di essi. In questa fase, di regola non viene emesso nessunevento che possiate intercettare: wxPython fa tutto da solo, ed è saggio non cercare di intromettersi nel processo.

Potete verificare se una finestra è sul punto di essere distrutta testando wx.Window.IsBeingDeleted: se otteneteTrue, 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 wxPythonla 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 perla 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 processarequando procede con la distruzione. Ma la distruzione completa di una finestra può richiedere diversi cicli di questotipo.

Tra il momento in cui wx.Window.Destroy viene chiamato e il momento in cui anche l’ultimo “pezzo” dellafinestra è effettivamente distrutto, è possibile avere il tempo per fare qualcosa di sbagliato? Di regola, no. Però dovec’è una regola, c’è anche un’eccezione.

160 Chapter 7. Appunti wxPython - livello avanzato

Page 169: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Per esempio immaginate che, nel corso della sua distruzione, il widget abbia il tempo di postare un evento nellacoda: l’evento farebbe riferimento a un oggetto la cui esistenza è... discutibile. Se un callback intercettasse l’eventoe chiamasse per esempio wx.Event.GetEventObject, non è chiaro che cosa potrebbe succedere. Forse statepensando di scrivere voi stessi una trappola di questo tipo, per esempio estendendo wx.Window.Destroy in unasotto-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 normalmentewx.Button.Destroy(self)

Purtroppo o forse per fortuna questo codice non fa proprio quello che vi aspettate. Il fatto è che non potete sem-plicemente sovrascrivere il proxy Python (in wxPython) di un metodo virtuale C++ (in wxWidgets), e sperare di aversovrascritto anche l’originale. E così, se provate a implementare l’esempio qui sopra, otterrete due comportamentidifferenti:

1. quando chiamate MyButton.Destroy dal vostro codice Python, allora sicuramente la vostra versione diDestroy verrà eseguita;

2. quando invece wxWidgets chiama internamente wxWindow::Destroy per distruggere il pulsante (magariperché l’utente ha chiuso la finestra che lo contiene), allora verrà eseguito il codice C++ sottostante, e il vostroMyButton.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 situazioniimbarazzanti. Proviamo a implementare concretamente la nostra idea:

import wx.lib.neweventTestEvent, EVT_TEST = wx.lib.newevent.NewCommandEvent()

class MyButton(wx.Button):def __init__(self, *a, **k):

wx.Button.__init__(self, *a, **k)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)

7.6. Chiudere i widget: aspetti avanzati. 161

Page 170: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 lointercetta. Abbiamo già capito che l’evento non verrà emesso quando wxWidgets distruggerà il pulsante al momentodi chiudere la finestra. Tuttavia ci aspettiamo di vedere l’evento quando siamo noi a distruggere manualmente ilpulsante chiamando self.test.Destroy() nel nostro codice. E invece no: sicuramente il nostro codice vieneeseguito, e possiamo vedere che in effetti il pulsante si distrugge; tuttavia non riusciamo a intercettare l’evento. Ilmotivo, in questo caso, è che wx.PostEvent posta l’evento nella coda senza processarlo immediatamente. Subitodopo il widget viene distrutto: siccome però il primo handler dell’evento è il widget stesso, quando viene il momentodi 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 immedi-atamente 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 puntodovrebbe essere chiaro: non è mai conveniente maneggiare direttamente wx.Window.Destroy. Quando voletereagire 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 unpaio di esempi rari ma relativamente ben conosciuti. L’importante è imparare a riconoscere il pattern, nel caso dovesteimbattervi 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 wx.EVT_TREE_SEL_CHANGED ogni volta che l’utente seleziona un diverso elemento dell’albero. Quando wxPythondeve 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 unelemento 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 completamentel’albero, nel processo di distruzione, questo fenomeno di solito passa inosservato: infatti, siccome in genere l’alberoha una sola radice e lo svuotamento procede dalla radice alle foglie, una volta eliminata la radice non esiste più unelemento 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 widgetprovoca una raffica di wx.EVT_TREE_SEL_CHANGED, in numero variabile a seconda di quante radici ci sono, equale 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)

162 Chapter 7. Appunti wxPython - livello avanzato

Page 171: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

self.tree = wx.TreeCtrl(self,# TR_HIDE_ROOT per avere molte radicistyle=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’altoverso 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) sitrova 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 nelcallback del codice “sensibile”, che può effettivamente combinare dei guai (scritture sul database, etc.) quando vieneeseguito 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 internoutilizza un wx.TreeCtrl (ops). E naturalmente su Windows il file system può avere più di una radice (ops!)... Avetegià capito il problema:

# anche qui, l'effetto si vede solo in Windowsclass MyFrame(wx.Frame):

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 internoself.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 costituis-cono altrettante radici del file system. Durante la fase di chiusura, viene emesso un wx.EVT_TREE_SEL_CHANGEDspurio: il callback relativo cerca di accedere alla proprietà wx.GenericDirCtrl.Path; questo provoca un ten-tativo di accesso a un elemento del wx.TreeCtrl interno, che però in quel momento non è accessibile; e questo, asua volta, provoca un clamoroso crash del programma!

7.6. Chiudere i widget: aspetti avanzati. 163

Page 172: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Notate che non sempre il programma va in crash. Le due radici vengono distrutte in ordine (prima c:, poi d:), e sevoi 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 es-empio wx.Treebook emette un suo specifico evento wx.EVT_TREEBOOK_PAGE_CHANGED, che non è soggetto aquesto problema... almeno fin quando non avete bisogno di personalizzare il suo comportamento al punto da accederedirettamente 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 delicatonel quale sarebbe meglio non intervenire. Se però vi trovate in circostanze eccezionali, nel dubbio wx.Window.IsBeingDeleted è sempre pronto per voi.

Logging in wxPython (1a parte).

Questa è la prima di una serie di pagine che dedichiamo agli argomenti in qualche modo interconnessi del logging edella gestione delle eccezioni. Non sono problemi specifici della programmazione di interfacce grafiche. In generalesi usano le stesse strategie di qualunque programma Python: in queste pagine supponiamo sempre che abbiate unabuona 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 poterrintracciare l’origine di un problema tra migliaia di azioni provenienti da migliaia di connessioni contemporanee. Leapplicazioni desktop in genere lavorano in ambienti più piccoli e più protetti, ma non per questo potete permettervidi trascurare il logging. Una volta che il programma ha abbandonato il vostro controllato ambiente di sviluppo perentrare 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 diaffrontare 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 manmano che scala verso un numero di connessioni sempre più elevato. Per un’applicazione desktop, raramentequesto aspetto ha importanza. Lo scopo del logging è quindi soprattutto di aiutarvi a rintracciare le cause deglierrori.

2. Di conseguenza, non abbiate paura di loggare molto. Nel web scegliere cosa loggare è un’arte, e un log alluvion-ato 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 ilregistro.

3. Ma fate attenzione a non loggare dati sensibili! Questo può essere molto pericoloso, perché in genere i file di logsono più vulnerabili del database dell’applicazione. In molti casi è comodo loggare, per esempio, l’ultima rigainserita in una tabella, per avere idea precisa di che cosa ha innescato un errore. Ma dovreste attribuire a questeregistrazioni un livello di criticità più basso (“debug”, per esempio) di quello che impostate in produzione. Inogni caso, concordate sempre una policy precisa con il committente/utente del vostro programma: fategli sapereche 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,

164 Chapter 7. Appunti wxPython - livello avanzato

Page 173: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

per esempio, generare un codice casuale al momento del login, da usare solo ai fini del log e da cambiare a ogninuova sessione.

5. Fornite all’utente un modo per cambiare temporaneamente il livello di criticità del logging, senza bisogno di unvostro diretto intervento sui file di configurazione (per esempio, potrebbe essere una regolazione da includere inun dialogo di opzioni del vostro programma).

6. Preparatevi all’eventualità che tutto possa andar bene! Se non intervenite per parecchio tempo, le dimensioni delfile di log cresceranno senza limiti, e questo è un problema. Se usate il modulo logging della libreria standarddi 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: inogni caso, fate delle prove e testate in anticipo.

Logging con Python.

Il modulo logging della libreria standard di Python è un ottimo framework: se non avete esigenze particolari, viconviene senz’altro affidarvi a questo. Se non conoscete logging, la documentazione ufficiale è un modello dichiarezza: 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 wx-Python 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 semplice-mente 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’organizzazionedel vostro codice, dalle necessità e dai gusti personali. Se riuscite a mantenere funzionalità 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 (evi ritroverete con un log sterminato). Se proprio vi serve loggare questi eventi, potete aiutarvi con strumenti comewx.Yield.

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, usarefile di configurazione diversi).

7.7. Logging in wxPython (1a parte). 165

Page 174: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Se preferite, tuttavia, potete indirizzare il log verso la vostra gui. Questo potrebbe essere desiderabile in fase disviluppo, 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 qualchewidget super-specializzato come RichTextCtrl, basato su Scintilla). In fase di produzione, d’altra parte, questonon 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 notetecniche per visualizzare l’output nella gui. In questo modo, tuttavia, lo stream del log si mescolerebbe agli standardoutput/error (che però in una applicazione grafica non dovrebbero comunque essere mai troppo impegnati). Se questoper voi è inaccettabile, e volete comunque mantenere il flusso del log ben separato, magari dedicandogli uno spazioad-hoc nella vostra interfaccia, anche questo non è difficile.

Quello che dovete fare è crearvi un logging.Handler personalizzato. Ecco un approccio minimalistico al prob-lema:

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.emitwrite = 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)

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):

166 Chapter 7. Appunti wxPython - livello avanzato

Page 175: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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, chesi potrà poi abbinare a qualunque widget wxPython: l’unica richiesta, naturalmente, è che il widget implementi unainterfaccia 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 mododa essere sicuri di rispettare l’ordine giusto. Finché il widget che deve contenere il log è nella finestra principaledell’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 widgetnon 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 accertarviche il widget esista sempre per tutto il ciclo di vita della vostra applicazione. Per esempio potreste nascondere lafinestra, 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)

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 MyLoggingHandlerself.log_record.AppendText(record)

class MainFrame(wx.Frame):def __init__(self, *a, **k):

wx.Frame.__init__(self, *a, **k)

7.7. Logging in wxPython (1a parte). 167

Page 176: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 almomento di chiudere la finestra principale (in risposta al wx.EVT_CLOSE). Per tutto il ciclo di vita dell’applicazione,l’utente apre e chiude a piacere la finestra, ma in realtà non fa altro che mostrarla e nasconderla.

Loggare da thread differenti.

logging è thread-safe, quindi non dovreste poter tranquillamente loggare da thread differenti, come per qualsiasialtro programma Python.

Il problema naturalmente si verifica quando, oltre a voler loggare da thread diversi, avete anche fatto qualcosa diesotico come re-indirizzare l’output del log verso qualche widget della gui. Questo è vietato dalla prima regola aureadei thread in wxPython: mai modificare lo stato della gui da un thread secondario. Niente paura, però: la secondaregola aurea dei thread in wxPython viene in soccorso: basta includere la chiamata pericolosa in wx.CallAfter etutto torna a funzionare come per magia.

Riprendendo l’esempio qui sopra, se volete usare usare l’handler personalizzato MyLoggingHandler in una situ-azione in cui è possibile che il log sia scritto anche da thread secondari, basterà modificarlo come segue:

168 Chapter 7. Appunti wxPython - livello avanzato

Page 177: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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.

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 dilogging, e tuttavia dovete ugualmente sapere come funziona. Infatti wxPython lo utilizza anche senza il vostroconsenso, per alcune importanti funzioni: ne parliamo più in dettaglio nella prossima pagina che dedichiamo a questiargomenti.

Logging in wxPython (2a parte).

Riprendiamo qui il discorso sul logging che abbiamo iniziato nella pagina dedicata alle strategie per integrarelogging in una applicazione wxPython. Ci occupiamo adesso del framework interno di logging di wxPython.

Logging con wxPython.

wxWidgets mette a disposizione un sistema completo di logging, e anche wxPython lo traduce, almeno in parte. A direil vero, da quando esiste logging nella libreria standard di Python, non c’è quasi più bisogno di ricorrere a wxPythonper 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, occasionalmentewxPython 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, dovetesapere 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 è direttamentenecessaria per loggare con le impostazioni di default. Esistono diversi livelli di allerta, simboleggiati dalle costantiwx.LOG_* (da wx.LOG_FatalError che vale 0 a wx.LOG_Trace che vale 7). Per loggare un messaggiocon il desiderato livello, si può ricorrere alle funzioni globali corrispondenti wx.Log* (da wx.LogFatalErrora wx.LogTrace). Questo specchietto riassuntivo dovrebbe aiutarvi a capire come funzionano le impostazioni didefault:

LOG_LABELS = ['Fatal Error', 'Error', 'Warning', 'Message', 'Status','Verbose', 'Debug', 'Trace', 'Sys Error']

LOG_FUNCTIONS = {# significato funzione di log

7.8. Logging in wxPython (2a parte). 169

Page 178: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

'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 livellolevel_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 defaultself.SetStatusBar(wx.StatusBar(self))

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:

170 Chapter 7. Appunti wxPython - livello avanzato

Page 179: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• potete cambiare in ogni momento la soglia del logging con wx.Log.SetLogLevel (è un metodo statico, nonserve istanziare prima wx.Log);

• wx.LogVerbose e wx.LogInfo sono sinonimi: entrambe le funzioni loggano un messaggio con livellowx.LOG_Info (ovvero, livello 5);

• i livelli fino a wx.LOG_Status, di default, emettono dei messaggi visibili all’utente, sotto forma di pop-updifferenziati a seconda della gravità del problema. Il messaggio di wx.LogStatus è visibile nella barra distato della finestra (nel nostro esempio qui sopra, sembra succedere solo la prima volta: poi la barra di stato nonviene pulita, e i successivi messaggi non si distinguono più). Sopra wx.LOG_Status, i messaggi non sonopiù 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 mostrareil messaggio in finestre diverse, esiste anche wx.LogStatusFrame. Per esempio questo, chiamato da unafinestra 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 delnormale wx.MessageBox. Infine, termina il programma (!) con exit code 3;

• wx.LogSysError si comporta come wx.LogError, ma è un caso speciale. Viene usato soprattutto interna-mente da wxPython, e restituisce anche l’ultimo errore di sistema occorso (errno su Unix, GetLastErrorsu 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). Ilproblema è che siccome i livelli predefiniti vanno da 0 a 7 (e wx.LOG_User vale 100!) non è possibiledefinire 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. Normal-mente 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 didebugging.

Todo

una pagina sul debugging.

Cambiare il log target.

Il framework di logging wxPython ha il concetto di “log target” per indicare dove dovrebbero essere diretti i messaggidi 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 abbiamovisto fin qui, è wx.LogGui. Per esaminare gli altri, utilizziamo questa semplice finestra di lavoro:

7.8. Logging in wxPython (2a parte). 171

Page 180: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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): passdef 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 accumulanole scritture di log, che vengono mostrate all’utente quando periodicamente il buffer viene svuotato. Il log target didefault 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 conseguenzail buffer viene svuotato sempre molto rapidamente, e l’impressione per l’utente è che che il messaggio di log siavisualizzato non appena viene emesso.

Potete tuttavia interrompere manualmente lo svuotamento del buffer chiamando wx.Log.Suspend, e riprenderlocon wx.Log.Resume (e in questo intervallo, se volete, potete usare wx.Log.Flush per svuotare il buffer inmomenti precisi). Nel nostro esempio, provate a cliccare sul pulsante “sospendi log”: le registrazioni successive siaccumulano 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 duevolte di seguito su “sospendi log”, dovete poi cliccare due volte su “riprendi log”. Questo comportamento sembrabizzarro, visto nell’interfaccia del nostro esempio: ma è il nostro esempio a essere anomalo. In realtà, sospendere losvuotamento del log dovrebbe essere considerata un’operazione temporanea (quindi, evitate di lasciare direttamentein mano all’utente questa opzione!): ci si aspetta che il componente che chiama wx.Log.Suspend si preoccupianche di chiamare wx.Log.Resume quanto prima, per evitare che il buffer continui a riempirsi all’infinito. Lostack di wx.Log.Suspend serve appunto a permettere che ciascun componente possa sospendere e ripristinare losvuotamento del buffer senza preoccuparsi degli altri.

Detto questo, esaminiamo gli altri log target disponibili, in aggiunta al predefinito wx.LogGui. Il primo è wx.LogWindow, che apre una finestra separata e re-indirizza il log verso quella:

def on_target(self, evt):self.actual_log = wx.LogWindow(pParent=self, szTitle='LOG',

bShow=True, bPassToOld=False)

def on_restore(self, evt):

172 Chapter 7. Appunti wxPython - livello avanzato

Page 181: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

self.actual_log = None

wx.LogWindow è un log target “evoluto” che deriva da wx.Log attraverso wxLogInterposer (una classewxWidgets che in wxPython non è tradotta). Questa aggiunta gli conferisce la capacità di mantenere un riferimento an-che al precedente target, e di indirizzare i messaggi a entrambi: potete settare l’argomento bPassToOld a True perverificare. Un altro effetto gradevole, dal punto di vista di un programmatore Python, è che per usare wx.LogWindowoccorre solo istanziarlo, e per tornare al log target precedente basta solo de-referenziarlo. I log target che vedremo inseguito, 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 distruggereil log target, con il risultato che non viene ripristinato il target di default. Purtroppo, impedire all’utente di chiud-ere la finestra non è immediato. wxWidgets mette a disposizione due hook, wxLogWindow::OnFrameClose ewxLogWindow::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 è bloccatoself.actual_log.GetFrame().Destroy()self.actual_log = None

Infine, ricordiamo che wx.LogWindow non usa un buffer interno: di conseguenza wx.Log.Suspend non haeffetto.

Un altro log target utile è wx.LogTextCtrl: questo target non è documentato ma, come suggerisce il nome, re-indirizza il log verso un wx.TextCtrl multilinea. Per usarlo, aggiungiamo quindi una casella di testo al nostroframe di esempio:

class MainFrame(wx.Frame):def __init__(self, *a, **k):

# etc. etc. come sopraself.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

7.8. Logging in wxPython (2a parte). 173

Page 182: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

e distruggere. Occorre passare da wx.Log.SetActiveTarget, un metodo che convenientemente restituisce unriferimento al target precedente, che possiamo poi usare al momento di ripristinare il log di default. Ricordiamo poiche 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: bastapassare al costruttore il riferimento di un file aperto. Purtroppo però wxPython non offre questa possibilità (un altrobaco...), 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: lescritture in coda vengono mostrate all’interno di un pop-up generico. L’effetto per l’utente è simile a quello delnormale 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’istanzadi wx.LogNull.

wx.LogNull va usato con cautela. A differenza di wx.Log.Suspend, che vi consente comunque di recuperare lescritture sul log in un secondo momento, questa classe disabilita completamente ogni tipo di logging. Se per esempioun errore di sistema (lato wxWidgets) dovesse accadere nel periodo “scollegato”, wxPython non potrebbe mostrarloall’utente nel consueto pop-up, né registrarlo in alcun modo (a meno che l’errore non sia così grave da terminare ilprogramma, certo).

Scrivere un log target personalizzato.

Non è difficile scrivere un log target a partire da zero: è semplicemente una sotto-classe di wx.Log (che però voidovete 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 occupadi formattare il messaggio. L’implementazione di default si limita ad aggiungere data e ora, e passare la stringaa wx.PyLog.DoLogTextAtLevel;

174 Chapter 7. Appunti wxPython - livello avanzato

Page 183: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• 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 delsistema, 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 intervenirealla 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: logging.critical, # non funziona! vedi sotto...wx.LOG_Error: logging.error,wx.LOG_Warning: logging.warning,wx.LOG_Message: logging.info,wx.LOG_Status: logging.info,wx.LOG_Info: logging.info,wx.LOG_Debug: logging.debug,

}

class MyLogTarget(wx.PyLog):def DoLogRecord(self, level, msg, info=None):

msg = '[da wxPython] ' + msgWXLOG_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')

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 mod-ulo logging. La formattazione dei messaggi è curata solo da logging.basicConfig: siccome abbiamosovrascritto wx.PyLog.DoLogRecord, la formattazione di wx.Log è completamente annullata. Potete speri-mentare 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’ultimoerrore di sistema riscontrato.

7.8. Logging in wxPython (2a parte). 175

Page 184: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 dawxPython.

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 mostrarliall’utente, per esempio). Il fatto è che, come spieghiamo meglio altrove, wxWidgets non usa le eccezioni (le eccezioniC++, naturalmente) per segnalare condizioni di errore: molto spesso solleva degli “assert” interni, che wxPython cat-tura e trasforma in wx.PyAssertionError. Ma altrettanto spesso utilizza wx.LogSysError per notificare ilproblema 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 è verificatol’errore), ma è meglio di niente. Possiamo almeno inserire un po’ di logica Python nel nostro log target, per reagire inmodo speciale a un errore grave:

class MyLogTarget(wx.PyLog):def DoLogRecord(self, level, msg, info=None):

msg = '[da wxPython] ' + msgWXLOG_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 logwx.LOG_Error, purtroppo). Questo però non è grave: wx.LogError non è usato internamente da wx-Python, e di sicuro non lo userete nemmeno voi (perché tutte le vostre scritture avverranno tramite il loggingdi Python, chiaramente!). Quindi, se il vostro log target intercetta un log di livello wx.LOG_Error, sicura-mente 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 esempioqui sopra, ma in realtà il comportamento di default non può essere cambiato (provate). Questo vuol dire chewxPython continuerà a mostrare un messaggio all’utente e chiudere l’applicazione, che vi piaccia o no. Anchequesto 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.

Quando il log di wxPython è utile.

Scrivere un log target specializzato che dirotta i messaggi verso il log di Python è probabilmente la cosa migliore dafare. 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 messaggioquando 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 contieneutili 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)

176 Chapter 7. Appunti wxPython - livello avanzato

Page 185: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 logtarget 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 recuperareil messaggio di wx.LogSysError e mostrarlo all’utente in un wx.MessageBox, ricostruendo in questo modo ilcomportamento 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 wx-Python l’uso di questo strumento è reso un po’ complicato dalla necessità di separare gli oggetti proxy Python daicorrispondenti oggetti C++: si tratta probabilmente di un vero e proprio baco, per giunta non documentato. Senzascendere troppo nel dettaglio, mostriamo un esempio funzionante di come si può usare wx.LogChain per dirigereil log contemporaneamente a wx.LogGui e al nostro log target specializzato (che a sua volta lo indirizza al log diPython):

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.LogGuiwx.Log.SetActiveTarget(wx.LogGui())# creiamo il nostro log target...log = MyLogTarget()# ... e lo aggiungiamo alla catenalog_chain = wx.LogChain(log)# separiamo i proxy Python dagli oggetti C++ sottostantilog.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 giustoreturn True

Se provate questa versione di wx.App con il codice dell’esempio precedente, noterete che i messaggi di log sonoeffettivamente indirizzati sia al log di Python (attraverso il log target personalizzato MyLogTarget), sia al log diwxPython (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 esattamenteoccorre distruggere gli oggetti proxy Python. this è un riferimento all’oggetto C++ collegato, e disown scollega ilproxy Python (che quindi può essere distrutto senza che l’oggetto C++ sia interessato).

Todo

una pagina su SWIG e l’oop Python/C++.

Il conclusione: come loggare in wxPython.

Se avete letto questa e la precedente pagina sul log, dovreste avere gli strumenti per implementare una strategia dilogging adatta alle vostre esigenze.

7.8. Logging in wxPython (2a parte). 177

Page 186: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Sarebbe questo il momento di dare qualche consiglio pratico riassuntivo prima di abbandonare definitivamentel’argomento. Siccome tuttavia il problema del logging in wxPython è strettamente intrecciato con il problema dellagestione delle eccezioni, preferiamo raccogliere queste indicazioni al termine del discorso sulle eccezioni.

Gestione delle eccezioni in wxPython (1a parte).

Questa è la prima di due pagine che affrontano il complesso problema della gestione delle eccezioni in un programmawxPython, strettamente interconnesso con le osservazioni che abbiamo già fatto sul logging. Potreste pensare chele eccezioni in wxPython funzionano come in un qualsiasi programma Python, ma come vedremo le cose non stannoproprio 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 derivanoalcune 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 segnalarecondizioni di errori. wxWidgets fa uso di varie formule di “assert” e/o di allarmi emessi con il suo sistema dilog interno: esiste un meccanismo di traduzione degli “assert” in eccezioni Python, e si possono escogitaresoluzioni 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 separataall’analisi delle condizioni di errore che possono originarsi dal codice C++ di wxWidgets.

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 trovarenessun handler disposto a gestirla, finché il gestore di default non termina il programma, inviando il tracebackdell’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:

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 tracebackrelativo 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’applicazionewxPython, 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 meccanismodell’emissione di un evento e della ricerca di un gestore: questa fase è controllata da wxWidgets (C++). Quandol’handler dell’evento è stato trovato, viene eseguito il callback relativo: questo in genere è codice che avete scritto voi

178 Chapter 7. Appunti wxPython - livello avanzato

Page 187: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(Python). Quando il callback è stato eseguito, il controllo ritorna al wx.App.MainLoop: questo è di nuovo codiceC++. E così via.

Ora, ecco il problema: una eccezione Python non può propagarsi oltre lo “strato” in cui viene emessa. Non è possibilepropagare un’eccezione Python attraverso strati di codice C++. Questo è un problema di cui gli sviluppatori wxPythonsono ben consapevoli: in teoria dovrebbe essere possibile tradurre al volo una eccezione Python nella sua controparteC++ e viceversa, in modo da permettere la propagazione libera tra i vari strati. In pratica però, non è affatto banaleimplementare 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: ilcodice C++ immediatamente successivo rileva che c’è una condizione di errore, chiama PyErr_Print per scriverlonello standard error, e resetta l’errore. Quindi l’eccezione termina lì, e non ha modo di propagarsi fino a raggiungere ilpunto 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 delleAPI C di Python).

Infine, tenete conto che esiste un caso interessante in cui questo comportamento non si applica. Se l’eccezionePython non controllata avviene prima di aver chiamato wx.App.MainLoop, allora effettivamente il programmasi interromperà “come al solito”: questo perché, prima di entrare nel main loop, Python ha ancora il controllodell’applicazione. In pratica, ci sono due posti in cui un’eccezione Python può verificarsi prima di entrare nel mainloop: in wx.App.OnInit e nell’__init__ del frame principale dell’applicazione.

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:

>>> 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!"...>>> salva_la_giornata()'salvo per miracolo!'>>>

Questa non è una buona pratica di programmazione: ma potete comunque farlo. In wxPython, invece, intercettareun’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)

7.9. Gestione delle eccezioni in wxPython (1a parte). 179

Page 188: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

def on_clic(self, evt):try:

self.SendSizeEvent() # genera manualmente un wx.EVT_SIZEexcept 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. Abbiamocollegato wx.EVT_SIZE a un callback che produce una eccezione Python non gestita. Possiamo aspettarci, tuttele volte che ridimensioniamo la finestra, di veder comparire il traceback nella shell (la finestra però si ridimensionacorrettamente. 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 emessoautomaticamente già al momento di creare la finestra: quindi dovremmo vedere un traceback nella shell proprio comeprima cosa.

Quando però facciamo clic sul pulsante, ci aspettiamo una cosa diversa. Nel callback del pulsante noi generiamomanualmente un wx.EVT_SIZE: quindi il callback difettoso verrà di nuovo eseguito, eccezione compresa. Questavolta 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 dicodice Python che innesca l’eccezione, c’è di mezzo un consistente strato di codice C++ (che si occupa del dispatchdel 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 ramoexcept 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: leeccezioni dovrebbero essere intercettate più vicino possibile al punto di emissione. In Python è una buona pratica diprogrammazione, ma in wxPython è un principio guida da seguire con il massimo scrupolo.

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 conuna domanda: come dovremmo comportarci con queste eccezioni non intercettate?

Il problema.

Prima di tutto, non dovrebbe esserci bisogno di dirlo, ma insomma: in un programma con interfaccia grafica, pensatoper l’utente finale, le eccezioni non gestite sono bachi puri e semplici. Non dovrebbero esserci. Se vi capitano in fasedi 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, lieseguite, 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 significache l’eccezione non gestita ferma il programma. Certo, l’utente non sarà felice. Forse l’uscita anomala lascerà qualcherisorsa 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 dinulla. Ecco uno scenario fin troppo comune (in pseudo-codice):

180 Chapter 7. Appunti wxPython - livello avanzato

Page 189: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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). Lanostra routine innesca un’eccezione custom “QualcosaNonVa” in caso di problemi, e noi correttamente la intercetti-amo 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 baconel layer di gestione del database, “db.orm.persisti” innesca un’eccezione che non abbiamo previsto: siccome non laintercettiamo, tutto sembra andare per il verso giusto (ricordate, wxPython non ferma il programma). Ma in realtà idati 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 daindividuare.

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:wx.MessageBox('Qualcosa non va!!')wx.Exit()

Purtroppo, come avrete già intuito, questo non funziona. A partire da quando invocate wx.App.MainLoop, cisono troppi strati di codice C++ perché una eccezione Python imprevista possa finire nella rete del “bare except” cheabbiamo 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')

7.9. Gestione delle eccezioni in wxPython (1a parte). 181

Page 190: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

# 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_excepthookapp = wx.App(False)MyFrame(None).Show()app.MainLoop()

Nel nostro “except hook” personalizzato possiamo inserire una logica arbitrariamente complessa. Per esempio, sarebbeperfettamente sicuro interagire con l’interfaccia grafica (aprire e chiudere finestre, postare eventi nella coda, etc.): lanostra 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 cosasta 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 nostroesempio. In alternativa, un buon momento è come sempre wx.App.OnInit.

Gestione delle eccezioni in wxPython (2a parte).

Riprendiamo il discorso sulle eccezioni da dove l’avevamo lasciato: dopo aver visto la difficile esistenza delle ec-cezioni Python nell’ecosistema wxPython, affrontiamo adesso lo stesso argomento dal punto di vista di wxPythonstesso (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 frame-work molto esteso e complesso. Lo sforzo di adattare wxWidgets alla nuova realtà è arrivato fino al punto di sup-portare l’utilizzo delle eccezioni nel codice client (ovvero, le applicazioni wxWidgets scritte in C++ possono usare leeccezioni). Ma al suo interno il framework ha continuato a utilizzare gli strumenti di cui si era nel frattempo dotatoper la gestione degli errori.

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 puntodi vista è che ogni volta che il codice wxWidgets emette un assert, questo si trasmette al livello superiore wxPythonsotto 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 cor-rispettivo 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, tuttidal significato piuttosto intuitivo.

182 Chapter 7. Appunti wxPython - livello avanzato

Page 191: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

wx.PyAssertionError è, in un certo senso, quanto di meglio wxPython può metterci a disposizione per sbirciarenel labirinto altrimenti inaccessibile del codice C++ sottostante. L’utilità di questo strumento, in ogni caso, è minoredi quanto si potrebbe sperare. In primo luogo, non è facile scoprire quando una particolare routine C++ potrebbeinnescare un assert: la documentazione di wxWidgets non è sempre così completa e accessibile. Ma più che altro, ilproblema è che non esiste una policy uniforme trasversale a tutto il codice wxWidget, su come e quando si dovrebberousare gli assert. E’ facile trovare incongruenze bizzarre.

Facciamo un esempio concreto (uno tra i mille possibili) che abbiamo già introdotto parlando di logging: che cosasuccede quando proviamo a caricare una wx.Bitmap con un file inesistente?

bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_ANY) # (1)bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP) # (2)

La prima versione non produce un assert, ma una scrittura di log (con wx.LogSysError che mostra un messaggiodi errore all’utente). La seconda versione non produce né assert né scritture di log (!). In entrambi i casi la creazionedell’oggetto wx.Bitmap va comunque a buon fine: potete scoprire solo a posteriori che c’è un problema, testandowx.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 testanowx.Bitmap.IsOk ed emettono assert se qualcosa non va. Per esempio, proviamo a usare una wx.Bitmap perprodurre 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’operazionefallisce silenziosamente, senza che nessun assert venga emesso, e ci viene mostrato un normale pulsante senza nessunaimmagine:

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 momentodi aggiungere il tool (con wx.ToolBar.AddTool), niente accade. Quando però finalizziamo la creazione dellatoolbar (wx.ToolBar.Realize), allora viene emesso un assert se uno dei tool è “sbagliato”... ma naturalmente aquesto 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()except PyAssertionError:

print 'presa al volo!'

L’elenco delle eccentricità potrebbe continuare a lungo. wx.PyAssertionError è uno strumento indubbiamenteutile, che però non può essere usato con la stessa disinvoltura con cui un programatore Python è abituato a usare le sueeccezioni. A noi verrebbe naturale scrivere codice più o meno così:

try:bmp = wx.Bitmap('non_esiste.bmp', type=wx.BITMAP_TYPE_BMP)

except IOError:...

7.10. Gestione delle eccezioni in wxPython (2a parte). 183

Page 192: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Questo è l’approccio “Easier to Ask for Forgiveness Than Permission” tipico di Python. Ma quando avete a che farecon wxWidgets dovete spesso adeguarvi al principio opposto “Look Before You Leap” del mondo C/C++. In questocaso, tutto considerato, sarebbe probabilmente più saggio verificare se il file esiste davvero, prima di caricarlo nellawx.Bitmap.

A parte queste considerazioni, vale infine la pena di ricordare che wx.PyAssertionError ha comunque gli stessilimiti di tutte le eccezioni Python nell’ecosistema wxPython: in particolare, deve essere intercettato nello stesso“strato” di codice Python in cui viene emesso.

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 chenon esiste più in quando è stato distrutto. Abbiamo già visto come, entro certi limiti, possa servire per testare sela 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 linguag-gio 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 (preferi-bilmente 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 variabileche 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 diPython ne programma la distruzione. Ma distruggere il proxy Python non ha nessun effetto sul corrispondenteoggetto C++, che quindi resta in vita.

Approfondiamo ciascuno di questi due casi separatamente.

Todo

una pagina su SWIG e l’oop Python/C++.

184 Chapter 7. Appunti wxPython - livello avanzato

Page 193: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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”: aveteun 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 diwxPython (per esempio gli id, o usando la catena dei parent); dall’altro, se il widget è visibile, l’utente ha semprela possibilità di intervenire: per esempio chiudere una finestra lasciata aperta. Nell’esempio che segue, ogni clic sulpulsante crea un nuovo wx.Frame (invisibile!): l’oggetto Python viene sempre cancellato, non appena la variabileframe esce dallo “scope” del callback on_clic. Tuttavia gli oggetti C++ che restano in vita in questo caso sonofigli 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 qualsiasimomento e anche indipendentemente da noi: basta che l’utente faccia clic sul pulsante di chisura di una finestra, enon 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 ilframe figlio. Come abbiamo già detto nella pagina sulla chiusura dei widget potete sfruttare questa eccezione pertestare se un widget esiste ancora, catturandola in un blocco try/except.

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 in-tercettare un evento nell’intervallo compreso tra la prima chiamata a wx.Window.Destroy e l’effettiva distruzionedi 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):

7.10. Gestione delle eccezioni in wxPython (2a parte). 185

Page 194: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 lafinestra, quando gli eventi spuri emessi dal wx.TreeCtrl provocano dei tentativi di accesso a un wx.TextCtrlche nel frattempo è stato già distrutto (per l’analisi completa del motivo di tutto ciò, dovete leggervi la pagina dedi-cata).

In ogni caso, abbiamo già anche visto la soluzione: tutte le volte che un evento potrebbe essere intercettato nel mezzodel processo di chiusura, potete evitare gli eventuali wx.PyDeadObjectError semplicemente testando la finestraper 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()

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 (parametridel tipo sbagliato etc.), ottenete una eccezione Python (che si comporta come di consueto in wxPython: non ferma ilprogramma, etc.).

Queste eccezioni sono originate dalle “%typemap” di SWIG: in sostanza, meccanismi di traduzione che convertono iltype 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 automaticiche, 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 ValueErrorwx.Colour(100, 'ops', 100) # restituisce ValueErrorwx.Colour(100, 500, 100) # restituisce OverflowError... naturalmente!

Esperienze del genere scoraggiano un po’ chi è abituato alla comodità di try/except. Non c’è dubbio che pro-grammando in wxPython bisogna adeguarsi all’approccio “Look Before You Leap” tipico del mondo C/C++. Tuttaviaanche le eccezioni Python si possono usare con successo: l’importante, come sempre, è non lesinare mai con gli unittest.

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.

186 Chapter 7. Appunti wxPython - livello avanzato

Page 195: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

• Ci sono pochi casi in cui forse vi conviene usare il framework di logging di wxPython (wx.Log etc.): quandol’applicazione è così semplice da non avere una logica di business “pure-Python” separata; o magari quandoscegliete di usare il log in modo intensivo per mostrare messaggi all’utente. Eventualmente, valutate se usarewx.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é wxPythonne fa uso in due modi:

• wxPython emette un wx.LogFatalError prima di chiudere l’applicazione, in caso di errore talmente graveda compromettere il funzionamento del motore di wxWidget: questo comportamento non è modificabile innessun modo;

• wxPython emette un wx.LogSysError (e di default mostra un messaggio all’utente) quando incontra unerrore interno non fatale. Potete (e dovreste, in effetti) reagire a questi errori scrivendo un log target personal-izzato: quando intercettate il messaggio, potete inviarlo al normale log Python; mostrare o meno un messaggioall’utente; chiudere l’applicazione, e così via.

• Un log target personalizzato non è uno strumento molto preciso, ma è il meglio che potete fare per intercettaregli 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 resti-tuisce sotto forma di wx.PyAssertionError. Potete intercettare questa eccezione in un normale bloccotry/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 condizionisiano tutte corrette, e soltanto allora chiamate l’api. Se fate così, tutte le eccezioni wx.PyAssertionErrorche dovessero ancora verificarsi sarebbero bachi imprevisti: catturatele nel vostro “hook acchiappa-tutto” (vedisotto) 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 YouLeap”: 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 acchiappa-tutto” edebuggate.

• 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 delsegmento di codice Python da cui sono originate. Un’eccezione Python non catturata è, ancora una volta, unbaco: in wxPython però l’applicazione non termina come al solito, e questo è un problema grave. Il meglio chepotete 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 im-mediatamente il programma, potrebbero avere effetti nascosti molto gravi. Il meglio che potete fare è sovrascri-vere sys.excepthook con un vostro “hook acchiappa-tutto”: quando catturate in questo modo un’eccezionenon gestita, dovreste senz’altro scriverla nel log. Potete eventualmente mostrare un messaggio all’utente, echiudere voi stessi l’applicazione.

• Tutte queste raccomandazioni (log target personalizzati, sys.excepthook sovrascritti, etc.) valgono soloquando il vostro programma è in produzione. In fase di sviluppo, naturalmente, volete invece che gli errorisaltino fuori nel modo più appariscente possibile. Una buona strategia potrebbe essere scrivere due versioniseparate della vostra wx.App (o almeno, due versioni del suo wx.App.OnInit e wx.App.OnExit), dausare in ambiente di sviluppo e in produzione.

7.10. Gestione delle eccezioni in wxPython (2a parte). 187

Page 196: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Pattern: Publisher/Subscriber.

Affrontiamo in questa pagina un design pattern di grande importanza nel mondo delle applicazioni gui: Pub-lisher/Subscriber (pub/sub, per brevità).

Questa pagina è un anello di congiunzione tra quelle, numerose, che abbiamo dedicato agli eventi, e quella sul patternModel/Controller/View. E’ consigliabile proseguire nella lettura solo se conoscete abbastanza bene il modello a eventidi 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 è questala sede per introdurre la filosofia dei pattern, spiegare cosa sono e quali problemi aiutano a risolvere. In rete e in libreriasi può trovare moltissimo: qui diamo per scontato che sappiate orientarvi già a sufficienza nella programmazione aoggetti.

Che cosa è pub/sub.

Detto molto in breve, pub/sub è un pattern per implementare un sistema di messaggistica molti-a-molti tra un numeroarbitrario di oggetti. “Molti-a-molti” significa che ciascun oggetto può sia inviare messaggi a, sia ricevere messaggida, potenzialmente tutti gli altri oggetti.

Un “messaggio” è in definitiva la chiamata a una funzione (metodo) di un oggetto remoto. Di norma, affinché unoggetto B possa chiamare un metodo di un altro oggetto A, è necessario che B “conosca” A: ovvero, che conservi unriferimento 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 messaggitra oggetti senza che abbiano necessità di “conoscersi” tra loro, aiutando a mantenere quel disaccoppiamento tra leparti 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 ciascunodi loro. Quando l’intermediario riceve un messaggio, lo inoltra a tutti gli oggetti del suo elenco. Se un oggetto vuolepartecipare al sistema dei messaggi, si iscrive presso l’intermediario. Tutto ciò che i partecipanti hanno bisogno diconoscere, è 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:

188 Chapter 7. Appunti wxPython - livello avanzato

Page 197: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

1 class Publisher(object):2 def __init__(self):3 self._subscribers = set()4

5 def subscribe(self, new_subscriber):6 self._subscribers.add(new_subscriber)7

8 def unsubscribe(self, old_subscriber):9 self._subscribers.discard(old_subscriber)

10

11 def publish(self, message):12 for subscriber in self._subscribers:13 subscriber(message)14

15 class Foo(object): # un subscriber16 def __init__(self, name, publisher):17 self.name = name18 self._publisher = publisher19

20 def make_subscription(self, subscribe=True):21 if subscribe:22 self._publisher.subscribe(self.deal_with_incoming_message)23 else:24 self._publisher.unsubscribe(self.deal_with_incoming_message)25

26 def say_hello(self):27 self._publisher.publish('Hello world da... ' + self.name)28

29 def deal_with_incoming_message(self, message):30 print 'Sono', self.name, 'e ho ricevuto:', message31

32 pub = Publisher()33 andrea = Foo('Andrea', pub)34 mario = Foo('Mario', pub)35 andrea.make_subscription()36 mario.make_subscription()37 andrea.say_hello()

Potete fare un po’ di prove con questo giocattolo, aggiungendo altre istanze di Foo, o scrivendo altre classi da aggiun-gere al sistema di messaggistica. In realtà ci sono poche regole: la “sottoscrizione” consiste in sostanza nel fornireall’intermediario un riferimento a un metodo da chiamare ogni volta che bisogna consegnare un messaggio (nel nos-tro caso, deal_with_incoming_message). Un dettaglio importante è la signature del metodo da usare per lasottoscrizione: 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 chiamatasubscriber(message)).

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 moltoimportante 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 indipen-dente dal resto del framework, e quindi si può usare anche in progetti non legati al mondo wx (si può anche scaricaree installare a parte: la documentazione completa si trova sul sito).

Per lavorare con questa versione di pub/sub, basta importare:

7.11. Pattern: Publisher/Subscriber. 189

Page 198: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

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. Questoci risparmia la noia di creare noi stessi una prima istanza, e poi passarla in giro (nel nostro esempio di sopra, inFoo.__init__). Addirittura, l’api di wx.lib.pubsub nasconde completamente la classe Publisher: bastaimportare 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 fun-zionalità. 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), potetecomunque 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. Questoci risparmia il disturbo di cancellare la sottoscrizione di un oggetto, prima di distruggerlo (altrimenti il riferimentoesistente dentro la lista degli abbonati non ci permetterebbe di distruggerlo!). Ancora meglio, quando distruggiamo unoggetto 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 unmessaggio, 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 ciascuncomponente 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 versionedi wxPython, ancora da completare) è invece aggiornata. La documentazione migliore, tuttavia, è nel sito stesso dipubsub.

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 lostato 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 pub2

3 TOPIC_VALUE_UPDATED = 'value-updated'4

5 class Test(wx.Frame):6 def __init__(self, *a, **k):7 wx.Frame.__init__(self, *a, **k)8 p = wx.Panel(self)9 self.slider = wx.Slider(p, -1, 50, 0, 100,

10 style=wx.SL_VERTICAL)11 check = wx.CheckBox(p, -1, 'connesso')12 button = wx.Button(p, -1, 'nuovo')13

14 self.slider.Bind(wx.EVT_SLIDER, self.on_slider)

190 Chapter 7. Appunti wxPython - livello avanzato

Page 199: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

15 check.Bind(wx.EVT_CHECKBOX, self.on_check)16 button.Bind(wx.EVT_BUTTON, self.on_clic)17

18 s = wx.BoxSizer(wx.VERTICAL)19 for ctl in (self.slider, check, button):20 s.Add(ctl, 0, wx.ALIGN_CENTRE_HORIZONTAL|wx.ALL, 20)21 p.SetSizer(s)22 s.Fit(self)23

24 pub.subscribe(self.update_value, TOPIC_VALUE_UPDATED)25 check.SetValue(True)26

27 def update_value(self, message):28 self.slider.SetValue(message)29

30 def on_slider(self, evt):31 pub.sendMessage(TOPIC_VALUE_UPDATED,32 message=self.slider.GetValue())33

34 def on_check(self, evt):35 if evt.IsChecked():36 pub.subscribe(self.update_value, TOPIC_VALUE_UPDATED)37 else:38 pub.unsubscribe(self.update_value, TOPIC_VALUE_UPDATED)39

40 def on_clic(self, evt):41 Test(self).Show()42

43 if __name__ == '__main__':44 app = wx.App(False)45 Test(None).Show()46 app.MainLoop()

Per mantenere più compatto il codice, qui le finestre da sincronizzare sono in realtà istanze diverse dalla stessa classeTest: 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 inrealtà qualsiasi oggetto python (nel nostro caso trasmettiamo un semplice valore numerico, ma nulla vieta di passarestrutture dati più complesse). Un “topic” è una semplice stringa di testo: per praticità, conviene astrarre i topic invariabili globali dichiarate all’inizio del modulo, soprattutto quando i topic cominciano a diventare numerosi. Unagerarchia 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 documen-tazione.

Più interessante è capire come wx.lib.pubsub implementa in pratica il pattern pub/sub. Come si vede, ognicomponente può abbonarsi e cancellare l’abbonamento in qualsiasi momento, a run-time. Il sistema è completamenteindifferente a quali e quanti componenti sono abbonati. Ciascun componente è in grado di trasmettere e ricevere. Uncomponente potrebbe trasmettere di volta in volta messaggi con topic diversi. Al limite, nulla vieta di abbonare lostesso metodo per l’ascolto di diversi topic (ma questa non è una buona pratica: conviene riservare metodi separati perl’ascolto di topic separati. Oppure, se si è interessati all’ascolto di più topic, conviene raggrupparli in una gerarchia).

Messaggi pub/sub ed eventi wxPython.

Pub/sub è un pattern che permette di scambiare messaggi tra componenti. Questa è però anche la funzione svolta dalsistema degli eventi, a cui abbiamo dedicato molte pagine. Un evento è simile a un messaggio pub/sub nel senso che siorigina in un componente, e provoca la chiamata a un metodo remoto (callback) precedentemente collegato mediante

7.11. Pattern: Publisher/Subscriber. 191

Page 200: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

una registrazione (è il compito di Bind, come sappiamo). Proprio l’esempio qui sopra ci permette di approfondirealcune 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 deiparent. Nel nostro esempio, ogni nuova finestra è figlia di quella in cui abbiamo cliccato sul pulsante “nuovo”, e quindisi possono facilmente creare anche finestre “cugine”: tuttavia, i messaggi si trasmettono indifferentemente non sololungo la linea genitori/figli, ma anche ai cugini più lontani. Per contrasto, si ricordi invece che gli eventi si propaganosolo 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ò iscriversiall’ascolto dei messaggi di tutti gli altri, e mandare a sua volta messaggi a tutti. Per contrasto, ricordiamo che glieventi 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 possonointercettarlo in successione, ma solo se gli event handler da cui sono chiamati appartengono alla catena dei parent, esolo 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 digestire i messaggi tra i componenti di un’applicazione gui. La domanda spontanea è: ma non sarebbe possibile faredel 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, siamocomunque 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 porzionedi interfaccia deve essere ridisegnata (wx.EVT_UPDATE_UI); per segnalare che le dimensioni della finestra stannocambiando (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 appostaper conservare molte informazioni sul contesto che li ha generati. Un messaggio di wx.lib.pubsub, di per sé, nonconserva neppure il riferimento all’oggetto da cui è partito. Naturalmente anche con pub/sub si potrebbe implementareuna gerarchia di classi “messaggio”, istanziare la classe appropriata, riempirla delle necessarie informazioni, e infinetrasmetterla 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ù desiderabilenel contesto di una applicazione gui. C’è un motivo per cui solo i wx.CommandEvent si propagano, e si propaganosolo lungo la catena dei parent: nel 90% dei casi è proprio quello che vi serve. Le tipiche interfacce grafiche sonoorganizzate in finestre che contengono panel che contengono pulsanti e altri widget: il rispetto di questa gerarchia vipermette di evitare che componenti estranei possano essere disturbati da messaggi che non li riguardano. E’ facile ecomodo contenere i messaggi degli eventi in flussi separati. Naturalmente anche i topic di wx.lib.pubsub hannouna funzione analoga: tuttavia, bisognerebbe costruire una gerarchia di topic elastica, che si adatti alla creazione edistruzione di nuove finestre, alla disattivazione occasionale di parti dell’interfaccia, etc. E ancora una volta, tuttoquesto andrebbe implementato a partire da zero.

192 Chapter 7. Appunti wxPython - livello avanzato

Page 201: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Infine, vale la pena di ricordare che wx.lib.pubsub non dà nessuna garanzia di recapitare un messaggio ai suoidestinatari in un ordine predefinito. Gli eventi, d’altra parte, vengono processati nell’ordine determinato dalla catenadei parent; ed è possibile manipolare ulteriormente lo stack degli handler, come sappiamo.

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 applicazionigui. 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 diwxWidgets).

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 puntoche “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 frame-work implementano questo tipo di comunicazioni grazie ai callback. (...) I callback hanno due problemifondamentali: (...) In secondo luogo, il callback è fortemente accoppiato alla funzione chiamante, dalmomento che questa deve conoscere il callback da chiamare.

Il bersaglio principale, qui, è naturalmente wxWidgets. “Callback” è un termine generico, e inoltre è difficile tradurrein 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 accoppi-amento 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 questatrasformazione è stato proprio wxPython con il suo Bind, che è stato introdotto da Robin Dunn e solo in seguitoimplementato anche nella versione “madre” del framework (a partire da wxWidgets 2.9). In effetti, un binder svolgeuna funzione un po’ simile a quella di un “Publisher” in pub/sub: è un oggetto mediatore che permette di disaccoppiareeventi, 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 usarelo stesso modello per gestire gli eventi nativi dell’interfaccia (clic sui pulsanti, etc.) e per qualsiasi altro messaggiooccorre scambiare tra i componenti. Dall’altro, wxWidgets mette a disposizione un sistema più strutturato e adeguatoalle normali esigenze degli eventi nativi. Quando però c’è bisogno di qualcosa di diverso, occorre integrare in altrimodi.

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 tradizionaleBind.

Event Manager non offre comunque tutta la libertà di pub/sub. In effetti, non è facile replicare esattamente la funzion-alità del nostro esempio con gli slider sincronizzati, usando solo Event Manager. Una possibile approssimazione, checi mostra comunque degli aspetti interessanti, è questa:

1 from wx.lib.evtmgr import eventManager2

3 class Test(wx.Frame):

7.11. Pattern: Publisher/Subscriber. 193

Page 202: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

4 def __init__(self, *a, **k):5 wx.Frame.__init__(self, *a, **k)6 p = wx.Panel(self)7 self.slider = wx.Slider(p, -1, 50, 0, 100,8 style=wx.SL_VERTICAL)9 check = wx.CheckBox(p, -1, 'connesso')

10 button = wx.Button(p, -1, 'nuovo')11

12 parent = self.GetParent()13 self.target = parent.slider if parent else self.slider14

15 eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)16 # usiamo il vecchio Bind per gli altri eventi di routine17 # ma potremmo usare Event Manager per tutti18 check.Bind(wx.EVT_CHECKBOX, self.on_check)19 button.Bind(wx.EVT_BUTTON, self.on_clic)20

21 s = wx.BoxSizer(wx.VERTICAL)22 for ctl in (self.slider, check, button):23 s.Add(ctl, 0, wx.ALIGN_CENTRE_HORIZONTAL|wx.ALL, 20)24 p.SetSizer(s)25 s.Fit(self)26

27 check.SetValue(True)28

29 # confronto: EventMananger | pub/sub30 def update_value(self, evt): # def update_value(self, message):31 self.slider.SetValue(evt.GetInt()) # self.slider.SetValue(message)32

33 def on_check(self, evt):34 if evt.IsChecked():35 eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)36 else:37 eventManager.DeregisterListener(self.update_value)38

39 def on_clic(self, evt):40 Test(self).Show()41

42 if __name__ == '__main__':43 app = wx.App(False)44 Test(None).Show()45 app.MainLoop()

E’ interessante confrontare il modo in cui comunichiamo il valore da assegnare allo slider: con pub/sub, il valore è ilcontenuto del messaggio. Con Event Manager, d’altra parte, trasferiamo pur sempre degli eventi wxPython, e quindirecuperiamo 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 fines-tra “conosce” il suo parent diretto, e quindi il “target” è lo slider del parent (tranne per la finestra iniziale, che non hanessun parent). Questo però limita la trasmissione dell’evento alla catena dei parent: gli slider non restano sincroniz-zati tra finestre “cugine”. Per ottenere una sincronizzazione completa, dovremmo gestire noi stessi la contabilità dellefinestre aperte in qualche tipo di registro globale, magari da conservare nella wx.App - ma allora, tanto vale ricorreredirettamente a pub/sub.

Anche se Event Manager non permette tutta la flessibilità di pub/sub, offre comunque qualcosa in più rispetto aBind: in particolare, con Event Manager è possibile registrare più ascoltatori per un singolo evento. Potete verificarlocon l’esempio qui sopra: se generate diverse finestre figlie da una stessa finestra, noterete che un solo parent è in

194 Chapter 7. Appunti wxPython - livello avanzato

Page 203: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

grado di “comandare” tutti i figli contemporaneamente. Questo non sarebbe possibile collegando l’evento nel modotradizionale: potete verificarlo cambiando una riga nel codice dell’esempio qui sopra:

# sostituite questo...eventManager.Register(self.update_value, wx.EVT_SLIDER, self.target)# ... 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 unascappatoia. 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. Tuttaviaquesta tecnica è valida solo per questo esempio particolare. Se volessimo registrare, per uno stesso evento, moltiascoltatori non legati da nessuna particolare parentela, Bind non potrebbe più aiutarci, ed Event Manager sarebbel’unica soluzione (oltre a pub/sub, naturalmente).

A conti fatti, Event Manager non è probabilmente lo strumento giusto da usare quando volete notificare un evento adascoltatori arbitrariamente lontani e generati dinamicamente. Se vi serve davvero questo tipo di flessibilità, probabil-mente 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 servenotificare 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 eventManager2

3 class MySlider(wx.Slider):4 def __init__(self, parent, id, value, minValue, maxValue, target):5 wx.Slider.__init__(self, parent, id, value, minValue,6 maxValue, style=wx.SL_VERTICAL)7 eventManager.Register(self.update, wx.EVT_SLIDER, target)8

9 def update(self, evt):10 self.SetValue(evt.GetInt())11

12 class Test(wx.Frame):13 def __init__(self, *a, **k):14 wx.Frame.__init__(self, *a, **k)15 p = wx.Panel(self)16 master_slider = wx.Slider(p, -1, 50, 0, 100,17 style=wx.SL_VERTICAL)18 s = wx.BoxSizer(wx.HORIZONTAL)19 for i in range(10):20 slider = MySlider(p, -1, 50, 0, 100, master_slider)21 s.Add(slider, 1, wx.EXPAND|wx.ALIGN_CENTRE_HORIZONTAL, 10)22 s1 = wx.BoxSizer(wx.VERTICAL)23 s1.Add(master_slider, 1, wx.ALIGN_CENTRE_HORIZONTAL, 10)24 s1.Add(s, 1, wx.EXPAND|wx.ALL, 5)25 p.SetSizer(s1)26

27 if __name__ == '__main__':28 app = wx.App(False)29 Test(None).Show()30 app.MainLoop()

7.11. Pattern: Publisher/Subscriber. 195

Page 204: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

In conclusione...

wxPython mette a disposizione un insieme articolato (anche se obiettivamente disarmonico) di strumenti per gestire lacomunicazione 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 avvienetipicamente quando sottoclassate un widget per personalizzarlo; ma è comune anche usare questo metodo per inviaremessaggi personalizzati non-nativi. Inoltre, wx.PostEvent è una delle due tecniche classiche per la comunicazioneinter-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 ese-cuzione dei callback.

Se avete bisogno che diversi widget possano rispondere allo stesso evento, la semplice accoppiata di Bind e Skipdovrebbe 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 avetevoglia 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 natividell’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à sempreibrida: spetta a voi capire fino a che punto usare pub/sub, e quando invece vi conviene lasciare le cose in mano allapropagazione degli eventi. Inoltre, un messaggio pub/sub è di per sé meno strutturato di un evento wxPython: è pos-sibile che dobbiate definire delle api precise per dare una struttura ai vostri messaggi. Anche la strategia dei topic diwx.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, dovetesempre inserirlo in un wx.CallAfter (ma in ogni caso, usare pub/sub per comunicare tra i thread è una strategiaun po’ avventurosa).

In conclusione, l’importante è capire che tutti questi strumenti, usati in modo intelligente, vi aiutano nel fondamentalecompito di mantenere disaccoppiati i componenti, e separare gli ambiti di interesse delle varie sezioni della vostraapplicazione. Questo è il cuore della programmazione a oggetti, e in particolare del pattern Model/Controller/View dicui parliamo in una pagina separata.

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 centinaiadi 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 soffermer-emo nel dettaglio su ciascuna di esse: per questo basta ricorrere alla documentazione ufficiale.

196 Chapter 7. Appunti wxPython - livello avanzato

Page 205: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Note: Per non sovraccaricare l’indice di riferimenti inutili, non vi troverete le funzioni elencate in questa pagina.

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 classewxPython. E’ facile riconoscerli, perché il loro nome segue la convenzione [ClassName]_[MethodName]. Unelenco 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 noioggi diamo per scontate. Nei suoi primi anni di vita, wxPython “traduceva” gli occasionali static method delle classic++ 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 conl’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 siaprecedentemente istanziata).

L’unica eccezione a questa regola è wx.Thread_IsMain, che può essere usata solo come funzione globale perché laclasse 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 larghissimaparte 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 meth-ods” di c++.

wxWidgets fa uso pervasivo, infatti, di metodi costruttori (ma anche molti metodi setter) che possono accettareparametri diversi. Molto spesso in wxPython questi “overloaded methods” sono tradotti con diversi metodi sepa-rati, 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 corrispondonoall’unico “overloaded method” wxWindow::SetSize di wxWidgets.

Questa complicazione sta per essere finalmente superata nella nuova incarnazione di wxPython, il cosiddetto “progettoPhoenix” 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 esposticome 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, altrespariranno proprio), e quindi non avrebbe senso fornire qui un elenco completo. Di seguito elenchiamo comunquetutte le funzioni globali disponibili in wxPython “classico”, segnalando quelle che sono destinate a diventare obsoletein Phoenix.

7.12. Un tour delle funzioni globali di wxPython. 197

Page 206: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Funzioni Pre[WidgetName] per la two-step creation.

Un altro blocco molto numeroso è quello delle funzioni globali che iniziano con Pre. Queste sono funzioni diconvenienza che generano un “pre-widget” utilizzato per la cosiddetta “two-step creation”. Si tratta di una tecnica unpo’ 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.

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 espostecome 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 nellalibreria standard di Python. Ma occorre sempre ricordare che wxPython è in circolazione da molto più tempo...

Logging.

wxPython mette a disposizione un sotto-sistema per gestire il logging, centrato sulla classe wx.Log. Per usare illogging 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 standarddi Python. E’ vero però che wx.Log è più integrato nella logica wxWidgets sottostante a wxPython, e potrebbe forniremessaggi di errore più completi per i problemi innescati strettamente all’interno del codice wxWidgets.

Drag & Drop.

Le operazioni di Drag & Drop (in sostanza, una forma particolare di copia e incolla) in wxPython sono affidate alleclassi wx.DropSource e wx.DropTarget e ai loro metodi. Alcune funzioni globali integrano delle funzionalitàin questo campo:

• CustomDataFormat, DragIcon, DragListItem, DragString, DragTreeItem,IsDragResultOk

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 conve-nienza ottimizzata per il trascinamento delle immagini, utile soprattutto in ambiente Windows). Queste funzioni, come

198 Chapter 7. Appunti wxPython - livello avanzato

Page 207: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

si intuisce, corrispondono a un costruttore “overloaded” nella corrispondente classe c++. In Phoenix non dovrebberopertanto 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.

Finding.

Alcune funzioni servono semplicemente per cercare un widget:

• FindWindowAtPoint, FindWindowAtPointer, FindWindowById, FindWindowByLabel,FindWindowByName, GenericFindWindowAtPoint

L’esistenza di queste funzioni è più facilmente spiegabile nell’ambito c++ di wxWidgets. In Python, dove i rifer-imenti agli oggetti possono essere passati liberamente come argomenti di funzioni, l’utilità di meccanismi del tipoFindWindowBy... è praticamente nulla (è un discorso simile a quello che abbiamo già fatto per gli id). Occasion-almente potreste invece trovare qualche utilità nelle funzioni del tipo FindWindowAtPoint[er], per esempio selavorate 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.

Scorciatoie per vari dialoghi.

Alcune funzioni creano e restituiscono istanze già pronte di varie sotto-classi specializzate di wx.Dialog, oppureusano queste per ottenere input dall’utente e restituiscono direttamente il risultato dopo aver chiuso e distrutto ildialogo:

• 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.

Costruttori di sizer item.

Come sappiamo, un wx.SizerItem è un elemento di un sizer. In genere otteniamo una istanza di questa classe comevalore di ritorno di wx.Sizer.Add, e nella pratica quotidiana non abbiamo mai bisogno di istanziare direttamentené wx.SizerItem né wx.GBSizerItem (la sottoclasse specializzata per i wx.GridBagSizer).

7.12. Un tour delle funzioni globali di wxPython. 199

Page 208: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Ancor meno bisogno, quindi, abbiamo di queste funzioni globali che restituiscono un wx.[GB]SizerItem:

• GBSizerItemSizer, GBSizerItemSpacer, GBSizerItemWindow, SizerItemSizer,SizerItemSpacer, SizerItemWindow

Inoltre, come è facile intuire, si tratta di funzioni globali che corrispondono a costruttori “overloaded” delle corrispon-denti classi c++. In Phoenix niente di tutto questo dovrebbe essere più necessario.

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 nondovrebbero più servire in Phoenix:

• FFont, FFontFromPixelSize, Font2 (un alias di FFont), FontFromNativeInfo,FontFromNativeInfoString, FontFromPixelSize

Infine, GetNativeFontEncoding e TestFontEncoding sono relitti del vecchio sistema di supporto degliencoding di wxWidgets, che in wxPython è completamente superato.

Todo

una pagina sui font

Primitive per il disegno.

Queste funzioni possono essere usate per ricavare informazioni sulla geometria del display (lo schermo o l’area dilavoro):

• 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 (eprobabilmente 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

200 Chapter 7. Appunti wxPython - livello avanzato

Page 209: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

una pagina su come disegnare

Immagini e colori.

Molte funzioni globali lavorano con le immagini. Per iniziare, molte sono funzioni di conversione, e hanno laforma [someClass]From[someClass]. In Phoenix dovrebbero essere tutte soppresse o quasi: in molti casibasterà usare il costruttore “overloaded” (per esempio, wx.BitmapFromImage(image) sarà deprecata in favoredi 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 didefault quando si crea la wx.App).

Todo

una pagina sulle immagini: wx.Image, wx.Bitmap...

Eventi, thread di esecuzione.

Abbiamo dedicato ormai molte pagine agli eventi, e non c’è quindi più bisogno di spendere parole a proposito diqueste 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 diPython, anche wxMutex di wxWidgets è assente in wxPython. Queste due funzioni globali sono ancora in giro perretro-compatibilità:

• MutexGuiEnter, MutexGuiLeave

Infine, alcune funzioni per “dormire”:

• MicroSleep, MilliSleep, Sleep, Usleep

7.12. Un tour delle funzioni globali di wxPython. 201

Page 210: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Todo

una pagina sui thread

Informazioni sul sistema.

wxPython è dotato di alcuni strumenti per ottenere informazioni sull’ambiente in cui deve operare: per esempio laclasse wx.PlatformInfo, ma anche alcune funzioni globali che elenchiamo qui di seguito. Spesso si tratta distrumenti che possono essere sostituiti con successo da moduli come sys e os nella libreria standard di Python. Maalcune 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

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 debuggerassociato 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 stessoambito, ma sarà deprecata in Phoenix e peraltro non è mai stata particolarmente utile.

EnableTopLevelWindows può essere usata come valvola di sicurezza per congelare temporaneamente tutta lagui. Per esempio, è usata internamente da wx.SafeYeld.

GetTranslation riguarda il supporto per il testo multilingue.

202 Chapter 7. Appunti wxPython - livello avanzato

Page 211: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

FileTypeInfoSequence e NullFileTypeInfo creano un wx.FileTypeInfo (c’entra il supporto MIMEdi 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.

7.12. Un tour delle funzioni globali di wxPython. 203

Page 212: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

204 Chapter 7. Appunti wxPython - livello avanzato

Page 213: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 8

Ricette wxPython

Catturare tutti gli eventi di un widget.

Come abbiamo visto parlando degli eventi, non è facile in generale sapere quali eventi può emettere un determinatowidget.

Questa ricetta cerca di scoprirlo con un approccio naif: semplicemente collega a un widget tutti i binder disponibilinel 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 innescatidi continuo, e producono “rumore di fondo”: vi conviene eliminarli. Alle righe 23-25 ne ho eliminati alcuni che trovoparticolarmente 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 (righe41-45).

1 import wx2

3 class TopFrame(wx.Frame):4 def __init__(self, *a, **k):5 wx.Frame.__init__(self, *a, **k)6 p = wx.Panel(self)7 # qui mettete il widget che volete testare, al posto del wx.Button8 test_widget = wx.Button(p, -1, 'test me!')9

10 self.output = wx.TextCtrl(p, -1, style=wx.TE_MULTILINE|wx.TE_READONLY)11 s = wx.BoxSizer(wx.VERTICAL)12 s.Add(test_widget, 1, wx.EXPAND|wx.ALL, 5)13 s.Add(self.output, 1, wx.EXPAND|wx.ALL, 5)14 p.SetSizer(s)15

16 binder_names = [i for i in dir(wx) if i.startswith('EVT_')]17 # questi due non si possono collegare con Bind:18 binder_names.remove('EVT_COMMAND')

205

Page 214: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

19 binder_names.remove('EVT_COMMAND_RANGE')20

21 # qui rimuovete gli eventi che non volete registrare:22 # per es. questi si ripetono di continuo e producono rumore di fondo23 for b in ('EVT_IDLE', 'EVT_UPDATE_UI', 'EVT_UPDATE_UI_RANGE',24 'EVT_MOTION', 'EVT_SET_CURSOR'):25 binder_names.remove(b)26

27 self.binder_dict = {}28

29 for binder_name in binder_names:30 obj_binder = getattr(wx, binder_name)31 if len(obj_binder.evtType) == 1:32 test_widget.Bind(obj_binder, self.callback)33 binder_type = obj_binder.evtType[0]34 self.binder_dict[binder_type] = binder_name35

36 self.last_printed_event = ''37

38 def callback(self, evt):39 evt.Skip()40 evt_type = self.binder_dict[evt.GetEventType()]41 if evt_type != self.last_printed_event:42 txt = '\n%s %s' % (evt.__class__.__name__, evt_type)43 self.last_printed_event = evt_type44 else:45 txt = '-'46 self.output.AppendText(txt)47

48

49 app = wx.App(False)50 TopFrame(None, title='Event Test').Show()51 app.MainLoop()52

Questa ricetta è uno script che avevo scritto per capire meglio come vengono generati gli eventi. In seguito mi sonoaccorto che nella libreria di wxPython è già compresa una versione molto più “professionale” e completa della stessaidea, nel modulo wx.lib.eventwatcher.py. Se lo eseguite, avvia una demo che mostra gli eventi di un framedi 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.

206 Chapter 8. Ricette wxPython

Page 215: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 eventipersonalizzati.

Il suo funzionamento è semplice: permette di selezionare un periodo (per esempio un trimestre), e restituisce gliestremi del periodo come datetime.date.

1 import datetime2 import wx3 import wx.lib.newevent4

5 PeriodEvent, EVT_PERIOD_MODIFIED = wx.lib.newevent.NewCommandEvent()6

7 class PeriodWidget(wx.Panel):8 """Un widget per selezionare periodi fissi. Chiamarlo con l'argomento9 aggiuntivo "period" imposastato a 1 (per selezionare mesi) oppure

10 2 (bimestri), 3 (trimestri), 4 (quadrimestri), o 6 (semestri)."""11 months = 'January February March April May June July August September October

→˓November December'.split()12 spans = ',, bimester, trimester, quadrimester,, semester'.split(',')13 def __init__(self, *a, **k):14 self.period = k.pop('period')15 wx.Panel.__init__(self, *a, **k)16 if self.period == 1:17 ch = self.months18 else:19 ch = [str(i)+self.spans[self.period] for i in range(1, (12/self.

→˓period)+1)]20 self.choose_period = wx.ComboBox(self, -1, choices=ch,21 style=wx.CB_DROPDOWN|wx.CB_READONLY)22 self.choose_period.SetSelection(0)23 self.choose_year = wx.SpinCtrl(self, initial=datetime.datetime.now().year,24 min=1800, max=2200, size=((80, -1)))25 s = wx.BoxSizer()26 s.Add(self.choose_period, 1, wx.EXPAND|wx.RIGHT, 5)27 s.Add(self.choose_year, 0, wx.FIXED_MINSIZE|wx.LEFT, 5)28 self.SetSizer(s)29 self.choose_period.Bind(wx.EVT_COMBOBOX, self.on_changed)30 self.choose_year.Bind(wx.EVT_SPINCTRL, self.on_changed)31

32 def on_changed(self, evt):33 my_event = PeriodEvent(self.GetId())34 my_event.SetEventObject(self)35 self.GetEventHandler().ProcessEvent(my_event)36

37 def GetValue(self):38 start = datetime.date(39 self.choose_year.GetValue(),40 ((self.period * self.choose_period.GetSelection()) + 1),41 1)42 end = ((start +43 datetime.timedelta(days=(30*self.period)+15)).replace(day=1) -44 datetime.timedelta(days=1))45 return start, end46

47

48 class TestFrame(wx.Frame):

8.2. Un widget per selezionare periodi di tempo. 207

Page 216: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

49 def __init__(self, *a, **k):50 wx.Frame.__init__(self, *a, **k)51 period = PeriodWidget(self, period=1)52 period.Bind(EVT_PERIOD_MODIFIED, self.on_period)53

54 def on_period(self, evt):55 print evt.GetEventObject().GetValue()56

57

58 app = wx.App(False)59 TestFrame(None).Show()60 app.MainLoop()

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 soladifferenza è che, in risposta a un EVT_BUTTON (un normale clic), chiede all’utente di inserire una password, e soloin caso positivo passa a eseguire il callback associato.

La prima versione.

1

2 import wx3

4 # una funzione dummy per verificare la password5 def check_psw(psw):6 if psw == 'secret':7 return True8 return False9

10

11 class CheckPermissionButton(wx.Button):12 """Un wx.Button che chiede la password all'utente13 prima di procedere."""14 def __init__(self, *a, **k):15 wx.Button.__init__(self, *a, **k)16 self.Bind(wx.EVT_LEFT_UP, self.on_leftup)17

18 def on_leftup(self, evt):19 msg = 'Inserire la password per procedere:'20 cpt = 'Password richiesta.'21 if check_psw(wx.GetPasswordFromUser(msg, cpt)):22 wx.PostEvent(23 self.GetEventHandler(),24 wx.PyCommandEvent(wx.EVT_BUTTON.typeId, self.GetId()))25 else:26 wx.MessageBox('Password errata!', 'Password errata',27 wx.ICON_ERROR|wx.OK)28

29

30 class TestFrame(wx.Frame):31 def __init__(self, *a, **k):32 wx.Frame.__init__(self, *a, **k)33 p = wx.Panel(self)

208 Chapter 8. Ricette wxPython

Page 217: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

34 b = CheckPermissionButton(p, -1, 'clic me', pos=((50, 50)))35 b.Bind(wx.EVT_BUTTON, self.onclic)36

37 def onclic(self, evt):38 wx.MessageBox(39 "Password corretta, procedo con l'azione prevista.")40 # e qui il codice previsto del callback, come di consueto41

42

43 app = wx.App(False)44 TestFrame(None).Show()45 app.MainLoop()46

Ecco come si può manipolare la catena degli eventi per ottenere effetti un po’ più concreti degli esempi presentati neicapitoli dedicati agli eventi. Nel mondo reale, chiaramente, la funzione check_psw potrebbe essere sostituita da uncontrollo 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. Peresempio, si potrebbe facilmente scrivere un decoratore da applicare ai singoli callback che necessitano di un controllosulle 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 wx-Python.

Approfondiamo il problema.

Ciò detto, questa ricetta richiede qualche parola in più per spiegare lo strano giro che abbiamo dovuto fare, per ricevereun primo evento ed emetterne subito dopo un secondo.

Il problema generale, qui, è che è molto difficile organizzare una catena “ordinata” di callback in wxPython. Comeregola generale, se avete bisogno di inserire più di un callback in risposta a un evento, è meglio scrivere il codice inmodo 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 avvengaprima 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)

8.3. Un pulsante che controlla le credenziali prima di procedere. 209

Page 218: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 vieneeseguita 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 classemadre. 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 (cheprovoca l’esecuzione del codice privilegiato) ha il tempo di passare per l’handler della classe madre, che innesca ilcontrollo della password.

Tuttavia questa soluzione non è molto sicura: ci fidiamo del fatto che il codice cliente utilizzi lo stile “giusto” dibinding per collegare il suo evento, in modo da metterlo educatamente in coda dietro al nostro. In molti casi possiamoconviverci 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 unwx.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 neabbiamo approfittato per fare il nostro controllo di sicurezza.

Purtroppo però, una volta intercettato, quell’evento è “consumato”. Possiamo chiamare Skip, ma anche questo siriferisce solo al wx.EVT_LEFT_UP: non c’è modo di “far tornare al via” il successivo wx.EVT_BUTTON, per farlointercettare 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 funzionacome dovrebbe solo fintanto che il codice cliente si limita a intercettare wx.EVT_BUTTON. Ma se per qualche ragionevolesse intercettare anche lui wx.EVT_LEFT_UP (o peggio ancora, wx.EVT_LEFT_DOWN), saremmo di nuovo alproblema 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, chefortunatamente ha la caratteristica di emettere tre eventi in successione. Ma se volessimo generalizzare il problemacon un altro widget, che emette un solo evento per volta, saremmo punto e a capo.

La seconda versione.

Se questo trucco vi sembra un po’ troppo sporco, in effetti esiste una soluzione più definitiva: basta scrivere un handlerpersonalizzato, e spingerlo in cima allo stack degli handler. Nel nostro handler gestiremo l’evento con l’operazioneprioritaria che ci sta a cuore.

Un esempio vale più di mille parole, come sempre:

1

2 class MyEvtHandler(wx.PyEvtHandler):3 def __init__(self):4 wx.PyEvtHandler.__init__(self)5 self.Bind(wx.EVT_BUTTON, self.onclic)6

210 Chapter 8. Ricette wxPython

Page 219: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

7 def onclic(self, evt):8 msg = 'Inserire la password per procedere:'9 cpt = 'Password richiesta.'

10 if check_psw(wx.GetPasswordFromUser(msg, cpt)):11 evt.Skip()12 else:13 wx.MessageBox('Password errata!', 'Password errata',14 wx.ICON_ERROR|wx.OK)15

16

17 class MyButton(wx.Button):18 def __init__(self, *a, **k):19 wx.Button.__init__(self, *a, **k)20 self.PushEventHandler(MyEvtHandler())21

22

23 class Test(wx.Frame):24 def __init__(self, *a, **k):25 wx.Frame.__init__(self, *a, **k)26 p = wx.Panel(self)27 b = MyButton(p, -1, 'clic', pos=((50,50)))28

29 # self.Bind(wx.EVT_BUTTON, self.onclic, b)30 b.Bind(wx.EVT_BUTTON, self.onclic)31

32 def onclic(self, evt):33 wx.MessageBox(34 "Password corretta, procedo con l'azione prevista.")35 # e qui il codice previsto del callback, come di consueto36

Abbiamo semplicemente spostato la logica del controllo della password da MyButton a MyEvtHandler. La veramagia è, naturalmente, chiamare PushEventHandler per portare il nostro handler in cima alla lista degli handlerdel pulsante. Una volta che ci siamo assicurati che il nostro codice sarà eseguito per primo, il resto è un gioco daragazzi: se la password è corretta chiamiamo Skip e lasciamo propagare l’evento. Se no, tutto si ferma lì. Notateanche che adesso il codice fa quello che vogliamo indipendentemente dallo “stile” di binding preferito dal codicesorgente.

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, eavercela 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 inun 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])

8.4. Convertire le date tra Python e wxPython. 211

Page 220: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

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 widgetdi 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 PyGetDatee PySetDate, e così molti altri widget. Prima di utilizzare questa ricetta, controllate quindi sempre che il vostrowidget non sia già pronto a fare il lavoro sporco dietro le quinte.

212 Chapter 8. Ricette wxPython

Page 221: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

CHAPTER 9

TODO list.

Argomenti che vorrei trattare in futuro.

Grandi temi.

Questi sono temi impegnativi: varie pagine, complesse.

• DISEGNARE. I wx.DC, e tutti gli strumenti per il disegno.

• Pattern MCV

• Unit test

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

Rimandi ai “todo” nelle pagine già scritte.

Todo

una pagina su SWIG e l’oop Python/C++.

213

Page 222: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/chiusura.rst, line 219.)

Todo

una pagina su SWIG e l’oop Python/C++.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/eventi_tecniche.rst, line 165.)

Todo

una pagina sui thread , una pagina sui timer , una pagina sulla clipboard

214 Chapter 9. TODO list.

Page 223: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/eventi_tecniche2.rst, line 167.)

Todo

una pagina sui pycontrols (cfr paragrafo seguente)

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/funzioni.rst, line 88.)

Todo

una pagina sulle sottoclassi di wx.Dialog.

9.2. Rimandi ai “todo” nelle pagine già scritte. 215

Page 224: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/funzioni.rst, line 120.)

Todo

una pagina sui font

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/integrazione_event_loop.rst, line 158.)

Todo

una pagina su MVC | una pagina su pub/sub.

216 Chapter 9. TODO list.

Page 225: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/integrazione_event_loop.rst, line 206.)

Todo

una pagina sui thread: accorciare tutto questo paragrafo di conseguenza.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/menu_avanzate.rst, line 31.)

Todo

una pagina su mvc.

9.2. Rimandi ai “todo” nelle pagine già scritte. 217

Page 226: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/menu_avanzate.rst, line 326.)

Todo

una pagina su MVC con collegamento a questa.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/nonusare.rst, line 59.)

Todo

una pagina su MCV con riferimenti a questa.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/pubsub.rst, line 16.)

Todo

una pagina sui thread.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/pubsub.rst, line 351.)

Todo

una pagina sulla two-step creation.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/stili.rst, line 137.)

Todo

una pagina sui pycontrols

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/validatori.rst, line 260.)

Todo

una pagina su MCV con molti riferimenti a questa.

218 Chapter 9. TODO list.

Page 227: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/checkouts/latest/source/validatoridue.rst, line 17.)

Todo

una pagina sui thread.

(The original entry is located in /home/docs/checkouts/readthedocs.org/user_builds/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/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/appunti-wxpython/checkouts/latest/source/wxapp_basi.rst, line 117.)

• genindex

9.2. Rimandi ai “todo” nelle pagine già scritte. 219

Page 228: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

220 Chapter 9. TODO list.

Page 229: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Index

Bbitmask, 30

Cchild window, 17chiusura

di finestre a cascata, 158di un dialogo, 23, 68di un frame, 68di una wx.App, 74wx.App.ExitMainLoop, 78wx.App.SetExitOnFrameDelete, 76wx.Event.CanVeto, 71, 78wx.Event.Veto, 70, 78wx.EVT_QUERY_END_SESSION, 78wx.Exit, 78wx.PyDeadObjectError, 72, 184wx.SafeShowMessage, 78wx.Window.Close, 68, 73, 158wx.Window.Destroy, 68, 73, 158wx.Window.DestroyChildren, 73wx.Window.IsBeingDeleted, 160

constraints, 121wx.IndividualLayoutConstraint, 121wx.LayoutConstraints, 121wx.Window.Layout, 121wx.Window.SetAutoLayout, 121wx.Window.SetConstraints, 121

Ddate, 211

wx.DateTime, 211demo di wxPython, 7dialogo, 20

chiusura, 23, 68con pulsanti predefiniti, 23, 28con risposte predefinite, 27con validazione automatica, 23, 29, 108wx.Dialog, 23, 27–29

wx.Dialog.ShowModal, 23wx.MessageDialog, 27wx.Window.Destroy, 23

dimensioni in wxPython, 79wx.EVT_SIZE, 80wx.Size, 79wx.Sizer.Fit, 80wx.Sizer.Layout, 80wx.Window.Fit, 80wx.Window.GetBestSize(Tuple), 80wx.Window.GetClientSize(Tuple), 80wx.Window.GetMaxSize, 80wx.Window.GetMinSize, 80wx.Window.GetSize(Tuple), 80wx.Window.GetVirtualSize(Tuple), 80wx.Window.Layout, 80wx.Window.SendSizeEvent, 80wx.Window.SetAutoLayout, 80wx.Window.SetClientSize(WH), 80wx.Window.SetInitialSize, 80wx.Window.SetMaxSize, 80wx.Window.SetMinSize, 80wx.Window.SetSize(WH), 80wx.Window.SetSize*, 79wx.Window.SetSizeHints(Sz), 80wx.Window.SetVirtualSize(WH), 80wx.Window.SetVirtualSizeHints(Sz), 80

documentazione, 8EventsinStyle, 8libri, 8wiki, 8

Eeccezioni

eccezioni Python, 178eccezioni wxPython, 182sys.excepthook, 181type checking, 186wx.App.GetAssertMode, 182wx.App.SetAssertMode, 182

221

Page 230: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

wx.PYAPP_ASSERT_*, 182wx.PyAssertionError, 182wx.PyDeadObjectError, 72, 184

eventi, 37, 87, 125, 131Bind, 40, 41, 94binder, 39binding con ’partial’, 126blocchi, 133callback, 38categorie, 134command event, 87Event Manager, 126, 193event type, 39eventi personalizzati, 126, 206filtri, 131handler, 39, 137handler personalizzati, 135, 210lambda binding, 29, 125loop degli eventi, 139, 148metodi e proprietà, 42processamento, 88propagazione, 87, 94, 191Skip, 88, 91wx.App.FilterEvent, 131wx.App.SafeYield, 143wx.App.SetCallFilterEvent, 131wx.App.Yield, 143, 150wx.CommandEvent, 87wx.Event, 38wx.Event.CanVeto, 71, 78wx.Event.GetEventCategory, 134wx.Event.SetEventObject, 206wx.Event.Skip, 88, 91wx.Event.Veto, 70, 78wx.EventBlocker, 133wx.EVT_*, 39wx.EVT_CLOSE, 68, 74, 158wx.EVT_CONTEXT_MENU, 97wx.EVT_IDLE, 140, 151wx.EVT_MENU, 47wx.EVT_MENU_RANGE, 55wx.EVT_QUERY_END_SESSION, 78wx.EVT_SIZE, 80wx.EvtHandler, 39wx.EvtHandler.Bind, 40, 94wx.EvtHandler.GetNextHandler, 137wx.EvtHandler.GetPreviousHandler, 137wx.EvtHandler.IsUnlinked, 137wx.EvtHandler.ProcessEvent, 88, 128wx.EvtHandler.SetEvtHandlerEnabled, 88, 137wx.EvtHandler.SetNextHandler, 137wx.EvtHandler.SetPreviousHandler, 137wx.EvtHandler.Unbind, 137wx.EvtHandler.Unlink, 137

wx.GUIEventLoop.IsYielding, 143wx.GUIEventLoop.Yield, 143, 150wx.GUIEventLoop.YieldFor, 143wx.IdleEvent.RequestMore, 151wx.lib.newevent.NewCommandEvent, 131, 206wx.lib.newevent.NewEvent, 131wx.NewEventType, 127wx.PostEvent, 128, 208wx.PyCommandEvent, 128, 208wx.PyEvent, 128wx.PyEventBinder, 39, 127wx.PyEventBinder.Bind, 40wx.PyEvtHandler, 135, 210wx.PyEvtHandler.ProcessEvent, 206wx.SafeYield, 143wx.WakeUpIdle, 140, 151wx.Window.GetEventHandler, 206wx.Window.PopEventHandler, 137wx.Window.PushEventHandler, 135, 210wx.Window.SendSizeEvent, 80wx.wxEVT_*, 39wx.wxEVT_CATEGORY_*, 134wx.YeldIfNeeded, 143wx.Yield (deprecato, usare wx.App.Yield), 143

Fframe, 20

chiusura, 68stili, 20wx.Frame, 20

funzioni globali, 196

Iicone

nei menu, 97id in wxPython, 25

dialogo con pulsanti predefiniti, 28dialogo con risposte predefinite, 27dialogo con validazione automatica, 29stock buttons, 27uso nei menu, 29wx.FindWindowById, 26wx.ID_ANY, 26wx.ID_CANCEL, 28wx.ID_HIGHEST, 26wx.ID_LOWEST, 26wx.ID_NO, 27wx.ID_OK, 28, 108wx.ID_YES, 27wx.NewId, 26wx.RegisterId, 26wx.Window.GetId, 26wx.Window.SetId, 26

IPython, 157

222 Index

Page 231: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

Llogging

con Python, 164con wxPython, 169wx.Log, 169wx.Log*, 169wx.Log.Flush, 171wx.Log.Resume, 171wx.Log.SetActiveTarget, 171wx.Log.SetLogLevel, 169wx.Log.Suspend, 171wx.LOG_*, 169wx.LogBuffer, 171wx.LogChain, 177wx.LogGui, 171wx.LogNull, 174wx.LogStderr, 171wx.LogTextCtrl, 171wx.LogWindow, 171wx.PyLog, 174wx.PyLog.DoLogRecord, 174wx.PyLog.DoLogRecordAtLevel, 174wx.PyLog.DoLogText, 174

loop degli eventi, 139integrare loop esterni, 148personalizzati, 146stack dei loop, 144wx.App.GetMainLoop, 142wx.App.OnEventLoopEnter, 142, 144wx.App.OnEventLoopExit, 142, 144wx.EventLoop (alias per GUIEventLoop), 139wx.EventLoopActivator, 144wx.EventLoopBase, 139wx.EVT_IDLE, 140wx.GUIEventLoop, 139wx.GUIEventLoop.Dispatch, 140wx.GUIEventLoop.Exit, 142, 144wx.GUIEventLoop.GetActive, 142, 144wx.GUIEventLoop.IsMain, 142wx.GUIEventLoop.IsRunning, 142wx.GUIEventLoop.IsYielding, 143wx.GUIEventLoop.Pending, 140, 142wx.GUIEventLoop.ProcessIdle, 140wx.GUIEventLoop.Run, 140, 146wx.GUIEventLoop.SetActive, 142, 144wx.GUIEventLoop.Yield, 144wx.GUIEventLoop.YieldFor, 143

Mmenu, 43, 49, 97

abilitare e disabilitare, 52acceleratori, 49contestuali, popup, 97icone, 97

scorciatoie, 49sottomenu, 46spuntabili e selezionabili, 53tecniche di fattorizzazione, 102tecniche di manipolazione, 101uso degli id, 29wx.AcceleratorEntry, 50wx.AcceleratorTable, 50wx.EVT_CONTEXT_MENU, 97wx.EVT_MENU, 29, 47wx.EVT_MENU_RANGE, 29, 55wx.Frame

PopupMenu, 97wx.ITEM_*, 53wx.Menu, 44wx.Menu.Append, 45wx.Menu.AppendMenu, 46wx.Menu.AppendSeparator, 46wx.MenuBar, 44wx.MenuBar.EnableTop, 52wx.MenuBar.IsEnabledTop, 52wx.MenuItem, 45wx.MenuItem.Check, 53wx.MenuItem.IsChecked, 53wx.MenuItem.SetBitmap, 97wx.StripMenuCodes, 49

Ppanel, 20

tab trasversing, 21wx.Panel, 21wx.TAB_TRASVERSAL, 21

parent window, 17parent, catena dei, 17, 191

wx.App.GetTopWindow, 19wx.App.SetTopWindow, 19wx.GetTopLevelParent, 19wx.GetTopLevelWindows, 19wx.Window.GetChildren, 19wx.Window.GetGrandParent, 19wx.Window.GetTopLevelParent, 19wx.Window.SetParent, 18

posizionamento assoluto, 33pub/sub, 187

confronto con gli eventi, 191confronto con signal/slot, 193wx.lib.pubsub, 189

Pygame, 157

QQt, 193

Ssizer, 33, 81

Index 223

Page 232: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

dimensioni dei widget, 79SendSizeEvent, 80wx.BoxSizer, 35wx.EVT_SIZE, 80wx.FlexGridSizer, 82wx.GridBagSizer, 83wx.GridSizer, 81wx.HORIZONTAL, 35wx.Sizer, 34wx.Sizer.Add, 35, 85wx.Sizer.AddSpacer, 37wx.Sizer.AddStretchSpacer, 37wx.Sizer.Fit, 80wx.Sizer.Layout, 80wx.SizerItem, 85wx.StaticBoxSizer, 83wx.StdDialogButtonSizer, 84wx.VERTICAL, 35wx.Window.CreateButtonSizer, 84wx.Window.Fit, 80wx.Window.Layout, 80wx.Window.SendSizeEvent, 85wx.Window.SetSizer, 34wx.WrapSizer, 84

stdout/errwx.App.outputWindowClass, 64wx.App.RedirectStdio, 64wx.PyOnDemandOutputWindow, 64

stili, 30di un frame, 20extra-style, 32, 108two-step creation, 32wx.PreFrame, 32wx.Window.SetExtraStyle, 32, 108wx.Window.SetWindowStyleFlag, 32

stock buttons, 27, 84strumenti

Boa Constructor, 11Editra, 9EventsinStyle, 8, 31wxGlade, 11XmlResource, 12

Ttop-level window, 19Twisted, 157two-step creation, 32

Vvalidatore, 104

composizione, 110trasferimento dati, 114validazione a cascata, 104, 106, 111validazione automatica, 23, 29, 108, 114

validazione ricorsiva, 108, 113wx.PyValidator, 29, 104wx.PyValidator.Clone, 104wx.PyValidator.TransferFromWindow, 114wx.PyValidator.TransferToWindow, 114wx.PyValidator.Validate, 104wx.Window.SetValidator, 108wx.WX_EX_VALIDATE_RECURSIVELY, 108

Wwx

PyDeadObjectError, 72, 184wx.AcceleratorEntry, 50wx.AcceleratorTable, 50wx.App, 15, 61, 74

chiusura, 74ExitMainLoop, 78FilterEvent, 131GetAssertMode, 182GetMainLoop, 142GetTopWindow, 19MainLoop, 16, 61, 74, 139OnEventLoopEnter, 142, 144OnEventLoopExit, 142, 144OnExit, 63, 74OnInit, 61outputWindowClass, 64RedirectStdio, 64SafeYield, 143SetAssertMode, 182SetCallFilterEvent, 131SetExitOnFrameDelete, 76SetTopWindow, 19Yield, 143, 150

wx.BoxSizer, 35wx.Button

SetDefault, 21wx.CallAfter, 76, 154wx.CallLater, 76wx.CommandEvent, 87wx.DateTime, 211wx.Dialog, 23, 27–29, 68

ShowModal, 23, 144wx.Event, 38

CanVeto, 71, 78GetEventCategory, 134SetEventObject, 206Skip, 88, 91Veto, 70, 78

wx.EventBlocker, 133wx.EventLoop (alias per GUIEventLoop), 139wx.EventLoopActivator, 144wx.EventLoopBase, 139wx.EVT_*, 39

224 Index

Page 233: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

wx.EVT_CLOSE, 68, 74, 158wx.EVT_CONTEXT_MENU, 97wx.EVT_IDLE, 140, 151wx.EVT_MENU, 29, 47wx.EVT_MENU_RANGE, 29, 55wx.EVT_QUERY_END_SESSION, 78wx.EVT_SIZE, 80wx.EvtHandler, 39

Bind, 40GetNextHandler, 137GetPreviousHandler, 137IsUnlinked, 137ProcessEvent, 88, 128SetEvtHandlerEnabled, 88, 137SetNextHandler, 137SetPreviousHandler, 137Unbind, 137Unlink, 137

wx.Exit, 78wx.FindWindowById, 26wx.FlexGridSizer, 82wx.Frame, 20, 68

PopupMenu, 97wx.GetTopLevelParent, 19wx.GetTopLevelWindows, 19wx.GridBagSizer, 83wx.GridSizer, 81wx.GUIEventLoop, 139

Dispatch, 140Exit, 142, 144GetActive, 142, 144IsMain, 142IsRunning, 142IsYielding, 143Pending, 140, 142ProcessIdle, 140Run, 140, 146SetActive, 142, 144Yield, 143, 144, 150YieldFor, 143

wx.HORIZONTAL, 35wx.ID_ANY, 26wx.ID_CANCEL, 28wx.ID_HIGHEST, 26wx.ID_LOWEST, 26wx.ID_NO, 27wx.ID_OK, 28, 108wx.ID_YES, 27wx.IdleEvent

RequestMore, 151wx.IndividualLayoutConstraint, 121wx.ITEM_* (nei menu), 53wx.LayoutConstraints, 121wx.lib

pubsub, 189wx.lib.evtmgr

eventManager, 126, 193wx.lib.newevent

NewCommandEvent, 131, 206NewEvent, 131

wx.Log, 169Flush, 171Resume, 171SetActiveTarget, 171SetLogLevel, 169Suspend, 171

wx.Log*, 169wx.LOG_*, 169wx.LogBuffer, 171wx.LogChain, 177wx.LogGui, 171wx.LogNull, 174wx.LogStderr, 171wx.LogTextCtrl, 171wx.LogWindow, 171wx.Menu, 44

Append, 45AppendMenu, 46AppendSeparator, 46

wx.MenuBar, 44EnableTop, 52IsEnabledTop, 52

wx.MenuItem, 45Check, 53IsChecked, 53SetBitmap, 97

wx.MessageDialog, 27wx.MilliSleep, 140wx.NewEventType, 127wx.NewId, 26wx.Panel, 21wx.PostEvent, 128, 208wx.PreFrame, 32wx.PYAPP_ASSERT_*, 182wx.PyAssertionError, 182wx.PyCommandEvent, 128, 208wx.PyEvent, 128wx.PyEventBinder, 39, 127

Bind, 40wx.PyEvtHandler, 135, 210

ProcessEvent, 206wx.PyLog, 174

DoLogRecord, 174DoLogRecordAtLevel, 174DoLogText, 174

wx.PyOnDemandOutputWindow, 64wx.PyValidator, 29, 104

Clone, 104

Index 225

Page 234: Appunti wxPython Documentation - media.readthedocs.org · CHAPTER 1 Novità e cambiamenti (man mano che ce ne sono...). Questi appunti sono stati scritti in una prima grande “infornata”

Appunti wxPython Documentation, Release 1

TransferFromWindow, 114TransferToWindow, 114Validate, 104

wx.RegisterId, 26wx.SafeShowMessage, 78wx.SafeYield, 143wx.Size, 79wx.Sizer, 34

Add, 35, 85AddSpacer, 37AddStretchSpacer, 37Fit, 80Layout, 80

wx.SizerItem, 85wx.StaticBoxSizer, 83wx.StdDialogButtonSizer, 84wx.StripMenuCodes, 49wx.TAB_TRASVERSAL, 21wx.VERTICAL, 35wx.WakeUpIdle, 140, 151wx.Window

Close, 68, 73, 74, 158CreateButtonSizer, 84Destroy, 23, 68, 73, 158DestroyChildren, 73Fit, 80GetBestSize(Tuple), 80GetChildren, 19GetClientSize(Tuple), 80GetEventHandler, 206GetGrandParent, 19GetId, 26GetMaxSize, 80GetMinSize, 80GetSize(Tuple), 80GetTopLevelParent, 19GetVirtualSize(Tuple), 80IsBeingDeleted, 160Layout, 80, 121PopEventHandler, 137PushEventHandler, 135, 210SendSizeEvent, 80, 85SetAutoLayout, 80, 121SetClientSize(WH), 80SetConstraints, 121SetExtraStyle, 32, 108SetId, 26SetInitialSize, 80SetMaxSize, 80SetMinSize, 80SetParent, 18SetSize(WH), 80SetSize*, 79SetSizeHints(Sz), 80

SetSizer, 34SetValidator, 108SetVirtualSize(WH), 80SetVirtualSizeHints(Sz), 80SetWindowStyleFlag, 32

wx.WrapSizer, 84wx.WX_EX_VALIDATE_RECURSIVELY, 108wx.wxEVT_*, 39wx.wxEVT_CATEGORY_*, 134wx.xrc

XmlResource, 12wx.YeldIfNeeded, 143wx.Yield (deprecato, usare wx.App.Yield), 143

226 Index