Apprendre à utiliser le module Python PyGTK 2.0

John Finlay

Traducteurs

| Marc Verney | Mc Collilieux |

Ce tutoriel décrit l'utilisation du module Python PyGTK. Il cherche à fournir une documentation aussi large que possible de PyGTK, mais n'est en aucun cas exhaustif. Commentez ♪

- Titre : Apprendre à utiliser le module Python PyGTK 2.0

- Auteur : John Finlay

- Traducteurs : Marc Verney, Mc Collilieux

- Parution : 13 avril 2005

- Licence : Copyright ® 2005 John Finlay. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Chapitre 1. Introduction

- I-A. Exploring PyGTK

- II. Chapitre 2. Premiers pas

- II-A. "Hello World" en PyGTK

- II-B. Le principe des signaux et des rappels

- II-C. Événements

- II-D. Le "Hello World" pas à pas

- III. Chapitre 3. Avançons

- III-A. Compléments sur les gestionnaires de signaux

- III-B. Modification de notre "Hello World"

- IV. Chapitre 4. Le placement des widgets

- IV-A. Le principe des boites de placement

- IV-B. Les boites en détail

- IV-C. Démonstration de placement avec les boites

- IV-D. Le placement avec les tableaux

- IV-E. Démonstration de placement avec les tableaux

- V. Chapitre 5. Vue d'ensemble des widgets

- V-A. Hiérarchie des widgets

- V-B. Les widgets sans fenêtre

- VI. Chapitre 6. Les boutons

- VI-A. Les boutons simples

- VI-B. Les boutons à bascule

- VI-C. Les cases à cocher

- VI-D. Les boutons radio

- VII. Chapitre 7. Les ajustements

- VII-A. Créer un ajustement

- VII-B. Utiliser facilement les ajustements

- VII-C. Les ajustements en détail

- VIII. Chapitre 8. Les widgets d'intervalle

- VIII-A. HScrollbar et VScrollbar : les barres de défilement

- VIII-B. HScale et VScale : les gradateurs

- VIII-B-1. Créer un gradateur

- VIII-B-2. Méthodes et signaux (enfin, au moins méthodes…)

- VIII-C. Méthodes communes aux widgets d'intervalle

- VIII-C-1. Définir le mode d'actualisation

- VIII-C-2. Définir et récupérer les ajustements

- VIII-D. Clavier et souris

- VIII-E. Démonstration des widgets d'intervalle

- IX. Chapitre 9. Widgets divers

- IX-A. Les étiquettes

- IX-B. Les flèches

- IX-C. Les infobulles

- IX-D. La barre de progression

- IX-E. Les boites de dialogue

- IX-F. Les images

- IX-F-1. Les pixmaps

- IX-G. Les règles

- IX-H. La barre d'état

- IX-I. Les champs de saisie

- IX-J. Les boites d'incrément

- IX-K. Les listes déroulantes

- IX-L. Le calendrier

- IX-M. Le sélecteur de couleur

- IX-N. Le sélecteur de fichier

- IX-O. Le sélecteur de police

- X. Chapitre 10. Les widgets conteneurs

- X-A. La boite à événement (EventBox)

- X-B. Le widget d'alignement (Alignment)

- X-C. Le conteneur Place (Fixed)

- X-D. Le conteneur Layout (Affiche)

- X-E. Les cadres (Frame)

- X-F. Les cadres proportionnels (AspectFrame)

- X-G. Fenêtre à volets (Paned Window)

- X-H. Viewports

- X-I. Fenêtre avec barres de défilement (Scrolled Window)

- X-J. Boites à boutons (ButtonBox)

- X-K. La barre d'outils (Toolbar)

- X-L. Le bloc-notes (Notebook)

- X-M. Les connexions et connecteurs (Plugs et Sockets)

- X-M-1. Les Plugs

- X-M-2. Sockets

- XI. Chapitre 11. Les menus

- XI-A. Création manuelle de menus

- XI-B. Démonstration de création manuelle de menus

- XI-C. Création de menus avec l'ItemFactory

- XI-D. Démonstration de l'ItemFactory

- XII. Chapitre 12. La zone de dessin (Drawing Area)

- XII-A. Le contexte graphique

- XII-B. Les méthodes pour dessiner

- XIII. Chapitre 13. Le widget TextView

- XIII-A. Présentation des widgets de texte

- XIII-B. TextView : la zone de texte

- XIII-C. TextBuffer : le buffer de texte

- XIII-C-1. Récupérer des informations sur le buffer

- XIII-C-2. Créer des TextIter

- XIII-C-3. Insérer, récupérer et supprimer du texte

- XIII-C-4. Créer et manipuler des TextMark

- XIII-C-5. Créer et appliquer des TextTag

- XIII-C-6. Insérer des images et des widgets

- XIII-D. TextIter : les itérateurs de texte

- XIII-D-1. Récupérer les attributs d'un TextIter

- XIII-D-2. Récupérer les attributs d'un texte portant un TextIter

- XIII-D-3. Copier un TextIter

- XIII-D-4. Récupérer du texte et des objets

- XIII-D-5. Vérifier les balises à une position de TextIter

- XIII-D-6. Vérifier la position dans le texte

- XIII-D-7. Se déplacer dans le texte

- XIII-D-8. Se déplacer à un emplacement spécifique

- XIII-D-9. Rechercher du texte

- XIII-E. TextMark : les marques de texte

- XIII-F. TextTag et TextTagTable : les balises de texte

- XIII-F-1. Les TextTag

- XIII-F-2. La TextTagTable

- XIII-G. Un exemple de TextView

- XIV. Chapitre 14. Le widget TreeView

- XIV-A. Présentation

- XIV-B. L'interface TreeModel et le stockage des données

- XIV-B-1. Introduction

- XIV-B-2. Créer des objets TreeStore et ListStore

- XIV-B-3. Les références aux lignes du modèle

- XIV-B-3-a. Le chemin d'accès

- XIV-B-3-b. Les TreeIter

- XIV-B-3-c. Les TreeRowReference

- XIV-B-4. Ajouter des lignes

- XIV-B-4-a. Ajouter des lignes à un ListStore

- XIV-B-4-b. Ajouter des lignes à un TreeStore

- XIV-B-4-c. Modèles de grande taille

- XIV-B-5. Supprimer des lignes

- XIV-B-5-a. Supprimer des lignes d'un ListStore

- XIV-B-5-b. Supprimer des lignes d'un TreeStore

- XIV-B-6. Manipuler les données des lignes

- XIV-B-6-a. Définir et récupérer des valeurs

- XIV-B-6-b. Réordonner les lignes d'un ListStore

- XIV-B-6-c. Réordonner les lignes d'un TreeStore

- XIV-B-6-d. Manipuler plusieurs lignes

- XIV-B-7. Support du protocole Python

- XIV-B-8. Les signaux du TreeModel

- XIV-B-9. Ordonner les lignes d'un TreeModel

- XIV-B-9-a. L'interface TreeSortable

- XIV-B-9-b. Classement dans les ListStore et les TreeStore

- XIV-C. Les TreeView

- XIV-C-1. Créer un TreeView

- XIV-C-2. Récupérer et spécifier le modèle du TreeView

- XIV-C-3. Les propriétés du TreeView

- XIV-D. Les CellRenderer

- XIV-D-1. Vue d'ensemble

- XIV-D-2. Les différents types de CellRenderer

- XIV-D-3. Les propriétés des CellRenderer

- XIV-D-4. Les attributs du CellRenderer

- XIV-D-5. Fonction d'affichage des données cellulaires

- XIV-D-6. CellRendererText Markup

- XIV-D-7. Cellules de texte éditables

- XIV-D-8. Cellules à bascule activables

- XIV-D-9. Programme d'exemple avec cellules éditables et activables

- XIV-E. TreeViewColumn

- XIV-E-1. Créer des TreeViewColumn

- XIV-E-2. Gérer les CellRenderer

- XIV-F. Manipuler les TreeView

- XIV-F-1. Gérer les colonnes

- XIV-F-2. Afficher ou masquer les enfants d'une ligne

- XIV-G. Les signaux des TreeView

- XIV-H. Gestion des Treeselection

- XIV-H-1. Obtenir un TreeSelection

- XIV-H-2. Modes du TreeSelection

- XIV-H-3. Retrouver la sélection

- XIV-H-4. Utiliser une fonction TreeSelection

- XIV-H-5. Sélectionner et désélectionner les lignes

- XIV-I. Glisser-déposer dans un TreeView

- XIV-I-1. Reclasser avec un glisser-déposer

- XIV-I-2. Glisser-déposer externe

- XIV-I-3. Exemple de glisser-déposer dans un TreeView

- XIV-J. TreeModelSort et TreeModelFilter

- XIV-J-1. Le TreeModelSort

- XIV-J-2. Le TreeModelFilter

- XIV-K. Le TreeModel générique

- XIV-K-1. Aperçu du GenericTreeModel

- XIV-K-2. Interface du GenericTreeModel

- XIV-K-3. Ajouter et supprimer des lignes

- XIV-K-4. Gestion de la mémoire

- XIV-K-5. Autres interfaces

- XIV-K-6. Utilisation du GenericTreeModel

- XIV-L. The Generic CellRenderer

- XV. Chapitre 15. Nouveaux Widgets dans PyGTK 2.2

- XV-A. Les Clipboards

- XV-A-1. Créer un Clipboard

- XV-A-2. Utiliser les Clipboard avec les Entry, Spinbutton et TextView

- XV-A-3. Affecter des données à un Clipboard

- XV-A-4. Récupérer le contenu du Clipboard

- XV-A-5. Exemple de Clipboard

- XVI. Chapitre 16. Nouveaux Widgets dans PyGTK 2.4

- XVI-A. Les objets Action et groupes d'Actions (ActionGroup)

- XVI-A-1. Actions

- XVI-A-1-a. Créer des Actions

- XVI-A-1-b. Utiliser les Actions

- XVI-A-1-c. Créer des widgets délégués (proxy)

- XVI-A-1-d. Propriétés des Actions

- XVI-A-1-e. Actions et Accélérateurs

- XVI-A-1-f. Actions à bascule

- XVI-A-1-g. Actions avec exclusivité

- XVI-A-1-h. Un exemple avec Actions

- XVI-A-2. ActionGroup

- XVI-A-2-a. Créer des ActionGroups

- XVI-A-2-b. Ajouter des Actions

- XVI-A-2-c. Récupérer une Action

- XVI-A-2-d. Controler les Actions

- XVI-A-2-e. Exemple de ActionGroup

- XVI-A-2-f. Les signaux de ActionGroup

- XVI-B. Les listes déroulantes (ComboBox) et listes déroulantes avec saisie (ComboBoxEntry)

- XVI-B-1. Le widget ComboBox

- XVI-B-1-a. Utiliser la boite déroulante de base

- XVI-B-1-b. Usage avancé de ComboBox

- XVI-B-2. Le widget ComboBoxEntry

- XVI-B-2-a. Utilisation fondamentale du ComboBoxEntry

- XVI-B-2-b. Utilisation avancée du ComboBoxEntry

- XVI-C. Bouton de couleur (ColorButton) et de police (FontButton)

- XVI-C-1. Bouton de couleur (ColorButton)

- XVI-C-2. Bouton de police (FontButton)

- XVI-D. Zone de saisie avec complètement (EntryCompletion)

- XVI-E. Expanseur (Expander)

- XVI-F. Sélection de fichiers basée sur le sélecteur FileChooser

- XVI-G. Le gestionnaire d'Interface Utilisateur UIManager

- XVI-G-1. Aperçu

- XVI-G-2. Créer un UIManager

- XVI-G-3. Ajouter et supprimer des groupes d'actions

- XVI-G-4. Descriptions de l'Interface Utilisateur

- XVI-G-5. Ajouter et supprimer des descriptions d'Interface Utilisateur

- XVI-G-6. Accès aux widgets d'une Interface Utilisateur

- XVI-G-7. Un exemple d'Interface Utilisateur simple

- XVI-G-8. Combiner des descriptions d'Interface Utilisateur

- XVI-G-9. Signaux de l'Interface Utilisateur

- XVII. Chapitre 17. Widgets non décrits

- XVII-A. Étiquette de raccourci

- XVII-B. Option de menu

- XVII-C. Éléments de menu

- XVII-C-1. Éléments de menu à cocher

- XVII-C-2. Élément de menu Radio

- XVII-C-3. Élément de menu séparateur

- XVII-C-4. Élément rattacheur de menu

- XVII-D. Courbes

- XVII-E. Fenêtre de dialogue de message

- XVII-F. Courbe Gamma

- XVIII. Chapitre 18. Définir les attributs des widgets

- XVIII-A. Méthodes des drapeaux des widgets

- XVIII-B. Méthodes d'affichage des widgets

- XVIII-C. Accélérateurs pour les widgets

- XVIII-D. Méthodes de nom des widgets

- XVIII-E. Style des widgets

- XIX. Chapitre 19. Temporisations, Entrées/Sorties et fonctions d'inactivité

- XIX-A. Temporisations

- XIX-B. Contrôler les Entrées/Sorties

- XIX-C. Les fonctions d'inactivité

- XX. Chapitre 20. Gestion avancée des événements et signaux

- XX-A. Méthodes des signaux

- XX-A-1. Connecter et déconnecter les gestionnaires de signaux

- XX-A-2. Bloquer et libérer les gestionnaires de signaux

- XX-A-3. Émettre et stopper les signaux

- XX-B. Émission et propagation du signal

- XXI. Chapitre 21. Gérer les sélections

- XXI-A. Généralités

- XXI-B. Récupérer la sélection

- XXI-C. Alimenter la sélection

- XXII. Chapitre 22. Le glisser-déposer (DND)

- XXII-A. Aperçu du DND

- XXII-B. Propriétés du glisser-déposer

- XXII-C. Méthodes du glisser-déposer

- XXII-C-1. Configurer le widget origine

- XXII-C-2. Les signaux du widget origine

- XXII-C-3. Configurer un widget destination

- XXII-C-4. Les signaux du widget destination

- XXIII. Chapitre 23. Les fichiers de style rc GTK

- XXIII-A. Les fonctions pour le fichier rc

- XXIII-B. Format du fichier rc GTK

- XXIII-C. Exemple de fichier rc

- XXIV. Chapitre 24. Scribble, Un programme simple de dessin

- XXIV-A. Aperçu de Scribble

- XXIV-B. Gestion des événements

- XXIV-B-1. Gestion des événements dans Scribble

- XXIV-C. Le widget zone de dessin (DrawingArea) et le dessin

- XXV. Tips For Writing PyGTK Applications

- XXVI. Contributing

- XXVII. Credits

- XXVII-A. PyGTK Credits

- XXVII-B. Original GTK+ Credits

- XXVIII. Chapitre 28. Copyright du tutoriel et licence d'utilisation

- XXIX. Annexe A - Les signaux GTK

- XXIX-A. GtkObject

- XXIX-B. GtkWidget

- XXIX-C. GtkData

- XXIX-D. GtkContainer

- XXIX-E. GtkCalendar

- XXIX-F. GtkEditable

- XXIX-G. GtkNotebook

- XXIX-H. GtkList

- XXIX-I. GtkMenuShell

- XXIX-J. GtkToolbar

- XXIX-K. GtkButton

- XXIX-L.  GtkItem

- XXIX-M. GtkWindow

- XXIX-N. GtkHandleBox

- XXIX-O. GtkToggleButton

- XXIX-P. GtkMenuItem

- XXIX-Q. GtkCheckMenuItem

- XXIX-R. GtkInputDialog

- XXIX-S. GtkColorSelection

- XXIX-T. GtkStatusBar

- XXIX-U. GtkCurve

- XXIX-V. GtkAdjustment

- Annexe B - Code Examples

- scribblesimple.py

- Annexe C - ChangeLog

- Annexe D - À propos de la traduction

I - Chapitre 1. Introduction

PyGTK 2.0 est un ensemble de modules Python offrant une interface Python à GTK+ 2.0. Tout au long de ce document, le terme PyGTK fera référence à la version 2.X de PyGTK, et les termes GTK et GTK+ à la version 2.X de GTK+. Le site web principal de PyGTK est www.pygtk.org. L'auteur principal de PyGTK est :

- James Henstridge james@daa.com.au

lequel est aidé par les développeurs inscrits dans le fichier AUTHORS de la distribution PyGTK et par la communauté PyGTK.

Python est un langage de programmation interprété, orienté objet et extensible. Il est doté d'une grande panoplie de modules lui ouvrant l'accès aux services de nombreux domaines : système d'exploitation, Internet (HTML, XML, FTP, etc.), librairies graphiques (dont OpenGL, TK, etc.), courrier électronique (IMAP, SMTP, POP3, etc.), traitement des chaines de caractères, multimédia (audio, JPEG), cryptographie. On peut ajouter à ces modules de nombreux autres proposés par des tiers et qui augmentent encore les possibilités du langage. Python est placé sous une licence similaire à la LGPL et est disponible pour les systèmes d'exploitation Linux, Unix, Windows et Macintosh. Pour plus d'information, rendez-vous sur www.python.org. L'auteur principal de Python est :

- Guido van Rossum guido@python.org.

GTK (GIMP Toolkit) est une bibliothèque de création d'interfaces graphiques. Elle est placée sous licence LGPL, et peut donc être utilisée pour le développement de logiciels libres, open source, ou même commerciaux et non libres sans qu'il faille s'acquitter de quelconques droits d'auteur ou licences.

Écrite à l'origine pour le développement du logiciel de traitement d'image the GIMP (GNU Image Manipulation Program) — d'où son nom de GIMP Toolkit (boîte à outils de the GIMP) — GTK a depuis servi dans de nombreux projets logiciels, dont l'environnement de bureau GNOME (GNU Network Object Model Environment). GTK est bâti au-dessus de GDK (GIMP Drawing Kit), qui est essentiellement une encapsulation des fonctions de bas niveau permettant d'accéder au système de fenêtrage (Xlib dans le cas du système X window). Les auteurs principaux de GTK sont :

- Peter Mattis petm@xcf.berkeley.edu ;

- Spencer Kimball spencer@xcf.berkeley.edu ;

- Josh MacDonald jmacd@xcf.berkeley.edu.

GTK est actuellement maintenu par :

- Owen Taylor otaylor@redhat.com ;

- Tim Janik timj@gtk.org.

GTK est principalement une interface de programmation d'application (API) orientée objet. Bien qu'écrite intégralement en C, elle est implémentée en utilisant le concept de classes et de fonctions de rappel (pointeurs sur fonctions).

Un troisième composant, appelé GLib, contient quelques remplacements d'appels standards, ainsi que des fonctions additionnelles pour gérer les listes chainées, etc. Les fonctions de remplacement permettent d'accroître la portabilité de GTK, car une fonction utilisée par un système Unix n'est pas forcément disponible ou standard sur un autre système (comme g_strerror(), par exemple). D'autres comportent des améliorations par rapport aux versions de la libc : g_malloc, par exemple, facilite le débogage.

Dans la version 2.0, GLib a intégré le système de typage qui pose les bases de la hiérarchie de classes de GTK, le système de signaux utilisé par GTK, une API de thread permettant de faire abstraction de celles utilisées par les différentes plates-formes, et une structure de chargement des modules.

Enfin, GTK utilise la bibliothèque Pango pour l'internationalisation du texte.

Ce tutoriel décrit l'interface Python de GTK+ et se base sur le Tutoriel GTK+ 2.0 écrit par Tony Gale et Ian Main. Il cherche à fournir une documentation aussi large que possible de PyGTK, mais n'est en aucun cas exhaustif.

Quelques connaissances de Python et de la manière de créer et d'exécuter des programmes dans ce langage vous seront nécessaires. En revanche, aucune connaissance préalable de GTK n'est requise  ; si vous apprenez PyGTK pour découvrir GTK, vos appréciations sur ce tutoriel ainsi que vos commentaires sur les difficultés rencontrées seront les bienvenus. Nous n'aborderons ni la compilation ni l'installation de Python, GTK+ et PyGTK.

Ce tutoriel est basé sur :

- GTK+ 2.0 à travers GTK+ 2.4 ;

- Python 2.2 ;

- PyGTK 2.0 à travers PyGTK 2.4.

Les exemples originaux ont été écrits et testés sur une RedHat 9.0.

Ce document est en constant développement. Vérifiez l'existence de mises à jour sur www.pygtk.org.

N'hésitez pas à me (l'auteur) faire part des problèmes que vous rencontrez en apprenant PyGTK avec ce document, ainsi que de vos suggestions pour l'améliorer. Voyez la section "Contribuer" pour plus d'information. Si vous découvrez des erreurs, signalez-les sur bugzilla.gnome.org à propos du projet PyGTK. Les informations à propos de Bugzilla sur le site www.pygtk.org peuvent vous aider.

Le Manuel de référence de PyGTK 2.0 peut être consulté sur http://www.pygtk.org/pygtk2reference. Il décrit en détail les classes PyGTK.

Le site PyGTK (www.pygtk.org) contient des ressources supplémentaires utiles pour apprendre PyGTK, d'autres articles et tutoriels ainsi qu'une liste de diffusion active et un canal IRQ (voir les détails sur www.pygtk.org).

I-A - Exploring PyGTK

Johan Dahlin est l'auteur d'un petit programme Python pour Linux (pygtkconsole.py) qui permet une exploration interactive de PyGTK. Le programme comprend un interpréteur interactif comme celui de Python, qui communique avec un processus fils chargé d'exécuter les commandes saisies. Les modules PyGTK sont chargés par défaut. Voici un exemple de session :

1. moe: 96:1095$ pygtkconsole.py

2. Python 2.2.2, PyGTK 1.99.14 (Gtk+ 2.0.6)

3. Interactive console to manipulate GTK+ widgets.

4. >>> w=Window()

5. >>> b=Button('Salut')

6. >>> w.add(b)

7. >>> def salut(b):

8. ...     print "Salut tout le monde !"

9. ...

10. >>> b.connect('clicked', salut)

11. 5

12. >>> w.show_all()

13. >>> Salut tout le monde !

14. Salut tout le monde !

15. Salut tout le monde !

17. >>> b.set_label("Salut toi")

18. >>>

Ceci crée une fenêtre contenant un bouton, lequel affiche le message "Salut tout le monde !" lorsqu'on le clique. Ce programme permet de tester très facilement différents widgets GTK et interfaces PyGTK.

J'utilise aussi un programme développé par Brian McErlean : ActiveState recipe 65109 avec quelques modules pour l'adapter à PyGTK 2.X. Je l'appelle gpython.py. Il fonctionne de la même manière que pygtkconsole.py

Ces deux programmes ne fonctionnent pas avec Microsoft Windows, car ils se basent sur une interface spécifique Unix.

II - Chapitre 2. Premiers pas

Nous commencerons notre découverte de PyGTK avec le programme le plus simple possible. Ce programme (base.py) crée une fenêtre de 200x200 pixels et n'offre aucune possibilité de sortie, si ce n'est par le shell.

1. #!/usr/bin/env python

3. # exemple base.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class Base:

10. def __init__(self):

11. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

12. self.fenetre.show()

14. def boucle(self):

15. gtk.main()

17. print __name__

18. if __name__ == "__main__":

19. base = Base()

20. base.boucle()

Vous pouvez exécuter le programme ci-dessus en tapant :

python base.py

Si base.py est rendu exécutable et qu'il se trouve dans votre PATH, il suffira de taper :

base.py

La ligne 1 invoquera alors Python pour exécuter le programme. Les lignes 5 et 6 font la distinction entre les différentes versions de PyGTK qui peuvent être installées sur votre système. On précise que l'on veut utiliser la version 2.0 de PyGTK, ce qui couvre toutes les versions dont le premier chiffre est 2. Ainsi, on empêche le programme d'utiliser une version antérieure de PyGTK au cas où elle serait installée sur le système. Les lignes 18 à 20 vérifient que la variable __name__ vaut bien "__main__", ce qui indique que le programme est lancé directement à partir de Python et non pas importé dans un interpréteur Python en cours de fonctionnement. Si cette condition est vérifiée, le programme crée une nouvelle instance de la classe Base et enregistre une référence à celle-ci dans la variable base. Puis il invoque la méthode main() pour démarrer la boucle de traitement des événements GTK+.

Une fenêtre semblable à celle de la Figure 2.1, « Une simple fenêtre PyGTK » devrait apparaitre à l'écran.

Figure 2.1. Une simple fenêtre PyGTK

La première ligne permet au programme base.py d'être invoqué à partir d'un shell Linux ou Unix, pourvu que python figure dans votre PATH. Cette ligne doit être la première dans chacun des programmes exemples.

Les lignes 5 à 7 importent le module PyGTK 2 et initialise l'environnement GTK+. Le module PyGTK définit les interfaces Python des fonctions GTK+ qui seront utilisées dans le programme. Pour ceux qui connaissent déjà GTK+, l'initialisation comprend un appel à la fonction gtk_init(). Celle-ci règle plusieurs choses pour nous, comme le format d'affichage et la table des couleurs par défaut, les gestionnaires de signaux par défaut, etc. Elle vérifie également si un ou plusieurs des paramètres suivants ont été passés à l'application via la ligne de commande :

- --gtk-module ;

- --g-fatal-warnings ;

- --gtk-debug ;

- --gtk-no-debug ;

- --gdk-debug ;

- --gdk-no-debug ;

- --display ;

- --sync ;

- --name ;

- --class.

Elle les retire alors de la liste des paramètres, et laisse tout ce qu'elle n'a pas reconnu être analysé ou ignoré par notre application. Ces paramètres sont standards, ils sont acceptés par toutes les applications GTK+.

Aux lignes 9 à 15, on définit une classe Python nommée Base, dans laquelle est définie une méthode d'initialisation d'instance : __init__(). Cette fonction crée une fenêtre racine (ligne 11) et ordonne à GTK+ de l'afficher (ligne 12). La gtk.Window est créée avec l'argument gtk.WINDOW_TOPLEVEL, indiquant que l'on souhaite que l'aspect et le positionnement de le fenêtre soient pris en charge par le gestionnaire de fenêtres. Plutôt que de créer une fenêtre de 0x0 pixel, une fenêtre sans enfant a une taille par défaut de 200x200 pixels de manière à pouvoir être manipulée facilement.

Aux lignes 14 et 15, on définit la méthode boucle() qui appelle la fonction main() de PyGTK. Cette dernière initialise la boucle principale du traitement des événements de GTK, pour capter les événements de la souris, du clavier, même des fenêtres.

Les lignes 18 à 20 permettent au programme de démarrer automatiquement s'il est appelé directement ou passé comme argument à l'interpréteur Python ; dans ces deux cas, le nom du programme contenu dans la variable Python __name__ sera la chaine de caractères "__main__" et le code des lignes 18-20 sera exécuté. Par contre, si le programme est importé dans un interpréteur Python en cours de fonctionnement, les lignes 18-20 seront ignorées.

À la ligne 19, on crée une instance de la classe Base nommée base. Ce qui a pour effet de créer et d'afficher une gtk.Window.

La ligne 20 invoque la méthode main() de la classe Base, qui démarre la boucle du traitement des événements de GTK+. Une fois ce point atteint, GTK se met en attente d'événements en provenance de X (un clic sur un bouton, une touche enfoncée, etc.), de timeouts ou de notifications d'entrées-sorties fichier. Dans notre exemple tout simple, ils seront cependant ignorés.

II-A - "Hello World" en PyGTK

Voici maintenant un programme avec un widget (un bouton). Il s'agit de la version PyGTK des classiques "Hello World" (helloworld.py).

1. # !/usr/bin/env python

2. # coding: utf8

3. # exemple helloworld.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class SalutMonde:

11. # Ceci est une fonction de rappel. Les arguments sont ignorés dans

12. # cet exemple. Plus de précisions sur ces fonctions plus bas.

13. def salut(self, widget, donnees=None):

14. print "Salut tout le monde !"

16. def evnmt_delete(self, widget, evenement, donnees=None):

17. # Si on renvoie FALSE dans le gestionnaire du signal "evnmt_delete",

18. # GTK émettra le signal "destroy". Renvoyer TRUE signifie que l'on

19. # ne veut pas que la fenêtre soit détruite.

20. # Ceci peut être utile pour faire apparaitre des fenêtres

21. # du type "Êtes-vous sûr de vouloir quitter ?"

22. print "Événement delete survenu."

24. # Changez FALSE par TRUE et la fenêtre principale ne

25. # sera pas détruite par un "delete_event"

26. return False

28. def destroy(self, widget, donnees=None):

29. print "Événement destroy survenu."

30. gtk.main_quit()

32. def __init__(self):

33. # création d'une nouvelle fenêtre

34. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

36. # Quand la fenêtre reçoit le signal "delete_event" (donné par le

37. # gestionnaire de fenêtres, généralement par l'option "fermer" ou

38. # la croix de la barre de titre), on lui demande d'appeler la

39. # fonction evnmt_delete() définie plus haut. On lui passe l'argument

40. # NULL, qui est ignoré.

41. self.fenetre.connect("delete_event", self.evnmt_delete)

43. # Ici on connecte l'événement "destroy" à un gestionnaire de signal.

44. # Cet événement se produit lorsqu'on invoque gtk_widget_destroy() sur

45. # la fenêtre ou lorsque le gestionnaire du signal "delete" renvoie FALSE.

46. self.fenetre.connect("destroy", self.destroy)

48. # On fixe la largeur des bordures de la fenêtre.

49. self.fenetre.set_border_width(10)

51. # On crée un nouveau bouton avec l'étiquette "Salut tout le monde !".

52. self.bouton = gtk.Button("Salut tout le monde !")

54. # Lorsque le bouton reçoit le signal "clicked", il appelle la

55. # fonction salut() et lui passe None comme argument. La fonction

56. # salut() est définie plus haut.

57. self.bouton.connect("clicked", self.salut, None)

59. # Ceci entrainera la destruction de la fenêtre par l'appel de

60. # gtk_widget_destroy(fenetre) si l'on clique le bouton. Encore une fois,

61. # le signal "destroy" peut venir d'ici ou du gestionnaire de fenêtres.

62. self.bouton.connect_object("clicked", gtk.Widget.destroy, self.fenetre)

64. # On place le bouton dans la fenêtre (un conteneur GTK).

65. self.fenetre.add(self.bouton)

67. # La dernière étape consiste à afficher ce nouveau widget...

68. self.bouton.show()

70. # … ainsi que la fenêtre

71. self.fenetre.show()

73. def boucle(self):

74. # Toutes les applications PyGTK doivent avoir un gtk.main(). Arrivé à ce point,

75. # le programme se met en attente d'un événement (clic, appui d'une touche, etc.)

76. gtk.main()

78. # Si le programme est lancé directement ou passé en argument à l'interpréteur

79. # Python, ceci crée une instance de la classe SalutMonde et l'affiche

80. if __name__ == "__main__":

81. salut = SalutMonde()

82. salut.boucle()

La Figure 2.2, « Exemple "Salut tout le monde !" » montre la fenêtre obtenue avec helloworld.py.

Figure 2.2. Exemple "Salut tout le monde !"

Les variables et fonctions du module PyGTK portent toutes un nom de la forme gtk.*. Par exemple, le programme helloworld.py fait appel à :

1. False

2. gtk.main_quit()

3. gtk.Window()

4. gtk.Button()

qui appartiennent au module PyGTK. Dans les prochaines sections, le préfixe gtk ne sera pas toujours précisé, mais il sera à chaque fois sous-entendu. Les programmes d'exemple, évidemment, l'utiliseront.

II-B - Le principe des signaux et des rappels

Nous n'entrerons pas dans le détail des différences entre les extensions du système de signaux de GLib 2.0 par rapport à celui de GTK 1.2. Les utilisateurs de PyGTK ne devraient pas remarquer de différence.

Avant de nous lancer dans une observation détaillée de helloworld.py, il nous faut expliquer les concepts de signal et de fonction de rappel. GTK est une boite à outils évènementielle, ce qui signifie qu'elle restera inactive dans gtk.main() jusqu'à ce qu'un événement survienne et que le relai soit passé à la fonction appropriée.

Ce passage de relais s'effectue par l'intermédiaire d'un « signal » (notez que ces signaux n'ont rien à voir avec les signaux système Unix, et ne sont pas implémentés en les utilisant, quoique la terminologie soit semblable). Lorsqu'un événement survient, comme un clic de souris, le signal correspondant est « émis » par le widget sur lequel on clique. C'est de cette façon que GTK réalise la plupart de son travail. Il y a des signaux dont tous les widgets héritent, comme "destroy", tandis que d'autres sont spécifiques à un widget, comme le "toggled" des boutons interrupteurs.

Pour qu'un bouton effectue une action, on définit un gestionnaire de signal qui sera chargé de capter ces signaux et d'appeler la fonction appropriée. On utilise alors une méthode de GtkWidget (de la classe GObject) comme ceci :

gestionnaire_id = objet.connect(nom, fonction, donnees_fct)

où objet est l'instance de GtkWidget qui doit émettre le signal. Le premier argument, nom, est une chaine de caractères donnant le nom du signal que l'on veut intercepter. Le deuxième argument, fonction, est la fonction qui devra être appelée une fois le signal intercepté. Enfin, donnees_fct représente les données que l'on souhaite passer à cette fonction. La méthode renvoie un identifiant de gestionnaire gestionnaire_id, que l'on pourra utiliser pour déconnecter ou bloquer le gestionnaire.

La fonction indiquée en deuxième argument est une « fonction de rappel », et devrait généralement avoir la forme :

def fct_rappel(widget, donnees_fct):

où le premier argument est un pointeur vers le widget qui a émis le signal, et le second (donnees_fct) un pointeur vers les données fournies en dernier argument à la méthode connect() ci-dessus.

Si la fonction de rappel est une méthode d'objet, elle aura généralement la forme suivante :

def meth_rappel(self, widget, donnees_meth):

où self est l'instance d'objet invoquant la méthode. C'est la forme utilisée dans le programme exemple helloworld.py.

La forme indiquée ci-dessus pour une déclaration de fonction de rappel n'est qu'un modèle général. Certains signaux spécifiques à un widget engendrent des paramètres d'appel différents.

Dans l'exemple helloworld.py, on trouve aussi un appel de la forme :

handler_id = object.connect_object(name, func, slot_object)
gestionnaire_id = objet.connect_object(nom, fonction, objet)

connect_object() est semblable à connect() à ceci près que la fonction de rappel ne nécessite qu'un seul argument et deux arguments pour la méthode :

def fct_rappel(objet)
def meth_rappel(self, objet)

où objet est habituellement un widget. connect_object() permet aux méthodes de widget PyGTK qui ne prennent qu'un seul argument (self) d'être utilisées comme gestionnaires de signaux.

II-C - Événements

En plus du système de signaux décrit auparavant, un certain nombre d'événements reflètent le système évènementiel de X, des fonctions de rappel peuvent être associées à ceux-ci. En voici une liste :

1. event

2. button_press_event

3. button_release_event

4. scroll_event

5. motion_notify_event

6. delete_event

7. destroy_event

8. expose_event

9. key_press_event

10. key_release_event

11. enter_notify_event

12. leave_notify_event

13. configure_event

14. focus_in_event

15. focus_out_event

16. map_event

17. unmap_event

18. property_notify_event

19. selection_clear_event

20. selection_request_event

21. selection_notify_event

22. proximity_in_event

23. proximity_out_event

24. visibility_notify_event

25. client_event

26. no_expose_event

27. window_state_event

Pour connecter une fonction de rappel à l'un de ces événements, on utilise la méthode connect() décrite à la section précédente, en mentionnant l'un des noms d'événement ci-dessus comme paramètre nom. Les fonctions (ou méthodes) de rappel employées pour les événements sont légèrement différentes de celles utilisées pour les signaux :

1. def fct_rappel(widget, evenement, donnees_fct):

3. def meth_rappel(self, widget, evenement, donnees_meth):

GdkEvent est un type d'objet Python dont l'attribut type indique lequel des événements ci-dessus est survenu. Les autres attributs de l'événement dépendent du type de celui-ci. Les valeurs pour les types peuvent être les suivantes :

1. NOTHING

2. DELETE

3. DESTROY

4. EXPOSE

5. MOTION_NOTIFY

6. BUTTON_PRESS

7. _2BUTTON_PRESS

8. _3BUTTON_PRESS

9. BUTTON_RELEASE

10. KEY_PRESS

11. KEY_RELEASE

12. ENTER_NOTIFY

13. LEAVE_NOTIFY

14. FOCUS_CHANGE

15. CONFIGURE

16. MAP

17. UNMAP

18. PROPERTY_NOTIFY

19. SELECTION_CLEAR

20. SELECTION_REQUEST

21. SELECTION_NOTIFY

22. PROXIMITY_IN

23. PROXIMITY_OUT

24. DRAG_ENTER

25. DRAG_LEAVE

26. DRAG_MOTION

27. DRAG_STATUS

28. DROP_START

29. DROP_FINISHED

30. CLIENT_EVENT

31. VISIBILITY_NOTIFY

32. NO_EXPOSE

33. SCROLL

34. WINDOW_STATE

35. SETTING

On accède à ces valeurs en préfixant le type d'événement avec gtk.gdk. Par exemple : gtk.gdk.DRAG_ENTER.

En résumé, pour connecter une fonction de rappel à un de ces événements, on peut procéder ainsi :

bouton.connect("button_press_event", fct_rappel_bouton)

Ainsi, le fait de cliquer avec la souris lorsque le pointeur se trouve au-dessus du bouton invoque la fonction fct_rappel_bouton. On peut définir cette fonction comme ceci :

def fct_rappel_bouton(widget, evenement, donnees):

La valeur renvoyée par cette fonction indique si l'événement doit être propagé par le système de gestion des événements de GTK+. Renvoyer True indique que l'événement a été traité et que sa diffusion s'arrête ici. En revanche, False laisse se poursuivre le processus normal de traitement de l'événement. Voir le Chapitre 20, Gestion avancée des signaux etévénements pour en apprendre plus sur ce processus de diffusion.

Les API de sélection et de glisser-déposer de GDK émettent également un certain nombre d'événements qui sont représentés dans GTK+ par des signaux. Voyez les sections 22.3.2 Les signaux du widget source et 22.3.4 Les signaux du widget destination (pas encore traduites), si vous souhaitez en apprendre plus sur les signatures des fonctions de rappel pour les signaux suivants :

1. selection_received

2. selection_get

3. drag_begin_event

4. drag_end_event

5. drag_data_delete

6. drag_motion

7. drag_drop

8. drag_data_get

9. drag_data_received

II-D - Le "Hello World" pas à pas

Après ces explications plutôt théoriques, revenons à présent à notre programme helloworld.py pour un examen plus détaillé.

Les lignes 9 à 76 définissent la classe HelloWorld. Celle-ci contient toutes les fonctions de rappel en tant que méthodes d'objet, ainsi qu'un constructeur (méthode d'initialisation d'instance). Attardons-nous sur les fonctions de rappel.

Aux lignes 13 et 14, on trouve la définition de la méthode de rappel salut(), qui sera invoquée par un clic sur le bouton et affichera "Salut tout le monde !" dans la console. Nous ignorons les paramètres (l'instance, le widget émetteur et les données transmises) dans cet exemple, mais la plupart des fonctions de rappel les utilisent. Le paramètre donnees est défini avec une valeur par défaut valant None, car Pygtk ne transmettrait pas de valeur de données si elle n'est pas incluse dans l'appel à connect(). Dans un tel cas, une erreur se produirait, car la fonction de rappel, qui attend obligatoirement trois paramètres, n'en recevrait que deux. Définir None comme valeur par défaut permet à la fonction de rappel d'être invoquée avec deux ou trois paramètres sans déclencher d'erreur. Dans le cas de notre exemple, on aurait pu se passer du paramètre donnees puisque la méthode salut() sera toujours appelée avec seulement deux paramètres (jamais elle ne recevra de données utilisateur). Notre prochain programme utilisera cet argument donnees pour nous dire quel bouton a été cliqué.

def salut(self, widget, donnees=None):
print "Salut tout le monde !"

La fonction de rappel suivante (lignes 16 à 26) est un peu spéciale. L'événement "delete_event" se produit lorsque le gestionnaire de fenêtres envoie cet événement à l'application. On a le choix quant à ce que l'on souhaite faire de ces événements : les ignorer, faire quelque chose en réponse ou simplement quitter l'application.

La valeur que l'on fait renvoyer par cette fonction de rappel fait connaître notre décision à GTK+. Renvoyer TRUE lui indique que l'on ne veut pas que le signal "destroy" soit émis, laissant l'application se poursuivre. En renvoyant FALSE, en revanche, nous demandons à ce que le signal "destroy" soit émis, lequel appellera le gestionnaire de signal "destroy". Les commentaires ont été retirés pour plus de clarté :

1. def evnmt_delete(widget, evenement, donnees=None):

2. print "Événement delete survenu."

3. return False

La fonction de rappel destroy() des lignes 28-30, entraine la sortie du programme par un appel à gtk.main_quit(). Cette fonction ordonne à GTK+ de sortir de gtk.main() lorsque le contrôle lui est rendu.

1. def destroy(self, widget, donnees=None):

2. print "Événement destroy survenu."

3. gtk.main_quit()

Les lignes 32 à 71 définissent la méthode d'initialisation d'instance __init__() de HelloWorld, qui crée la fenêtre et les widgets utilisés par le programme.

À la ligne 34, on crée une nouvelle fenêtre. Cependant, celle-ci ne s'affichera que lorsque GTK+ en recevra l'ordre, vers la fin du programme. On enregistre une référence à cette fenêtre dans un attribut d'instance (self.fenetre) pour pouvoir y accéder plus tard.

self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

Les lignes 41 et 46 illustrent deux exemples de connexion d'un gestionnaire de signal à un objet, en l'occurrence à la fenêtre. Ici, on capte les signaux "delete_event" et "destroy". Le premier est émis lorsque l'on utilise le gestionnaire de fenêtres pour fermer la fenêtre, ou par un appel à la méthode destroy() de GtkWidget. Le second est émis lorsque dans le gestionnaire du "delete_event", on renvoie FALSE.

self.fenetre.connect("delete_event", self.evnmt_delete)
self.fenetre.connect("destroy", self.destroy)

La ligne 49 précise un attribut d'un objet conteneur, ici la fenêtre, pour lui donner un espace vide de 10 pixels de large le long de son périmètre intérieur, où aucun widget ne pourra être placé. D'autres fonctions similaires existent, nous nous y intéresserons dans le Chapitre 18, Définir les attributs des widgets.

self.fenetre.set_border_width(10)

À la ligne 52, on crée un bouton et on en enregistre une référence dans self.bouton. Le bouton portera l'étiquette "Salut tout le monde !" lors de son affichage.

self.bouton = gtk.Button("Salut tout le monde !")

À la ligne 57, on attache un gestionnaire de signal au bouton, de sorte que la méthode de rappel salut() soit invoquée lorsqu'il émettra le signal "clicked". Nous n'avons aucune donnée à transmettre à salut(), aussi nous passons la valeur None (rien) en lieu de paramètre donnees. Comme l'on pourrait s'y attendre, le signal "clicked" est émis lorsque l'on clique sur le bouton de la souris. Indiquer la valeur du paramètre de données utilisateur None n'est pas exigé et pourrait être omis. On appellerait alors la fonction de rappel avec un paramètre de moins.

self.bouton.connect("clicked", self.salut, None)

Nous voulons aussi utiliser ce bouton pour quitter notre programme. Comme nous l'avons déjà vu, le signal "destroy" peut être émis par le gestionnaire de fenêtres, mais également par le programme lui-même, ce qu'illustre la ligne 62. Lorsque l'on clique sur le bouton, celui-ci appelle tout d'abord la méthode de rappel salut(), puis la suivante, dans le même ordre où elles ont été définies. On peut attacher autant de fonctions de rappel que l'on souhaite à un objet, elles seront toutes exécutées dans l'ordre dans lequel on les a connectées.

Puisque nous voulons utiliser la méthode destroy() de GtkWidget et que celle-ci n'accepte qu'un argument (le widget à détruire — la fenêtre dans notre cas), nous employons la méthode connect_object et lui transmettons une référence à notre fenêtre. La méthode connect_object transmettra le paramètre fenêtre comme premier argument au lieu du bouton.

L'appel de la méthode destroy() de gtk.Widget déclenchera l'émission du signal "destroy" par la fenêtre. Ceci entrainera à son tour l'appel de la méthode destroy() de notre classe HelloWorld pour terminer le programme.

self.bouton.connect_object("clicked", gtk.Widget.destroy, self.fenetre)

La ligne 65 est un appel de placement, ce que nous verrons en détail un peu plus tard dans le Chapitre 4, Le placement des widgets. Quoi qu'il en soit, cet appel est assez facile à comprendre : il indique juste à GTK+ que le bouton doit être placé dans la fenêtre, où il s'affichera. À noter qu'un conteneur GTK+ ne peut comporter qu'un seul widget. D'autres widgets, destinés à en recevoir plusieurs, de différentes manières, seront décrits plus loin.

self.fenetre.add(self.bouton)

Maintenant que tout est mis en place comme nous le voulions (les gestionnaires de signaux, le bouton placé dans sa fenêtre…), nous demandons à GTK+ d'afficher les widgets à l'écran (lignes 68 et 71). Le widget "fenêtre" est affiché en dernier de sorte qu'il apparaisse avec son bouton déjà placé, et non que la fenêtre s'affiche vide, puis que le bouton apparaisse à l'intérieur, (même si l'on ne s'en apercevrait pas avec un si petit exemple).

1. self.bouton.show()

3. self.fenetre.show()

Les lignes 73 à 76 définissent la méthode boucle() qui appelle la fonction gtk.main().

def boucle(self):
gtk.main()

Les lignes 80 à 82 permettent au programme de se lancer automatiquement s'il est appelé directement ou en tant qu'argument de l'interpréteur Python. La ligne 81 crée une instance de la classe HelloWorld et en enregistre une référence dans la variable salut. Enfin, la ligne 82 appelle la méthode boucle() de HelloWorld afin d'initialiser la boucle de traitement d'événements de GTK+.

1. if __name__ == "__main__":

2. salut = HelloWorld()

3. salut.boucle()

Quand on clique sur un bouton GTK+, le widget émet un signal "clicked". Afin d'utiliser cette information, notre programme prévoit un gestionnaire de signal qui devra la capter et passer le contrôle à une fonction de notre choix. Dans notre exemple, quand on clique sur le bouton créé, la méthode salut() est appelée avec None comme argument. Ensuite, le gestionnaire suivant du signal "clicked" prend la main : il appelle la fonction destroy() du widget avec la fenêtre comme argument, entrainant par là l'émission du signal "destroy" par cette dernière. Ce signal est intercepté et appelle la méthode destroy() de notre classe HelloWorld.

On peut aussi choisir de fermer la fenêtre par le gestionnaire de fenêtres. Le signal "delete_event" sera alors émis et son gestionnaire invoquera la fonction de rappel evnmt_delete(). Si cette dernière renvoie TRUE, la fenêtre sera laissée telle quelle et rien ne se passera. Si, au contraire, elle renvoie FALSE, GTK+ émettra le signal "destroy" qui, évidemment, appellera la fonction de rappel du même nom. Et l'on quittera GTK.

III - Chapitre 3. Avançons

III-A - Compléments sur les gestionnaires de signaux

Regardons à nouveau la méthode connect() :

objet.connect(nom, fonction, donnees_fct)

La valeur de retour d'un appel connect() est un marqueur entier qui identifie votre fonction de rappel. Comme nous l'avons déjà vu, il est possible de définir autant de fonctions de rappel que l'on souhaite pour chaque signal et chaque objet ; elles s'exécuteront à tour de rôle, dans l'ordre où elles ont été attachées.

Ce marqueur nous permet de retirer une fonction de rappel de la liste en utilisant :

objet.disconnect(marqueur)

Donc, en passant le marqueur renvoyé par l'une des méthodes de connexion du signal, on peut déconnecter un gestionnaire de signal.

On peut aussi désactiver temporairement les gestionnaires de signaux grâce au couple de méthodes signal_handler_block() et signal_handler_unblock() :

1. objet.signal_handler_block(marqueur)

3. objet.signal_handler_unblock(marqueur)

III-B - Modification de notre "Hello World"

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple helloworld2.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class HelloWorld2:

11. # Notre fonction de rappel améliorée. Les données

12. # qui lui sont transmises sont affichées sur stdout.

13. def salut(self, widget, donnees):

14. print "Re-salut ! - Clic sur le %s" % donnees

16. # Une autre fonction de rappel

17. def evnmt_delete(self, widget, evenement, donnees=None):

18. gtk.main_quit()

19. return False

21. def __init__(self):

22. # Création d'une nouvelle fenêtre

23. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

25. # Nouvel appel, qui donne le titre "Salut les boutons !"

26. # à notre nouvelle fenêtre

27. self.fenetre.set_title("Salut les boutons !")

29. # Ici on définit juste un gestionnaire pour le signal

30. # delete_event (qui quittera GTK immédiatement)

31. self.fenetre.connect("delete_event", self.evnmt_delete)

33. # On fixe la largeur des bordures de la fenêtre.

34. self.fenetre.set_border_width(10)

36. # On crée une boite pour y placer des widgets. Nous y reviendrons

37. # en détail dans la section "placement". La boite n'est pas visible,

38. # c'est juste un outil pour disposer des widgets.

39. self.boite1 = gtk.HBox(False, 0)

41. # On place la boite dans la fenêtre principale.

42. self.fenetre.add(self.boite1)

44. # Création d'un nouveau bouton avec l'étiquette "Bouton 1".

45. self.bouton1 = gtk.Button("Bouton 1")

47. # Lorsque l'on cliquera sur le bouton, la ligne suivante appellera la

48. # méthode "salut" avec la chaine "bouton 1" comme argument.

49. self.bouton1.connect("clicked", self.salut, "bouton 1")

51. # Plutôt que add(), on utilise pack_start() afin de placer le bouton

52. # dans la boite invisible (qui a elle-même été placée dans la fenêtre).

53. self.boite1.pack_start(self.bouton1, True, True, 0)

55. # N'oubliez pas cette étape, elle fait savoir à GTK que la préparation

56. # de ce bouton est terminée et qu'il peut maintenant être affiché

57. self.bouton1.show()

59. # On répète les mêmes étapes pour créer un second bouton

60. self.bouton2 = gtk.Button("Bouton 2")

62. # On appelle la même fonction de rappel, mais avec un argument

63. # différent : la chaine de caractères "bouton 2".

64. self.bouton2.connect("clicked", self.salut, "bouton 2")

66. self.boite1.pack_start(self.bouton2, True, True, 0)

68. # L'ordre d'affichage des boutons n'est pas très important, mais je conseille

69. # d'afficher la fenêtre en dernier pour que tout s'affiche d'un seul coup.

70. self.bouton2.show()

71. self.boite1.show()

72. self.fenetre.show()

74. def boucle():

75. gtk.main()

77. if __name__ == "__main__":

78. salut = HelloWorld2()

79. boucle()

L'exécution de helloworld2.py produit la fenêtre représentée à la Figure 3.1, « Exemple de "Hello World" amélioré ».

Figure 3.1. Exemple de "Hello World" amélioré

À noter que cette fois, rien n'a été prévu pour sortir du programme, il faut utiliser le gestionnaire de fenêtres ou la ligne de commande. En guise d'exercice, vous pourriez ajouter un troisième bouton "Quitter" qui mettrait fin au programme. Amusez-vous aussi avec les différentes possibilités de la méthode pack_start() en lisant le prochain chapitre. Essayez de redimensionner la fenêtre et observez son comportement.

Voici un bref commentaire des parties du code qui ont changé par rapport à notre premier "Hello World" :

comme précisé plus haut, il n'y a pas de gestionnaire du signal "destroy" dans notre version améliorée.

La fonction de rappel salut() des lignes 13-14 est similaire à celle de la première version. La différence est qu'elle affiche désormais un message contenant les données qu'elle a reçues.

À la ligne 27, on définit une chaine de caractères qui s'affichera dans la barre de titre de la fenêtre (voir Figure 3.1, « Exemple de "Hello World" amélioré »).

La ligne 39 crée une boite horizontale (gtk.HBox) destinée à contenir les deux boutons créés aux lignes 45 et 60. On place cette boite dans le conteneur "fenêtre" à la ligne 42.

Aux lignes 49 et 64, on connecte la méthode salut() au signal "clicked" émis par les boutons. Chaque bouton définit une chaine de caractères différente qui sera transmise à la méthode salut() lors de son appel.

On place les boutons dans la boite horizontale aux lignes 53 et 66 ; ils seront affichés par GTK aux lignes 57 et 70.

Enfin, on affiche la boite et la fenêtre, respectivement aux lignes 71 et 72.

IV - Chapitre 4. Le placement des widgets

Pour créer une application, on aura la plupart du temps, besoin de mettre plusieurs widgets dans la même fenêtre. Notre premier exemple de programme (helloworld.py) n'utilisait qu'un seul widget, nous pouvions donc simplement l'ajouter à la fenêtre avec la méthode de GtkContainer add(). Mais si l'on souhaite mettre plusieurs widgets dans une fenêtre, comment contrôle-t-on la manière dont ils sont placés ? C'est ce que nous allons développer dans ce chapitre.

IV-A - Le principe des boites de placement

Le placement des widgets passe la plupart du temps par la création de boites. Il s'agit de conteneurs invisibles dans lesquels on peut placer nos widgets, et qui existent sous deux formes : la boite horizontale et la boite verticale. Lorsque l'on place des widgets dans une boite horizontale, ceux-ci sont insérés horizontalement, de gauche à droite ou de droite à gauche en fonction de l'appel utilisé. Dans une boite verticale, les widgets sont placés de haut en bas ou inversement. Vous pouvez utiliser toutes les combinaisons de boites que vous voulez à l'intérieur ou à l'extérieur d'autres boites pour obtenir l'effet recherché.

Pour créer une boite horizontale, on fait appel à gtk.HBox(), et pour une boite verticale, à gtk.VBox(). Pour placer des objets dans ces conteneurs, on utilise les méthodes pack_start() et pack_end(). La première les place de haut en bas dans une VBox et de gauche à droite dans une HBox. La seconde fait le contraire : de bas en haut dans une VBox et de droite à gauche dans une HBox. Ces méthodes nous permettent d'aligner nos widgets à gauche ou à droite, et d'obtenir exactement l'effet désiré en les alternant. Nous utiliserons pack_start() dans la majeure partie de nos exemples. Les objets placés à l'intérieur des boites peuvent être des widgets ou de nouveaux conteneurs. En fait, beaucoup de widgets sont déjà eux-mêmes des conteneurs, y compris le bouton (mais on n'utilise généralement qu'une étiquette à l'intérieur d'un bouton).

Grâce à ces appels et à leurs options, GTK sait exactement où et de quelle manière vous voulez placer vos widgets dans le conteneur. De leur définition dépendent de nombreux paramètres comme, par exemple, le redimensionnement automatique. Ces méthodes nous offrent une grande flexibilité dans le placement et la création de nos widgets.

IV-B - Les boites en détail

Du fait même de cette flexibilité, les boites de placement peuvent paraître un peu déroutantes au début. Il y a beaucoup d'options et il n'est pas toujours évident de comprendre leurs effets. Mais en somme, on peut dire qu'il y a cinq styles différents. La figure 4.1 montre ce qu'on obtient en lançant le programme boites.py avec l'argument 1 :

Figure 4.1. Cinq styles de placement

Chaque ligne contient une boite horizontale comportant plusieurs boutons. L'appel de placement que vous pouvez y lire est un abrégé de celui utilisé pour placer chacun des boutons de la ligne. Tous les boutons d'un même HBox sont placés de la même manière (mêmes arguments pour la méthode pack_start()).

Voici comment s'utilise la méthode pack_start().

boite.pack_start(child, expand, fill, padding)

boite est la boite dans laquelle vous allez placer l'objet child (enfant). Pour le moment, les objets seront des boutons. Nous allons donc voir comment placer des boutons dans des boites.

L'argument expand de pack_start() et pack_end() contrôle si les widgets doivent occuper tout l'espace disponible dans la boite (TRUE) — celle-ci s'étirera alors pour remplir la zone qui lui est allouée —, ou si la boite doit au contraire rétrécir jusqu'à la taille minimale nécessaire aux widgets (FALSE). Régler expand sur FALSE vous permettra d'aligner tous vos widgets à droite ou à gauche. Autrement, ils s'étaleront pour occuper tout l'espace de la boite. Le même effet pourrait être obtenu en utilisant uniquement une des deux fonctions pack_start() ou pack_end().

Grâce à l'argument fill, on peut ordonner que l'espace vide autour des widgets leur soit alloué (TRUE). Sinon, on ne touche à rien (FALSE), cet espace jouera le rôle d'un padding supplémentaire autour des objets. fill n'a d'effet que si expand vaut TRUE.

Les fonctions ou méthodes que nous découvrirons tout au long de ce tutoriel peuvent comporter, grâce à Python, des mot-clés et des valeurs par défaut. Le cas échéant, nous les mentionnerons dans la définition de leur fonction, comme à présent avec les méthodes pack_start() et pack_end() :

1. boite.pack_start(child, expand=True, fill=True, padding=0)

3. boite.pack_end(child, expand=True, fill=True, padding=0)

child, expand, fill et padding sont des mot-clés (ils apparaitront toujours en gras). On peut remarquer les valeurs par défaut des arguments expand, fill et padding. L'argument child, quant à lui, devra obligatoirement être spécifié.

La création d'une boite ressemble à ceci :

1. boite_h = gtk.HBox(homogeneous=gtk.FALSE, spacing=0)

3. boite_v = gtk.VBox(homogeneous=gtk.FALSE, spacing=0)

L'argument homogeneous (homogène) de gtk.HBox() et gtk.VBox() attribue ou pas la même taille à chaque objet dans la boite (la même largeur dans une HBox, ou la même hauteur dans une VBox). S'il vaut TRUE, on obtiendra à peu de choses près le même effet qu'en activant l'argument expand pour chaque objet.

Quelle est la différence entre spacing (défini lors de la création d'une boite) et padding (défini lors du placement d'un objet), qui fixent tous les deux un espacement ? spacing ajoute de l'espace entre les objets, et padding de chaque côté d'un objet. La figure 4.2 illustre cette différence en passant l'argument 2 à boites.py :

Figure 4.2. Placement avec spacing et padding

La figure 4.3 montre l'utilisation de la méthode pack_end() (passez 3 comme argument à boites.py). L'étiquette "fin" est placée avec cette méthode. Elle colle à l'extrémité droite de la fenêtre, même en cas de redimensionnement.

Figure 4.3. Placement avec pack_end()

IV-C - Démonstration de placement avec les boites

Voici le code qui a généré les images de la section précédente. Il est assez abondamment commenté, aussi j'espère que vous n'aurez pas de problème à le suivre. Lancez-le et modifiez-le à souhait.

En route pour une brève visite guidée du code :

1. #!/usr/bin/env python

3. # exemple boite.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

8. import sys, string

10. # Cette fonction nous aide a créer une nouvelle HBox remplie de boutons+étiquettes.

11. # Des arguments pour les variables qui nous intéressent lui sont transmis. On n'affiche

12. # pas la boite (avec show()), mais on affiche tout ce qui se trouve a l'intérieur.

14. def creer_boite(homogeneous, spacing, expand, fill, padding):

16. # On crée une nouvelle boite avec les valeurs

17. # transmises pour "homogeneous" et "spacing"

18. boite = gtk.HBox(homogeneous, spacing)

20. # On crée une série de boutons, toujours avec les valeurs transmises.

21. bouton = gtk.Button("boite.pack")

22. boite.pack_start(bouton, expand, fill, padding)

23. bouton.show()

25. bouton = gtk.Button("(bouton,")

26. boite.pack_start(bouton, expand, fill, padding)

27. bouton.show()

29. # On crée un bouton dont l'étiquette dépendra de la valeur

30. # de "expand".

31. if expand == True:

32. bouton = gtk.Button("True,")

33. else:

34. bouton = gtk.Button("False,")

36. boite.pack_start(bouton, expand, fill, padding)

37. bouton.show()

39. # Même principe que ci-dessus, mais cette fois avec la

40. # formulation abrégée

41. bouton = gtk.Button(("False,", "True,")[fill==True])

42. boite.pack_start(bouton, expand, fill, padding)

43. bouton.show()

45. chaine_pad = "%d)" % padding

47. bouton = gtk.Button(chaine_pad)

48. boite.pack_start(bouton, expand, fill, padding)

49. bouton.show()

50. return boite

52. class BoitesPLacement:

53. def evnmt_delete(self, widget, evenement, donnees=None):

54. gtk.main_quit()

55. return False

57. def __init__(self, numero):

59. # Création de notre fenêtre

60. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

62. # N'oubliez pas de connecter le signal "delete_event"

63. # a la fenêtre principale. Cela donne au programme

64. # un comportement plus intuitif.

65. self.fenetre.connect("delete_event", self.evnmt_delete)

66. self.fenetre.set_border_width(10)

68. # On crée une boite verticale (VBox) pour y placer nos boites

69. # horizontales. Ainsi on peut empiler les boites horizontales

70. # remplies de boutons les unes sur les autres dans la VBox.

71. boite1 = gtk.VBox(False, 0)

73. # Sur quel scenario partons-nous ? (cf. images de la section 4.2)

74. if numero == 1:

75. # Création d'une nouvelle etiquette.

76. etiquette = gtk.Label("HBox(False, 0)")

78. # On aligne l'étiquette à gauche. Nous verrons cette méthode

79. # ainsi que d'autres dans la section sur les attributs des widgets.

80. etiquette.set_alignment(0, 0)

82. # On place l'étiquette dans la boite verticale (la VBox boite1).

83. # Souvenez-vous que les widgets ajoutés à une VBox seront placés

84. # les uns sur les autres, dans l'ordre.

85. boite1.pack_start(etiquette, False, False, 0)

87. # On affiche l'étiquette.

88. etiquette.show()

90. # On appelle notre fonction creer_boite en lui transmettant :

91. # homogeneous=False, spacing=0, expand=False, fill=False, padding=0

92. boite2 = creer_boite(False, 0, False, False, 0)

93. boite1.pack_start(boite2, False, False, 0)

94. boite2.show()

96. # On appelle notre fonction creer_boite en lui transmettant :

97. # homogeneous=False, spacing=0, expand=True, fill=False, padding=0

98. boite2 = creer_boite(False, 0, True, False, 0)

99. boite1.pack_start(boite2, False, False, 0)

100. boite2.show()

102. # homogeneous=False, spacing=0, expand=True, fill=True, padding=0

103. boite2 = creer_boite(False, 0, True, True, 0)

104. boite1.pack_start(boite2, False, False, 0)

105. boite2.show()

107. # Création d'un séparateur. On en apprendra plus sur ce widget

108. # ultérieurement, mais il est assez simple.

109. separateur = gtk.HSeparator()

111. # On place le séparateur dans la VBox. Gardez bien à l'esprit que

112. # tous ces widgets sont placés dans une VBox, ils sont donc

113. # empilés les uns sur les autres.

114. boite1.pack_start(separateur, False, True, 5)

115. separateur.show()

117. # On crée une autre étiquette, et on l'affiche.

118. etiquette = gtk.Label("HBox(True, 0)")

119. etiquette.set_alignment(0, 0)

120. boite1.pack_start(etiquette, False, False, 0)

121. etiquette.show()

123. # homogeneous=True, spacing=0, expand=True, fill=False, padding=0

124. boite2 = creer_boite(True, 0, True, False, 0)

125. boite1.pack_start(boite2, False, False, 0)

126. boite2.show()

128. # homogeneous=True, spacing=0, expand=True, fill=True, padding=0

129. boite2 = creer_boite(True, 0, True, True, 0)

130. boite1.pack_start(boite2, False, False, 0)

131. boite2.show()

133. # Un autre separateur.

134. separateur = gtk.HSeparator()

135. # Les trois derniers arguments de pack_start() sont :

136. # expand, fill, padding.

137. boite1.pack_start(separateur, False, True, 5)

138. separateur.show()

139. elif numero == 2:

140. # On crée une étiquette, souvenez-vous que boite1 est une VBox,

141. # créée vers le début de __init__()

142. etiquette = gtk.Label("HBox(False, 10)")

143. etiquette.set_alignment( 0, 0)

144. boite1.pack_start(etiquette, False, False, 0)

145. etiquette.show()

147. # homogeneous=False, spacing=10, expand=True, fill=False, padding=0

148. boite2 = creer_boite(False, 10, True, False, 0)

149. boite1.pack_start(boite2, False, False, 0)

150. boite2.show()

152. # homogeneous=False, spacing=10, expand=True, fill=True, padding=0

153. boite2 = creer_boite(False, 10, True, True, 0)

154. boite1.pack_start(boite2, False, False, 0)

155. boite2.show()

157. separateur = gtk.HSeparator()

158. # Les trois derniers arguments de pack_start() sont :

159. # expand, fill, padding.

160. boite1.pack_start(separateur, False, True, 5)

161. separateur.show()

163. etiquette = gtk.Label("HBox(False, 0)")

164. etiquette.set_alignment(0, 0)

165. boite1.pack_start(etiquette, False, False, 0)

166. etiquette.show()

168. # homogeneous=False, spacing=0, expand=True, fill=False, padding=10

169. boite2 = creer_boite(False, 0, True, False, 10)

170. boite1.pack_start(boite2, False, False, 0)

171. boite2.show()

173. # homogeneous=False, spacing=0, expand=True, fill=True, padding=10

174. boite2 = creer_boite(False, 0, True, True, 10)

175. boite1.pack_start(boite2, False, False, 0)

176. boite2.show()

178. separateur = gtk.HSeparator()

179. # Les trois derniers arguments de pack_start() sont :

180. # expand, fill, padding.

181. boite1.pack_start(separateur, False, True, 5)

182. separateur.show()

184. elif numero == 3:

186. # Ce scenario est la pour montrer qu'avec pack_end() on peut aligner

187. # les widgets à droite. D'abord, créons une boite, comme avant.

188. boite2 = creer_boite(False, 0, False, False, 0)

190. # on crée l'étiquette que l'on alignera à droite.

191. etiquette = gtk.Label("fin")

192. # On la place avec pack_end(), elle va se placer à l'extrémité

193. # droite de la HBox créée par l'appel à creer_boite().

194. boite2.pack_end(etiquette, False, False, 0)

195. # On affiche l'étiquette.

196. etiquette.show()

198. # On place boite2 dans boite1

199. boite1.pack_start(boite2, False, False, 0)

200. boite2.show()

202. # Un séparateur pour le bas.

203. separateur = gtk.HSeparator()

205. # Cette méthode fixe les dimensions du séparateur (400 pixels de large

206. # pour 5 pixels de haut). C'est pour que la HBox qu'on vient de créer

207. # fasse aussi 400 de large, et que l'étiquette "fin" soit bien séparée

208. # des autres widgets de la HBox. Autrement, tous les widgets de la

209. # HBox seraient placés aussi près l'un de l'autre que possible.

210. separateur.set_size_request(400, 5)

211. # On place le séparateur dans la HBox (boite1) créée vers le début

212. # de __init__()

213. boite1.pack_start(separateur, False, True, 5)

214. separateur.show()

216. # On crée une autre HBox. On peut en utiliser autant qu'on veut !

217. boitequitter = gtk.HBox(False, 0)

219. # Notre bouton "Quitter".

220. bouton = gtk.Button("Quitter")

222. # Lorsque l'on clique sur le bouton, le signal émis doit terminer le programme.

223. bouton.connect_object("clicked", gtk.main_quit, self.fenetre)

224. # On place le bouton dans la HBox boitequitter.

225. # Les trois derniers arguments de pack_start() sont :

226. # expand, fill, padding.

227. boitequitter.pack_start(bouton, True, False, 0)

228. # On place boitequitter dans la VBox (boite1)

229. boite1.pack_start(boitequitter, False, False, 0)

231. # On place la VBox boite1 (qui contient maintenant tous nos widgets)

232. # dans la fenêtre principale.

233. self.fenetre.add(boite1)

235. # Et on affiche ce qu'il reste à afficher.

236. bouton.show()

237. boitequitter.show()

239. boite1.show()

240. # La fenêtre en dernier, afin que tout s'affiche d'un coup.

241. self.fenetre.show()

243. def main():

244. # Et bien sûr, notre boucle principale.

245. gtk.main()

246. # Le contrôle revient ici lorsque main_quit() est appelée.

247. return 0

249. if __name__ =="__main__":

250. if len(sys.argv) != 2:

251. sys.stderr.write("La commande doit être de la forme \"boite.py num\", avec num = 1 ou 2 ou 3\n")

252. sys.exit(1)

253. BoitesPLacement(string.atoi(sys.argv[1]))

254. main()

De la ligne 14 à la ligne 50, on définit une fonction creer_boite() qui nous facilitera la création des boites horizontales. À chaque appel, elle créera une HBox et la remplira de cinq boutons en respectant les paramètres spécifiés. Sa valeur de retour est une référence à elle-même.

Puis on définit le constructeur __init__() de la classe BoitesPLacement (lignes 52 à 241), qui crée une fenêtre et une boite verticale enfant. Cette dernière contiendra d'autres widgets dont la nature et la disposition dépendent de l'argument qu'elle reçoit. Si elle reçoit 1, elle crée une fenêtre montrant les cinq arrangements de widgets que l'on peut obtenir en jouant sur les paramètres homogeneous , expand et fill (lignes 75 à 138). Si elle reçoit 2, elle crée une fenêtre qui affiche cette fois-ci les différentes combinaisons de remplissage offertes par les paramètres spacing et padding (lignes 140 à 182). Enfin, en recevant 3, la fenêtre créée par la fonction montrera l'utilisation des méthodes pack_start() et pack_end(), la première pour aligner les boutons à gauche et la seconde pour aligner une étiquette à droite (lignes 188 à 214). Aux lignes 215 à 235, on crée une boite horizontale contenant un bouton, et on la place dans la boite verticale. Le signal "clicked" du bouton est connecté à la fonction PyGTK main_quit() pour terminer le programme.

Aux lignes 250 à 252, on vérifie la ligne de commande : celle-ci doit comporter un seul argument. Dans le cas contraire, on sort du programme avec la fonction sys.exit(). La ligne 253 crée une instance de BoitesPLacement et la ligne 254 invoque la fonction main() qui va initialiser la boucle du réceptionnaire d'événements de GTK.

Dans ce programme exemple, aucune référence de widget n'est enregistrée dans un attribut d'instance (sauf pour la fenêtre). En effet, nos widgets ne seront plus sollicités après leur création.

IV-D - Le placement avec les tableaux

Intéressons-nous à présent à un autre moyen de placer les widgets, qui peut se révéler extrêmement utiles dans certaines occasions : les tableaux.

Avec les tableaux, on crée une grille dans laquelle on peut placer des widgets. Ces derniers peuvent occuper autant d'espace que nous le voulons.

Examinons tout d'abord la fonction gtk.Table() :

tableau = gtk.Table(rows=1, columns=1, homogeneous=FALSE)

Le premier argument est le nombre de lignes (rows) souhaitées dans le tableau et le deuxième le nombre de colonnes (columns).

L'argument homogeneous concerne l'homogénéité ou pas des cellules du tableau. S'il vaut TRUE, les cellules feront toutes la même taille : celle du plus grand widget dans le tableau. S'il vaut FALSE, la largeur d'une cellule sera dictée par le widget le plus large de la colonne, et sa hauteur par le widget le plus haut de la ligne.

Les lignes et les colonnes sont numérotées de 0 à n, n étant le nombre spécifié lors de l'appel à gtk.Table(). Donc, en donnant rows=2 et columns=2, on obtient un tableau qui peut être schématisé ainsi :

1. 1          2

2. 0+----------+----------+

3. |          |          |

4. 1+----------+----------+

5. |          |          |

6. 2+----------+----------+

Notez que le système de coordonnées commence dans le coin supérieur gauche. Pour placer un widget dans une cellule, on utilisera la méthode suivante :

tableau.attach(child, left_attach, right_attach, top_attach, bottom_attach,
xoptions=EXPAND|FILL, yoptions=EXPAND|FILL, xpadding=0, ypadding=0)

L'instance tableau est le tableau créé avec gtk.Table(). Le premier paramètre, child (enfant), est le widget que vous souhaitez placer dans le tableau.

Les arguments left_attach (point d'attache gauche), right_attach (point d'attache droit), top_attach (point d'attache haut) et bottom_attach (point d'attache bas) indiquent où placer le widget et combien de cellules il doit occuper. Si l'on veut mettre un bouton dans la cellule inférieure droite de notre tableau 2x2, et s'il ne doit occuper QUE cette cellule, il faudra donner à ces arguments les valeurs suivantes : left_attach = 1, right_attach = 2, top_attach = 1, bottom_attach = 2.

Maintenant, si l'on veut qu'un widget occupe toute la première ligne de notre tableau 2x2, on doit donner left_attach = 0, right_attach = 2, top_attach = 0, bottom_attach = 1.

Les paramètres xoptions et yoptions servent à spécifier une ou plusieurs (avec un OU logique) options de placement.

Ces options sont :

FILL

Si la cellule est plus large que le widget et que FILL est spécifié, le widget s'étirera jusqu'à occuper tout l'espace disponible dans la cellule.

SHRINK

Quand le tableau ne dispose pas de suffisamment d'espace pour afficher les widgets (par exemple, lors d'un redimensionnement de la fenêtre par l'utilisateur), ces derniers sont normalement éjectés vers le bas de la fenêtre et disparaissent. Si SHRINK est spécifié, les widgets rétréciront avec le tableau.

EXPAND

Si cette option est spécifiée, la cellule s'étirera jusqu'à occuper toute la place restant dans l'espace attribué au tableau.

Comme pour les boites de placement, les paramètres de padding créent une zone vide, spécifiée en pixels, autour du widget.

Deux autres méthodes, set_row_spacing() et set_col_spacing(), nous permettent de définir des espacements entre des lignes ou des colonnes :

tableau.set_row_spacing(row, spacing)

et

tableau.set_col_spacing(column, spacing)

Pour les colonnes (column), l'espacement (spacing) sera appliqué à la droite de la colonne spécifiée ; pour les lignes (rows) ce sera au-dessous de la ligne spécifiée.

On peut aussi fixer en une fois un même espacement pour toutes les lignes et/ou colonnes du tableau :

tableau.set_row_spacings(spacing)

et

tableau.set_col_spacings(spacing)

Notez qu'avec ces appels la dernière ligne et la dernière colonne ne reçoivent aucun espacement.

IV-E - Démonstration de placement avec les tableaux

Le programme exemple tableaux.py crée une fenêtre avec trois boutons dans un tableau de 2x2. Les deux premiers boutons sont placés sur la ligne du haut. Le troisième, le bouton "Quitter", est placé sur la ligne du bas et occupe les deux colonnes. La figure 4.4 montre la fenêtre obtenue :

Figure 4.4. Placement avec un tableau

Voici le code source :

1. #!/usr/bin/env python

3. # exemple tableau.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class Tableau:

10. # Notre fonction de rappel. Le paramètre "donnees"

11. # transmis a cette méthode est affiché sur stdout

12. def salut(self, widget, donnees=None):

13. print "Salut ! - Clic sur le %s." % donnees

15. # Cette fonction de rappel quitte le programme

16. def evnmt_delete(self, widget, evenement, donnees=None):

17. gtk.main_quit()

18. return False

20. def __init__(self):

21. # Création d'une nouvelle fenêtre.

22. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. # On définit le titre de la fenêtre.

25. self.fenetre.set_title("Tableau")

27. # On définit un gestionnaire de signal pour "delete_event",

28. # qui quitte GTK immédiatement.

29. self.fenetre.connect("delete_event", self.evnmt_delete)

31. # On fixe la largeur des bordures de la fenêtre.

32. self.fenetre.set_border_width(20)

34. # Création d'un tableau de 2x2.

35. tableau = gtk.Table(2, 2, True)

37. # On place le tableau dans la fenêtre principale.

38. self.fenetre.add(tableau)

40. # Création du premier bouton.

41. bouton = gtk.Button("bouton 1")

43. # Lorsque l'on clique sur le bouton, la méthode salut() est

44. # appelée, avec un pointeur sur "bouton 1" comme argument.

45. bouton.connect("clicked", self.salut, "bouton 1")

48. # Insertion du bouton 1 dans le quart supérieur gauche du tableau.

49. tableau.attach(bouton, 0, 1, 0, 1)

51. bouton.show()

53. # Création du second bouton.

55. bouton = gtk.Button("bouton 2")

57. # Lorsque l'on clique sur le bouton, la méthode "salut" est

58. # appelée, avec un pointeur sur "bouton 2" comme argument.

59. bouton.connect("clicked", self.salut, "bouton 2")

60. # Insertion du bouton 2 dans le quart supérieur droit du tableau.

61. tableau.attach(bouton, 1, 2, 0, 1)

63. bouton.show()

65. # Création du bouton "Quitter".

66. bouton = gtk.Button("Quitter")

68. # Lorsque l'on clique sur le bouton, la fonction mainquit est

69. # appelée et le programme se termine.

70. bouton.connect("clicked", gtk.main_quit)

72. # Insertion du bouton "Quitter" dans les deux quarts inférieurs du tableau.

73. tableau.attach(bouton, 0, 2, 1, 2)

75. bouton.show()

77. tableau.show()

78. self.fenetre.show()

80. def main():

81. gtk.main()

82. return 0

84. if __name__ =="__main__":

85. Tableau()

86. main()

La classe Tableau est définie de la ligne 9 à la ligne 78.

Aux lignes 12-13, on définit la fonction de rappel salut(), appelée par un clic sur le bouton 1 ou sur le bouton 2. Elle affiche juste un message dans la console qui indique quel bouton a été cliqué, et se sert pour cela de la chaine de caractères qui lui est transmise.

Aux lignes 16-18, on définit la méthode evnmt_delete(), qui est invoquée lorsque le gestionnaire de fenêtres tente de fermer la fenêtre principale.

De la ligne 20 à la ligne 78, on définit __init__(), la méthode constructeur de la classe Tableau. Elle crée une fenêtre (ligne 22), lui donne un titre (ligne 25), connecte la fonction de rappel evnmt_delete() au signal "delete_event" (ligne 29), et fixe les bordures de la fenêtre (ligne 32). Un gtk.Table est créé à la ligne 35 et est ajouté à la fenêtre principale à la ligne 38.

Puis les deux boutons du haut sont créés (lignes 41 et 55), on connecte leurs signaux "clicked" à la méthode salut() (lignes 45 et 59) et on les place dans la première ligne du tableau (lignes 49 et 61). Pour finir, on crée le bouton "Quitter" aux lignes 66 à 73, on connecte son signal "clicked" à la fonction mainquit(), et on le place sur toute l'étendue de la seconde ligne du tableau.

V - Chapitre 5. Vue d'ensemble des widgets

Avec PyGTK, l'utilisation d'un widget passe en général par les étapes suivantes :

- invoquer gtk.* - l'une des différentes fonctions de création de widget. Elles sont toutes détaillées dans cette section ;

- connecter tous les signaux et événements que l'on veut utiliser aux gestionnaires appropriés ;

- configurer les attributs du widget ;

- placer le widget dans un conteneur grâce à l'appel ad hoc : gtk.Container.add(), gtk.Box.pack_start(), etc. ;

- afficher le widget avec gtk.Widget.show().

show() fait savoir à GTK que la préparation du widget est achevée et qu'il peut maintenant être affiché. Pour le faire disparaître à nouveau, vous pouvez utiliser gtk.Widget.hide(). L'ordre d'affichage des widgets n'est pas important, mais je conseille d'afficher la fenêtre en dernier de sorte que celle-ci apparaisse complète, plutôt que de voir chaque widget s'y afficher à tour de rôle. Les enfants d'un widgets (une fenêtre est aussi un widget) ne seront pas affichés tant que la fenêtre elle-même ne l'aura pas été avec la méthode show().

V-A - Hiérarchie des widgets

Pour information, voici représentée sous forme d'arbre la hiérarchie des classes utilisées pour implémenter les widgets (les widgets obsolètes et les classes auxiliaires ont été omis).

1. gobject.GObject

2. |

3. gtk.Object

4. +gtk.Widget

5. | +gtk.Misc

6. | | +gtk.Label

7. | | | `gtk.AccelLabel

8. | | +gtk.Arrow

9. | | `gtk.Image

10. | +gtk.Container

11. | | +gtk.Bin

12. | | | +gtk.Alignment

13. | | | +gtk.Frame

14. | | | | `gtk.AspectFrame

15. | | | +gtk.Button

16. | | | | +gtk.ToggleButton

17. | | | | | `gtk.CheckButton

18. | | | | |   `gtk.RadioButton

19. | | | | `gtk.OptionMenu

20. | | | +gtk.Item

21. | | | | +gtk.MenuItem

22. | | | |   +gtk.CheckMenuItem

23. | | | |   | `gtk.RadioMenuItem

24. | | | |   +gtk.ImageMenuItem

25. | | | |   `gtk.TearoffMenuItem

26. | | | +gtk.Window

27. | | | | +gtk.Dialog

28. | | | | | +gtk.ColorSelectionDialog

29. | | | | | +gtk.FileSelection

30. | | | | | +gtk.FontSelectionDialog

31. | | | | | +gtk.InputDialog

32. | | | | | `gtk.MessageDialog

33. | | | | `gtk.Plug

34. | | | +gtk.EventBox

35. | | | +gtk.HandleBox

36. | | | +gtk.ScrolledWindow

37. | | | `gtk.Viewport

38. | | +gtk.Box

39. | | | +gtk.ButtonBox

40. | | | | +gtk.HButtonBox

41. | | | | `gtk.VButtonBox

42. | | | +gtk.VBox

43. | | | | +gtk.ColorSelection

44. | | | | +gtk.FontSelection

45. | | | | `gtk.GammaCurve

46. | | | `gtk.HBox

47. | | |   +gtk.Combo

48. | | |   `gtk.Statusbar

49. | | +gtk.Fixed

50. | | +gtk.Paned

51. | | | +gtk.HPaned

52. | | | `gtk.VPaned

53. | | +gtk.Layout

54. | | +gtk.MenuShell

55. | | | +gtk.MenuBar

56. | | | `gtk.Menu

57. | | +gtk.Notebook

58. | | +gtk.Socket

59. | | +gtk.Table

60. | | +gtk.TextView

61. | | +gtk.Toolbar

62. | | `gtk.TreeView

63. | +gtk.Calendar

64. | +gtk.DrawingArea

65. | | `gtk.Curve

66. | +gtk.Editable

67. | | +gtk.Entry

68. | | | `gtk.SpinButton

69. | +gtk.Ruler

70. | | +gtk.HRuler

71. | | `gtk.VRuler

72. | +gtk.Range

73. | | +gtk.Scale

74. | | | +gtk.HScale

75. | | | `gtk.VScale

76. | | `gtk.Scrollbar

77. | |   +gtk.HScrollbar

78. | |   `gtk.VScrollbar

79. | +gtk.Separator

80. | | +gtk.HSeparator

81. | | `gtk.VSeparator

82. | +gtk.Invisible

83. | +gtk.Preview

84. | `gtk.ProgressBar

85. | +gtk.Adjustment

86. +gtk.CellRenderer

87. | +gtk.CellRendererPixbuf

88. | +gtk.CellRendererText

89. | +gtk.CellRendererToggle

90. +gtk.ItemFactory

91. +gtk.Tooltips

92. `gtk.TreeViewColumn

V-B - Les widgets sans fenêtre

Les widgets ci-dessous n'ont pas de fenêtre associée. Si vous voulez capter des événements, il vous faudra utiliser une boiteàévénements.

1. gtk.Alignment

2. gtk.Arrow

3. gtk.Bin

4. gtk.Box

5. gtk.Button

6. gtk.CheckButton

7. gtk.Fixed

8. gtk.Image

9. gtk.Label

10. gtk.MenuItem

11. gtk.Notebook

12. gtk.Paned

13. gtk.RadioButton

14. gtk.Range

15. gtk.ScrolledWindow

16. gtk.Separator

17. gtk.Table

18. gtk.Toolbar

19. gtk.AspectFrame

20. gtk.Frame

21. gtk.VBox

22. gtk.HBox

23. gtk.VSeparator

24. gtk.HSeparator

Nous poursuivrons notre exploration de PyGTK en examinant chaque widget tour à tour et en créant quelques petits programmes simples pour les afficher.

VI - Chapitre 6. Les boutons

VI-A - Les boutons simples

Nous avons pratiquement déjà fait le tour du widget bouton. Il est en effet très simple. Lors de la création d'un bouton avec la fonction gtk.Button(), vous pouvez lui donner une étiquette en lui passant une chaine de caractères. Mais vous pouvez tout aussi bien créer un bouton vierge en ne passant rien à la fonction ; il vous suffira ensuite, si vous le souhaitez, de placer une étiquette ou une image "pixmap" dans ce nouveau bouton. Pour ce faire, créez une nouvelle boite et placez-y vos objets avec l'habituelle pack_start(). Puis, avec add(), placez la boite dans le bouton.

La fonction de création des boutons est la suivante :

bouton = gtk.Button(label=None, stock=None)

Si l'on donne une chaine de caractères à l'argument label, elle sera affichée sur le bouton. L'argument stock, quant à lui, sert à afficher sur le bouton une icône du stock de GTK ainsi que le texte correspondant. Les éléments du stock sont :

1. STOCK_DIALOG_INFO

2. STOCK_DIALOG_WARNING

3. STOCK_DIALOG_ERROR

4. STOCK_DIALOG_QUESTION

5. STOCK_DND

6. STOCK_DND_MULTIPLE

7. STOCK_ADD

8. STOCK_APPLY

9. STOCK_BOLD

10. STOCK_CANCEL

11. STOCK_CDROM

12. STOCK_CLEAR

13. STOCK_CLOSE

14. STOCK_CONVERT

15. STOCK_COPY

16. STOCK_CUT

17. STOCK_DELETE

18. STOCK_EXECUTE

19. STOCK_FIND

20. STOCK_FIND_AND_REPLACE

21. STOCK_FLOPPY

22. STOCK_GOTO_BOTTOM

23. STOCK_GOTO_FIRST

24. STOCK_GOTO_LAST

25. STOCK_GOTO_TOP

26. STOCK_GO_BACK

27. STOCK_GO_DOWN

28. STOCK_GO_FORWARD

29. STOCK_GO_UP

30. STOCK_HELP

31. STOCK_HOME

32. STOCK_INDEX

33. STOCK_ITALIC

34. STOCK_JUMP_TO

35. STOCK_JUSTIFY_CENTER

36. STOCK_JUSTIFY_FILL

37. STOCK_JUSTIFY_LEFT

38. STOCK_JUSTIFY_RIGHT

39. STOCK_MISSING_IMAGE

40. STOCK_NEW

41. STOCK_NO

42. STOCK_OK

43. STOCK_OPEN

44. STOCK_PASTE

45. STOCK_PREFERENCES

46. STOCK_PRINT

47. STOCK_PRINT_PREVIEW

48. STOCK_PROPERTIES

49. STOCK_QUIT

50. STOCK_REDO

51. STOCK_REFRESH

52. STOCK_REMOVE

53. STOCK_REVERT_TO_SAVED

54. STOCK_SAVE

55. STOCK_SAVE_AS

56. STOCK_SELECT_COLOR

57. STOCK_SELECT_FONT

58. STOCK_SORT_ASCENDING

59. STOCK_SORT_DESCENDING

60. STOCK_SPELL_CHECK

61. STOCK_STOP

62. STOCK_STRIKETHROUGH

63. STOCK_UNDELETE

64. STOCK_UNDERLINE

65. STOCK_UNDO

66. STOCK_YES

67. STOCK_ZOOM_100

68. STOCK_ZOOM_FIT

69. STOCK_ZOOM_IN

70. STOCK_ZOOM_OUT

Le programme boutons.py donne un exemple d'utilisation de gtk.Button() pour créer un bouton contenant une image et une étiquette. Le code de création de la boite est séparé du reste afin que vous puissiez l'utiliser dans vos programmes. Nous verrons d'autres exemples faisant appel à des images un peu plus loin dans ce tutoriel. La figure 6.1 montre la fenêtre et son bouton qui contient une étiquette et une image "pixmap" :

Figure 6.1. Bouton avec étiquette et image pixmap

Voici le code source du programme boutons.py :

1. #!/usr/bin/env python

3. # exemple boutons.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # On crée une boite verticale, on y place une image

10. # et une étiquette, et on renvoie la boite.

12. def boite_xpm_etiquette(parent, fichier_xpm, texte_etiquette):

13. # On crée une boite pour la pixmap et l'étiquette

14. boite1 = gtk.HBox(False, 0)

15. boite1.set_border_width(2)

17. # A présent l'image.

18. image = gtk.Image()

19. image.set_from_file(fichier_xpm)

21. # On crée une étiquette pour le bouton.

22. etiquette = gtk.Label(texte_etiquette)

24. # On place la pixmap et l'étiquette dans la boite.

25. boite1.pack_start(image, False, False, 3)

26. boite1.pack_start(etiquette, False, False, 3)

28. image.show()

29. etiquette.show()

30. return boite1

32. class Bouton:

33. # Notre méthode de rappel habituelle.

34. def salut(self, widget, donnees=None):

35. print "Salut ! - Clic sur le %s." % donnees

37. def __init__(self):

38. # Création d'une nouvelle fenêtre.

39. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

41. self.fenetre.set_title("Bouton et image")

43. # C'est une bonne idée de faire ceci pour chaque fenêtre.

44. self.fenetre.connect("destroy", lambda wid: gtk.main_quit())

45. self.fenetre.connect("delete_event", lambda a1,a2: gtk.main_quit())

47. # On fixe la largeur des bordures de la fenêtre.

48. self.fenetre.set_border_width(10)

50. # Création d'un nouveau bouton.

51. bouton = gtk.Button()

53. # On connecte le signal "clicked" du bouton à la fonction de rappel

54. bouton.connect("clicked", self.salut, "bouton cool")

56. # Ceci appelle notre fonction de création de boites.

57. boite1= boite_xpm_etiquette(self.fenetre, "info.xpm", "bouton cool")

59. # On place et on affiche tous nos widgets.

60. bouton.add(boite1)

62. boite1.show()

63. bouton.show()

65. self.fenetre.add(bouton)

66. self.fenetre.show()

68. def main():

69. gtk.main()

70. return 0

72. if __name__ == "__main__":

73. Bouton()

74. main()

De la ligne 12 à la ligne 32, on définit la fonction boite_xpm_etiquette() qui prend en charge la création d'une boite horizontale avec une bordure de 2 (lignes 14-15), puis on y place une image (lignes 25) et une étiquette (ligne 26).

Aux lignes 32-66, on définit la classe Bouton. La méthode constructeur s'étale de la ligne 38 à la ligne 66 ; elle crée une fenêtre (ligne 39), lui donne un titre (ligne 41), connecte les signaux "delete_event" et "destroy" (lignes 44-45). Puis, à la ligne 51, elle crée le bouton sans étiquette, avant de connecter son signal "clicked" à la méthode de rappel salut() (ligne 54). Enfin, elle appelle la fonction boite_xpm_etiquette() à la ligne 57, afin de créer l'image et l'étiquette qu'elle placera dans le bouton à la ligne 60.

La fonction boite_xpm_etiquette() pourrait être utilisée pour placer des images "pixmap" et des étiquettes dans n'importe quel widget pouvant jouer le rôle de conteneur.

Le widget bouton peut émettre les signaux suivants :

1. pressed - émis lorsque le bouton de la souris est enfoncé sur le widget

3. released - émis lorsque le bouton de la souris est relâché sur le widget

5. clicked - émis lorsque le bouton de la souris est enfoncé puis relâché sur le widget

7. enter - émis lorsque le pointeur de la souris arrive sur le widget

9. leave - émis lorsque le pointeur de la souris quitte le widget

VI-B - Les boutons à bascule

Le bouton à bascule dérive du bouton simple et lui est donc très similaire. Il a cependant ceci de différent qu'il possède deux états et qu'un clic le fait basculer de l'un à l'autre. Lorsqu'il est enfoncé, un clic le fait remonter. Un autre clic et le voilà de nouveau enfoncé.

Les boutons à bascule sont la base des cases à cocher et des boutons radio, ces derniers héritent donc de beaucoup de leurs appels (nous le signalerons le cas échéant).

Création d'un bouton à bascule :

bouton_bascule = gtk.ToggleButton(label=None)

Comme vous pouvez l'imaginer, le fonctionnement de cet appel est identique à celui des boutons normaux. Si aucune étiquette n'est spécifiée, le bouton sera vierge. Dans le cas contraire, le texte donné sera inspecté à la recherche de caractères mnémoniques (préfixés par '_').

Pour récupérer l'état d'un bouton à bascule, ainsi que des boutons radio et des cases à cocher, on fait appel à une construction semblable à l'exemple ci-dessous. Elle teste l'état du bouton en invoquant la méthode get_active() de gtk.ToggleButton. Parmi les signaux émis par les boutons à bascule, celui qui nous intéresse est "toggled". Pour vérifier l'état d'un bouton à bascule, on définira alors un gestionnaire de signal qui se chargera de le capter, puis qui accédera aux attributs de l'objet afin de déterminer son état. La fonction de rappel ressemblera à ceci :

1. def fct_rappel_bascule(widget, donnees):

2. if widget.get_active():

3. # si le contrôle arrive ici, le bouton à bascule est enfoncé

4. else:

5. # si le contrôle arrive ici, le bouton à bascule est relevé

Pour forcer l'état d'un bouton à bascule (et de ses descendants les cases à cocher et les boutons radio) on utilise la méthode :

bouton_bascule.set_active(is_active)

La méthode ci-dessus peut être utilisée pour imposer l'état d'un bouton à bascule, et de ses descendants, les cases à cocher et les boutons radio. En donnant les valeurs TRUE ou FALSE à l'argument is_active, on indique que le bouton doit être respectivement enfoncé ou relevé. Lors de sa création, un bouton à bascule est en position relevée (FALSE) par défaut.

Notez que lorsque l'on utilise la méthode set_active() et que l'état est effectivement modifié, les signaux "clicked" et "toggled" sont automatiquement émis par le bouton.

bouton_bascule.get_active()

Cette méthode renvoie l'état actuel du bouton à bascule sous la forme d'une valeur booléenne TRUE/FALSE.

Le programme boutonsbascule.py donne un exemple courant d'utilisation des boutons à bascule. La figure 6.2 montre la fenêtre obtenue, dans laquelle le deuxième bouton est enfoncé :

Figure 6.2. Exemple de boutons à bascule

Voici le code source du programme :

1. #!/usr/bin/env python

3. # exemple boutonsbascule.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class BoutonsBascule:

10. # Notre fonction de rappel. Le paramètre "donnees"

11. # transmis à cette méthode est affiché sur stdout

12. def fct_rappel(self, widget, donnees=None):

13. print "Le %s a ete %s." % (donnees, ("releve", "enfonce")[widget.get_active()])

15. # Cette fonction de rappel quitte le programme

16. def evnmt_delete(self, widget, evenement, donnees=None):

17. gtk.main_quit()

18. return gtk.FALSE

20. def __init__(self):

21. # Création d'une nouvelle fenêtre.

22. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. # On définit le titre de la fenêtre.

25. self.fenetre.set_title("Boutons a bascule")

27. # On définit un gestionnaire de signal pour "delete_event",

28. # qui quitte GTK immédiatement.

29. self.fenetre.connect("delete_event", self.evnmt_delete)

31. # On fixe la largeur des bordures de la fenêtre.

32. self.fenetre.set_border_width(20)

34. # Création d'une boite verticale.

35. boite_v = gtk.VBox(True, 2)

37. # On place la VBox dans la fenêtre principale.

38. self.fenetre.add(boite_v)

40. # Création du premier bouton.

41. bouton = gtk.ToggleButton("bouton a bascule 1")

43. # Lorsque l'on change l'état du bouton, la méthode fct_rappel() est

44. # appelée, avec un pointeur sur "bouton a bascule 1" comme argument.

45. bouton.connect("toggled", self.fct_rappel, "bouton a bascule 1")

48. # Insertion du bouton 1 dans le quart supérieur gauche du tableau.

49. boite_v.pack_start(bouton, True, True, 2)

51. bouton.show()

53. # Création du deuxième bouton.

55. bouton = gtk.ToggleButton("bouton a bascule 2")

57. # Lorsque l'on change l'état du bouton, la méthode fct_rappel() est

58. # appelée, avec un pointeur sur "bouton a bascule 2" comme argument.

59. bouton.connect("toggled", self.fct_rappel, "bouton a bascule 2")

60. # Insertion du bouton 2 dans le quart supérieur droit du tableau.

61. boite_v.pack_start(bouton, True, True, 2)

63. bouton.show()

65. # Création du bouton "Quitter".

66. bouton = gtk.Button("Quitter")

68. # Lorsque l'on clique sur le bouton, la fonction mainquit() est

69. # appelée et le programme se termine.

70. bouton.connect("clicked", lambda wid: gtk.main_quit())

72. # Insertion du bouton "Quitter" dans les deux quarts inférieurs du tableau.

73. boite_v.pack_start(bouton, True, True, 2)

75. bouton.show()

76. boite_v.show()

77. self.fenetre.show()

79. def main():

80. gtk.main()

81. return 0

83. if __name__ == "__main__":

84. BoutonsBascule()

85. main()

Les parties intéressantes sont les lignes 12-13, qui définissent la méthode fct_rappel() chargée d'afficher le nom et l'état du bouton à bascule quand celui-ci voit son état modifié, et les lignes 45 et 59, qui connectent le signal "toggled" des deux boutons à cette même méthode.

VI-C - Les cases à cocher

Les cases à cocher héritent de nombreuses propriétés et méthodes des boutons à bascule vus précédemment. Leur aspect est cependant différent : il ne s'agit plus de boutons avec du texte à l'intérieur, mais de petits carrés portant le texte à leur droite. Elles servent souvent pour activer/désactiver des options dans les applications.

La méthode de création est semblable à celle des boutons normaux :

case_a_cocher = gtk.CheckButton(label=None)

Si l'argument label est fourni, la méthode crée une case à cocher avec une étiquette à son côté. Le texte donné sera inspecté à la recherche de caractères mnémoniques (préfixés par '_').

Les opérations de vérification et de définition de l'état des cases à cocher sont identiques à celles des boutons à bascule.

Le programme casesacocher.py donne un exemple d'utilisation des cases à cocher. La figure 6.3 montre la fenêtre obtenue :

Figure 6.3. Exemple de cases à cocher

Voici le code source du programme casesacocher.py :

1. #!/usr/bin/env python

3. # exemple casesacocher.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class Casesacocher:

10. # Notre fonction de rappel. Le paramètre "donnees"

11. # transmis a cette méthode est affiché sur stdout

12. def fct_rappel(self, widget, donnees=None):

13. print "La %s a ete %s." % (donnees, ("desactivee", "activee")[widget.get_active()])

15. # Cette fonction de rappel quitte le programme

16. def evnmt_delete(self, widget, evenement, donnees=None):

17. gtk.main_quit()

18. return gtk.FALSE

20. def __init__(self):

21. # Création d'une nouvelle fenêtre.

22. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. # On définit le titre de la fenêtre.

25. self.fenetre.set_title("Cases a cocher")

27. # On définit un gestionnaire de signal pour "delete_event",

28. # qui quitte GTK immédiatement.

29. self.fenetre.connect("delete_event", self.evnmt_delete)

31. # On fixe la largeur des bordures de la fenêtre.

32. self.fenetre.set_border_width(20)

34. # Création d'une boite verticale.

35. boite_v = gtk.VBox(True, 2)

37. # On place la VBox dans la fenêtre principale.

38. self.fenetre.add(boite_v)

40. # Création du premier bouton.

41. bouton = gtk.CheckButton("case a cocher 1")

43. # Lorsque l'on change l'état du bouton, la méthode fct_rappel() est

44. # appelée, avec un pointeur sur "case à cocher 1" comme argument.

45. bouton.connect("toggled", self.fct_rappel, "case a cocher 1")

48. # Insertion du bouton 1 dans le quart supérieur gauche du tableau.

49. boite_v.pack_start(bouton, True, True, 2)

51. bouton.show()

53. # Création du deuxième bouton.

55. bouton = gtk.CheckButton("case a cocher 2")

57. # Lorsque l'on change l'état du bouton, la méthode fct_rappel() est

58. # appelée, avec un pointeur sur "case a cocher 2" comme argument.

59. bouton.connect("toggled", self.fct_rappel, "case a cocher 2")

60. # Insertion du bouton 2 dans le quart supérieur droit du tableau.

61. boite_v.pack_start(bouton, True, True, 2)

63. bouton.show()

65. # Création du bouton "Quitter".

66. bouton = gtk.Button("Quitter")

68. # Lorsque l'on clique sur le bouton, la fonction mainquit() est

69. # appelée et le programme se termine.

70. bouton.connect("clicked", lambda wid: gtk.main_quit())

72. # Insertion du bouton "Quitter" dans les deux quarts inférieurs du tableau.

73. boite_v.pack_start(bouton, True, True, 2)

75. bouton.show()

76. boite_v.show()

77. self.fenetre.show()

79. def main():

80. gtk.main()

81. return 0

83. if __name__ == "__main__":

84. Casesacocher()

85. main()

VI-D - Les boutons radio

Les boutons radio sont similaires aux cases à cocher, à ceci près qu'on les assemble par groupes, dans lesquels un seul bouton peut être sélectionné/enfoncé à la fois. Ils sont très pratiques pour les parties de vos applications où l'on doit effectuer une sélection parmi une courte liste.

La création d'un nouveau bouton radio s'effectue avec l'appel suivant :

bouton_radio = gtk.RadioButton(group=None, label=None)

Vous aurez remarqué le nouvel argument group pour cet appel. En effet, pour fonctionner correctement, les boutons radio nécessitent un groupe. Lors du premier appel à gtk.RadioButton(), l'argument group doit valoir None, ce qui créera un nouveau groupe comptant comme seul membre le nouveau bouton radio.

Pour ajouter de nouveaux boutons radio à un groupe, transmettez lors de vos appels à gtk.RadioButton() une référence à un bouton radio du groupe.

Si vous avez spécifié un texte pour l'argument label, il sera passé en revue à la recherche de caractères mnémoniques (préfixés par '_').

Pour définir explicitement le bouton qui doit être sélectionné par défaut, utilisez :

bouton_radio.set_active(is_active)

Cette méthode est décrite dans la section sur les boutons à bascule et fonctionne exactement de la même manière. Une fois que les boutons radio sont assemblés en un groupe, on ne pourra sélectionner qu'un seul bouton de ce groupe à la fois. Si l'utilisateur clique sur un bouton radio, puis sur un autre, alors le premier émettra un signal "toggled" pour informer qu'il a été désactivé, avant que le second l'imite, pour signaler cette fois qu'il vient d'être activé.

Le programme exemple boutonsradio.py crée un groupe de trois boutons radio. La figure 6.4 montre la fenêtre obtenue :

Figure 6.4. Exemple de boutons radio

Voici le code source du programme :

1. #!/usr/bin/env python

3. # exemple boutonsradio.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class BoutonsRadio:

10. def fct_rappel(self, widget, donnees=None):

11. print "Le %s a ete %s." % (donnees, ("desactive", "active")[widget.get_active()])

13. def quitter_pgm(self, widget, evenement, donnees=None):

14. gtk.main_quit()

15. return False

17. def __init__(self):

18. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

20. self.fenetre.connect("delete_event", self.quitter_pgm)

22. self.fenetre.set_title("Boutons radio")

23. self.fenetre.set_border_width(0)

25. boite1 = gtk.VBox(False, 0)

26. self.fenetre.add(boite1)

27. boite1.show()

29. boite2 = gtk.VBox(False, 10)

30. boite2.set_border_width(10)

31. boite1.pack_start(boite2, True, True, 0)

32. boite2.show()

34. bouton = gtk.RadioButton(None, "bouton radio 1")

35. bouton.connect("toggled", self.fct_rappel, "bouton radio 1")

36. boite2.pack_start(bouton, True, True, 0)

37. bouton.show()

39. bouton = gtk.RadioButton(bouton, "bouton radio 2")

40. bouton.connect("toggled", self.fct_rappel, "bouton radio 2")

41. bouton.set_active(True)

42. boite2.pack_start(bouton, True, True, 0)

43. bouton.show()

45. bouton = gtk.RadioButton(bouton, "bouton radio 3")

46. bouton.connect("toggled", self.fct_rappel, "bouton radio 3")

47. boite2.pack_start(bouton, True, True, 0)

48. bouton.show()

50. separateur = gtk.HSeparator()

51. boite1.pack_start(separateur, False, True, 0)

52. separateur.show()

54. boite2 = gtk.VBox(False, 10)

55. boite2.set_border_width(10)

56. boite1.pack_start(boite2, False, True, 0)

57. boite2.show()

59. bouton = gtk.Button("fermer")

60. bouton.connect_object("clicked", self.quitter_pgm,

61. self.fenetre, None)

62. boite2.pack_start(bouton, True, True, 0)

63. bouton.set_flags(gtk.CAN_DEFAULT)

64. bouton.grab_default()

65. bouton.show()

66. self.fenetre.show()

68. def main():

69. gtk.main()

70. return 0

72. if __name__ == "__main__":

73. BoutonsRadio()

74. main()

Le code est assez explicite. Aux lignes 63-64, on fait du bouton "fermer" le widget par défaut, de sorte qu'en appuyant sur la touche "Entrée" lorsque la fenêtre est active, il émette le signal "clicked".

VII - Chapitre 7. Les ajustements

GTK dispose de plusieurs widgets pouvant être réglés visuellement par l'utilisateur au moyen de la souris ou du clavier, comme les widgets d'intervalle, par exemple. D'autres existent qui affichent une partie ajustable d'une plus grande aire de données, comme la zone de texte (gtk.TextView) et la vue orientable (gtk.Viewport).

Une application doit être capable de réagir aux changements que l'utilisateur effectue dans les widgets d'intervalle. Pour ce faire, chaque widget que l'on ajuste pourrait émettre son propre type de signal, puis soit transmettre la nouvelle valeur au gestionnaire de signal, soit faire inspecter sa structure de données par ce dernier afin de vérifier la valeur. Mais il peut aussi arriver que l'on souhaite connecter ensemble les changements de plusieurs widgets, de sorte qu'en réglant l'un deux, on règle les autres. L'exemple le plus évident est le cas où l'on souhaite connecter une barre de défilement à une vue orientable ou à une zone de texte à défilement. Si chaque widget a sa propre façon de définir ou de récupérer la valeur d'ajustement, le programmeur devra certainement écrire un gestionnaire de signal pour chacun, ceci afin d'effectuer la traduction entre, d'un côté le signal émis par un widget, et de l'autre, les données reçues par la méthode d'ajustement d'un autre widget.

GTK résout ce problème avec l'objet Adjustment (ajustement, réglage), qui n'est pas un widget, mais un moyen pour les widgets de stocker et transmettre, sous une forme abstraite et flexible, des informations de réglage. L'utilisation la plus évidente de l'ajustement est de stocker les paramètres et valeurs de configuration des widgets d'intervalle, comme les barres de défilement ou les gradateurs. Mais ce n'est pas tout : du fait qu'il dérive de la classe Object, il offre des possibilités dont ne disposent pas les structures de données normales, en particulier la possibilité d'émettre des signaux, comme les widgets. On peut utiliser ces signaux pour faire réagir le programme aux modifications des widgets ajustables par l'utilisateur, mais également pour propager les valeurs d'ajustement entre ces widgets de manière transparente.

Certains widgets incorporent des ajustements. C'est le cas, entre autres, des barres de progression, des vues orientables et des fenêtres à défilement. Nous observerons cette intégration des ajustements dans les chapitres concernant ces widgets.

VII-A - Créer un ajustement

Un grand nombre des widgets qui utilisent des ajustements le font de manière automatique. Cependant, certains cas peuvent se présenter où il vous faudra en créer un vous-même. Vous utiliserez alors la méthode suivante :

ajustement = gtk.Adjustment(value=0, lower=0, upper=0, step_incr=0, page_incr=0, page_size=0)

L'argument value (valeur) est la valeur initiale que vous voulez donner à votre ajustement, elle correspondra généralement à la position la plus haute ou la plus à gauche d'un widget ajustable. L'argument lower (minimale) spécifie la valeur la plus basse que peut prendre ajustement. L'argument step_incr (incrémentation par pas) spécifie le plus petit des deux incréments par lequel l'utilisateur peut modifier la valeur, et page_incr (incrémentation par page) le plus grand. L'argument page_size (taille de la page) correspond en quelque sorte à la partie visible d'un widget à défilement. Enfin, upper (maximale) représente la coordonnée la plus basse ou la plus à droite de l'enfant d'un widget à défilement. Il ne s'agit donc pas forcément de la valeur la plus grande que peut prendre value ,car dans ces widgets, l'argument page_size est généralement différent de zéro.

VII-B - Utiliser facilement les ajustements

On pourrait diviser grossièrement les widgets ajustables en deux catégories : ceux qui utilisent et nécessitent des unités spécifiques pour les valeurs dont on vient de parler, et ceux qui les traitent comme des nombres arbitraires. Ce dernier groupe inclut les widgets d'intervalle (barre de défilement et gradateur), la barre de progression et les boutons fléchés. Il s'agit de l'ensemble des widgets qui, de par leur nature même, sont ajustés directement par l'utilisateur au moyen de la souris ou du clavier. Ils traitent toujours les valeurs données aux arguments lower et upper d'un ajustement comme un intervalle à l'intérieur duquel l'utilisateur peut manipuler la valeur (l'argument value) de l'ajustement. Par défaut, ils ne modifient que cette dernière.

L'autre groupe inclut la zone de texte, la vue orientable, la liste composée et la fenêtre à défilement. Tous ces widgets utilisent des valeurs en pixels pour leurs ajustements. Il s'agit, de plus, de l'ensemble des widgets qui sont ajustés indirectement, par l'intermédiaire de barres de défilement. Même si tous les widgets qui utilisent des ajustements peuvent soit créer les leurs, soit utiliser ceux qu'on leur fournit, il vaut mieux, en règle générale, laisser cette catégorie-là créer les siens. La plupart du temps, ils finissent par ne tenir compte d'aucune valeur provenant des ajustements qu'on leur a affectés, excepté la valeur elle-même (value) ; les résultats sont, en général, indéfinis (il vous faudra chercher dans les codes sources, et ils peuvent être différents d'un widget à l'autre).

Vous devez être en train de vous dire que puisque les zones de texte et les vues orientables insistent pour tout définir excepté la valeur de leurs ajustements, et puisque les barres de défilement ne touchent qu'à la valeur de l'ajustement, alors si l'on partage un ajustement entre une barre de défilement et une zone de texte, manipuler la barre de défilement devrait ajuster comme par magie la zone de texte. Parfaitement ! Ça se passe comme ça :

1. # Elle crée ses propres ajustements

2. vue_orientable = gtk.Viewport()

3. # l'ajustement qui vient d'être créé servira aussi pour la barre de défilement

4. barredef_v = gtk.VScrollbar(vue_orientable.get_vadjustment())

VII-C - Les ajustements en détail

Vous devez penser : « C'est bien beau tout ça, mais qu'est-ce qui se passe si je veux créer mes propres gestionnaires pour réagir quand l'utilisateur règle un widget d'intervalle ou un bouton fléché ? Et comment je récupère la valeur d'un ajustement dans ces gestionnaires ? » Afin de répondre à ces questions et à bien d'autres, commençons par jeter un œil aux attributs de l'objet gtk.Adjustment :

1. lower

2. upper

3. value

4. step_increment

5. page_increment

6. page_size

Pour une instance donnée ajust de gtk.Adjustment, on définit ou récupère chacun de ses attributs avec ajust.lower, ajust.value, etc.

En règle générale, lorsque l'on définit la valeur d'un ajustement, on veut que le changement soit reflété par chaque widget qui l'utilise. À cet effet, PyGTK fournit la méthode suivante :

ajustement.set_value(value)

La classe gtk.Adjustment, nous l'avons vu plus haut, est une sous-classe de gtk.Object au même titre que n'importe quel widget, et peut donc émettre des signaux. Voilà pourquoi les modifications sont reflétées automatiquement lorsque vous partagez un ajustement entre une barre de défilement et un autre widget ajustable ; tous les widgets ajustables connectent des gestionnaires au signal "value_changed" de leur ajustement, ce que peut aussi faire votre programme. Voici comment définir la fonction de rappel de ce signal :

def value_changed(ajustement):

Les différents widgets qui utilisent un ajustement lui font émettre ce signal à chaque fois qu'ils modifient sa valeur. Cela arrive lorsqu'une action de l'utilisateur fait se déplacer le curseur dans un widget d'intervalle, mais aussi lorsque le programme change explicitement la valeur grâce à la méthode set_value(). Imaginons, par exemple, que vous ayez un gradateur et que vous vouliez changer la rotation d'une image à chaque fois que sa valeur est modifiée. Vous écririez alors une fonction de rappel semblable à celle-ci :

1. def rpl_rotation_img(ajust, image):

2. rotation_image (image, ajust.value)

3. ...

Puis il vous faudrait la connecter à l'ajustement du gradateur ainsi :

ajust.connect("value_changed", rpl_rotation_img, image)

Et si un widget reconfigure les champs upper ou lower de son ajustement ? Ça arrive, par exemple quand un utilisateur ajoute du texte à une zone de texte. L'ajustement émet alors le signal "changed", qui ressemble à ça :

def changed(ajustement):

Les widgets d'intervalle connectent tous automatiquement un gestionnaire à ce signal, qui reflète le changement en modifiant leur apparence. La taille du curseur d'une barre de défilement est, par exemple, inversement proportionnelle à la différence entre les valeurs des arguments lower et upper de son ajustement.

Il est peu probable que vous ayez un jour besoin d'attacher un gestionnaire à ce signal, à moins que vous n'écriviez un nouveau type de widget d'intervalle. Dans tous les cas, si vous modifiez directement une des valeurs d'un ajustement, vous devriez lui faire émettre ce signal afin de reconfigurer tous les widgets qui l'utilisent. Utilisez pour cela cette méthode :

ajustement.emit("changed")

Maintenant, en avant, et ajustez !

VIII - Chapitre 8. Les widgets d'intervalle

La catégorie des widgets d'intervalle (gtk.Range) inclut l'incontournable barre de défilement ainsi que le "gradateur", un peu moins courant. Bien que l'on utilise généralement ces deux types d'objets à différentes fins, ils sont assez similaires dans leur implémentation et leur fonctionnement. Les widgets d'intervalle ont un jeu d'éléments graphiques commun : la coulisse et le curseur, qui possèdent tous les deux leur propre fenêtre X et qui reçoivent des événements. Pour faire avancer ou reculer le curseur dans la coulisse on peut le tirer avec la souris ou bien cliquer directement dans la coulisse. Dans ce second cas, et selon le bouton de la souris utilisé, le curseur viendra se positionner à l'endroit du clic ou s'en rapprochera suivant un pas prédéfini.

Comme nous venons de le voir dans le chapitre sur les ajustements, tous les widgets d'intervalle sont associés à un gtk.Adjustment, à partir duquel ils calculent la longueur du curseur et sa position dans la coulisse. Lorsque l'utilisateur manipule le curseur, le widget d'intervalle modifie la valeur de l'ajustement.

VIII-A - HScrollbar et VScrollbar : les barres de défilement

Voici les très classiques barres de défilement. Elles ne devraient être employées que pour faire défiler un autre widget, comme une liste, une zone de texte ou une vue orientable (il est même bien plus pratique d'utiliser une fenêtre à défilement dans la plupart des cas). Pour le reste, les gradateurs seront plus appropriés, car plus simples d'utilisation et offrant plus de fonctionnalités.

Les barres de défilement horizontale et verticale sont implémentées dans deux types différents. Il n'y a pas grand-chose à dire à leur sujet. On les crée en appelant l'une de ces méthodes :

1. barredefil_h = gtk.HScrollbar(adjustment=None)

3. barredefil_v = gtk.VScrollbar(adjustment=None)

et voilà tout. L'argument adjustment (ajustement) attend une référence à un ajustement existant. Si vous ne lui en fournissez pas, la méthode en créera un pour vous, ce qui peut être intéressant si vous voulez transmettre ce nouvel ajustement à la méthode constructeur d'un autre widget, qui se chargera de le configurer pour vous (une zone de texte, par exemple).

VIII-B - HScale et VScale : les gradateurs

Les gradateurs permettent à l'utilisateur de sélectionner et de manipuler une valeur dans un intervalle donné, ceci graphiquement. Ils peuvent servir, par exemple, à ajuster le degré d'agrandissement de l'aperçu d'une image, à contrôler la luminosité d'une couleur, ou encore à spécifier le nombre de minutes d'inactivité avant le lancement d'un économiseur d'écran.

VIII-B-1 - Créer un gradateur

Tout comme pour les barres de défilement, il existe un type de widget pour les gradateurs verticaux et un autre pour les horizontaux (la majorité des programmeurs semble préférer ces derniers). Malgré tout, vu qu'ils fonctionnent de la même manière, nous ne les traiterons pas séparément. Les méthodes suivantes créent respectivement un gradateur vertical et un gradateur horizontal :

1. gradateurv = gtk.VScale(adjustment=None)

3. gradateurh = gtk.HScale(adjustment=None)

L'argument adjustment (ajustement) attend une référence à un ajustement déjà créé avec gtk.Adjustment(). Si vous ne lui en fournissez pas, la méthode en créera un anonyme dont toutes les valeurs vaudront 0.0 (ce qui n'est pas très utile ici). Afin de vous y retrouver, vous pourriez créer un ajustement dont vous fixeriez la page_size à 0.0, de sorte que sa valeur upper corresponde bien à la plus grande valeur que l'utilisateur peut sélectionner (si vous êtes déjà complètement perdu, relisez la section sur les ajustements pour mieux comprendre leur rôle, leur processus de création et leur manipulation).

VIII-B-2 - Méthodes et signaux (enfin, au moins méthodes…)

Les gradateurs peuvent afficher un nombre représentant leur valeur courante à côté de la coulisse. Cette option, activée par défaut, peut être modifiée avec la méthode :

gradateur.set_draw_value(draw_value)

Comme vous l'avez sûrement deviné, draw_value (valeur d'affichage) vaut soit TRUE soit FALSE, chacune de ces deux expressions entrainant les conséquences que l'on peut en attendre.

La valeur affichée par un gradateur est arrondie par défaut au premier chiffre après la virgule, comme la valeur de son ajustement. Vous pouvez changer ceci avec la méthode suivante :

gradateur.set_digits(digits)

où digits est le nombre de décimales. Vous pouvez en demander autant que vous le souhaitez, mais sachez que seules les 13 premières seront réellement affichées.

Enfin, la valeur peut être affichée à différents endroits autour de la coulisse :

gradateur.set_value_pos(pos)

L'argument pos peut prendre l'une des valeurs suivantes :

1. POS_LEFT    # à gauche

2. POS_RIGHT    # à droite

3. POS_TOP    # au-dessus

4. POS_BOTTOM    # au-dessous

Si vous placez la valeur le long de la coulisse (au-dessus ou au-dessous d'un gradateur horizontal, par exemple), elle suivra le curseur dans ses déplacements.

VIII-C - Méthodes communes aux widgets d'intervalle

La classe des widgets d'intervalle (gtk.Range) a des mécanismes internes assez compliqués, qui, comme tous ceux des classes "de base", ne sont pas très intéressants à moins de vouloir les bidouiller. De plus, parmi ses méthodes et signaux, la quasi-totalité n'est vraiment utile que pour écrire des widgets dérivés. On trouve cependant quelques méthodes utiles qui fonctionnent avec tous les widgets de la classe.

VIII-C-1 - Définir le mode d'actualisation

Le "mode d'actualisation" d'un widget d'intervalle précise le moment où, durant sa manipulation par l'utilisateur, le widget devra changer la valeur de son ajustement et lui faire émettre le signal "value_changed". Les différents modes d'actualisation sont :

UPDATE_CONTINUOUS(continu)

C'est le mode par défaut. Le signal "value_changed" est émis de manière continue, c'est-à-dire au moindre déplacement du curseur.

UPDATE_DISCONTINUOUS(discontinu)

Le signal "value_changed" n'est émis qu'après que le curseur a cessé de bouger et que l'utilisateur a relâché le bouton de la souris.

UPDATE_DELAYED(différé)

Le signal "value_changed" est émis lorsque l'utilisateur relâche le bouton de la souris, ou si le curseur cesse de bouger durant un court instant.

On définit le mode d'actualisation d'un widget d'intervalle en l'indiquant à l'argument policy (mode) de cette méthode :

widget.set_update_policy(policy)

VIII-C-2 - Définir et récupérer les ajustements

Pour définir et récupérer "à la volée" l'ajustement d'un widget d'intervalle, on utilise naturellement les méthodes :

1. widget.set_adjustment(adjustment)

3. ajustement = widget.get_adjustment()

La méthode get_adjustment() renvoie une référence à l'ajustement auquel widget est connecté.

La méthode set_adjustment() ne fait absolument rien si vous lui transmettez l'ajustement que widget utilise déjà, que vous ayez modifié ou non ses valeurs. En revanche, si vous lui passez un nouvel ajustementajustements, elle déréférence le précédent s'il existe (voire le détruit), connecte les signaux appropriés au nouveau, puis recalcule la taille et/ou la position du curseur avant de le réafficher si nécessaire. Comme nous l'avons vu dans la section sur les ajustements, si vous voulez réutiliser le même, vous devriez lui faire émettre le signal "changed" quand vous modifiez directement ses valeurs :

ajustement.emit("changed")

VIII-D - Clavier et souris

Tous les widgets d'intervalle de GTK réagissent aux clics de souris plus ou moins de la même manière. En cliquant dans la coulisse avec le premier bouton, on augmente ou diminue la valeur (value) de son ajustement d'un page_increment, et le curseur est déplacé en conséquence. Si l'on y clique avec le deuxième bouton, le curseur est amené directement au point cliqué. Un clic de n'importe quel bouton sur les flèches d'une barre de défilement augmente ou diminue la valeur de son ajustement d'un step_increment.

Les barres de défilement ne peuvent pas avoir le focus, elle n'ont donc aucune connexion directe avec le clavier. Chez les autres widgets d'intervalle, ces connexions (actives, bien sûr, seulement lorsque les widgets ont le focus) sont les mêmes pour les widgets horizontaux et verticaux.

Tous les widgets d'intervalle peuvent être manipulés avec les flèches haut, bas, gauche et droite du clavier, ainsi qu'avec les touches Page précédente et Page suivante. Les flèches déplacent le curseur par step_incr, et Page précédente/Page suivante par page_incr.

L'utilisateur a également la possibilité de placer le curseur directement à l'une ou l'autre des extrémités de la coulisse avec les touches Début et Fin.

VIII-E - Démonstration des widgets d'intervalle

Le programme d'exemple widgetsintervalle.py affiche une fenêtre comprenant trois widgets d'intervalle connectés au même ajustement, ainsi que plusieurs autres widgets par lesquels il est possible de modifier certains des paramètres vus plus haut ou à la section sur les ajustements. Ceci vous permettra de vous faire une idée plus précise de ce que ces paramètres contrôlent dans le widget. La figure 8.1 montre ce que l'on obtient après avoir lancé le programme :

Figure 8.1. Exemple de widgets d'intervalle

Le code source de widgetsintervalle.py est le suivant :

1. #!/usr/bin/env python

3. # exemple widgetsintervalle.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # Ces fonctions sont là pour nous faciliter les choses

11. def nouvelle_entree(nom, rappel, donnees=None):

12. entree = gtk.MenuItem(nom)

13. entree.connect("activate", rappel, donnees)

14. entree.show()

15. return entree

17. def gradateurs_valeurs_defaut(gradateur):

18. gradateur.set_update_policy(gtk.UPDATE_CONTINUOUS)

19. gradateur.set_digits(1)

20. gradateur.set_value_pos(gtk.POS_TOP)

21. gradateur.set_draw_value(True)

23. class WidgetsIntervalle:

24. def modif_position(self, entree, position):

25. # Fixe la position de la valeur pour les deux gradateurs

26. self.gradateurh.set_value_pos(position)

27. self.gradateurv.set_value_pos(position)

29. def modif_mode_actu(self, entree, mode):

30. # Fixe le mode d'actualisation pour les deux gradateurs

31. self.gradateurh.set_update_policy(mode)

32. self.gradateurv.set_update_policy(mode)

34. def modif_decimales(self, ajust):

35. # Fixe le nombre de décimales pour arrondir la valeur de ajust

36. self.gradateurh.set_digits(ajust.value)

37. self.gradateurv.set_digits(ajust.value)

39. def modif_taille_page(self, ajust2, ajust1):

40. # Les valeurs "taille de la page" et "incrémentation par page" de l'ajustement

41. # exemple prennent la valeur donnée par le gradateur "Taille de la page".

42. ajust1.page_size = ajust2.value

43. ajust1.page_incr = ajust2.value

44. # Puis on fait émettre le signal "changed" à ajust1 pour reconfigurer tous les

45. # widgets qui lui sont attachés

46. ajust1.emit("changed")

48. def affiche_valeur(self, bouton):

49. # Active ou desactive l'affichage de la valeur sur les gradateurs,

50. # en fonction de l'état du bouton à bascule

51. self.gradateurh.set_draw_value(bouton.get_active())

52. self.gradateurv.set_draw_value(bouton.get_active())

54. # Construction de notre fenêtre exemple

56. def __init__(self):

57. # Creation standard d'une fenêtre

58. self.fenetre = gtk.Window (gtk.WINDOW_TOPLEVEL)

59. self.fenetre.connect("destroy", gtk.main_quit)

60. self.fenetre.set_title("Widgets d'intervalle")

62. boite1 = gtk.VBox(False, 0)

63. self.fenetre.add(boite1)

64. boite1.show()

66. boite2 = gtk.HBox(False, 10)

67. boite2.set_border_width(10)

68. boite1.pack_start(boite2, True, True, 0)

69. boite2.show()

71. # valeur, minimale, maximale, incr/pas, incr/page, taille de la page

72. # Notez que le paramètre "taille de la page" n'a d'effet qu'avec les

73. # barres de défilement, la plus haute valeur étant alors égale à

74. # "maximale" moins "taille de la page"

75. ajust1 = gtk.Adjustment(0.0, 0.0, 101.0, 0.1, 1.0, 1.0)

77. self.gradateurv = gtk.VScale(ajust1)

78. gradateurs_valeurs_defaut(self.gradateurv)

79. boite2.pack_start(self.gradateurv, True, True, 0)

80. self.gradateurv.show()

82. boite3 = gtk.VBox(False, 10)

83. boite2.pack_start(boite3, True, True, 0)

84. boite3.show()

86. # On réutilise le même ajustement

87. self.gradateurh = gtk.HScale(ajust1)

88. self.gradateurh.set_size_request(200, 30)

89. gradateurs_valeurs_defaut(self.gradateurh)

90. boite3.pack_start(self.gradateurh, True, True, 0)

91. self.gradateurh.show()

93. # On réutilise encore le même ajustement

94. barredefil = gtk.HScrollbar(ajust1)

95. # Notez que l'on demande a ce que les gradateurs soient toujours

96. # actualisés en continu quand la barre de défilement est manipulée

97. barredefil.set_update_policy(gtk.UPDATE_CONTINUOUS)

98. boite3.pack_start(barredefil, True, True, 0)

99. barredefil.show()

101. boite2 = gtk.HBox(False, 10)

102. boite2.set_border_width(10)

103. boite1.pack_start(boite2, True, True, 0)

104. boite2.show()

106. # Un bouton à bascule pour afficher ou pas la valeur

107. bouton = gtk.CheckButton("Afficher la valeur sur les gradateurs")

108. bouton.set_active(True)

109. bouton.connect("toggled", self.affiche_valeur)

110. boite2.pack_start(bouton, True, True, 0)

111. bouton.show()

113. boite2 = gtk.HBox(False, 10)

114. boite2.set_border_width(10)

116. # Un menu déroulant pour changer la position de la valeur

117. etiquette = gtk.Label("Position de la valeur :")

118. boite2.pack_start(etiquette, False, False, 0)

119. etiquette.show()

121. menuderoul = gtk.OptionMenu()

122. menu = gtk.Menu()

124. entree = nouvelle_entree ("Au-dessus", self.modif_position, gtk.POS_TOP)

125. menu.append(entree)

127. entree = nouvelle_entree ("Au-dessous", self.modif_position,

128. gtk.POS_BOTTOM)

129. menu.append(entree)

131. entree = nouvelle_entree ("A gauche", self.modif_position, gtk.POS_LEFT)

132. menu.append(entree)

134. entree = nouvelle_entree ("A droite", self.modif_position, gtk.POS_RIGHT)

135. menu.append(entree)

137. menuderoul.set_menu(menu)

138. boite2.pack_start(menuderoul, True, True, 0)

139. menuderoul.show()

141. boite1.pack_start(boite2, True, True, 0)

142. boite2.show()

144. boite2 = gtk.HBox(False, 10)

145. boite2.set_border_width(10)

147. # Encore un autre menu déroulant, cette fois-ci pour le mode

148. # d'actualisation des gradateurs

149. etiquette = gtk.Label("Mode d'actualisation :")

150. boite2.pack_start(etiquette, False, False, 0)

151. etiquette.show()

153. menuderoul = gtk.OptionMenu()

154. menu = gtk.Menu()

156. entree = nouvelle_entree("Continu", self.modif_mode_actu,

157. gtk.UPDATE_CONTINUOUS)

158. menu.append(entree)

160. entree = nouvelle_entree ("Discontinu", self.modif_mode_actu,

161. gtk.UPDATE_DISCONTINUOUS)

162. menu.append(entree)

164. entree = nouvelle_entree ("Differe", self.modif_mode_actu,

165. gtk.UPDATE_DELAYED)

166. menu.append(entree)

168. menuderoul.set_menu(menu)

169. boite2.pack_start(menuderoul, True, True, 0)

170. menuderoul.show()

172. boite1.pack_start(boite2, True, True, 0)

173. boite2.show()

175. boite2 = gtk.HBox(False, 10)

176. boite2.set_border_width(10)

178. # Création d'un gradateur horizontal pour choisir le nombre de

179. # décimales dans les gradateurs exemples.

180. etiquette = gtk.Label("Decimales :")

181. boite2.pack_start(etiquette, False, False, 0)

182. etiquette.show()

184. ajust2 = gtk.Adjustment(1.0, 0.0, 5.0, 1.0, 1.0, 0.0)

185. ajust2.connect("value_changed", self.modif_decimales)

186. gradateur = gtk.HScale(ajust2)

187. gradateur.set_digits(0)

188. boite2.pack_start(gradateur, True, True, 0)

189. gradateur.show()

191. boite1.pack_start(boite2, True, True, 0)

192. boite2.show()

194. boite2 = gtk.HBox(False, 10)

195. boite2.set_border_width(10)

197. # Un dernier gradateur horizontal pour choisir la taille de la page

198. # de la barre de défilement.

199. etiquette = gtk.Label("Taille de la page de la\nbarre de defilement :")

200. boite2.pack_start(etiquette, False, False, 0)

201. etiquette.show()

203. ajust2 = gtk.Adjustment(1.0, 1.0, 101.0, 1.0, 1.0, 0.0)

204. ajust2.connect("value_changed", self.modif_taille_page, ajust1)

205. gradateur = gtk.HScale(ajust2)

206. gradateur.set_digits(0)

207. boite2.pack_start(gradateur, True, True, 0)

208. gradateur.show()

210. boite1.pack_start(boite2, True, True, 0)

211. boite2.show()

213. separateur = gtk.HSeparator()

214. boite1.pack_start(separateur, False, True, 0)

215. separateur.show()

217. boite2 = gtk.VBox(False, 10)

218. boite2.set_border_width(10)

219. boite1.pack_start(boite2, False, True, 0)

220. boite2.show()

222. bouton = gtk.Button("Quitter")

223. bouton.connect_object("clicked", gtk.main_quit, self.fenetre)

224. boite2.pack_start(bouton, True, True, 0)

225. bouton.set_flags(gtk.CAN_DEFAULT)

226. bouton.grab_default()

227. bouton.show()

228. self.fenetre.show()

230. def main():

231. gtk.main()

232. return 0

234. if __name__ == "__main__":

235. WidgetsIntervalle()

236. main()

Vous avez peut-être remarqué que le programme n'appelle pas la méthode connect() pour l'événement "delete_event", mais seulement pour le signal "destroy", et pourtant tout se passe très bien. Ceci est dû au fait qu'un événement "delete_event" non traité, déclenche automatiquement l'émission du signal "destroy" sur la fenêtre.

IX - Chapitre 9. Widgets divers

IX-A - Les étiquettes

Les étiquettes (gtk.Label) sont des widgets relativement simples et très utilisés dans GTK. Elles ne peuvent émettre aucun signal, car elles n'ont pas de fenêtres X associées. Pour capter quand même des signaux ou pour ce qui concerne le découpage, il vous faudra les placer dans des boites àévénements ou dans des boutons (voir Boutons simples).

Pour créer une étiquette, on utilise la méthode suivante :

etiquette = gtk.Label(str)

L'unique argument à fournir, str, est la chaine de caractères que doit afficher l'étiquette. Pour changer ce texte une fois l'étiquette créée, on fera appel à la méthode :

etiquette.set_text(str)

etiquette est l'étiquette précédemment créée, et str la nouvelle chaine de caractères. L'espace alloué à la nouvelle chaine sera automatiquement ajusté si nécessaire. Vous pouvez créer une étiquette de plusieurs lignes en insérant des sauts de lignes dans la chaine de caractères.

Pour récupérer la chaine courante, faites :

chaine = etiquette.get_text()

etiquette est l'étiquette que vous avez créée, et chaine la chaine de caractères. On peut définir le type d'alignement du texte d'une étiquette avec :

etiquette.set_justify(jtype)

L'argument jtype peut prendre les valeurs suivantes :

1. JUSTIFY_LEFT        # aligné à gauche

2. JUSTIFY_RIGHT        # aligné à droite

3. JUSTIFY_CENTER    # centré (valeur par défaut)

4. JUSTIFY_FILL        # justifié

Le texte des étiquettes peut aussi être renvoyé à la ligne automatiquement. Il faut pour cela utiliser cette méthode :

etiquette.set_line_wrap(wrap)

L'argument wrap peut prendre les valeurs TRUE (activé) ou FALSE (désactivé).

Pour que le texte de votre étiquette soit souligné, vous pouvez lui fournir un patron à suivre :

etiquette.set_pattern(pattern)

L'argument pattern (patron) précise le profil du soulignement. Il s'agit d'une chaine de caractères constituée de "_" et d'espaces, un "_" indiquant que le caractère correspondant de l'étiquette doit être souligné. Par exemple, la chaine "__ __" soulignerait les deux premiers caractères ainsi que le huitième et le neuvième. Si vous voulez simplement définir un raccourci-clavier souligné (un caractère "mnémonique") dans votre étiquette, utilisez plutôt la méthode set_text_with_mnemonic (str), et non set_pattern().

Le programme etiquettes.py est un court exemple qui illustre ces méthodes. Pour mieux distinguer les différents styles d'étiquettes, il utilise des cadres (pas encore traduit) (gtk.Frame). N'en tenez cependant pas compte pour l'instant, nous aborderons ce widget plus loin.

Les étiquettes de GTK+ 2.0 peuvent être sélectionnées (pour le copier-coller), et leur texte peut contenir des balises qui modifient la police et d'autres attributs de texte. Ces caractéristiques avancées ne seront toutefois pas abordées ici.

La figure 9.1 montre la fenêtre affichée par le programme etiquettes.py :

Figure 9.1. Exemples d'étiquettes

Le code source du programme est le suivant :

1. #!/usr/bin/env python

3. # exemple etiquettes.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class Etiquettes:

10. def __init__(self):

11. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

12. self.fenetre.connect("destroy", gtk.main_quit)

14. self.fenetre.set_title("Étiquettes")

15. boite_v = gtk.VBox(False, 5)

16. boite_h = gtk.HBox(False, 5)

17. self.fenetre.add(boite_h)

18. boite_h.pack_start(boite_v, False, False, 0)

19. self.fenetre.set_border_width(5)

21. cadre = gtk.Frame("Etiquette normale")

22. etiquette = gtk.Label("Voici une étiquette normale")

23. cadre.add(etiquette)

24. boite_v.pack_start(cadre, False, False, 0)

26. cadre = gtk.Frame("Etiquette de plusieurs lignes")

27. etiquette = gtk.Label("Voici une étiquette de plusieurs lignes.\nDeuxieme ligne\n"

28. "Troisième ligne")

29. cadre.add(etiquette)

30. boite_v.pack_start(cadre, False, False, 0)

32. cadre = gtk.Frame("Étiquette alignée à gauche")

33. etiquette = gtk.Label("Voici une étiquette de plusieurs lignes\n"

34. "dont le texte est aligné à gauche.\nTroisieme ligne")

35. etiquette.set_justify(gtk.JUSTIFY_LEFT)

36. cadre.add(etiquette)

37. boite_v.pack_start(cadre, False, False, 0)

39. cadre = gtk.Frame("Étiquette alignée à droite")

40. etiquette = gtk.Label("Voici une étiquette de plusieurs lignes\ndont le texte est aligné à droite.\n"

41. "Quatrième ligne, (j/k)")

42. etiquette.set_justify(gtk.JUSTIFY_RIGHT)

43. cadre.add(etiquette)

44. boite_v.pack_start(cadre, False, False, 0)

46. boite_v = gtk.VBox(False, 5)

47. boite_h.pack_start(boite_v, False, False, 0)

48. cadre = gtk.Frame("Étiquette avec retour à la ligne auto")

49. etiquette = gtk.Label("Voici un exemple d'étiquette avec retour à la ligne auto. Elle "

50. "ne doit pas remplir l'espace qui lui est             "

51. "alloué, mais elle s'ajuste automatiquement "

52. "en renvoyant les mots à la ligne.  "

53. "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed "

54. "do eiusmod tempor incididunt ut labore et dolore magna aliqua.  "

55. "Seize jacinthes sèchent dans seize sachets secs.\n"

56. "     Ces étiquettes peuvent contenir plusieurs paragraphes "

57. "et ajoutent correctement "

58. "de nombreux espaces supplémentaires ")

59. etiquette.set_line_wrap(True)

60. cadre.add(etiquette)

61. boite_v.pack_start(cadre, False, False, 0)

63. cadre = gtk.Frame("Étiquette justifiee avec retour a la ligne auto")

64. etiquette = gtk.Label("Voici un exemple d'étiquette justifiée avec retour à la ligne auto.  "

65. "Elle doit remplir "

66. "entièrement l'espace qui lui est alloué.  "

67. "Quelques phrases pour les besoins de la "

68. "démonstration :  Voila, voila. Le lundi au soleil, "

69. "c'est une chose qu'on n'aura jamais, ta la la ta da la la...\n"

70. "    Nouveau paragraphe.\n"

71. "    Nouveau paragraphe, mais mieux que"

72. " l'autre. Hélas ! Il est déjà fini.

73. ")

74. etiquette.set_justify(gtk.JUSTIFY_FILL)

75. etiquette.set_line_wrap(True)

76. cadre.add(etiquette)

77. boite_v.pack_start(cadre, False, False, 0)

79. cadre = gtk.Frame("Etiquette avec texte souligne")

80. etiquette = gtk.Label("Cette étiquette comporte du texte souligné !\n"

81. "Cette phrase est bizarrement soulignee")

82. etiquette.set_justify(gtk.JUSTIFY_LEFT)

83. etiquette.set_pattern(

84. "____________________________________________ _ _________ _ ______     __ _______ ___")

85. cadre.add(etiquette)

86. boite_v.pack_start(cadre, False, False, 0)

87. self.fenetre.show_all ()

89. def main():

90. gtk.main()

91. return 0

93. if __name__ == "__main__":

94. Etiquettes()

95. main()

À noter que le "Étiquette justifiée avec retour à la ligne auto" n'est pas rempli.

IX-B - Les flèches

Le widget gtk.Arrow consiste en une tête de flèche pouvant pointer dans différentes directions et à laquelle on peut appliquer un certain nombre de styles. Dans beaucoup d'applications, ce widget se révélera très utile une fois placé à l'intérieur d'un bouton. À l'instar des étiquettes, il n'émet pas de signaux.

Pour manipuler les flèches, il n'y a que deux appels :

1. fleche = gtk.Arrow(arrow_type, shadow_type)

3. fleche.set(arrow_type, shadow_type)

Le premier crée une flèche, en prenant en compte le type et le style demandés. Le second appel permet de modifier ces valeurs après la création. L'argument arrow_type (type de flèche) peut prendre l'une des valeurs suivantes :

1. ARROW_UP        # haut

2. ARROW_DOWN        # bas

3. ARROW_LEFT        # gauche#!/usr/bin/env python

4. ARROW_RIGHT        # droite

Ces valeurs indiquent évidemment la direction dans laquelle pointera la flèche. Les valeurs possibles pour l'argument shadow_type (type d'ombre) sont :

1. SHADOW_IN        # empreinte

2. SHADOW_OUT        # relief (par défaut)

3. SHADOW_ETCHED_IN    # contour en empreinte

4. SHADOW_ETCHED_OUT    # contour en relief

Le programme exemple fleches.py donne un aperçu de l'utilisation du widget gtk.Arrow. La Figure 9.2 montre la fenêtre que l'on obtient au lancement du programme :

Figure 9.2. Exemple de boutons avec flèches

Voici le code source de fleches.py :

1. #!/usr/bin/env python

3. # exemple fleches.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # On crée une flèche avec les paramètres spécifiés

10. # et on la place dans un bouton

11. def cree_fleche_bouton(type, ombre):

12. bouton = gtk.Button();

13. fleche = gtk.Arrow(type, ombre);

14. bouton.add(fleche)

15. bouton.show()

16. fleche.show()

17. return bouton

19. class Fleches:

20. def __init__(self):

21. # Création d'une fenêtre

22. fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. fenetre.set_title("Flèches dans boutons")

26. # C'est une bonne idée de faire ceci pour chaque fenêtre

27. fenetre.connect("destroy", gtk.main_quit)

29. # On fixe la largeur des bordures de la fenêtre

30. fenetre.set_border_width(10)

32. # On crée une boite pour contenir les boutons/flèches

33. boite = gtk.HBox(False, 0)

34. boite.set_border_width(2)

35. fenetre.add(boite)

37. # On place et on affiche tous nos widgets

38. boite.show()

40. bouton = cree_fleche_bouton(gtk.ARROW_UP, gtk.SHADOW_IN)

41. boite.pack_start(bouton, False, False, 3)

43. bouton = cree_fleche_bouton(gtk.ARROW_DOWN, gtk.SHADOW_OUT)

44. boite.pack_start(bouton, False, False, 3)

46. bouton = cree_fleche_bouton(gtk.ARROW_LEFT, gtk.SHADOW_ETCHED_IN)

47. boite.pack_start(bouton, False, False, 3)

49. bouton = cree_fleche_bouton(gtk.ARROW_RIGHT, gtk.SHADOW_ETCHED_OUT)

50. boite.pack_start(bouton, False, False, 3)

52. fenetre.show()

54. def main():

55. gtk.main()

56. return 0

58. if __name__ == "__main__":

59. Fleches()

60. main()

IX-C - Les infobulles

Les infobulles sont les petites chaines de texte qui apparaissent lorsque le pointeur de la souris est maintenu quelques secondes au-dessus d'un bouton ou de tout autre widget (à l'exception de ceux qui ne reçoivent pas d'événements — qui ne disposent pas de leur propre fenêtre).

Dans un premier temps, on utilise l'appel qui suit pour créer une infobulle. Il suffit de le faire une fois pour un groupe d'infobulles donné, car l'objet gtk.Tooltips renvoyé peut être utilisé pour en créer de nouvelles.

infobulles = gtk.Tooltips()

Une fois créés l'infobulle ainsi que le widget auquel on souhaite la rattacher, on utilisera simplement l'appel suivant pour appliquer la première au second :

infobulles.set_tip(widget, tip_text, tip_private=None)

L'objet infobulles est l'infobulle que l'on a créée auparavant. Le premier argument (widget) est le widget pour lequel elle doit apparaitre et le second (tip_text) le texte qu'elle doit afficher. Le dernier argument (tip_private) est une chaine de caractères pouvant servir d'identifiant.

Le programme exemple infobulles.py modifie le programme fleches.py en ajoutant une infobulle à chaque bouton. La Figure 9.3 montre la fenêtre qu'il génère ainsi que l'infobulle du deuxième bouton :

Figure 9.3. Exemple d'infobulle

Voici le code source de infobulles.py :

1. #!/usr/bin/env python

3. # exemple infobulles.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # On crée une flèche avec les paramètres spécifiés

10. # et on la place dans un bouton

11. def cree_fleche_bouton(type, ombre):

12. bouton = gtk.Button();

13. fleche = gtk.Arrow(type, ombre);

14. bouton.add(fleche)

15. bouton.show()

16. fleche.show()

17. return bouton

19. class Infobulles:

20. def __init__(self):

21. # Créeation d'une fenêtre

22. fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. fenetre.set_title("Infobulles")

26. # C'est une bonne idée de faire ceci pour chaque fenêtre

27. fenetre.connect("destroy", gtk.main_quit)

29. # On fixe la largeur des bordures de la fenêtre

30. fenetre.set_border_width(10)

32. # On crée une boite pour contenir les boutons/flèches

33. boite = gtk.HBox(False, 0)

34. boite.set_border_width(2)

35. fenetre.add(boite)

37. # Création d'un objet Tooltips

38. self.infobulles = gtk.Tooltips()

40. # On place et on affiche tous nos widgets

41. boite.show()

43. bouton = cree_fleche_bouton(gtk.ARROW_UP, gtk.SHADOW_IN)

44. boite.pack_start(bouton, False, False, 3)

45. self.infobulles.set_tip(bouton, "SHADOW_IN")

47. bouton = cree_fleche_bouton(gtk.ARROW_DOWN, gtk.SHADOW_OUT)

48. boite.pack_start(bouton, False, False, 3)

49. self.infobulles.set_tip(bouton, "SHADOW_OUT")

51. bouton = cree_fleche_bouton(gtk.ARROW_LEFT, gtk.SHADOW_ETCHED_IN)

52. boite.pack_start(bouton, False, False, 3)

53. self.infobulles.set_tip(bouton, "SHADOW_ETCHED_IN")

55. bouton = cree_fleche_bouton(gtk.ARROW_RIGHT, gtk.SHADOW_ETCHED_OUT)

56. boite.pack_start(bouton, False, False, 3)

57. self.infobulles.set_tip(bouton, "SHADOW_ETCHED_OUT")

59. fenetre.show()

61. def main():

62. gtk.main()

63. return 0

65. if __name__ == "__main__":

66. infob = Infobulles()

67. main()

Il existe d'autres méthodes utilisables avec les infobulles. En voici une liste, accompagnée d'une brève description pour chacune :

infobulles.enable()

Active un groupe d'infobulles désactivé.

infobulles.disable()

Désactive un groupe d'infobulles activé.

infobulles.set_delay(delay)

Fixe le nombre de millisecondes pour lequel le pointeur de la souris doit rester au-dessus du widget avant que l'infobulle apparaisse. La valeur par défaut est 500 millisecondes (une demi-seconde).

Et voilà. C'est déjà plus qu'il n'en faut :-).

IX-D - La barre de progression

On emploie la barre de progression pour montrer l'état d'avancement d'une opération. Son utilisation est très facile, comme vous pourrez le constater plus bas avec le code source de notre exemple. Mais commençons par découvrir son appel de création :

barreprogression = gtk.ProgressBar(adjustment=None)

L'argument adjustment indique un ajustement à utiliser avec la barre de progression. Si aucun n'est spécifié, il en sera créé un automatiquement. À présent que notre barre de progression existe, nous pouvons l'utiliser.

barreprogression.set_fraction(fraction)

L'objet barreprogression est la barre de progression sur laquelle l'on souhaite agir, et l'argument fraction la proportion "effectuée", c'est-à-dire le pourcentage de remplissage de la barre de progression. Cette indication est donnée à la méthode sous la forme d'un réel compris entre 0 et 1.

L'orientation de la barre de progression peut être définie avec la méthode :

barreprogression.set_orientation(orientation)

L'argument orientation peut prendre les valeurs suivantes, qui indiquent le sens de progression de la barre :

1. PROGRESS_LEFT_TO_RIGHT    # de gauche à droite

2. PROGRESS_RIGHT_TO_LEFT    # de droite à gauche

3. PROGRESS_BOTTOM_TO_TOP    # de bas en haut

4. PROGRESS_TOP_TO_BOTTOM    # de haut en bas

On peut configurer la barre de progression pour que, plutôt que de donner l'état d'avancement de l'opération, elle indique juste un état d'activité. Cela peut servir dans des situations où l'état d'avancement ne peut pas être représenté dans un intervalle de valeurs. La fonction suivante indique que l'opération a avancé dans une certaine mesure.

barreprogression.pulse()

Le pas de déplacement de l'indicateur d'activité se définit avec la méthode suivante, où fraction est compris entre 0.0 et 1.0.

barreprogression.set_pulse_step(fraction)

Lorsqu'elle n'est pas en mode "activité", la barre de progression peut également afficher une chaine de texte configurable dans sa coulisse. On utilise alors la méthode suivante :

barreprogression.set_text(text)

Notez que set_text() ne prend pas en charge le formatage de type printf() des barres de progression de GTK+ 1.2.

Pour désactiver l'affichage de la chaine de caractères, on appelle set_text() à nouveau, mais sans argument.

Le texte présent dans une barre de progression peut se récupérer à l'aide de la méthode suivante :

text = barreprogression.get_text()

Les barres de progression sont généralement utilisées avec des temporisations ou d'autres fonctions de ce genre (cf. Chapitre 19. Temporisations, Entrées/Sorties et fonctions d'inactivité) pour donner une illusion de mode multitâche. Dans tous les cas, les fonctions set_fraction() ou pulse() sont employées de la même manière.

Le programme barreprogression.py offre un exemple de barre de progression actualisée par l'intermédiaire de temporisations. Ce code vous montre en outre comment réinitialiser la barre de progression. La Figure 9.4 montre la fenêtre que l'on obtient :

Figure 9.4. Exemple de barre de progression

Voici le code source de barreprogression.py :

1. #!/usr/bin/env python

3. # exemple barreprogression.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # Fonction qui actualise la valeur de la barre de progression

10. # pour qu'on ait un petit peu de mouvement

11. def tempo_progression(objetbarre):

12. if objetbarre.case_activite.get_active():

13. objetbarre.barreprogression.pulse()

14. else:

15. # On calcule la valeur de la barre de progression en prenant

16. # en compte l'intervalle défini dans l'ajustement

17. nouv_val = objetbarre.barreprogression.get_fraction() + 0.01

18. if nouv_val > 1.0:

19. nouv_val = 0.0

20. # On fixe la nouvelle valeur

21. objetbarre.barreprogression.set_fraction(nouv_val)

23. # Cette fonction étant une fonction de temporisation, on

24. # renvoie TRUE afin qu'elle puisse encore être appelée

25. return True

27. class BarreProgression:

28. # Fonction de rappel qui active ou désactive l'affichage du texte

29. # dans la coulisse de la barre de progression

30. def modif_affich_texte(self, widget, data=None):

31. if widget.get_active():

32. self.barreprogression.set_text("exemple de texte")

33. else:

34. self.barreprogression.set_text("")

36. # Fonction de rappel qui active ou désactive le mode "activite" de

37. # la barre de progression

38. def modif_mode(self, widget, data=None):

39. if widget.get_active():

40. self.barreprogression.pulse()

41. else:

42. self.barreprogression.set_fraction(0.0)

44. # Fonction de rappel qui modifie l'orientation de la barre de progression

45. def modif_orientation(self, widget, data=None):

46. if self.barreprogression.get_orientation() == gtk.PROGRESS_LEFT_TO_RIGHT:

47. self.barreprogression.set_orientation(gtk.PROGRESS_RIGHT_TO_LEFT)

48. elif self.barreprogression.get_orientation() == gtk.PROGRESS_RIGHT_TO_LEFT:

49. self.barreprogression.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)

51. # Liberation de la mémoire réservée et retrait de la temporisation

52. def sortie(self, widget, data=None):

53. gtk.timeout_remove(self.tempo)

54. self.tempo = 0

55. gtk.main_quit()

57. def __init__(self):

58. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

59. self.fenetre.set_resizable(True)

61. self.fenetre.connect("destroy", self.sortie)

62. self.fenetre.set_title("Barre de progression")

63. self.fenetre.set_border_width(0)

65. boite_v = gtk.VBox(False, 5)

66. boite_v.set_border_width(10)

67. self.fenetre.add(boite_v)

68. boite_v.show()

70. # Création d'un objet alignement au centre

71. align = gtk.Alignment(0.5, 0.5, 0, 0)

72. boite_v.pack_start(align, False, False, 5)

73. align.show()

75. # Création de la barre de progression en utilisant l'ajustement

76. self.barreprogression = gtk.ProgressBar()

78. align.add(self.barreprogression)

79. self.barreprogression.show()

81. # On ajoute une fonction de rappel temporisée, qui actualisera

82. # la valeur de la barre de progression

83. self.tempo = gtk.timeout_add (100, tempo_progression, self)

85. separateur = gtk.HSeparator()

86. boite_v.pack_start(separateur, False, False, 0)

87. separateur.show()

89. # lignes, colonnes, homogène

90. tableau = gtk.Table(2, 2, False)

91. boite_v.pack_start(tableau, False, True, 0)

92. tableau.show()

94. # Ajout d'une case à cocher pour l'affichage du texte dans la coulisse

95. case = gtk.CheckButton("Afficher le texte")

96. tableau.attach(case, 0, 1, 0, 1,

97. gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL,

98. 5, 5)

99. case.connect("clicked", self.modif_affich_texte)

100. case.show()

102. # Ajout d'une case à cocher pour activer ou désactiver le mode "activite"

103. self.case_activite = case = gtk.CheckButton("Mode activite")

104. tableau.attach(case, 0, 1, 1, 2,

105. gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL,

106. 5, 5)

107. case.connect("clicked", self.modif_mode)

108. case.show()

110. # Ajout d'une case à cocher pour modifier l'orientation

111. case = gtk.CheckButton("De droite à gauche")

112. tableau.attach(case, 0, 1, 2, 3,

113. gtk.EXPAND | gtk.FILL, gtk.EXPAND | gtk.FILL,

114. 5, 5)

115. case.connect("clicked", self.modif_orientation)

116. case.show()

118. # Ajout d'un bouton pour quitter le programme

119. bouton = gtk.Button("Fermer")

120. bouton.connect("clicked", self.sortie)

121. boite_v.pack_start(bouton, False, False, 0)

123. # Ceci fait en sorte que le bouton puisse être le widget par défaut

124. bouton.set_flags(gtk.CAN_DEFAULT)

126. # Ceci fait du bouton le bouton par défaut. Le simple fait d'appuyer

127. # sur la touche "Enter" l'activera

128. bouton.grab_default ()

129. bouton.show()

131. self.fenetre.show()

133. def main():

134. gtk.main()

135. return 0

137. if __name__ == "__main__":

138. BarreProgression()

139. main()

IX-E - Les boites de dialogue

Les boites de dialogue sont très simples, et ne sont en définitive que des fenêtres dans lesquelles quelques widgets sont préplacés. Un gtk.Dialog crée une fenêtre, place dans sa partie supérieure une boite horizontale contenant un séparateur, et place à sa suite une boite horizontale qui sera la "zone d'action" (action area).

Les boites de dialogues peuvent servir pour des messages à destination de l'utilisateur, et pour d'autres tâches similaires. Il s'agit vraiment d'un widget basique, qui ne possède en lui-même qu'une fonction :

boitedialogue = gtk.Dialog(title=None, parent=None, flags=0, buttons=None)

L'argument title (titre) attend le texte à utiliser dans la barre de titre, parent représente la fenêtre principale de l'application, et flags (drapeaux) définit plusieurs modes d'opération pour la boite de dialogue :

1. DIALOG_MODAL            # rend la boite de dialogue modale

2. DIALOG_DESTROY_WITH_PARENT    # détruit la boite de dialogue quand son parent est détruit

3. DIALOG_NO_SEPARATOR        # pas de séparateur entre la boite verticale et la zone d'action

L'argument buttons (boutons) est un tuple contenant pour chaque bouton une couple texte-réponse. Tous les arguments ont des valeurs par défaut et peuvent être spécifiés avec des mot-clés.

Vous savez maintenant créer une boite de dialogue. Il ne vous reste plus qu'à vous en servir. Vous pourriez placer un bouton dans la zone d'action :

1. bouton = ...

2. boitedialogue.action_area.pack_start(bouton, True, True, 0)

3. bouton.show()

Vous pourriez également enrichir votre boite verticale d'une étiquette, par exemple. Essayer quelque chose dans ce style :

1. etiquette = gtk.Label("Cool, la boite de dialogue")

2. boitedialogue.vbox.pack_start(etiquette, True, True, 0)

3. etiquette.show()

En guise d'exemple d'utilisation de la boite de dialogue, vous pourriez mettre deux boutons dans la zone d'action, un bouton Annuler et un bouton OK, puis placer une étiquette dans la boite verticale qui interrogerait l'utilisateur ou l'informerait d'une erreur. Vous connecteriez ensuite un signal différent à chacun des boutons et feriez en sorte que l'opération choisie soit exécutée.

Si la simple configuration par défaut (boites verticale et horizontale dans les deux zones de la fenêtre) ne vous offre pas assez de possibilités pour ce que vous souhaitez faire dans votre application, rien ne vous empêche de placer un autre widget de placement dans les boites. Il est tout à fait possible, par exemple, de placer un tableau dans la boite verticale.

IX-F - Les images

Les gtk.Image sont des structures de données qui contiennent des images. Ces images peuvent être utilisées à plusieurs endroits.

On peut créer des gtk.Image à partir de pixbufs, de pixmaps, de fichiers contenant des informations soit de type image (XPM, PNG, JPEG, TIFF, etc.) soit de type animation.

On crée un gtk.Image avec la fonction :

image = gtk.Image()

Puis on y charge l'image en utilisant l'une des méthodes suivantes :

1. image.set_from_pixbuf(pixbuf)

2. image.set_from_pixmap(pixmap, mask)

3. image.set_from_image(image)

4. image.set_from_file(filename)

5. image.set_from_stock(stock_id, size)

6. image.set_from_icon_set(icon_set, size)

7. image.set_from_animation(animation)

pixbuf est un GdkPixbuf, pixmap et mask sont des GdkPixmaps, image est un GdkImage, stock_id est le nom d'un GtkStockItem, icon_set est un GtkIconSet, et animation est une GdkPixbufAnimation. Quant à l'argument size (taille), il peut prendre l'une des valeurs suivantes :

1. ICON_SIZE_MENU            # taille de l'icône : menu

2. ICON_SIZE_SMALL_TOOLBAR    #          "        : petite barre d'outils

3. ICON_SIZE_LARGE_TOOLBAR    #          "        : grande barre d'outils

4. ICON_SIZE_BUTTON        #          "        : bouton

5. ICON_SIZE_DND            #          "        : glisser-déposer

6. ICON_SIZE_DIALOG        #          "        : boite de dialogue

Le moyen le plus simple de créer une image est d'utiliser la méthode set_from_file(), laquelle détermine automatiquement le type de l'image et le charge.

Le programme images.py illustre le chargement de plusieurs types d'images (goal.gif, pomme-rouge.png, chaos.jpg, important.tif, ballonfoot.gif) dans des gtk.Image, que l'on place ensuite dans des boutons :

Figure 9.5. Images exemples dans des boutons

Voici le code source :

1. #!/usr/bin/env python

3. # exemple images.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ImagesExemple:

10. # Si elle est invoquée (via le "delete_event"), cette fonction ferme l'application

11. def fermer_application(self, widget, evnmt, data=None):

12. gtk.main_quit()

13. return False

15. # Cette fonction est invoquée quand on clique sur le bouton. Elle affiche un message.

16. def clic_bouton(self, widget, data=None):

17. print "Clic sur le bouton %s" % data

19. def __init__(self):

20. # création de la fenêtre principale, et connexion du signal

21. # delete_event à la fermeture de l'application

22. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

23. fenetre.connect("delete_event", self.fermer_application)

24. fenetre.set_border_width(10)

25. fenetre.show()

27. # Une boite horizontale pour contenir les boutons

28. boite_h = gtk.HBox()

29. boite_h.show()

30. fenetre.add(boite_h)

32. animpixbuf = gtk.gdk.PixbufAnimation("goal.gif")

33. image = gtk.Image()

34. image.set_from_animation(animpixbuf)

35. image.show()

36. # un bouton pour contenir le widget image

37. bouton = gtk.Button()

38. bouton.add(image)

39. bouton.show()

40. boite_h.pack_start(bouton)

41. bouton.connect("clicked", self.clic_bouton, "1")

43. # création de plusieurs gtk.Image à partir de données de

44. # fichiers, et chargement dans des boutons

45. image = gtk.Image()

46. image.set_from_file("pomme-rouge.png")

47. image.show()

48. # un bouton pour contenir le widget image

49. bouton = gtk.Button()

50. bouton.add(image)

51. bouton.show()

52. boite_h.pack_start(bouton)

53. bouton.connect("clicked", self.clic_bouton, "2")

55. image = gtk.Image()

56. image.set_from_file("chaos.jpg")

57. image.show()

58. # un bouton pour contenir le widget image

59. bouton = gtk.Button()

60. bouton.add(image)

61. bouton.show()

62. boite_h.pack_start(bouton)

63. bouton.connect("clicked", self.clic_bouton, "3")

65. image = gtk.Image()

66. image.set_from_file("important.tif")

67. image.show()

68. # un bouton pour contenir le widget image

69. bouton = gtk.Button()

70. bouton.add(image)

71. bouton.show()

72. boite_h.pack_start(bouton)

73. bouton.connect("clicked", self.clic_bouton, "4")

75. image = gtk.Image()

76. image.set_from_file("ballonfoot.gif")

77. image.show()

78. # un bouton pour contenir le widget image

79. bouton = gtk.Button()

80. bouton.add(image)

81. bouton.show()

82. boite_h.pack_start(bouton)

83. bouton.connect("clicked", self.clic_bouton, "5")

86. def main():

87. gtk.main()

88. return 0

90. if __name__ == "__main__":

91. ImagesExemple()

92. main()

IX-F-1 - Les pixmaps

Les pixmaps sont des structures de données qui contiennent des imagesqui peuvent être utilisées à plusieurs endroits, mais le sont le plus souvent sous la forme d'icônes pour le bureau X ou bien de pointeurs.

Un pixmap de seulement deux couleurs est un bitmap. Des routines additionnelles existent pour manipuler ce cas, relativement commun, à part.

Pour comprendre les pixmaps, il vous faut comprendre le fonctionnement du système X Window. Sous X, il n'est pas nécessaire que les applications tournent sur l'ordinateur qui dialogue avec l'utilisateur. Les différentes applications, appelées "clients", communiquent toutes avec un programme qui se charge de l'affichage de la partie graphique ainsi que de la gestion du clavier et de la souris. Ce programme, qui dialogue directement avec l'utilisateur, est appelé un "serveur d'affichage" ou "serveur X". Du fait que la communication peut avoir lieu dans le cadre d'un réseau, il est important que certaines informations soient conservées par le serveur X. Les pixmaps, par exemple, sont stockés dans sa mémoire. Cela signifie que, une fois que des valeurs de pixmaps sont définies, elles n'ont plus besoin d'être à nouveau transmises dans le réseau ; une simple commande sera envoyée pour "afficher le pixmap numéro XYZ à tel endroit". Même si vous n'utilisez pas X avec GTK en ce moment, l'utilisation de structures comme les pixmaps fera tourner convenablement vos programmes sous X.

Pour utiliser des pixmaps dans PyGTK, il nous faut d'abord construire un GdkPixmap en nous servant des fonctions gtk.gdk de PyGTK. Les pixmaps peuvent être créés soit à partir de données en mémoire, soit à partir de données lues dans un fichier. Nous allons examiner les différents appels de création.

pixmap = gtk.gdk.pixmap_create_from_data(window, data, width, height, fg, bg)

On utilise la routine ci-dessus pour créer un pixmap à partir de données (data) en mémoire. Sa profondeur de couleur est donnée par depth. Si depth vaut -1, la profondeur de couleur sera dérivée de celle de window (fenêtre). Pour représenter les couleurs, chaque pixel utilisera autant de bits de données que mentionné à l'argument depth (profondeur). width (largeur) et height (hauteur) sont spécifiés en pixels. L'argument window doit faire référence à une GdkWindow réalisée, les ressources d'un pixmap n'ayant de sens que dans le contexte de l'écran où il doit être affiché. fg et bg sont les couleurs de premier plan (foreground) et d'arrière-plan (background) du pixmap.

Les pixmaps peuvent être créés à partir de fichiers XPM grâce à la fonction :

pixmap, masque = gtk.gdk.pixmap_create_from_xpm(window, transparent_color, filename)

Le format XPM est une représentation lisible d'un pixmap, destinée au système X Window. Il s'agit d'un format très répandu et de nombreux utilitaires existent pour créer des fichiers images dans ce format. Dans la fonction pixmap_create_from_xpm(), le premier argument est un type GdkWindow — la majorité des widgets GTK possèdent une fenêtre GdkWindow sous-jacente, récupérable par l'attribut window du widget. Le fichier est spécifié à l'argument filename (nom du fichier). Il doit contenir une image au format XPM à charger dans la structure pixmap. Le masque est un bitmap qui indique les bits de pixmap devant être opaques ; il est créé par la fonction. Tous les autres pixels sont colorés en utilisant la couleur spécifiée par transparent_color (couleur transparente). Vous trouverez ci-dessous un exemple d'utilisation de cette fonction.

Les pixmaps peuvent également être créés à partir de données en mémoire, et ce avec la fonction suivante :

pixmap, masque = gtk.gdk.pixmap_create_from_xpm_d(window, transparent_color, data)

Cette fonction permet d'incorporer de petites images à un programme sous la forme de données au format XPM. Un pixmap sera créé à partir de ces données-là plutôt qu'en lisant celles d'un fichier. Voici un exemple de ces données :

1. donnees_xpm = [

2. "16 16 3 1",

3. "       c None",

4. ".      c #000000000000",

5. "X      c #FFFFFFFFFFFF",

6. "                ",

7. "   ......       ",

8. "   .XXX.X.      ",

9. "   .XXX.XX.     ",

10. "   .XXX.XXX.    ",

11. "   .XXX.....    ",

12. "   .XXXXXXX.    ",

13. "   .XXXXXXX.    ",

14. "   .XXXXXXX.    ",

15. "   .XXXXXXX.    ",

16. "   .XXXXXXX.    ",

17. "   .XXXXXXX.    ",

18. "   .XXXXXXX.    ",

19. "   .........    ",

20. "                ",

21. "                "

22. ]

Enfin, la dernière possibilité permet de créer un pixmap vierge, prêt pour les opérations de dessin :

pixmap = gtk.gdk.Pixmap(window, width, height, depth=-1)

window est soit une GdkWindow, soit None. Dans le premier cas, depth (profondeur) peut valoir -1 afin d'indiquer que la profondeur doit être déterminée par la fenêtre. Dans le second cas, depth devra être spécifié.

Le programme pixmap.py est un exemple d'utilisation d'un pixmap dans un bouton. La Figure 9.6 en montre le résultat :

Figure 9.6. Exemples de pixmap dans un bouton

Voici le code source :

1. #!/usr/bin/env python

3. # exemple pixmap.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # Données XPM d'une icône "Ouvrir Fichier"

10. donnees_xpm = [

11. "16 16 3 1",

12. "       c None",

13. ".      c #000000000000",

14. "X      c #FFFFFFFFFFFF",

15. "                ",

16. "   ......       ",

17. "   .XXX.X.      ",

18. "   .XXX.XX.     ",

19. "   .XXX.XXX.    ",

20. "   .XXX.....    ",

21. "   .XXXXXXX.    ",

22. "   .XXXXXXX.    ",

23. "   .XXXXXXX.    ",

24. "   .XXXXXXX.    ",

25. "   .XXXXXXX.    ",

26. "   .XXXXXXX.    ",

27. "   .XXXXXXX.    ",

28. "   .........    ",

29. "                ",

30. "                "

31. ]

33. class ExemplePixmap:

34. # Si elle est invoquée (via le "delete_event"), cette fonction ferme l'application

35. def fermer_application(self, widget, evnmt, data=None):

36. gtk.main_quit()

37. return False

39. # Fonction invoquée par un clic sur le bouton. Elle affiche juste un message.

40. def clic_bouton(self, widget, data=None):

41. print "Clic sur le bouton"

43. def __init__(self):

44. # création de la fenêtre principale, et connexion du signal

45. # delete_event a la fermeture de l'application

46. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

47. fenetre.connect("delete_event", self.fermer_application)

48. fenetre.set_border_width(10)

49. fenetre.show()

51. # venons-en à la création du pixmap à partir des données XPM

52. pixmap, masque = gtk.gdk.pixmap_create_from_xpm_d(fenetre.window,

53. None,

54. donnees_xpm)

56. # Création d'un widget gtk.Image pour contenir le pixmap

57. image = gtk.Image()

58. image.set_from_pixmap(pixmap, masque)

59. image.show()

61. # Création d'un bouton pour contenir le gtk.Image

62. bouton = gtk.Button()

63. bouton.add(image)

64. fenetre.add(bouton)

65. bouton.show()

67. bouton.connect("clicked", self.clic_bouton)

69. def main():

70. gtk.main()

71. return 0

73. if __name__ == "__main__":

74. ExemplePixmap()

75. main()

Les pixmap ont malgré tout cet inconvénient que l'objet affiché est toujours rectangulaire, quelle que soit l'image. On aimerait bien pouvoir créer des bureaux ou des applications avec des icônes aux formes plus naturelles. Des boutons ronds seraient, par exemple, bienvenus dans l'interface d'un jeu. C'est ici qu'entrent en jeu les fenêtres adaptées.

Il s'agit en fait simplement d'un pixmap dont les pixels d'arrière-plan sont transparents. Ainsi, lorsque l'image d'arrière-plan est multicolore, on ne l'encombre pas avec la bordure rectangulaire inadaptée de notre icône. Le programme d'exemple brouette.py affiche une image représentant une brouette pleine sur le bureau. La Figure 9.7 montre cette brouette par-dessus une fenêtre de terminal :

Figure 9.7. Exemple de fenêtre en forme de brouette

Voici le code source de brouette.py :

1. #!/usr/bin/env python

3. # exemple brouette.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. # XPM

10. BrouettePleine_xpm = [

11. "48 48 64 1",

12. "       c None",

13. ".      c #DF7DCF3CC71B",

14. "X      c #965875D669A6",

15. "o      c #71C671C671C6",

16. "O      c #A699A289A699",

17. "+      c #965892489658",

18. "@      c #8E38410330C2",

19. "#      c #D75C7DF769A6",

20. "$      c #F7DECF3CC71B",

21. "%      c #96588A288E38",

22. "&      c #A69992489E79",

23. "*      c #8E3886178E38",

24. "=      c #104008200820",

25. "-      c #596510401040",

26. ";      c #C71B30C230C2",

27. ":      c #C71B9A699658",

28. ">      c #618561856185",

29. ",      c #20811C712081",

30. "<      c #104000000000",

31. "1      c #861720812081",

32. "2      c #DF7D4D344103",

33. "3      c #79E769A671C6",

34. "4      c #861782078617",

35. "5      c #41033CF34103",

36. "6      c #000000000000",

37. "7      c #49241C711040",

38. "8      c #492445144924",

39. "9      c #082008200820",

40. "0      c #69A618611861",

41. "q      c #B6DA71C65144",

42. "w      c #410330C238E3",

43. "e      c #CF3CBAEAB6DA",

44. "r      c #71C6451430C2",

45. "t      c #EFBEDB6CD75C",

46. "y      c #28A208200820",

47. "u      c #186110401040",

48. "i      c #596528A21861",

49. "p      c #71C661855965",

50. "a      c #A69996589658",

51. "s      c #30C228A230C2",

52. "d      c #BEFBA289AEBA",

53. "f      c #596545145144",

54. "g      c #30C230C230C2",

55. "h      c #8E3882078617",

56. "j      c #208118612081",

57. "k      c #38E30C300820",

58. "l      c #30C2208128A2",

59. "z      c #38E328A238E3",

60. "x      c #514438E34924",

61. "c      c #618555555965",

62. "v      c #30C2208130C2",

63. "b      c #38E328A230C2",

64. "n      c #28A228A228A2",

65. "m      c #41032CB228A2",

66. "M      c #104010401040",

67. "N      c #492438E34103",

68. "B      c #28A2208128A2",

69. "V      c #A699596538E3",

70. "C      c #30C21C711040",

71. "Z      c #30C218611040",

72. "A      c #965865955965",

73. "S      c #618534D32081",

74. "D      c #38E31C711040",

75. "F      c #082000000820",

76. "                                                ",

77. "          .XoO                                  ",

78. "         +@#$%o&                                ",

79. "         *=-;#::o+                              ",

80. "           >,<12#:34                            ",

81. "             45671#:X3                          ",

82. "               +89<02qwo                        ",

83. "e*                >,67;ro                       ",

84. "ty>                 459@>+&&                    ",

85. "$2u+                  >                ",

86. "Oh$;ya             *3d.a8j,Xe.d3g8+             ",

87. " Oh$;ka          *3d$a8lz,,xxc:.e3g54           ",

88. "  Oh$;kO       *pd$%svbzz,sxxxxfX..&wn>         ",

89. "   Oh$@mO    *3dthwlsslszjzxxxxxxx3:td8M4       ",

90. "    Oh$@g& *3d$XNlvvvlllm,mNwxxxxxxxfa.:,B*     ",

91. "     Oh$@,Od.czlllllzlmmqV@V#V@fxxxxxxxf:%j5&   ",

92. "      Oh$1hd5lllslllCCZrV#r#:#2AxxxxxxxxxcdwM*  ",

93. "       OXq6c.%8vvvllZZiqqApA:mq:Xxcpcxxxxxfdc9* ",

94. "        2r<6gde3bllZZrVi7S@SV77A::qApxxxxxxfdcM ",

95. "        :,q-6MN.dfmZZrrSS:#riirDSAX@Af5xxxxxfevo",

96. "         +A26jguXtAZZZC7iDiCCrVVii7Cmmmxxxxxx%3g",

97. "          *#16jszN..3DZZZZrCVSA2rZrV7Dmmwxxxx&en",

98. "           p2yFvzssXe:fCZZCiiD7iiZDiDSSZwwxx8e*>",

99. "           OA1666     >=01-kuu666>        ",

100. "                 ,6ky&      &46-10ul,66,        ",

101. "                 Ou0<>       o66y66By7=xu664       ",

102. "                   <>       +66uv,zN666*       ",

103. "                              566,xxj669        ",

104. "                              4666FF666>        ",

105. "                               >966666M         ",

106. "                                oM6668+         ",

107. "                                  *4            ",

108. "                                                ",

109. "                                                "

110. ]

112. class ExempleBrouette:

113. # Si elle est invoquée (via le "delete_event"), cette fonction ferme l'application

114. def fermer_application(self, widget, evnmt, data=None):

115. gtk.main_quit()

116. return False

118. def __init__(self):

119. # Création de la fenêtre principale et connexion du signal "delete_event"

120. # à la fermeture de l'application. Notez que la fenêtre principale n'aura

121. # pas de barre de titre, car nous la définissons comme popup.

122. fenêtre = gtk.Window(gtk.WINDOW_POPUP)

123. fenetre.connect("delete_event", self.fermer_application)

124. fenetre.set_events(fenetre.get_events() | gtk.gdk.BUTTON_PRESS_MASK)

125. fenetre.connect("button_press_event", self.fermer_application)

126. fenetre.show()

128. # Venons-en à la création du pixmap et du widget gtk.Image

129. pixmap, masque = gtk.gdk.pixmap_create_from_xpm_d(

130. fenetre.window, None, BrouettePleine_xpm)

131. image = gtk.Image()

132. image.set_from_pixmap(pixmap, masque)

133. image.show()

135. # Pour afficher l'image, on va la placer dans un widget fixe

136. wfixe = gtk.Fixed()

137. wfixe.set_size_request(200, 200)

138. wfixe.put(image, 0, 0)

139. fenetre.add(wfixe)

140. wfixe.show()

142. # Ici nous masquons tout sauf l'image elle-même

143. fenetre.shape_combine_mask(masque, 0, 0)

145. # On affiche la fenêtre

146. fenetre.set_position(gtk.WIN_POS_CENTER_ALWAYS)

147. fenetre.show()

149. def main():

150. gtk.main()

151. return 0

153. if __name__ == "__main__":

154. ExempleBrouette()

155. main()

Pour rendre la brouette sensible à la pression d'un bouton de la souris, on a connecté son signal "button_press_event" à la méthode fermer_application() (lignes 138-139), laquelle fait se terminer le programme.

IX-G - Les règles

Le widget gtk.Ruler sert à indiquer la position du pointeur de la souris dans une fenêtre donnée. Une fenêtre peut être dotée d'une règle horizontale s'étendant sur sa largeur et d'une verticale dans sa hauteur. La position exacte du pointeur mesurée par la règle est matérialisée sur celle-ci par un petit indicateur triangulaire.

Le widget doit tout d'abord être créé. On utilisera à cet effet les fonctions suivantes :

1. regle_h = gtk.HRuler()    # crée une règle horizontale

3. regle_v = gtk.VRuler()    # crée une règle verticale

Une fois qu'une règle est créée, on peut définir l'unité de mesure : PIXELS (pixels), INCHES (pouces) ou CENTIMETERS (centimètres). La méthode à utiliser est la suivante :

regle.set_metric(metric)

L'unité de mesure par défaut est PIXELS.

regle.set_metric(gtk.PIXELS)

Les gtk.Ruler possèdent d'autres caractéristiques configurables, telles que la graduation de l'intervalle choisi et la position initiale de l'indicateur. Ces deux options se définissent avec la méthode suivante :

regle.set_range(lower, upper, position, max_size)

Les arguments lower (minimum) et upper (maximum) spécifient l'étendue de la règle, et max_size (taille maximum) représente le plus grand nombre qui puisse être affiché. Quant à position, il précise la position initiale de l'indicateur triangulaire dans la règle.

On peut donc faire s'étendre une règle verticale sur une fenêtre de 800 pixels de haut ainsi :

regle_v.set_range(0, 800, 0, 800)

Les graduations de la règle iront de 0 à 800, avec un nombre tous les 100 pixels. Si l'on voulait que la règle aille de 7 à 16, on coderait plutôt :

regle_v.set_range(7, 16, 0, 20)

L'indicateur, petite marque triangulaire présente sur la règle, précise la position du pointeur mesurée par cette dernière. Si l'on utilise une règle dans le but de suivre le pointeur de la souris, le signal "motion_notify_event" (événement "notification de mouvement") devrait être connecté à la méthode "motion_notify_event" de la règle. Il nous faut définir une fonction de rappel du "motion_notify_event" pour la zone de dessin et utiliser connect_object() pour faire émettre à la règle un signal de notification de mouvement :

1. def notif_mouvement(regle, evnmt):

2. return regle.emit("motion_notify_event", evnmt)

4. zonedess.connect_object("motion_notify_event", notif_mouvement, regle)

Le programme d'exemple regles.py crée une zone de dessin encadrée par une règle horizontale en haut et une verticale à gauche. La zone de dessin fait 600 pixels de large pour 800 pixels de haut. La règle horizontale est graduée de 7 à 13 avec une marque tous les 100 pixels, tandis que la règle verticale est graduée de 0 à 400 avec une marque tous les 100 pixels. Le placement de la zone de dessin ainsi que des règles est effectué au moyen d'un tableau. La Figure 9.8 montre le résultat :

Figure 9.8. Exemple de règles

Le code source de regles.py est le suivant :

1. #!/usr/bin/env python

3. # exemple regles.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleRegles:

10. TAILLE_X = 400

11. TAILLE_Y = 400

13. # Cette routine prend le controle lorsque l'on clique sur le bouton fermer

14. def fermer_application(self, widget, evnmt, data=None):

15. gtk.main_quit()

16. return False

18. def __init__(self):

19. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

20. fenetre.connect("delete_event", self.fermer_application)

21. fenetre.set_border_width(10)

23. # Creation d'un tableau pour le placement des regles et de la zone de dessin

24. tableau = gtk.Table(3, 2, False)

25. fenetre.add(tableau)

27. zonedess = gtk.DrawingArea()

28. zonedess.set_size_request(self.TAILLE_X, self.TAILLE_Y)

29. tableau.attach(zonedess, 1, 2, 1, 2,

30. gtk.EXPAND|gtk.FILL, gtk.FILL, 0, 0)

31. zonedess.set_events(gtk.gdk.POINTER_MOTION_MASK |

32. gtk.gdk.POINTER_MOTION_HINT_MASK )

34. # La règle horizontale va en haut. Quand la souris passe dans la zone de dessin,

35. # un "motion_notify-event" est transmis au gestionnaire d'événement approprié,

36. # qui le passe a son tour a la règle.

37. regle_h = gtk.HRuler()

38. regle_h.set_metric(gtk.PIXELS)

39. regle_h.set_range(7, 13, 0, 20)

40. def notif_mouvmt(regle, evnmt):

41. return regle.emit("motion_notify_event", evnmt)

42. zonedess.connect_object("motion_notify_event", notif_mouvmt, regle_h)

43. tableau.attach(regle_h, 1, 2, 0, 1,

44. gtk.EXPAND|gtk.SHRINK|gtk.FILL, gtk.FILL, 0, 0 )

46. # La règle verticale va à gauche. Quand la souris passe dans la zone de dessin,

47. # un "motion_notify-event" est transmis au gestionnaire d'événement approprié,

48. # qui le passe a son tour a la règle.

49. regle_v = gtk.VRuler()

50. regle_v.set_metric(gtk.PIXELS)

51. regle_v.set_range(0, self.TAILLE_Y, 10, self.TAILLE_Y)

52. zonedess.connect_object("motion_notify_event", notif_mouvmt, regle_v)

53. tableau.attach(regle_v, 0, 1, 1, 2,

54. gtk.FILL, gtk.EXPAND|gtk.SHRINK|gtk.FILL, 0, 0 )

56. # On affiche tout

57. zonedess.show()

58. regle_h.show()

59. regle_v.show()

60. tableau.show()

61. fenetre.show()

63. def main():

64. gtk.main()

65. return 0

67. if __name__ == "__main__":

68. ExempleRegles()

69. main()

Les lignes 42 et 52 connectent la fonction de rappel notif_mouvmt() à la zone de dessin, la première ligne passant regle_h et la seconde regle_v comme données utilisateur. La fonction de rappel notif_mouvmt() sera invoquée deux fois à chaque mouvement de la souris : une fois avec regle_h et une autre avec regle_v.

IX-H - La barre d'état

La barre d'état (gtk.Statusbar) est un widget assez simple qui sert à afficher un message textuel. Elle tient à jour une pile des messages qu'on lui a donnés, de sorte que le fait de dépiler le message courant entraine le réaffichage du précédent.

Afin de permettre l'utilisation de la même barre d'état par différentes parties d'une application, le widget gtk.Statusbar distribue des "identifiants contextuels" qui serviront à identifier différents "utilisateurs". Le message affiché est celui qui se trouve au sommet de la pile, peu importe son contexte. Les messages sont empilés suivant le schéma "dernier entré/premier sorti", et non suivant leur identifiant contextuel.

On crée une barre d'état avec un appel à :

barredetat = gtk.Statusbar()

On obtient un nouvel identifiant contextuel en invoquant la méthode suivante avec une courte description textuelle du contexte (context_description) :

context_id = barredetat.get_context_id(context_description)

Trois autres méthodes agissent sur les barres d'état :

1. message_id = barredetat.push(context_id, text)

3. barredetat.pop(context_id)

5. barredetat.remove(context_id, message_id)

La première, push() (empiler), sert à ajouter un nouveau message à barredetat. Elle renvoie un identifiant de message (message_id). Ce dernier, accompagné du context_id approprié peut être transmis plus tard à la méthode remove() (retirer) afin de retirer le message de la pile de barredetat.

La méthode pop() retire le message, ayant le context_id spécifié, qui se trouve le plus haut placé dans la pile.

Le programme d'exemple barredetat.py crée une barre d'état ainsi que deux boutons : un pour empiler des éléments dans la barre d'état, et un autre pour dépiler le dernier élément affiché. La Figure 9.9 montre le résultat :

Figure 9.9. Exemple de barre d'état

Voici le code source de barredetat.py :

1. #!/usr/bin/env python

3. # exemple barredetat.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBarreEtat:

10. def empiler(self, widget, donnees):

11. tampon = " Element %d" % self.compteur

12. self.compteur = self.compteur + 1

13. self.barredetat.push(donnees, tampon)

14. return

16. def depiler(self, widget, donnees):

17. self.barredetat.pop(donnees)

18. return

20. def __init__(self):

21. self.compteur = 1

22. # Création d'une fenêtre

23. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

24. fenetre.set_size_request(200, 100)

25. fenetre.set_title("Exemple de barre d'état PyGTK")

26. fenetre.connect("delete_event", gtk.main_quit)

28. boite_v = gtk.VBox(False, 1)

29. fenetre.add(boite_v)

30. boite_v.show()

32. self.barredetat = gtk.Statusbar()

33. boite_v.pack_start(self.barredetat, True, True, 0)

34. self.barredetat.show()

36. context_id = self.barredetat.get_context_id("Exemple barredetat")

38. bouton = gtk.Button("empiler un élément")

39. bouton.connect("clicked", self.empiler, context_id)

40. boite_v.pack_start(bouton, True, True, 2)

41. bouton.show()

43. bouton = gtk.Button("dépiler le dernier element")

44. bouton.connect("clicked", self.depiler, context_id)

45. boite_v.pack_start(bouton, True, True, 2)

46. bouton.show()

48. # On affichera toujours la fenêtre à la derniere etape, de sorte que

49. # tout apparaisse a l'ecran en un bloc.

50. fenetre.show()

52. def main():

53. gtk.main()

54. return 0

56. if __name__ == "__main__":

57. ExempleBarreEtat()

58. main()

IX-I - Les champs de saisie

Le widget gtk.Entry permet la saisie et l'affichage de texte dans une boite, sur une seule ligne. Le texte peut être défini en appelant différentes méthodes, lesquelles peuvent soit remplacer intégralement le contenu courant de la boite, soit y ajouter du texte avant ou après.

La fonction de création d'un champ de saisie est la suivante :

champsaisie = gtk.Entry(max=0)

Si l'argument max est spécifié, il fixe une limite de longueur au texte du champ de saisie. S'il vaut 0, aucune limite n'est fixée.

La longueur maximale du champ de saisie peut être modifiée avec la méthode :

champsaisie.set_max_length(max)

La méthode qui suit modifie le texte présent dans le gtk.Entry.

champsaisie.set_text(text)

La méthode set_text() remplace le contenu courant du champ de saisie par text. Notez que la classe gtk.Entry implémente l'interface Editable (oui, gobject supporte les interfaces "à la Java") qui contient davantage de fonctions pour manipuler le contenu du champ de saisie. Par exemple, la méthode :

champsaisie.insert_text(text, position=0)

insère text dans champsaisie, à la position spécifiée.

On peut récupérer le contenu du gtk.Entry en appelant la méthode suivante. Cela nous servira pour notre fonction de rappel, plus bas.

texte = champsaisie.get_text()

Si l'on ne souhaite pas que le contenu du gtk.Entry puisse être modifié en tapant directement du texte dedans, on peut changer son éditabilité.

champsaisie.set_editable(is_editable)

La méthode ci-dessus permet de basculer le mode d'éditabilité du champ de saisie en donnant à l'argument is_editable (littéralement : "est éditable") la valeur TRUE (vrai) ou FALSE (faux).

Si l'on utilise le gtk.Entry dans un contexte où le texte saisi ne doit pas être visible, par exemple pour un mot de passe, on peut faire appel à la méthode suivante (qui attend également un drapeau booléen) :

champsaisie.set_visibility(visible)

La méthode qui suit permet de définir une partie du texte comme sélectionnée, de start (début) à end (fin). Cette option est principalement utilisée dans les cas où l'on fixe un texte par défaut pour le champ de saisie et que l'on souhaite que l'utilisateur puisse le supprimer facilement.

champsaisie.select_region(start, end)

Si l'on veut être prévenu lorsque l'utilisateur a saisi du texte, on peut se connecter aux signaux "activate" ou "changed". activate (activé) est émis lorsque l'utilisateur appuie sur la touche Entrée dans le widget gtk.Entry, et changed (changé) lorsque un changement est apporté au texte (par exemple, pour chaque caractère ajouté ou supprimé).

Le programme d'exemple champsaisie.py illustre l'utilisation d'un widget gtk.Entry. La Figure 9.10 montre ce que l'on obtient en exécutant le programme :

Figure 9.10. Exemple de champ de saisie

Voici le code source de champsaisie.py :

1. #!/usr/bin/env python

3. # exemple champsaisie.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleSaisieTexte:

10. def fct_rappel_entree(self, widget, champsaisie):

11. textesaisi = champsaisie.get_text()

12. print "Contenu du champ de saisie : %s\n" % textesaisi

14. def champsaisie_editable(self, casecocher, champsaisie):

15. champsaisie.set_editable(casecocher.get_active())

17. def champsaisie_visible(self, casecocher, champsaisie):

18. champsaisie.set_visibility(casecocher.get_active())

20. def __init__(self):

21. # Création d'une nouvelle fenêtre

22. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

23. fenetre.set_size_request(200, 100)

24. fenetre.set_title("Saisie de texte GTK")

25. fenetre.connect("delete_event", gtk.main_quit)

27. boite_v = gtk.VBox(False, 0)

28. fenetre.add(boite_v)

29. boite_v.show()

31. champsaisie = gtk.Entry()

32. champsaisie.set_max_length(50)

33. champsaisie.connect("activate", self.fct_rappel_entree, champsaisie)

34. champsaisie.set_text("salut")

35. champsaisie.insert_text(" tout le monde", len(champsaisie.get_text()))

36. champsaisie.select_region(0, len(champsaisie.get_text()))

37. boite_v.pack_start(champsaisie, True, True, 0)

38. champsaisie.show()

40. boite_h = gtk.HBox(False, 0)

41. boite_v.add(boite_h)

42. boite_h.show()

44. casecocher = gtk.CheckButton("Editable")

45. boite_h.pack_start(casecocher, True, True, 0)

46. casecocher.connect("toggled", self.champsaisie_editable, champsaisie)

47. casecocher.set_active(True)

48. casecocher.show()

50. casecocher = gtk.CheckButton("Visible")

51. boite_h.pack_start(casecocher, True, True, 0)

52. casecocher.connect("toggled", self.champsaisie_visible, champsaisie)

53. casecocher.set_active(True)

54. casecocher.show()

56. bouton = gtk.Button(stock=gtk.STOCK_CLOSE)

57. bouton.connect_object("clicked", gtk.main_quit, fenêtre)

58. boite_v.pack_start(bouton, True, True, 0)

59. bouton.set_flags(gtk.CAN_DEFAULT)

60. bouton.grab_default()

61. bouton.show()

62. fenetre.show()

64. def main():

65. gtk.main()

66. return 0

68. if __name__ == "__main__":

69. ExempleSaisieTexte()

70. main()

IX-J - Les boites d'incrément

On utilise généralement les boites d'incrément (gtk.SpinButton) pour permettre à l'utilisateur de sélectionner une valeur numérique dans un intervalle donné. Ce widget consiste en un champ de texte flanqué de deux boutons fléchés haut et bas. En sélectionnant l'un ou l'autre de ces boutons, on incrémente ou décrémente la valeur dans les limites de l'intervalle. Le champ de texte peut également être édité directement afin de saisir une valeur spécifique.

La valeur d'un gtk.SpinButton peut être affichée sous sa forme entière ou décimale, et il est possible de définir son pas d'incrémentation/décrémentation. On peut également décider que plus l'on maintient enfoncés les boutons, plus cette incrémentation/décrémentation s'accélère.

Les boites d'incrément utilisent des ajustements pour stocker les données concernant l'intervalle de valeurs du gtk.SpinButton. Cela fait du gtk.SpinButton un widget très puissant.

Pour bien montrer les informations stockées par l'ajustement, rappelons-nous sa fonction de création :

ajustement = gtk.Adjustment(value=0, lower=0, upper=0, step_incr=0, page_incr=0, page_size=0)

La boite d'incrément utilise ces attributs de l'ajustement de la manière suivante :

value

valeur initiale de la boite d'incrément

lower

limite basse de l'intervalle

upper

limite haute de l'intervalle

step_increment

pas de l'incrémentation/décrémentation lorsqu’on clique sur l'un des deux boutons avec le bouton 1 de la souris

page_increment

pas de l'incrémentation/décrémentation lorsqu’on clique sur l'un des deux boutons avec le bouton 2 de la souris

page_size

inutilisé

Un clic sur un des deux boutons avec le bouton 3 de la souris peut également servir à aller directement à la valeur upper ou lower. Voyons donc comment créer une boite à incrément :

boite_increment = gtk.SpinButton(adjustment=None, climb_rate=0.0, digits=0)

L'argument climb_rate accepte une valeur comprise entre 0.0 et 1.0 et indique le degré d'accélération de la boite d'incrément. L'argument digits spécifie le nombre de décimales de la valeur affichée.

On peut reconfigurer une boite à incrément après sa création en appelant la méthode suivante :

boite_increment.configure(adjustment, climb_rate, digits)

L'argument boite_increment spécifie le gtk.SpinButton à reconfigurer. Les autres arguments sont les mêmes que ceux décrits plus haut.

L'ajustement (adjustment) peut être défini et récupéré indépendamment à l'aide des deux méthodes suivantes :

1. boite_increment.set_adjustment(adjustment)

3. ajustement = spin_button.get_adjustment()

On peut également modifier le nombre de décimales ainsi :

boite_increment.set_digits(digits)

La valeur affichée par une boite d'incrément peut être changée avec cette méthode :

boite_increment.set_value(value)

La valeur courante d'une boite d'incrément peut être récupérée sous sa forme décimale ou entière à l'aide des deux méthodes suivantes :

1. valeur_dec = spin_button.get_value()

3. valeur_ent = spin_button.get_value_as_int()

Si l'on souhaite que la modification de la valeur soit effectuée par rapport à la valeur courante, on peut utiliser cette méthode :

boite_increment.spin(direction, increment)

Le paramètre direction peut prendre l'une de ces valeurs :

1. SPIN_STEP_FORWARD

2. SPIN_STEP_BACKWARD

3. SPIN_PAGE_FORWARD

4. SPIN_PAGE_BACKWARD

5. SPIN_HOME

6. SPIN_END

7. SPIN_USER_DEFINED

Cette méthode réunit plusieurs fonctionnalités que nous allons tenter d'expliquer clairement. Un grand nombre de ces réglages utilise les valeurs de l'ajustement associées à la boite d'incrément.

SPIN_STEP_FORWARD (en avant d'un pas) et SPIN_STEP_BACKWARD (en arrière d'un pas) modifient la valeur de la boite d'incrément en lui ajoutant/soustrayant celle spécifiée par l'argument increment, ou, si ce dernier vaut 0, celle du step_increment de l'ajustement.

SPIN_PAGE_FORWARD (en avant d'une page) et SPIN_PAGE_BACKWARD (en arrière d'une page) modifient simplement la valeur de la boite d'incrément en lui ajoutant/soustrayant celle de increment.

SPIN_HOME (début) fixe la valeur de la boite d'incrément à la borne minimum de l'intervalle de l'ajustement.

SPIN_END (fin) fixe la valeur de la boite d'incrément à la borne maximum de l'intervalle de l'ajustement.

SPIN_USER_DEFINED (défini par l'utilisateur) modifie simplement la valeur de la boite d'incrément en lui ajoutant la valeur spécifiée.

Abandonnons à présent les méthodes de définition et récupération des attributs d'intervalle, pour nous intéresser à celles qui affectent l'apparence et le comportement du widget gtk.SpinButton en lui-même.

La première de ces méthodes sert à faire en sorte que la boite de texte du gtk.SpinButton ne puisse contenir que des valeurs numériques. L'utilisateur ne pourra alors pas y saisir autre chose :

boite_increment.set_numeric(numeric)

Si numeric vaut TRUE (vrai), le champ de saisie n'acceptera que les valeurs numériques. Si elle vaut FALSE (faux), aucune contrainte n'est posée.

On peut indiquer si l'on veut que la boite d'incrément effectue une boucle après avoir passé la valeur maximum de l'intervalle, et revienne donc à la valeur minimum. On utilisera pour ce faire la méthode suivante :

boite_increment.set_wrap(wrap)

La boite d'incrément tournera en boucle si wrap vaut TRUE.

On peut faire en sorte que la boite d'incrément arrondisse la valeur au step_increment le plus proche (le step_increment est défini dans l'ajustement utilisé par la boite d'incrément). On fera alors appel à la méthode suivante, où snap_to_ticks (s'accrocher aux marques) devra valoir TRUE :

boite_increment.set_snap_to_ticks(snap_to_ticks)

Le mode d'actualisation d'une boite d'incrément peut être modifié avec la méthode suivante :

boite_increment.set_update_policy(policy)

Les valeurs possibles pour policy (mode) sont :

1. UPDATE_ALWAYS

3. UPDATE_IF_VALID

Ces modes d'actualisation affectent le comportement de la boite d'incrément lorsqu'elle analyse le texte saisi et qu'elle synchronise sa valeur avec celles de l'ajustement.

Avec UPDATE_IF_VALID (actualiser si valide), la valeur de la boite d'incrément n'est modifiée que si le texte saisi est une valeur numérique comprise dans l'intervalle de l'ajustement. Dans le cas contraire, le champ de saisie reprend sa valeur courante.

Avec UPDATE_ALWAYS (actualiser toujours), toute erreur sera ignorée lors de la conversion du texte en valeur numérique.

Pour finir, on peut demander explicitement une actualisation de la boite d'incrément :

boite_increment.update()

Le programme d'exemple boitesincrement.py offre une démonstration d'utilisation des boites d'incrément, incluant la modification de plusieurs caractéristiques. La Figure 9.11 montre ce que l'on obtient en lançant le programme :

Figure 9.11. Exemple de boites d'incrément

Voici le code source de boitesincrement.py :

1. #!/usr/bin/env python

3. # exemple boiteincrement.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBoiteIncrement:

10. def modif_arrondi(self, widget, boiteincr):

11. boiteincr.set_snap_to_ticks(widget.get_active())

13. def modif_numerique(self, widget, boiteincr):

14. boiteincr.set_numeric(widget.get_active())

16. def modif_decimales(self, widget, boiteincr, boiteincr1):

17. boiteincr1.set_digits(boiteincr.get_value_as_int())

19. def recup_valeur(self, widget, donnees, boiteincr, boiteincr2, etiquette):

20. if donnees == 1:

21. tampon = "%d" % boiteincr.get_value_as_int()

22. else:

23. tampon = "%0.*f" % (boiteincr2.get_value_as_int(),

24. boiteincr.get_value())

25. etiquette.set_text(tampon)

27. def __init__(self):

28. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

29. fenetre.connect("destroy", gtk.main_quit)

30. fenetre.set_title("Boites d'increment")

32. boite_v0 = gtk.VBox(False, 5)

33. boite_v0.set_border_width(10)

34. fenetre.add(boite_v0)

36. cadre = gtk.Frame("Sans accélération")

37. boite_v0.pack_start(cadre, True, True, 0)

39. boite_v1 = gtk.VBox(False, 0)

40. boite_v1.set_border_width(5)

41. cadre.add(boite_v1)

43. # Boites d'incrément pour le jour, le mois et l'année

44. boite_h = gtk.HBox(False, 0)

45. boite_v1.pack_start(boite_h, True, True, 5)

47. boite_v2 = gtk.VBox(False, 0)

48. boite_h.pack_start(boite_v2, True, True, 5)

50. etiquette = gtk.Label("Jour :")

51. etiquette.set_alignment(0, 0.5)

52. boite_v2.pack_start(etiquette, False, True, 0)

54. ajustement = gtk.Adjustment(1.0, 1.0, 31.0, 1.0, 5.0, 0.0)

55. boiteincr = gtk.SpinButton(ajustement, 0, 0)

56. boiteincr.set_wrap(True)

57. boite_v2.pack_start(boiteincr, False, True, 0)

59. boite_v2 = gtk.VBox(False, 0)

60. boite_h.pack_start(boite_v2, True, True, 5)

62. etiquette = gtk.Label("Mois :")

63. etiquette.set_alignment(0, 0.5)

64. boite_v2.pack_start(etiquette, False, True, 0)

66. ajustement = gtk.Adjustment(1.0, 1.0, 12.0, 1.0, 5.0, 0.0)

67. boiteincr = gtk.SpinButton(ajustement, 0, 0)

68. boiteincr.set_wrap(True)

69. boite_v2.pack_start(boiteincr, False, True, 0)

71. boite_v2 = gtk.VBox(False, 0)

72. boite_h.pack_start(boite_v2, True, True, 5)

74. etiquette = gtk.Label("Annee :")

75. etiquette.set_alignment(0, 0.5)

76. boite_v2.pack_start(etiquette, False, True, 0)

78. ajustement = gtk.Adjustment(1998.0, 0.0, 2100.0, 1.0, 100.0, 0.0)

79. boiteincr = gtk.SpinButton(ajustement, 0, 0)

80. boiteincr.set_wrap(False)

81. boiteincr.set_size_request(55, -1)

82. boite_v2.pack_start(boiteincr, False, True, 0)

84. cadre = gtk.Frame("Avec accélération")

85. boite_v0.pack_start(cadre, True, True, 0)

87. boite_v1 = gtk.VBox(False, 0)

88. boite_v1.set_border_width(5)

89. cadre.add(boite_v1)

91. boite_h = gtk.HBox(False, 0)

92. boite_v1.pack_start(boite_h, False, True, 5)

94. boite_v2 = gtk.VBox(False, 0)

95. boite_h.pack_start(boite_v2, True, True, 5)

97. etiquette = gtk.Label("Valeur :")

98. etiquette.set_alignment(0, 0.5)

99. boite_v2.pack_start(etiquette, False, True, 0)

101. ajustement = gtk.Adjustment(0.0, -10000.0, 10000.0, 0.5, 100.0, 0.0)

102. boiteincr1 = gtk.SpinButton(ajustement, 1.0, 2)

103. boiteincr1.set_wrap(True)

104. boiteincr1.set_size_request(100, -1)

105. boite_v2.pack_start(boiteincr1, False, True, 0)

107. boite_v2 = gtk.VBox(False, 0)

108. boite_h.pack_start(boite_v2, True, True, 5)

110. etiquette = gtk.Label("Décimales :")

111. etiquette.set_alignment(0, 0.5)

112. boite_v2.pack_start(etiquette, False, True, 0)

114. ajustement = gtk.Adjustment(2, 1, 5, 1, 1, 0)

115. boiteincr2 = gtk.SpinButton(ajustement, 0.0, 0)

116. boiteincr2.set_wrap(True)

117. ajustement.connect("value_changed", self.modif_decimales, boiteincr2, boiteincr1)

118. boite_v2.pack_start(boiteincr2, False, True, 0)

120. boite_h = gtk.HBox(False, 0)

121. boite_v1.pack_start(boite_h, False, True, 5)

123. bouton = gtk.CheckButton("Déplacement tous les 0,5")

124. bouton.connect("clicked", self.modif_arrondi, boiteincr1)

125. boite_v1.pack_start(bouton, True, True, 0)

126. bouton.set_active(True)

128. bouton = gtk.CheckButton("Saisie numérique seulement")

129. bouton.connect("clicked", self.modif_numerique, boiteincr1)

130. boite_v1.pack_start(bouton, True, True, 0)

131. bouton.set_active(True)

133. etiquette_valeur = gtk.Label("")

135. boite_h = gtk.HBox(False, 0)

136. boite_v1.pack_start(boite_h, False, True, 5)

137. bouton = gtk.Button("Valeur entiere")

138. bouton.connect("clicked", self.recup_valeur, 1, boiteincr1, boiteincr2,

139. etiquette_valeur)

140. boite_h.pack_start(bouton, True, True, 5)

142. bouton = gtk.Button("Valeur decimale")

143. bouton.connect("clicked", self.recup_valeur, 2, boiteincr1, boiteincr2,

144. etiquette_valeur)

145. boite_h.pack_start(bouton, True, True, 5)

147. boite_v1.pack_start(etiquette_valeur, True, True, 0)

148. etiquette_valeur.set_text("0")

150. boite_h = gtk.HBox(False, 0)

151. boite_v0.pack_start(boite_h, False, True, 0)

153. bouton = gtk.Button("Fermer")

154. bouton.connect("clicked", gtk.main_quit)

155. boite_h.pack_start(bouton, True, True, 5)

156. fenetre.show_all()

158. def main():

159. gtk.main()

160. return 0

162. if __name__ == "__main__":

163. ExempleBoiteIncrement()

164. main()

IX-K - Les listes déroulantes

L'utilisation du widget gtk.Combo n'est plus recommandée depuis PyGTK 2.4.

La liste déroulante est un autre widget très simple, qui n'est en fait qu'une association d'autres widgets. Du point de vue de l'utilisateur, il s'agit d'un champ de saisie, accompagné d'un menu déroulant qui permet à l'utilisateur d'effectuer un choix parmi un ensemble d'entrées prédéfinies. L'utilisateur peut également taper directement une autre option dans le champ de saisie.

Les deux parties qui nous intéressent réellement dans le widget gtk.Combo sont le champ de saisie (appelé entry) et la liste (list). On peut accéder y en utilisant les attributs suivants :

1. listder.entry

3. listeder.list

Pour commencer, on créera une liste déroulante ainsi :

listeder = gtk.Combo()

Maintenant, si l'on souhaite définir la chaine de caractères du champ de saisie, la manipulation doit se faire directement sur ce dernier :

listeder.entry.set_text(text)

Pour définir les valeurs de la liste, on utilise la méthode suivante :

listeder.set_popdown_strings(strings)

Mais avant, il faudra créer une liste regroupant les chaines de caractères que l'on veut.

Voici un segment de code typique pour créer un ensemble d'options :

1. listechaines = [ "Chaine 1", "Chaine 2", "Chaine 3", "Chaine 4" ]

3. listeder.set_popdown_strings(listechaines)

Arrivé ici, on a configuré une liste déroulante fonctionnelle. Quelques aspects de son comportement peuvent encore être modifiés, et ce par l'intermédiaire des méthodes qui suivent :

1. listeder.set_use_arrows(val)

3. listeder.set_use_arrows_always(val)

5. listeder.set_case_sensitive(val)

La méthode set_use_arrows() laissera l'utilisateur modifier la valeur du champ de saisie avec les touches de déplacement haut et bas du clavier si val vaut TRUE. Cela ne fera pas apparaitre la liste, mais remplacera le texte courant du champ de saisie par l'entrée suivante ou précédente dans la liste (en fonction de la touche pressée). La méthode déclenche en effet une recherche, dans cette liste, de l'entrée qui correspond à la valeur du champ de saisie, puis sélectionne l'entrée qui la suit ou la précède. Dans un champ de saisie, les touches de déplacement du clavier servent habituellement à changer la zone active (on peut quand même le faire avec TAB). Notez que lorsque l'élément courant est le dernier élément de la liste, une pression sur la touche de déplacement vers le bas changera de zone active (idem pour le premier élément avec la touche de déplacement vers le haut).

Si la valeur courante du champ de saisie ne figure pas dans la liste, la méthode set_use_arrows() est désactivée.

La méthode set_use_arrows_always() permet également, si val vaut TRUE, l'utilisation des touches de déplacement pour passer d'une entrée de la liste déroulante à une autre. Toutefois, elle désactive complètement les fonctions de changement de zone active des touches de déplacement. Les valeurs de la liste tournent alors en boucle.

La méthode set_case_sensitive() détermine si GTK doit prendre en compte la casse des caractères lors des recherches d'entrées dans la liste. Cela sert lorsque l'on demande au gtk.Combo de trouver une valeur dans la liste à partir du texte entré dans le champ de saisie. Cette sorte de complétion peut se réaliser en prenant la casse en compte ou pas, en fonction de ce qu'indique la méthode. Le gtk.Combo peut aussi simplement compléter le texte du champ de saisie si l'utilisateur presse la combinaison de touches MOD-1-Tab. MOD-1 est souvent associé à la touche Alt par l'utilitaire xmodmap. Notez, cependant, que certains gestionnaires de fenêtres utilisent eux aussi cette combinaison, et que dans ce cas ils l'emportent sur GTK.

Maintenant que nous disposons d'une liste déroulante taillée sur mesure, il ne nous reste plus qu'à pouvoir en obtenir des données. C'est relativement simple. La plupart du temps, les seules données que l'on souhaitera récupérer seront celles du champ de saisie, le combo.entry. On pourra vouloir lui connecter le signal "activate" (qui indique que l'utilisateur a pressé la touche Entrée), ou bien lire le texte. Dans le premier cas, on utilisera quelque chose comme ceci :

listeder.entry.connect("activate", fct_rappel, donnees)

Quant à la récupération du texte, elle s'effectue simplement, et à n'importe quel moment, par un appel à la méthode du champ de saisie suivante :

chaine = listeder.entry.get_text()

Voilà, c'est à peu près tout ce qu'il y a à savoir. Il y a bien cette méthode :

listeder.disable_activate()

qui désactive le signal "activate" pour le champ de saisie. Personnellement, je ne vois pas pourquoi l'on voudrait s'en servir, mais bon, elle existe.

IX-L - Le calendrier

Le widget gtk.Calendar est un moyen efficace pour afficher et récupérer des informations calendaires organisées par mois. Il s'agit d'un widget très simple à créer et à utiliser.

Pour créer un calendrier, il suffit d'utiliser la méthode suivante :

calendrier = gtk.Calendar()

Par défaut, le calendrier affiche le mois et l'année courants.

On pourra parfois avoir besoin de modifier plusieurs données de ce widget ; les méthodes qui suivent permettront d'apporter un grand nombre de modifications au calendrier, ceci sans que l'utilisateur n'ait à voir son affichage s'actualiser à chacune d'elles.

1. calendrier.freeze()

3. calendrier.thaw()

Le fonctionnement est exactement le même que pour les méthodes de gel (freeze) et de dégel (thaw) de tout autre widget.

Le calendrier dispose de quelques options permettant de changer son aspect et son comportement. On y accède par l'intermédiaire de la méthode suivante :

calendrier.display_options(flags)

L'argument flags (drapeaux) peut comporter n'importe lesquelles des cinq options suivantes, combinées le cas échéant avec l'opérateur de manipulation de bits OR (|) :

CALENDAR_SHOW_HEADING

Cette option spécifie que le mois et l'année doivent apparaitre dans l'affichage du calendrier.

CALENDAR_SHOW_DAY_NAMES

Cette option spécifie que des désignations à trois lettres doivent être affichées pour chaque jour (lun, mar, etc.).

CALENDAR_NO_MONTH_CHANGE

Cette option spécifie que l'utilisateur ne doit pas pouvoir afficher d'autre mois que celui qui est affiché. Cela peut être utile lorsqu’on ne souhaite afficher qu'un seul mois précis, par exemple dans le cas où l'on affiche 12 calendriers, un pour chaque mois d'une année donnée.

CALENDAR_SHOW_WEEK_NUMBERS

Cette option spécifie que le numéro de chaque semaine doit être affiché sur la gauche du calendrier (1er Jan = Semaine 1, 31 Déc = Semaine 52).

CALENDAR_WEEK_START_MONDAY

Cette option spécifie que la semaine du calendrier commence le lundi, et non pas le dimanche (réglage par défaut). Seul est affecté l'ordre dans lequel sont affichés les jours de gauche à droite.

On utilise les méthodes suivantes pour définir la date affichée :

1. resultat = calendrier.select_month(month, year)

3. calendrier.select_day(day)

La méthode select_month() renvoie une valeur booléenne indiquant si la sélection s'est déroulée avec succès.

Avec la méthode select_day(), le numéro de jour spécifié se voit sélectionné dans le mois courant, dans la mesure du possible. Un numéro de jour de 0 aura pour effet d'invalider toute sélection courante.

Outre le fait qu'un jour du mois puisse être sélectionné, chacun des autres jours peut être "marqué", c'est-à-dire mis en relief dans l'affichage du calendrier. On se servira des méthodes suivantes pour manipuler les jours marqués :

1. resultat = calendrier.mark_day(day)       # marquer un jour

3. resultat = calendrier.unmark_day(day)     # démarquer un jour

5. calendrier.clear_marks()        # enlever toutes les marques

mark_day() et unmark_day() renvoient une valeur booléenne indiquant si l'opération s'est déroulée avec succès. Notez que les marques sont conservées même lors de changements de mois ou d'année.

La dernière méthode du calendrier permet de récupérer la date, le mois et l'année sélectionnés.

année, mois, jour = calendrier.get_date()

Le widget gtk.Calendar peut générer un certain nombre de signaux indiquant un changement ou une sélection de date. En voici une liste :

1. month_changed            # mois changé

3. day_selected            # jour sélectionné

5. day_selected_double_click            # jour sélectionné double-clic

7. prev_month            # mois précédent

9. next_month            # mois suivant

11. prev_year            # année précédente

13. next_year            # année suivante

Il ne nous reste plus qu'à rassembler tout ceci dans le programme exemple calendrier.py. La Figure 9.12 montre le résultat du programme :

Figure 9.12. Exemple de calendrier

Voici le code source de calendrier.py :

1. #!/usr/bin/env python

3. # exemple calendrier.py

4. #

5. # Copyright (C) 1998 Cesar Miquel, Shawn T. Amundson, Mattias Gronlund

6. # Copyright (C) 2000 Tony Gale

7. # Copyright (C) 2001-2002 John Finlay

8. #

9. # This program is free software; you can redistribute it and/or modify

10. # it under the terms of the GNU General Public License as published by

11. # the Free Software Foundation; either version 2 of the License, or

12. # (at your option) any later version.

13. #

14. # This program is distributed in the hope that it will be useful,

15. # but WITHOUT ANY WARRANTY; without even the implied warranty of

16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

17. # GNU General Public License for more details.

18. #

19. # You should have received a copy of the GNU General Public License

20. # along with this program; if not, write to the Free Software

21. # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

23. import pygtk

24. pygtk.require('2.0')

25. import gtk, pango

26. import time

28. class ExempleCalendrier:

29. DEF_ESP = 10

30. DEF_PETIT_ESP = 5

31. ANNEE_BASE = 1900

33. calendrier_affich_entete = 0

34. calendrier_affich_jours = 1

35. calendrier_change_mois = 2

36. calendrier_affich_semaine = 3

38. def calendrier_date_chaine(self):

39. annee, mois, jour = self.fenetre.get_date()

40. date = time.mktime((annee, mois+1, jour, 0, 0, 0, 0, 0, -1))

41. return time.strftime("%x", time.localtime(date))

43. def calendrier_chaines_signaux(self, chaine_signal):

44. penult_sig = self.penult_sig.get()

45. self.apenult_sig.set_text(penult_sig)

47. penult_sig = self.dern_sig.get()

48. self.penult_sig.set_text(penult_sig)

49. self.dern_sig.set_text(chaine_signal)

51. def calendrier_change_mois(self, widget):

52. tampon = "month_changed : %s" % self.calendrier_date_chaine()

53. self.calendrier_chaines_signaux(tampon)

55. def calendrier_selec_jour(self, widget):

56. tampon = "day_selected : %s" % self.calendrier_date_chaine()

57. self.calendrier_chaines_signaux(tampon)

59. def calendrier_selec_jour_dbleclic(self, widget):

60. tampon = "day_selected_double_click : %s"

61. tampon = tampon % self.calendrier_date_chaine()

62. self.calendrier_chaines_signaux(tampon)

64. annee, mois, jour = self.fenetre.get_date()

66. if self.date_marquee[jour-1] == 0:

67. self.fenetre.mark_day(jour)

68. self.date_marquee[jour-1] = 1

69. else:

70. self.fenetre.unmark_day(jour)

71. self.date_marquee[jour-1] = 0

73. def calendrier_mois_prec(self, widget):

74. tampon = "prev_month : %s" % self.calendrier_date_chaine()

75. self.calendrier_chaines_signaux(tampon)

77. def calendrier_mois_suiv(self, widget):

78. tampon = "next_month : %s" % self.calendrier_date_chaine()

79. self.calendrier_chaines_signaux(tampon)

81. def calendrier_annee_prec(self, widget):

82. tampon = "prev_year : %s" % self.calendrier_date_chaine()

83. self.calendrier_chaines_signaux(tampon)

85. def calendrier_annee_suiv(self, widget):

86. tampon = "next_year : %s" % self.calendrier_date_chaine()

87. self.calendrier_chaines_signaux(tampon)

89. def calendrier_def_drapeaux(self):

90. options = 0

91. for i in range(5):

92. if self.reglages[i]:

93. options = options + (1<

IX-M - Le sélecteur de couleur

Le sélecteur de couleur est, on l'aura deviné, un widget permettant une sélection interactive des couleurs. Ce widget composite permet à l'utilisateur de choisir une couleur en manipulant des triplets RVB (Rouge, Vert, Bleu) ou TSV (Teinte, Saturation, Valeur). Chacune des valeurs peut être ajustée à l'aide de gradateurs ou de champs de saisie, ou bien en désignant directement la couleur désirée dans une roue de teinte-saturation/barre de valeur. Il est également possible de définir l'opacité de la couleur.

Le sélecteur de couleur ne dispose actuellement que d'un seul signal, "color_changed". Il est émis à chaque modification de la couleur courante dans le widget, qu'elle vienne de l'utilisateur ou bien d'une définition explicite par la méthode set_color().

Observons à présent ce que peut nous offrir ce widget. Il est proposé sous deux formes : gtk.ColorSelection et gtk.ColorSelectionDialog.

selectcouleur = gtk.ColorSelection()

Vous n'aurez probablement pas à utiliser ce constructeur directement. Il crée un gtk.ColorSelection orphelin auquel vous devrez affecter un parent vous-même. Le widget gtk.ColorSelection hérite du widget gtk.VBox.

dialselectcouleur = gtk.ColorSelectionDialog(title)

title (titre) est une chaine de caractères à utiliser pour la barre de titre de la boite de dialogue.

Il s'agit là du constructeur le plus courant pour les sélecteurs de couleur. Il crée un gtk.ColorSelectionDialog, qui consiste en un cadre (gtk.Frame) contenant un gtk.ColorSelection, un séparateur horizontal, ainsi qu'une boite horizontale possédant trois boutons : "OK", "Cancel" et "Help". Ces derniers sont accessibles par les attributs ok_button, cancel_button et help_button du ColorSelectionDialog (ex. : dialselectcouleur.ok_button). Pour accéder au sélecteur de couleur en lui-même, on utilisera l'attribut colorsel :

selectcouleur = dialselectcouleur.colorsel

Le widget gtk.ColorSelection dispose de plusieurs méthodes qui changent ses caractéristiques ou permettent d'accéder à la sélection de couleur.

selectcouleur.set_has_opacity_control(has_opacity)

Le sélecteur de couleur permet de régler l'opacité d'une couleur (aussi appelée canal alpha). Cette option est désactivée par défaut. Pour autoriser les réglages d'opacité, il faut appeler la méthode ci-dessus en donnant la valeur TRUE à son argument has_opacity. Inversement, l'appeler avec FALSE interdira cette possibilité.

selectcouleur.set_current_color(color)
selectcouleur.set_current_alpha(alpha)

On peut définir explicitement la couleur courante en appelant la méthode set_current_color() avec une GdkColor. Le réglage d'opacité (le canal alpha) s'effectue avec la méthode set_current_alpha(). La valeur alpha doit être comprise entre 0 (transparence totale) et 65 636 (opacité totale).

couleur = selectcouleur.get_current_color()
alpha = selectcouleur.get_current_alpha()

Pour récupérer la couleur courante, généralement lors de la réception d'un signal "color_changed", on utilise ces méthodes.

Le programme selectcouleur.py donne un exemple d'utilisation du gtk.ColorSelectionDialog. Le programme affiche une fenêtre contenant une zone de dessin qui, si elle est cliquée, ouvre une boite de dialogue de sélection de couleur. Les modifications de couleur effectuées dans cette boite de dialogue entrainent le changement de la couleur d'arrière-plan de la zone de dessin. La Figure 9.13 montre ce programme en exécution :

Figure 9.13. Exemple de sélecteur de couleur

Le code source de selectcouleur.py est le suivant :

1. #!/usr/bin/env python

3. # exemple selectcouleur.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleSelecteurCouleur:

10. # Gestionnaire du signal "color_changed"

11. def rappel_color_changed(self, widget):

12. # On récupère la table des couleurs de la zone de dessin

13. tablecouleurs = self.zonedessin.get_colormap()

15. # On récupère la couleur courante

16. couleur = self.dialselectcouleur.colorsel.get_current_color()

18. # On définit la couleur d'arrière-plan de la fenêtre

19. self.zonedessin.modify_bg(gtk.STATE_NORMAL, couleur)

21. # Gestionnaire d'événement de la zone de dessin

22. def evnmt_zone(self, widget, evnmt):

23. prisencharge = False

25. # On vérifie si on a reçu un événement "button-press"

26. if evnmt.type == gtk.gdk.BUTTON_PRESS:

27. prisencharge = True

29. # Création de la boite de dialogue du sélecteur de couleur

30. if self.dialselectcouleur == None:

31. self.dialselectcouleur = gtk.ColorSelectionDialog(

32. "Selection de la couleur d'arriere-plan")

34. # On récupère le sélecteur de couleur

35. selectcouleur = self.dialselectcouleur.colorsel

37. selectcouleur.set_previous_color(self.couleur)

38. selectcouleur.set_current_color(self.couleur)

39. selectcouleur.set_has_palette(True)

41. # Connexion au signal "color_changed"

42. selectcouleur.connect("color_changed", self.rappel_color_changed)

43. # Affichage de la boite de dialogue

44. reponse = self.dialselectcouleur.run()

46. if reponse -- gtk.RESPONSE_OK:

47. self.couleur = selectcouleur.get_current_color()

48. else:

49. self.zonedessin.modify_bg(gtk.STATE_NORMAL, self.couleur)

51. self.dialselectcouleur.hide()

53. return prisencharge

55. # Fermeture et sortie du gestionnaire

56. def destruct_fenetre(self, widget, evnmt):

57. gtk.main_quit()

58. return True

60. def __init__(self):

61. self.dialselectcouleur = None

62. # Création d'une fenêtre racine + titre + modes

63. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

64. fenetre.set_title("Test de sélection de couleur")

65. fenetre.set_resizable(True)

67. # Connexion aux événements "delete" et "destroy" pour pouvoir sortir

68. fenetre.connect("delete_event", self.destruct_fenetre)

70. # Création de la zone de dessin + taille + recup evnmts des boutons

71. self.zonedessin = gtk.DrawingArea()

73. self.couleur = self.zonedessin.get_colormap().alloc_color(0, 65535, 0)

75. self.zonedessin.set_size_request(200, 200)

76. self.zonedessin.set_events(gtk.gdk.BUTTON_PRESS_MASK)

77. self.zonedessin.connect("event", self.evnmt_zone)

79. # Ajout de la zone de dessin a la fenêtre, puis affichage des deux

80. fenetre.add(self.zonedessin)

81. self.zonedessin.show()

82. fenetre.show()

84. def main():

85. gtk.main()

86. return 0

88. if __name__ == "__main__":

89. ExempleSelecteurCouleur()

90. main()

IX-N - Le sélecteur de fichier

Le sélecteur de fichier est un moyen simple et rapide d'afficher une boite de dialogue de fichier déjà équipée des boutons Valider, Annuler et Aide. Une bonne économie en temps de programmation !

Pour créer une nouvelle boite de sélection de fichier, on utilise :

selectfichier = gtk.FileSelection(title=None)

Pour définir le nom du fichier, par exemple pour joindre directement un dossier spécifique ou donner un nom de fichier par défaut, on utilise la méthode :

selectfichier.set_filename(filename)

où filename est le nom du fichier. Pour récupérer, sous forme de texte, le nom du fichier que l'utilisateur a saisi ou cliqué, on fait appel à la méthode suivante :

nomfichier = selectfichier.get_filename()

Il existe aussi des références aux widgets contenus dans le sélecteur de fichier. Ce sont tous des attributs de selectfichier :

1. selectfichier.dir_list                 # (liste des dossiers)

2. selectfichier.file_list                # (liste des fichiers)

3. selectfichier.selection_entry          # (champ de saisie "Sélection")

4. selectfichier.selection_text           # (texte "Sélection")

5. selectfichier.main_vbox                # (boite verticale principale)

6. selectfichier.ok_button                # (bouton Valider)

7. selectfichier.cancel_button            # (bouton Annuler)

8. selectfichier.help_button              # (bouton Aide)

9. selectfichier.history_pulldown         # (menu historique déroulant)

10. selectfichier.history_menu             #

11. selectfichier.fileop_dialog            # (boutons d'opération sur fichier : boite de dialogue)

12. selectfichier.fileop_entry             # (boutons d'opération sur fichier : champ de saisie)

13. selectfichier.fileop_file              # (boutons d'opération sur fichier : fichier)

14. selectfichier.fileop_c_dir             # (boutons d'opération sur fichier : bouton "Créer un dossier")

15. selectfichier.fileop_del_file          # (boutons d'opération sur fichier : bouton "Supprimer le fichier")

16. selectfichier.fileop_ren_file          # (boutons d'opération sur fichier : bouton "Renommer le fichier")

17. selectfichier.button_area              # (zone des boutons d'opération sur fichier)

18. selectfichier.action_area              #

On utilisera le plus souvent les attributs ok_button, cancel_button et help_button afin de connecter les signaux de leur widget à des fonctions de rappel.

Le programme selectfichier.py donne un exemple d'utilisation du widget de sélection de fichier. Notez que, bien que le bouton Aide apparaisse à l'écran, il n'a aucun effet, car aucun signal ne lui est attaché. La Figure 9.14 montre l'affichage obtenu :

Figure 9.14. Exemple de sélecteur de fichier

Le code source de selectfichier.py est le suivant :

1. #!/usr/bin/env python

3. # exemple selectfichier.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleSelecteurFichier:

10. # On récupère le nom du fichier choisi et on l'affiche dans la console

11. def ok_fichier(self, w):

12. print "%s" % self.selectfichier.get_filename()

14. def destroy(self, widget):

15. gtk.main_quit()

17. def __init__(self):

18. # Création d'un nouveau widget de sélection de fichier

19. self.selectfichier = gtk.FileSelection("Sélection de fichier")

21. self.selectfichier.connect("destroy", self.destroy)

22. # Connexion du bouton OK à la méthode ok_fichier()

23. self.selectfichier.ok_button.connect("clicked", self.ok_fichier)

25. # Connexion du bouton Annuler à la destruction du widget

26. self.selectfichier.cancel_button.connect("clicked",

27. lambda w: self.selectfichier.destroy())

29. # Définissons le fichier, comme s'il s'agissait d'une boite de dialogue

30. # d'enregistrement de fichier et que nous donnions un fichier par défaut

31. self.selectfichier.set_filename("pingouin.png")

33. self.selectfichier.show()

35. def main():

36. gtk.main()

37. return 0

39. if __name__ == "__main__":

40. ExempleSelecteurFichier()

41. main()

IX-O - Le sélecteur de police

Le gtk.FontSelectionDialog permet à l'utilisateur de sélectionner interactivement une police pour le programme. Il s'agit d'une boite de dialogue contenant un widget gtk.FontSelection et des boutons "Valider" et "Annuler". Un bouton "Appliquer" est également disponible, mais il est caché par défaut. Le sélecteur de fichier permet de choisir parmi les polices système disponibles (les mêmes que celles que l'on obtient avec xlsfonts).

La Figure 9.15 montre la boite de dialogue du sélecteur de police :

Figure 9.15. Boite de dialogue du sélecteur de police

La boite de dialogue contient un ensemble de trois onglets qui proposent :

- une interface de sélection de la police, de son style et de sa taille ;

- des informations détaillées sur la police sélectionnée ;

- une interface pour le mécanisme de filtrage des polices, qui décide des polices pouvant être sélectionnées.

La fonction de création de cette boite de dialogue est la suivante :

dialselectpolice = gtk.FontSelectionDialog(title)

Le titre (title) est une chaine de caractères qui sera utilisée comme texte pour la barre de titre.

Une instance de gtk.FontSelectionDialog possède plusieurs attributs :

1. fontsel

2. main_vbox

3. action_area

4. ok_button

5. apply_button

6. cancel_button

L'attribut fontsel fait référence au widget de sélection de police, et main_vbox à la boite verticale contenant ce dernier ainsi que l'"action_area". action_area se réfère à la gtk.HButtonBox qui contient les boutons 'Valider', 'Appliquer' et 'Annuler'. Les attributs ok_button, cancel_button et apply_button se rapportent aux boutons 'Valider', 'Appliquer' et 'Annuler', et peuvent servir à établir des connexions aux signaux de ces derniers. apply_button peut servir à afficher le bouton 'Appliquer'.

Avec la méthode qui suit, on peut définir la police qui doit être initialement affichée dans le sélecteur de police :

dialselectpolice.set_font_name(fontname)

L'argument fontname (nom de police) attend le nom d'une police système intégralement ou partiellement spécifiée. Par exemple :

dialselectpolice.set_font_name('-adobe-courier-bold-*-*-*-*-120-*-*-*-*-*-*')

spécifie partiellement la police initiale.

Le nom de la police sélectionnée peut être récupéré avec cette méthode :

nom_police = dialselectpolice.get_font_name()

La boite de dialogue du sélecteur de police dispose d'une zone d'aperçu qui affiche du texte dans la police sélectionnée. Le texte utilisé dans cette zone d'aperçu peut être défini avec la méthode :

dialselectpolice.set_preview_text(text)

Le texte de l'aperçu peut être récupéré à l'aide de la méthode suivante :

texte = dialselectpolice.get_preview_text()

Le programme exemple calendrier.py utilise un gtk.FontSelectionDialog pour la sélection de la police du calendrier. Les lignes 106 à 111 définissent une fonction de rappel qui récupère le nom de la police à partir du sélecteur de police et l'utilise pour définir la police du calendrier. Les lignes 113-132 définissent la méthode qui crée le sélecteur de police, connecte les boutons 'Valider' et 'Annuler' à leur fonction de rappel, et affiche la boite de dialogue.

X - Chapitre 10. Les widgets conteneurs

X-A - La boite à événement (EventBox)

Certains widgets GTK ne possèdent pas de fenêtre X associée, ils se dessinent juste sur leur widget parent. Donc, ils ne peuvent recevoir d'événements et si leurs dimensions sont incorrectes, ils ne s'ajustent pas et vous pouvez obtenir des chevauchements d'écriture, etc. Si vous voulez obtenir plus de ces widgets, il faut utiliser la boite à événement EventBox.

À première vue, la boite à événement EventBox peut sembler totalement inutile. Elle ne dessine rien sur l'écran et ne répond à aucun événement. Cependant, elle réalise une fonction : elle fournit une fenêtre X pour son widget enfant. Et comme beaucoup de widgets ne possèdent pas de fenêtre X associée, cette fonction est importante. Ne pas avoir de fenêtre X économise de la mémoire et augmente les performances, mais a aussi quelques inconvénients. Un widget sans fenêtre X ne peut recevoir un événement, ne peut réaliser aucun ajustement sur son contenu et ne peut posséder de couleur d'arrière-plan. Quoique le nom EventBox met en évidence la fonction de gestion d'événement, ce widget peut aussi être redimensionné, (et plus encore, voir l'exemple ci-dessous).

Pour créer une nouvelle boite à événement EventBox, faire :

boite_evenement = gtk.EventBox()

Un widget enfant peut être ajouté à la boite à événement par :

event_box.add(widget)

Le programme d'exemple eventbox.py illustre les deux utilisations de la boite à événement. Créer un label, inscrit dans une petite boite, possédant un arrière-plan vert et permettant, lors d'un clic sur ce label, de sortir du programme. Redimensionner la fenêtre permet les variations de taille du label. La Figure 10.1, « Exemple de boite à événement » montre l'affichage du programme.

Figure 10.1. Exemple de boite à événement

Voici le code source du programme eventbox.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple eventbox.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBoiteEvenement:

10. def __init__(self):

11. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

12. fenetre.set_title("Boite à événement")

13. fenetre.connect("destroy", lambda w: gtk.main_quit())

14. fenetre.set_border_width(10)

16. # On crée une boite à événement et on l'ajoute à la fenêtre principale

17. boite_evenement = gtk.EventBox()

18. fenetre.add(boite_evenement)

19. boite_evenement.show()

21. # On crée un label long

22. label = gtk.Label("Cliquer ici pour quitter, finir, terminer, sortir...")

23. boite_evenement.add(label)

24. label.show()

26. # On fixe sa taille de départ (largeur, hauteur).

27. label.set_size_request(110, 30)

29. # On relie une action à la boite

30. boite_evenement.set_events(gtk.gdk.BUTTON_PRESS_MASK)

31. boite_evenement.connect("button_press_event", lambda w,e: gtk.main_quit())

33. # D'autres choses pour lesquelles il faut une fenêtre X...

34. boite_evenement.realize()

35. boite_evenement.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND1))

37. # Un arrière-plan en vert

38. boite_evenement.modify_bg(gtk.STATE_NORMAL,

39. boite_evenement.get_colormap().alloc_color("green"))

41. fenetre.show()

43. def main():

44. gtk.main()

45. return 0

47. if __name__ == "__main__":

48. ExempleBoiteEvenement()

49. main()

X-B - Le widget d'alignement (Alignment)

Le widget d'alignement Alignment permet de placer un widget dans sa fenêtre à une position et taille relatives à la taille du widget d'alignement lui-même. Ce qui peut être très utile pour, par exemple, centrer un widget dans sa fenêtre.

Il n'y a que deux appels(1) liés au widget d'alignement :

1. alignement = gtk.Alignment(xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0)

3. alignement.set(xalign, yalign, xscale, yscale)

La fonction gtk.Alignment() crée un nouveau widget d'alignement Alignment avec les paramètres indiqués. La méthode set() permet de modifier les paramètres d'un widget d'alignement préexistant.

Les quatre paramètres d'alignement sont des nombres décimaux, compris entre 0.0 et 1.0. Les paramètres xalign et yalign influent sur la position du widget placé dans le widget d'alignement (voir gtk.Alignment dans le manuel de référence). Les propriétés d'alignement indiquent le rapport d'espace libre existant au-dessus et à gauche du widget enfant. Leurs valeurs sont comprises entre 0.0 (pas d'espace libre au-dessus ni à gauche) et 1.0 (tout l'espace libre au-dessus et à gauche). Bien sûr, si les échelles sont toutes les deux fixées à 1.0, les propriétés d'alignement n'auront pas d'effet, car le widget enfant s'étendra pour remplir tout l'espace disponible.

Les paramètres xscale et yscale indiquent la fraction d'espace libre qui sera absorbé par le widget enfant. Leurs valeurs sont comprises entre 0.0 (le widget n'absorbe aucun espace) et 1.0 (le widget absorbe tout l'espace libre.

Un exemple(2) pour rendre les choses plus claires :

- un widget bouton (gtk.Button) de 32x32 pixels est placé dans un widget d'alignement de 256 pixels de large sur 128 pixels de haut. Les valeurs d'alignement sont, xalign=0.25, yalign=0.25, xscale=0.25, yscale=0.25 ;

- l'espace libre horizontal vaut 256-32=224 pixels, l'espace libre vertical vaut 128-32=96 pixels.

Le bouton absorbe 0.25x224=56 pixels de l'espace horizontal et 0.25x96=24 pixels de l'espace vertical. Sa taille devient donc 32+56=88 pixels de large pour 32+24=56 pixels de haut. Ce qui nous laisse 256-88=168 pixels d'espace libre horizontal et 128-56=72 pixels d'espace libre vertical. Puisque les valeurs de xalign et yalign sont de 0.25, l'espace libre horizontal est distribué entre 0.25x168=42 pixels à gauche du bouton et le reste à droite ; l'espace libre vertical est réparti entre 0.25x72=18 pixels au-dessus et le reste en dessous.

On peut ajouter un widget enfant à un widget d'alignement par :

alignement.add(widget)

Pour un exemple d'utilisation du widget d'alignement, voir le programme progressbar.py sur la barre de progression.

X-C - Le conteneur Place (Fixed)

Le conteneur Place (Fixed) permet de disposer des widgets dans sa fenêtre, à une position précise, par rapport au coin supérieur gauche. Cette position peut être modifiée de manière dynamique.

Il existe seulement trois appels associés au conteneur Fixed

1. conteneur_place = gtk.Fixed()

3. conteneur_place.put(widget, x, y)

5. conteneur_place.move(widget, x, y)

La fonction gtk.Fixed() crée un nouveau conteneur Fixed.

La méthode put() insère le widget dans le conteneur Fixed à la position indiquée par les paramètres x et y.

La méthode move() permet au widget indiqué d'être déplacé dans une nouvelle position.

Le programme fixed.py illustre l'utilisation du conteneur Fixed. La Figure 10.2, « Exemple de conteneur Fixed » montre le résultat.

Figure 10.2. Exemple de conteneur Fixed

Voici le code du programme fixed.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple fixed.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExemplePlace:

10. # Cette méthode de rappel déplace le bouton dans

11. # le conteneur placé vers une nouvelle position

12. def deplace_bouton(self, widget):

13. self.x = (self.x+30)%300

14. self.y = (self.y+50)%300

15. self.place.move(widget, self.x, self.y)

17. def __init__(self):

18. self.x = 50

19. self.y = 50

21. # On crée une nouvelle fenêtre

22. fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

23. fenetre.set_title("Conteneur Fixed")

25. # On connecte l'événement "destroy" au gestionnaire de signal

26. fenetre.connect("destroy", lambda w: gtk.main_quit())

28. # On indique la largeur des bordures de la fenêtre

29. fenetre.set_border_width(10)

31. # On crée le conteneur Fixed (place)

32. self.place = gtk.Fixed()

33. fenetre.add(self.place)

34. self.place.show()

36. for i in range(1, 4):

37. # On crée un bouton avec un label "Appuyez !"

38. bouton = gtk.Button("Appuyez !")

40. # Lorsque le bouton reçoit le signal "clicked",

41. # il appelle la méthode deplace_bouton().

42. bouton.connect("clicked", self.deplace_bouton)

44. # Ceci place le bouton dans la fenêtre du conteneur

45. self.place.put(bouton, i*50, i*50)

47. # La dernière tâche est d'afficher ce nouveau bouton.

48. bouton.show()

50. # On affiche la fenêtre

51. fenetre.show()

53. def main():

54. # Boucle principale

55. gtk.main()

56. return 0

58. if __name__ == "__main__":

59. ExemplePlace()

60. main()

X-D - Le conteneur Layout (Affiche)

Le conteneur Layout ressemble au conteneur Fixed à ceci près qu'il implémente une zone de défilement infinie (un infini limité à 2^32). Le système X window possède une limitation de taille de fenêtre de 32767x32767 pixels. Le conteneur Layout tourne cette limitation en réalisant des trucs exotiques utilisant la gravité de fenêtre et de bit. Il devient ainsi possible d'obtenir un défilement régulier même lorsque l'on a beaucoup de widgets enfants dans la zone de défilement.

On crée un conteneur Layout par :

affiche = gtk.Layout(hadjustment=None, vadjustment=None)

Comme on peut le constater, il est possible d'indiquer, de manière facultative, des objets Adjustment, (voir Chapitre 7, Adjustments) que le widget Layout va utiliser pour son défilement. Si l'on ne précise pas d'objets Adjustment, des nouveaux seront créés automatiquement.

On peut ajouter et déplacer des widgets dans un conteneur Layout avec les méthodes suivantes :

1. affiche.put(widget_enfant, x, y)

3. affiche.move(widget_enfant, x, y)

On peut indiquer ou connaître la taille du conteneur Layout en utilisant les méthodes :

1. affiche.set_size(largeur, hauteur)

3. taille = layout.get_size()

Ces quatre dernières méthodes du widget Layout permettent de manipuler les widgets d'ajustement horizontal et vertical :

1. horiz_ajust = affiche.get_hadjustment()

3. vert_ajust = affiche.get_vadjustment()

5. affiche.set_hadjustment(ajustement)

7. affiche.set_vadjustment(ajustement)

Le programme layout.py crée trois boutons et les place dans un conteneur Layout "affiche". Quand on clique sur l'un des boutons, il est transféré à un nouvel emplacement, aléatoire, dans le conteneur. La Figure 10.3, « Exemple de conteneur Layout » montre l'affichage de départ.

Figure 10.3. Exemple de conteneur Layout

Voici le code source du programme layout.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple layout.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

8. import random

10. class ExempleAffiche:

11. def WindowDeleteEvent(self, widget, event):

12. # retourne False pour que la fenêtre soit détruite

13. return False

15. def WindowDestroy(self, widget, *data):

16. # quitte la boucle principale

17. gtk.main_quit()

19. def ButtonClicked(self, bouton):

20. # déplace le bouton

21. self.affiche.move(bouton, random.randint(0,500),

22. random.randint(0,500))

24. def __init__(self):

25. # crée la fenêtre de niveau supérieur

26. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

27. fenetre.set_title("Exemple conteneur Layout")

28. fenetre.set_default_size(300, 300)

29. fenetre.connect("delete-event", self.WindowDeleteEvent)

30. fenetre.connect("destroy", self.WindowDestroy)

31. # crée une table et la place dans la fenêtre

32. table = gtk.Table(2, 2, False)

33. fenetre.add(table)

34. # crée le conteneur affiche et le place dans la table

35. self.affiche = gtk.Layout(None, None)

36. self.affiche.set_size(600, 600)

37. table.attach(self.affiche, 0, 1, 0, 1, gtk.FILL|gtk.EXPAND,

38. gtk.FILL|gtk.EXPAND, 0, 0)

39. # crée les barres de défilement et les place dans la table

40. vert_defil = gtk.VScrollbar(None)

41. table.attach(vert_defil, 1, 2, 0, 1, gtk.FILL|gtk.SHRINK,

42. gtk.FILL|gtk.SHRINK, 0, 0)

43. horiz_defil = gtk.HScrollbar(None)

44. table.attach(horiz_defil, 0, 1, 1, 2, gtk.FILL|gtk.SHRINK,

45. gtk.FILL|gtk.SHRINK, 0, 0)

46. # les barres de défilement utilisent les ajustements du conteneur

47. vAdjust = self.affiche.get_vadjustment()

48. vert_defil.set_adjustment(vAdjust)

49. hAdjust = self.affiche.get_hadjustment()

50. horiz_defil.set_adjustment(hAdjust)

51. # crée 3 boutons et les place dans le conteneur

52. bouton = gtk.Button("Clic !")

53. bouton.connect("clicked", self.ButtonClicked)

54. self.affiche.put(bouton, 0, 0)

55. bouton = gtk.Button("Clic !")

56. bouton.connect("clicked", self.ButtonClicked)

57. self.affiche.put(bouton, 100, 0)

58. bouton = gtk.Button("Clic !")

59. bouton.connect("clicked", self.ButtonClicked)

60. self.affiche.put(bouton, 200, 0)

61. # Montre tous les widgets

62. fenetre.show_all()

64. def main():

65. # on entre dans la boucle principale

66. gtk.main()

67. return 0

69. if __name__ == "__main__":

70. ExempleAffiche()

71. main()

X-E - Les cadres (Frame)

Les cadres sont utilisés pour englober un ou plusieurs groupes de widgets dans une boite qui peut, éventuellement, avoir une étiquette. La position de l'étiquette et le style de la boite sont modifiables.

(A Frame can be created with the following function:) Un cadre Frame se crée en utilisant :

cadre = gtk.Frame(label=None)

où l'étiquette label est placée, par défaut, dans le coin supérieur gauche du cadre. Donner la valeur "None" au paramètre label ou ne pas indiquer de paramètre label entrainera qu'aucune étiquette ne sera affichée. Le texte de l'étiquette est modifiable par :

cadre.set_label(label)

Pour modifier la position de l'étiquette, on utilise la méthode :

cadre.set_label_align(xalign, yalign)

où xalign et yalign ont une valeur comprise entre 0.0 et 1.0. L'argument xalign donne la position de l'étiquette sur le bord horizontal du cadre. Le paramètre yalign n'est pas utilisé actuellement. La valeur par défaut de xalign est 0.0, ce qui place l'étiquette sur le côté gauche du cadre.

La méthode suivante modifie le style de la boite qui délimite le cadre.

cadre.set_shadow_type(type)

où le paramètre type peur prendre l'une des valeurs suivantes :

1. SHADOW_NONE

2. SHADOW_IN

3. SHADOW_OUT

4. SHADOW_ETCHED_IN      # par défaut

5. SHADOW_ETCHED_OUT

Le programme frame.py montre l'utilisation du widget de cadre Frame. La Figure 10.4, « Exemple de cadre » illustre le résultat obtenu :

Figure 10.4. Exemple de cadre

Voici le code du programmeframe.py

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple frame.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleCadre:

10. def __init__(self):

11. # Créer une nouvelle fenêtre

12. fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

13. fenetre.set_title("Exemple de cadre")

15. # On connecte l'événement "destroy" au gestionnaire de signal

16. fenetre.connect("destroy", lambda w: gtk.main_quit())

17. fenetre.set_size_request(300, 300)

19. # On définit la largeur de bordure de la fenêtre

20. fenetre.set_border_width(10)

22. # On crée un cadre

23. cadre = gtk.Frame()

24. fenetre.add(cadre)

26. # On indique l'étiquette du cadre

27. cadre.set_label("Widget GTK Cadre")

29. # On aligne l'étiquette sur la droite du cadre

30. cadre.set_label_align(1.0, 0.0)

32. # On précise le style du cadre

33. cadre.set_shadow_type(gtk.SHADOW_ETCHED_OUT)

34. cadre.show()

36. # On affiche la fenêtre

37. fenetre.show()

39. def main():

40. # Enter the event loop

41. gtk.main()

42. return 0

44. if __name__ == "__main__":

45. ExempleCadre()

46. main()

Les programmes calendar.py, label.py et spinbutton.py utilisent aussi des cadres.

X-F - Les cadres proportionnels (AspectFrame)

Le cadre proportionnel est semblable au widget cadre, en outre il maintient le rapport largeur/hauteur du widget enfant à sa valeur en ajoutant de l'espace supplémentaire si nécessaire. Ceci est utile si, par exemple, vous voulez présenter une image plus grande. La taille de la prévisualisation peut varier lorsque l'utilisateur redimensionne la fenêtre, mais les proportions de l'image doivent toujours correspondre à l'image originale.

Pour créer un cadre proportionnel, on utilise :

cadre_prop = gtk.AspectFrame(label=None, xalign=0.5, yalign=0.5, ratio=1.0, obey_child=TRUE)

où l'étiquette label indique le texte qui sera affiché, les paramètres xalign et yalign indiquent l'alignement comme décrit dans gtk.Alignment. Si le paramètre obey_child vaut TRUE, les proportions du widget enfant correspondront à la taille idéale qu'il demande, autrement elles seront données par la valeur de ratio.

Pour modifier les arguments d'un cadre proportionnel existant, on utilise :

cadre_prop.set(xalign=0.0, yalign=0.0, ratio=1.0, obey_child=TRUE)

En exemple, le programme aspectframe.py utilise un cadre proportionnel AspectFrame pour offrir une zone de dessin dont le rapport de proportions sera toujours de 2/1, quelle que soit la manière dont l'utilisateur redimensionne la fenêtre. La Figure 10.5, « Exemple de cadre proportionnel » montre l'affichage du programme.

Figure 10.5. Exemple de cadre proportionnel

Voici le code du programme aspectframe.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple aspectframe.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleAspectFrame:

10. def __init__(self):

11. fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL);

12. fenetre.set_title("Cadre_proportionnel")

13. fenetre.connect("destroy", lambda x: gtk.main_quit())

14. fenetre.set_border_width(10)

16. # Créer un cadre proportionnel - l'ajouter à la fenêtre principale

17. cadre_prop = gtk.AspectFrame("2x1", # étiquette

18. 0.5, # x centré dans la fenêtre à

19. 0.5, # y centré dans la fenêtre à

20. 2, # rapport x/y = 2

21. False) # ignorer taille des widgets enfant

22. fenetre.add(cadre_prop)

23. cadre_prop.show()

25. # Ajouter un widget enfant dans le cadre proportionnel

26. zone_dessin = gtk.DrawingArea()

28. # Déclarer une fenêtre de 200x200, mais le cadre proportionnel

29. # avec un rapport de 2x1 affiche une zone de 200x100

30. zone_dessin.set_size_request(200, 200)

31. cadre_prop.add(zone_dessin)

32. zone_dessin.show()

33. fenetre.show()

35. def main():

36. gtk.main()

37. return 0

39. if __name__ == "__main__":

40. ExempleAspectFrame()

41. main()

X-G - Fenêtre à volets (Paned Window)

Le widget de fenêtre à volets permet de diviser une zone en deux parties, la taille relative de chaque partie est gérée par l'utilisateur. Un sillon, dessiné entre les deux parties, possède une poignée qui permet à l'utilisateur de modifier le partage. La division peut être horizontale (HPaned) ou verticale (VPaned).

Pour créer une nouvelle fenêtre à volets, on appelle :

1. volet_horiz = gtk.HPaned()

3. volet_vert = gtk.VPaned()

Après avoir créé la fenêtre à volets, il faut ajouter un widget enfant dans chaque moitié. Pour cela, on utilise les méthodes :

1. volet.add1(enfant)

3. volet.add2(enfant)

La méthode add1() ajoute le widget enfant au volet gauche ou supérieur, la méthode add2() ajoute le widget enfant au volet droit ou inférieur de la fenêtre à volets.

Le programme paned.py crée une partie de l'interface utilisateur d'un logiciel de courrier électronique imaginaire. Une fenêtre est divisée en deux parties verticales, la partie supérieure comprend une liste de messages et la partie inférieure le texte du message. La plus grande part du programme est plutôt simple. Deux points sont cependant à noter : on ne peut ajouter de texte à un widget Texte tant qu'il n'est pas réalisé. Ce qui peut être fait en appelant la méthode realize(), mais comme démonstration d'une technique alternative, on connecte un gestionnaire au signal "realize" pour ajouter le texte. Il faudrait aussi ajouter l'option SHRINK à certains des items dans la partie contenant la fenêtre de texte et ses ascenseurs. Ainsi, lorsque la partie basse est réduite, les sections corrigées diminuent au lieu de disparaître par le bas de la fenêtre. La Figure 10.6, « Exemple de fenêtre à volets » illustre le programme en cours de fonctionnement.

Figure 10.6. Exemple de fenêtre à volets

Voici le code du programme paned.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # example paned.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk, gobject

9. class ExempleVolets:

10. # Crée la liste des "messages"

11. def create_list(self):

12. # Crée une nouvelle fenêtre à défilement avec ascenseurs si nécessaire

13. fenetre_defil = gtk.ScrolledWindow()

14. fenetre_defil.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

16. modele = gtk.ListStore(gobject.TYPE_STRING)

17. tree_view = gtk.TreeView(modele)

18. fenetre_defil.add_with_viewport (tree_view)

19. tree_view.show()

21. # Ajoute quelques messages dans la fenêtre

22. for i in range(10):

23. msg = "Message #%d" % i

24. iter = modele.append()

25. modele.set(iter, 0, msg)

27. cellule = gtk.CellRendererText()

28. colonne = gtk.TreeViewColumn("Messages", cellule, text=0)

29. tree_view.append_column(colonne)

31. return fenetre_defil

33. # Ajoute du texte au widget texte - ceci est un rappel qui est invoqué

34. # quand la fenêtre est réalisée. On pourrait forcer aussi la fenêtre à être

35. # réalisée avec gtk.Widget.realize(), mais elle devrait appartenir

36. # d'abord à une hiérarchie.

37. def insert_text(self, buffer):

38. iter = buffer.get_iter_at_offset(0)

39. buffer.insert(iter,

40. "From: pathfinder@nasa.gov\n"

41. "To: mom@nasa.gov\n"

42. "Subject: Faites-le !\n"

43. "\n"

44. "Nous sommes arrivés juste ce matin. \n"

45. "Le temps a été superbe, clair, mais froid\n"

46. "et il y a plein de vues amusantes.\n"

47. "Sojourner vous dit bonjour. À bientôt.\n"

48. " -Path\n")

50. # Crée une zone de texte avec ascenseurs qui affiche un "message"

51. def create_text(self):

52. vuetexte = gtk.TextView()

53. buffer = vuetexte.get_buffer()

54. fenetre_defil = gtk.ScrolledWindow()

55. fenetre_defil.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

56. fenetre_defil.add(vuetexte)

57. self.insert_text(buffer)

58. fenetre_defil.show_all()

59. return fenetre_defil

61. def __init__(self):

62. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

63. fenetre.set_title("Fenêtre à volets")

64. fenetre.connect("destroy", lambda w: gtk.main_quit())

65. fenetre.set_border_width(10)

66. fenetre.set_size_request(450, 400)

68. # Crée une fenêtre à volets verticale et l'ajoute à la fenêtre principale

69. vpaned = gtk.VPaned()

70. fenetre.add(vpaned)

71. vpaned.show()

73. # Crée le contenu des deux moitiés de la fenêtre

74. liste = self.create_list()

75. vpaned.add1(liste)

76. liste.show()

78. texte = self.create_text()

79. vpaned.add2(texte)

80. texte.show()

81. fenetre.show()

83. def main():

84. gtk.main()

85. return 0

87. if __name__ == "__main__":

88. ExempleVolets()

89. main()

X-H - Viewports

Il y a peu de chances que vous ayez besoin d'utiliser directement le widget Viewport. Plus probablement, vous utiliserez le widget ScrolledWindow (voir Section 10.9, « Fenêtre avec barres de défilement (Scrolled Window) ») qui lui, utilise Viewport.

Un widget Viewport permet de placer un widget plus grand à l'intérieur de lui-même de sorte qu'il n'est possible de voir qu'une partie de celui-ci à la fois. Il utilise l'objet Adjustment (voir Chapitre 7, Adjustments) pour définir la partie actuellement visible.

On crée un Viewport par la fonction :

viewport = gtk.Viewport(hadjustment=None, vadjustment=None)

Comme on peut le constater, on peut préciser lors de la création du widget les objets Adjustment verticaux et horizontaux qu'il devra utiliser. Le widget créera ses propres ajustements si on attribue la valeur None aux arguments ou si on n'utilise pas d'arguments.

Il est possible de retrouver et de définir les ajustements après la création du widget grâce aux quatre méthodes suivantes :

1. viewport.get_hadjustment()

3. viewport.get_vadjustment()

5. viewport.set_hadjustment(adjustment)

7. viewport.set_vadjustment(adjustment)

La seule autre méthode de viewport sert à modifier son apparence :

viewport.set_shadow_type(type)

Voici les valeurs possibles pour le paramètre type :

1. SHADOW_NONE           # pas d'ombrage

2. SHADOW_IN             # ombrage intérieur

3. SHADOW_OUT            # ombrage extérieur

4. SHADOW_ETCHED_IN      # ombrage gravé en creux

5. SHADOW_ETCHED_OUT     # ombrage gravé en relief

X-I - Fenêtre avec barres de défilement (Scrolled Window)

Les fenêtres avec barres de défilement servent à créer une zone défilante contenant un autre widget. On peut insérer n'importe quel widget dans ces fenêtres, ils seront accessibles, quelle que soit leur taille en utilisant les barres de défilement.

La fonction suivante sert à créer une fenêtre avec barre de défilement :

scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)

où le premier paramètre est l'ajustement horizontal, et le second l'ajustement vertical. Ils valent presque toujours None ou ne sont pas indiqués.

scrolled_window.set_policy(hscrollbar_policy, vscrollbar_policy)

Cette méthode définit la politique à utiliser par rapport aux barres de défilement. Le premier argument règle la politique pour la barre de défilement horizontale, et le second, la politique pour la barre de défilement verticale.

Cette politique peut être POLICY_AUTOMATIC ou POLICY_ALWAYS. POLICY_AUTOMATIC décidera automatiquement de votre besoin en barres de défilement, alors que POLICY_ALWAYS affichera toujours celles-ci.

On peut ensuite placer un objet dans la fenêtre à défilement grâce à la méthode :

scrolled_window.add_with_viewport(child)

Le programme scrolledwin.py place 100 boutons commutateurs dans une fenêtre à défilement. Je n'ai commenté que les parties qui pouvaient vous paraître nouvelles. La Figure 10.7, « Exemple de fenêtre à défilement »« Exemple de fenêtre à défilement »« Exemple de fenêtre à défilement » montre l'affichage du programme :

Figure 10.7. Exemple de fenêtre à défilement

Voici le code source du programme scrolledwin.py

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple scrolledwin.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleScrolledWindow:

10. def destroy(self, widget):

11. gtk.main_quit()

13. def __init__(self):

14. # Créer une boite de dialogue pour y placer

15. # la fenêtre à défilement

16. fenêtre = gtk.Dialog()

17. fenetre.connect("destroy", self.destroy)

18. fenetre.set_title("Fenêtre à défilement")

19. fenetre.set_border_width(0)

20. fenetre.set_size_request(300, 300)

22. # Créer une fenêtre à défilement.

23. fenetre_defil = gtk.ScrolledWindow()

24. fenetre_defil.set_border_width(10)

26. # La gestion des barres est soit POLICY AUTOMATIC, soit POLICY_ALWAYS.

27. # POLICY_AUTOMATIC décide automatiquement s'il faut ou non des barres,

28. # POLICY_ALWAYS met toujours des barres.

29. # Le premier paramètre correspond à la barre horizontale,

30. # le second à la barre verticale.

31. fenetre_defil.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)

33. # Créer une boite de dialogue avec une boite verticale.

34. fenetre.vbox.pack_start(fenetre_defil, True, True, 0)

35. fenetre_defil.show()

37. # Créer une table de 10x10 cases.

38. table = gtk.Table(10, 10, False)

40. # Définir l'espacement des lignes et colonnes à 10 pixels.

41. table.set_row_spacings(10)

42. table.set_col_spacings(10)

44. # Placer la table dans la fenêtre à défilement.

45. fenetre_defil.add_with_viewport(table)

46. table.show()

48. # Créer une grille de boutons commutateurs dans la table

49. # pour la démonstration de la fenêtre à défilement

50. for i in range(10):

51. for j in range(10):

52. buffer = "bouton (%d,%d)" % (i, j)

53. bouton = gtk.ToggleButton(buffer)

54. table.attach(bouton, i, i+1, j, j+1)

55. bouton.show()

57. # Ajouter un bouton « Fermer » en bas de la boite de dialogue.

58. bouton = gtk.Button("Fermer")

59. bouton.connect_object("clicked", self.destroy, fenêtre)

61. # Définir ce bouton en « bouton par défaut ».

62. bouton.set_flags(gtk.CAN_DEFAULT)

63. fenetre.action_area.pack_start(bouton, True, True, 0)

65. # Récupérer le bouton par défaut. Presser

66. # la touche « Entrée » activera le bouton.

67. bouton.grab_default()

68. bouton.show()

69. fenetre.show()

71. def main():

72. gtk.main()

73. return 0

75. if __name__ == "__main__":

76. ExempleScrolledWindow()

77. main()

Jouez à modifier la taille de la fenêtre et observez les réactions des barres de défilement. On peut aussi utiliser la fonction set_size_request() pour définir la taille par défaut de la fenêtre et des autres widgets.

X-J - Boites à boutons (ButtonBox)

La boite à boutons ButtonBox est un moyen simple de réaliser rapidement un groupe de boutons. Elle peut être horizontale ou verticale. On crée une nouvelle boite à boutons par un des appels suivants, respectivement pour une boite horizontale ou verticale :

1. hboite_bout = gtk.HButtonBox()

3. vboite_bout = gtk.VButtonBox()

Les seules méthodes s'appliquant aux boites à boutons concernent leur disposition à l'intérieur de la boite.

La disposition des boutons dans leur boite se fait par :

boite_bout.set_layout(layout_style)

Le paramètre layout_style peut prendre une des valeurs suivantes :

1. BUTTONBOX_DEFAULT_STYLE   # par défaut

2. BUTTONBOX_SPREAD          # espacé

3. BUTTONBOX_EDGE            # collé aux bords

4. BUTTONBOX_START           # collé au début

5. BUTTONBOX_END             # collé à la fin

Le style d'affichage layout_style peut être retrouvé en utilisant :

layout_style = boite_bout.get_layout()

Les boutons sont ajoutés dans la ButtonBox en utilisant la méthode habituelle des Container :

boite_bout.add(widget)

Le programme exemple buttonbox.py utilise tous les différents modes de placement des ButtonBoxes. En voici le résultat :

Exemple de boite à boutons

Voici le code source du programme buttonbox.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # example boutonbox.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBoiteBouton:

10. # Créer une boite à boutons avec les paramètres indiqués

11. def create_boite_bout(self, horizontal, title, spacing, layout):

12. cadre = gtk.Frame(title)

14. if horizontal:

15. boite_bout = gtk.HButtonBox()

16. else:

17. boite_bout = gtk.VButtonBox()

19. boite_bout.set_border_width(5)

20. cadre.add(boite_bout)

22. # Définir l'apparence de la boite à boutons

23. boite_bout.set_layout(layout)

24. boite_bout.set_spacing(spacing)

26. bouton = gtk.Button(stock=gtk.STOCK_OK)

27. boite_bout.add(bouton)

29. bouton = gtk.Button(stock=gtk.STOCK_CANCEL)

30. boite_bout.add(bouton)

32. bouton = gtk.Button(stock=gtk.STOCK_HELP)

33. boite_bout.add(bouton)

35. return cadre

37. def __init__(self):

38. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

39. fenetre.set_title("Boites à boutons")

41. fenetre.connect("destroy", lambda x: gtk.main_quit())

43. fenetre.set_border_width(10)

45. boîtev_princ = gtk.VBox(False, 0)

46. fenetre.add(boîtev_princ)

48. cadre_horz = gtk.Frame("Boites à boutons horizontales")

49. boîtev_princ.pack_start(cadre_horz, True, True, 10)

51. boitev = gtk.VBox(False, 0)

52. boîtev.set_border_width(10)

53. cadre_horz.add(boitev)

55. boîtev.pack_start(self.create_boite_bout(True, "Spread (spacing 40)",

56. 40, gtk.BUTTONBOX_SPREAD),

57. True, True, 0)

59. boîtev.pack_start(self.create_boite_bout(True, "Edge (spacing 30)",

60. 30, gtk.BUTTONBOX_EDGE),

61. True, True, 5)

63. boîtev.pack_start(self.create_boite_bout(True, "Start (spacing 20)",

64. 20, gtk.BUTTONBOX_START),

65. True, True, 5)

67. boîtev.pack_start(self.create_boite_bout(True, "End (spacing 10)",

68. 10, gtk.BUTTONBOX_END),

69. True, True, 5)

71. cadre_vert = gtk.Frame("Boites à boutons verticales")

72. boîtev_princ.pack_start(cadre_vert, True, True, 10)

74. boiteh = gtk.HBox(False, 0)

75. boîteh.set_border_width(10)

76. cadre_vert.add(boiteh)

78. boîteh.pack_start(self.create_boite_bout(False, "Spread (spacing 5)",

79. 5, gtk.BUTTONBOX_SPREAD),

80. True, True, 0)

82. boîteh.pack_start(self.create_boite_bout(False, "Edge (spacing 30)",

83. 30, gtk.BUTTONBOX_EDGE),

84. True, True, 5)

86. boîteh.pack_start(self.create_boite_bout(False, "Start (spacing 20)",

87. 20, gtk.BUTTONBOX_START),

88. True, True, 5)

90. boîteh.pack_start(self.create_boite_bout(False, "End (spacing 20)",

91. 20, gtk.BUTTONBOX_END),

92. True, True, 5)

94. fenetre.show_all()

96. def main():

97. # Entrer dans la boucle principale

98. gtk.main()

99. return 0

101. if __name__ == "__main__":

102. ExempleBoiteBouton()

103. main()

X-K - La barre d'outils (Toolbar)

Les barres d'outils Toolbars sont habituellement utilisées pour grouper des widgets dans le but de faciliter la personnalisation de leur style et de leur disposition. Typiquement une barre d'outils se compose de boutons avec des icônes, des étiquettes et des infobulles, mais n'importe quel autre widget peut aussi être placé dans une barre d'outils. Enfin, les éléments peuvent être disposés horizontalement ou verticalement et les boutons peuvent être affichés avec des icônes, des étiquettes, ou les deux.

Créer une barre d'outils se fait (comme on pourrait s'en douter) de la façon suivante :

barre_outil = gtk.Toolbar()

Une fois la barre d'outils créée, on peut y ajouter en début ou en fin, ou y insérer au milieu, des items (du texte simple) ou des éléments (de type widget). Pour décrire un item, il nous faut un texte pour l'étiquette, l'infobulle, un texte privé pour l'infobulle, une icône pour le bouton et une fonction de rappel pour celui-ci. Par exemple, pour ajouter un item en début ou en fin, on peut utiliser la méthode suivante :

1. barre_outil.append_item(text, tooltip_text, tooltip_private_text, icon, callback, user_data=None)

3. barre_outil.prepend_item(text, tooltip_text, tooltip_private_text, icon, callback, user_data)

Si l'on souhaite utiliser la méthode insert_item(), le seul paramètre supplémentaire à indiquer est la position à laquelle il faut insérer l'item, ainsi :

barre_outil.insert_item(text, tooltip_text, tooltip_private_text, icon, callback, user_data,
position)

Pour simplifier l'ajout d'espaces entre les items de la barre d'outils, on peut utiliser les méthodes suivantes :

1. barre_outil.append_space()

3. barre_outil.prepend_space()

5. barre_outil.insert_space(position)

Si nécessaire, l'orientation de la barre d'outils, son style et la disponibilité des infobulles peuvent être modifiés "à la volée" en utilisant les méthodes suivantes :

1. barre_outil.set_orientation(orientation)

3. barre_outil.set_style(style)

5. barre_outil.set_tooltips(enable)

où le paramètre orientation peut avoir pour valeur ORIENTATION_HORIZONTAL ou ORIENTATION_VERTICAL. style sert à définir l'apparence de la barre d'outils grâce aux valeurs TOOLBAR_ICONS, TOOLBAR_TEXT ou TOOLBAR_BOTH. L'argument enable est TRUE soit FALSE.

Pour montrer certaines autres possibilités de la barre d'outils, prenons le programme exemple toolbar.py (nous interromprons le listing avec quelques explications supplémentaires) :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # example barre_outils.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBarreOutils:

10. # Cette méthode est connectée au bouton Fermer ou à la fermeture

11. # de la fenêtre depuis le gestionnaire de fenêtre

12. def delete_event(self, widget, event=None):

13. gtk.main_quit()

14. return False

Le début ci-dessus devrait vous sembler familier si ce n'est pas votre premier programme PyGTK. Il y a cependant un élément supplémentaire, nous importons une jolie image XPM (gtk.xpm) pour l'utiliser comme icône pour tous les boutons. La ligne 10 débute la classe ExempleBarreOutils et aux lignes 12-14, on définit la méthode de rappel qui terminera le programme.

1. # Ceci est simple... quand l'un de ces boutons est commuté,

2. # on cherche lequel est actif et on règle le style de la

3. # barre d'outils en conséquence.

4. def change_radio(self, widget, barre_outils):

5. if self.bouton_texte.get_active():

6. barre_outils.set_style(gtk.TOOLBAR_TEXT)

7. elif self.bouton_icone.get_active():

8. barre_outils.set_style(gtk.TOOLBAR_ICONS)

9. elif self.bouton_deux.get_active():

10. barre_outils.set_style(gtk.TOOLBAR_BOTH)

12. # Encore plus simple, on vérifie le bouton à bascule et on

13. # autorise/interdit les infobulles

14. def change_bascule(self, widget, barre_outils):

15. barre_outils.set_tooltips(widget.get_active())

Aux lignes 19-30, deux méthodes de rappel qui seront appelées quand on cliquera sur un des boutons de la barre d'outils. Ce genre de choses devrait déjà vous être familier si vous avez déjà utilisé des boutons à bascule (et des boutons radio).

1. def __init__(self):

2. # Ici, la fenêtre principale (un dialogue) et la boite à poignée.

3. # Bon, on a besoin d'une barre d'outils, d'une icône avec un masque

4. # (un pour tous les boutons) et un widget icône dans lequel la placer

5. # (on crée un widget pour chaque bouton)

6. # On crée une nouvelle fenêtre avec son titre, d'une taille adaptée

7. dialogue = gtk.Dialog()

8. dialogue.set_title("Exemple GTKToolbar")

9. dialogue.set_size_request(450, 250)

10. dialogue.set_resizable(True)

12. # En général, on quitte quand quelqu'un essaie de nous fermer

13. dialogue.connect("delete_event", self.delete_event)

15. # Pour faire les choses bien, nous plaçons la barre d'outils dans une

16. # boite à poignées, ainsi elle peut être détachée de la fenêtre principale.

17. boite_poing = gtk.HandleBox()

18. dialogue.vbox.pack_start(boite_poing, False, False, 5)

La partie ci-dessus devrait être similaire à toute autre application PyGTK. Simplement une initialisation d'une instance de ExempleBarreOutils, création de la fenêtre, etc. Un élément, peut-être, demande une explication : la boite à poignées. Une boite à poignées est simplement une boite qui peut être utilisée pour y placer des widgets. La différence avec une boite standard est que la précédente peut être détachée d'une fenêtre parent (en réalité, la boite demeure à l'intérieur de son parent, mais est réduite à un tout petit rectangle, pendant que tous ses éléments peuvent être raccordés à une nouvelle fenêtre flottante libre). Habituellement il est pratique d'avoir une barre d'outils détachable, donc ces deux widgets vont souvent ensemble.

1. # La barre d'outils sera horizontale, avec à la fois des icônes

2. # et du texte et un espace de 5 pixels entre les items. Enfin,

3. # nous la mettons dans notre boite à poignées.

4. barre_outils = gtk.Toolbar()

5. barre_outils.set_orientation(gtk.ORIENTATION_HORIZONTAL)

6. barre_outils.set_style(gtk.TOOLBAR_BOTH)

7. barre_outils.set_border_width(5)

8. boite_poing.add(barre_outils)

Bon, ce que nous faisons ici est une simple initialisation de la barre d'outils.

1. # le premier item est le bouton "Fermer"

2. img_icone = gtk.Image() # widget icône

3. img_icone.set_from_file("gtk.xpm")

4. bouton_fermer = barre_outils.append_item(

5. "Fermer",               # étiquette du bouton

6. "Fermer l'application", # infobulle du bouton

7. "Privé",                # info privée du bouton

8. img_icone,              # widget icône

9. self.delete_event)      # un signal

10. barre_outils.append_space() # espace après l'item

Dans le code ci-dessus, nous voyons le cas le plus simple : l'ajout d'un bouton à la barre d'outils. Juste avant d'ajouter un nouvel élément nous devons créer un widget image pour servir d'icône pour cet élément ; il faudra répéter cette étape pour chaque nouvel élément. Nous ajoutons aussi un espace juste après l'élément, de façon à ce que les éléments ne soient pas collés les uns aux autres. Comme vous pouvez le voir, la méthode append_item() renvoie une référence au nouveau bouton créé, ainsi on peut l'utiliser de la manière habituelle

1. # maintenant, on crée le groupe de boutons radio

2. img_icone = gtk.Image() # widget icône

3. img_icone.set_from_file("gtk.xpm")

4. bouton_icone = barre_outils.append_element(

5. gtk.TOOLBAR_CHILD_RADIOBUTTON, # type d'élément

6. None,                          # widget

7. "Icônes",                      # étiquette

8. "Barre d'outils avec icônes uniquement",       # infobulle

9. "Privé",                       # info privée du bouton

10. img_icone,                     # icône

11. self.change_radio,             # signal

12. barre_outils)                  # donnée pour le signal

13. barre_outils.append_space()

14. self.bouton_icone = bouton_icone

Ici nous commençons à créer un groupe de boutons radio. Pour ce faire nous utilisons la méthode append_element(). En fait, en utilisant cette méthode on peut aussi ajouter des éléments simples et même des espaces (type = gtk.TOOLBAR_CHILD_SPACE ou gtk.TOOLBAR_CHILD_BUTTON). Dans l'exemple précédent, nous commençons à créer un groupe de boutons radio. Pour créer d'autres boutons radio, une référence au bouton précédent dans ce groupe est exigée, de sorte que l'on puisse construire facilement une liste de boutons (voir la Section 6.4, « Radio Buttons »). Nous sauvons aussi une référence au bouton dans l'instance de classe ExempleBarreOutils pour y accéder ensuite.

1. # les boutons radio suivants renvoient aux précédents

2. img_icone = gtk.Image() # widget icône

3. img_icone.set_from_file("gtk.xpm")

4. bouton_texte = barre_outils.append_element(

5. gtk.TOOLBAR_CHILD_RADIOBUTTON,

6. bouton_icone,

7. "Texte",

8. "Barre d'outils avec texte uniquement",

9. "Privé",

10. img_icone,

11. self.change_radio,

12. barre_outils)

13. barre_outils.append_space()

14. self.bouton_texte = bouton_texte

16. img_icone = gtk.Image() # widget icône

17. img_icone.set_from_file("gtk.xpm")

18. bouton_deux = barre_outils.append_element(

19. gtk.TOOLBAR_CHILD_RADIOBUTTON,

20. bouton_texte,

21. "Les deux",

22. "Barre d'outils avec icônes et texte",

23. "Privé",

24. img_icone,

25. self.change_radio,

26. barre_outils)

27. barre_outils.append_space()

28. self.bouton_deux = bouton_deux

29. bouton_deux.set_active(True)

Nous créons les autres boutons radio de la même manière en rajoutant le nom de l'un des boutons créés à la méthode append_element() pour indiquer à quel groupe de boutons appartient ce dernier.

Enfin, il faut définir manuellement l'état de l'un des boutons radio (sinon ils seraient tous dans l'état actif, nous interdisant de passer de l'un à l'autre).

1. # Ici un simple bouton à bascule

2. img_icone = gtk.Image() # widget icône

3. img_icone.set_from_file("gtk.xpm")

4. bouton_info = barre_outils.append_element(

5. gtk.TOOLBAR_CHILD_TOGGLEBUTTON,

6. None,

7. "Infobulles",

8. "Barre d'outils avec/sans infobulles",

9. "Privé",

10. img_icone,

11. self.change_bascule,

12. barre_outils)

13. barre_outils.append_space()

14. bouton_info.set_active(True)

Un bouton à bascule peut être créé de la manière habituelle (si l'on sait déjà créer les boutons radio).

1. # pour placer un élément dans la barre d'outils, il faut

2. # seulement le créer et l'ajouter avec une infobulle adéquate

3. zone_saisie = gtk.Entry()

4. barre_outils.append_widget(zone_saisie,  "Juste une zone de saisie", "Privé")

6. # il n'est pas créé dans la barre d'outils, il faut l'afficher

7. zone_saisie.show()

On constate qu'ajouter n'importe quel type de widget dans une barre d'outils est simple. La chose à se rappeler est que ce widget doit être affiché manuellement (contrairement aux éléments qui seront affichés ensemble avec la barre d'outils)

1. # C'est fait ! Affichons tout.

2. barre_outils.show()

3. boite_poing.show()

4. dialogue.show()

6. def main():

7. # On reste dans gtk_main et on attend que la fête commence !

8. gtk.main()

9. return 0

11. if __name__ == "__main__":

12. ExempleBarreOutils()

13. main()

À la ligne 142, on clôt la définition de la classe ExempleBarreOutils. Aux lignes 144-147, on définit la fonction main() qui appelle simplement la fonction gtk.main() pour lancer la boucle de traitement d'événement. Aux lignes 149-151, on crée l'instance de ExempleBarreOutils et ensuite on entre dans la boucle de traitement d'événement. Nous voici à la fin du tutoriel de barre d'outils. Naturellement, pour l'apprécier dans sa totalité, il vous faut aussi cette sympathique icône gtk.xpm. La Figure 10.8, « Exemple de barre d'outils » montre l'affichage obtenu :

Figure 10.8. Exemple de barre d'outils

X-L - Le bloc-notes (Notebook)

Le bloc-notes Notebook est une collection de "pages" qui se recouvrent ; chaque page contient des informations différentes et une seule de ces pages est visible à la fois. Ce widget est devenu très commun dernièrement dans la programmation d'interface graphique, et c'est un bon moyen de montrer des blocs d'information similaires en garantissant une séparation de leur affichage.

La première fonction dont on aura besoin, comme on pourrait s'en douter, sert à créer un nouveau bloc-notes.

bloc_notes = gtk.Notebook()

Une fois le bloc-notes créé, il existe un certain nombre de méthodes qui agissent sur lui. Regardons-les une à une.

La première que nous verrons sert à positionner les indicateurs de pages. Ces indicateurs de pages ou "onglets" comme on les appelle, peuvent être positionnés de quatre manières : en haut, en bas, à gauche ou à droite.

bloc_notes.set_tab_pos(pos)

pos peut prendre une des valeurs suivantes (assez claires par elles-mêmes) :

1. POS_LEFT       # à gauche

2. POS_RIGHT      # à droite

3. POS_TOP        # en haut

4. POS_BOTTOM     # en bas

La valeur par défaut est POS_TOP.

Ensuite, voyons comment ajouter des pages au bloc-notes. Il existe trois moyens d'ajouter des pages à un bloc-notes Notebook. Voyons les deux premiers, presque similaires :

1. bloc_notes.append_page(child, tab_label)

3. bloc_notes.prepend_page(child, tab_label)

Ces méthodes ajoutent des pages en les insérant à la fin (append) ou au début (prepend) du bloc-notes. Le paramètre child est le widget qui est placé dans la page et le paramètre tab_label est l'étiquette de la page ajoutée. Le widget child doit être créé séparément et consiste en un ensemble de déclarations d'options placées dans un autre widget conteneur, tel qu'une table.

La dernière méthode pour ajouter des pages à un bloc-notes contient toutes les propriétés des deux précédentes, mais permet de préciser la position où l'on veut insérer la page.

bloc_notes.insert_page(child, tab_label, position)

Les paramètres sont les mêmes que pour append() et prepend() avec un paramètre supplémentaire, position. Celui-ci indique la position de la page à insérer, la première page possédant la position zéro.

Maintenant que nous savons comment ajouter des pages, voyons comment supprimer une page du bloc-notes.

bloc_notes.remove_page(num_page)

Cette méthode supprime du bloc-notes la page indiquée par num_page.

Pour connaître la page courante d'un bloc-notes, on utilise la méthode :

page = bloc_notes.get_current_page()

Les deux méthodes suivantes sont de simples appels qui permettent de passer à la page précédente ou à la page suivante. Il suffit de fournir à chaque méthode le bloc-notes sur lequel on veut agir.

1. bloc_notes.next_page()

3. bloc_notes.prev_page()

Lorsque le bloc-notes est sur la dernière page et que l'on appelle la méthode next_page(), rien ne se passe. De même, lorsque l'on est sur la première page et que l'on appelle prev_page(), rien ne se passe.

La méthode suivante définit la page "active". Si l'on veut que le bloc-notes s'ouvre à la page 5, par exemple, on utilisera cette méthode. Sinon, le bloc-notes affichera par défaut la première page.

bloc_notes.set_current_page(num_page)

Les deux méthodes suivantes ajoutent ou enlèvent respectivement les onglets des pages et les bordures du bloc-notes.

1. bloc_notes.set_show_tabs(montre_onglet)

3. bloc_notes.set_show_border(montre_bordure)

La méthode suivante est utile lorsque l'on a un grand nombre de pages, et que tous les onglets ne peuvent pas être affichés. Cela permet de faire défiler les onglets en utilisant deux boutons de navigation.

bloc_notes.set_scrollable(defile)

Les paramètres montre_onglet, montre_bordure et defile ont pour valeur TRUE ou FALSE.

Voyons maintenant un exemple. Le programme notebook.py crée une fenêtre avec un bloc-notes et six boutons. Le bloc-notes contient 11 pages, ajoutées selon les trois méthodes différentes, au début, à la fin ou insérées. Les boutons vous permettent de décaler la position des onglets, d'ajouter ou d'enlever les onglets ou les bordures, de supprimer une page, de circuler entre les pages d'avant en arrière et de quitter le programme.

Figure 10.9. Exemple de bloc-notes

Voici le code source de notebook.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # example bloc_notes.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleBlocNotes:

10. # Cette méthode fait circuler la position des étiquettes

11. def circule_bloc(self, bouton, bloc_notes):

12. bloc_notes.set_tab_pos((bloc_notes.get_tab_pos()+1) %4)

14. # Ajoute/supprime les onglets et bordures des pages

15. def style_bloc(self, bouton, bloc_notes):

16. val_onglet = False

17. val_bord = False

18. if self.show_tabs == False:

19. val_onglet = True

20. if self.show_border == False:

21. val_bord = True

23. bloc_notes.set_show_tabs(val_onglet)

24. self.show_tabs = val_onglet

25. bloc_notes.set_show_border(val_bord)

26. self.show_border = val_bord

28. # Supprime une page du bloc-notes

29. def supprime_bloc(self, bouton, bloc_notes):

30. page = bloc_notes.get_current_page()

31. bloc_notes.remove_page(page)

32. # Il faut rafraîchir l'affichage du widget

33. # Ceci oblige le widget à se redessiner

34. bloc_notes.queue_draw_area(0,0,-1,-1)

36. def delete(self, widget, event=None):

37. gtk.main_quit()

38. return False

40. def __init__(self):

41. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

42. fenetre.connect("delete_event", self.delete)

43. fenetre.set_border_width(10)

45. table = gtk.Table(3,6,False)

46. fenetre.add(table)

48. # Créer un nouveau bloc-notes, définir la position des onglets

49. bloc_notes = gtk.Notebook()

50. bloc_notes.set_tab_pos(gtk.POS_TOP)

51. table.attach(bloc_notes, 0,6,0,1)

52. bloc_notes.show()

53. self.show_tabs = True

54. self.show_border = True

56. # On ajoute quelques pages au bloc-notes

57. for i in range(5):

58. bufferf = "Ajout après cadre %d" % (i+1)

59. bufferl = "PostPage %d" % (i+1)

61. cadre = gtk.Frame(bufferf)

62. cadre.set_border_width(10)

63. cadre.set_size_request(100, 75)

64. cadre.show()

66. label = gtk.Label(bufferf)

67. cadre.add(label)

68. label.show()

70. label = gtk.Label(bufferl)

71. bloc_notes.append_page(cadre, label)

73. # Ajout d'une page à un endroit précis

74. checkbouton = gtk.CheckButton("Cliquez-moi svp !")

75. checkbouton.set_size_request(100, 75)

76. checkbouton.show ()

78. label = gtk.Label("Ajout page")

79. bloc_notes.insert_page(checkbouton, label, 2)

81. # Enfin, ajout de page au début du bloc-notes

82. for i in range(5):

83. bufferf = "Ajout avant cadre %d" % (i+1)

84. bufferl = "PréPage %d" % (i+1)

86. cadre = gtk.Frame(bufferf)

87. cadre.set_border_width(10)

88. cadre.set_size_request(100, 75)

89. cadre.show()

91. label = gtk.Label(bufferf)

92. cadre.add(label)

93. label.show()

95. label = gtk.Label(bufferl)

96. bloc_notes.prepend_page(cadre, label)

98. # Définir la page d'ouverture (page 4)

99. bloc_notes.set_current_page(3)

101. # Créer quelques boutons

102. bouton = gtk.Button("fermer")

103. bouton.connect("clicked", self.delete)

104. table.attach(bouton, 0,1,1,2)

105. bouton.show()

107. bouton = gtk.Button("page suiv.")

108. bouton.connect("clicked", lambda w: bloc_notes.next_page())

109. table.attach(bouton, 1,2,1,2)

110. bouton.show()

112. bouton = gtk.Button("page préc.")

113. bouton.connect("clicked", lambda w: bloc_notes.prev_page())

114. table.attach(bouton, 2,3,1,2)

115. bouton.show()

117. bouton = gtk.Button("pos. onglet")

118. bouton.connect("clicked", self.circule_bloc, bloc_notes)

119. table.attach(bouton, 3,4,1,2)

120. bouton.show()

122. bouton = gtk.Button("onglet/bords")

123. bouton.connect("clicked", self.style_bloc, bloc_notes)

124. table.attach(bouton, 4,5,1,2)

125. bouton.show()

127. bouton = gtk.Button("supp. page")

128. bouton.connect("clicked", self.supprime_bloc, bloc_notes)

129. table.attach(bouton, 5,6,1,2)

130. bouton.show()

132. table.show()

133. fenetre.show()

135. def main():

136. gtk.main()

137. return 0

139. if __name__ == "__main__":

140. ExempleBlocNotes()

141. main()

J'espère que ceci vous aidera dans vos créations de blocs-notes pour vos applications PyGTK.

X-M - Les connexions et connecteurs (Plugs et Sockets)

Les Plugs et les Sockets agissent ensemble pour insérer l'interface utilisateur d'un processus dans un autre. Ceci peut aussi être réalisé avec Bonobo.

X-M-1 - Les Plugs

Un Plug encapsule une interface utilisateur fournie par une application pour qu'elle puisse être insérée dans l'interface d'une autre application. Le signal "embedded" avertit l'application connectée qu'elle a été insérée dans l'interface utilisateur de l'autre application.

On crée un Plug par la fonction suivante :

plug = gtk.Plug(socket_id)

qui crée un nouveau Plug et l'insère dans le Socket identifié par socket_id. Si socket_id vaut OL, le Plug est laissé "déconnecté" et pourra plus tard, être connecté dans un Socket en utilisant la méthode add_id() du Socket.

La méthode de Plug :

id = plug.get_id()

retourne l'identifiant de la fenêtre d'un Plug, qui peut être utilisé pour l'insérer dans un Socket en utilisant la méthode add_id() du Socket.

Le programme exemple plug.py illustre l'utilisation d'un Plug :

1. #!/usr/bin/python

2. # -*- coding:utf-8 -*-

3. import pygtk

4. pygtk.require('2.0')

5. import gtk,sys

7. Wid = 0L

8. if len(sys.argv) == 2:

9. Wid = long(sys.argv[1])

11. plug = gtk.Plug(Wid)

12. print "Id de Plug=", plug.get_id()

14. def embed_event(widget):

15. print "Je (", widget, ") viens juste d'être inséré !"

17. plug.connect("embedded", embed_event)

19. entry = gtk.Entry()

20. entry.set_text("salut")

21. def entry_point(widget):

22. print "Vous avez modifié mon texte en '%s'" % widget.get_text()

24. entry.connect("changed", entry_point)

25. plug.connect("destroy", lambda w: gtk.main_quit())

27. plug.add(entry)

28. plug.show_all()

31. gtk.main()

Ce programme se lance ainsi :

plug.py [windowID]

où le paramètre windowID est l'identifiant d'un Socket où connecter le Plug.

X-M-2 - Sockets

Un Socket fournit le widget pour insérer, de manière transparente, un Plug d'une autre application dans votre interface graphique. Une application crée un Socket et transmet l'identifiant de fenêtre de ce widget à une autre application qui alors, crée un Plug en utilisant cet identifiant de fenêtre comme paramètre. Tout widget contenu dans le Plug apparaitra dans la fenêtre de la première application.

L'ID (identifiant) de fenêtre du Socket s'obtient en utilisant la méthode de Socket.get_id(). Avant d'utiliser cette méthode, le Socket doit être réalisé et ajouté à son parent.

Si l'on transmet l'ID de fenêtre d'un Socket à un autre processus qui va créer un Plug dans ce Socket, il faut s'assurer que le widget Socket n'est pas détruit tant que le Plug n'est pas créé.

Quand GTK+ est prévenu que la fenêtre insérée a été détruite, il détruira aussi le Socket. Il faut toujours être préparé à ce que vos sockets soient détruits à n'importe quel moment quand la boucle principale s'exécute. Détruire un Socket entraine aussi la destruction du Plug inséré.

La communication entre un Socket et un Plug suit le protocole XEmbed. Ce protocole a été implémenté dans d'autres toolkits, par exemple Qt, ce qui permet le même niveau d'intégration lorsque l'on incorpore un widget Qt dans GTK ou vice versa.

Création d'un nouveau Socket vide :

socket = gtk.Socket()

Le Socket doit être placé dans une fenêtre de premier niveau avant d'invoquer la méthode add_id() :

socket.add_id(window_id)

qui ajoute un client XEMBED, tel un Plug, au Socket. Le client peut être dans le même processus ou un processus différent.

Pour insérer un Plug dans un Socket, on peut soit créer le Plug par :

plug = gtk.Plug(0L)

et ensuite, transmettre à la méthode add_id() du Socket le nombre renvoyé par la méthode get_id() du Plug :

socket.add_id(plug)

soit utiliser la méthode get_id() du Socket :

window_id = socket.get_id()

pour obtenir l'ID de fenêtre du socket et ensuite créer le Plug par :

plug = gtk.Plug(window_id)

Le Socket doit avoir été déjà ajouté à une fenêtre de niveau supérieur avant cet appel.

Le programme exemple socket.py illustre l'utilisation d'un Socket :

1. #!/usr/bin/python

2. # -*- coding:utf-8 -*-

3. import string

5. import pygtk

6. pygtk.require('2.0')

7. import gtk,sys

9. window = gtk.Window()

10. window.show()

12. socket = gtk.Socket()

13. socket.show()

14. window.add(socket)

16. print "ID du Socket = ", socket.get_id()

17. window.connect("destroy", lambda w: gtk.main_quit())

19. def plugged_event(widget):

20. print "Je (", widget, ") viens juste d'avoir un plug inséré !"

22. socket.connect("plug-added", plugged_event)

24. if len(sys.argv) == 2:

25. socket.add_id(long(sys.argv[1]))

27. gtk.main()

Pour tester l'exemple, vous pouvez lancer d'abord plug.py :

$ python plug.py
Id de Plug= 39845891

et copier l'ID résultant comme premier argument à socket.py :

1. $ python socket.py 39845891

2. Socket ID= 48234523

3. ID du Socket =  44040231

4. Je ( <gtk.Plug object (GtkPlug) at 0x404057ac> ) viens juste d'être inséré !

5. Je ( <gtk.Socket object (GtkSocket) at 0x4040a9b4> ) viens juste d'avoir un plug inséré !

Ou vous pouvez lancer socket.py :

$ python socket.py
ID du Socket =  39845927

et ensuite lancer plug.py, en recopiant L'ID de fenêtre :

1. $ python plug.py

2. 39845927

3. Je ( <gtk.Socket object (GtkSocket) at 0x4040a9b4> ) viens juste d'avoir un plug inséré !

4. Id de Plug= 44040195

XI - Chapitre 11. Les menus

Il y a deux méthodes de création des menus : la facile et la difficile. Toutes deux ont leur utilité, bien que dans la plupart des cas l'ItemFactory (la méthode facile) suffise. La méthode "difficile" consiste à créer tous les menus en les appelant directement, là où la méthode facile utilisera les appels de GtkItemFactory. Même si c'est effectivement beaucoup plus simple, des avantages et des inconvénients existent pour chaque approche.

Il est vrai que l'Itemfactory rend nettement plus simples la création et l'ajout ultérieur de menus ; mais faire la même chose manuellement, à l'aide de quelques fonctions, peut apporter beaucoup en termes d'ergonomie. Avec l'Itemfactory, vous ne pourrez pas ajouter d'images aux menus, ni y faire figurer le caractère "/".

XI-A - Création manuelle de menus

Comme le veut la bonne vieille tradition pédagogique, nous commençons par la méthode difficile :).

Trois widgets entrent en jeu dans la création d'une barre de menu et de ses sous-menus :

- l'entrée de menu, qui représente ce que l'utilisateur veut faire/choisir (par exemple "Enregistrer") ;

- le menu, qui contient les entrées de menu ;

- la barre de menu, qui contient les menus.

Mais tout n'est pas si simple. En effet, les entrées de menu servent à deux choses : elles représentent à la fois les widgets placés dans le menu et celui placé dans la barre de menu (celui qui, une fois sélectionné, active le menu).

Observons les fonctions servant à créer menus et barres de menus :

barre_menus = gtk.MenuBar()

Cette première fonction, vous l'aurez deviné, crée une nouvelle barre de menu. On utilisera la méthode add() de GtkContainer pour la placer dans une fenêtre, ou bien les différentes méthodes pack() de GtkBox pour la placer dans une boite (comme pour les boutons).

menu = gtk.Menu()

Cette fonction renvoie une référence à un nouveau menu ; il n'est jamais concrètement affiché (par la méthode show()), c'est juste un conteneur pour les entrées de menu. Tout ceci vous paraîtra, je l'espère, beaucoup plus clair après que vous aurez jeté un œil à l'exemple plus bas.

La fonction ci-dessous sert à créer les entrées qui seront placées dans le menu (et la barre de menu) :

entree = gtk.MenuItem(label=None)

L'argument label (étiquette), si spécifié, sera parcouru à la recherche de caractères mnémoniques. Cet appel crée les entrées de menu qui doivent apparaitre dans le menu. Attention à ne pas confondre le "menu", créé avec gtk.Menu(), et l'"entrée de menu", créée avec la fonction gtk.MenuItem(). L'entrée sera un véritable bouton, auquel sera associée une action, tandis que le menu sera un conteneur renfermant des entrées.

Une fois votre entrée créée, vous devez la placer dans un menu grâce à la méthode append(). Puis, pour pouvoir réagir à une sélection de cette entrée par l'utilisateur, il faudra capter son signal "activate" (activer) de la manière habituelle.

Imaginons que nous voulions créer un menu "Fichier" standard, avec les entrées Ouvrir, Enregistrer, et Quitter. Notre code devrait alors ressembler à ceci :

1. menu_fichier = gtk.Menu()    # Inutile d'afficher les menus avec show()

3. # Création des entrées de menu

4. entree_ouvrir = gtk.MenuItem("Ouvrir")

5. entree_enreg = gtk.MenuItem("Enregistrer")

6. entree_quitter = gtk.MenuItem("Quitter")

8. # On les place dans le menu

9. menu_fichier.append(entree_ouvrir)

10. menu_fichier.append(entree_enreg)

11. menu_fichier.append(entree_quitter)

13. # On connecte les fonctions de rappel au signal "activate"

14. entree_ouvrir.connect_object("activate", entrees_reponse, "fichier.ouvrir")

15. entree_enreg.connect_object("activate", entrees_reponse, "fichier.enregistrer")

17. # On peut connecter l’entrée Quitter à notre fonction de sortie

18. entree_quitter.connect_object ("activate", destroy, "fichier.quitter")

20. # Il nous faut afficher les entrées

21. entree_ouvrir.show()

22. entree_enreg.show()

23. entree_quitter.show()

Nous avons maintenant notre menu. Il nous reste à créer une barre de menu, puis une entrée "Fichier" à laquelle nous relierons notre menu. Le code pour ce faire est le suivant :

1. barre_menus = gtk.MenuBar()

2. fenetre.add(barre_menus)

3. barre_menus.show()

5. entree_fichier = gtk.MenuItem("Fichier")

6. entree_fichier.show()

Maintenant associons le menu avec entree_fichier. On fait appel pour cela à la méthode suivante :

entree.set_submenu(sous_menu)

Ce qui donnerait, pour la suite de notre exemple :

entree.set_submenu(menu_fichier)

Tout ce qui nous reste à faire est de placer le menu dans la barre de menu, en utilisant la méthode :

barre_menus.append(enfant)

qui, dans notre cas, donne :

barre_menus.append(entree_fichier)

Si nous voulions aligner notre menu sur la droite de la barre de menu, comme le sont souvent les menus d'aide, nous pourrions faire appel à la méthode ci-dessous (toujours sur entree_fichier dans notre exemple) avant de l'attacher à la barre de menu.

entree.set_right_justified(align_droite)

Voici un résumé des étapes nécessaires à la céation d'une barre de menus avec ses menus :

- créez un nouveau menu avec gtk.Menu() ;

- faites un appel à gtk.MenuItem() pour chaque entrée que vous voulez voir figurer dans votre menu, puis utilisez la méthode append() pour placer chacune de ces nouvelles entrées dans le menu ;

- créez une entrée avec gtk.MenuItem(). Ce sera la racine du menu, son étiquette apparaitra directement dans la barre de menus.

- Utilisez la méthode set_submenu() pour attacher le menu à l'entrée racine (celle créée à l'étape précédente) ;

- créez une barre de menu avec gtk.MenuBar(). Si l'on crée une série de menus que l'on veut placer sur une seule barre de menu, cette étape n'a besoin d'être effectuée qu'une seule fois ;

- utilisez la méthode append() pour placer la racine dans la barre de menu.

Le procédé de création d'un menu popup est plus ou moins identique. La différence est que le menu n'est pas affiché "automatiquement" par une barre de menu, mais explicitement par un appel à la méthode popup(), lors d'un événement "button-press", par exemple. Procédez comme suit :

- créez une fonction de rappel gestionnaire d'événement. Elle doit être de la forme :

def fct_rappel(widget, evenement):

et utilisera l'événement pour savoir où afficher le menu ;

- dans le gestionnaire d'événement, si l'événement est une pression d'un bouton de la souris, traitez-le comme un événement bouton (ce qu'il est) et utilisez-le comme montré dans l'exemple de code pour transmettre l'information à la méthode popup() ;

- attachez ce gestionnaire d'événement à un widget ainsi :

widget.connect_object("evenement", fct_rappel, menu)

où widget est le widget auquel vous connectez l'événement fct_rappel la fonction de rappel, et menu le menu crée par GtkMenu(). Ce dernier peut aussi être affiché par une barre d'état, comme dans notre code.

XI-B - Démonstration de création manuelle de menus

Voilà pour la théorie. Maintenant, jetons un œil du côté de l'exemple menu.py pour tenter de clarifier un peu tout ça. La Figure 11.1 nous montre la fenêtre du programme :

Figure 11.1. Exemple de menu créé manuellement

Le code source de menu.py est le suivant :

1. #!/usr/bin/env python

3. # exemple menu.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleMenu:

10. def __init__(self):

11. # Création d'une fenêtre

12. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

13. fenetre.set_size_request(200, 100)

14. fenetre.set_title("Test menu GTK")

15. fenetre.connect("delete_event", gtk.main_quit)

17. # Initialisation du widget. Souvenez-vous : on n'affiche jamais

18. # (avec show()) le widget menu !

19. # Il s'agit du menu qui contiendra les commandes, celui qui apparaitra

20. # après un clic sur le "Menu racine" de l'application.

21. menu = gtk.Menu()

23. # Ensuite, on écrit une petite boucle qui crée trois entrees pour le menu.

24. # Notez l'appel à gtk_menu_append. Ici, nous ajoutons une liste de commandes

25. # à notre menu. Nous devrions aussi nous débrouiller pour capter le signal

26. # "clicked" de chacune des commandes et mettre en place une fonction de rappel

27. # pour le traiter, mais pour ne pas faire trop long, on s'en passera ici.

28. for i in range(3):

29. # On copie les noms dans une variable "tampon".

30. tampon = "Sous-menu test - %d" % i

32. # On crée une nouvelle commande, avec un nom...

33. commandes = gtk.MenuItem(tampon)

35. # … et on l'ajoute au menu

36. menu.append(commandes)

38. # On réagit à la sélection de la commande

39. commandes.connect("activate", self.reponse_commande, tampon)

41. # On affiche le widget

42. commandes.show()

44. # Voici le menu racine. Ce sera aussi l'étiquette visible dans la barre de

45. # menus. On ne lui connecte pas de gestionnaire de signal, son rôle étant

46. # seulement de faire apparaitre le reste du menu lorsqu'on le clique.

47. menu_racine = gtk.MenuItem("Menu racine")

49. menu_racine.show()

51. # Ici on indique que notre "menu" créé plus haut doit être le menu de

52. # "Menu racine"

53. menu_racine.set_submenu(menu)

55. # Une boite verticale pour y placer un menu et un bouton :

56. boitev = gtk.VBox(False, 0)

57. fenetre.add(boitev)

58. boitev.show()

60. # On crée une barre pour contenir les menus, et on la place dans notre fenêtre

61. barre_menus = gtk.MenuBar()

62. boitev.pack_start(barre_menus, False, False, 2)

63. barre_menus.show()

65. # Création d'un bouton, auquel on attache le menu comme popup

66. bouton = gtk.Button("Cliquez-moi")

67. bouton.connect_object("event", self.evnmt_button_press, menu)

68. boitev.pack_end(bouton, True, True, 2)

69. bouton.show()

71. # Enfin, on rattache la commande à la barre de menu -- il s'agit de la commande

72. # "Menu racine" sur laquelle je me suis emballé =)

73. barre_menus.append (menu_racine)

75. # Toujours afficher la fenêtre en dernier, afin qu'elle soit complète lors de

76. # son apparition à l'écran.

77. fenetre.show()

79. # On répond à l'événement "button-press" en affichant un menu, qui est

80. # transmis comme "widget".

81. # Notez que l'argument "widget" N'EST PAS le bouton qui a été enfoncé,

82. #, mais bien le menu à afficher

83. def evnmt_button_press(self, widget, evenement):

84. if evenement.type == gtk.gdk.BUTTON_PRESS:

85. widget.popup(None, None, None, evenement.button, evenement.time)

86. # On fait savoir au code appelant que l'on a traité l'événement. Son

87. # histoire s'arrête ici.

88. return True

89. # On fait savoir au code appelant que l'événement n'a pas été traité. Il continue.

90. return False

92. # Affiche une chaine de caractères lorsqu'une commande est sélectionnée.

93. def reponse_commande(self, widget, chaine):

94. print "%s" % chaine

96. def main():

97. gtk.main()

98. return 0

100. if __name__ == "__main__":

101. ExempleMenu()

102. main()

Vous pouvez aussi n'affecter aucune réaction à la sélection d'une entrée de menu et, par l'intermédiaire d'une table de raccourcis-clavier, lier certaines touches aux fonctions de rappel du menu.

XI-C - Création de menus avec l'ItemFactory

À présent que nous avons vu la méthode difficile, voici comment faire la même chose avec les appels de gtk.ItemFactory.

XI-D - Démonstration de l'ItemFactory

La Figure 11.2 montre la fenêtre créée par le programme d'exemple itemfactory.py, qui utilise la gtk.ItemFactory :

Figure 11.2. Exemple de menu créé avec l'ItemFactory

Voici le code source de itemfactory.py :

1. #!/usr/bin/env python

3. # exemple itemfactory.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleItemFactory:

10. # L'inévitable petite fonction de rappel

11. def affiche_hello(self, w, donnees):

12. print "Salut tout le monde !"

14. # Voici la structure d'ItemFactoryEntry utilisée pour générer de nouveaux menus

15. # Element 1: Le chemin du menu. la lettre qui suit le caractère "_" indique un

16. #            raccourci-clavier actif une fois le menu ouvert.

17. # Element 2: Le raccourci-clavier pour cette entrée.

18. # Element 3: La fonction de rappel.

19. # Element 4: L'action de la fonction de rappel. Modifie les paramètres avec

20. #            lesquels la fonction de rappel est appelée. 0 par défaut.

21. # Element 5: Le type d'entrée. Indique quelle sorte d'entrée l'on souhaite créer.

22. #            Voici les différentes valeurs possibles :

24. #       NULL               -> ""

25. #       ""                 -> ""

26. #       "

XII - Chapitre 12. La zone de dessin (Drawing Area)

La zone de dessin DrawingArea adapte une gtk.gdk.Window, laquelle est une sous-classe de gtk.gdk.Drawable (de même que gtk.gdk.Pixmap). En effet, la DrawingArea fournit un simple "canevas", la gtk.gdk.Window adaptée sur laquelle on peut dessiner en utilisant les méthodes de la classe gtk.gdk.Drawable.

On crée une DrawingArea en utilisant le constructeur :

zone_dessin = gtk.DrawingArea()

Une DrawingArea est créée au départ avec une taille de (0, 0) donc pour rendre visible la zone_dessin, il faut préciser sa largeur et sa hauteur avec des valeurs supérieures à zéro en utilisant la méthode suivante :

zone_dessin.set_size_request(largeur, hauteur)

Pour dessiner sur une DrawingArea, il faut récupérer la gtk.gdk.Window en utilisant l'attribut window de la zone de dessin de cette manière :

dessinable = drawing_area.window

Ensuite, il est possible de dessiner sur le dessinable en utilisant la méthode gtk.gdk.Drawable décrite dans la Section 12.2, « Les méthodes pour dessiner ».

Pour posséder une gtk.gdk.Window associée pouvant être utilisée pour le dessin, le widget DrawingArea doit être "réalisé" (on a utilisé la méthode realize()).

XII-A - Le contexte graphique

Il existe un grand nombre de méthodes pour dessiner sur la gtk.gdk.Window d'une zone de dessin. Toutes ces méthodes exigent un contexte graphique (gtk.gdk.GC) pour encapsuler l'information nécessaire au dessin sous forme d'attributs. Un gtk.gdk.GC dispose des attributs suivants :

1. background            # fond

2. cap_style             # style de fin de ligne

3. clip_mask             # masque de découpe

4. clip_x_origin         # origine x de la découpe

5. clip_y_origin         # origine y de la découpe

6. fill                  # remplissage

7. font                  # police

8. foreground            # couleur de premier plan

9. function              # fonction

10. graphics_exposures    # exposition graphique

11. join_style            # style de jointure de lignes

12. line_style            # style de ligne

13. line_width            # largeur de ligne

14. stipple               # style de pointillé

15. sub_window            # sous-fenêtre

16. tile                  # mosaïque

17. ts_x_origin           # origine x

18. ts_y_origin           # origine y

background définit la gtk.gdk.Color utilisée comme couleur de fond.

foreground définit la gtk.gdk.Color utilisée comme couleur de premier plan.

Une gtk.gdk.Color est une couleur qui peut être allouée ou non allouée. Une couleur non allouée peut être créée avec le constructeur :

couleur = gtk.gdk.Color(red=0, green=0, blue=0, pixel=0)

où les paramètres red, green et blue sont des entiers compris entre 0 et 65535. Le paramètre pixel n'est pas souvent défini, car il est réécrit lorsque la couleur est allouée.

Une gtk.gdk.Color non allouée peut aussi être créée par la fonction :

couleur = gtk.gdk.color_parse(spec)

où spec est une chaine de spécification de couleur qui peut être :

- un nom de couleur (par exemple "red", "orange", "navajo white" tels qu'ils sont définis dans le fichier X Window rgb.txt), oo ;

- une chaine hexadécimale débutant par '#' et contenant trois groupes des chiffres hexadécimaux de même longueur ((1, 2, 3 ou 4 chiffres). Par exemple, "#F0A", "#FF00AA", "#FFF000AAA" et "#FFFF0000AAAA" qui représentent tous la même couleur.

Pour créer une gtk.gdk.Color représentant une couleur allouée, on utilise la méthode alloc_color() de gtk.gdk.Colormap qui possède trois variantes :

1. couleur = colormap.alloc_color(color, writeable=FALSE, best_match=TRUE)

3. couleur = colormap.alloc_color(spec, writeable=FALSE, best_match=TRUE)

5. couleur = colormap.alloc_color(red, green, blue, writeable=FALSE, best_match=TRUE)

Le paramètre couleur est un gtk.gdk.Color non alloué, spec est une chaine de spécification de couleur comme on l'a vu précédemment dans la fonction gtk.gdk.color_parse(). Les paramètres red, green et blue sont des valeurs entières de couleur décrites dans la fonction constructeur gtk.gdk.Color(). On peut aussi indiquer si la couleur allouée peut être modifiable (c.-à-d. , peut être modifiée plus tard, mais ne peut pas être partagée) ou si une meilleure approximation avec des couleurs existantes peut se faire lorsque la couleur exacte n'est pas disponible.

Par exemple :

1. navajowhite = colormap.alloc('navajo white')

3. cyan = colormap.alloc(0, 65535, 65535)

5. red = colormap.alloc_color('#FF0000', True, True)

On peut connaître la palette de couleurs associée à un widget par la méthode :

palette = widget.get_colormap()

cap_style précise le style de fin de ligne utilisé pour une ligne qui se termine sans rejoindre une autre. Les différents styles disponibles sont :

CAP_NOT_LAST

Dessine la même fin de ligne que CAP_BUTT pour des lignes de largeur non nulle. Pour des lignes de largeur nulle, le point final de la ligne n'est pas dessiné.

CAP_BUTT

La fin de ligne est carrée et étendue jusqu'aux coordonnées du point final.

CAP_ROUND

La fin de ligne est arrondie (demi-cercle d'un diamètre égal à l'épaisseur de la ligne) et centrée sur le point final.

CAP_PROJECTING

La fin de ligne est carrée et étendue de la moitié de l'épaisseur de la ligne après le point final.

clip_mask définit un gtk.gdk.Pixmap utilisé pour découper le dessin de la zone de dessin drawing_area.

clip_x_origin et clip_y_origin précise pour le découpage, l'origine en x et y par rapport au coin supérieur gauche de la zone de dessin.

fill précise le style de remplissage utilisé dans le dessin. Les styles disponibles sont :

SOLID

Dessine avec la couleur de premier plan.

TILED

Dessine avec un dessin en mosaïque.

STIPPLED

Dessine avec un patron de pointillé. Les pixels définis qui correspondent aux bits dans le patron seront dessinés dans la couleur de premier plan ; les pixels non définis qui correspondent aux bits dans le patron seront laissés intacts.

OPAQUE_STIPPLED

Dessine avec un patron de pointillé. Les pixels correspondants aux bits. Les pixels définis qui correspondent aux bits dans le patron seront dessinés dans la couleur de premier plan ; les pixels non définis qui correspondent aux bits dans le patron seront dessinés dans la couleur d'arrière-plan (de fond).

font est un gtk.gdk.Font qui est la police utilisée par défaut pour le texte du dessin.

L'utilisation de l'attribut font est abandonnée.

function précise comment les valeurs de bit pour les pixels source sont combinées avec les valeurs de bit pour les pixels de destination pour produire les bits de pixels résultat. Les seize valeurs suivantes correspondent aux 16 différentes possibilités d'une table de vérité 2x2, mais deux ou trois de ces valeurs seulement sont réellement utiles. Pour les images en couleur, seul COPY, XOR et INVERT sont généralement utiles alors que pour les bitmaps AND et OR sont aussi utilisés. Voici les valeurs possibles de function :

1. COPY           # copie

2. INVERT         # inverser

3. XOR            # OU exclusif

4. CLEAR          # nettoyer

5. AND            # ET

6. AND_REVERSE

7. AND_INVERT     # ET inversé

8. NOOP           # nul

9. OR             # OU

10. EQUIV          # équivalent

11. OR_REVERSE

12. COPY_INVERT    # copie inversée

13. OR_INVERT      # OU inversé

14. NAND           # non ET

15. SET            # afficher ?

graphics_exposures indique si l'affichage graphique est autorisé (TRUE) ou interdit (FALSE). Lorsque graphics_exposures vaut TRUE, l'échec de copie d'un pixel dans une action de dessin génère un événement expose. Si la copie réussit, un événement noexpose est généré.

join_style indique le style de croisement entre deux lignes. Les styles disponibles sont :

JOIN_MITER

Les côtés de chaque ligne sont prolongés pour se rencontrer en formant un angle.

JOIN_ROUND

Les côtés des deux lignes se rejoignent en formant un arrondi.

JOIN_BEVEL

Les côtés des deux lignes sont joints par un segment formant un angle égal avec chaque ligne.

line_style indique le style de traçage d'une ligne. Les possibilités sont :

LINE_SOLID

Les lignes sont tracées en continu.

LINE_ON_OFF_DASH

Les segments pairs sont dessinés, pas les segments impairs (ligne discontinue).

LINE_DOUBLE_DASH

Les segments pairs sont dessinés normalement. Les segments impairs sont dessinés dans la couleur de fond si le style de remplissage est SOLID, dans la couleur de fond avec le masque de pointillé si le style est STIPPLED.

line_width indique l'épaisseur de la ligne.

stipple indique le gtk.gdk.Pixmap utilisé pour un dessin en pointillé quand le paramètre fill vaut STIPPLED ou OPAQUE_STIPPLED.

sub_window indique le mode de dessin dans une gtk.gdk.Window qui possède des enfants gtk.gdk.Window. Les valeurs possibles pour sub_window sont :

CLIP_BY_CHILDREN

Dessine seulement dans la fenêtre elle-même, pas dans ses fenêtres enfants.

INCLUDE_INFERIORS

Dessine dans la fenêtre et dans ses fenêtres enfants.

tile indique le gtk.gdk.Pixmap utilisé pour dessiner en mosaïque lorsque le paramètre fill vaut TILED.

ts_x_origin et ts_y_origin indiquent l'origine de la mosaïque ou du pointillé (la position de départ du bitmap).

Un nouveau contexte graphique est créé par un appel à la méthode gtk.gdk.Drawable.new_gc() :

1. contexte_graph = drawable.new_gc(foreground=None, background=None, font=None,

2. function=-1, fill=-1, tile=None,

3. stipple=None, clip_mask=None, subwindow_mode=-1,

4. ts_x_origin=-1, ts_y_origin=-1, clip_x_origin=-1,

5. clip_y_origin=-1, graphics_exposures=-1,

6. line_width=-1, line_style=-1, cap_style=-1

7. join_style=-1)

Pour pouvoir créer un nouveau contexte graphique avec cette méthode, le drawable doit être :

- une gtk.gdk.Window qui a été réalisée (créée), ou ;

- un gtk.gdk.Pixmap associé à une gtk.gdk.Window réalisée.

Les différents attributs du contexte graphique possèdent des valeurs par défaut lorsqu'elles ne sont pas précisées dans la méthode new_gc(). Lorsque l'on souhaite définir des attributs du contexte graphique par la méthode new_gc(), il est plus facile d'utiliser les arguments par mot-clés de Python.

On peut aussi définir les attributs individuels d'un gtk.gdk.GC en assignant une valeur aux attributs de l'objet GC. Par exemple :

1. gc.cap_style = CAP_BUTT

2. gc.line_width = 10

3. gc.fill = SOLD

4. gc.foreground = macouleur

ou en utilisant les méthodes suivantes :

1. gc.set_foreground(color)

2. gc.set_background(color)

3. gc.set_function(function)

4. gc.set_fill(fill)

5. gc.set_tile(tile)

6. gc.set_stipple(stipple)

7. gc.set_ts_origin(x, y)

8. gc.set_clip_origin(x, y)

9. gc.set_clip_mask(mask)

10. gc.set_clip_rectangle(rectangle)

11. gc.set_subwindow(mode)

12. gc.set_exposures(exposures)

13. gc.set_line_attributes(line_width, line_style, cap_style, join_style)

Le patron de segments utilisé lorsque le paramètre de style de ligne line_style est LINE_ON_OFF_DASH ou LINE_DOUBLE_DASH peut être défini par la méthode suivante :

gc.set_dashes(offset, dash_list)

où le paramètre offset est la position de la valeur du segment initial dans la dash_list et dash_list est une liste de tuples contenant le nombre de pixels à dessiner ou à sauter pour réaliser les segments. Les segments sont dessinés en démarrant avec le nombre de pixels indiqué à la position de décalage (offset) ; ensuite le nombre suivant est le nombre de pixels sautés ; le nombre suivant est le nombre de pixels dessinés, et ainsi de suite, parcourant tous les nombres de la dash_list en recommençant au début quand on arrive à la fin de la liste. Par exemple, si la dash_list se compose de (2, 4, 8, 16) avec un décalage (offset) de 1, les segments sont dessinés ainsi : dessin sur 4 pixels, saut de 8 pixels, dessin sur 16 pixels, saut de 2 pixels, dessin sur 4 pixels, etc.

On peut réaliser une copie d'un gtk.gdk.GC existant par la méthode :

gc.copy(src_gc)

Les attributs du paramètre gc sont identiques à ceux du paramètre source src_gc.

XII-B - Les méthodes pour dessiner

Il existe un ensemble de méthodes pour dessiner sur le "canevas" d'une zone de dessin. Ces méthodes peuvent être utilisées pour n'importe quelle sous-classe de gtk.gdk.Drawable (soit un gtk.gdk.Window, soit un gtk.gdk.Pixmap). Les voici :

drawable.draw_point(gc, x, y) # dessiner un point

gc est le contexte graphique utilisé pour le dessin.

x et y sont les coordonnées du point.

drawable.draw_line(gc, x1, y1, x2, y2) # dessiner une ligne

gc est le contexte graphique.

x1 et y1 sont les coordonnées du point de départ, x2 et y2 les coordonnées du point d'arrivée.

drawable.draw_rectangle(gc, filled, x, y, largeur, hauteur) # dessiner un rectangle

gc est le contexte graphique.

filled est un booléen indiquant si le rectangle doit être rempli avec la couleur de premier plan (TRUE) ou non (FALSE).

x et y sont les coordonnées du coin haut gauche du rectangle.

largeur et hauteur indiquent la largeur et la hauteur du rectangle :

drawable.draw_arc(gc, filled, x, y, largeur, hauteur, angle1, angle2) # dessiner un arc de cercle

gc est le contexte graphique.

filled est un booléen indiquant si le rectangle doit être rempli avec la couleur de premier plan (TRUE) ou non (FALSE).

x et y sont les coordonnées du coin haut gauche du rectangle inscrit. largeur et hauteur indiquent la largeur et hauteur du rectangle inscrit.

angle1 est l'angle de départ de l'arc, en partant de la position 3 heures, dans le sens inverse des aiguilles d'une montre, par pas de 1/64 degré.

angle2 est la fin de l'angle de l'arc, relatif à angle1, par pas de 1/64 degré.

drawable.draw_polygon(gc, filled, points) # dessiner un polygone

gc est le contexte graphique.

filled est un booléen indiquant si le rectangle doit être rempli avec la couleur de premier plan (TRUE) ou non (FALSE).

points est un tuple contenant une liste de paires de coordonnées, par exemple [ (0,0), (2,5), (3,7), (4,11) ], désignant les points devant être reliés pour dessiner un polygone.

1. drawable.draw_string(font, gc, x, y, string) # dessiner une chaine de caractères

3. drawable.draw_text(font, gc, x, y, string) # dessiner un texte

font est le gtk.gdk.Font utilisé pour dessiner la chaine de caractères.

gc est le contexte graphique.

x et y sont les coordonnées du point où commence le dessin de la chaine, c'est-à-dire, la ligne de base de gauche.

string est la chaine de caractères à dessiner.

Les deux méthodes draw_string() et draw_text() sont démodées. Il faut plutôt utiliser un pango.Layout avec la méthode draw_layout().

drawable.draw_layout(gc, x, y, layout) # dessiner un affichage

gc est le contexte graphique.

x et y sont les coordonnées du point où débute le dessin du layout.

layout est le pango.Layout qui va être dessiné.

drawable.draw_drawable(gc, src, xsrc, ysrc, xdest, ydest, largeur, hauteur) # dessiner un dessinable

gc est le contexte graphique.

src est le dessinable d'origine.

xsrc et ysrc sont les coordonnées du rectangle supérieur gauche dans la source dessinable.

xdest et ydest sont les coordonnées du coin haut gauche de la zone de dessin.

largeur et hauteur sont la largeur et hauteur du dessinable d'origine à copier sur le dessinable. Si l'un de ces paramètres vaut -1, on utilise la largeur ou hauteur complète du dessinable.

drawable.draw_image(gc, image, xsrc, ysrc, xdest, ydest, largeur, hauteur) # dessiner une image

gc est le contexte graphique.

image est l'image origine.

xsrc et ysrc sont les coordonnées du coin supérieur gauche du rectangle dans le dessinable source.

xdest et ydest sont les coordonnées du coin supérieur gauche dans la zone de dessin.

largeur et hauteur sont la largeur et hauteur de la zone dessinable origine qui doit être copiée sur le dessinable (drawable). Si l'un de ces paramètres vaut -1, on utilise la largeur ou la hauteur complète de l'image.

drawable.draw_points(gc, points) # dessiner des points

gc est le contexte graphique.

points est une liste ou un tuple contenant les tuples de paires de coordonnées, par exemple [ (0,0), (2,5), (3,7), (4,11) ] désignant les points à dessiner.

drawable.draw_segments(gc, segs) # dessiner des segments de ligne

gc est le contexte graphique.

segs est une liste ou un tuple des coordonnées des points de départ et de fin des segments à dessiner, contenues dans un tuple, par exemple [ (0,0, 1,5), (2,5, 1,7), (3,7, 1,11), (4,11, 1,13) ].

drawable.draw_lines(gc, points) # dessiner une ligne brisée

gc est le contexte graphique.

points est une liste ou un tuple contenant les tuples de paires de coordonnées, par exemple [ (0,0), (2,5), (3,7), (4,11) ] désignant les points à relier par des lignes.

1. drawable.draw_rgb_image(gc, x, y, largeur, hauteur, dith, rgb_buf, rowstride)

3. drawable.draw_rgb_32_image(gc, x, y, largeur, hauteur, dith, buf, rowstride)

5. drawable.draw_gray_image(gc, x, y, largeur, hauteur, dith, buf, rowstride)

gc est le contexte graphique.

x et y sont les coordonnées du coin supérieur gauche du rectangle qui contient l'image.

largeur et hauteur sont la largeur et la hauteur du rectangle qui contient l'image.

dith est le mode de diffusion (tramage) décrit ci-dessous.

Pour la méthode draw_rgb_image(), le paramètre rgb_buf représente les données de l'image RGB, codifiées dans une chaine sous forme de séquence de triplets de pixels RGB de 8 bits. Pour la méthode draw_rgb_32_image(), buf représente les données de l'image RGB, codifiées dans une chaine sous forme de séquence de triplets de pixels RGB de 8 bits avec 8 bits de padding (4 caractères par pixel RGB). Pour la méthode draw_gray_image(), buf représente les données de l'image codifiées dans une chaine de pixels de 8 bits.

rowstride est le nombre de caractères depuis le début d'une rangée jusqu'au début de la rangée suivante de l'image. Normalement sa valeur par défaut est 3 * largeur pour la méthode draw_rgb_image(), 4 * largeur pour la méthode draw_rgb_32_image() et largeur pour la méthode draw_gray_image(). Si rowstride vaut 0, la ligne sera répétée hauteur fois.

Voici les différents modes de diffusion (dither) :

1. RGB_DITHER_NONE    # Ne pas utiliser de diffusion.

3. RGB_DITHER_NORMAL  # Utiliser la diffusion si seulement 8 bits par pixels (ou moins).

5. RGB_DITHER_MAX     # Utiliser la diffusion si 16 bits par pixels (ou moins).

Le programme exemple drawingarea.py illustre l'utilisation de la plupart des méthodes de la DrawingArea. Il place une zone de dessin (DrawingArea) dans une fenêtre défilante ScrolledWindow et ajoute des règles horizontales et verticales (Ruler). La Figure 12.1, « Exemple de Drawing Area » montre le programme après lancement :

Figure 12.1. Exemple de Drawing Area

Le code source ci-dessous drawingarea.py ci-dessous utilise l'image pixmap gtk.xpm

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple drawingarea.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

8. import operator

9. import time

10. import string

12. class ExempleZoneDessin:

13. def __init__(self):

14. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

15. fenetre.set_title("Exemple de zone de dessin")

16. fenetre.connect("destroy", lambda w: gtk.main_quit())

17. self.zone_dessin = gtk.DrawingArea()

18. self.zone_dessin.set_size_request(400, 300)

19. self.stylepango = self.zone_dessin.create_pango_layout("")

20. self.fenetre_defil = gtk.ScrolledWindow()

21. self.fenetre_defil.add_with_viewport(self.zone_dessin)

22. self.table = gtk.Table(2,2)

23. self.reglehoriz = gtk.HRuler()

24. self.reglevert = gtk.VRuler()

25. self.reglehoriz.set_range(0, 400, 0, 400)

26. self.reglevert.set_range(0, 300, 0, 300)

27. self.table.attach(self.reglehoriz, 1, 2, 0, 1, yoptions=0)

28. self.table.attach(self.reglevert, 0, 1, 1, 2, xoptions=0)

29. self.table.attach(self.fenetre_defil, 1, 2, 1, 2)

30. fenetre.add(self.table)

31. self.zone_dessin.set_events(gtk.gdk.POINTER_MOTION_MASK |

32. gtk.gdk.POINTER_MOTION_HINT_MASK )

33. self.zone_dessin.connect("expose-event", self.rappel_zone_dessin_expose)

34. def a_bouge(ruler, event):

35. return ruler.emit("motion_notify_event", event)

36. self.zone_dessin.connect_object("motion_notify_event", a_bouge,

37. self.reglehoriz)

38. self.zone_dessin.connect_object("motion_notify_event", a_bouge,

39. self.reglevert)

40. self.ajuste_hor = self.fenetre_defil.get_hadjustment()

41. self.ajuste_ver = self.fenetre_defil.get_vadjustment()

42. def rappel_valeur(ajuste, regle, horiz):

43. if horiz:

44. span = self.fenetre_defil.get_allocation()[3]

45. else:

46. span = self.fenetre_defil.get_allocation()[2]

47. l,u,p,m = regle.get_range()

48. v = ajuste.value

49. regle.set_range(v, v+span, p, m)

50. while gtk.events_pending():

51. gtk.main_iteration()

52. self.ajuste_hor.connect('value-changed', rappel_valeur, self.reglehoriz, True)

53. self.ajuste_ver.connect('value-changed', rappel_valeur, self.reglevert, False)

54. def rappel_taille_fixee(wid, allocation):

55. x, y, w, h = allocation

56. l,u,p,m = self.reglehoriz.get_range()

57. m = max(m, w)

58. self.reglehoriz.set_range(l, l+w, p, m)

59. l,u,p,m = self.reglevert.get_range()

60. m = max(m, h)

61. self.reglevert.set_range(l, l+h, p, m)

62. self.fenetre_defil.connect('size-allocate', rappel_taille_fixee)

63. self.zone_dessin.show()

64. self.reglehoriz.show()

65. self.reglevert.show()

66. self.fenetre_defil.show()

67. self.table.show()

68. fenetre.show()

70. def rappel_zone_dessin_expose(self, zone_dessin, event):

71. self.style = self.zone_dessin.get_style()

72. self.contexte_graph = self.style.fg_gc[gtk.STATE_NORMAL]

73. self.trace_point(10,10)

74. self.trace_points(110, 10)

75. self.trace_ligne(210, 10)

76. self.trace_lignes(310, 10)

77. self.trace_segments(10, 100)

78. self.trace_rectangles(110, 100)

79. self.trace_arcs(210, 100)

80. self.trace_pixmap(310, 100)

81. self.trace_polygone(10, 200)

82. self.trace_image_rgb(110, 200)

83. return True

85. def trace_point(self, x, y):

86. self.zone_dessin.window.draw_point(self.contexte_graph, x+30, y+30)

87. self.stylepango.set_text("Point")

88. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+50, self.stylepango)

89. return

91. def trace_points(self, x, y):

92. points = [(x+10,y+10), (x+10,y), (x+40,y+30),

93. (x+30,y+10), (x+50,y+10)]

94. self.zone_dessin.window.draw_points(self.contexte_graph, points)

95. self.stylepango.set_text("Points")

96. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+50, self.stylepango)

97. return

99. def trace_ligne(self, x, y):

100. self.zone_dessin.window.draw_line(self.contexte_graph, x+10, y+10, x+20, y+30)

101. self.stylepango.set_text("Ligne")

102. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+50, self.stylepango)

103. return

105. def trace_lignes(self, x, y):

106. points = [(x+10,y+10), (x+10,y), (x+40,y+30),

107. (x+30,y+10), (x+50,y+10)]

108. self.zone_dessin.window.draw_lines(self.contexte_graph, points)

109. self.stylepango.set_text("Lignes")

110. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+50, self.stylepango)

111. return

113. def trace_segments(self, x, y):

114. segments = ((x+20,y+10, x+20,y+70), (x+60,y+10, x+60,y+70),

115. (x+10,y+30 , x+70,y+30), (x+10, y+50 , x+70, y+50))

116. self.zone_dessin.window.draw_segments(self.contexte_graph, segments)

117. self.stylepango.set_text("Segments")

118. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+80, self.stylepango)

119. return

121. def trace_rectangles(self, x, y):

122. self.zone_dessin.window.draw_rectangle(self.contexte_graph, False, x, y, 80, 70)

123. self.zone_dessin.window.draw_rectangle(self.contexte_graph, True, x+10, y+10, 20, 20)

124. self.zone_dessin.window.draw_rectangle(self.contexte_graph, True, x+50, y+10, 20, 20)

125. self.zone_dessin.window.draw_rectangle(self.contexte_graph, True, x+20, y+50, 40, 10)

126. self.stylepango.set_text("Rectangles")

127. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+80, self.stylepango)

128. return

130. def trace_arcs(self, x, y):

131. self.zone_dessin.window.draw_arc(self.contexte_graph, False, x+10, y, 70, 70,

132. 0, 360*64)

133. self.zone_dessin.window.draw_arc(self.contexte_graph, True, x+30, y+20, 10, 10,

134. 0, 360*64)

135. self.zone_dessin.window.draw_arc(self.contexte_graph, True, x+50, y+20, 10, 10,

136. 0, 360*64)

137. self.zone_dessin.window.draw_arc(self.contexte_graph, True, x+30, y+10, 30, 50,

138. 210*64, 120*64)

139. self.stylepango.set_text("Arcs")

140. self.zone_dessin.window.draw_layout(self.contexte_graph, x+30, y+80, self.stylepango)

141. return

143. def trace_pixmap(self, x, y):

144. pixmap, mask = gtk.gdk.pixmap_create_from_xpm(

145. self.zone_dessin.window, self.style.bg[gtk.STATE_NORMAL], "gtk.xpm")

147. self.zone_dessin.window.draw_drawable(self.contexte_graph, pixmap, 0, 0, x+15, y+25,

148. -1, -1)

149. self.stylepango.set_text("Pixmap")

150. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+80, self.stylepango)

151. return

153. def trace_polygone(self, x, y):

154. points = [(x+10,y+60), (x+10,y+20), (x+40,y+70),

155. (x+30,y+30), (x+50,y+40)]

156. self.zone_dessin.window.draw_polygon(self.contexte_graph, True, points)

157. self.stylepango.set_text("Polygone")

158. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+80, self.stylepango)

159. return

161. def trace_image_rgb(self, x, y):

162. b = 80*3*80*['\0']

163. for i in range(80):

164. for j in range(80):

165. b[3*80*i+3*j] = chr(255-3*i)

166. b[3*80*i+3*j+1] = chr(255-3*abs(i-j))

167. b[3*80*i+3*j+2] = chr(255-3*j)

168. buff = string.join(b, '')

169. self.zone_dessin.window.draw_rgb_image(self.contexte_graph, x, y, 80, 80,

170. gtk.gdk.RGB_DITHER_NONE, buff, 80*3)

171. self.stylepango.set_text("Image RGB")

172. self.zone_dessin.window.draw_layout(self.contexte_graph, x+5, y+80, self.stylepango)

173. return

175. def main():

176. gtk.main()

177. return 0

179. if __name__ == "__main__":

180. ExempleZoneDessin()

181. main()

XIII - Chapitre 13. Le widget TextView

XIII-A - Présentation des widgets de texte

Le widget TextView (zone de texte) ainsi que les objets qui lui sont associés (TextBuffer, TextMark, TextIter, TextTag et TextTagTable) offrent une structure puissante d'édition de texte multiligne.

Un TextBuffer (buffer de texte) contient le texte qui est affiché par un ou plusieurs widgets TextView. Avec GTK+ 2.0 le texte est encodé en UTF-8, ce qui signifie qu'un caractère peut être encodé avec plusieurs octets. Dans un TextBuffer on doit bien faire la distinction entre le décompte des caractères (appelé offset) et le décompte des octets (appelé index).

Les TextIter (itérateurs de texte) permettent une représentation volatile d'une position dans un TextBuffer, située entre deux caractères. Les TextIter ne sont valables que tant que le nombre de caractères dans le TextBufferTextBuffer est inchangé. Chaque fois que l'on insère ou efface des caractères dans un TextBuffer tous les TextIterTextIter sont invalidés. Les TextIter sont le moyen le plus utilisé de spécifier des positions dans un TextBuffer afin d'y manipuler du texte.

Les TextMark (marques de texte) permettent de marquer des positions qui ne seront pas affectées par les modifications du TextBuffer. Une marque est comme un TextIter (elle représente une position entre deux caractères d'un TextBuffer) à la différence que si l'on efface le texte qui l'entoure elle reste à l'endroit où était le texte effacé. Si l'on insère du texte à la position indiquée par cette marque, celle-ci se retrouvera soit à gauche, soit à droite du texte inséré, en fonction de la gravité de la marque — la marque restera à droite du texte inséré dans le cas d'une gravité à droite, et à gauche dans le cas d'une gravité à gauche. On peut choisir de donner un nom à une TextMark, ou bien la laisser anonyme. Chaque TextBuffer comporte deux TextMark prédéfinies, nommées insert et selection_bound, faisant référence aux positions respectives du point d'insertion et de la fin de la sélection (la sélection est comprise entre les marques insert et selection_bound).

Les objets TextTag (balises de texte) spécifient un ensemble d'attributs pouvant être appliqués à une portion de texte d'un TextBufferTextBuffer. Chaque TextBuffer a une TextTagTable (table des balises de texte) qui recense les balises disponibles dans ce buffer. Plusieurs TextBuffer peuvent se partager la même TextTagTable pour offrir une meilleure cohérence. On utilise souvent les TextTag pour modifier l'apparence d'une portion de texte, mais elles peuvent tout aussi bien servir à empêcher l'édition de cette même portion de texte.

XIII-B - TextView : la zone de texte

Il n'y a qu'une fonction pour créer un nouveau widget TextView.

textview = gtk.TextView(buffer=None)

Le fait de créer une TextView entraine aussi la création d'un TextBuffer et d'une TextTagTable par défaut qui lui seront associés. Si vous souhaitez utiliser un TextBufferTextBuffer existant pour votre TextView, spécifiez-le dans la méthode ci-dessus. Pour changer le TextBuffer utilisé par une TextView, utilisez la méthode suivante :

textview.set_buffer(buffer)

Utilisez la méthode suivante pour obtenir une référence au TextBuffer d'une TextView :

buffer = textview.get_buffer()

Le widget TextView n'a pas de barres de défilement pour ajuster la vue quand le texte dépasse de la fenêtre. Pour lui en donner, vous devez placer votre TextView dans une ScrolledWindow (pas encore traduit).

Vous pouvez utiliser une TextView pour permettre à l'utilisateur d'éditer un texte ou bien pour afficher plusieurs lignes d'un texte en lecture seule. Pour passer d'un mode à l'autre, faites appel à la méthode suivante :

textview.set_editable(choix)

L'argument choix prend la valeur TRUE ou FALSE pour préciser si l'utilisateur est autorisé à éditer le contenu du widget TextView ou non. Le mode d'édition de la TextView peut être modifié pour des portions de texte du TextBuffer par l'intermédiaire des TextTag.

On retrouve le mode d'édition courant avec la méthode suivante :

edition = textview.get_editable()

Lorsque la TextView n'est pas éditable, vous devriez probablement masquer le curseur en utilisant cette méthode :

textview.set_cursor_visible(choix)

L'argument choix prend la valeur TRUE ou FALSE pour préciser si le curseur doit être visible.

Le widget TextView peut ajuster la largeur du texte à la fenêtre d'affichage grâce au retour à la ligne automatique. Cette option n'est pas activée par défaut, mais peut l'être à tout moment grâce à la méthode qui suit :

textview.set_wrap_mode(choix)

Cette méthode vous permet d'indiquer si la coupure du retour à la ligne automatique doit s'effectuer après le dernier caractère de la ligne, ou bien si ce dernier doit préserver l'intégrité des mots. L'argument choix est à choisir parmi :

1. gtk.WRAP_NONE    # pas de retour à la ligne automatique

2. gtk.WRAP_CHAR    # couper après le dernier caractère de la ligne

3. gtk.WRAP_WORD    # couper après le dernier mot de la ligne

L'alignement par défaut du texte peut être défini et récupéré avec les méthodes :

textview.set_justification(alignement)
alignement = textview.get_justification()

où alignement peut être :

1. gtk.JUSTIFY_LEFT      # aligné à gauche

2. gtk.JUSTIFY_RIGHT     # aligné à droite

3. gtk.JUSTIFY_CENTER    # centré

Le texte sera aligné à gauche (JUSTIFY_LEFT) si le retour à la ligne automatique est désactivé (WRAP_NONE). L'alignement par défaut peut toutefois être modifié avec des TextTag dans le TextBuffer correspondant.

Parmi les autres attributs de texte, la marge gauche, la marge droite, les tabulations et l'indentation des paragraphes peuvent être définis et récupérés avec les méthodes suivantes :

1. textview.set_left_margin(marge_gauche)

2. marge_gauche = textview.get_left_margin()

4. textview.set_right_margin(marge_droite)

5. marge_droite = textview.get_right_margin()

7. textview.set_indent(indentation)

8. indentation = textview.get_indent()

10. textview.set_pixels_above_lines(pixels_au_dessus_de_la_ligne)

11. pixels_au_dessus_de_la_ligne = textview.get_pixels_above_lines()

13. textview.set_pixels_below_lines(pixels_au_dessous_de_la_ligne)

14. pixels_au_dessous_de_la_ligne = textview.get_pixels_below_lines()

16. textview.set_pixels_inside_wrap(pixels_retour_a_la_ligne)

17. pixels_retour_a_la_ligne = textview.get_pixels_inside_wrap()

19. textview.set_tabs(tabulations)

20. tabulations = textview.get_tabs()

marge_gauche, marge_droite, indentation, pixels_au_dessus_de_la_ligne, pixels_au_dessous_de_la_ligne et pixels_retour_a_la_ligne doivent être indiqués en pixels. Ces valeurs par défaut peuvent être modifiées en employant des TextTag dans le TextBuffer correspondant. tabs doit être un pango.TabArray.

Le programme d'exemple textview-basic.py illustre l'utilisation basique du widget TextView :

Figure 13.1. Exemple basique de TextView

Le code source du programme est le suivant :

1. #!/usr/bin/env python

3. # exemple textview-basic.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleTextView:

10. def change_editable(self, case, textview):

11. textview.set_editable(case.get_active())

13. def change_curseur_visible(self, case, textview):

14. textview.set_cursor_visible(case.get_active())

16. def change_marge_gauche(self, case, textview):

17. if case.get_active():

18. textview.set_left_margin(50)

19. else:

20. textview.set_left_margin(0)

22. def change_marge_droite(self, case, textview):

23. if case.get_active():

24. textview.set_right_margin(50)

25. else:

26. textview.set_right_margin(0)

28. def change_retour_ligne(self, boutonradio, textview, val):

29. if boutonradio.get_active():

30. textview.set_wrap_mode(val)

32. def change_alignement(self, boutonradio, textview, val):

33. if boutonradio.get_active():

34. textview.set_justification(val)

36. def fermer_application(self, widget):

37. gtk.main_quit()

39. def __init__(self):

40. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

41. fenetre.set_resizable(True)

42. fenetre.connect("destroy", self.fermer_application)

43. fenetre.set_title("Exemple de widget TextView simple")

44. fenetre.set_border_width(0)

46. boite1 = gtk.VBox(False, 0)

47. fenetre.add(boite1)

48. boite1.show()

50. boite2 = gtk.VBox(False, 10)

51. boite2.set_border_width(10)

52. boite1.pack_start(boite2, True, True, 0)

53. boite2.show()

55. fd = gtk.ScrolledWindow()

56. fd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

57. textview = gtk.TextView()

58. buffertexte = textview.get_buffer()

59. fd.add(textview)

60. fd.show()

61. textview.show()

63. boite2.pack_start(fd)

64. # Chargement du fichier textview-basic.py dans la fenêtre

65. fichier = open("textview-basic.py", "r")

67. if fichier:

68. chaine = fichier.read()

69. fichier.close()

70. buffertexte.set_text(chaine)

72. boiteH = gtk.HButtonBox()

73. boite2.pack_start(boiteH, False, False, 0)

74. boiteH.show()

76. boiteV = gtk.VBox()

77. boiteV.show()

78. boiteH.pack_start(boiteV, False, False, 0)

79. # case à cocher pour autoriser l'édition du texte

80. case = gtk.CheckButton("Editable")

81. boiteV.pack_start(case, False, False, 0)

82. case.connect("toggled", self.change_editable, textview)

83. case.set_active(True)

84. case.show()

85. # case à cocher pour afficher le curseur

86. case = gtk.CheckButton("Curseur visible")

87. boiteV.pack_start(case, False, False, 0)

88. case.connect("toggled", self.change_curseur_visible, textview)

89. case.set_active(True)

90. case.show()

91. # case à cocher pour afficher une marge gauche

92. case = gtk.CheckButton("Marge gauche")

93. boiteV.pack_start(case, False, False, 0)

94. case.connect("toggled", self.change_marge_gauche, textview)

95. case.set_active(False)

96. case.show()

97. # case à cocher pour afficher une marge droite

98. case = gtk.CheckButton("Marge droite")

99. boiteV.pack_start(case, False, False, 0)

100. case.connect("toggled", self.change_marge_droite, textview)

101. case.set_active(False)

102. case.show()

103. # boutons radio pour définir le type de retour à la ligne automatique

104. boiteV = gtk.VBox()

105. boiteV.show()

106. boiteH.pack_start(boiteV, False, False, 0)

107. radio = gtk.RadioButton(None, "WRAP__NONE")

108. boiteV.pack_start(radio, False, True, 0)

109. radio.connect("toggled", self.change_retour_ligne, textview, gtk.WRAP_NONE)

110. radio.set_active(True)

111. radio.show()

112. radio = gtk.RadioButton(radio, "WRAP__CHAR")

113. boiteV.pack_start(radio, False, True, 0)

114. radio.connect("toggled", self.change_retour_ligne, textview, gtk.WRAP_CHAR)

115. radio.show()

116. radio = gtk.RadioButton(radio, "WRAP__WORD")

117. boiteV.pack_start(radio, False, True, 0)

118. radio.connect("toggled", self.change_retour_ligne, textview, gtk.WRAP_WORD)

119. radio.show()

121. # boutons radio pour définir l'alignement

122. boiteV = gtk.VBox()

123. boiteV.show()

124. boiteH.pack_start(boiteV, False, False, 0)

125. radio = gtk.RadioButton(None, "JUSTIFY__LEFT")

126. boiteV.pack_start(radio, False, True, 0)

127. radio.connect("toggled", self.change_alignement, textview,

128. gtk.JUSTIFY_LEFT)

129. radio.set_active(True)

130. radio.show()

131. radio = gtk.RadioButton(radio, "JUSTIFY__RIGHT")

132. boiteV.pack_start(radio, False, True, 0)

133. radio.connect("toggled", self.change_alignement, textview,

134. gtk.JUSTIFY_RIGHT)

135. radio.show()

136. radio = gtk.RadioButton(radio, "JUSTIFY__CENTER")

137. boiteV.pack_start(radio, False, True, 0)

138. radio.connect("toggled", self.change_alignement, textview,

139. gtk.JUSTIFY_CENTER)

140. radio.show()

142. separateur = gtk.HSeparator()

143. boite1.pack_start(separateur, False, True, 0)

144. separateur.show()

146. boite2 = gtk.VBox(False, 10)

147. boite2.set_border_width(10)

148. boite1.pack_start(boite2, False, True, 0)

149. boite2.show()

151. bouton = gtk.Button("Fermer")

152. bouton.connect("clicked", self.fermer_application)

153. boite2.pack_start(bouton, True, True, 0)

154. bouton.set_flags(gtk.CAN_DEFAULT)

155. bouton.grab_default()

156. bouton.show()

157. fenetre.show()

159. def main():

160. gtk.main()

161. return 0

163. if __name__ == "__main__":

164. ExempleTextView()

165. main()

Aux lignes 10 à 34, on définit les fonctions de rappel des cases à cocher et des boutons radio, que l'on utilisera pour changer les attributs par défaut de la TextView. Aux lignes 55 à 63, on crée une ScrolledWindow pour contenir la TextView. La ScrolledWindow est placée dans une VBox avec les cases à cocher et les boutons radio créés aux lignes 72 à 140. Le TextBuffer associé à la TextView est chargé avec le contenu du fichier source aux lignes 64 à 70.

XIII-C - TextBuffer : le buffer de texte

Le TextBuffer est le composant principal du système d'édition de texte de PyGTK. Il contient le texte, les TextTag (dans une TextTagTable), et les TextMark, qui, ensemble, décrivent comment le texte doit être affiché et permettent à l'utilisateur de modifier interactivement le texte ou la manière dont il s'affiche. Comme nous l'avons fait remarquer dans la section précédente, un TextBuffer est associé à un ou plusieurs TextView, qui se chargent d'afficher son contenu.

La création d'un TextBuffer se fait soit automatiquement lorsque l'on crée un TextView, soit avec la fonction :

bufferdetexte = TextBuffer(table=None)

où table est une TextTagTable. Si table n'est pas spécifié (ou s'il vaut None), une nouvelle TextTagTable sera créée pour le TextBuffer.

Un grand nombre de méthodes est disponible pour :

- insérer et supprimer du texte dans un buffer ;

- créer, supprimer et manipuler des marques ;

- manipuler le curseur et la sélection ;

- créer, appliquer et supprimer des balises ;

- spécifier et manipuler des TextIter ;

- récupérer des informations sur l'état du buffer.

XIII-C-1 - Récupérer des informations sur le buffer

Vous pouvez récupérer le nombre de lignes d'un TextBuffer avec la méthode :

nombre_lignes = bufferdetexte.get_line_count()

De la même manière, vous pouvez obtenir le nombre de caractères présents dans le TextBuffer :

nombre_caract = bufferdetexte.get_char_count()

Lorsque le contenu du TextBuffer est modifié, le drapeau de modification est levé. L'état de ce drapeau peut être obtenu à l'aide de la méthode :

modif = bufferdetexte.get_modified()

Si le programme sauvegarde le contenu du buffer, la méthode suivante peut réinitialiser le drapeau de modification :

bufferdetexte.set_modified(choix)

XIII-C-2 - Créer des TextIter

On utilise les TextIterTextIterTextIter pour spécifier une position entre deux caractères d'un TextBuffer. Les méthodes du TextBuffer qui manipulent du texte s'en servent pour indiquer à quelle position elles doivent s'appliquer. Les TextIterTextIter possèdent de nombreuses méthodes qui seront décrites dans la section TextIter.

Les méthodes de TextBuffer basiques pour créer des TextIter sont :

1. iter = bufferdetexte.get_iter_at_offset(n)

3. iter = bufferdetexte.get_iter_at_line(x)

5. iter = bufferdetexte.get_iter_at_line_offset(x, n)

7. iter = bufferdetexte.get_iter_at_mark(marque)

get_iter_at_offset() crée un itérateur juste après le ne caractère à partir du début du TextBuffer.

get_iter_at_line() crée un itérateur juste avant le premier caractère de la xe ligne.

get_iter_at_line_offset() crée un itérateur juste après le ne caractère de la xe ligne.

get_iter_at_mark() crée un itérateur à la même position que la TextMark marque.

Le méthode qui suit crée un ou plusieurs TextIter à des emplacements spécifiques du buffer :

1. iterdebut = bufferdetexte.get_start_iter()

3. iterfin = bufferdetexte.get_end_iter()

5. iterdebut, iterfin = bufferdetexte.get_bounds()

7. debut, fin = bufferdetexte.get_selection_bounds()

get_start_iter() crée un itérateur juste avant le premier caractère du buffer.

get_end_iter() crée un itérateur juste après le dernier caractère du buffer.

get_bounds() crée un tuple de deux itérateurs respectivement situés juste avant le premier caractère et juste après le dernier caractère du buffer.

get_selection_bounds() crée un tuple de deux itérateurs aux mêmes positions que les marques insert et selection_bound du buffer.

XIII-C-3 - Insérer, récupérer et supprimer du texte

Le texte contenu par un TextBuffer peut être défini avec la méthode suivante :

bufferdetexte.set_text(texte)

Cette méthode remplace le contenu actuel de bufferdetexte par texte.

La méthode la plus générale pour insérer des caractères dans un TextBuffer est :

bufferdetexte.insert(iter, texte)

où l'on insère le texte texte à l'emplacement de bufferdetexte spécifié par iter.

Si vous souhaitez simuler une insertion de texte par un utilisateur interactif, faites appel à la méthode suivante :

resultat = bufferdetexte.insert_interactive(iter, texte, defaut_editable)

qui insérera texte dans le buffer, à la position spécifiée par iter, mais seulement si cette position est éditable (c'est-à-dire si elle ne porte pas de balise mentionnant que le texte est non éditable) et que la valeur de defaut_editable est TRUE. Le résultat indique si le texte a été inséré.

L'argument defaut_editable précise si un texte peut être édité dans le cas où il ne porte pas de balise le spécifiant ; defaut_editable est généralement déterminé par un appel à la méthode get_editable() du TextView.

D'autres méthodes existent pour insérer du texte :

1. bufferdetexte.insert_at_cursor(texte)

3. resultat = bufferdetexte.insert_at_cursor_interactive(texte, defaut_editable)

5. bufferdetexte.insert_range(iter, debut, fin)

7. resultat = bufferdetexte.insert_range_interactive(iter, debut, fin, defaut_editable)

insert_at_cursor() est une méthode pratique pour insérer du texte à l'emplacement courant du curseur (insert).

insert_range() copie le texte, les pixbufs et les balises compris entre les itérateurs debut et fin d'un TextBuffer (si ce TextBuffer n'est pas bufferdetexte, la table des balises doit être la même), puis colle le tout dans bufferdetexte à l'emplacement indiqué par iter.

Les versions interactives de ces méthodes opèrent de la même manière à la différence qu'elles n'insèrent la copie que si l'emplacement est éditable.

Enfin, on peut insérer du texte et lui appliquer des balises en même temps par l'intermédiaire des méthodes :

1. bufferdetexte.insert_with_tags(iter, texte, balise1, balise2, ...)

3. bufferdetexte.insert_with_tags_by_name(iter, texte, nom_balise1, nom_balise2, ...)

insert_with_tags() insère le texte dans le bufferdetexte, à l'emplacement spécifié par l'iter, et lui applique les balises données.

insert_with_tags_by_name() fait la même chose, mais permet de spécifier les balises en les nommant.

On peut effacer du texte d'un buffer avec les méthodes :

1. bufferdetexte.delete(debut, fin)

3. resultat = bufferdetexte.delete_interactive(debut, fin, defaut_editable)

delete() supprime le texte compris entre les TextIter debut et fin de bufferdetexte.

delete_interactive() supprime tout texte éditable (déterminé comme tel par les balises de texte applicables et par l'argument defaut_editable) compris entre debut et fin.

Vous pouvez récupérer une copie du texte d'un TextBuffer grâce aux méthodes :

1. texte = bufferdetexte.get_text(debut, fin, include_hidden_chars=TRUE)

3. texte = bufferdetexte.get_slice(debut, fin, include_hidden_chars=TRUE)

get_text() retourne une copie du texte du buffer compris entre debut et fin ; le texte non affiché est exclu si include_hidden_chars vaut FALSE. Les caractères représentant des images incrustées ou des widgets sont exclus.

get_slice() est identique à get_text() à la différence que le texte qu'elle retourne inclut un caractère 0xFFFC pour chaque image incrustée ou widget.

XIII-C-4 - Créer et manipuler des TextMark

Les TextMark sont similaires aux TextIter en ceci qu'elles spécifient une position entre deux caractères d'un TextBuffer. Cependant, les TextMarkTextMarkTextMark maintiennent leur information de position même après modification du buffer. Les méthodes des TextMarkTextMark seront décrites plus loin dans la section TextMark.

Un buffer de texte contient deux marques inhérentes : la marque insert (insertion) et la marque selection_bound (fin de sélection). La marque insert est la position par défaut pour l'insertion de texte (le curseur). La marque selection_bound se combine avec la marque insert pour définir une sélection.

On peut retrouver ces deux marques en faisant appel aux méthodes suivantes :

1. marque_insert = bufferdetexte.get_insert()

3. marque_selectionbound = bufferdetexte.get_selection_bound()

Les marques insert et selection_bound peuvent être placées simultanément à une position grâce à la méthode :

bufferdetexte.place_cursor(emplacement)

emplacement est un TextIter qui indique la position. La méthode place_cursor() est nécessaire pour éviter de créer temporairement une sélection si les marques ont été déplacées individuellement.

On crée des TextMark avec la méthode :

marque = bufferdetexte.create_mark(nom_marque, emplacement, left_gravity=FALSE)

où nom_marque est le nom assigné à la marque créée (None pour une marque anonyme), emplacement est l'itérateur de texte indiquant la position de la marque dans le buffer, et left_gravity indique la situation de la marque après que du texte y aura été inséré (gauche si TRUE, droite si FALSE).

Une marque peut être déplacée dans le buffer avec les méthodes :

1. bufferdetexte.move_mark(marque, emplacement)

3. bufferdetexte.move_mark_by_name(nom_marque, emplacement)

marque désigne la marque à déplacer. nom_marque spécifie le nom de la marque à déplacer. emplacement est un itérateur de texte indiquant la nouvelle position.

On peut supprimer une marque d'un buffer avec les méthodes :

1. bufferdetexte.delete_mark(marque)

3. bufferdetexte.delete_mark_by_name(nom_marque)

On peut récupérer une marque par son nom avec la méthode :

marque = bufferdetexte.get_mark(nom_marque)

XIII-C-5 - Créer et appliquer des TextTag

Les TextTag contiennent un ou plusieurs attributs (comme les couleurs d'arrière-plan et de premier plan, la police, la possibilité d'édition) que l'on peut appliquer à une ou plusieurs portions du texte d'un buffer. Les attributs pouvant être spécifiés par une TextTagTextTag seront décrits dans la section TextTag.

La méthode suivante crée une TextTag avec des attributs et l'installe dans la TextTagTable d'un TextBuffer :

balise = bufferdetexte.create_tag(name=None, attribut1=valeur1, attribut2=valeur2, ...)

name est soit une chaine de caractères spécifiant le nom de la balise, soit None si la balise est anonyme. Les couples "attribut-valeur" déterminent les attributs de la balise. Reportez-vous à la section TextTagTextTag pour des informations concernant les attributs qui peuvent être définis par les propriétés d'une TextTag.

Une balise peut être appliquée à une portion du texte d'un buffer par la méthode suivante :

1. bufferdetexte.apply_tag(balise, debut, fin)

3. bufferdetexte.apply_tag_by_name(nom_balise, debut, fin)

où balise est la balise à appliquer au texte, nom_balise le nom de la balise, et debut et fin des itérateurs de texte qui indiquent la portion de texte à laquelle la balise doit être appliquée.

On peut retirer une balise d'une portion de texte en utilisant les méthodes suivantes :

1. bufferdetexte.remove_tag(balise, debut, fin)

3. bufferdetexte.remove_tag_by_name(nom_balise, debut, fin)

Pour retirer toutes les balises d'une portion de texte, on utilisera :

bufferdetexte.remove_all_tags(debut, fin)

XIII-C-6 - Insérer des images et des widgets

En plus du texte, un TextBuffer peut contenir des images pixbuf et un point d'ancrage pour widgets. Ce point d'ancrage sert à ajouter un widget à une TextViewTextView. Un widget différent peut être ajouté dans chaque TextView qui affiche un buffer contenant un point d'ancrage.

On peut insérer une image pixbuf avec la méthode suivante :

bufferdetexte.insert_pixbuf(iter, pixbuf)

dans laquelle iter précise l'emplacement du buffer où insérer l'image pixbuf. Cette image comptera pour un caractère et sera représentée par le caractère Unicode "0xFFFC" dans le texte retourné par la méthode get_slice() (il ne figurera pas dans celui retourné par get_text()).

On peut insérer un widget GTK+ dans une TextView, ` un emplacement du buffer spécifié par un TextChildAnchor (point d'ancrage pour enfant). Tout comme une image pixbuf, le TextChildAnchor comptera pour un caractère représenté par "0xFFFC".

Le moyen le plus commode de créer et d'insérer un TextChildAnchor est la méthode suivante :

ancrage = bufferdetexte.create_child_anchor(iter)

où iter est l'emplacement destiné à recevoir le point d'ancrage.

Mais on peut aussi créer et insérer le TextChildAnchor en deux temps ainsi :

1. ancrage = gtk.TextChildAnchor()

3. bufferdetexte.insert_child_anchor(iter, ancrage)

Puis l'on peut ajouter le widget à la TextView, au point d'ancrage, grâce à la méthode suivante :

zonedetexte.add_child_at_anchor(widget_enfant, ancrage)

La méthode qui suit permet de récupérer la liste des widgets attachés à un point d'ancrage :

liste_widgets = ancrage.get_widgets()

On peut également ajouter un widget à une TextView en utilisant cette méthode :

zonedetexte.add_child_in_window(widget_enfant, fenêtre, position_x, position_y)

où le widget_enfant sera placé dans la fenêtre fenêtre à la positon spécifiée par position_x et position_y. fenêtre indique laquelle des fenêtres qui composent la TextView doit recevoir le widget enfant :

1. gtk.TEXT_WINDOW_TOP       # fenêtre supérieure

2. gtk.TEXT_WINDOW_BOTTOM    # fenêtre inférieure

3. gtk.TEXT_WINDOW_LEFT      # fenêtre de gauche

4. gtk.TEXT_WINDOW_RIGHT     # fenêtre de droite

5. gtk.TEXT_WINDOW_TEXT      # fenêtre du texte

6. gtk.TEXT_WINDOW_WIDGET    # fenêtre du widget

XIII-D - TextIter : les itérateurs de texte

Les TextIter représentent une position entre deux caractères d'un TextBufferTextBuffer. Ils sont généralement créés par une méthode de TextBuffer et sont invalidés lorsque le nombre de caractères de ce dernier est modifié (excepté le TextIter utilisé pour l'insertion ou la suppression). Insérer ou supprimer des images pixbuf ou des points d'ancrage consiste aussi en une modification du buffer, et invalide par conséquent les TextIter.

L'objet TextIter possède un grand nombre de méthodes. Celles-ci sont groupées par fonction similaire dans les sections suivantes.

XIII-D-1 - Récupérer les attributs d'un TextIter

On peut récupérer le TextBuffer qui contient le TextIter iter avec la méthode :

buffer = iter.get_buffer()

Les méthodes suivantes permettent de récupérer la localisation du TextIter iter dans le TextBuffer :

1. num_car = iter.get_offset()            # renvoie la position de iter en partant du début du buffer

3. num_ligne = iter.get_line()            # renvoie le numéro de la ligne de iter

5. num_car_ligne = iter.get_line_offset()    # renvoie la position de iter en partant du début de la ligne

7. nbr_car_ligne = iter.get_chars_in_line()    # renvoie le nombre de caractères dans la ligne de iter

XIII-D-2 - Récupérer les attributs d'un texte portant un TextIter

Le PangoLanguage utilisé à un emplacement du buffer portant un itérateur peut s'obtenir avec la méthode suivante :

langage = iter.get_language()

Plus généralement, on fera appel à la méthode qui suit pour récupérer les attributs d'un texte à une position portant un TextIter :

resultat = iter.get_attributes(valeurs)

La variable resultat indiquera si les valeurs fournies en argument (qui doivent être des objets TextAttributes) ont été modifiées. On obtient ces valeurs par l'intermédiaire de la méthode de TextView suivante :

valeurs = zonedetexte.get_default_attributes()

où zonedetexte est la TextView en question.

On peut accéder aux attributs suivants à partir d'un objet TextAttributes (non implémenté dans PyGTK <= 1.99.15) :

bg_color

couleur de l'arrière-plan

fg_color

couleur du premier plan

bg_stipple

bitmap à utiliser comme masque pour l'arrière-plan

fg_stipple

bitmap à utiliser comme masque pour le premier plan

rise

décalage du texte au-dessus de la ligne de base

underline

style de soulignement

strikethrough

texte barré

draw_bg

TRUE si des balises affectent le dessin de l'arrière-plan

justification

type d'alignement

direction

direction du texte

font

PangoFontDescription utilisée

font_scale

échelle de la police utilisée

left_margin

largeur de la marge de gauche

right_margin

largeur de la marge de droite

pixels_above_lines

espacement en pixels au-dessus des paragraphes

pixels_below_lines

espacement en pixels au-dessous des paragraphes

pixels_inside_wrap

espacement en pixels entre les lignes coupées dans un paragraphe

tabs

PangoTabArray utilisé

wrap_mode

mode de retour à la ligne automatique utilisé

language

PangoLanguage utilisé

invisible

texte invisible (non implémenté dans GTK+ 2.0)

bg_full_height

hauteur de l'arrière-plan par rapport à la ligne

editable

possibilité d'édition du texte

realized

texte réalisé

pad1

pad2

pad3

pad4

XIII-D-3 - Copier un TextIter

Il est possible de dupliquer un TextIter grâce à la méthode :

copie_iter = iter.copy()

XIII-D-4 - Récupérer du texte et des objets

On peut récupérer différentes quantités de texte et d'objets de TextBuffer en utilisant les méthodes suivantes :

1. caract = iter.get_char()            # renvoie le caractère qui suit iter (ou 0 si l'on est à la fin du buffer)

3. texte = debut.get_slice(fin)            # renvoie le texte compris entre les itérateurs debut et fin

5. texte = debut.get_text(fin)            # renvoie le texte compris entre les itérateurs debut et fin

7. pixbuf = iter.get_pixbuf()            # renvoie l'image pixbuf située à l'itérateur iter (ou None)

9. ancrage = iter.get_child_anchor()        # renvoie le point d'ancrage situé à l'itérateur iter(ou None)

11. liste_marques = iter.get_marks()        # renvoie une liste de marques

13. liste_balises = iter.get_toggled_tags()    # renvoie une liste de balises activées ou désactivées

15. liste_balises = iter.get_tags()        # renvoie une liste de balises par priorités

XIII-D-5 - Vérifier les balises à une position de TextIter

Les méthodes suivantes permettent de vérifier certaines caractéristiques des balises se trouvant à une position de TextIter :

1. resultat = iter.begins_tag(tag=None)    # renvoie TRUE si tag s'active à l'emplacement iter

3. resultat = iter.ends_tag(tag=None)    # renvoie TRUE si tag se désactive à l'emplacement iter

5. resultat = iter.toggles_tag(tag=None)    # renvoie TRUE si tag s'active ou se désactive à l'emplacement iter

7. resultat = iter.has_tag(tag)        # renvoie TRUE si la portion de texte contenant iter est balisée par tag

Ces méthodes renvoient TRUE si la balise tag à la position iter remplit la condition. Si l'argument tag vaut None aux trois premières méthodes, le résultat sera TRUE si n'importe quelle balise remplit la condition à la position iter.

Les deux méthodes qui suivent indiquent si le texte situé à la position spécifiée par le TextIter est éditable ou accepte une insertion de texte :

1. resultat = iter.editable()

3. resultat = iter.can_insert(defaut_editable)

La méthode editable() indique si l'itérateur iter se situe dans une portion de texte éditable. La méthode can_insert() indique si l'on peut insérer du texte à l'emplacement spécifié par iter, en prenant en compte la possibilité d'édition par défaut de la TextView, du TextBuffer et des balises applicables. L'argument defaut_editable est généralement déterminé par un appel à la méthode de TextView :

defaut_editable = zonedetexte.get_editable()

où zonedetexte est la TextView.

On peut savoir si deux TextIter sont équivalents grâce à la méthode :

egalite = iter1.equal(iter2)

La méthode suivante permet de comparer deux TextIter :

resultat = iterA.compare(iterB)

resultat aura pour valeur : -1 si iterA est inférieur à iterB ; 0 si iterA égale iterB ; et 1 si iterA est supérieur à iterB.

Pour savoir si un TextIter se trouve entre deux TextIter donnés, utilisez la méthode :

resultat = iter.in_range(debut, fin)

resultat vaudra TRUE si iter est situé entre debut et fin. Attention, debut et fin doivent être donnés dans l'ordre croissant ! Ceci peut être assuré par la méthode :

iterA.order(iterB)

qui réorganise les positions des TextIter de sorte que iterA soit avant iterB.

XIII-D-6 - Vérifier la position dans le texte

On peut déterminer la position d'un TextIter par rapport au texte d'un TextBuffer grâce aux méthodes suivantes :

1. resultat = iter.starts_word()            # iter commence un mot

3. resultat = iter.ends_word()            # iter termine un mot

5. resultat = iter.inside_word()            # iter est à l'intérieur d'un mot

7. resultat = iter.starts_sentence()        # iter commence une phrase

9. resultat = iter.ends_sentence()        # iter termine une phrase

11. resultat = iter.inside_sentence()        # iter est à l'intérieur d'une phrase

13. resultat = starts_line()            # iter commence une ligne

15. resultat = iter.ends_line()            # iter termine une ligne

resultat renvoie TRUE si le TextIter est à la position donnée. La définition de "mot" et "phrase" dépend du langage utilisé à la position du TextIter. Quant à la ligne, il s'agit d'un ensemble de phrases s'apparentant à un paragraphe.

La méthode suivante permet de déterminer si un TextIter se situe au début ou à la fin du TextBuffer :

1. resultat = iter.is_start()

3. resultat = iter.is_end()

resultat vaudra TRUE si le TextIter est situé au début ou à la fin du TextBuffer.

Du fait qu'un TextBuffer peut contenir des groupes de caractères s'affichant comme une seule position de curseur (par exemple, un retour chariot + saut de ligne, ou une lettre + marque d'accent), il est possible qu'un TextIter soit à un emplacement ne représentant pas une position de curseur possible. Dans un tel cas, la méthode suivante renverrait FALSE :

resultat = iter.is_cursor_position()

XIII-D-7 - Se déplacer dans le texte

On peut déplacer les TextIter dans un TextBuffer par sauts de différents éléments de texte. La définition de ces éléments de texte est fixée par le PangoLanguage utilisé à l'emplacement du TextIter. Les méthodes basiques sont :

1. resultat = iter.forward_char()            # avancer d'un caractère

3. resultat = iter.backward_char()            # reculer d'un caractère

5. resultat = iter.forward_word_end()            # avancer à la fin du mot

7. resultat = iter.backward_word_start()            # reculer au début du mot

9. resultat = iter.forward_sentence_end()        # avancer à la fin de la phrase

11. resultat = iter.backward_sentence_start()        # reculer à au début de la phrase

13. resultat = iter.forward_line()            # avancer au début de la prochaine ligne

15. resultat = iter.backward_line()            # reculer au début de la ligne précédente

17. resultat = iter.forward_to_line_end()            # avancer à la fin de la ligne

19. resultat = iter.forward_cursor_position()        # avancer à la prochaine position de curseur

21. resultat = iter.backward_cursor_position()        # reculer à la position de curseur précédente

resultat vaut TRUE si le TextIter a été déplacé et FALSE s'il se trouve au début ou à la fin du TextBuffer.

Toutes les méthodes ci-dessus (hormis forward_to_line_end()) ont un équivalent avec argument. Ce dernier est un nombre (positif ou négatif) indiquant de combien d'unités doit se déplacer le TextIter :

1. resultat = iter.forward_chars(combien)

3. resultat = iter.backward_chars(combien)

5. resultat = iter.forward_word_ends(combien)

7. resultat = iter.backward_word_starts(combien)

9. resultat = iter.forward_sentence_ends(combien)

11. resultat = iter.backward_sentence_starts(combien)

13. resultat = iter.forward_lines(combien)

15. resultat = iter.backward_lines(combien)

17. resultat = iter.forward_cursor_positions(combien)

19. resultat = iter.backward_cursor_positions(combien)

XIII-D-8 - Se déplacer à un emplacement spécifique

Un TextIter peut être déplacé à un emplacement spécifique du TextBuffer avec les méthodes suivantes :

1. iter.set_offset(n)        # déplace iter juste après le nième caractère du buffer

3. iter.set_line(x)        # déplace iter au début de la xi ligne

5. iter.set_line_offset(n)    # déplace iter juste après le nième caractère de sa ligne

7. iter.forward_to_end()        # déplace iter à la dernière position du buffer

Un TextIter peut également être déplacé vers un emplacement où une balise s'active ou se désactive, et ce, grâce à la méthode suivante :

1. resultat = iter.forward_to_tag_toggle(balise)

3. resultat = iter.backward_to_tag_toggle(balise)

resultat vaudra TRUE si le TextIter a été déplacé vers une nouvelle position où balise s'active ou se désactive. Si balise vaut None, la méthode déplacera TextIter vers la prochaine position où n'importe quelle balise s'active ou se désactive.

XIII-D-9 - Rechercher du texte

La méthode suivante permet de rechercher une chaine de caractères dans un TextBuffer :

1. debut_resultat, fin_resultat = iter.forward_search(chaine, drapeaux, limit=None)

3. debut_resultat, fin_resultat = iter.backward_search(chaine, drapeaux, limit=None)

La méthode renvoie un tuple contenant deux TextIter. Ces derniers indiquent respectivement la position du premier caractère du résultat et celle du premier caractère suivant ce résultat. chaine est la chaine de caractères à localiser. Les drapeaux modifient les paramètres de la recherche ; ils peuvent prendre comme valeurs :

1. gtk.TEXT_SEARCH_VISIBLE_ONLY        # les caractères invisibles seront ignorés

3. gtk.TEXT_SEARCH_TEXT_ONLY        # les images pixbuf et les points d'ancrage seront ignorés

limit est un TextIter facultatif qui limite la recherche au texte le précédant.

XIII-E - TextMark : les marques de texte

Une TextMark indique une position entre deux caractères d'un TextBufferTextBufferTextBuffer, position qui sera préservée en cas de modification du buffer. Pour créer, déplacer, ou supprimer des TextMark, on utilise les méthodes de TextBufferTextBuffer décrites dans la section TextBuffer.

Un TextBuffer possède deux TextMark inhérentes, nommées insert et selection_bound, qui font respectivement référence à la position du point d'insertion et à celle de la fin de la sélection (elles peuvent désigner la même position).

On peut récupérer le nom d'une TextMark avec la méthode :

nom_marque = marque.get_name()

Par défaut, seule la marque insert est visible. La visibilité d'une marque (une barre verticale) peut être définie et récupérée en utilisant respectivement les méthodes suivantes :

1. marque.set_visible(choix)

3. visible = marque.get_visible()

où visible vaut TRUE si la marque est visible.

On récupère le TextBuffer qui contient une TextMark donnée avec la méthode :

buffer = marque.get_buffer()

On peut savoir si une TextMark a été supprimée en faisant appel à la méthode :

marque_suppr = marque.get_deleted()

La gravité d'une TextMark peut être récupérée par la méthode :

gravite = marque.get_left_gravity()

La gravité d'une marque indique la situation de celle-ci après que l'on aura inséré du texte à sa position. Si la gravite vaut TRUE, la marque sera à gauche du texte inséré ; si elle vaut FALSE, elle sera à droite.

XIII-F - TextTag et TextTagTable : les balises de texte

Les TextTag (balises de texte) spécifient les attributs qui peuvent être appliqués à une portion du texte d'un TextBufferTextBufferTextBufferTextBuffer. Chaque TextBufferTextBufferTextBuffer a une TextTagTable (table des balises de texte) qui contient les balises pouvant être appliquées à l'intérieur du TextBufferTextBuffer. Les TextTagTables peuvent être partagées par plusieurs TextBuffer, offrant par là une meilleure homogénéité des styles de texte.

XIII-F-1 - Les TextTag

Les TextTag peuvent porter un nom ou bien être anonymes. On crée une TextTag en utilisant la fonction :

balise = gtk.TextTag(name=None)

Si aucun name (nom) n'est donné ou s'il vaut None, la balise sera anonyme. Il peut être plus commode de créer des TextTag avec la méthode de TextBuffer create_tag(), celle-ci permettant aussi de spécifier les attributs de la balise et de l'ajouter à la table des balises du buffer (cf. la section TextBuffer)

Les attributs que peut porter une TextTag sont :

name

Lecture / Écriture

Nom de la balise de texte. None si anonyme.

background

Écriture

Couleur de l'arrière-plan donnée sous forme de chaine de caractères.

foreground

Écriture

Couleur du premier plan donnée sous forme de chaine de caractères.

background_gdk

Lecture / Écriture

Couleur de l'arrière-plan donnée sous forme de GdkColor.

foreground_gdk

Lecture / Écriture

Couleur du premier plan donnée sous forme de GdkColor.

background_stipple

Lecture / Écriture

Bitmap à utiliser comme masque lorsque l'on dessine le texte d'arrière-plan.

foreground_stipple

Lecture / Écriture

Bitmap à utiliser comme masque lorsque l'on dessine le texte du premier plan.

font

Lecture / Écriture

Description de la police donnée sous forme de chaine de caractères. Ex. : "Sans Italic 12".

font_desc

Lecture / Écriture

Description de la police donnée sous forme de PangoFontDescription.

family

Lecture / Écriture

Nom de la famille de la police. Ex. : Sans, Helvetica, Times, Monospace.

style

Lecture / Écriture

Style de la police donné sous forme de PangoStyle. Ex. : pango.STYLE_ITALIC.

variant

Lecture / Écriture

Variante de police donnée sous forme de PangoVariant. Ex. : pango.VARIANT_SMALL_CAPS.

weight

Lecture / Écriture

Épaisseur de police donnée sous forme d'entier (voir aussi les valeurs prédéfinies dans PangoWeight, ex. : pango.WEIGHT_BOLD).

stretch

Lecture / Écriture

Étirement de la police donné sous forme de PangoStretch. Ex. : pango.STRETCH_CONDENSED.

size

Lecture / Écriture

Taille de la police en unités Pango.

size_points

Lecture / Écriture

Taille de la police en points.

scale

Lecture / Écriture

Taille de la police donnée sous forme de facteur d'échelle par rapport à la taille de la police par défaut. Recommandé, car très bonne adaptation aux changements de thème, etc. Pango possède quelques échelles prédéfinies, comme pango.SCALE_X_LARGE.

pixels_above_lines

Lecture / Écriture

Espacement au-dessus des paragraphes en pixels.

pixels_below_lines

Lecture / Écriture

Espacement au-dessous des paragraphes en pixels.

pixels_inside_wrap

Lecture / Écriture

Espacement entre les lignes des paragraphes en pixels.

editable

Lecture / Écriture

Possibilité pour l'utilisateur d'éditer le texte.

wrap_mode

Lecture / Écriture

Mode de retour à la ligne automatique (après le dernier mot de la ligne, après le dernier caractère de la ligne, ou désactivé).

justification

Lecture / Écriture

Alignement à gauche, à droite, ou centré.

direction

Lecture / Écriture

Direction du texte (de gauche à droite ou de droite à gauche).

left_margin

Lecture / Écriture

Largeur de la marge de gauche en pixels.

indent

Lecture / Écriture

Indentation du paragraphe en pixels.

strikethrough

Lecture / Écriture

Indique si le texte doit être barré.

right_margin

Lecture / Écriture

Largeur de la marge de droite en pixels.

underline

Lecture / Écriture

Style de soulignement.

rise

Lecture / Écriture

Décalage du texte par rapport à la ligne de base (au-dessus s'il est positif, au-dessous si négatif) en pixels.

background_full_height

Lecture / Écriture

Indique si la couleur d'arrière-plan doit remplir toute la hauteur de la ligne ou seulement la hauteur des caractères balisés.

language

Lecture / Écriture

La langue du texte, donnée sous forme de code ISO. Ceci peut aider Pango pour le rendu du texte. Si vous ne comprenez pas ce paramètre, vous n'en avez probablement pas besoin.

tabs

Lecture / Écriture

Tabulations personnalisées.

invisible

Lecture / Écriture

Visibilité du texte (non implémentée dans GTK 2.0)

On peut définir les attributs d'une balise avec la méthode :

balise.set_property(nom_attr, valeur)

dans laquelle nom_attr est une chaine contenant le nom de l'attribut et valeur la valeur qui doit lui être affectée.

De la même manière, on peut récupérer la valeur de l'attribut avec la méthode :

valeur = balise.get_property(nom_attr)

Du fait qu'une balise ne contient pas forcément tous les attributs précédemment cités, un certain nombre de propriétés booléennes ont été créées, qui permettent simplement de savoir si la balise est porteuse ou pas de tel attribut :

background_set

Lecture / Écriture

foreground_set

Lecture / Écriture

background_stipple_set

Lecture / Écriture

foreground_stipple_set

Lecture / Écriture

family_set

Lecture / Écriture

style_set

Lecture / Écriture

variant_set

Lecture / Écriture

weight_set

Lecture / Écriture

stretch_set

Lecture / Écriture

size_set

Lecture / Écriture

scale_set

Lecture / Écriture

pixels_above_lines_set

Lecture / Écriture

pixels_below_lines_set

Lecture / Écriture

pixels_inside_wrap_set

Lecture / Écriture

editable_set

Lecture / Écriture

wrap_mode_set

Lecture / Écriture

justification_set

Lecture / Écriture

direction_set

Lecture / Écriture

left_margin_set

Lecture / Écriture

indent_set

Lecture / Écriture

strikethrough_set

Lecture / Écriture

right_margin_set

Lecture / Écriture

underline_set

Lecture / Écriture

rise_set

Lecture / Écriture

background_full_height_set

Lecture / Écriture

language_set

Lecture / Écriture

tabs_set

Lecture / Écriture

invisible_set

Lecture / Écriture

Pour récupérer un attribut d'une balise, vous devez donc d'abord vérifier s'il est bien défini dans cette balise. Par exemple, pour obtenir un attribut valide d'alignement, vous pourriez procéder comme suit :

if balise.get_property("justification_set"):
alignement = balise.get_property("justification")

Par défaut, la priorité des balises correspond à l'ordre dans lequel elles sont ajoutées à la TextTagTable. La balise qui a la plus haute priorité prend l'avantage dans le cas où plusieurs tentent de définir le même attribut pour une portion de texte. La priorité se récupère et se définit avec les méthodes suivantes :

1. priorite = balise.get_priority()

3. balise.set_priority(priorite)

La priorité d'une balise doit être comprise entre 0 et la taille de la TextTagTable moins 1.

XIII-F-2 - La TextTagTable

Par défaut, une TextTagTable est créée avec la création d'un TextBuffer. Mais elle peut tout aussi bien être créée par la fonction :

table = TextTagTable()

On ajoute une TextTag a une TextTagTable avec la méthode :

table.add(balise)

La balise ne doit pas déjà être dans la table et ne doit pas porter le même nom qu'une balise s'y trouvant déjà.

On peut chercher une TextTag dans une TextTagTable en utilisant la méthode :

balise = table.lookup(nom)

La méthode renvoie la balise qui porte ce nom dans la table, ou None si aucune balise ne porte ce nom.

On peut retirer une TextTag d'une TextTagTable avec la méthode :

table.remove(balise)

Enfin, la taille d'une TextTagTable peut être récupérée par :

taille_table = table.get_size()

XIII-G - Un exemple de TextView

Le programme testtexte.py (dérivé du programme testtext.c inclus dans la distribution GTK+ 2.0.x) montre un exemple d'utilisation du widget TextView et des objets qui lui sont associés : TextBuffer, TextIter, TextMark, TextTag, et TextTagTable. La Figure 13.2 illustre le résultat :

Figure 13.2. Exemple de TextView

En plus de TestTexte, la classe principale de l'application, le programme testtexte.py définit plusieurs autres classes :

- la classe Buffer, aux lignes 102 à 499, est une sous-classe dérivée de gtk.TextBuffer. Elle apporte les fonctions d'édition du buffer utilisées par les objets Fenetre ;

- la classe Fenetre, de la ligne 501 à la ligne 1129, est une sous-classe dérivée de gtk.Window. Elle englobe une gtk.TextView, qui utilise un objet Buffer au lieu d'un gtk.TextBuffer. Elle fournit une fenêtre équipée d'une barre de menu, et y affiche le contenu d'un objet Buffer ;

- la classe SelectionFichier des lignes 76 à 100, est une sous-classe dérivée de gtk.FileSelection, qui permet de sélectionner des noms de fichiers pour le contenu du Buffer ;

- Enfin, la classe Pile crée des piles assez simples.

L'alternance des couleurs est implémentée en utilisant des balises que l'on applique à une portion de texte d'un buffer. Ces balises sont créées aux lignes 112 à 118 par la méthode __init__(). Puis, la méthode change_appliquer_couleurs() se charge de les appliquer à une portion de texte, et ce tous les deux caractères (lignes 766 à 787). Les lignes 205 à 242 fournissent les méthodes (cycle_couleurs(), couleurs() et alterner_couleurs()) qui produisent l'alternance une fois celle-ci activée. Cette activation s'effectue à la ligne 223 en définissant la propriété foreground_gdk de chaque balise de balises_couleurs (ce qui définit par la même occasion la propriété foreground_set). On désactive cette même alternance de couleurs en donnant la valeur FALSE à la propriété foreground_set (ligne 225). On alterne les couleurs périodiquement en modifiant la variable teinte_depart (ligne 240).

Lorsque la commande Test->Exemple est sélectionnée, un nouveau Buffer est rempli avec un exemple de texte (il s'agit de la méthode remplir_buffer_exmpl() des lignes 305 à 375). Le buffer contient alors du texte de différentes couleurs, styles et langages, ainsi que des images pixbuf. La méthode init_balises() des lignes 263 à 303 définit un ensemble de TextTag qui sont utilisés par le texte d'exemple. Le signal "event" (événement) de ces balises est connecté à la méthode gest_evnmt_balises (lignes 243 à 259) pour illustrer la capture de différents événements (mouvement, clics).

On règle le type de retour à la ligne automatique de la TextViewTextView sur WRAP_WORD (ligne 583) et on affiche ses fenêtres de bordure en fixant leurs dimensions (590-591 et 599-600). Les fenêtres de bordure supérieure et inférieure serviront à afficher les positions des tabulations lorsque les tabulations personnalisées sont sélectionnées, et les fenêtres latérales à afficher les numéros des lignes. Les fenêtres de bordures sont rafraîchies lorsqu'un signal "expose-event" (besoin de réaffichage) est reçu par la TextView (lignes 593 et 602). La méthode affichage_num_lignes() (lignes 1082 à 1119) détermine si le signal "expose-event" provient d'une des fenêtres de bordure latérales, auquel cas elle calcule la taille de la zone à réafficher. Puis l'emplacement du début de la ligne ainsi que le numéro de chaque ligne de cette zone sont calculés par la méthode recup_lignes() (lignes 1060-1080). Les numéros de ligne sont alors affichés dans la fenêtre de bordure, à l'emplacement adéquat (transformé par la ligne 1112).

Les emplacements des tabulations personnalisées apparaissent dans les fenêtres supérieure et inférieure (lignes 1016 à 1058), mais seulement lorsque le curseur se situe dans une portion de texte portant l'attribut "tabulations personnalisées". Pour vérifier cette condition, on traite le signal "mark-set" (marque définie) avec la méthode rappel_mvt_curseur() (lignes 1002 à 1014), qui invalide ces deux fenêtres si insert est la marque définie.

Des objets mobiles sont ajoutés à une Fenetre par la méthode ajouter_enfants() (lignes 895 à 902), qui appelle la méthode ajouter_enfants_deplace() (lignes 877-893). Les enfants sont des gtk.Label que l'on peut faire glisser à l'intérieur des différentes fenêtres composant une TextView.

De la même manière, des widgets sont insérés dans le buffer ainsi que dans les différentes fenêtres d'un objet Fenetre en faisant appel à la méthode ajouter_enfants_focus (lignes 904 à 952).

XIV - Chapitre 14. Le widget TreeView

Le widget TreeView affiche des listes et des arborescences sur plusieurs colonnes. Il remplace les anciens widgets List, CList, Tree and CTree par un ensemble beaucoup plus puissant et flexible d'objets utilisant le principe Modèle-Vue-Contrôleur (MVC). Il propose :

- deux modèles prédéfinis : un pour les listes et un pour les arbres ;

- l'actualisation automatique des différentes vues du même modèle lorsque celui-ci est modifié ;

- l'affichage sélectif des données du modèle ;

- l'utilisation des données du modèle pour personnaliser l'affichage du TreeView ligne par ligne ;

- des objets prédéfinis de rendu des données pour l'affichage de texte, d'images ou de données booléennes ;

- des modèles empilables afin de permettre des vues classées et filtrées des données du modèle inférieur ;

- la possibilité de réordonner et de redimensionner les colonnes ;

- la possibilité d'ordonner automatiquement les lignes du TreeView par un clic dans l'en-tête des colonnes ;

- le support du glisser-déposer ;

- le support de modèles personnels entièrement écrits en Python ;

- le support de cell-renderers personnels entièrement écrits en Python.

La puissance de ce nouvel ensemble d'objets et d'interfaces a pour contrepartie une complexité indéniablement plus importante, pouvant même paraître insurmontable au début. Dans ce chapitre, les objets et les interfaces du TreeView seront présentés de manière à pouvoir en faire un usage courant. Nous vous laisserons le soin d'explorer par vous-même leurs aspects plus ésotériques.

Nous commencerons par un bref aperçu de ces objets et interfaces, avant de nous plonger dans l'interface TreeModel et dans les classes prédéfinies ListStore et TreeStore.

XIV-A - Présentation

Le widget TreeView est l'objet de l'interface utilisateur qui se charge d'afficher les données stockées dans un objet implémentant l'interface TreeModel. Deux classes TreeModel de base sont définies dans PyGTK 2.0 :

- le TreeStore, qui permet un stockage hiérarchique des données, organisé en lignes d'arborescence comportant des données dans des colonnes. Chaque ligne d'arborescence peut avoir zéro ou plusieurs lignes filles, et toutes les lignes doivent avoir le même nombre de colonnes ;

- le ListStore, qui permet un stockage tabulaire des données, organisé en lignes et en colonnes, de la même manière qu'un tableau dans une base de données relationnelle. Le ListStore est en réalité une version simplifiée du TreeStore dont les lignes n'ont pas de lignes filles. Il fut créé afin d'offrir une interface plus simple (et certainement plus efficace) de ce modèle commun de données.

Les deux autres TreeModel s'ajoutent par dessus (ou s'interposent entre) les modèles de base :

- le TreeModelSort est un modèle dans lequel les données du TreeModel inférieur sont maintenues dans un ordre donné ;

- le TreeModelFilter est un modèle contenant un sous-ensemble des données du modèle inférieur. Notez que ce modèle n'est disponible que dans PyGTK 2.4 et supérieurs.

Un TreeView affiche toutes les lignes d'un TreeModel, mais pas forcément toutes les colonnes. Ces dernières peuvent en outre ne pas être présentées dans le même ordre que dans le TreeModel.

Le TreeView utilise des TreeViewColumn pour organiser l'affichage des données des colonnes. Chaque TreeViewColumn affiche une colonne (avec ou sans en-tête) pouvant contenir les données de plusieurs colonnes de TreeModel. Dans chaque TreeViewColumn sont placés (comme dans des conteneurs HBox) des CellRenderer, qui prennent en charge l'affichage des données correspondantes en provenance d'un emplacement ligne-colonne dans un TreeModel. Trois classes CellRenderer sont prédéfinies :

- le CellRendererPixbuf, qui prend en charge l'affichage d'une image pixbuf dans les cellules d'un TreeViewColumn ;

- le CellRendererText, qui prend en charge l'affichage d'une chaine de texte dans les cellules d'un TreeViewColumn. Si les données des colonnes ne sont pas des chaines de texte, il effectue la conversion. Par exemple, pour afficher une colonne de modèle contenant une donnée à virgule flottante (float), le CellRendererText la convertira en chaine avant de l'afficher ;

- Le CellRendererToggle affiche une valeur booléenne dans les cellules d'un TreeViewColumn sous la forme d'un bouton à bascule.

Un TreeViewColumn peut contenir plusieurs CellRenderer pour, par exemple, afficher de l'image et du texte dans une même colonne.

Enfin, les objets TreeIter, TreeRowReference et TreeSelection sont respectivement un pointeur temporaire vers une ligne d'un TreeModel, un pointeur permanent vers une ligne d'un TreeModel et un objet gérant les sélections dans un TreeView.

Pour afficher un TreeView, on effectuera les opérations suivantes (sans nécessairement respecter l'ordre proposé) :

- création d'un objet TreeModel, généralement un ListStore ou un TreeStore, avec une ou plusieurs colonnes d'un type spécifique de données ;

- placement éventuel d'une ou plusieurs lignes de données dans le TreeModel ;

- création d'un widget TreeView et association de ce dernier avec le TreeModel ;

- création d'un ou plusieurs TreeViewColumn et insertion d'un ou de ces derniers dans le TreeView. Chacun représentera une colonne affichée. ;

- création, pour chaque TreeViewColumn, d'un ou plusieurs CellRenderer qui y sont ensuite placés ;

- définition des attributs de chaque CellRenderer de manière à indiquer dans quelle colonne du TreeModel l'on doit récupérer les données de l'attribut. Par exemple, le texte à afficher. Cela permet au CellRenderer d'afficher différemment chaque colonne d'une ligne ;

- insertion et affichage du TreeView dans une fenêtre (gtk.Window) ou une fenêtre à défilement (gtk.ScrolledWindow) ;

- programmation des manipulations de données à effectuer en réponse aux actions de l'utilisateur. Le TreeView doit suivre automatiquement les modifications.

Le programme d'exemple treeviewbasique.py illustre la création et l'affichage d'un TreeView simple.

1. #!/usr/bin/env python

3. # exemple treeviewbasique.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class ExempleTreeViewBasique:

11. # fermeture de la fenêtre et sortie du programme

12. def evnmt_delete(self, widget, evnmt, donnees=None):

13. gtk.main_quit()

14. return False

16. def __init__(self):

17. # Création d'une nouvelle fenêtre

18. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

20. self.fenetre.set_title("Exemple TreeView simple")

22. self.fenetre.set_size_request(200, 200)

24. self.fenetre.connect("delete_event", self.evnmt_delete)

26. # Création d'un TreeStore avec une colonne de type chaine, pour servir de modèle

27. self.treestore = gtk.TreeStore(str)

29. # Ajoutons des données : 4 lignes ayant 3 lignes filles chacune

30. for mere in range(4):

31. m_iter = self.treestore.append(None, ['ligne mere %i' % mere])

32. for fille in range(3):

33. self.treestore.append(m_iter, ['ligne fille %i de la ligne %i'

34. % (fille, mere)])

36. # création du TreeView en utilisant notre TreeStore

37. self.treeview = gtk.TreeView(self.treestore)

39. # création du TreeViewColumn pour afficher les données

40. self.tvcolumn = gtk.TreeViewColumn('Column 0')

42. # on place tvcolumn dans notre TreeView

43. self.treeview.append_column(self.tvcolumn)

45. # création d'un CellRendererText pour afficher les données

46. self.cell = gtk.CellRendererText()

48. # on place cell dans le TreeViewColumn et on lui permet de s'étirer

49. self.tvcolumn.pack_start(self.cell, True)

51. # réglage de l'attribut "text" de cell sur la colonne 0 - récupère le

52. # texte dans cette colonne du TreeStore

53. self.tvcolumn.add_attribute(self.cell, 'text', 0)

55. # on autorise la recherche

56. self.treeview.set_search_column(0)

58. # on autorise le classement de la colonne

59. self.tvcolumn.set_sort_column_id(0)

61. # on autorise le classement des lignes par glisser-déposer

62. self.treeview.set_reorderable(True)

64. self.fenetre.add(self.treeview)

66. self.fenetre.show_all()

68. def main():

69. gtk.main()

71. if __name__ == "__main__":

72. exempletv = ExempleTreeViewBasique()

73. main()

Dans de vrais programmes, les données seraient probablement fournies au TreeStore après l'affichage du TreeView, par une action de l'utilisateur. Nous étudierons en détail les interfaces du TreeView dans les prochaines sections. La Figure 14.1, « Programme d'exemple de TreeView simple » montre la fenêtre créée par le programme treeviewbasique.py après que quelques lignes ont été développées.

Figure 14.1. Programme d'exemple de TreeView simple

Examinons à présent l'interface TreeModel et les modèles qui l'implémentent.

XIV-B - L'interface TreeModel et le stockage des données

XIV-B-1 - Introduction

Toutes les sous-classes TreeModel implémentent l'interface TreeModel. Elle fournit des méthodes pour :

- récupérer les caractéristiques du modèle, telles que le nombre de colonnes et le type de données dans une colonne ;

- récupérer un TreeIter (une référence temporaire pointant sur une ligne du modèle) ;

- récupérer des informations sur un nœud (ou une ligne) comme une liste de ses nœuds enfants, leur nombre, le contenu de ses colonnes ou un pointeur sur son nœud parent ;

- obtenir des notifications de changement des données du TreeModel

XIV-B-2 - Créer des objets TreeStore et ListStore

Les classes de base de stockage des données, le ListStore et le TreeStore, permettent de définir et de manipuler les lignes et les colonnes de données dans le modèle. Dans les constructeurs de ces deux objets, les colonnes devront être déclarées comme étant de l'un des types suivants :

- un type Python, comme les types prédéfinis int, str, long, float et object ;

- un type PyGTK, comme les Button, VBox, gdk.Rectangle, gdk.Pixbuf ;

- un type GObject (les GTypes de GTK+), spécifié sous la forme soit d'une constante gobject.TYPE, soit d'une chaine de caractères. La majorité des GTypes sont mappés sur un type Python :

- gobject.TYPE_CHAR ou 'gchar',

- gobject.TYPE_UCHAR ou 'guchar',

- gobject.TYPE_BOOLEAN ou 'gboolean',

- gobject.TYPE_INT ou 'gint',

- gobject.TYPE_UINT ou 'guint',

- gobject.TYPE_LONG ou 'glong',

- gobject.TYPE_ULONG ou 'gulong',

- gobject.TYPE_INT64 ou 'gint64',

- gobject.TYPE_UINT64 ou 'guint64',

- gobject.TYPE_FLOAT ou 'gfloat',

- gobject.TYPE_DOUBLE ou 'gdouble',

- gobject.TYPE_STRING ou 'gchararray',

- gobject.TYPE_OBJECT ou 'Gobject.

Par exemple, la création d'un ListStore ou d'un TreeStore dont les lignes contiendraient un gdk.Pixbuf, un entier, une chaine et un booléen, pourrait ressembler à ceci :

1. liststore = ListStore(gtk.gdk.Pixbuf, int, str, 'gboolean')

3. treestore = TreeStore(gtk.gdk.Pixbuf, int, str, 'gboolean')

Une fois un ListStore ou un TreeStore créé et ses colonnes définies, ils ne peuvent plus être modifiés. Il est également important de savoir qu'il n'y a pas de relation prédéfinie entre les colonnes d'un TreeView et celles de son TreeModel. Ainsi, la cinquième colonne de données d'un TreeModel peut être affichée dans la première colonne d'un TreeView et dans la troisième d'un autre. On n'a donc pas à se soucier de l'affichage des données lorsque l'on crée un modèle.

Si ces deux modèles ne conviennent pas à votre application, il est possible de définir votre propre modèle personnalisé en Python, pourvu qu'il implémente l'interface TreeModel. Nous verrons ceci plus en détail dans la Section 14.11, « Le TreeModel générique ».

XIV-B-3 - Les références aux lignes du modèle

Avant de pouvoir manipuler des lignes de données dans un TreeStore ou un ListStore, il nous faut pouvoir désigner les lignes à traiter. PyGTK offre trois moyens de faire référence à des lignes de TreeModel : le chemin d'accès, le TreeIter et le TreeRowReference.

XIV-B-3-a - Le chemin d'accès

Le chemin d'accès consiste en un entier, une chaine ou un tuple représentant l'emplacement d'une ligne dans le modèle. Une valeur entière indique le numéro d'une ligne dans un modèle en partant de 0 (le premier niveau). Par exemple, un chemin d'accès valant 4 indiquerait la cinquième ligne du modèle. Par comparaison, la même ligne serait désignée par "4" sous forme de chaine et par (4) sous forme de tuple. Si cela suffit effectivement à désigner toutes les lignes d'un ListStore, il nous reste encore à désigner les lignes enfants pour un TreeStore. On n'utilisera pour ce faire que les représentations sous forme de chaine ou de tuple.

La profondeur de l'arborescence d'un TreeStore étant complètement arbitraire, la représentation sous forme de chaine indique le chemin d'accès en partant du premier niveau jusqu'à la ligne voulue, et en séparant chaque valeur par le caractère ":". La représentation par tuple suit le même principe, le tuple étant constitué d'une séquence d'entiers conduisant à la ligne voulue en partant du premier niveau. Par exemple, les chemins "0:2" (troisième ligne enfant de la première ligne) et "4:0:1" (deuxième ligne enfant du premier enfant de la cinquième ligne) sont des représentations valides sous la forme de chaines de caractères. Les équivalents en tuples de ces chemins d'accès sont respectivement (0, 2) et (4, 0, 1).

Le chemin d'accès représente la seule possibilité de mapper une ligne d'un TreeView sur une ligne d'un TreeModel, car leurs chemins d'accès sont identiques. Le chemin d'accès pose toutefois quelques problèmes :

- il peut désigner une ligne qui n'existe pas dans le ListStore ou dans le TreeStore ;

- il peut pointer vers une autre ligne de données après l'insertion ou la suppression d'une ligne dans le ListStore ou dans le TreeStore.

La représentation par tuple est utilisée par PyGTK lors du renvoi de chemins d'accès, mais les trois formes sont acceptées indifféremment en entrée. La représentation par tuple est conseillée dans un souci de cohérence.

On peut récupérer un chemin d'accès à partir d'un TreeIter à l'aide de la méthode get_path() :

chemin = modele.get_path(iter)

où iter est un TreeIter pointant sur une ligne de modele et chemin le chemin d'accès de la ligne sous la forme d'un tuple.

XIV-B-3-b - Les TreeIter

Un TreeIter est un objet offrant une référence temporaire à une ligne de ListStore ou de TreeStore. Si le contenu du modèle est modifié (généralement par l'insertion ou la suppression d'une ligne), le TreeIter peut devenir invalide. Un TreeModel supportant les TreeIter persistants devrait avoir le drapeau gtk.TREE_MODEL_ITERS_PERSIST. Une application peut vérifier la présence de ce drapeau avec la méthode get_flags().

Pour créer un TreeIter, on utilise une des méthodes du TreeModel qui sont applicables aux TreeStore comme aux ListStore :

treeiter = modele.get_iter(path)

où treeiter pointe sur la ligne indiquée par le chemin d'accès path. Si le chemin d'accès est invalide, l'exception ValueError est levée.

treeiter = modele.get_iter_first()

où treeiter est un TreeIter pointant sur la ligne située au chemin (0,). treeiter vaudra None si le modèle est vide.

treeiter = modele.iter_next(iter)

où treeiter est un TreeIter qui pointe sur la ligne suivante, sur le même niveau que le TreeIter indiqué par iter. treeiter vaudra None s'il n'y a pas de ligne suivante (et iter sera invalidé).

Les méthodes qui suivent ne servent à récupérer un TreeIter qu'à partir d'un TreeStore :

treeiter = treestore.iter_children(parent)

où treeiter est un TreeIter pointant sur le premier enfant de la ligne indiquée par le TreeIter parent.treeiter vaudra None s'il n'y pas de ligne enfant.

treeiter = treestore.iter_nth_child(parent, n)

treeiter est un TreeIter pointant sur la ligne enfant d'index n de la ligne désignée par le TreeIter parent.parent peut valoir None dans le cas où l'on souhaite récupérer une ligne de premier niveau. treeiter vaudra None s'il n'y a pas de ligne enfant.

treeiter = treestore.iter_parent(child)

où treeiter est un TreeIter pointant sur la ligne parent de la ligne désignée par le TreeIter child.treeiter vaudra None s'il n'y a pas de ligne enfant.

La méthode get_path() permet de récupérer un chemin d'accès à partir d'un TreeIter :

chemin = modele.get_path(iter)

où iter est un Treeiter pointant sur une ligne de modele et chemin le chemin d'accès de la ligne sous la forme d'un tuple.

XIV-B-3-c - Les TreeRowReference

Un TreeRowReference est une référence persistante à une ligne de données dans un modèle. Alors que le chemin d'accès (c'est-à-dire la situation) de la ligne (c'est-à-dire son emplacement) est susceptible de changer avec l'ajout ou la suppression d'autres lignes, le TreeRowReference pointera toujours sur la même ligne de données.

Les TreeRowReference ne sont disponibles dans PyGTK qu'à partir de la version 2.4.

On crée un TreeRowReference grâce à son constructeur :

treerowref = TreeRowReference(model, path)

où model est le TreeModel contenant la ligne et path le chemin d'accès vers la ligne à laquelle faire référence. Si path n'est pas un chemin d'accès valide pour model, None est renvoyé.

XIV-B-4 - Ajouter des lignes

XIV-B-4-a - Ajouter des lignes à un ListStore

Une fois que vous avez un ListStore, il vous faut lui ajouter des lignes de données. Plusieurs méthodes sont à votre disposition :

1. iter = append(row=None)

2. iter = prepend(row=None)

3. iter = insert(position, row=None)

4. iter = insert_before(sibling, row=None)

5. iter = insert_after(sibling, row=None)

Chacune de ces méthodes insère une ligne à un emplacement implicite ou spécifié du ListStore. Les méthodes append() et prepend() utilisent des emplacements implicites : respectivement après la dernière ligne et avant la première. La méthode insert() attend un entier (le paramètre position) qui spécifie l'emplacement où insérer la ligne. Les deux autres méthodes attendent un TreeIter (sibling) pointant sur une ligne du ListStore, afin d'insérer la ligne avant ou après.

Le paramètre row indique les données devant être insérées dans la ligne après sa création. Si row vaut None ou n'est pas spécifié, la ligne créée sera vide. Si row est spécifié, il doit être un tuple ou une liste contenant autant d'éléments que le nombre de colonnes dans le ListStore. Les éléments doivent également respecter le type de données de leurs colonnes respectives dans le ListStore.

Toutes ces méthodes renvoient un TreeIter pointant sur la ligne qui vient d'être insérée. La portion de code suivante illustre la création d'un ListStore et l'ajout de lignes de données :

1. ...

2. liststore = gtk.ListStore(int, str, gtk.gdk.Color)

3. liststore.append([0,'red',colormap.alloc_color('red')])

4. liststore.append([1,'green',colormap.alloc_color('green')])

5. iter = liststore.insert(1, (2,'blue',colormap.alloc_color('blue')) )

6. iter = liststore.insert_after(iter, [3,'yellow',colormap.alloc_color('blue')])

7. ...

XIV-B-4-b - Ajouter des lignes à un TreeStore

L'ajout de lignes à un TreeStore est identique à l'ajout de lignes dans un ListStore à ceci près qu'il faut également indiquer une ligne parent (avec un TreeIter) à laquelle ajouter la nouvelle ligne. Les méthodes pour les TreeStore sont :

1. iter = append(parent, row=None)

2. iter = prepend(parent, row=None)

3. iter = insert(parent, position, row=None)

4. iter = insert_before(parent, sibling, row=None)

5. iter = insert_after(parent, sibling, row=None)

Si parent vaut None, la ligne sera ajoutée au premier niveau.

Chacune de ces méthodes insère une ligne à un emplacement implicite ou spécifié du TreeStore. Les méthodes append() et prepend() utilisent des emplacements implicites : respectivement après la dernière ligne enfant et avant la première. La méthode insert() attend un entier (le paramètre position) qui spécifie l'emplacement où insérer la ligne enfant. Les deux autres méthodes attendent un TreeIter (sibling) pointant sur une ligne enfant du TreeStore, afin d'insérer la ligne avant ou après.

Le paramètre row indique les données devant être insérées dans la ligne après sa création. Si row vaut None ou n'est pas spécifié, la ligne créée sera vide. Si row est spécifié, il doit être un tuple ou une liste contenant autant d'éléments que le nombre de colonnes dans le TreeStore. Les éléments doivent également respecter le type de données de leurs colonnes respectives dans le TreeStore.

Toutes ces méthodes renvoient un TreeIter pointant sur la ligne qui vient d'être insérée. La portion de code suivante illustre la création d'un TreeStore et l'ajout de lignes de données :

1. ...

2. pbdossier = gtk.gdk.pixbuf_from_file('dossier.xpm')

3. pbfichier = gtk.gdk.pixbuf_from_file('fichier.xpm')

4. treestore = gtk.TreeStore(int, str, gtk.gdk.Pixbuf)

5. iter0 = treestore.append(None, [1,'(0,)',pbdossier] )

6. treestore.insert(iter0, 0, [11,'(0,0)',pbfichier])

7. treestore.append(iter0, [12,'(0,1)',pbfichier])

8. iter1 = treestore.insert_after(None, iter0, [2,'(1,)',pbdossier])

9. treestore.insert(iter1, 0, [22,'(1,1)',pbfichier])

10. treestore.prepend(iter1, [21,'(1,0)',pbfichier])

11. ...

XIV-B-4-c - Modèles de grande taille

Lorsqu'un ListStore ou un TreeStore contient déjà un grand nombre de lignes, en ajouter de nouvelles peut se révéler très lent. Pour pallier cet inconvénient, on pourra suivre les conseils suivants :

- dans le cas de l'ajout d'un grand nombre de lignes, déconnecter le TreeModel de son TreeView (en utilisant la méthode set_model() avec son paramètre model à None). Ceci afin d'éviter que le TreeView n'actualise l'affichage à chaque nouvelle ligne ;

- toujours dans le cas de l'ajout d'un grand nombre de lignes, désactiver le classement (en utilisant la méthode set_default_sort_func() avec son paramètre sort_func à None) ;

- limiter le nombre de TreeRowReference utilisés, car ils actualisent leur chemin d'accès à chaque ajout/suppression ;

- fixez la propriété "fixed-height-mode" du TreeView sur TRUE pour faire en sorte que toutes les lignes aient la même hauteur et ainsi éviter le calcul individuel de la hauteur pour chaque ligne (disponible seulement à partir de PyGTK 2.4).

XIV-B-5 - Supprimer des lignes

XIV-B-5-a - Supprimer des lignes d'un ListStore

On peut supprimer une ligne de données d'un ListStore en faisant appel à la méthode remove() :

treeiter = liststore.remove(iter)

où iter est un TreeIter pointant sur la ligne à supprimer. Le TreeIter renvoyé (treeiter) pointera sur la ligne suivante ou sera invalide si iter pointait sur la dernière ligne.

La méthode clear() supprimer toutes les lignes du ListStore :

liststore.clear()

XIV-B-5-b - Supprimer des lignes d'un TreeStore

Les méthodes servant à supprimer des lignes de données d'un TreeStore sont similaires à celles du ListStore :

result = treestore.remove(iter)
treestore.clear()

où resultat vaudra TRUE si la ligne a été supprimée et iter pointera sur la ligne valide suivante. Dans le cas contraire, resultat vaudra FALSE et iter sera invalidé.

XIV-B-6 - Manipuler les données des lignes

XIV-B-6-a - Définir et récupérer des valeurs

Les méthodes permettant d'accéder aux valeurs stockées dans un ListStore et dans un TreeStore ont le même format. Toutes les manipulations des données du modèle nécessitent un TreeIter pour indiquer la ligne sur laquelle on travaille. Une fois que l'on a un TreeIter, la méthode get_value() permet de récupérer les valeurs d'une colonne dans une rangée donnée :

valeur = modele.get_value(iter, column)

où iter est un TreeIter pointant sur une ligne, column le numéro d'une colonne de modele, et valeur la valeur stockée à l'emplacement ligne-colonne.

Pour récupérer les valeurs de plusieurs colonnes en un seul appel, on utilisera la méthode get() :

valeurs = modele.get(iter, column, ...)

où iter est un TreeIter pointant sur une ligne, column est le numéro d'une colonne de modele, ... représente d'éventuels numéros de colonnes supplémentaires et valeurs est un tuple contenant les valeurs récupérées. Par exemple, pour récupérer les valeurs des colonnes 0 et 2 :

val0, val2 = modele.get(iter, 0, 2)

La méthode get() n'est disponible qu'à partir de PyGTK 2.4

Pour définir la valeur d'une colonne unique, on utilise la méthode set_value() :

modele.set_value(iter, column, value)

où iter (un TreeIter) et column (un entier) indiquent l'emplacement ligne-colonne dans modele et column est le numéro de la colonne où l'on veut stocker value. value doit être du même type de données que la colonne du modele.

Pour définir les valeurs de plusieurs colonnes d'une même ligne à la fois, on utilisera la méthode set() :

modele.set(iter, ...)

où iter désigne la ligne du modèle et ... est un ou plusieurs numéros de colonne - les paires de valeurs indiquant la colonne et la valeur à stocker. Par exemple, l'appel suivant :

modele.set(iter, 0, 'Foo', 5, 'Bar', 1, 123)

stockera 'Foo' dans la première colonne, 'Bar' dans la sixième et 123 dans la deuxième, tout ceci à la ligne de modele indiquée par iter.

XIV-B-6-b - Réordonner les lignes d'un ListStore

Les lignes d'un ListStore peuvent être déplacées à l'aide des méthodes suivantes (disponibles à partir de PyGTK 2.2) :

1. liststore.swap(a, b)

2. liststore.move_after(iter, position)

3. liststore.move_before(iter, position)

swap() intervertit les positions des lignes désignées par les TreeIter a et b. move_after() et move_before() déplacent la ligne désignée par le TreeIter iter après ou avant la ligne désignée par le TreeIter position. Si position vaut None, move_after() placera la ligne au début du modèle et move_before() à la fin.

Pour réordonner complètement les lignes d'un ListStore, on fera appel à la méthode suivante :

liststore.reorder(nouvel_ordre)

où nouvel_ordre est une liste d'entiers représentant le nouvel ordre des lignes tel que :

nouvel_ordre[nouvelleposition] = ancienneposition

Par exemple, si liststore contient quatre lignes :

1. 'un'

2. 'deux'

3. 'trois'

4. 'quatre'

l'appel suivant :

liststore.reorder([2, 1, 3, 0])

produirait le nouvel ordre suivant :

1. 'trois'

2. 'deux'

3. 'quatre'

4. 'un'

Ces méthodes réordonnent uniquement les ListStore non ordonnés.

Pour réordonner les lignes avec PyGTK 2.0, il vous faudra les supprimer et les insérer avec les méthodes décrites aux Section 14.2.4, « Ajouter des lignes » et Section 14.2.5, « Supprimer des lignes ».

XIV-B-6-c - Réordonner les lignes d'un TreeStore

Les méthodes utilisées pour réordonner les lignes du TreeStore sont identiques à celles du ListStore à ceci près qu'elles n'affectent que les lignes enfants d'une ligne parent implicite - il n'est pas possible, par exemple, d'intervertir des lignes de parents différents :

1. treestore.swap(a, b)

2. treestore.move_after(iter, position)

3. treestore.move_before(iter, position)

swap() intervertit les positions des lignes enfants désignées par les TreeIter a et b. a et b doivent avoir la même ligne parent. move_after() et move_before() déplacent la ligne désignée par le TreeIter iter après ou avant la ligne désignée par le TreeIter position. iter et position doivent avoir la même ligne parent. Si position vaut None, move_after() placera la ligne au début du modèle et move_before() à la fin.

La méthode reorder() attend un paramètre supplémentaire qui désigne la ligne parent dont les enfants doivent être réordonnés :

treestore.reorder(parent, new_order)

où new_order est une liste d'entiers représentant le nouvel ordre des lignes enfants pour la ligne parent spécifiée par le TreeIter parent, telle que :

new_order[nouvelleposition] = ancienneposition

Par exemple, si treestore contient quatre lignes :

1. 'ligne parent'

2. 'un'

3. 'deux'

4. 'trois'

5. 'quatre'

l'appel suivant

treestore.reorder(parent, [2, 1, 3, 0])

produit le nouvel ordre suivant :

1. 'ligne parent'

2. 'trois'

3. 'deux'

4. 'quatre'

5. 'un'

Ces méthodes réordonnent uniquement les TreeStore non ordonnés.

XIV-B-6-d - Manipuler plusieurs lignes

L'une des opérations les plus délicates avec les ListStore ou les TreeStore est la manipulation de plusieurs lignes, par exemple le déplacement de plusieurs lignes, d'une ligne parent à une autre, ou encore la suppression de certaines lignes en fonction de critères spéciaux. La difficulté provient de la nécessité d'utiliser un TreeIter qui peut devenir invalide du fait même de l'opération. Pour les ListStore et les TreeStore, les TreeIter sont persistants (afin de s'en assurer, la présence du drapeau gtk.TREE_MODEL_ITERS_PERSIST peut être vérifiée avec la méthode get_flags()). En revanche, les classes empilables TreeModelFilter et TreeModelSort ne disposent pas de TreeIter persistants.

Comment déplacer toutes les lignes enfants d'une ligne vers une autre si les TreeIter ne résistent pas à l'opération ? Il faut :

- itérer sur les enfants de la ligne parent ;

- récupérer les données de chaque ligne enfant ;

- supprimer chaque ligne enfant ;

- ajouter une nouvelle ligne, contenant les données de la ligne supprimée, à la liste de la nouvelle ligne parent.

On ne peut pas se fier à la méthode remove() pour renvoyer un TreeIter valide. On demandera donc simplement l'iter de la première ligne enfant, et ce jusqu'à recevoir None. Voici un exemple de fonction pouvant déplacer des lignes enfants :

1. def deplace_enfants(treestore, parent_source, parent_destination):

2. nb_colonnes = treestore.get_n_columns()

3. iter = treestore.iter_children(parent_source)

4. while iter:

5. valeurs = treestore.get(iter, *range(nb_colonnes))

6. treestore.remove(iter)

7. treestore.append(parent_destination, valeurs)

8. iter = treestore.iter_children(parent_source)

9. return

La fonction ci-dessus convient pour le simple cas où l'on souhaiterait déplacer tous les enfants d'une même ligne parent. Mais que faire si l'on veut supprimer toutes les lignes du TreeStore qui correspondent à un critère donné, comme la valeur de la première colonne par exemple ? Vous pensez peut-être à utiliser la méthode foreach() pour itérer sur toutes les lignes et supprimer celles qui correspondent :

modele.foreach(fonction, donnees_utilisateur)

où fonction est une fonction à invoquer pour chaque ligne du modèle et dont la signature est :

def fonction(modele, chemin, iter, donnees_utilisateur):

où modele est le TreeModel, chemin le chemin d'accès à une ligne de modele, iter un TreeIter pointant sur chemin et donnees_utilisateur les données transmises. Si fonction renvoie TRUE, la méthode foreach() cesse d'itérer et se termine.

Le problème de ce procédé est que modifier le contenu du modèle pendant que la méthode foreach() est en train d'itérer peut avoir des conséquences imprévisibles. Une meilleure stratégie serait d'utiliser la méthode foreach() pour créer et sauvegarder des TreeRowReferences pointant sur les lignes à supprimer, puis de supprimer celles-ci une fois la méthode foreach() terminée. Mais cela ne fonctionnerait pas avec PyGTK 2.0 et 2.2 dans lesquels les TreeRowReference n'existent pas.

Si l'on souhaite couvrir toutes les variantes de PyGTK, on peut utiliser la méthode foreach() pour rassembler les chemins d'accès des lignes à supprimer, puis les supprimer dans l'ordre inverse afin de préserver la validité des chemins d'accès. Par exemple, la portion de code suivante utilise cette stratégie :

1. ...

2. # On vérifie que la valeur de la première colonne >= la valeur transmise

3. # donnée est un tuple contenant la valeur de comparaison ainsi qu'une liste

4. # pour sauvegarder les chemins d'accès

5. def rappel_compar_valeur(modele, chemin, iter, donnees):

6. if modele.get_value(iter, 0) >= donnees[0]:

7. donnees[1].append(chemin)

8. return False     # foreach continue d'itérer

10. listechemins = []

11. treestore.foreach(rappel_compar_valeur, (10, listechemins))

13. # foreach fonctionne en profondeur d'abord (depth-first)

14. listechemins.reverse()

15. for chemin in listechemins:

16. treestore.remove(treestore.get_iter(chemin))

17. ...

Dans le cas où l'on voudrait rechercher la première ligne d'un TreeStore répondant à un critère donné, on pourrait effectuer soi-même l'itération :

1. treestore = TreeStore(str)

2. ...

3. def fonction_compar(modele, iter, donnees):

4. colonne, critere = donnees # donnes est un tuple contenant un numéro de colonne et un critère

5. valeur = modele.get_value(iter, colonne)

6. return valeur == critere

7. def recherche(modele, iter, fonction, donnees):

8. while iter:

9. if fonction(modele, iter, donnees):

10. return iter

11. resultat = recherche(modele, modele.iter_children(iter), fonction, donnees)

12. if resultat: return resultat

13. iter = modele.iter_next(iter)

14. return None

15. ...

16. compar_iter = recherche(treestore, treestore.iter_children(None),

17. fonction_compar, (0, 'bla'))

La fonction recherche() effectue une itération en profondeur sur la ligne (spécifiée par iter), sur ses pairs et sur leurs enfants à la recherche d'une ligne dont une colonne correspond à la chaine de caractères donnée. La recherche prend fin lorsqu'une ligne est trouvée.

XIV-B-7 - Support du protocole Python

Les classes qui implémentent l'interface TreeModel (TreeStore, ListStore et, à partir de PyGTK 2.4 TreeModelSort et TreeModelFilter) supportent les protocoles Python de mappage et d'itération. Le protocole d'itération permet d'utiliser la fonction iter() de Python sur un Treemodel pour créer un itérateur qui servira à itérer sur ses lignes de premier niveau. Une possibilité plus utile est d'itérer en utilisant l'instruction for ou une création fonctionnelle de liste (list comprehension). Par exemple :

1. ...

2. liststore = gtk.ListStore(str, str)

3. ...

4. # on ajoute des lignes au ListStore

5. ...

6. # une boucle for

7. for ligne in liststore:

8. # traitement individuel des lignes

9. ...

10. # création fonctionnelle de liste renvoyant une liste des valeurs de la première colonne

11. valeurs = [ l[0] for l in liststore ]

12. ...

L'utilisation de del pour supprimer une ligne du modèle et l'extraction d'un TreeModelRow PyGTK de ce dernier à partir d'une valeur-clé qui est un chemin d'accès ou un TreeIter, sont d'autres parties supportées des protocoles de mappage. Par exemple, les instructions suivantes renvoient toutes la première ligne d'un TreeModel et la dernière instruction supprime le premier enfant de la première ligne :

1. ligne = modele[0]

2. ligne = modele['0']

3. ligne = modele["0"]

4. ligne = modele[(0,)]

5. i = modele.get_iter(0)

6. ligne = modele[i]

7. del modele[(0,0)]

De plus, on peut fixer les valeurs d'une ligne existante comme ceci :

1. ...

2. liststore = gtk.ListStore(str, int, object)

3. ...

4. liststore[0] = ['Bouton', 23, gtk.Button('Etiquette')]

Les objets TreeModelRow de PyGTK supportent les protocoles de séquence et d'itération Python. On peut faire en sorte qu'un itérateur itère sur les valeurs des colonnes de la ligne, ou bien utiliser l'instruction for ou encore une création fonctionnelle de liste. Les TreeModelRow utilisent le numéro de colonne comme l'index à partir duquel extraire une valeur. Par exemple :

1. ...

2. liststore = gtk.ListStore(str, int)

3. liststore.append(['Chaine de caracteres', 514])

4. ...

5. ligne = liststore[0]

6. valeur1 = ligne[1]

7. valeur0 = liststore['0'][0]

8. for valeur in ligne:

9. print valeur

10. val0, val1 = ligne

11. ...

On utilise l'exemple de la section précédente pour itérer sur un TreeStore afin de localiser une ligne contenant une valeur particulière, le code devient :

1. treestore = TreeStore(str)

2. ...

3. def fonction_compar(ligne, donnees):

4. colonne, critere = donnees # "donnees" est un tuple contenant un numéro de colonne et un critère

5. return ligne[colonne] == critere

6. ...

7. def recherche(lignes, fonction, donnees):

8. if not lignes: return None

9. for ligne in lignes:

10. if fonction(ligne, donnees):

11. return ligne

12. resultat = recherche(ligne.iterchildren(), fonction, donnees)

13. if resultat: return resultat

14. return None

15. ...

16. compar_ligne = recherche(treestore, fonction_compar, (0, 'bla'))

On peut également définir une valeur dans une colonne existante ainsi :

treestore[(1,0,1)][1] = 'abc'

Les TreeModelRow supportent aussi l'instruction del et la conversion en listes ou tuples avec les fonctions list() et tuple() de Python. Comme illustré dans l'exemple suivant, le TreeModelRow dispose de la méthode iterchildren() qui renvoie un itérateur pour itérer sur les lignes enfants du TreeModelRow.

XIV-B-8 - Les signaux du TreeModel

Votre application peut suivre les changements d'un TreeModel en se connectant aux signaux qu'il émet : "row-changed", "row-deleted", "row-inserted", "row-has-child-toggled" et "rows-reordered". Ces signaux sont utilisés par un TreeView pour suivre les changements dans son TreeModel.

Dans une application connectée à ces signaux, l'appel de certaines méthodes peut déclencher l'émission de plusieurs signaux. Par exemple, l'appel pour ajouter à une ligne sa première ligne enfant :

treestore.append(parent, ['qwe', 'asd', 123])

déclenchera l'émission des signaux suivants :

- "row-inserted" (ligne-insérée), du fait de l'insertion de la ligne (vide) ;

- "row-has-child-toggled" (ligne-possède-enfant-modifié), puisque parent n'avait pas de ligne enfant auparavant ;

- "row-changed" (ligne-modifiée), car la valeur de la première colonne est fixée à 'qwe' ;

- "row-changed" (ligne-modifiée), car la valeur de la deuxième colonne est fixée à 'asd' ;

- "row-changed" (ligne-modifiée), car la valeur de la troisième colonne est fixée à 123.

Notez qu'il n'est pas possible de récupérer l'ordre des lignes dans la fonction de rappel du signal "rows-reordered", car le nouvel ordre des lignes est transmis sous la forme d'un pointeur opaque vers un tableau d'entiers.

On trouvera plus d'information sur les signaux des TreeModel dans le PyGTK Reference Manual.

XIV-B-9 - Ordonner les lignes d'un TreeModel

XIV-B-9-a - L'interface TreeSortable

Les objets ListStore et TreeStore implémentent l'interface TreeSortable qui fournit des méthodes pour contrôler le classement des lignes de TreeModel. L'élément clé de l'interface est l'identificateur de classement de colonne, une valeur entière arbitraire faisant référence à une fonction de comparaison de classement et à des données utilisateur associées. Un identificateur de classement de colonne doit être supérieur ou égal à zéro. On en crée un avec la méthode suivante :

treesortable.set_sort_func(id_class_colonne, fonction_classement, donnees_utilisateur=None)

où id_class_colonne est une valeur entière assignée par le programmeur, fonction_classement une fonction ou une méthode utilisée pour comparer des lignes et donnees_utilisateur des données contextuelles. fonction_classement a la signature suivante :

def fonction_classement(modele, iter1, iter2, donnees)
def methode_classement(self, modele, iter1, iter2, donnees)

où modele est le TreeModel contenant les lignes sur lesquelles pointent les TreeIter iter1 et iter2, et où donnees est donnees_utilisateur. fonction_classement doit renvoyer -1 si la ligne iter1 précède la ligne iter2, 0 si les lignes sont égales, et 1 si la ligne iter2 précède la ligne iter1. La fonction de comparaison de classement devrait toujours considérer que l'ordre de classement est gtk.SORT_ASCENDING puisqu'il sera pris en compte par les implémentations TreeSortable.

La même fonction de comparaison de classement peut être utilisée pour plusieurs identificateurs de classement de colonne en variant les données utilisateur afin de fournir des informations contextuelles. Par exemple, les donnees_utilisateur spécifiées dans la méthode set_sort_func() pourraient être l'index de la colonne d'où extraire les données de classement.

Une fois qu'un identificateur de classement de colonne est créé, un modèle peut s'en servir pour le classement en appelant la méthode :

treesortable.set_sort_column_id(sort_column_id, order)

où order est l'ordre de classement ascendant gtk.SORT_ASCENDING ou descendant gtk.SORT_DESCENDING.

Un identificateur de classement de colonne valant -1 signifie que le modèle devrait utiliser la fonction de classement par défaut qui se définit avec la méthode suivante :

treesortable.set_default_sort_func(fonction_classement, donnees_utilisateur=None)

On peut vérifier si un modèle a une fonction de classement par défaut avec la méthode :

resultat = treesortable.has_default_sort_func()

qui renvoie TRUE si une fonction de classement par défaut a été définie.

Une fois qu'un identificateur de classement de colonne a été appliqué sur un TreeModel implémentant l'interface TreeSortable, il ne peut plus revenir à son état original non classé. On peut changer la fonction de classement ou utiliser une fonction de classement par défaut, mais on ne peut pas retirer au TreeModel la fonction de classement.

XIV-B-9-b - Classement dans les ListStore et les TreeStore

Lorsqu'un objet ListStore ou TreeStore est créé, il définit automatiquement des identificateurs de classement de colonne correspondant aux colonnes du modèle en utilisant le numéro de l'index de la colonne. Par exemple, un ListStore avec trois colonnes aurait trois identificateurs de classement de colonnes (0, 1, 2) créés automatiquement. Ces identificateurs de classement de colonne sont associés avec une fonction interne de comparaison de classement qui gère les types fondamentaux :

- 'gboolean' ;

- str ;

- int ;

- long ;

- float.

Initialement, un ListStore ou un TreeStore sont définis avec un identificateur de classement de colonne de -2 qui indique qu'aucune fonction de classement n'est utilisée et que le modèle est non classé. Une fois défini un identificateur de classement de colonne sur un ListStore ou sur un TreeStore, il est impossible de le ramener à -2.

Si l'on souhaite maintenir les identificateurs de classement de colonne par défaut, on peut définir un identificateur de classement de colonne bien en dehors de l'intervalle du nombre de colonnes, comme 1000 ou plus. Puis l'on peut alterner entre la fonction de classement par défaut et les fonctions de classement de l'application selon les besoins.

XIV-C - Les TreeView

Le TreeView n'est rien de plus qu'un conteneur pour les objets TreeViewColumn et CellRenderer, les deux vrais responsables de l'affichage des données du modèle. Il fournit également une interface pour les lignes de données affichées et pour les caractéristiques contrôlant l'affichage des données.

XIV-C-1 - Créer un TreeView

On crée un TreeView en faisant appel à son constructeur :

treeview = gtk.TreeView(model=None)

où model est un objet implémentant l'interface TreeModel (en général un ListStore ou un TreeStore). Si model vaut None ou n'est pas spécifié, le TreeView ne sera associé à aucun modèle.

XIV-C-2 - Récupérer et spécifier le modèle du TreeView

Le modèle fournissant les données au TreeView peut être récupéré avec la méthode get_model() :

modele = treeview.get_model()

Un TreeModel peut être associé simultanément avec plusieurs TreeView qui actualiseront automatiquement leur affichage lorsque les données du TreeModel seront modifiées. Si un TreeView affiche obligatoirement toutes les lignes de son modèle, il peut en revanche n'afficher que certaines des colonnes de ce dernier. Deux TreeView associés au même TreeModel peuvent donc donner deux affichages différents des mêmes données.

Il est également important de savoir qu'il n'y a pas de relation prédéfinie entre les colonnes d'un TreeView et celles de son TreeModel. Ainsi, la cinquième colonne de données d'un TreeModel peut être affichée dans la première colonne d'un TreeView et dans la troisième d'un autre.

Un TreeView peut changer de modèle grâce à la méthode set_model() :

treeview.set_model(model=None)

où model est un objet implémentant l'interface TreeModel (par exemple, un ListStore ou un TreeStore). Si model vaut None, le modèle courant est abandonné.

XIV-C-3 - Les propriétés du TreeView

Le TreeView dispose de plusieurs propriétés ainsi que de méthodes pour traiter ces dernières :

"enable-search"

Lecture-écriture

Si elle vaut TRUE, l'utilisateur peut effectuer une recherche interactive dans les colonnes. TRUE par défaut.

"expander-column"

Lecture-écriture

La colonne où doit apparaitre la flèche de développement pour les lignes mères. Colonne 0 par défaut.

"fixed-height-mode"

Lecture-écriture

Si elle vaut TRUE, toutes les lignes seront de même hauteur (ce qui accélère l'affichage). Disponible à partir de GTK+ 2.4. FALSE par défaut.

"hadjustment"

Lecture-écriture

L'Adjustment horizontal pour le widget. Un nouveau est créé par défaut.

"headers-clickable"

Écriture

Si elle vaut TRUE, les en-têtes des colonnes répondront aux clics. FALSE par défaut.

"headers-visible"

Lecture-écriture

Si elle vaut TRUE, les boutons d'en-tête des colonnes seront visibles. TRUE par défaut.

"model"

Lecture-écriture

Le modèle du TreeView. None par défaut.

"reorderable"

Lecture-écriture

TRUE indique la possibilité de réordonner le TreeView. FALSE par défaut.

"rules-hint"

Lecture-écriture

TRUE ordonne au moteur de thèmes d'alterner les couleurs des lignes. FALSE par défaut.

"search-column"

Lecture-écriture

La colonne du modèle dans laquelle le code de recherche interactive doit effectuer la recherche. -1 par défaut.

"vadjustment"

Read-Write

L'Adjustment vertical pour le widget. Un nouveau est créé par défaut.

Les méthodes correspondantes sont les suivantes :

1. autorise_rechercher = treeview.get_enable_search()

2. treeview.set_enable_search(enable_search)

4. colonne = treeview.get_expander_column()

5. treeview.set_expander_column(column)

7. ajustement_horizontal = treeview.get_hadjustment()

8. treeview.set_hadjustment(adjustment)

10. treeview.set_headers_clickable(active)

12. entetes_visibles = treeview.get_headers_visible()

13. treeview.set_headers_visible(headers_visible)

15. reordonner = treeview.get_reorderable()

16. treeview.set_reorderable(reorderable)

18. alterner_couleurs = treeview.get_rules_hint()

19. treeview.set_rules_hint(setting)

21. colonne = treeview.get_search_column()

22. treeview.set_search_column(column)

24. ajustement_vertical = treeview.get_vadjustment()

25. treeview.set_vadjustment(adjustment)

Si la plupart se passent de description, signalons cependant que pour activer la propriété "enable-search" il est nécessaire d'indiquer un numéro de colonne valide à "search-column". Quand l'utilisateur pressera la combinaison de touches Control+f, une boite de dialogue de recherche apparaitra alors, dans laquelle il pourra saisir le texte à rechercher. La première ligne correspondante sera automatiquement sélectionnée pendant la saisie du texte.

La propriété "headers-clickable" ne fait en réalité qu'activer la propriété "clickable" des TreeViewColumn sous-jacents. Un TreeViewColumn ne pourra pas être réordonné si le modèle n'implémente pas l'interface TreeSortable et si la méthode de TreeViewColumn set_sort_column_id() n'a pas été appelée avec un numéro de colonne valide.

La propriété "reorderable" permet à l'utilisateur de réordonner le modèle du TreeView en glissant-déposant les lignes affichées du TreeView.

La propriété "rules-hint" ne devrait être activée que dans les cas où l'on aurait beaucoup de colonnes et où l'on penserait qu'alterner les couleurs pourrait aider l'utilisateur.

XIV-D - Les CellRenderer

XIV-D-1 - Vue d'ensemble

Les TreeViewColumn et les CellRenderer travaillent ensemble pour afficher une colonne de données dans un TreeView. Le TreeViewColumn fournit un en-tête de colonne ainsi qu'un espace vertical que les CellRenderer utiliseront pour afficher une partie des données stockées dans le modèle du TreeView. Un CellRenderer prend en charge l'affichage des données de chaque ligne et colonne dans l'espace offert par le TreeViewColumn. Un TreeViewColumn peut contenir plusieurs CellRenderer pour afficher les lignes comme des HBox. On combine par exemple assez fréquemment un CellRendererPixbuf et un CellRendererText dans la même colonne.

La Figure 14.2, « TreeViewColumn avec CellRenderer » montre un exemple de disposition pour deux TreeViewColumn : l'un contenant deux CellRenderer et l'autre un seul.

Figure 14.2. TreeViewColumn avec CellRenderer

L'application de chaque CellRenderer est indiquée avec une couleur d'arrière-plan : jaune pour le CellRendererPixbuf, cyan pour le CellRendererText et rose pour l'autre CellRendererText. Notez que le CellRendererPixbuf et le premier CellRendererText sont dans la même colonne, dont l'en-tête est "Pixbuf and Text". La couleur d'arrière-plan du CellRendererText qui affiche "Imprime un fichier" est la couleur par défaut afin de montrer la zone d'application sur une seule ligne.

La Figure 14.2, « TreeViewColumn avec CellRenderer » a été générée par le programme treeviewcolumn.py.

XIV-D-2 - Les différents types de CellRenderer

Le type de CellRenderer dont on a besoin dépend du type d'affichage attendu pour les données du modèle ; PyGTK dispose de trois CellRenderer prédéfinis :

CellRendererPixbuf

affiche des images pixbuf créées par le programme ou disponibles dans le stock de PyGTK.

CellRendererText

affiche des chaines de caractères, ainsi que des nombres pouvant être convertis en chaines de caractères (incluant les entiers, les réels, les booléens).

CellRendererToggle

affiche une valeur booléenne sous la forme d'un bouton à bascule ou d'un bouton radio.

XIV-D-3 - Les propriétés des CellRenderer

Les propriétés d'un CellRenderer déterminent la manière dont les données doivent être affichées :

"mode"

Lecture-écriture

La possibilité ou non d'éditer le CellRenderer. Peut être : gtk.CELL_RENDERER_MODE_INERT, gtk.CELL_RENDERER_MODE_ACTIVATABLE ou gtk.CELL_RENDERER_MODE_EDITABLE.

"visible"

Lecture-écriture

Si elle vaut TRUE, la cellule est affichée.

"xalign"

Lecture-écriture

La fraction (entre 0.0 et 1.0) d'espace vide devant se situer dans la partie gauche de la cellule.

"yalign"

Lecture-écriture

La fraction (entre 0.0 et 1.0) d'espace vide devant se situer dans la partie supérieure de la cellule.

"xpad"

Lecture-écriture

La quantité d'espacement à gauche et à droite de la cellule.

"ypad"

Read-Write

La quantité d'espacement au-dessus et au-dessous de la cellule.

"width"

Lecture-écriture

La largeur fixe de la cellule.

"height"

Lecture-écriture

La hauteur fixe de la cellule.

"is-expander"

Lecture-écriture

Si elle vaut TRUE, la ligne a des lignes filles.

"is-expanded"

Lecture-écriture

Si elle vaut TRUE, la ligne a des lignes filles et est développée afin de montrer celles-ci.

"cell-background"

Écriture

La couleur d'arrière-plan de la cellule sous la forme d'une chaine de caractères.

"cell-background-gdk"

Lecture-écriture

La couleur d'arrière-plan de la cellule sous la forme d'une gtk.gdk.Color.

"cell-background-set"

Lecture-écriture

Si elle vaut TRUE, la couleur d'arrière-plan de la cellule est définie par ce CellRenderer.

Les propriétés ci-dessus sont disponibles pour toutes les sous-classes de CellRenderer. Chaque type de CellRenderer dispose en plus de ses propres propriétés.

Le CellRendererPixbuf a les propriétés suivantes :

"pixbuf"

Lecture-écriture

La pixbuf à afficher - invalidée par "stock-id".

"pixbuf-expander-open"

Lecture-écriture

Pixbuf pour le symbole de développement ouvert.

"pixbuf-expander-closed"

Lecture-écriture

Pixbuf pour le symbole de développement fermé.

"stock-id"

Lecture-écriture

L'icône du stock de PyGTK à afficher.

"stock-size"

Lecture-écriture

La taille de l'icône à afficher.

"stock-detail"

Lecture-écriture

Détail d'affichage à transmettre au moteur de thèmes.

Le CellRendererText possède un grand nombre de propriétés donnant pour la plupart des indications stylistiques :

"text"

Lecture-écriture

Texte à afficher.

"markup"

Lecture-écriture

Texte balisé à afficher.

"attributes"

Lecture-écriture

Une liste d'attributs de style à appliquer au texte du CellRenderer.

"background"

Écriture

Couleur d'arrière-plan, sous la forme d'une chaine de caractères.

"foreground"

Écriture

Couleur d'avant-plan, sous la forme d'une chaine de caractères.

"background-gdk"

Lecture-écriture

Couleur d'arrière-plan, sous la forme d'une gtk.gdk.Color.

"foreground-gdk"

Lecture-écriture

Couleur d'avant-plan, sous la forme d'une gtk.gdk.Color.

"font"

Lecture-écriture

Description de police, sous la forme d'une chaine de caractères.

"font-desc"

Lecture-écriture

Description de police, sous la forme d'une pango.FontDescription.

"family"

Lecture-écriture

Nom de la famille de police (Sans, Helvetica, Times, Monospace, etc.).

"style"

Lecture-écriture

Style de police.

"variant"

Lecture-écriture

Variante de police.

"weight"

Lecture-écriture

Graisse de la police.

"stretch"

Lecture-écriture

Chasse de la police.

"size"

Lecture-écriture

Taille de la police.

"size-points"

Lecture-écriture

Taille de la police en points.

"scale"

Lecture-écriture

Facteur de taille de la police.

"editable"

Lecture-écriture

Si elle vaut TRUE, le texte peut être édité par l'utilisateur.

"strikethrough"

Lecture-écriture

Si elle vaut TRUE, le texte est barré.

"underline"

Lecture-écriture

Style de soulignement du texte.

"rise"

Lecture-écriture

Position du texte par rapport à la ligne de base (au-dessus si positif, au-dessous si négatif).

"language"

Lecture-écriture

La langue dans laquelle est rédigé le texte, sous la forme d'un code ISO. Peut servir à Pango lors de l'affichage du texte. Si vous ne comprenez pas ce paramètre, il est fort probable que vous n'en ayez pas besoin. GTK+ 2.4 et supérieur.

"single-paragraph-mode"

Lecture-écriture

Si elle vaut TRUE, tout le texte sera maintenu dans un seul paragraphe. GTK+ 2.4 et supérieur.

"background-set"

Lecture-écriture

Si elle vaut TRUE, applique la couleur d'arrière-plan.

"foreground-set"

Lecture-écriture

Si elle vaut TRUE, applique la couleur d'avant-plan.

"family-set"

Lecture-écriture

Si elle vaut TRUE, applique la famille de police.

"style-set"

Lecture-écriture

Si elle vaut TRUE, applique le style de police.

"variant-set"

Lecture-écriture

Si elle vaut TRUE, applique la variante de police.

"weight-set"

Lecture-écriture

Si elle vaut TRUE, applique la graisse de police.

"stretch-set"

Lecture-écriture

Si elle vaut TRUE, applique la chasse de police.

"size-set"

Lecture-écriture

Si elle vaut TRUE, applique la taille de police.

"scale-set"

Lecture-écriture

Si elle vaut TRUE, applique le facteur de taille de police.

"editable-set"

Lecture-écriture

Si elle vaut TRUE, applique la possibilité ou non d'éditer le texte.

"strikethrough-set"

Lecture-écriture

Si elle vaut TRUE, applique le trait barrant le texte.

"underline-set"

Lecture-écriture

Si elle vaut TRUE, applique le soulignement du texte.

"rise-set"

Lecture-écriture

Si elle vaut TRUE, applique le positionnement du texte par rapport à la ligne de base.

"language-set"

Lecture-écriture

Si elle vaut TRUE, applique la langue utilisée pour afficher le texte. GTK+ 2.4 et supérieur.

La quasi-totalité des propriétés du CellRendererText possède un équivalent booléen (avec le suffixe "-set") qui indique si la propriété doit être appliquée. Cela permet de définir une propriété globalement et d'activer ou non son application de manière sélective.

Le CellRendererToggle possède les propriétés suivantes :

"activatable"

Lecture-écriture

Si elle vaut TRUE, le bouton à bascule peut être activé.

"active"

Lecture-écriture

Si elle vaut TRUE, le bouton est actif.

"radio"

Lecture-écriture

Si elle vaut TRUE, le bouton à bascule sera dessiné comme un bouton radio.

"inconsistent"

Lecture-écriture

Si elle vaut TRUE, le bouton sera dans un état inconsistant. GTK+ 2.2 et supérieur.

Les propriétés peuvent être définies pour l'ensemble des lignes en utilisant la méthode gobject.set_property(), comme dans le programme treeviewcolumn.py, par exemple.

XIV-D-4 - Les attributs du CellRenderer

Les attributs associent une colonne de TreeModel à une propriété de CellRenderer ; le CellRenderer définit la propriété à partir de la valeur de colonne de la ligne avant d'afficher la cellule. Cela permet de personnaliser l'affichage des cellules en utilisant les données du modèle. On peut ajouter un attribut à l'ensemble courant de la manière suivante :

treeviewcolumn.add_attribute(cell_renderer, attribute, column)

où la propriété spécifiée par attribute est définie pour le cell_renderer à partir de la colonne column. Par exemple :

treeviewcolumn.add_attribute(cell, "cell-background", 1)

fixe la couleur d'arrière-plan du CellRenderer à la couleur spécifiée par la chaine de caractères située dans la deuxième colonne du modèle.

Afin de supprimer tous les attributs et d'en fixer de nouveaux immédiatement, on utilisera :

treeviewcolumn.set_attributes(cell_renderer, ...)

où les attributs de cell_renderer sont définis par des couples clé-valeur : propriété=colonne. Par exemple, pour un CellRendererText, la méthode :

treeviewcolumn.set_attributes(cell, text=0, cell_background=1, xpad=3)

fixe, pour chaque ligne, le texte à partir de la première colonne, la couleur d'arrière-plan à partir de la deuxième et l'espacement à gauche et à droite de la cellule à partir de la quatrième. Le programme treeviewcolumn.py donne des exemples d'utilisation de ces méthodes.

Les attributs d'un CellRenderer peuvent être supprimés comme suit :

treeviewcolumn.clear_attributes(cell_renderer)

XIV-D-5 - Fonction d'affichage des données cellulaires

Si définir des attributs ne suffit pas, il est possible de définir une fonction à appeler à chaque ligne pour fixer les propriétés du CellRenderer :

treeviewcolumn.set_cell_data_func(cell_renderer, fonction_rendu, donnees=None)

où fonction_rendu est de la forme :

def fonction_rendu(colonne, cell_renderer, tree_model, iter, donnees_utilisateur)

où colonne est le TreeViewColumn contenant le cell_renderer, tree_model est le modèle et iter est un TreeIter pointant vers une ligne du tree_model. donnees_utilisateur contient la valeur des donnees transmises par la méthode set_cell_data_func().

Dans fonction_rendu on peut définir toutes les propriétés voulues du cell_renderer. La portion de code suivante définit la propriété "text" pour afficher les objets PyGTK sous la forme d'une chaine les identifiant.

1. ...

2. def chaine_objet(treeviewcolumn, cellrenderer, modele, iter):

3. objetpygtk = modele.get_value(iter, 0)

4. cell.set_property('text', str(objetpygtk))

5. return

6. ...

7. treestore = gtk.TreeStore(object)

8. fenêtre = gtk.Window()

9. treeview = gtk.TreeView(treestore)

10. fenetre.add(treeview)

11. cell = gtk.CellRendererText()

12. tvcolumn = gtk.TreeViewColumn('Objet', cell)

13. treeview.append_column(tvcolumn)

14. iter = treestore.append(None, [fenêtre])

15. iter = treestore.append(iter, [treeview])

16. iter = treestore.append(iter, [tvcolumn])

17. iter = treestore.append(iter, [cell])

18. iter = treestore.append(None, [treestore])

19. ...

La Figure 14.3, « Fonction d'affichage des données cellulaires » montre le résultat :

Figure 14.3. Fonction d'affichage des données cellulaires

Cette fonction permet aussi de contrôler l'affichage des données numériques en format texte (par exemple, d'un nombre réel). Un CellRendererText affiche et convertit automatiquement un réel en chaine avec le format par défaut "%f".

Les fonctions d'affichage de données cellulaires permettent même de créer les données des colonnes à partir de données externes. Le programme listefichiers.py utilise un ListStore avec une seule colonne contenant une liste de fichiers. Le TreeView affiche des colonnes qui comprennent un pixbuf, le nom du fichier, sa taille, son mode et la date de dernière modification. Les données sont créées par la fonction suivante :

1. def fichier_pixbuf(self, colonne, cell, modele, iter):

2. nomfichier = os.path.join(self.nomrep, modele.get_value(iter, 0))

3. statsfichier = os.stat(nomfichier)

4. if stat.S_ISDIR(statsfichier.st_mode):

5. pb = dossierpb

6. else:

7. pb = fichierpb

8. cell.set_property('pixbuf', pb)

9. return

11. def nom_fichier(self, colonne, cell, modele, iter):

12. cell.set_property('text', modele.get_value(iter, 0))

13. return

15. def taille_fichier(self, colonne, cell, modele, iter):

16. nomfichier = os.path.join(self.nomrep, modele.get_value(iter, 0))

17. statsfichier = os.stat(nomfichier)

18. cell.set_property('text', statsfichier.st_size)

19. return

21. def mode_fichier(self, colonne, cell, modele, iter):

22. nomfichier = os.path.join(self.nomrep, modele.get_value(iter, 0))

23. statsfichier = os.stat(nomfichier)

24. cell.set_property('text', oct(stat.S_IMODE(statsfichier.st_mode)))

25. return

28. def modif_fichier(self, colonne, cell, modele, iter):

29. nomfichier = os.path.join(self.nomrep, modele.get_value(iter, 0))

30. statsfichier = os.stat(nomfichier)

31. cell.set_property('text', time.ctime(statsfichier.st_mtime))

32. return

Ces fonctions d'affichage de données cellulaires récupèrent l'information sur chaque fichier par son nom, extraient les données nécessaires et fixent la propriété 'text' ou 'pixbuf' des cellules selon les données. La Figure 14.4, « Exemple de liste de fichiers utilisant les fonctions d'affichage de données cellulaires » illustre le résultat du programme.

Figure 14.4. Exemple de liste de fichiers utilisant les fonctions d'affichage de données cellulaires

XIV-D-6 - CellRendererText Markup

Un CellRendererText peut présenter un texte enrichi avec des polices et des styles différents plutôt qu'une simple chaine de caractères bruts grâce à l'utilisation du balisage Pango (en définissant la propriété "markup"). On trouvera davantage de renseignements sur le balisage Pango dans la section Pango Markup du PyGTK Reference Manual.

L'extrait de code ci-dessous montre l'utilisation de la propriété "markup" :

1. ...

2. liststore = gtk.ListStore(str)

3. cell = gtk.CellRendererText()

4. tvcolumn = gtk.TreeViewColumn('Pango Markup', cell, markup=0)

5. ...

6. liststore.append(['<span foreground="blue"><b>Pango</b></span> markup peut'

7. ' modifier\n<i>le style</i> <big>la taille</big>, <u>souligner,'

8. <s>barrer</s></u>,\n'

9. 'et <span font_family="URW Chancery L"><big>changer la police '

10. 'ex : URW Chancery L</big></span>\n<span foreground="red">avant-plan'

11. ' rouge et <span background="cyan">arrière-plan cyan</span></span>'])

12. ...

et donne un résultat semblable à la Figure 14.5, « CellRendererText et balisage »:

Figure 14.5. CellRendererText et balisage

Lorsque l'on crée un balisage Pango à la volée, il faut faire attention à remplacer les caractères spécifiques au langage balisé : "<", ">", "&". La fonction cgi.escape() de la bibliothèque Python peut réaliser ces conversions élémentaires.

XIV-D-7 - Cellules de texte éditables

Les cellules CellRendererText peuvent devenir éditables pour permettre à un utilisateur de changer le contenu de la cellule sélectionnée en la cliquant ou en pressant une des touches Retour, Entrée, Espace ou Maj+Espace. On rend un CellRendererText éditable pour toutes les lignes en fixant sa propriété "editable" à TRUE de cette façon :

cellrenderertext.set_property('editable', True)

Pour rendre éditable une seule cellule, il faut ajouter un attribut au TreeViewColumn et utiliser le CellRendererText ainsi :

treeviewcolumn.add_attribute(cellrenderertext, "editable", 2)

qui fixe la propriété "editable" pour la valeur contenue dans la troisième colonne du treemodel.

Les modifications de la cellule terminées, l'application doit gérer le signal "edited" pour récupérer le nouveau texte et modifier la valeur du treemodel associé. Sinon, la cellule reprend sa valeur précédente. Le gestionnaire du signal "edited" a la signature suivante :

def rappel_edited(celltext, chemin, nouveau_texte, donnees_utilisateur)

où celltext est le CellRendererText, chemin est le chemin d'accès (une chaine) à la ligne contenant la cellule éditée, nouveau_texte est le texte modifié et donnees_utilisateur les données contextuelles. Le TreeModel étant indispensable pour définir le nouveau_texte dans le stock de données, il faudra peut-être transmettre le TreeModel comme donnees_utilisateur dans la méthode connect() :

cellrenderertext.connect('edited', rappel_edited, modele)

S'il y a deux ou plusieurs cellules éditables dans une ligne, on peut passer le numéro de la colonne dans donnees_utilisateur, en même temps que le TreeModel :

cellrenderertext.connect('edited', rappel_edited, (modele, num_col))

Ensuite, on peut définir le nouveau texte dans le gestionnaire "edited" comme le montre cet exemple avec un ListStore :

1. def rappel_edited(celltext, chemin, nouveau_texte, donnees_utilisateur):

2. liststore, colonne = donnees_utilisateur

3. liststore[chemin][colonne] = nouveau_texte

4. return

XIV-D-8 - Cellules à bascule activables

Les boutons CellRendererToggle peuvent être rendus activables en fixant leur propriété "activatable" à TRUE. De la même manière que pour les cellules éditables CellRendererText, la propriété "activatable" peut être fixée pour l'ensemble des cellules CellRendererToggle en utilisant la méthode set_property() ou pour des cellules individuelles en ajoutant un attribut au TreeViewColumn contenant le CellRendererToggle.

1. cellrenderertoggle.set_property('activatable', True)

3. treeviewcolumn.add_attribute(cellrenderertoggle, "activatable", 1)

La déclaration individuelle des boutons à bascule peut être obtenue à partir des valeurs d'une colonne du TreeModel en ajoutant un attribut, comme ceci :

treeviewcolumn.add_attribute(cellrenderertoggle, "active", 2)

Une connexion au signal "toggled" est nécessaire si l'on veut savoir quand l'utilisateur a cliqué sur le bouton et pouvoir modifier la valeur dans le treemodel. Par exemple :

cellrenderertoggle.connect("toggled", rappel_toggled, (modele, colonne))

La fonction de rappel aura la signature suivante :

def rappel_toggled(cellrenderertoggle, chemin, donnees_utilisateur)

où chemin est le chemin d'accès (une chaine), pointant sur la ligne contenant le bouton à bascule qui a été cliqué. On doit passer le TreeModel et éventuellement l'index de colonne dans le paramètre donnees_utilisateur pour fournir les éléments nécessaires à l'établissement des valeurs du treemodel. Par exemple, l'application peut modifier la valeur du treemodel de la façon suivante :

1. def rappel_toggled(cellule, chemin, donnees_utilisateur):

2. modele, colonne = donnees_utilisateur

3. modele[chemin][colonne] = not modele[chemin][colonne]

4. return

Si l'on veut afficher les boutons à bascule comme des boutons radio dont un seul serait sélectionné, l'application doit parcourir le treemodel pour désactiver le bouton radio actif et ensuite déclarer le bouton à activer. Par exemple, la fonction de rappel suivante :

1. def rappel_toggled(cellule, chemin, donnees_utilisateur):

2. modele, colonne = donnees_utilisateur

3. for ligne in modele:

4. ligne[colonne] = False

5. modele[chemin][colonne] = True

6. return

utilise la solution de facilité qui consiste à mettre toutes les valeurs du treemodel à FALSE avant de remettre à TRUE la valeur pour la ligne indiquée par le chemin.

XIV-D-9 - Programme d'exemple avec cellules éditables et activables

Le programme cellrenderer.py illustre l'utilisation de CellRendererText éditables et de CellRendererToggle activables dans un TreeStore.

1. #!/usr/bin/env python

2. # vim: ts=4:sw=4:tw=78:nowrap

3. """ Exemple avec Cellrenderer éditables et activables """

4. import pygtk

5. pygtk.require("2.0")

6. import gtk, gobject

8. taches =  {

9. "Faire les courses": "Acheter une baguette",

10. "Programmer": "Mettre a jour le programme",

11. "Cuisiner": "Allumer le four",

12. "Regarder la TV": "Enregistrer \"Urgences\""

13. }

15. class GUI_Controleur:

16. """ La classe GUI est le contrôleur de l'application """

17. def __init__(self):

18. # Création de la fenêtre principale

19. self.fenetre = gtk.Window(type=gtk.WINDOW_TOPLEVEL)

20. self.fenetre.set_title("Exemple de CellRenderer")

21. self.fenetre.connect("destroy", self.evnmt_destroy)

22. # On récupère le modèle et on le relie à la vue

23. self.modele = Ranger.recup_modele()

24. self.vue = Afficher.cree_vue( self.modele )

25. # Ajouter la vue à la fenêtre principale

26. self.fenetre.add(self.vue)

27. self.fenetre.show_all()

28. return

29. def evnmt_destroy(self, *kw):

30. """ Fonction de rappel pour fermer l'application """

31. gtk.main_quit()

32. return

33. def lance(self):

34. """ La fonction est appelée pour lancer la boucle principale GTK """

35. gtk.main()

36. return

38. class InfoModele:

39. """ La classe du modèle contient l'information que nous voulons afficher """

40. def __init__(self):

41. """ Création et remplissage du gtk.TreeStore """

42. self.tree_store = gtk.TreeStore( gobject.TYPE_STRING,

43. gobject.TYPE_BOOLEAN )

44. # on place les données utilisateur globales dans une liste

45. # on crée une arborescence simple.

46. for item in taches.keys():

47. lignemere = self.tree_store.append( None, (item, None) )

48. self.tree_store.append( lignemere, (taches[item],None) )

49. return

50. def recup_modele(self):

51. """ Renvoie le modèle """

52. if self.tree_store:

53. return self.tree_store

54. else:

55. return None

57. class AfficheModele:

58. """ Affiche le modèle InfoModele dans un treeview """

59. def cree_vue( self, modele ):

60. """ Crée une vue pour le Tree Model """

61. self.vue = gtk.TreeView( modele )

62. # Crée le CellRendererText et permet aux cellules

63. # d'être éditées.

64. self.renderer = gtk.CellRendererText()

65. self.renderer.set_property( 'editable', True )

66. self.renderer.connect( 'edited', self.rappel_edited_col0, modele )

68. # Crée le CellRendererToggle et permet sa

69. # modification (toggled) par l'utilisateur.

70. self.renderer1 = gtk.CellRendererToggle()

71. self.renderer1.set_property('activatable', True)

72. self.renderer1.connect( 'toggled', self.rappel_toggled_col1, modele )

74. # On connecte la colonne 0 de l'affichage à la colonne 0 du modèle

75. # Le cellrenderer va alors afficher tout ce qui est en colonne 0 de

76. # notre modèle .

77. self.colonne0 = gtk.TreeViewColumn("Nom", self.renderer, text=0)

79. # L'état actif des colonnes est relié à la seconde colonne

80. # du modèle. Ainsi, quand le modèle indique True, le bouton

81. # apparaitra comme actif.

82. self.colonne1 = gtk.TreeViewColumn("Fait", self.renderer1 )

83. self.colonne1.add_attribute( self.renderer1, "active", 1)

84. self.vue.append_column( self.colonne0 )

85. self.vue.append_column( self.colonne1 )

86. return self.vue

87. def rappel_edited_col0( self, cellrenderer, chemin, nouveau_texte, modele ):

88. """

89. Appelé quand un texte est modifie. Il inscrit le nouveau texte

90. dans le modèle pour qu'il puisse être affiché correctement.

91. """

92. print "Change '%s' en '%s'" % (modele[chemin][0], nouveau_texte)

93. modele[chemin][0] = nouveau_texte

94. return

95. def rappel_toggled_col1( self, cellrenderer, chemin, modele ):

96. """

97. Fixe l'état du bouton à bascule sur true ou false.

98. """

99. modele[chemin][1] = not modele[chemin][1]

100. print "Valeur de '%s'  : %s" % (modele[chemin][0], modele[chemin][1],)

101. return

103. if __name__ == '__main__':

104. Ranger = InfoModele()

105. Afficher = AfficheModele()

106. monGUI = GUI_Controleur()

107. monGUI.lance()

Le programme propose des cellules éditables dans la première colonne et des cellules activables dans la deuxième. Les lignes 64-66 créent un CellRendererText éditable et connectent le signal "edited" à la fonction de rappel rappel_edited_col0() (lignes 87-94) qui modifie la valeur de colonne de la ligne adéquate dans le TreeStore. De même, les lignes 70-72 créent un CellRendererToggle activable et connectent le signal "toggled" à la fonction de rappel rappel_toggled_col0()() (lignes 95-101) qui modifie la valeur de la ligne adéquate. Lorsqu'une cellule éditable ou activable est modifiée, un message s'affiche, indiquant de quelle modification il s'agissait.

La Figure 14.6, « Cellules éditables et activables » montre le programme cellrenderer.py en action.

Figure 14.6. Cellules éditables et activables

XIV-E - TreeViewColumn

XIV-E-1 - Créer des TreeViewColumn

La fonction de création du TreeViewColumn est la suivante :

treeviewcolumn = gtk.TreeViewColumn(titre=None, cell_renderer=None, ...)

où titre est la chaine utilisée comme en-tête de colonne et cell_renderer est le premier CellRenderer placé dans la colonne. Les autres arguments pouvant être passés au constructeur sont des mot-clés (sous la forme attribute=colonne) qui donnent des attributs au cell_renderer. Par exemple :

treeviewcolumn = gtk.TreeViewColumn('États', cellule, text=0, foreground=1)

crée un TreeViewColumn dont le CellRendererText récupère son texte dans la première colonne du TreeModel et la couleur du texte dans la seconde.

XIV-E-2 - Gérer les CellRenderer

On peut ajouter un CellRenderer à un TreeViewColumn par l'une de ces méthodes :

treeviewcolumn.pack_start(cell, expand)
treeviewcolumn.pack_end(cell, expand)

Les méthodes pack_start() et pack_end() ajoutent une cellule respectivement au début ou à la fin du TreeViewColumn. Si le paramètre expand est réglé sur TRUE, la cellule s'étirera pour remplir tout l'espace donné par le TreeViewColumn.

La méthode get_cell_renderers() :

liste_cell = treeviewcolumn.get_cell_renderers()

renvoie une liste de tous les CellRenderer de la colonne.

La méthode clear() enlève tous les attributs de CellRenderer du TreeViewColumn :

treeviewcolumn.clear()

Il y a un grand nombre d'autres méthodes disponibles pour le TreeViewColumn - principalement pour fixer ou retrouver des propriétés. On trouvera davantage de renseignements sur les propriétés du TreeViewColumn dans le PyGTK Reference Manual. La possibilité de faire appel ou non à la fonction de tri intégré s'installe ainsi ;

treeviewcolumn.set_sort_column_id(sort_column_id)

définit sort_column_id comme étant l'identifiant de classement de colonne du treemodel à utiliser lors du tri de l'affichage du TreeView. Cette méthode active aussi la propriété "clickable" de la colonne et permet ainsi à l'utilisateur de cliquer sur l'en-tête de colonne pour réaliser le classement. Quand l'utilisateur clique sur cet en-tête, l'identifiant de classement de colonne du TreeViewColumn est défini comme étant l'identifiant de classement de colonne du TreeModel et les lignes sont réordonnées selon la fonction associée de comparaison de classement. La fonction de classement automatique inverse également l'ordre de classement de la colonne et contrôle l'affichage de l'indicateur de classement. On trouvera plus de renseignements au sujet des identifiants de classement de colonne et des fonctions de comparaison de tri dans Section 14.2.9, « Ordonner les lignes d'un TreeModel ». En règle générale, lorsqu'on utilise un ListStore ou un TreeStore, l'identifiant de classement de colonne par défaut (c'est-à-dire l'index de la colonne) de la colonne du TreeModel associée au TreeViewColumn est défini comme étant l'identifiant de classement de colonne du TreeViewColumn.

Si on se sert des en-têtes de colonne du TreeViewColumn pour le classement avec la méthode set_sort_column_id(), il n'est pas nécessaire d'utiliser la méthode de TreeSortable set_sort_column_id().

Il est possible de tracer les opérations de tri ou d'utiliser le clic sur l'en-tête à d'autres fins en le connectant au signal "clicked" de la colonne du TreeView. La fonction de rappel doit être de la forme :

def callback(treeviewcolumn, donnees_utilisateur, ...)

XIV-F - Manipuler les TreeView

XIV-F-1 - Gérer les colonnes

On peut récupérer un TreeViewColumn dans un TreeView soit isolément, soit comme une liste :

treeviewcolumn = treeview.get_column(n)
columnlist = treeview.get_columns()

où n est l'index (à partir de zéro) de la colonne à récupérer. Une colonne peut être effacée par la méthode :

treeview.remove_column(column)

où column est un TreeViewColumn du treeview.

Les lignes qui possèdent des lignes enfants sont affichées avec une flèche de développement (voir Figure 14.3, « Fonction d'affichage des données cellulaires ») sur laquelle l'utilisateur peut cliquer pour masquer ou afficher les lignes enfants. On peut choisir la colonne qui possède la flèche de développement par cette méthode :

treeview.set_expander_column(column)

où column est un TreeViewColumn du treeview. Cette méthode est utile lorsqu'on ne souhaite pas que la première colonne soit indentée. Par exemple, Figure 14.7, « Flèche de développement dans la seconde colonne » montre la flèche de développement dans la seconde colonne :

Figure 14.7. Flèche de développement dans la seconde colonne

XIV-F-2 - Afficher ou masquer les enfants d'une ligne

Toutes les lignes enfants d'un TreeView peuvent être affichées ou masquées grâce aux méthodes suivantes :

treeview.expand_all()
treeview.collapse_all()

Ces méthodes sont pratiques pour initialiser l'affichage du TreeView dans un état donné. Chaque ligne peut avoir ses lignes enfants affichées ou masquées de manière indépendante par :

treeview.expand_row(path, open_all)
treeview.collapse_row(path)

où path est le chemin arborescent vers une ligne du treeview ; si open_all vaut TRUE, toute la descendance de la ligne est affichée, sinon, seuls ses enfants directs sont affichés.

On peut savoir si une ligne a ses enfants affichés par cette méthode :

is_expanded = treeview.row_expanded(path)

XIV-G - Les signaux des TreeView

Les TreeView émettent un grand nombre de signaux qui permettent de repérer les modifications dans l'afficheur du modèle. Les signaux appartiennent généralement aux catégories suivantes :

- afficher ou masquer les lignes : "row-collapsed", "row-expanded", "test-collapse-row", "test-expand-row" and "expand-collapse-cursor-row" ;

- curseur : "cursor-changed", "expand-collapse-cursor-row", "move-cursor", "select-cursor-parent", "select-cursor-row" et "toggle-cursor-row" ;

- sélection : "select-all", "select-cursor-parent", "select-cursor-row" et "unselect-all" ;

- divers : "columns-changed", "row-activated", "set-scroll-adjustments" et "start-interactive-search".

Les signaux "test-collapse-row" et "test-expand-row" sont émis avant qu'une ligne masque ou affiche ses lignes enfants. La valeur de retour de la fonction de rappel peut annuler ou autoriser l'opération - TRUE pour l'autoriser, FALSE pour l'annuler.

def callback(treeview, iter, chemin, donnees_utilisateur)

où iter est un TreeIter, chemin est un chemin de l'arbre pointant sur la ligne et donnees_utilisateur représente les données indiquées dans la méthode connect().

Le signal "row-activated" est émis soit lors d'un double-clic sur une ligne, soit lorsqu'une ligne non modifiable est sélectionnée et une des touches suivantes enfoncée : Espace, Maj+Espace, Retour or Entrée

Les autres signaux sont émis après la modification du TreeView. Le curseur est la ligne entourée par une boite. La plupart du temps, déplacer le curseur déplace la sélection. Le curseur peut être déplacé de manière indépendante par un appui simultané sur les touches Ctrl+Flèche bas ou Ctrl+Flèche haut et diverses autres combinaisons de touches.

Pour plus d'information sur les signaux du TreeView, voir le PyGTK Reference Manual

XIV-H - Gestion des Treeselection

XIV-H-1 - Obtenir un TreeSelection

Les TreeSelection sont des objets qui gèrent les sélections dans un TreeView. Quand on crée un TreeView, un TreeSelection est automatiquement créé en même temps. Le TreeSelection peut être obtenu à partir du TreeView par la méthode :

treeselection = treeview.get_selection()

On peut retrouver le TreeView associé au TreeSelection par la méthode :

treeview = treeselection.get_treeview()

XIV-H-2 - Modes du TreeSelection

Le TreeSelection dispose des modes de sélection suivants :

gtk.SELECTION_NONE

Aucune sélection n'est permise.

gtk.SELECTION_SINGLE

Permet une sélection unique par un clic.

gtk.SELECTION_BROWSE

Permet une sélection unique par un survol du pointeur.

gtk.SELECTION_MULTIPLE

Plusieurs items peuvent être sélectionnés ensemble.

On peut récupérer le mode de sélection en cours par la méthode :

mode = treeselection.get_mode()

Ce mode peut être fixé en utilisant :

treeselection.set_mode(mode)

où mode est l'un des modes de sélection ci-dessus.

XIV-H-3 - Retrouver la sélection

La méthode à utiliser pour retrouver la sélection est fonction du mode de sélection en cours. Si le mode de sélection est gtk.SELECTION_SINGLE ou gtk.SELECTION_BROWSE, il faut utiliser la méthode suivante :

(modele, iter) = treeselection.get_selected()

qui renvoie un 2-tuple contenant modele, le TreeModel utilisé par le TreeView associé au treeselection et iter, un TreeIter pointant sur la ligne sélectionnée. S'il n'y a pas de ligne sélectionnée, alors iter vaut None. Si le mode de sélection est gtk.SELECTION_MULTIPLE, une exception TypeError est déclenchée.

Pour un TreeView utilisant le mode de sélection gtk.SELECTION_MULTIPLE, il faut utiliser la méthode :

(modele, listechemins) = treeselection.get_selected_rows()

qui renvoie un 2-tuple contenant le modèle et une liste des chemins des lignes sélectionnées dans l'arborescence. Cette méthode n'est pas disponible en PyGTK 2.0 ; pour retrouver cette liste, il faut passer par une fonction intermédiaire en utilisant :

treeselection.selected_foreach(fonct, donnees=None)

où fonct est une fonction appelée pour chaque ligne sélectionnée avec les données donnees. La fonction a la signature suivante :

def fonct(modele, chemin, iter, donnees)

où modele est le TreeModel, chemin est le chemin de la ligne sélectionnée dans l'arborescence et iter est un TreeIter pointant sur la ligne sélectionnée.

Cette méthode peut être utilisée pour simuler la méthode get_selected_row() de la manière suivante :

1. ...

2. def rappel_chacune(modele, chemin, iter, listechemins) :

3. liste.append(chemin)

4. ...

5. def ma_recup_selection(treeselection) :

6. listechemins = []

7. treeselection.selected_foreach(rappel_chacune, listechemins)

8. modele = choix.get_treeview().get_model()

9. return(modele, listechemins)

10. ...

La méthode selected_foreach() ne peut servir à modifier le treemodel ou la sélection, mais peut permettre de modifier les données des lignes.

XIV-H-4 - Utiliser une fonction TreeSelection

Si on souhaite avoir un contrôle total sur la sélection de ligne, on peut définir une fonction à appeler avant qu'une ligne soit sélectionnée ou déselectionnée grâce à la méthode :

treeselection.set_select_function(fonct, donnees)

où fonct est une fonction de rappel et donnees sont les données utilisateur qui sont transmises à fonct quand celle-ci est appelée. La méthode fonct a pour signature :

def fonct(selection, modele, chemin, est_choisi, donnees_utilisateur)

où selection est le TreeSelection, modele est le TreeModel utilisé avec le TreeView associé à la selection, chemin est le chemin dans l'arbre de la ligne sélectionnée, est_choisi vaut TRUE si la ligne est actuellement sélectionnée et donnees_utilisateur est données. fonct renvoie TRUE si l'état de sélection de la ligne peut être modifié.

Établir une fonction de sélection est utile si :

- on souhaite contrôler la sélection ou désélection d'une ligne en fonction d'une information complémentaire du contexte. On doit indiquer d'une façon ou d'une autre que le changement de sélection ne peut être réalisé et pourquoi. Par exemple, on peut différencier visuellement la ligne ou ouvrir un MessageDialog contextuel ;

- on doit maintenir soi-même la liste des lignes sélectionnées ou désélectionnées quoique cela puisse aussi être réalisé, mais de manière plus laborieuse, en se connectant au signal "changed" ;

- on veut faire un traitement complémentaire avant qu'une ligne soit sélectionnée ou non. Changer par exemple l'apparence de la ligne ou modifier les données de cette ligne.

XIV-H-5 - Sélectionner et désélectionner les lignes

Il est possible de modifier la sélection par programme en employant les méthodes suivantes :

1. treeselection.select_path(path)

2. treeselection.unselect_path(path)

4. treeselection.select_iter(iter)

5. treeselection.unselect_iter(iter)

Ces méthodes sélectionnent ou désélectionnent une ligne unique indiquée, soit par le path, un chemin de l'arbre, soit par iter, un TreeIter pointant sur la ligne. Les méthodes qui suivent sélectionnent ou désélectionnent plusieurs lignes à la fois :

1. treeselection.select_all()

2. treeselection.unselect_all()

4. treeselection.select_range(start_path, end_path)

5. treeselection.unselect_range(start_path, end_path)

Les méthodes select_all() et select_range() nécessitent que le mode de sélection soit gtk.SELECTION_MULTIPLE. Les méthodes unselect_all() et unselect_range() fonctionnent quel que soit le mode de sélection. À noter que la méthode unselect_all() n'est pas disponible en PyGTK 2.0.

On peut vérifier si une ligne est sélectionnée en utilisant l'une de ces méthodes :

resultat = treeselection.path_is_selected(chemin)
resultat = treeselection.iter_is_selected(iter)

qui renvoie TRUE si la ligne indiquée par chemin ou iter est actuellement sélectionnée. On peut connaître le nombre de lignes sélectionnées avec la méthode :

compte = treeselection.count_selected_rows()

Cette méthode n'est pas disponible en PyGTK 2.0 ; il faut donc la simuler en utilisant la méthode selected_foreach() semblable à l'émulation de la méthode get_selected_rows() dans Section 21.2, « Retrieving the Selection » (pas encore traduit). Par exemple :

1. ...

2. def rappel_chacune(modele, chemin, iter, compteur) :

3. compteur[0] += 1

4. ...

5. def mon_compte_lignes_choisies(treeselection) :

6. compteur = [0]

7. treeselection.selected_foreach(rappel_chacune, compteur)

8. return compteur[0]

9. ...

XIV-I - Glisser-déposer dans un TreeView

XIV-I-1 - Reclasser avec un glisser-déposer

Reclasser les lignes d'un TreeView (et les lignes sous-jacentes d'un treemodel) s'effectue par la méthode set_reorderable() mentionnée précédemment. La méthode set_reorderable() fixe la propriété "reorderable" à la valeur indiquée et autorise ou interdit le glisser- déposer interne aux lignes du TreeView. Lorsque la propriété "reorderable" vaut TRUE, l'utilisateur peut faire glisser des lignes et les relâcher à un nouvel endroit. Cette action provoque le reclassement en parallèle des lignes sous-jacentes du TreeModel. Reclasser avec un glisser-déposer n'est possible qu'avec des treeview non triés.

XIV-I-2 - Glisser-déposer externe

Si l'on souhaite contrôler ou gérer un glisser-déposer depuis une source extérieure, il faut permettre et contrôler le glisser-déposer avec les méthodes suivantes :

treeview.enable_model_drag_source(start_button_mask, targets, actions)
treeview.enable_model_drag_dest(targets, actions)

Ces méthodes permettent d'utiliser les lignes respectivement comme source du glisser et cible du déposer. Le paramètre start_button_mask est un masque modificateur (voir la référence à gtk.gtk Constants dans le PyGTK Reference Manual qui indique les boutons ou les touches qui doivent être utilisés pour débuter le glisser. Le paramètre targets est une liste de 3-tuples qui définit l'information de cible qui peut être envoyée ou reçue. Pour qu'un glisser-déposer réussisse, au moins une des cibles de la source doit correspondre à une des cibles de la destination (par exemple, la cible "STRING"). Chaque tuple de cible contient le type de la cible, des indicateurs (une combinaison de gtk.TARGET_SAME_APP et gtk.TARGET_SAME_WIDGET ou aucune) et un identifiant unique entier. Le paramètre actions définit le résultat attendu de l'opération :

gtk.gdk.ACTION_DEFAULT, gtk.gdk.ACTION_COPY

Copie les données.

gtk.gdk.ACTION_MOVE

Déplace les données, c.-à-d. réalise une copie puis efface les données de la source en utilisant le DELETE cible du protocole de sélection de X.

gtk.gdk.ACTION_LINK

Crée un lien vers les données. Ceci n'est utile que si source et cible concordent sur ce qu'il signifie.

gtk.gdk.ACTION_PRIVATE

Action spécifique qui informe la source que la destination fera quelque chose que la source ne comprend pas.

gtk.gdk.ACTION_ASK

Demande à l'utilisateur que faire avec les données.

Par exemple, pour créer la destination d'un glisser-déposer

treeview.enable_model_drag_dest([('text/plain', 0, 0)],
gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)

Ensuite, il faut gérer le signal "drag-data-received" du Widget pour accueillir ces données déposées - peut-être remplacer les données de la ligne où on les dépose. La fonction de rappel du signal "drag-data-received" a pour signature :

def fonct_rappel(widget, drag_context, x, y, selection_data, info, timestamp)

où widget est le TreeView, drag_context est un DragContext contenant le contexte de la sélection, x et y représente la position où le "déposer" est fait, selection_data est le SelectionData contenant les données, info est l'identifiant entier du type, timestamp est le moment où le "déposer" est réalisé. La ligne peut être récupérée par cette méthode :

info_depot = treeview.get_dest_row_at_pos(x, y)

où (x, y) représentent la position transmise à la fonction de rappel, info_depot est un 2-tuple contenant le chemin d'une ligne et une constante indiquant la position du "déposer" par rapport à cette ligne : gtk.TREE_VIEW_DROP_BEFORE, gtk.TREE_VIEW_DROP_AFTER, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ou gtk.TREE_VIEW_DROP_INTO_OR_AFTER (avant, après, dedans ou avant, dedans ou après). La fonction de rappel ressemble à ceci :

1. treeview.enable_model_drag_dest([('text/plain', 0, 0)],

2. gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)

3. treeview.connect("drag-data-received", rappel_donnees_du__deposer)

4. ...

5. ...

6. def rappel_donnees_du__deposer(treeview, contexte, x, y, selection, info, dateur):

7. depot_info = treeview.get_dest_row_at_pos(x, y)

8. if depot_info:

9. modele = treeview.get_model()

10. chemin, position = depot_info

11. donnees = selection.data

12. # faire quelque chose avec les données et le modèle

13. ...

14. return

15. ...

Si une ligne est utilisée comme source du glisser, elle doit gérer le signal "drag-data-get" du Widget, lequel remplit une sélection avec les données devant être transmises à la destination du glisser-déposer par une fonction de rappel ayant comme signature :

def fonct_rappel(widget, drag_context, selection_data, info, timestamp)

Les paramètres de cette fonction de rappel sont semblables à ceux de la fonction de rappel "drag-data-received". Comme la fonction de rappel ne transmet pas le chemin de l'arborescence ou tout autre moyen simple de récupérer l'information sur la ligne qui est glissée, nous présumons que la ligne glissée est celle qui est sélectionnée et que le mode de sélection est gtk.SELECTION_SINGLE ou gtk.SELECTION_BROWSE. De cette façon, il est possible de récupérer la ligne en obtenant le TreeSelection et de retrouver le modele et le TreeIter pointant sur cette ligne. Par exemple, un texte dans une ligne peut être transmis ainsi dans le glisser-déposer :

1. ...

2. treestore = gtk.TreeStore(str, str)

3. ...

4. treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,

5. [('text/plain', 0, 0)],

6. gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)

7. treeview.connect("drag-data-get", rappel_donnees_du_glisser)

8. ...

9. ...

10. def rappel_donnees_du_glisser(treeview, contexte, selection, info, dateur):

11. treeselection = treeview.get_selection()

12. modele, iter = treeselection.get_selected()

13. texte = modele.get_value(iter, 1)

14. selection.set('text/plain', 8, texte)

15. return

16. ...

On peut désactiver l'utilisation du TreeView comme source ou destination du glisser-déposer par la méthode :

treeview.unset_rows_drag_source()
treeview.unset_rows_drag_dest()

XIV-I-3 - Exemple de glisser-déposer dans un TreeView

Un exemple simple est nécessaire pour assembler les extraits de code présentés ci-dessus. Cet exemple (treeviewdnd.py) consiste en une liste dans laquelle des URL peuvent être glissées et déposées. Les URL peuvent aussi être reclassées en effectuant un glisser-déposer à l'intérieur du TreeView. Deux boutons permettent de vider la liste ou de supprimer un item sélectionné.

1. #!/usr/bin/env python

3. # exemple treeviewdnd.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class TreeViewDnDExemple:

11. CIBLES = [

12. ('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),

13. ('text/plain', 0, 1),

14. ('TEXT', 0, 2),

15. ('STRING', 0, 3),

16. ]

17. # fermer la fenêtre et quitter

18. def ferme_event(self, widget, event, donnees=None):

19. gtk.main_quit()

20. return False

22. def efface_selection(self, bouton):

23. selection = self.treeview.get_selection()

24. modele, iter = selection.get_selected()

25. if iter:

26. modele.remove(iter)

27. return

29. def __init__(self):

30. # Créer une nouvelle fenêtre

31. self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)

33. self.fenetre.set_title("Cache URL")

35. self.fenetre.set_size_request(250, 200)

37. self.fenetre.connect("delete_event", self.ferme_event)

39. self.fen_deroule = gtk.ScrolledWindow()

40. self.vboite = gtk.VBox()

41. self.hboite = gtk.HButtonBox()

42. self.vboite.pack_start(self.fen_deroule, True)

43. self.vboite.pack_start(self.hboite, False)

44. self.b0 = gtk.Button('Effacer tout')

45. self.b1 = gtk.Button('Effacer selection')

46. self.hboite.pack_start(self.b0)

47. self.hboite.pack_start(self.b1)

49. # pour modèle, créer une liste avec une colonne contenant une chaine

50. self.modeleliste = gtk.ListStore(str)

52. # créer la vue arborescente utilisant le modeleliste

53. self.treeview = gtk.TreeView(self.modeleliste)

55. # créer un CellRenderer pour préparer les données

56. self.cell = gtk.CellRendererText()

58. # créer unTreeViewColumn pour afficher les données

59. self.colonneTV = gtk.TreeViewColumn('URL', self.cell, text=0)

61. # ajouter la colonne au treeview

62. self.treeview.append_column(self.colonneTV)

63. self.b0.connect_object('clicked', gtk.ListStore.clear, self.modeleliste)

64. self.b1.connect('clicked', self.efface_selection)

65. # autoriser la recherche dans le treeview

66. self.treeview.set_search_column(0)

68. # autoriser le tri pour la colonne

69. self.colonneTV.set_sort_column_id(0)

71. # Autoriser le glisser-deposer y compris interne à la colonne

72. self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,

73. self.CIBLES,

74. gtk.gdk.ACTION_DEFAULT|

75. gtk.gdk.ACTION_MOVE)

76. self.treeview.enable_model_drag_dest(self.CIBLES,

77. gtk.gdk.ACTION_DEFAULT)

79. self.treeview.connect("drag_data_get", self.donnees_du_glisser)

80. self.treeview.connect("drag_data_received",

81. self.donnees_du_deposer)

83. self.fen_deroule.add(self.treeview)

84. self.fenetre.add(self.vboite)

85. self.fenetre.show_all()

87. def donnees_du_glisser(self, treeview, context, selection, id_cible,

88. etime):

89. treeselection = treeview.get_selection()

90. modele, iter = treeselection.get_selected()

91. donnees = modele.get_value(iter, 0)

92. selection.set(selection.target, 8, donnees)

94. def donnees_du_deposer(self, treeview, context, x, y, selection,

95. info, etime):

96. modele = treeview.get_model()

97. donnees = selection.data

98. info_depot = treeview.get_dest_row_at_pos(x, y)

99. if info_depot:

100. chemin, position = info_depot

101. iter = modele.get_iter(chemin)

102. if (position == gtk.TREE_VIEW_DROP_BEFORE

103. or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):

104. modele.insert_before(iter, [donnees])

105. else:

106. modele.insert_after(iter, [donnees])

107. else:

108. modele.append([donnees])

109. if context.action == gtk.gdk.ACTION_MOVE:

110. context.finish(True, True, etime)

111. return

113. def main():

114. gtk.main()

116. if __name__ == "__main__":

117. treeviewdndex = TreeViewDnDExemple()

118. main()

Voici une copie d'écran Figure 14.8, « Exemple de glisser-déposer dans un TreeView » du programme exemple treeviewdnd.py en cours d'exécution :

Figure 14.8. Exemple de glisser-déposer dans un TreeView

L'essentiel pour permettre à la fois un glisser-déposer externe et un reclassement interne des lignes est l'organisation des cibles (l'attribut TARGETS - ligne 11). Une cible spécifique à l'application (MY_TREE_MODEL_ROW) est créée et utilisée pour signifier un glisser-déposer interne au treeview en fixant l'indicateur à gtk.TARGET_SAME_WIDGET. En indiquant celle-ci comme première cible, la destination tentera d'abord cette correspondance avec les cibles sources. Ensuite, les actions de glisser de la source doivent comprendre gtk.gdk.ACTION_MOVE et gtk.gdk.ACTION_DEFAULT (voir lignes 72-75). Quand la destination reçoit les données en provenance de la source, si l'action du DragContext est gtk.gdk.ACTION_MOVE, la source est informée qu'elle doit détruire les données (la ligne dans cet exemple) en appelant la méthode finish() du DragContext (lignes 109-110). Le TreeView fournit un certain nombre de fonctions internes que nous complétons pour glisser, déposer et effacer les données.

XIV-J - TreeModelSort et TreeModelFilter

Les TreeModelSort et TreeModelFilter sont des modèles d'arbre qui s'intercalent entre le TreeModel de base (un TreeStore ou un ListStore) et le TreeView pour fournir un modèle modifié en conservant la structure d'origine du modèle de base. Ces modèles ,qui peuvent être interposés, implémentent les interfaces TreeModel et TreeSortable, mais ne fournissent aucune méthode pour insérer ou retirer des lignes dans le modèle, il faut insérer ou retirer ces lignes dans le magasin de données sous-jacent. Le TreeModelSort fournit un modèle dans lequel les lignes sont toujours classées, le TreeModelFilter fournit un modèle contenant un sous-ensemble des lignes du modèle de base.

Ces modèles peuvent être reliés sur une longueur arbitraire, si désiré. Par exemple, un TreeModelFilter peut avoir un enfant TreeModelSort qui peut avoir un enfant TreeModelFilter et ainsi de suite. Tant qu'il existe un TreeStore ou un ListStore comme point de départ de la chaine, cela devrait fonctionner. Sous PyGTK :2.0 et 2.2 les objets TreeModelSort et TreeModelFilter ne fonctionnent pas avec le protocole de mappage (mise en correspondance) de Python.

XIV-J-1 - Le TreeModelSort

Le TreeModelSort maintient un modèle trié du modèle enfant indiqué dans son constructeur. L'usage principal du TreeModelSort est de fournir, pour un modèle, des vues multiples qui peuvent être classées diversement. Lorsque l'on a plusieurs vues d'un même modèle, toute opération de tri se répercute dans toutes les vues. Utiliser le TreeModelSort permet de laisser le modèle de départ dans son état originel pendant que les modèles de tri absorbent toutes les opérations de tri. Pour créer un TreeModelSort, il faut utiliser le constructeur :

treemodelsort = gtk.TreeModelSort(child_model)

où child_model est un TreeModel. La plupart des méthodes du TreeModelSort portent sur la conversion des chemins de l'arborescence et des TreeIter du modèle enfant vers le modèle trié, et réciproquement :

sorted_path = treemodelsort.convert_child_path_to_path(child_path)
child_path = treemodelsort.convert_path_to_child_path(sorted_path)

Ces méthodes de conversion de chemin renvoient None si le chemin donné ne peut être converti en chemin dans le modèle trié ou dans le modèle enfant. Les méthodes de conversion du TreeIter sont :

sorted_iter = treemodelsort.convert_child_iter_to_iter(sorted_iter, child_iter)
child_iter = treemodelsort.convert_iter_to_child_iter(child_iter, sorted_iter)

Les méthodes de conversion du TreeIter dupliquent l'argument converti (la valeur de retour comme le premier argument) pour une préserver une compatibilité antérieure. Il faut donner la valeur None au premier argument et n'utiliser que la valeur de retour. Par exemple :

sorted_iter = treemodelsort.convert_child_iter_to_iter(None, child_iter)
child_iter = treemodelsort.convert_iter_to_child_iter(None, sorted_iter)

Comme les méthodes de conversion du chemin, ces méthodes renvoient None si le TreeIter indiqué ne peut être converti.

On peut retrouver le TreeModel enfant grâce à la méthode get_model().

treemodelsort.py est un exemple simple utilisant les objets du TreeModelSort. Les résultats obtenus avec six lignes sont illustrés par Figure 14.9, « Exemple de TreeModelSort ».

Figure 14.9. Exemple de TreeModelSort

Chacune des colonnes d'une fenêtre peut être réordonnée avec un clic sur son titre indépendamment des autres fenêtres. Lorsque le bouton "Add a Row" est pressé, une nouvelle ligne est ajoutée dans le ListStore de base et cette nouvelle ligne est affichée dans chaque fenêtre comme étant la ligne sélectionnée.

XIV-J-2 - Le TreeModelFilter

Le TreeModelFilter est disponible avec PyGTK 2.4 et supérieur.

Un objet TreeModelFilter fournit plusieurs façons de modifier la vue du TreeModel de base, y compris :

- afficher un sous-ensemble de lignes dans le modèle fils basé soit sur la valeur booléenne d'une "colonne visible", soit sur la valeur booléenne de retour d'une "fonction visible" ayant comme arguments un modèle fils, un TreeIter pointant sur une ligne du modèle fils et des données utilisateurs. Dans les deux cas, si la valeur booléenne vaut TRUE, la ligne est affichée sinon elle est cachée ;

- utiliser une racine virtuelle qui fournit une vue d'un sous-arbre des enfants d'une ligne dans un modèle fils. Ceci n'est réalisable que si les données sont dans un TreeStore ;

- combiner les colonnes et données d'un modèle relativement aux données du modèle fils. Par exemple, on peut afficher une colonne dans laquelle les données sont calculées à partir de données dans plusieurs colonnes du modèle fils.

Un objet TreeModelFilter est créé par la méthode TreeModel :

treemodelfilter = treemodel.filter_new(root=None)

où root est un chemin dans l'arbre du treemodel précisant une racine virtuelle du modèle ou None si la racine du treemodel doit être utilisée.

Indiquer une "racine virtuelle" quand on crée le TreeModelFilter permet de limiter la vue aux enfants de cette ligne "racine" dans la hiérarchie du modèle fils. Ceci, bien sûr, n'est utile que si le modèle fils est basé sur un TreeStore. Par exemple, on peut vouloir fournir une vue de la liste des pièces qui composent un lecteur de CDROM, distincte de la liste complète des pièces de l'ordinateur.

Les modes de visibilité sont mutuellement exclusifs et ne peuvent être fixés qu'une seule fois. Une fois la visibilité de la colonne ou de la fonction établie, on ne peut plus la modifier et l'autre mode ne peut plus être utilisé. Le mode de visibilité le plus simple extrait une valeur booléenne d'une colonne du modèle fils pour déterminer si la ligne doit être affichée. La visibilité colonne est fixée par :

treemodelfilter.set_visible_column(column)

où column est le numéro de la colonne dans le TreeModel fils, de laquelle extraire les valeurs booléennes. Par exemple, le code suivant utilise les valeurs de la troisième colonne pour fixer la visibilité des lignes :

1. ...

2. treestore = gtk.TreeStore(str, str, "gboolean")

3. ...

4. modelfilter = treestore.filter_new()

5. modelfilter.set_visible_column(2)

6. ...

Ainsi, toutes les lignes du treestore qui possèdent la valeur TRUE dans la troisième colonne seront affichées.

Si on veut utiliser des critères de visibilité plus complexes, une fonction visibilité devrait fournir des capacités suffisantes :

treemodelfilter.set_visible_func(func, data=None)

où func est la fonction appelée pour chaque ligne du modèle fils afin de décider si elle doit être affichée et data représente les données utilisateur transmises à la fonction func. La fonction func renvoie TRUE si la ligne doit être affichée. Cette fonction a pour signature :

def func(modele, iter, donnees_utilisateur)

où modele est le TreeModel fils, iter est un TreeIter pointant sur une ligne du modele et donnees_utilisateur sont les data passées dans la fonction.

Si on modifie le critère de visibilité, il faut utiliser :

treemodelfilter.refilter()

pour imposer un nouveau filtrage des lignes du modèle fils.

Par exemple, l'extrait de code ci-dessous illustre un TreeModelFilter qui affiche des lignes selon une comparaison entre la valeur de la troisième colonne et le contenu des données utilisateurs :

1. ...

2. def match_type(modele, iter, donnees_utilisateur):

3. valeur = model.get_value(iter, 2)

4. return valeur in donnees_utilisateur

5. ...

6. show_vals = ['OPEN', 'NEW', 'RESO']

7. liststore = gtk.ListStore(str, str, str)

8. ...

9. modelfilter = liststore.filter_new()

10. modelfilter.set_visible_func(match_type, show_vals)

11. ...

Le programme treemodelfilter.py illustre l'utilisation de la méthode set_visible_func(). Figure 14.10, « Exemple de visibilité d'un TreeModelFilter » montre le résultat obtenu.

Figure 14.10. Exemple de visibilité d'un TreeModelFilter

En agissant sur les boutons du bas, on modifie le contenu du TreeView pour afficher seulement les lignes qui correspondent à l'étiquette du bouton.

Une fonction modify permet un autre niveau de contrôle sur l'affichage du TreeView sur la manière dont on peut combiner une ou plusieurs (ou toutes) colonnes présentées par le TreeModelFilter. Il faut utiliser un modèle fils de base, un TreeStore ou un ListStore pour déterminer le nombre de lignes et la hiérarchie, mais les colonnes peuvent être tout ce qui est indiqué dans la méthode :

treemodelfilter.set_modify_func(types, func, data=None)

où types est une suite (liste ou tuple) précisant le type des colonnes présentes, func est une fonction appelée pour renvoyer la valeur pour une rangée et une colonne et data est un argument passé à la fonction func. La fonction func a pour signature :

def func(modele, iter, colonne, donnees_utilisateur)

où modele est le TreeModelFilter, iter le TreeIter qui pointe sur une ligne du modèle, colonne est le numéro de la colonne pour laquelle une valeur est nécessaire et donnees_utilisateur est le paramètre data. La fonction func doit renvoyer une valeur correspondant au type de colonne.

Une fonction de modification est utile quand on souhaite fournir une colonne de données qui doivent être générées à partir de données de colonnes du modèle fils. Par exemple, on dispose d'une colonne contenant des dates de naissance et on veut fournir une colonne affichant les âges ; une fonction de modification peut générer l'information sur l'âge à partir de la date de naissance et de la date actuelle. Un autre exemple serait de choisir l'image à afficher selon l'analyse des données (un nom de fichier) dans une colonne. Cette action peut aussi être réalisée par la méthode set_cell_data_func() du TreeViewColumn.

Généralement, avec la fonction modify, il faut convertir le TreeIter du TreeModelFilter en un TreeIter dans le modèle fils par :

child_iter = treemodelfilter.convert_iter_to_child_iter(filter_iter)

Bien sûr, il faut aussi retrouver le modèle fils par :

child_model = treemodelfilter.get_model()

Ceci donne accès à la ligne du modèle fils et à son contenu pour fournir la valeur de la ligne et colonne indiquées du TreeModelFilter. Il existe aussi une méthode pour convertir les chemins du modèle filtre de et vers les chemins de l'arborescence fille.

1. filter_iter = treemodelfilter.convert_child_iter_to_iter(child_iter)

3. child_path = treemodelfilter.convert_path_to_child_path(filter_path)

4. filter_path = treemodelfilter.convert_child_path_to_path(child_path)

Bien sûr, il est possible de combiner les modes de visibilité et la fonction modify pour, à la fois, filtrer les lignes et produire les colonnes. Pour obtenir encore plus de contrôle sur la vue, il faudrait utiliser un TreeModel personnalisé.

XIV-K - Le TreeModel générique

Si le TreeModel standard n'est pas assez puissant pour les besoins de l'application, il est possible d'utiliser le GenericTreeModel pour construire son propre TreeModel personnalisé en Python. Créer un GenericTreeModel peut être utile en cas de problème de performance avec les objets TreeStore et ListStore standards, ou si on veut une interface directe avec une source de données externe (une base de données ou un système de fichiers) pour éviter une duplication des données de et vers le TreeStore ou le ListStore.

XIV-K-1 - Aperçu du GenericTreeModel

Avec un GenericTreeModel, on construit et gère son modèle de données et on fournit un accès externe à travers l'interface du TreeModel standard en définissant un ensemble de méthodes de classe. PyGTK implémente l'interface du TreeModel et prend en charge les méthodes du TreeModel appelées pour fournir le modèle de données réel.

Les détails de l'implémentation de votre modèle devraient rester complètement cachés aux applications externes. Ce qui signifie que la manière dont votre modèle identifie, range et retrouve les données n'est pas connue de l'application. En général, la seule information qui est sauvegardée en dehors du GenericTreeModel sont les références de ligne qui sont enveloppées par les TreeIter externes. Ces références ne sont pas visibles par l'application.

Voici un examen détaillé de l'interface GenericTreeModel qu'il faut fournir.

XIV-K-2 - Interface du GenericTreeModel

L'interface du GenericTreeModel comprend les méthodes suivantes qui doivent être implémentées dans le modèle personnalisé :

def on_get_flags(self)
def on_get_n_columns(self)
def on_get_column_type(self, index)
def on_get_iter(self, path)
def on_get_path(self, rowref)
def on_get_value(self, rowref, column)
def on_iter_next(self, rowref)
def on_iter_children(self, parent)
def on_iter_has_child(self, rowref)
def on_iter_n_children(self, rowref)
def on_iter_nth_child(self, parent, n)
def on_iter_parent(self, child)

Il faut remarquer que ces méthodes supportent entièrement l'interface du TreeModel, y compris :

def get_flags()
def get_n_columns()
def get_column_type(index)
def get_iter(path)
def get_iter_from_string(path_string)
def get_string_from_iter(iter)
def get_iter_root()
def get_iter_first()
def get_path(iter)
def get_value(iter, column)
def iter_next(iter)
def iter_children(parent)
def iter_has_child(iter)
def iter_n_children(iter)
def iter_nth_child(parent, n)
def iter_parent(child)
def get(iter, column, ...)
def foreach(func, user_data)

Pour illustrer l'utilisation du GenericTreeModel j'ai modifié le programme listefichiers.py et montré comment les méthodes d'interface sont réalisées. Le programme listefichiers-gtm.py affiche les fichiers d'un répertoire avec une icône indiquant si le fichier est ou non un répertoire, le nom du fichier, sa taille, son mode et sa date de dernière modification.

La méthode on_get_flags() doit retourner une valeur qui est une combinaison de  :

gtk.TREE_MODEL_ITERS_PERSIST

Les TreeIter perdurent quels que soient les signaux émis par l'arbre.

gtk.TREE_MODEL_LIST_ONLY

Le modèle est uniquement une liste et n'a jamais d'enfant.

Si un modèle possède des références de lignes valides malgré les changements de lignes (réordonnancement, ajout, suppression), on utilise gtk.TREE_MODEL_ITERS_PERSIST. De la même façon, si un modèle est seulement une liste, on utilise gtk.TREE_MODEL_LIST_ONLY. Autrement, on renvoie 0 si le modèle ne possède pas de références de lignes persistantes et est une arborescence. Dans l'exemple, le modèle est une liste avec des TreeIter persistants.

def on_get_flags(self):
return gtk.TREE_MODEL_LIST_ONLY|gtk.TREE_MODEL_ITERS_PERSIST

La méthode on_get_n_columns() doit retourner le nombre de colonnes que le modèle exporte vers l'application. L'exemple garde une liste de types de colonnes, ainsi on peut renvoyer la longueur de la liste :

1. class Fichmodeleliste(gtk.GenericTreeModel):

2. ...

3. column_types = (gtk.gdk.Pixbuf, str, long, str, str)

4. ...

5. def on_get_n_columns(self):

6. return len(self.types_colonnes)

La méthode on_get_column_type() doit renvoyer le type de la colonne pour l'index indiqué. Cette méthode est généralement appelée par un TreeView quand le modèle est établi. On peut soit créer une liste ou un tuple contenant l'information de type de colonne, soit le créer à la volée. Dans l'exemple :

def on_get_column_type(self, n):
return self.types_colonnes[n]

L'interface GenericTreeModel convertit le type Python en un GType, donc le code suivant :

flm = Fichmodeleliste()
print flm.on_get_column_type(1), flm.get_column_type(1)

imprimerait :

<type 'str'> <GType gchararray (64)>

Les méthodes suivantes utilisent les références de ligne qui sont conservées comme données privées dans un TreeIter. L'application ne peut lire la référence de ligne dans un TreeIter, donc on peut utiliser n'importe quel item unique voulu comme référence de ligne. Par exemple, dans un modèle comportant des lignes comme des tuples, il est possible d'utiliser l'index de tuple comme la référence de ligne. Un autre exemple serait d'utiliser un nom de fichier comme référence de ligne dans un modèle représentant des fichiers dans un répertoire. Dans ces deux cas, le référence de ligne n'est pas modifiée par les changements du modèle, aussi les TreeIter peuvent être déclarés persistants. L'interface de l'application PyGTK GenericTreeModel extraira vos références de ligne à partir des TreeIter et enveloppera vos références de ligne dans les TreeIter selon les besoins.

Dans les méthodes suivantes, refligne se réfère à une référence de ligne interne.

La méthode on_get_iter() devrait renvoyer un refligne pour le chemin de l'arborescence indiqué par le chemin. Le chemin de l'arborescence doit toujours être représenté par un tuple. L'exemple utilise le nom de fichier comme refligne. Les noms de fichier sont conservés dans une liste dans le modèle, ainsi on prend le premier index du chemin comme index du nom de fichier :

def on_get_iter(self, chemin):
return self.fichiers[chemin[0]]

Il faut être cohérent dans l'utilisation de la référence de ligne puisqu'on récupérera une référence de ligne dans les appels des méthodes GenericTreeModel qui prennent les arguments TreeIter : on_get_path(), on_get_value(), on_iter_next(), on_iter_children(), on_iter_has_child(), on_iter_n_children(), on_iter_nth_child() et on_iter_parent().

La méthode on_get_path() devrait retourner un chemin de l'arborescence donnant un refligne. Par exemple, poursuivant l'exemple précédent où le nom de fichier est utilisé comme refligne, on pourrait définir la méthode on_get_path() comme suit :

def on_get_chemin(self, refligne):
return self.fichiers.index(refligne)

Cette méthode trouve l'index de la liste contenant le nom de fichier dans refligne. Cet exemple montre clairement qu'un choix judicieux de référence de ligne rend l'exécution plus efficiente. Par exemple, on pourrait utiliser un dictionnaire Python pour relier refligne à un chemin.

La méthode on_get_value() devrait renvoyer les données rangées à la ligne et colonne indiquées par refligne et colonne. Dans l'exemple :

1. def on_get_value(self, refligne, colonne):

2. fname = os.path.join(self.nomrep, refligne)

3. try:

4. statutfich = os.stat(fname)

5. except OSError:

6. return None

7. mode = statutfich.st_mode

8. if colonne is 0:

9. if stat.S_ISDIR(mode):

10. return dossierpb

11. else:

12. return fichierpb

13. elif colonne is 1:

14. return refligne

15. elif colonne is 2:

16. return statutfich.st_size

17. elif colonne is 3:

18. return oct(stat.S_IMODE(mode))

19. return time.ctime(statutfich.st_mtime)

on doit extraire l'information de fichier associée et renvoyer la valeur appropriée selon la colonne indiquée.

La méthode on_iter_next() devrait retourner une référence de ligne, à la ligne (de même niveau), après la ligne indiquée par refligne. Dans l'exemple :

1. def on_iter_next(self, refligne):

2. try:

3. i = self.fichiers.index(refligne)+1

4. return self.fichiers[i]

5. except IndexError:

6. return None

L'index du nom de fichier de refligne est établi et le nom de fichier suivant est renvoyé ou s'il n'existe pas, None est renvoyé.

La méthode on_iter_children() devrait retourner une référence de ligne vers la première ligne enfant de la ligne indiquée par refligne. Si refligne vaut None, une référence à la première ligne du niveau supérieur est retournée. S'il n'existe pas de ligne enfant, None est retourné. Dans l'exemple :

1. def on_iter_children(self, refligne):

2. if refligne:

3. return None

4. return self.fichiers[0]

Puisque le modèle est une liste, seul le niveau supérieur (refligne=None) peut posséder des lignes enfant. None est retourné si refligne contient un nom de fichier.

La méthode on_iter_has_child() doit renvoyer TRUE si la ligne indiquée par refligne possède des lignes enfant, FALSE sinon. Dans l'exemple, on renvoie FALSE puisqu’aucune ligne ne peut posséder d'enfant :

def on_iter_has_child(self, refligne):
return False

La méthode on_iter_n_children() renvoie le nombre de lignes enfant que possède la ligne indiquée par refligne. Si refligne vaut None, on renvoie le nombre de lignes du niveau supérieur. Dans l'exemple, on renvoie 0 si refligne ne vaut pas None :

1. def on_iter_n_children(self, refligne):

2. if refligne:

3. return 0

4. return len(self.fichiers)

La méthode on_iter_nth_child() renvoie une référence de ligne à la énième ligne enfant de la ligne indiquée par parent. Si parent vaut None, une référence à la énième ligne du niveau supérieur est retournée. Dans l'exemple, on retourne une référence à la énième ligne du niveau supérieur si parent vaut None, None sinon :

1. def on_iter_nth_child(self, refligne, n):

2. if refligne:

3. return None

4. try:

5. return self.fichiers[n]

6. except IndexError:

7. return None

La méthode on_iter_parent() renvoie une référence de ligne à la ligne parent de la ligne indiquée par refligne. Si refligne pointe sur une ligne du niveau supérieur, on renvoie None. Dans l'exemple, None est toujours renvoyé en supposant que refligne doit pointer sur une ligne du niveau supérieur :

def on_iter_parent(child):
return None

Ces exemples sont assemblés dans le programme listefichiers-gtm.py. La Figure 14.11, « Exemple de TreeModel générique » montre le résultat.

Figure 14.11. Exemple de TreeModel générique

XIV-K-3 - Ajouter et supprimer des lignes

Le programme listefichiers-gtm.py calcule la liste des noms de fichier lorsqu'il crée une instance de FileListModel. Si l'on souhaite vérifier régulièrement la création de nouveaux fichiers et ajouter ou retirer des fichiers du modèle, on peut soit créer un nouveau FileListModel pour le même répertoire, soit ajouter une méthode pour ajouter ou retirer des lignes dans le modèle. Selon le type de modèle créé, il peut être nécessaire d'ajouter une méthode semblable à celles des modèles de TreeStore et de ListStore :

- insert()

- insert_before()

- insert_after()

- prepend()

- append()

- remove()

- clear()

Il n'est, bien sûr, pas nécessaire d'utiliser toutes ou même une seule de ces méthodes. On peut créer ses propres méthodes plus adaptées à son modèle.

En utilisant le programme exemple précédent pour montrer l'ajout de méthodes pour supprimer ou ajouter des fichiers, voici comment on implémente ces méthodes :

def remove(iter)
def add(filename)

La méthode remove() retire le fichier indiqué par le paramètre iter. Outre retirer la ligne du modèle, la méthode efface aussi le fichier du répertoire. Évidemment, si l'utilisateur n'a pas les droits de suppression du ficher, la ligne n'est pas supprimée non plus. Par exemple :

1. def remove(self, iter):

2. path = self.get_path(iter)

3. pathname = self.get_pathname(path)

4. try:

5. if os.path.exists(pathname):

6. os.remove(pathname)

7. del self.files[path[0]]

8. self.row_deleted(path)

9. except OSError:

10. pass

11. return

La méthode transmet un TreeIter qu'il faut transformer en un chemin à utiliser pour récupérer le chemin du fichier par la méthode get_pathname(). Il est possible que le fichier ait déjà été supprimé, il faut donc tester son existence avant de le supprimer. Si une exception OSError intervient pendant la suppression du fichier, c'est probablement parce que le fichier est dans un répertoire où l'utilisateur n'a pas suffisamment de droits. Finalement, le fichier est supprimé et le signal "row-deleted" est émis par la méthode rows_deleted(). Le signal "file-deleted" indique aux TreeView utilisant le modèle que celui-ci a changé, ainsi ils peuvent mettre à jour leur état interne et afficher le modèle modifié.

La méthode add() impose de créer un fichier dans le répertoire courant avec le nom donné. Si le fichier est créé, son nom est ajouté à la liste des fichiers du modèle. Par exemple :

1. def add(self, filename):

2. pathname = os.path.join(self.dirname, filename)

3. if os.path.exists(pathname):

4. return

5. try:

6. fd = file(pathname, 'w')

7. fd.close()

8. self.dir_ctime = os.stat(self.dirname).st_ctime

9. files = self.files[1:] + [filename]

10. files.sort()

11. self.files = ['. '] + files

12. path = (self.files.index(filename),)

13. iter = self.get_iter(path)

14. self.row_inserted(path, iter)

15. except OSError:

16. pass

17. return

Cet exemple simple s'assure que le fichier n'existe pas déjà, puis tente d'ouvrir le fichier en écriture. Si cela réussit, le fichier est refermé et son nom inséré dans la liste de fichiers. Le chemin et le TreeIter de la ligne du fichier ajouté sont récupérés pour être utilisés dans la méthode row_inserted() qui émet le signal "row-inserted". Ce signal "row-inserted" sert à indiquer aux TreeView utilisant le modèle qu'ils doivent mettre à jour leur état et rafraîchir leur affichage.

Les autres méthodes mentionnées précédemment (par exemple, append et prepend) n'ont pas de signification pour cet exemple puisque le modèle a sa liste de fichiers triée.

D'autres méthodes peuvent être utiles dans un TreeModel découlant du GenericTreeModel :

- set_value()

- reorder()

- swap()

- move_after()

- move_before()

L'implémentation de ces méthodes est similaire à celle des méthodes précédentes. Il faut synchroniser le modèle et l'état externe, et ensuite indiquer aux TreeView que le modèle a changé. Les méthodes suivantes sont utilisées pour indiquer aux TreeView les changements du modèle en envoyant le signal approprié :

def row_changed(path, iter)
def row_inserted(path, iter)
def row_has_child_toggled(path, iter)
def row_deleted(path)
def rows_reordered(path, iter, new_order)

XIV-K-4 - Gestion de la mémoire

L'un des problèmes avec le GenericTreeModel est que le TreeIter contient une référence à un objet Python venant du modèle personnalisé. Puisque le TreeIter peut être créé et initialisé dans un module en C et être présent dans la pile, il n'est pas possible de connaître le moment où le TreeIter est détruit et la référence à l'objet Python n'est plus d'utilité. Donc l'objet Python référencé dans un TreeIter voit par défaut son compteur incrémenté, mais il n'est pas décrémenté lorsque le TreeIter est détruit. Ceci garantit que l'objet Python ne peut être détruit quand il est utilisé par un TreeIter et produire éventuellement une erreur de segmentation. Malheureusement les comptes de référence supplémentaires font que, au mieux, l'objet Python aura un compte de référence excessif et, au pire, il ne sera jamais libéré même lorsqu’il n'est pas utilisé. Le dernier cas cause des fuites de mémoire et le premier, des fuites de références.

Pour parer à la situation où le TreeModel personnalisé maintient une référence à l'objet Python jusqu'à ce qu'il ne soit plus disponible (le TreeIter est invalide, car le modèle a changé) et il n'y a pas besoin de relâcher les références, le GenericTreeModel possède une propriété "leak-references". Par défaut, "leak-references" vaut TRUE pour indiquer que le GenericTreeModel relâchera les références. Si "leak-references" vaut FALSE, le compteur de références de l'objet Python ne sera pas incrémenté quand il sera référencé dans un TreeIter. Ce qui signifie que le TreeModel personnalisé doit conserver une référence à tous les objets Python utilisés dans un TreeIter jusqu'à la destruction du modèle. Malheureusement, même ceci ne protège pas d'un mauvais code qui tente d'utiliser un TreeIter sauvegardé dans un GenericTreeModel différent. Pour se protéger contre ce cas de figure, l'application doit conserver une référence à tous les objets Python référencés dans un TreeIter pour toutes les instances du GenericTreeModel. Naturellement, ceci a le même effet qu'une fuite de références.

Avec PyGTK 2.4 et ultérieurs, les méthodes invalidate_iters() et iter_is_valid() sont disponibles comme aide à la gestion des TreeIter et des références des objets Python :

1. generictreemodel.invalidate_iters()

3. result = generictreemodel.iter_is_valid(iter)

Ceci est particulièrement utile lorsque la propriété "leak-references" vaut FALSE. Les modèles d'arbre dérivés du GenericTreeModel sont protégés des problèmes de TreeIter périmés, car la validité des iters est automatiquement vérifiée avec le modèle arbre.

Si un modèle d'arbre personnalisé ne gère pas les iters persistants (par exemple, gtk.TREE_MODEL_ITERS_PERSIST n'est pas établi dans le retour de la méthode TreeModel.get_flags(), il est possible d'appeler la méthode invalidate_iters() pour annuler tous les TreeIter en cours quand le modèle est modifié (après insertion d'une nouvelle ligne, par exemple). Le modèle d'arbre peut aussi utiliser n'importe quel objet Python qui a été référencé par un TreeIter après l'appel à la méthode invalidate_iters().

Les applications peuvent utiliser la méthode iter_is_valid() pour déterminer si un TreeIter est encore valide pour le modèle d'arbre personnalisé.

XIV-K-5 - Autres interfaces

Les modèles ListStore et TreeStore comprennent outre l'interface TreeModel, les interfaces TreeSortable, TreeDragSource et TreeDragDest. Le GenericTreeModel ne comprend que l'interface TreeModel. Je pense que c'est à cause de la référence directe au modèle dans le langage C par les modèles TreeView, TreeModelSort et TreeModelFilter. Créer et utiliser un TreeIter exige un code collant au C pour l'interface avec le modèle d'arbre personnalisé Python qui contient les données. Ce code collant est fourni par le GenericTreeModel et il semble qu'il n'y a pas d'alternative purement Python de réaliser ceci, car le TreeView et les autres modèles appellent les fonctions du GtkTreeModel en C en passant leur référence au modèle d'arbre personnalisé.

L'interface TreeSortable nécessite aussi un code collant au C pour agir sur le mécanisme de tri par défaut du TreeViewColumn ainsi qu'il est expliqué dans la Section 14.2.9, « Ordonner les lignes d'un TreeModel ». Cependant un modèle personnalisé doit réaliser ses propres tris et une application doit gérer l'utilisation des critères de tri en prenant en compte les clics sur les en-têtes des TreeViewColumn et en appelant les méthodes de tri du modèle d'arbre personnalisé. Le modèle effectue la mise à jour du TreeView en émettant le signal "rows-reordered" grâce à la méthode rows_reordered() du TreeModel. Ainsi le GenericTreeModel ne nécessite probablement pas d'implémenter l'interface TreeSortable.

Pareillement, le GenericTreeModel n'a pas besoin d' implémenter les interfaces TreeDragSource et TreeDragDest puisque le modèle d'arbre personnalisé peut effectuer ses propres interfaces de glisser-déposer et l'application peut gérer les signaux TreeView appropriés et faire appel aux méthodes du modèle d'arbre personnalisé tant que nécessaire.

XIV-K-6 - Utilisation du GenericTreeModel

Je crois que le GenericTreeModel ne devrait être utilisé qu'en dernier ressort. Les objets standards du TreeView comprennent des mécanismes puissants qui devraient être suffisants pour la plupart des applications. Sans doute, il existe des applications qui peuvent avoir besoin du GenericTreeModel,mais il faudrait d'abord essayer d'utiliser ce qui suit :

Cell Data Functions

Comme il est montré dans la Section 14.4.5, « Fonction d'affichage des données cellulaires », les fonctions de données des cellules peuvent être utilisées pour modifier et même produire les données pour un affichage d'une colonne du TreeView. On peut créer autant d'affichages de colonnes avec des données générées que l'on souhaite. Cela donne beaucoup de contrôle de la présentation de données d'une source de données sous-jacente.

TreeModelFilter

En PyGTK 2.4, le TreeModelFilter, comme indiqué dans la Section 14.10.2, « Le TreeModelFilter », permet un fort contrôle sur l'affichage des colonnes et lignes d'un TreeModel enfant, y compris afficher seulement les lignes filles d'une ligne. Les colonnes de données peuvent être produites d'une façon semblable à l'utilisation des fonctions de données en cellule ; ici le modèle est un TreeModel avec un nombre et un type de colonnes indiqués alors qu'une fonction de données de cellule laisse les colonnes modèles inchangées et modifie juste l'affichage dans un TreeView.

Si un GenericTreeModel doit être utilisé, il faut veiller à :

- l'interface complète du TreeModel doit être créée et être en mesure de fonctionner comme indiqué. I y a des finesses qui peuvent induire des erreurs. Par opposition, le TreeModel standard a été complètement testé ;

- gérer les références des objets Python utilisés par un TreeIter peut se révéler difficile, particulièrement pour des programmes longs avec beaucoup d'affichages variés ;

- il faut créer une interface pour ajouter, supprimer ou modifier le contenu des lignes. Le lien d'un TreeIter aux objets de Python et aux rangées modèles dans cette interface n'est pas très élégant ;

- le développement d'interfaces de glisser-déposer et de tri demande un effort important, l'application doit très probablement participer à ce que ces interfaces soient entièrement fonctionnelles.

XIV-L - The Generic CellRenderer

XV - Chapitre 15. Nouveaux Widgets dans PyGTK 2.2

L'objet Clipboard a été ajouté en PyGTK 2.2. Le GtkClipboard était disponible en GTK+ 2.0, mais n'était pas utilisé par PyGTK 2.0, car il n'était pas un GObject complet. Quelques nouveaux objets ont été ajoutés au module gtk.gdk de PyGTK 2.2, mais ils ne seront pas décrits dans ce tutoriel. Lisez le PyGTK 2 Reference Manual pour obtenir des informations sur les objets gtk.gdk.Display, gtk.gdk.DisplayManager et gtk.gdk.Screen.

XV-A - Les Clipboards

Un Clipboard fournit une zone de stockage pour partager des données entre processus ou entre différents widgets du même processus. Chaque Clipboard est identifié par un nom encodé en tant que gdk.Atom. On peut utiliser n'importe quel nom souhaité pour identifier un Clipboard, celui-ci sera créé s'il n'existe pas. Si on souhaite partager un Clipboard avec d'autres processus, chaque processus doit connaître le nom du Clipboard.

Les Clipboard sont construits à partir des SelectionData et des interfaces de sélection. Le Clipboard par défaut utilisé par les widgets TextView, Label et Entry se nomme "CLIPBOARD". Les autres Clipboard courants sont "PRIMARY" et "SECONDARY", ce qui correspond aux sélections primaires et secondaires (inconnues de Win32). Elles peuvent aussi être désignées par les objets gtk.gdk.Atom : gtk.gdk.SELECTION_CLIPBOARD, gtk.gdk.SELECTION_PRIMARY et gtk.gdk.SELECTION_SECONDARY. Se reporter à la documentation de référence gtk.gdk.Atom pour plus d'information.

XV-A-1 - Créer un Clipboard

Un Clipboard est créé par le constructeur :

presse_papier = gtk.Clipboard(display, selection)

où display est le gtk.gdk.Display associé avec le Clipboard désigné par le paramètre selection. La fonction suivante crée un Clipboard en utilisant le gtk.gdk.Display par défaut :

presse_papier = gtk.clipboard_get(selection)

Enfin, un Clipboard peut aussi être créé par la méthode Widget :

presse_papier = widget.get_clipboard(selection)

Le widget doit être "réalisé" et appartenir à la hiérarchie de la fenêtre racine.

XV-A-2 - Utiliser les Clipboard avec les Entry, Spinbutton et TextView

Les widgets Entry, SpinButton et TextView possèdent des menus contextuels qui offrent la possibilité de couper ou copier le texte sélectionné et de le coller à partir du presse-papier "CLIPBOARD". De plus, des liens sont établis pour permettre des raccourcis-clavier pour couper, copier et coller. Couper est activé par Control+X , copier par Control+C et coller par Control+V.

Les widgets (Entry et SpinButton) mettent en œuvre l'interface Editable qui possède les méthodes suivantes pour couper, copier et coller de et vers le "CLIPBOARD" clipboard :

1. editable.cut_clipboard()

2. editable.copy_clipboard()

3. editable.paste_clipboard()

Un Label sélectionnable (la propriété "selectable" vaut TRUE) permet aussi de copier le texte dans le presse-papier "CLIPBOARD" en utilisant un menu contextuel ou le raccourci-clavier Control+C.

Les TextBuffer ont des méthodes similaires bien qu'ils permettent aussi de préciser le presse-papier à utiliser :

textbuffer.copy_clipboard(clipboard)

Le texte sélectionné sera copié dans le Clipboard indiqué par clipboard.

textbuffer.cut_clipboard(clipboard, default_editable)

Le texte sélectionné sera copié dans clipboard. Si le paramètre default_editable vaut TRUE, le texte sélectionné sera aussi effacé du TextBuffer. Sinon, la méthode cut_clipboard() fonctionnera comme la méthode copy_clipboard() .

textbuffer.paste_clipboard(clipboard, override_location, default_editable)

Si le paramètre default_editable vaut TRUE, le contenu de clipboard sera inséré dans le TextBuffer à l'endroit indiqué par le paramètre override_location du TextIter.

Si default_editable vaut FALSE, la méthode paste_clipboard() n'insérera pas le contenu de clipboard. Si le paramètre override_location vaut None, le contenu de clipboard sera inséré à l'emplacement du curseur.

Les TextBuffer possèdent aussi deux méthodes pour gérer un ensemble de Clipboard qui sont automatiquement affectées avec le contenu de la sélection en cours :

textbuffer.add_selection_clipboard(clipboard)
textbuffer.remove_selection_clipboard(clipboard)

Quand un TextBuffer est ajouté à un TextView, le presse-papiers "PRIMARY" est automatiquement ajouté aux presse-papiers choisis. L'application peut ajouter d'autres presse-papiers (par exemple, le presse-papier "CLIPBOARD").

XV-A-3 - Affecter des données à un Clipboard

Il est possible d'affecter des données au Clipboard par programme en utilisant l'une de ces méthodes :

1. presse_papier.set_with_data(cibles, get_func, clear_func, donnees_utilisateur)

3. presse_papier.set_text(text, len=-1)

La méthode set_with_data() indique quelles cibles de données sélectionnées sont acceptées et fournit les fonctions (get_func et clear_func) qui sont appelées quand les données le sont également ou quand les données du presse-papiers sont modifiées. Le paramètre donnees_utilisateur est transmis à get_func où à clear_func quand elles sont appelées. Le paramètre cibles est une liste de 3-tuples contenant :

- une chaine représentant une cible acceptée par le presse-papiers ;

- une valeur de drapeau utilisée pour le glisser/déposer - on utilise 0 ;

- un entier affecté à l'application qui est transmis comme paramètre au gestionnaire de signal pour aider à identifier le type de cible.

Les paramètres de get_func et de clear_func sont :

1. def get_func(presse_papier, donnees_selection, info, data):

3. def clear_func(presse_papier, data):

où le paramètre presse_papier est le Clipboard, le paramètre donnees_selection est un objet SelectionData contenant les données, le paramètre info est l'entier assigné à l'application, associé à une cible et le paramètre data représente les données utilisateur.

La méthode set_text() est une méthode commode qui utilise la méthode set_with_data() pour placer les données texte dans un Clipboard avec pour cibles : "STRING", "TEXT", "COMPOUND_TEXT", et "UTF8_STRING". Elle utilise les fonctions internes get et clear pour gérer les données. Cette méthode est équivalente à :

1. def my_set_text(self, texte, len=-1):

2. targets = [ ("STRING", 0, 0),

3. ("TEXT", 0, 1),

4. ("COMPOUND_TEXT", 0, 2),

5. ("UTF8_STRING", 0, 3) ]

6. def text_get_func(presse_papier, donnees_selection, info, data):

7. selection.set_text(data)

8. return

9. def text_clear_func(presse_papier, data):

10. del data

11. return

12. self.set_with_data(cibles, text_get_func, text_clear_func, texte)

13. return

Une fois les données placées dans le presse-papiers, elles demeurent disponibles jusqu'à la fermeture de l'application ou jusqu'à leur remplacement.

Pour obtenir le comportement habituel du couper dans un presse-papiers, l'application doit supprimer le texte ou l'objet sélectionné après l'avoir recopié dans le presse-papiers.

XV-A-4 - Récupérer le contenu du Clipboard

Le contenu du Clipboard peut être récupéré avec la méthode suivante :

clipboard.request_contents(target, callback, user_data=None)

Les contenus indiqués par target sont récupérés de manière asynchrone dans la fonction indiquée par le paramètre callback qui est appelé avec user_data. le paramètre callback a la forme :

def callback(presse_papier, donnees_selection, data):

où donnees_selection est un objet SelectionData conservant le contenu du presse_papier. data contient les données utilisateur. La méthode request_contents() est la façon la plus commune de récupérer le contenu d'un Clipboard. La méthode suivante est commode pour retrouver le contenu texte d'un Clipboard :

clipboard.request_text(callback, user_data=None)

La chaine texte est renvoyée à la fonction de rappel à la place d'un objet Selectiondata. Vous pouvez vérifier quelles cibles sont disponibles sur le Clipboard en employant la méthode :

clipboard.request_targets(callback, user_data=None)

Les cibles sont retournées à la fonction de rappel comme un tuple d'objets gtk.gdk.Atom.

Deux méthodes pratiques sont fournies pour renvoyer le contenu du Clipboard de manière synchrone :

1. donnees_selection = clipboard.wait_for_contents(cible)

3. texte = clipboard.wait_for_text()

XV-A-5 - Exemple de Clipboard

Pour illustrer l'utilisation d'un Clipboard, le programme clipboard.py récupère les éléments de texte qui sont coupés ou copiés vers le presse-papiers "CLIPBOARD" et sauvegarde les dix dernières entrées du presse-papiers. Il y a dix boutons qui permettent d'accéder au texte des entrées sauvegardées. Le texte du bouton affiche les seize premiers caractères du texte sauvé et les infobulles montrent les cibles que la zone de texte avait à l'origine. Quand on clique sur un bouton d'entrée, le texte sauvegardé associé s'inscrit dans la fenêtre de texte ; il est modifiable. Le bouton sous la fenêtre de texte sauvegarde le contenu actuel de la fenêtre dans le presse-papiers.

Figure 15.1, « Exemple de Clipboard » montre le programme clipboard.py en action :

Figure 15.1. Exemple de Clipboard

Le programme d'exemple sonde le presse-papiers toutes les 1,5 seconde pour voir si le contenu a changé. Le programme peut être modifié pour dupliquer la totalité de contenu ciblé et prendre le contrôle en employant la méthode set_with_data(). Plus tard, quand un autre programme place du contenu dans le presse-papiers, le paramètre clear_func sera appelé, il pourra être utilisé pour recharger le contenu du presse-papiers et en reprendre le contrôle .

XVI - Chapitre 16. Nouveaux Widgets dans PyGTK 2.4

Quelques nouveaux widgets et objets de contrôle ont été ajoutés dans PyGTK 2.4, incluant :

- Action (Action), RadioAction (Action d'exclusion), ToggleAction (Action binaire) - objets qui représentent des actions qu'un utilisateur peut prendre. Les actions contiennent l'information nécessaire pour créer des widgets délégués (proxy) (par exemple : des icônes, des éléments de menu ou des éléments de barre d'outils) ;

- ActionGroup - un objet contenant des Actions qui ont une certaine similarité, par exemple, les actions ouvrir, fermer et imprimer un document ;

- Border - un objet contenant les valeurs pour une bordure ;

- ColorButton - un bouton pour lancer un dialogue de sélection de couleurs ;

- ComboBox - un widget proposant une liste d'éléments parmi lesquels choisir. Il remplace l'OptionMenu ;

- ComboBoxEntry - un widget fournissant un champ d'entrée de texte avec une liste d'éléments parmi lesquels choisir. Il remplace le Combo ;

- EntryCompletion - un objet permettant la complétion dans un champ de saisie Entry ;

- Expander - un container qui peut montrer ou cacher son enfant en réponse à un clic sur son bouton ;

- FileChooser - une interface pour le choix de fichiers ;

- FileChooserWidget - un widget implémentant l'interface FileChooser. Il remplace le widget FileSelection ;

- FileChooserDialog - une fenêtre de dialogue utilisée pour les actions "Fichier/Ouvrir" et "Fichier/Enregistrer". Elle remplace le FileSelectionDialog ;

- FileFilter - un objet servant à filtrer les fichiers selon un jeu interne de règles ;

- FontButton - un bouton qui ouvre la fenêtre de sélection de polices FontSelectionDialog ;

- IconInfo - un objet contenant des informations sur une icône d'un IconTheme ;

- IconTheme - un objet permettant une consultation des icônes avec leur nom et leur taille ;

- ToolItem, ToolButton, RadioToolButton, SeparatorToolItem, ToggleToolButton - widgets qui peuvent être inclus dans une barre d'outils Toolbar. Ils remplacent les items de barre d'outils Toolbar précédents ;

- TreeModelFilter - un objet fournissant un outil puissant pour transformer la représentation d'un modèle TreeModel sous-jacent. Il est décrit dans Section 14.10.2, « Le TreeModelFilter » ;

- UIManager - un objet fournissant un moyen de construire des menus et des barres d'outils à partir d'une description d'interface utilisateur (UI) au format XML. Il possède aussi des méthodes pour gérer la fusion et séparation de multiples descriptions d'interfaces utilisateur.

XVI-A - Les objets Action et groupes d'Actions (ActionGroup)

Les objets Action et ActionGroup collaborent pour fournir les images, textes, fonctions de rappel et accélérateurs aux menus et barres d'outils des applications. L'UIManager utilise les Action et ActionGroup pour construire de manière automatique des barres de menus et des barres d'outils basées sur une description XML. Il est beaucoup plus facile de créer et remplir les menus et barres d'outils en utilisant le UIManager décrit dans une section ultérieure. Les sections suivantes sur les objets Action et ActionGroup décrivent comment utiliser directement ces objets, mais je recommande l'usage, dans la mesure du possible, de l'UIManager.

XVI-A-1 - Actions

Un objet Action représente une action que l'utilisateur peut effectuer dans une interface d'application. Il contient l'information utilisée par les éléments de l'interface (par exemple, des éléments de MenuItem ou de Toolbar) pour présenter l'action à l'utilisateur. Il existe deux sous-classes d'Action :

ToggleAction

Un objet Action qui peut être commuté entre deux états.

RadioAction

Des objets Action qui peuvent être regroupés, mais dont un seul peut être activé.

Par exemple, l'élément de menu standard Fichier ? Quitter peut être représenté par une icône, un texte mnémonique ou un raccourci-clavier. Quand il est activé, l'élément de menu déclenche une fonction de rappel qui peut fermer l'application. En outre, un bouton Quitter d'une barre d'outils Toolbar peut partager les mêmes icônes, texte mnémonique et fonction de rappel. Ces deux éléments d'interface peuvent être des éléments délégués de la même Action.

Les widgets ordinaires Button, ToggleButton et RadioButton peuvent aussi agir comme une Action, quoique le UIManager ne possède aucun support pour ceux-ci.

XVI-A-1-a - Créer des Actions

Une Action peut être créée en utilisant le constructeur :

action = gtk.Action(name, label, tooltip, stock_id)

où name est une chaine employée pour identifier l'Action dans un ActionGroup ou dans des descriptions d'UIManager. Le label et le tooltip sont des chaines utilisées comme étiquette et infobulle dans des widgets délégués. Si label vaut None alors stock_id doit être une chaine indiquant un élément de Stock pour en récupérer le label. Si tooltip vaut None, l'Action n'aura pas d'infobulle.

Comme nous le verrons dans la Section 16.1.2, « ActionGroup », il est beaucoup plus facile de créer des objets Action en utilisant les méthodes commodes de ActionGroup :

1. actiongroup.add_actions(entries, user_data=None)

2. actiongroup.add_toggle_actions(entries, user_data=None)

3. actiongroup.add_radio_actions(entries, value=0, on_change=None, user_data=None)

On en reparlera plus loin, mais d'abord, décrivons comment utiliser une Action avec un Button pour illustrer les opérations élémentaires de connexion d'une Action à un widget délégué.

XVI-A-1-b - Utiliser les Actions

La procédure de base pour l'utilisation d'une Action avec un Bouton proxy est illustrée par le programme exemple simpleaction.py. Le Bouton est connecté à l'Action par la méthode :

action.connect_proxy(proxy)

où le proxy est un widget élément de menu MenuItem, élément de barre d'outils ToolItem ou un bouton Button.

Une action (Action) possède un signal, le signal "activate" qui est déclenché quand l'Action est activée, normalement par le résultat d'une activation d'un widget proxy (par exemple, on a cliqué sur un bouton de barre d'outils ToolButton). Il faut juste connecter une fonction de rappel à ce signal pour contrôler l'activation de n'importe lequel des widgets délégués (proxy).

Voici le code source pour le programme exemple simpleaction.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. import pygtk

4. pygtk.require('2.0')

5. import gtk

7. class SimpleAction:

8. def __init__(self):

9. # Créer la fenêtre de premier niveau

10. window = gtk.Window()

11. window.set_size_request(70, 30)

12. window.connect('destroy', lambda w: gtk.main_quit())

14. # Créer un groupe de raccourcis

15. groupe_rapide = gtk.AccelGroup()

16. # Ajouter le groupe à la fenêtre de premier niveau

17. window.add_accel_group(groupe_rapide)

19. # Créer une action_quitte pour quitter le programme avec un élément de Stock

20. action_quitte = gtk.Action('Quitter', None, None, gtk.STOCK_QUIT)

21. # Connecter une fonction de rappel à l'action

22. action_quitte.connect('activate', self.fonct_quitte)

24. # Créer un ActionGroup appelé ActionSimple

25. groupe_actions = gtk.ActionGroup('ActionSimple')

26. # Ajouter l'action à groupe_actions avec un raccourci-clavier

27. # "None" signifie utiliser l'accélérateur de l'élément Stock

28. groupe_actions.add_action_with_accel(action_quitte, None)

30. ## Lier l'action au groupe_rapide

31. action_quitte.set_accel_group(groupe_rapide)

33. # Connecter l'accélérateur à l'action

34. action_quitte.connect_accelerator()

36. # Créer un bouton pour l'utiliser comme autre widget délégué

37. bouton_quitter = gtk.Button()

38. # l'ajouter à la fenêtre

39. window.add(bouton_quitter)

41. # Lier l'action à son widget délégué

42. action_quitte.connect_proxy(bouton_quitter)

44. window.show_all()

45. return

47. def fonct_quitte(self, b):

48. print 'Quitter le programme'

49. gtk.main_quit()

51. if __name__ == '__main__':

52. sa = SimpleAction()

53. gtk.main()

L'exemple crée une Action (ligne 20) qui utilise un élément Stock Item pour récupérer le texte de l'étiquette avec un mnémonique, une icône, un raccourci-clavier et un domaine de traduction. Si on n'utilise pas de Stock Item, il faut indiquer un label à la place. Ligne 22, on connecte le signal "activate" du paramètre action à la méthode self.quit_cb() afin qu'elle soit appelée quand l'action Action est activée par le bouton quitbutton. L'exemple simpleaction.py utilise pas mal de code (lignes 15, 17, 31 et 34) pour installer le raccourci pour le bouton. La procédure est la même pour les éléments de menu MenuItem, les barres d'outils Toolbar et les éléments de barre d'outils ToolItem.

La Figure 16.1, « Exemple d'Action simple » montre le résultat du programme simpleaction.py

Figure 16.1. Exemple d'Action simple

XVI-A-1-c - Créer des widgets délégués (proxy)

Dans la section précédente, nous avons vu qu'un widget existant pouvait être connecté à une Action comme intermédiaire. Dans cette section nous verrons comment un widget délégué peut être créé par les méthodes d'Action :

1. menuitem = action.create_menu_item()

3. toolitem = action.create_tool_item()

Le programme basicaction.py montre un élément de menu MenuItem, un bouton de barre d'outils ToolButton et un bouton Button partageant une même Action. Le MenuItem et le ToolButton ont été créés par les méthodes précédentes. Voici le code source du programme basicaction.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. import pygtk

4. pygtk.require('2.0')

5. import gtk

7. class BasicAction:

8. def __init__(self):

9. # Créer la fenêtre de premier niveau

10. window = gtk.Window()

11. window.connect('destroy', lambda w: gtk.main_quit())

12. vbox = gtk.VBox()

13. vbox.show()

14. window.add(vbox)

16. # Créer un groupe de raccourcis

17. groupe_rapide = gtk.AccelGroup()

18. # Ajouter le groupe à la fenêtre de premier niveau

19. window.add_accel_group(groupe_rapide)

21. # Créer une action_quitte pour quitter le programme avec un élément de Stock

22. action_quitte = gtk.Action('Quitter', '_Quittez moi ! ', 'Quitter le programme',

23. gtk.STOCK_QUIT)

24. action_quitte.set_property('short-label', '_Quitter')

25. # Connecter une fonction de rappel à l'action

26. action_quitte.connect('activate', self.fonct_quitte)

28. # Créer un ActionGroup appelé ActionBasic

29. groupe_actions = gtk.ActionGroup('ActionBasic')

30. # Ajouter l'action à groupe_actions avec un raccourci-clavier

31. # "None" signifie utiliser le raccourci de l'élément Stock

32. groupe_actions.add_action_with_accel(action_quitte, None)

34. # Lier l'action au groupe_rapide

35. action_quitte.set_accel_group(groupe_rapide)

37. # Créer une barre de menu

38. barremenu = gtk.MenuBar()

39. barremenu.show()

40. vbox.pack_start(barremenu, False)

42. # Créer une action "Fichier" et l'élément de menu correspondant

43. action_fichier = gtk.Action('Fichier', '_Fichier', None, None)

44. groupe_actions.add_action(action_fichier)

45. elmenu_fichier = action_fichier.create_menu_item()

46. barremenu.append(elmenu_fichier)

48. # Créer le menu Fichier

49. menu_fichier = gtk.Menu()

50. elmenu_fichier.set_submenu(menu_fichier)

52. # Créer un élément de menu délégué

53. menuitem = action_quitte.create_menu_item()

54. menu_fichier.append(menuitem)

56. # Créer une barre d'outils

57. barreoutils = gtk.Toolbar()

58. barreoutils.show()

59. vbox.pack_start(barreoutils, False)

61. # Créer un élément de barre d'outils délégué

62. elbarre1 = action_quitte.create_tool_item()

63. barreoutils.insert(elbarre1, 0)

65. # Créer et placer l'étiquette

66. label = gtk.Label('''

67. Choisir Fichier->quittez-moi ! ou

68. cliquer sur le bouton Quitter de la barre d'outils ou

69. cliquer sur le bouton Quitter du bas ou

70. taper Control+q

71. pour sortir.

72. ''')

73. label.show()

74. vbox.pack_start(label)

76. # Créer un bouton pour l'utiliser comme autre widget délégué

77. bouton_quitter = gtk.Button()

78. # l'ajouter à la fenêtre

79. vbox.pack_start(bouton_quitter, False)

81. # Lier l'action à son widget délégué

82. action_quitte.connect_proxy(bouton_quitter)

83. # On doit placer l'infobulle après l'ajout de l'élément à la barre d'outils

84. action_quitte.set_property('tooltip', action_quitte.get_property('tooltip'))

85. les_infobulles = gtk.Tooltips()

86. les_infobulles.set_tip(bouton_quitter, action_quitte.get_property('tooltip'))

88. window.show()

89. return

91. def fonct_quitte(self, b):

92. print 'On quitte le programme'

93. gtk.main_quit()

95. if __name__ == '__main__':

96. ba = BasicAction()

97. gtk.main()

Cet exemple introduit un groupe d'actions ActionGroup contenant les Action utilisées dans ce programme. La Section 16.1.2, « ActionGroup » entrera plus dans le détail de l'utilisation des ActionGroup.

Le code des lignes 9-14 crée une fenêtre de premier niveau contenant une boite VBox. Lignes 16-35, on crée l' Action "Quitter" semblable à celle du programme simpleaction.py et on l'ajoute (ligne 32) avec le raccourci gtk.STOCK_QUIT à l' ActionGroup "BasicAction" créé ligne 29. Noter que, contrairement au programme simpleaction.py, il n'est pas nécessaire d'appeler la méthode connect_accelerator(), celle-ci étant appelée automatiquement quand la méthode create_menu_item() est appelée ligne 53.

Lignes 38-40, on crée une barre de menu MenuBar et on la place dans la VBox. Lignes 43-44, on crée une Action (action_fichier) pour le menu Fichier et on l'ajoute au actiongroup. Les éléments de menu Fichier et Quitter sont créés lignes 45 et 53 et ajoutés à la barre de menu barremenu et au menu menu_fichier en ligne 46 et 54.

De la même manière, on crée une Toolbar et on l'ajoute à la VBox aux lignes 57-59. Le ToolItem délégué est créé et ajouté à la Toolbar barreoutils ligne 62-63. Noter que l'infobulle de l'Action doit être établie (ligne 84) après l'ajout du ToolItem à la Toolbar pour être utilisable. De même l'infobulle du Button doit être ajoutée manuellement (lignes 84-86).

La Figure 16.2, « Exemple d'Action élémentaire » montre le programme basicaction.py en action.

Figure 16.2. Exemple d'Action élémentaire

Un widget délégué peut être déconnecté d'une Action par la méthode :

action.disconnect_proxy(proxy)

XVI-A-1-d - Propriétés des Actions

Une Action possède un certain nombre de propriétés qui contrôlent l'affichage et le fonctionnement de ses widgets délégués. Parmi celles-ci, les plus importantes sont les propriétés "sensitive" et "visible". La propriété "sensitive" détermine la sensibilité des widgets délégués. Si "sensitive" est FALSE les widgets délégués ne sont pas activables et seront normalement affichés "grisés". De même, la propriété "visible" détermine si les widgets délégués seront visibles. Si la propriété "visible" d'une Action est FALSE, ses widgets délégués seront cachés.

Comme on le verra dans la prochaine section, la sensibilité ou la visibilité d'une Action est également contrôlée par la sensibilité ou la visibilité de l'ActionGroup auquel elle appartient. Par conséquent, pour qu'une Action soit sensible (ou visible), elle-même et son ActionGroup doivent être sensibles (ou visibles) tous les deux. Pour déterminer la sensibilité ou visibilité effective d'une Action, il faut utiliser les méthodes suivantes :

1. resultat = action.is_sensitive()

3. resultat = action.is_visible()

Le nom assigné à une Action est contenu dans sa propriété "name" qui est établie lors de la création de l'Action. On peut retrouver ce nom par la méthode :

nom = action.get_name()

Les autres propriétés qui contrôlent l'affichage des widgets délégués d'une Action sont :

"hide-if-empty"

Si vaut TRUE, les contrôles délégués de menus vides sont cachés pour cette action.

"is-important"

Si vaut TRUE, les délégués ToolItem proxies pour cette action affichent le texte dans le mode gtk.TOOLBAR_BOTH_HORIZ.

"visible-horizontal"

Si vaut TRUE, le ToolItem est visible quand la barre d'outils est horizontale.

"visible-vertical"

Si vaut TRUE, le ToolItem est visible quand la barre d'outils est verticale.

Autres propriétés intéressantes :

"label"

L'étiquette utilisée pour les éléments de menu et les boutons qui déclenchent cette action.

"short-label"

Une étiquette plus courte qui peut être utilisée sur les boutons de barres d'outils et les boutons simples.

"stock-id"

L'élément de Stock à utiliser pour récupérer l'icône, l'étiquette et le raccourci à utiliser dans les widgets qui représentent cette action.

"tooltip"

Une infobulle pour cette action.

Noter que le programme d'exemple basicaction.py réécrit l'étiquette du gtk.STOCK_QUIT avec "_Quitter moi !" et définit l'étiquette courte à "_Quitter". L'étiquette courte est utilisée pour le bouton de barre d'outils ToolButton et pour le bouton isolé. L'étiquette complète est utilisée pour le l'élément de menu MenuItem. Noter aussi que l'infobulle ne peut pas être définie pour un ToolItem tant qu'elle n'est pas ajoutée à la barre d'outils Toolbar.

XVI-A-1-e - Actions et Accélérateurs

Une Action a trois méthodes qui peuvent être employées pour établir un accélérateur (raccourci-clavier) :

1. action.set_accel_group(accel_group)

3. action.set_accel_path(accel_path)

5. action.connect_accelerator()

Celles-ci, outre la méthode gtk.ActionGroup.add_action_with_accel(), devraient couvrir la plupart des cas de création d'accélérateur.

Il faut toujours définir un AccelGroup pour une Action. La méthode set_accel_path() est appelée par la méthode gtk.ActionGroup.add_action_with_accel(). Si on utilise la méthode set_accel_path(), le chemin de l'accélérateur doit avoir le format par défaut : "<Actions>/nom_du_groupe_actions/nom_de_action". Enfin, la méthode connect_accelerator() est appelée pour compléter la mise en place de l'accélérateur.

Une Action doit avoir un AccelGroup et un chemin d'accélérateur associé avant que la méthode connect_accelerator() soit appelée.

Puisque la méthode connect_accelerator() peut être appelée plusieurs fois (par exemple, une fois pour chaque widget délégué), le nombre d'appels est compté, ainsi un nombre égal d'appels à la méthode disconnect_accelerator() doit être réalisé avant d'éliminer l'accélérateur.

Comme illustré dans les programmes d'exemple précédents, un accélérateur d'Action peut être employé par tous les widgets délégués. Une Action doit faire partie d'un ActionGroup afin d'utiliser le chemin d'accélérateur par défaut avec ce format : "<Actions>/nom_du_groupe_actions/nom_de_action". La manière la plus simple d'ajouter un accélérateur est d'utiliser la méthode gtk.ActionGroup.add_action_with_accel() et la procédure générale suivante.

- Créer un AccelGroup et l'ajouter à la fenêtre de premier niveau.

- Créer un nouveau ActionGroup.

- Créer une Action en précisant un élément de Stock avec un accélérateur.

- Ajouter l'Action à l'ActionGroup avec la méthode gtk.ActionGroup.add_action_with_accel() en indiquant None pour utiliser l'accélérateur de l'élément de Stock ou une chaine d'accélérateur acceptable par la méthode gtk.accelerator_parse().

- Définir l'AccelGroup pour l'Action en utilisant la méthode gtk.Action.set_accel_group().

- Terminer la configuration de l'accélérateur avec la méthode gtk.Action.connect_accelerator().

Tous les widgets délégués créés ou connectés à l'Action utiliseront le raccourci.

XVI-A-1-f - Actions à bascule

Comme mentionnée précédemment, une action à bascule ToggleAction est une sous-classe d'Action qui peut être basculée entre deux états. Le constructeur pour une ToggleAction prend les mêmes paramètres que pour une Action :

toggleaction = gtk.ToggleAction(name, label, tooltip, stock_id)

En supplément des méthodes pour Action, les méthodes suivantes pour ToggleAction :

toggleaction.set_active(is_active)
is_active = toggleaction.get_active()

spécifient et récupèrent la valeur actuelle de toggleaction. Le paramètre is_active contient une valeur booléenne.

On peut se connecter au signal "toggled" en spécifiant une fonction de rappel qui a la forme :

def toggled_cb(toggleaction, user_data)

Le signal "toggled" est émis lorsque le ToggleAction change d'état.

Le widget délégué MenuItem d'un ToggleAction sera affiché par défaut comme un CheckMenuItem. Pour obtenir l'affichage du MenuItem comme un RadioMenuItem, il faut donner à la propriété "draw-as-radio" la valeur TRUE par la méthode :

toggleaction.set_draw_as_radio(draw_as_radio)

On peut utiliser la méthode suivante pour déterminer si les éléments MenuItem d'une action ToggleAction seront affichés comme des RadioMenuItem :

draw_as_radio = toggleaction.get_draw_as_radio()

XVI-A-1-g - Actions avec exclusivité

Une action exclusive RadioAction est une sous-classe d'action ToggleAction qui peut être groupée de manière à ce qu'un seul RadioAction soit actif en même temps. Les widgets délégués correspondants sont le RadioMenuItem pour la barre de menu et le RadioToolButton pour la barre d'outils.

Le constructeur d'un RadioAction accepte les mêmes arguments qu'une Action avec l'ajout d'une valeur entière unique qui permet d'identifier le RadioAction actif dans le groupe :

radioaction = gtk.RadioAction(name, label, tooltip, stock_id, value)

Le groupe d'un RadioAction peut être créé par la méthode :

radioaction.set_group(group)

où group est un autre RadioAction auquel doit se rattacher le radioaction. Le groupe contenant un RadioAction peut être obtenu par la méthode :

group = radioaction.get_group()

qui renvoie une liste du groupe d'objets RadioAction qui inclut le radioaction.

La valeur de l'élément actuellement actif du groupe peut s'obtenir par la méthode :

active_value = radioaction.get_current_value()

On peut connecter une fonction de rappel au signal "changed" pour être prévenu lorsque l'élément actif du RadioAction a été modifié. Noter qu'il faut se connecter à un seul des objets RadioAction pour surveiller les changements. La fonction de rappel est :

def changed_cb(radioaction, current, user_data)

où current est le RadioAction du groupe qui est actuellement actif.

XVI-A-1-h - Un exemple avec Actions

Le programme exemple actions.py montre l'utilisation d'objets Action, ToggleAction et RadioAction. Figure 16.3, « Exemple avec Actions » illustre le programme en cours de fonctionnement :

Figure 16.3. Exemple avec Actions

Cet exemple est suffisamment proche de basicaction.py, une description détaillée est inutile.

XVI-A-2 - ActionGroup

Comme indiqué dans la section précédente, des objets d'Action apparentés devraient être ajoutés à un groupe d'actions ActionGroup pour fournir un contrôle commun de leur visibilité et sensibilité. Par exemple, dans une application de traitement de texte, les éléments de menu et les boutons de la barre d'outils demandant la justification du texte peuvent être contenus dans un ActionGroup. On s'attend à ce qu'une interface utilisateur possède des objets ActionGroup multiples qui couvrent des aspects différents de l'application. Par exemple, les actions globales comme créer de nouveaux documents, ouvrir et sauvegarder un document ou quitter l'application forment probablement un ActionGroup tandis que des actions telles que modifier la vue du document en formeraient un autre.

XVI-A-2-a - Créer des ActionGroups

On crée un ActionGroup avec le constructeur :

actiongroup = gtk.ActionGroup(name)

où name est un nom unique pour l'ActionGroup. Le nom doit être unique, car il est utilisé lors de la création des raccourcis des objets Action qu'il contient.

Le paramètre ActionGroup peut être obtenu par la méthode :

name = actiongroup.get_name()

ou en récupérant le contenu de la propriété "name".

XVI-A-2-b - Ajouter des Actions

Comme on l'a montré dans Section 16.1.1, « Actions », une Action préexistante peut être ajoutée à un ActionGroup en utilisant une des méthodes :

1. actiongroup.add_action(action)

3. actiongroup.add_action_with_accel(action, accelerator)

où le paramètre action est l'Action qui doit être ajoutée et le paramètre accelerator est une chaine précisant un raccourci acceptable pour la fonction gtk.accelerator_parse(). Si accelerator vaut None, l'accélérateur (s'il existe) associé à la propriété "stock-id" du paramètre action sera utilisé. Comme on l'a vu précédemment, il vaut mieux utiliser la méthode add_action_wih_accel() si on souhaite utiliser les raccourcis-clavier.

Le groupe ActionGroup offre trois méthodes pour faciliter la création et l'ajout d'objets Action à un groupe ActionGroup :

1. actiongroup.add_actions(entries, user_data=None)

3. actiongroup.add_toggle_actions(entries, user_data=None)

5. actiongroup.add_radio_actions(entries, value=0, on_change=None, user_data=None)

Le paramètre entries est une liste de tuples d'entrée d'action qui fournissent l'information utilisée pour créer les actions ajoutées à l'ActionGroup. Le RadioAction avec la valeur value est réglé au départ comme actif. Le paramètre on_change représente la fonction de rappel connectée au signal "changed" du premier RadioAction du groupe. La signature de on_changed est :

def on_changed_cb(radioaction, current, user_data)

Les tuples d'entrées pour les objets Action contiennent :

- le nom de l'action. Doit être indiqué ;

- l'identificateur de Stock pour cette action. Optionnel, sa valeur par défaut est None si l'étiquette est indiquée ;

- l'étiquette pour l'action. Optionnel, sa valeur par défaut est None si l'identificateur de Stock est précisé ;

- le raccourci pour l'action, dans un format compris par la fonction gtk.accelerator_parse(). Optionnel, sa valeur par défaut est None ;

- l'infobulle pour l'action. Optionnel, sa valeur par défaut est None ;

- la fonction de rappel appelée quand l'action est activée. Optionnel, sa valeur par défaut est None

Il faut indiquer au minimum une valeur pour le champ name et une valeur soit pour l'identificateur stock id, soit pour le champ étiquette label. Si on indique une étiquette, alors, on peut donner une valeur None à l'identificateur de stock si on ne l'utilise pas. Par exemple, la méthode suivante :

actiongroup.add_actions([('quitter', gtk.STOCK_QUIT, '_Quittter moi !', None,
'Quitter le programme', quit_cb)])

ajoute une action à actiongroup pour quitter un programme.

Les tuples d'entrée pour les objets ToggleAction sont semblables aux tuples d'entrée d'Action, mais il y a un champ facultatif flag supplémentaire qui contient une valeur booléenne indiquant si l'action est active.La valeur par défaut pour le champ flag est FALSE. Par exemple, la méthode suivante :

actiongroup.add_toggle_actions([('volume, None, '_Volume', '<control>v',
'Niveau de son', mute_cb, True)])

ajoute un ToggleAction à l'actiongroup et l'initialise comme actif.

Les tuples d'entrée pour le RadioAction sont semblables à ceux de l'Action,mais avec un champ value à la place du champ callback :

- le nom de l'action. Doit être indiqué ;

- l'identificateur de Stock pour cette action. Optionnel, sa valeur par défaut vaut None si le label est précisé ;

- le label pour l'action. Optionnel, sa valeur par défaut vaut None si l'identificateur de Stock est précisé ;

- l'accélérateur pour l'action, dans un format compris par la fonction gtk.accelerator_parse(). Optionnel, sa valeur par défaut vaut None ;

- l'infobulle pour l'action. Optionnel, sa valeur par défaut vaut None ;

- la valeur à attribuer au radio-action. Optionnel, sa valeur par défaut vaut None. Devrait toujours être indiqué dans les applications.

Par exemple le fragment de code suivant :

1. radioactionlist = [('ma', None, 'M_A', '<control>a', 'Mode MA', 0)

2. ('mf', None, 'M_F', '<control>f', 'Mode MF', 1)

3. ('blu', None, 'B_LU', '<control>b', 'Mode BLU', 2)]

4. actiongroup.add_radio_actions(radioactionlist, 0, changed_cb)

crée trois objets RadioAction et fixe l'action active initiale à 'ma' et la fonction de rappel qui est appelée quand l'une de ces actions est activée à changed_cb.

XVI-A-2-c - Récupérer une Action

Une Action peut être retrouvée par son nom à partir d'un ActionGroup en utilisant la méthode :

action = actiongroup.get_action(action_name)

Une liste de tous les objets Action contenus dans un ActionGroup peut être recherchée en utilisant la méthode :

actionlist = actiongroup.list_actions()

XVI-A-2-d - Controler les Actions

La sensibilité et la visibilité de tous les objets Action d'un ActionGroup peuvent être contrôlées en fixant les valeurs de propriétés associées. Les méthodes suivantes récupèrent et fixent les propriétés :

1. is_sensitive = actiongroup.get_sensitive()

2. actiongroup.set_sensitive(sensitive)

4. is_visible = actiongroup.get_visible()

5. actiongroup.set_visible(visible)

Enfin, il est possible de supprimer une Action d'un ActionGroup par la méthode :

actiongroup.remove_action(action)

XVI-A-2-e - Exemple de ActionGroup

Le programme d'exemple actiongroup.py reprend la barre de menu et la barre d'outils du programme actions.py en utilisant les méthodes d'ActionGroup. En outre le programme fournit des boutons pour contrôler la sensibilité et la visibilité des éléments du menu et de la barre d'outils. La Figure 16.4, « Exemple avec ActionGroup » montre le programme en fonctionnement :

Figure 16.4. Exemple avec ActionGroup

XVI-A-2-f - Les signaux de ActionGroup

L'application peut surveiller la connexion et le retrait de widgets délégués aux objets Action dans un ActionGroup en utilisant les signaux "connect-proxy" et "disconnect-proxy". Les fonctions de rappel sont de la forme :

1. def connect_proxy_cb(actiongroup, action, proxy, user_params)

3. def disconnect_proxy_cb(actiongroup, action, proxy, user_params)

Par exemple, on peut vouloir surveiller ces changements pour effectuer quelques modifications supplémentaires aux propriétés du nouveau widget délégué quand il est connecté ou pour mettre à jour quelque autre partie de l'interface utilisateur quand un widget délégué est déconnecté.

Les signaux "pre-activate" et "post-activate" autorisent l'application à réaliser un traitement additionnel juste avant ou après l'activation d'une action. Les fonctions de rappel sont de la forme :

1. def pre_activate_cb(actiongroup, action, user_params)

3. def post_activate_cb(actiongroup, action, user_params)

Ces signaux sont surtout utilisés par le UIManager pour fournir une notification globale pour tous les objets Action des objets ActionGroup qu'il utilise.

XVI-B - Les listes déroulantes (ComboBox) et listes déroulantes avec saisie (ComboBoxEntry)

XVI-B-1 - Le widget ComboBox

La boite déroulante ComboBox remplace l'OptionMenu par un widget puissant qui utilise un TreeModel (généralement un ListStore) pour obtenir la liste d'éléments à afficher. Le ComboBox applique l'interface du CellLayout qui fournit un certain nombre de méthodes pour gérer l'affichage des éléments de liste. Un ou plusieurs CellRenderers peuvent être placés dans un ComboBox pour personnaliser l'affichage des éléments de liste.

XVI-B-1-a - Utiliser la boite déroulante de base

La manière simple de créer et remplir un ComboBox est d'utiliser la fonction suivante :

boitederoul = gtk.combo_box_new_text()

Cette fonction crée un ComboBox et son ListStore associé et le place avec un CellRendererText. On utilise les méthodes suivantes pour remplir ou supprimer le contenu du ComboBox et de son ListStore :

1. boitederoul.append_text(text)

2. boitederoul.prepend_text(text)

3. boitederoul.insert_text(position, text)

4. boitederoul.remove_text(position)

où text est une chaine à ajouter au ComboBox et position est l'index où le text doit être inséré ou retiré. Dans la plupart des cas, on n'aura besoin que de cette fonction et de ces méthodes.

Le programme d'exemple comboboxbasic.py montre l'usage des fonctions et méthodes ci-dessus. La Figure 16.5, « Basic ComboBox » illustre le programme en cours :

Figure 16.5. Basic ComboBox

Malheureusement, les développeurs de GTK+ n'ont pas fourni de méthode commode pour récupérer l'élément actif. Ceci serait pourtant une méthode utile. Il faut créer la sienne, semblable à :

1. def get_active_text(boitederoul):

2. modele = boitederoul.get_model()

3. est_actif = boitederoul.get_active()

4. if est_actif < 0:

5. return None

6. return modele[est_actif][0]

L'index de l'élément actif est récupéré par la méthode :

est_actif = boitederoul.get_active()

L'élément actif peut être défini par la méthode :

boitederoul.set_active(index)

où index est un entier supérieur à -2. Si index vaut -1, il n'y a aucun élément actif et l'affichage du ComboBox est vierge. Si index est inférieur à -1, l'appel sera ignoré. Si index est supérieur à -1, l'élément de liste qui possède cette valeur d'index sera affiché.

On peut se connecter au signal "changed" d'un ComboBox pour être prévenu lorsque l'élément actif change. Le gestionnaire de "changed" a la forme :

def fct_rappel_change(combobox, ...):

où ... représente zéro ou plusieurs arguments transmis à la méthode GObject.connect().

XVI-B-1-b - Usage avancé de ComboBox

Créer un ComboBox par la fonction gtk.combo_box_new_text() correspond à peu près au code suivant :

1. liststore = gtk.ListStore(str)

2. boitederoul = gtk.ComboBox(liststore)

3. case = gtk.CellRendererText()

4. boitederoul.pack_start(case, True)

5. boitederoul.add_attribute(case, 'text', 0)

Pour utiliser les possibilités des divers objets des TreeModel et des CellRenderer, il faut créer le ComboBox par le constructeur :

combobox = gtk.ComboBox(model=None)

où model est un TreeModel. Lorsque l'on crée un ComboBox sans l'associer à un TreeModel, on peut ajouter celui-ci ensuite par la méthode :

boitederoul.set_model(model)

Le TreeModel associé peut être récupéré par la méthode :

modele = boitederoul.get_model()

Quelques-unes des possibilités réalisables avec un ComboBox

- Partager le même TreeModel avec un autre ComboBox et un autre TreeView.

- Afficher des images et du texte dans les éléments de liste du ComboBox.

- Utiliser un TreeStore ou un ListStore préexistant comme modèle des éléments de liste du ComboBox.

- Utiliser un TreeModelSort pour obtenir une liste classée dans le ComboBox.

- Utiliser un TreeModelFilter pour utiliser un sous-arbre d'un TreeStore comme la source pour les éléments de liste du ComboBox.

- Utiliser un TreeModelFilter pour utiliser un sous-ensemble des lignes d'un TreeStore ou d'un ListStore comme éléments de liste du ComboBox.

- Utiliser une fonction de données de cellule pour modifier ou synthétiser l'affichage des articles de liste.

L'utilisation des objets TreeModel et CellRenderer objects est exposée en détail dans Chapitre 14, Le widget TreeView.

Si on a un ComboBox avec beaucoup d'éléments de liste, ceux-ci peuvent être affichés dans une grille. Autrement, si on ne peut afficher la liste entière, celle-ci aura des flèches de défilement. La méthode suivante est utilisée pour fixer le nombre de colonnes à afficher :

boitederoul.set_wrap_width(width)

où width est le nombre de colonnes de la grille affichant les éléments de liste. Par exemple, le programme comboboxwrap.py affiche une liste de 50 éléments sur 5 colonnes. La Figure 16.6, « ComboBox avec colonnes »« ComboBox avec colonnes »« ComboBox avec colonnes » illustre le programme en action :

Figure 16.6. ComboBox avec colonnes

Avec un grand nombre d'éléments, c'est-à-dire au-delà de 50, l'utilisation de la méthode set_wrap_width() fournira une faible performance à cause du calcul de l'affichage de la grille. Pour avoir un aperçu de l'effet, modifier la ligne 18 du programme comboboxwrap.py pour afficher 150 éléments.

for n in range(150):

Lancer le programme pour obtenir une estimation du temps d'initialisation. Puis le modifier en commentant la ligne 17 :

#boitederoul.set_wrap_width(5)

Relancer le programme et chronométrer à nouveau. Il devrait s'exécuter beaucoup plus vite, à peu près 20 fois plus rapidement.

Outre la méthode get_active() décrite ci-dessus, il est possible d'obtenir un TreeIter désignant la ligne active en utilisant la méthode :

iter = boitederoul.get_active_iter()

On peut aussi établir l'élément de liste actif en utilisant un TreeIter avec la méthode :

boitederoul.set_active_iter(iter)

Les méthodes set_row_span_column() et set_column_span_column() sont supposées permettre la spécification du nombre de colonnes d'un TreeModel qui contient le nombre de lignes ou de colonnes sur lesquelles les éléments de liste doivent s'étendre dans la grille. Malheureusement, ces méthodes sont cassées dans GTK+ 2.4.

Puisque le ComboBox implémente l'interface CellLayout qui possède les mêmes capacités que le TreeViewColumn (voir Section 14.5, « TreeViewColumn » pour plus d'information). En bref, l'interface fournit :

1. boitederoul.pack_start(cell, expand=True)

2. boitederoul.pack_end(cell, expand=True)

3. boitederoul.clear()

Les deux premières méthodes placent un CellRenderer dans le ComboBox, la méthode clear() supprime tous les attributs de tous les CellRenderer.

Les méthodes suivantes :

1. comboboxentry.add_attribute(cell, attribute, column)

3. comboboxentry.set_attributes(cell, ...)

fixent les attributs pour les CellRenderer indiqués par cell. La méthode add_attribute() prend une chaine, nom d'attribut attribute (par exemple, 'texte') et un entier, numéro de colonne de la column du TreeModel à utiliser pour placer l'attribut. Les arguments supplémentaires de la méthode set_attributes() sont des paires attribute=column (par exemple, texte=1).

XVI-B-2 - Le widget ComboBoxEntry

Le widget ComboBoxEntry remplace le widget Combo. C'est une sous-classe du widget ComboBox qui contient un widget fils Entry dont le contenu est fixé en choisissant un élément dans une liste déroulante, en entrant directement le texte au clavier ou par un coller depuis un presse-papier Clipboard ou une sélection.

XVI-B-2-a - Utilisation fondamentale du ComboBoxEntry

Comme le ComboBox, le ComboBoxEntry peut être créé par la fonction :

boitderoulsaisie = gtk.combo_box_entry_new_text()

Le ComboBoxEntry devrait être rempli en utilisant les méthodes auxiliaires du ComboBox décrites dans la section Section 16.2.1.1, « Utiliser la boite déroulante de base ».

Comme un widget ComboBoxEntry est un widget Bin, son widget fils Entry est disponible en utilisant l'attribut "child" ou la méthode get_child() :

saisie = boitderoulsaisie.child
saisie = boitderoulsaisie.get_child()

On peut récupérer le texte de la zone Entry par la méthode get_text().

Comme pour le ComboBox, on peut surveiller les changements de l'élément actif de la liste en se connectant au signal "changed". Malheureusement, cela ne permet pas de suivre les modifications de texte dans le Entry fils lorsque ce sont des saisies directes. Quand une saisie directe est réalisée dans le widget fils Entry, le signal "changed" est bien émis, mais l'index retourné par la méthode get_active() vaudra -1. Pour surveiller tous les changements dans le texte de la zone de saisie Entry, il faudra utiliser le signal "changed" du widget Entry. Par exemple :

1. def fct_rappel_change(saisie):

2. print saisie.get_text()

4. boitderoulsaisie.child.connect('changed', fct_rappel_change)

affichera le texte après chaque modification dans le widget fils Entry. Pour exemple, le programme comboboxentrybasic.py montre l'utilisation de l'API. La figure Figure 16.7, « Liste déroulante avec zone de saisie » illustre le programme en action.

Figure 16.7. Liste déroulante avec zone de saisie

Il faut noter que lorsque le texte du Entry est modifié par un choix dans la liste déroulante, le gestionnaire du signal "changed" est appelé deux fois : une fois quand le texte est supprimé et une autre fois quand le texte prend la valeur du texte de l'élément de liste choisi.

XVI-B-2-b - Utilisation avancée du ComboBoxEntry

Le constructeur du ComboBoxEntry est :

boitderoulsaisie = gtk.ComboBoxEntry(model=None, column=-1)

où model est un TreeModel et column est le numéro de la colonne du model à utiliser pour créer la liste d'éléments. Si la colonne n'est pas précisée, la valeur par défaut est -1, ce qui signifie que la colonne du texte n'est pas indiquée.

Créer un ComboBoxEntry avec la fonction auxiliaire gtk.combo_box_entry_new_text() est équivalent à :

liststore = gtk.ListStore(str)
boitderoulsaisie = gtk.ComboBoxEntry(liststore, 0)

Le ComboBoxEntry ajoute quelques méthodes utilisées pour fixer et récupérer le numéro de colonne du TreeModel à utiliser pour fixer les chaines des éléments de liste :

boitderoulsaisie.set_text_column(text_column)
text_column = boitderoulsaisie.get_text_column()

La colonne du texte peut aussi être récupérée et fixée en utilisant la propriété "text-column". Se reporter à la section Section 16.2.1.2, « Usage avancé de ComboBox » pour plus de renseignements sur l'utilisation avancée du ComboBoxEntry.

Votre application doit indiquer la colonne de texte pour que le ComboBoxEntry établisse le contenu de la zone Entry depuis la liste déroulante. La colonne de texte ne peut être indiquée qu'une fois, soit en utilisant le constructeur, soit en utilisant la méthode set_text_column().

Lorsque l'on crée un ComboBoxEntry, il est placé avec un nouveau CellRendererText qui n'est pas accessible. L' attribut 'text' du CellRendererText doit être fixé, c'est un effet secondaire de l'utilisation de la méthode set_text_column() pour déterminer la colonne de texte. On peut placer des CellRenderer supplémentaires dans un ComboBoxEntry pour l'affichage dans la liste déroulante. Se reporter à la section Section 16.2.1.2, « Usage avancé de ComboBox » pour plus d'information.

XVI-C - Bouton de couleur (ColorButton) et de police (FontButton)

XVI-C-1 - Bouton de couleur (ColorButton)

Un widget ColorButton fournit une manière commode d'afficher une couleur dans un bouton que l'on peut cliquer pour ouvrir une fenêtre ColorSelectionDialog de choix de couleur. C'est utile pour afficher et sélectionner des couleurs dans un dialogue et, de préférence d'utilisateurs. Un ColorButton se charge d'établir, d'afficher et récupérer le résultat de la fenêtre de sélection de couleur ColorSelectionDialog. On crée un ColorButton par le constructeur :

colorbutton = gtk.ColorButton(color=gtk.gdk.Color(0,0,0))

La couleur de départ peut être précisée par le paramètre color ou établie par la suite avec la méthode :

colorbutton.set_color(color)

Le titre de la fenêtre ColorSelectionDialog qui s'affiche quand on clique sur le bouton peut être fixé et récupéré par la méthode :

1. colorbutton.set_title(title)

3. title = colorbutton.get_title()

L'opacité de la couleur est fixée en utilisant le canal alpha. Les méthodes suivantes récupèrent et fixent l'opacité de la couleur dans une gamme de 0 (transparent) à 65535 (opaque) :

1. alpha = colorbutton.get_alpha()

3. colorbutton.set_alpha(alpha)

Par défaut, la valeur alpha est ignorée, car la propriété "use_alpha" est fixée à FALSE. La valeur de cette propriété "use_alpha" peut être fixée et récupérée par la méthode :

1. colorbutton.set_use_alpha(use_alpha)

3. use_alpha = colorbutton.get_use_alpha()

Si "use_alpha" vaut TRUE, la fenêtre ColorSelectionDialog affiche un curseur pour régler l'opacité et montre la couleur sur un fond à damier.

On peut surveiller les changements de la couleur choisie en se reliant au signal "color-set" qui est émis quand l'utilisateur choisit une couleur. La fonction de rappel du signal est :

def color_set_cb(colorbutton, user_data):

Le programme colorbutton.py illustre l'utilisation du bouton de couleur ColorButton. La Figure 16.8, « Exemple de bouton de couleur »« Exemple de bouton de couleur »« Exemple de bouton de couleur » montre le programme en cours d'exécution.

Figure 16.8. Exemple de bouton de couleur

XVI-C-2 - Bouton de police (FontButton)

Comme le ColorButton, le bouton de police FontButton est un widget pratique qui fournit un aperçu de la police actuellement choisie et qui ouvre une fenêtre FontSelectionDialog quand on le clique. Un bouton FontButton se charge d'établir, d'afficher et de récupérer le résultat de la fenêtre de sélection de police FontSelectionDialog. On le crée par le constructeur :

boutonpolice = gtk.FontButton(nompolice=None)

où nompolice est une chaine indiquant la police courante de la fenêtre FontSelectionDialog. Par exemple, le nom de la police peut être écrit comme 'Sans 12', 'Sans Bold 14', ou 'Monospace Italic 14'. Il est nécessaire d'indiquer au minimum la famille et la taille de la police.

La police en cours peut aussi être fixée et récupérée par les méthodes suivantes :

1. resultat = boutonpolice.set_font_name(nompolice)

3. nompolice = boutonpolice.get_font_name()

où resultat renvoie TRUE ou FALSE pour indiquer si la prise en compte de la police a été réalisée avec succès. Le FontButton possède un certain nombre de propriétés et méthodes associées qui agissent sur l'affichage de la police en cours dans le bouton FontButton. Les propriétés "show-size" et "show-style" contiennent des valeurs booléennes qui contrôlent si la taille et le style seront affichés dans l'étiquette du bouton. Les méthodes suivantes fixent et récupèrent les valeurs de ces propriétés :

1. boutonpolice.set_show_style(show_style)

2. show_style = boutonpolice.get_show_style()

4. boutonpolice.set_show_size(show_size)

5. show_size = boutonpolice.get_show_size()

Autre manière, vous pouvez utiliser la taille et le style actuellement affichés par l'étiquette pour illustrer directement le choix de police. Voici les méthodes associées aux propriétés "use-size" et "use-font" :

1. boutonpolice.set_use_font(use_font)

2. use_font = boutonpolice.get_use_font()

4. boutonpolice.set_use_size(use_size)

5. use_size = boutonpolice.get_use_size()

Employer la police courante sur l'étiquette semble une technique utile malgré les inévitables changements de taille du bouton, mais utiliser la taille choisie n'apparait pas aussi utile, surtout avec de très grandes ou très petites tailles de police. Il faut noter que si on fixe "use-font" ou "use-size" à TRUE pour ensuite les mettre à FALSE, les dernières polices et tailles utilisées seront conservées. Par exemple, si "use-font" et "use-size" ont la valeur TRUE et que la police actuelle est Monospace Italic 20, l'étiquette du bouton FontButton s'affiche en Monospace Italic 20 ; puis si "use-font" et "use-size" passent à FALSE et que la police en cours est modifiée en Sans 12, l'étiquette s'affichera encore en Monospace Italic 20. Employez le programme d'exemple fontbutton.py pour voir comment ceci fonctionne.

Enfin, le titre de la fenêtre FontSelectionDialog peut être fixé et récupéré grâce aux méthodes :

1. boutonpolice.set_title(titre)

3. titre = boutonpolice.get_title()

Comme pour le bouton ColorButton, on peut surveiller les changements dans la police en cours en se connectant au signal émis lorsque l'utilisateur choisit une police. La fonction de rappel du signal a la forme :

def fct_rappel_choixpolice(boutonpolice, données utilisateur):

Le programme d'exemple fontbutton.py illustre l'utilisation du FontButton. On peut fixer les propriétés "use-font", "use-size", "show-size" et "show-style" avec les boutons interrupteurs. La Figure 16.9, « Exemple de bouton de police » montre le programme en cours d'exécution.

Figure 16.9. Exemple de bouton de police

XVI-D - Zone de saisie avec complètement (EntryCompletion)

Une zone de saisie avec complètement (EntryCompletion) est un objet associé à un champ de saisie (Entry) pour fournir la fonction de complètement. Lorsque l'utilisateur entre des données dans le champ de saisie Entry, le EntryCompletion fait apparaitre une fenêtre contextuelle montrant un ensemble de chaines de caractères correspondant au texte du champ de saisie (Entry).

Un EntryCompletion est créé par le constructeur :

completion = gtk.EntryCompletion()

On peut utiliser la méthode set_completion() du champ de saisie Entry pour associer le EntryCompletion au Entry :

entry.set_completion(completion)

Les chaines de caractères qu'utilise le EntryCompletion pour la correspondance sont récupérées depuis un TreeModel (généralement un ListStore) qui doit être défini par la méthode :

completion.set_model(model)

Le EntryCompletion implémente l'interface CellLayout, à la manière du TreeViewColumn, pour gérer l'affichage des données du TreeModel. La méthode suivante définit un EntryCompletion sous la forme la plus courante - une liste de chaines de caractères :

completion.set_text_column(column)

Cette méthode est équivalente à  :

1. cell = CellRendererText()

2. completion.pack_start(cell)

3. completion.add_attribute(cell, 'text', column)

Pour définir le nombre de caractères qui doivent être entrés avant que le EntryCompletion recherche une correspondance, on peut utiliser la méthode :

completion.set_minimum_key_length(length)

Le programme d'exemple entrycompletion.py illustre l'utilisation du EntryCompletion. La Figure 16.10, « Saisie avec complètement » montre le programme en cours d'exécution.

Figure 16.10. Saisie avec complètement

Le programme d'exemple démarre avec un faible nombre de chaines de "complètement" qui peut être augmenté en entrant des données dans le champ de saisie et en appuyant sur la touche Entrée. Si la chaine est unique, elle est ajoutée à la liste des chaines de "complètement".

La fonction interne de concordance ne tient pas compte de la casse (minuscule/majuscule). Si on a besoin d'une fonction plus spécifique, il est possible d'utiliser la méthode suivante pour installer sa propre fonction de concordance :

completion.set_match_func(func, user_data)

La fonction func est :

def func(completion, key_string, iter, data):

où le paramètre key_string contient le contenu courant du champ Entry, iter est un TreeIter pointant sur une ligne dans le TreeModel associé et data contient les données utilisateur user_data. Le paramètre func devrait renvoyer TRUE si la chaine de "complètement" de la ligne devrait être affichée.

L'extrait de code suivant utilise une fonction de concordance pour afficher les possibles chaines de "complètement" qui commencent par le contenu du champ de saisie et qui possèdent le suffixe indiqué (ici, un nom se terminant par .png pour un fichier PNG file.

1. ...

2. completion.set_match_func(end_match, (0, '.png'))

3. ...

4. def end_match(completion, entrystr, iter, data):

5. column, suffix = data

6. model = completion.get_model()

7. modelstr = model[iter][column]

8. return modelstr.startswith(entrystr) and modelstr.endswith(suffix)

9. ...

Par exemple, si l'utilisateur tape 'mou' et que le modèle de "complètement" contient une chaine comme 'moulin.png', 'farine.png', 'moule.png' et 'mou.tif', les choix 'moulin.png' et moule.png' seront proposés comme "complètement".

XVI-E - Expanseur (Expander)

L'expanseur (Expander) est un conteneur assez simple qui peut montrer ou cacher son widget fils en cliquant sur un triangle semblable au triangle du TreeView. On crée un Expander par le constructeur :

expander = gtk.Expander(label=None)

où le paramètre label est une chaine de caractères utilisée comme étiquette de l'expanseur. Si label vaut None ou n'est pas défini, aucune étiquette ne sera créée. D'une autre manière, on peut utiliser la fonction :

expander = gtk.expander_new_with_mnemonic(label=None)

qui définit le caractère de l'étiquette précédé par un trait de soulignement comme un raccourci-clavier.

Le widget Expander utilise l'API de Container pour ajouter ou retirer son widget fils :

1. expander.add(widget)

3. expander.remove(widget)

Le widget fils peut être récupéré en utilisant l'attribut "child" de Bin ou par la méthode get_child().

L'option qui contrôle l'interprétation des caractères soulignés de l'étiquette peut être récupérée et modifiée par ces méthodes :

1. use_underline = expander.get_use_underline()

3. expander.set_use_underline(use_underline)

Si on souhaite utiliser le marquage Pango (voir les détails dans Référence du marquage Pango) dans le libellé de l'étiquette, il faut utiliser les méthodes suivantes pour récupérer et définir l'état de la propriété "use-markup" :

1. expander.set_use_markup(use_markup)

3. use_markup = expander.get_use_markup()

Enfin, on peut utiliser n'importe quel widget comme label en utilisant la méthode suivante :

expander.set_label_widget(label_widget)

Ceci permet, par exemple, d'utiliser une boite HBox contenant une Image et un Label comme étiquette du Expander.

L'état du Expander peut être récupéré et défini par ces méthodes :

1. expanded = expander.get_expanded()

3. expander.set_expanded(expanded)

Si le paramètre expanded vaut TRUE, le widget fils est affiché.

Dans la plupart des cas, le Expander effectue automatiquement ce que l'on veut lorsqu'il affiche ou cache le widget fils. Parfois, l'application peut vouloir créer un widget fils au moment de l'expansion. Le signal "notify::expanded" peut être utilisé pour garder trace des changements d'état du triangle d'expansion. Le gestionnaire de signal peut alors créer ou modifier le widget fils en fonction des besoins.

Le programme d'exemple expander.py illustre l'utilisation du Expander. La Figure 16.11, « Expander Widget » montre le programme en cours d'exécution.

Figure 16.11. Expander Widget

Ce programme crée un Label contenant le temps courant et l'affiche quand l'expanseur est ouvert.

XVI-F - Sélection de fichiers basée sur le sélecteur FileChooser

La nouvelle manière de choisir un fichier dans PyGTK 2.4 consiste à utiliser les variantes du widget FileChooser. Les deux objets qui implémentent cette nouvelle interface dans PyGTK 2.4 sont FileChooserWidget et FileChooserDialog. Ce dernier est un dialogue complet avec la fenêtre et des boutons facilement définis. Le premier est un widget utile pour l'inscrire dans un autre widget.

Le FileChooserWidget comme le FileChooserDialog possèdent les capacités de naviguer dans l'arborescence du système de fichiers et d'en sélectionner. L'apparence du widget dépend de l'action utilisée pour l'ouvrir.

Pour créer une nouvelle fenêtre de sélection de fichiers et pour choisir un fichier existant (comme dans le classique Fichier ? Ouvrir), il faut utiliser :

chooser = gtk.FileChooserDialog(title=None,action=gtk.FILE_CHOOSER_ACTION_OPEN,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))

Pour créer une nouvelle fenêtre de sélection de fichiers et pour choisir un nouveau nom de fichier (comme dans le classique Fichier ? Enregistrer), il faut utiliser :

chooser = gtk.FileChooserDialog(title=None,action=gtk.FILE_CHOOSER_ACTION_SAVE,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))

Dans les exemples précédents, les deux boutons (éléments de stock Annuler et Ouvrir) sont créés et connectés à leur réponse respective (réponses Annuler et Valider(OK)).

Pour définir le dossier qui sera affiché dans le sélecteur de fichiers, on utilise la méthode :

chooser.set_current_folder(pathname)

Pour définir le nom de fichier suggéré comme s'il était tapé par l'utilisateur (le classique Fichier ? Enregistrer Sous), on utilise la méthode :

chooser.set_current_name(name)

La méthode ci-dessus n'exige pas que le nom du fichier existe déjà. Si on préfère présélectionner un fichier existant particulier (comme dans Fichier ? Ouvrir), on utilise la méthode :

chooser.set_filename(filename)

Pour récupérer le nom du fichier que l'utilisateur a tapé ou sur lequel il a cliqué, on utilise la méthode :

filename = chooser.get_filename()

Il est possible d'autoriser de multiples sélections (uniquement pour l'action gtk.FILE_CHOOSER_ACTION_OPEN) en utilisant la méthode :

chooser.set_select_multiple(select_multiple)

où le paramètre select_mutiple doit valoir TRUE pour autoriser les sélections multiples. Dans ce cas, il faudra utiliser la méthode suivante pour récupérer la liste des fichiers sélectionnés :

filenames = chooser.get_filenames()

Une caractéristique importante de tous les sélecteurs de fichiers est leur capacité à ajouter des filtres de sélection de fichiers. Le filtre peut être ajouté par cette méthode :

chooser.add_filter(filter)

Dans l'exemple ci-dessus, filter doit être une instance de la classe FileFilter.

Le panneau de gauche du sélecteur de fichier contient quelques signets (raccourcis vers les dossiers) tels Dossier personnel, Système de fichiers, CD-ROM, etc. Il est possible d'ajouter un dossier à la liste de ces signets, ou de l'enlever, avec ces méthodes :

chooser.add_shortcut_folder(folder)
chooser.remove_shortcut_folder(folder)

où folder est le chemin du dossier. Le programme d'exemple filechooser.py illustre l'utilisation du sélecteur de fichier. La Figure 16.12, « Exemple de sélecteur de fichiers » montre le programme en cours d'exécution.

Figure 16.12. Exemple de sélecteur de fichiers

Voici le code source du programme filechooser.py  :

1. #!/usr/bin/env python

2. # coding: utf8

3. # exemple filechooser.py

5. import pygtk

6. pygtk.require('2.0')

8. import gtk

10. # Vérification du nouveau PyGtk : c'est une nouvelle classe de PyGtk 2.4

11. if gtk.pygtk_version < (2,3,90):

12. print "Cet exemple nécessite PyGtk 2.3.90 ou ultérieur"

13. raise SystemExit

15. dialogue = gtk.FileChooserDialog("Ouvrir. ",

16. None,

17. gtk.FILE_CHOOSER_ACTION_OPEN,

18. (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,

19. gtk.STOCK_OPEN, gtk.RESPONSE_OK))

20. dialogue.set_default_response(gtk.RESPONSE_OK)

22. filtre = gtk.FileFilter()

23. filtre.set_name("All files")

24. filtre.add_pattern("*")

25. dialogue.add_filter(filtre)

27. filtre = gtk.FileFilter()

28. filtre.set_name("Images")

29. filtre.add_mime_type("image/png")

30. filtre.add_mime_type("image/jpeg")

31. filtre.add_mime_type("image/gif")

32. filtre.add_pattern("*.png")

33. filtre.add_pattern("*.jpg")

34. filtre.add_pattern("*.gif")

35. filtre.add_pattern("*.tif")

36. filtre.add_pattern("*.xpm")

37. dialogue.add_filter(filtre)

39. reponse = dialogue.run()

40. if reponse == gtk.RESPONSE_OK:

41. print dialogue.get_filename(), 'choisi'

42. elif reponse == gtk.RESPONSE_CANCEL:

43. print 'On ferme, pas de fichier sélectionné'

44. dialogue.destroy()

XVI-G - Le gestionnaire d'Interface Utilisateur UIManager

XVI-G-1 - Aperçu

Le gestionnaire UIManager fournit des outils pour créer des menus et des barres d'outils à partir d'une description de type XML. Le UIManager utilise les objets du ActionGroup pour gérer les objets de l'Action fournissant une infrastructure commune aux éléments de menu et de barre d'outils.

L'utilisation de l'UIManager permet de fusionner et de scinder dynamiquement des descriptions et actions de UI multiples. Ceci permet de modifier les menus et les barres d'outils selon le mode utilisé dans l'application (par exemple, passer d'une édition de texte à une édition d'image), ou quand de nouvelles extensions sont ajoutées ou enlevées de l'application.

Un UIManager peut être utilisé pour créer les menus et les barres d'outils d' un interface utilisateur de la façon suivante.

- Créer une instance de l'UIManager.

- Extraire l'AccelGroup de l'UIManager et l'ajouter à la Fenêtre de niveau supérieur.

- Créer des instances de l'ActionGroup et les remplir avec les Action voulues. instances.

- Ajouter les instances de l'ActionGroup à l'UIManager dans l'ordre dans lequel on souhaite présenter les instances des Action.

- Ajouter les descriptions XML de l'interface UI à l'UIManager. Il faut s'assurer que toutes les Actions définies par les descriptions soient disponibles dans les instances ActionGroup de l'UIManager.

- Extraire, par leur nom, les références des widgets de la barre de menu, du menu et de la barre d'outils pour les utiliser dans la construction de l'interface utilisateur.

- Modifier l'interface de manière dynamique, en ajoutant ou retirant des descriptions d'Interface Utilisateur et en ajoutant, réordonnant ou retirant les instances associées des groupes d'actions ActionGroup.

XVI-G-2 - Créer un UIManager

Une instance de l'UIManager se crée par le constructeur :

gestionui = gtk.UIManager()

Un nouveau UIManager est créé avec son groupe d'accélérateurs associé AccelGroup, lequel peut être récupéré avec la méthode  :

grouperacc = gestionui.get_accel_group()

Le groupe AccelGroup doit être ajouté à la fenêtre de niveau supérieur pour que les accélérateurs (raccourcis-clavier) de l'Action puissent être disponibles pour les utilisateurs. Par exemple :

1. fenetre = gtk.Window()

2. ...

3. gestionui = gtk.UIManager()

4. grouperacc = gestionui.get_accel_group()

5. fenetre.add_accel_group(accelgroup)

XVI-G-3 - Ajouter et supprimer des groupes d'actions

Comme on l'a vu dans la Section 16.1.2, « ActionGroup », un groupe d'actions ActionGroups peut être rempli avec des Actions en utilisant les méthodes add_actions(), add_toggle_actions() et add_radio_actions(). Un ActionGroup peut être utilisé par un UIManager lorsqu'il a été ajouté à sa liste des ActionGroup par la méthode :

gestionui.insert_action_group(action_group, pos)

où pos est l'indication de la position où l'action_group doit être inséré. Un UIManager peut comporter plusieurs groupes ActionGroup avec des noms de Action en double. L'ordre des objets d'un ActionGroup est important, car la recherche d'une Action par son nom s'arrête sur la première Action trouvée. Ceci signifie que les premières actions dans les objets ActionGroup masquent les suivantes.

Les actions inscrites dans la description XML d'une Interface Utilisateur doivent être ajoutées dans l'UIManager avant que la description elle-même puisse l'être.

Un ActionGroup peut être supprimé d'un UIManager par la méthode :

gestionui.remove_action_group(action_group)

Une liste des objets ActionGroup associés à l'UIManager peut être récupérée par la méthode :

listegroupesactions = gestionui.get_action_groups()

XVI-G-4 - Descriptions de l'Interface Utilisateur

Les descriptions d'Interface Utilisateur acceptées par le UIManager sont de simples définitions XML qui comportent les éléments suivants :

ui

L'élément racine de la description de l'Interface Utilisateur. Il peut être omis. Il peut contenir les éléments menubar, popup, toolbar et accelerator.

menubar

Un élément de premier niveau qui décrit une structure de barre de menu MenuBar qui peut contenir les éléments MenuItem, separator, placeholder et menu. Il possède en option un attribut name. Si cet attribut name n'est pas précisé, il aura pour valeur "menubar".

popup

Un élément de premier niveau qui décrit une structure de menu popup Menu qui peut contenir les éléments menuitem, separator, placeholder et menu. Il possède en option un attribut name. Si cet attribut name n'est pas précisé, il aura pour valeur "popup".

toolbar

Un élément de premier niveau qui décrit une structure de barre d'outils Toolbar qui peut contenir les éléments tolite, separator et placeholder. Il possède en option un attribut name. Si cet attribut name n'est pas précisé, il aura pour valeur "toolbar".

placeholder

Un élément identifiant une position dans un menubar, toolbar, popup ou menu. Ce paramètre substituable peut contenir les éléments menuitem, separator, placeholder et menu. Les éléments substituables Placeholder sont utilisés pour fusionner plusieurs descriptions d'Interface Utilisateur pour permettre, par exemple, la construction d'un menu à partir de descriptions d'Interface Utilisateur en utilisant des noms de placeholder partagés. Il possède en option un attribut name. Si cet attribut name n'est pas précisé, il aura pour valeur "placeholder".

menu

Un élément décrivant une structure de Menu qui peut contenir les éléments menuitem, separator, placeholder et menu. Un élément menu possède obligatoirement un attribut action qui désigne un objet Action, utilisé pour créer le Menu. Il possède en option les attributs name et position. Si l'attribut name n'est pas précisé, il aura pour valeur le nom de l'élément action. L'attribut position peut avoir pour valeur "top" ou "bottom". Si sa valeur n'est pas précisée, ce sera, par défaut, "bottom".

menuitem

Un élément décrivant un MenuItem. Un élément menuitem possède un attribut obligatoire action qui désigne un objet Action utilisé pour créer l'élément de menu MenuItem. Il possède également les attributs facultatifs name et position. Si name n'est pas précisé, le nom de action sera utilisé. L'attribut position peut avoir pour valeur "top" ou "bottom". Si sa valeur n'est pas précisée, ce sera, par défaut, "bottom".

tolite

Décrit un élément de barre d'outils ToolItem. Un élément tolite possède un attribut obligatoire action qui désigne un objet Action utilisé pour créer la barre d'outils Toolbar. Il possède également les attributs facultatifs name et position. Si name n'est pas précisé, le nom de action sera utilisé. L'attribut position peut avoir pour valeur "top" ou "bottom". Si sa valeur n'est pas précisée, ce sera par défaut, "bottom".

separator

Élément décrivant un séparateur d'éléments de menu SeparatorMenuItem ou SeparatorToolItem selon les cas.

accelerator

Élément décrivant un raccourci-clavier. Un élément accelerator possède un attribut obligatoire action qui désigne un objet Action pour définir la combinaison de touches du clavier et qui est activé par l'accélérateur. Il possède également l'attribut facultatif name. Si name n'est pas précisé, le nom de l'action sera utilisé.

Par exemple, voici une description d'Interface Utilisateur qui peut être utilisée pour créer une interface semblable à celle de la Figure 16.4, « Exemple avec ActionGroup » :

1. <ui>

2. <menubar name="BarreMenu">

3. <menu action="Fichier">

4. <menuitem action="Quitter"/>

5. </menu>

6. <menu action="Son">

7. <menuitem action="Volume"/>

8. </menu>

9. <menu action="Modulation">

10. <menuitem action="MA"/>

11. <menuitem action="MF"/>

12. <menuitem action="BLU"/>

13. </menu>

14. </menubar>

15. <toolbar name="BarreOutils">

16. <tolite action="Quitter"/>

17. <separator/>

18. <tolite action="Volume"/>

19. <separator/>

20. <placeholder name="ElementsMod">

21. <tolite action="MA"/>

22. <tolite action="MF"/>

23. <tolite action="BLU"/>

24. </placeholder>

25. </toolbar>

26. </ui>

Il faut noter que cette description utilise simplement les noms d'attributs du action comme nom de la plupart des éléments plutôt que de préciser leurs attributs name. Aussi, je recommanderais de ne pas préciser l'élément ui lorsque cela n'est pas indispensable.

La hiérarchie de widgets créée par une description d'Interface Utilisateur est tout à fait semblable à une hiérarchie d'éléments XML, excepté que les éléments placeholder sont fusionnés dans leurs parents.

On peut avoir accès à un widget appartenant à la hiérarchie de la description de l'Interface Utilisateur en utilisant son chemin ; celui-ci est composé du nom du widget et de ses éléments parents reliés par des barres obliques ("/"). À partir de la description ci-dessus, voici des exemples de chemins de widgets valides :

1. /BarreMenu

2. /BarreMenu/Fichier/Quitter

3. /BarreMenu/Modulation/BLU

4. /BarreOutils/Volume

5. /BarreOutils/ElementsMod/MF

Il faut noter que le nom de l'élément de substitution placeholder doit être inclus dans le chemin. Dans la plupart des cas, on ne s'intéresse qu'aux widgets de niveau supérieur (par exemple "/MenuBar" et "/Toolbar"), mais on peut avoir besoin d'accéder à un widget de plus bas niveau pour, par exemple, modifier une propriété.

XVI-G-5 - Ajouter et supprimer des descriptions d'Interface Utilisateur

Une fois le UIManager configuré avec un ActionGroup, une description d'Interface Utilisateur peut être ajoutée ou fusionnée avec l'Interface Utilisateur existant par l'une de ces méthodes :

1. fusion_id = gestionui.add_ui_from_string(buffer)

3. fusion_id = gestionui.add_ui_from_file(filename)

où le paramètre buffer est une chaine de caractères contenant une description d'Interface Utilisateur et filename se réfère au fichier contenant la description. Les deux méthodes renvoient un identifiant fusion_id qui est une valeur entière unique. Si la méthode échoue, une exception GError est levée. L'identifiant merge_id peut servir à supprimer la description d'Interface Utilisateur en utilisant la méthode :

gestionui.remove_ui(fusion_id)

Les mêmes méthodes peuvent être utilisées plusieurs fois pour ajouter des descriptions supplémentaires qui peuvent être fusionnées pour obtenir une description combinée d'Interface Utilisateur en XML. Les Interfaces Utilisateurs combinées seront traitées plus en détail dans la Section 16.7.8, « Combiner des descriptions d'Interface Utilisateur ».

On peut ajouter un seul élément à la description d'Interface Utilisateur existante par la méthode :

gestionui.add_ui(fusion_id, path, name, action, type, top)

où l'identifiant fusion_id est un entier unique, path est le chemin où le nouvel élément doit être ajouté, action est le nom de l'Action ou None pour ajouter un separator, type est le type de l'élément à ajouter et top une valeur booléenne. Si top vaut TRUE, l'élément est inséré avant son frère (élément de même niveau), sinon, il est ajouté à la suite.

Le paramètre merge_id sera obtenu par la méthode :

fusion_id = gestionui.new_merge_id()

Les valeurs entières retournées par la méthode new_merge_id() augmentent arithmétiquement.

Le paramètre path est une chaine de caractères composée du nom de l'élément et du nom de ses ancêtres, séparés par un trait oblique ("/"), qui n'inclut pas le nœud racine optionnel "/ui". Par exemple "/BarreMenu/Modulation" représente le chemin de l'élément de menu nommé "Modulation" dans la description d'Interface Utilisateur suivante :

1. <menubar name="BarreMenu">

2. <menu action="Modulation">

3. </menu>

4. </menubar>

La valeur du paramètre type doit être parmi :

gtk.UI_MANAGER_AUTO

Le type de l'élément d'Interface Utilisateur (menuitem, toolitem ou separator) est défini en fonction du contexte.

gtk.UI_MANAGER_MENUBAR

Une barre de menu.

gtk.UI_MANAGER_MENU

Un menu.

gtk.UI_MANAGER_TOOLBAR

Une barre d'outils.

gtk.UI_MANAGER_PLACEHOLDER

Une élément de réservation (placeholder).

gtk.UI_MANAGER_POPUP

Un menu popup.

gtk.UI_MANAGER_MENUITEM

Un élément de menu).

gtk.UI_MANAGER_TOOLITEM

Un élément de barre d'outils.

gtk.UI_MANAGER_SEPARATOR

Un séparateur.

gtk.UI_MANAGER_ACCELERATOR

Un raccourci ou accélérateur.

La méthode add_ui() échoue sans le signaler si l'élément n'est pas ajouté. La méthode add_ui() est de si bas niveau qu'il faut plutôt toujours essayer d'utiliser les méthodes pratiques add_ui_from_string() et add_ui_from_file()

Ajouter un élément ou une description d'Interface Utilisateur entraine l'actualisation de la hiérarchie des widgets dans une fonction en attente. On peut s'assurer que la hiérarchie a bien été actualisée avant d'y accéder en appelant la méthode :

gestionui.ensure_update()

XVI-G-6 - Accès aux widgets d'une Interface Utilisateur

On peut avoir accès à un widget dans la hiérarchie de l'Interface Utilisateur en utilisant la méthode :

widget = gestionui.get_widget(path)

où le paramètre path est une chaine de caractères qui contient le nom de l'élément recherché et celui de ses ancêtres comme on l'a expliqué dans la Section 16.7.4, « Descriptions de l'Interface Utilisateur ».

Par exemple, étant donné la description d'Interface Utilisateur suivante :

1. <menubar name="BarreMenu">

2. <menu action="Fichier">

3. <menuitem action="Quitter"/>

4. </menu>

5. <menu action="Son">

6. <menuitem action="Volume"/>

7. </menu>

8. <menu action="Modulation">

9. <menuitem action="MA"/>

10. <menuitem action="MF"/>

11. <menuitem action="BLU"/>

12. </menu>

13. </menubar>

14. <toolbar name="BarreOutils">

15. <tolite action="Quitter"/>

16. <separator/>

17. <tolite action="Volume"/>

18. <separator name="sep1"/>

19. <placeholder name="ElementsMod">

20. <tolite action="MA"/>

21. <tolite action="MF"/>

22. <tolite action="BLU"/>

23. </placeholder>

24. </toolbar>

que l'on a ajouté au gestionnaire d'UIManager gestionui, on peut accéder à la BarreMenu et à la BarreOutils pour les utiliser dans une application Window en utilisant le code suivant :

1. fenêtre = gtk.Window()

2. vbox = gtk.VBox()

3. barremenus = gestionui.get_widget('/BarreMenu')

4. barreoutils = gestionui.get_widget('/BarreOutils')

5. vbox.pack_start(barremenus, False)

6. vbox.pack_start(barreoutils, False)

De la même manière, les widgets de plus bas niveau dans la hiérarchie sont accessibles en utilisant leur chemin. Par exemple, le bouton RadioToolButton appelé "BLU" est accessible ainsi :

blu = gestionui.get_widget('/BarreOutils/ElementsMod/BLU')

Pour faciliter la tâche, on peut accéder à tous les widgets de haut niveau d'un même type avec la méthode :

toplevels = gestionui.get_toplevels(type)

où le paramètre type indique le type des widgets à renvoyer en utilisant une combinaison des drapeaux : gtk.UI_MANAGER_MENUBAR, gtk.UI_MANAGER_TOOLBAR et gtk.UI_MANAGER_POPUP. On peut utiliser la méthode gtk.Widget.get_name() pour déterminer quel est le widget de haut niveau obtenu.

Pour trouver l'Action utilisée par le widget délégué associé à un élément d'Interface Utilisateur, on utilise la méthode :

action = gestionui_get_action(path)

où le paramètre path est une chaine de caractères représentant le chemin d'un élément d'Interface Utilisateur dans le gestionnaire gestionui. Si l'élément ne possède pas d'Action associé, la valeur est retournée.

XVI-G-7 - Un exemple d'Interface Utilisateur simple

Le programme uimanager.py illustre l'utilisation du gestionnaire UIManager. La Figure 16.13, « Exemple simple de UIManager » montre le programme en cours d'exécution.

Figure 16.13. Exemple simple de UIManager

Le programme uimanager.py utilise la description XML de la Section 16.7.6, « Accès aux widgets d'une Interface Utilisateur ». Le texte des deux étiquettes est modifié par l'activation de l'action interrupteur ToggleAction "Volume" et des choix des RadioAction "MA", "MF" et "BLU". Toutes les actions sont inscrites dans un unique ActionGroup qui permet à la sensibilité et à la visibilité de tous les widgets délégués d'être commutées en utilisant les boutons interrupteurs "Sensible" et "Visible". L'utilisation de l'élément placeholder sera décrite en détail dans la Section 16.7.8, « Combiner des descriptions d'Interface Utilisateur ».

XVI-G-8 - Combiner des descriptions d'Interface Utilisateur

La fusion de plusieurs descriptions d'Interface Utilisateur se réalise en fonction du nom des éléments XML. Comme on a ce nom auparavant, on peut avoir accès aux éléments individuels de la hiérarchie par leur nom de chemin qui est représenté par leur nom plus le nom de leurs ancêtres. Par exemple, si on utilise la description d'Interface Utilisateur de la Section 16.7.4, « Descriptions de l'Interface Utilisateur », l'élément tolite "MA" a pour nom de chemin "/BarreOutils/ElementsMod/MA" alors que le nom de chemin de l'élément menuitem est "/BarreMenu/Modulation/MF".

Si une description d'Interface Utilisateur est fusionnée avec la précédente, les éléments sont ajoutés en tant que frères (au même niveau) aux éléments existants. Par exemple, si la description d'Interface Utilisateur :

1. <menubar name="BarreMenu">

2. <menu action="Fichier">

3. <menuitem action="Enregistrer" position="top"/>

4. <menuitem action="Nouveau" position="top"/>

5. </menu>

6. <menu action="Son">

7. <menuitem action="Intensite"/>

8. </menu>

9. <menu action="Modulation">

10. <menuitem action="CB"/>

11. <menuitem action="OndesCourtes"/>

12. </menu>

13. </menubar>

14. <toolbar name="BarreOutils">

15. <tolite action="Enregistrer" position="top"/>

16. <tolite action="Nouveau" position="top"/>

17. <separator/>

18. <tolite action="Intensite"/>

19. <separator/>

20. <placeholder name="ElementsMod">

21. <tolite action="CB"/>

22. <tolite action="OndesCourtes"/>

23. </placeholder>

24. </toolbar>

est ajoutée à notre exemple précédent :

1. <menubar name="BarreMenu">

2. <menu action="Fichier">

3. <menuitem action="Quitter"/>

4. </menu>

5. <menu action="Son">

6. <menuitem action="Volume"/>

7. </menu>

8. <menu action="Modulation">

9. <menuitem action="MA"/>

10. <menuitem action="MF"/>

11. <menuitem action="BLU"/>

12. </menu>

13. </menubar>

14. <toolbar name="BarreOutils">

15. <tolite action="Quitter"/>

16. <separator/>

17. <tolite action="Volume"/>

18. <separator name="sep1"/>

19. <placeholder name="ElementsMod">

20. <tolite action="MA"/>

21. <tolite action="MF"/>

22. <tolite action="BLU"/>

23. </placeholder>

24. </toolbar>

Le résultat final créé sera :

1. <menubar name="BarreMenu">

2. <menu name="Fichier" action="Fichier">

3. <menuitem name="Nouveau" action="Nouveau"/>

4. <menuitem name="Enregistrer" action="Enregistrer"/>

5. <menuitem name="Quitter" action="Quitter"/>

6. </menu>

7. <menu name="Son" action="Son">

8. <menuitem name="Volume" action="Volume"/>

9. <menuitem name="Intensite" action="Intensité"/>

10. </menu>

11. <menu name="Modulation" action="Modulation">

12. <menuitem name="MA" action="MA"/>

13. <menuitem name="MF" action="MF"/>

14. <menuitem name="BLU" action="BLU"/>

15. <menuitem name="CB" action="CB"/>

16. <menuitem name="OndesCourtes" action="OndesCourtes"/>

17. </menu>

18. </menubar>

19. <toolbar name="BarreOutils">

20. <tolite name="Nouveau" action="Nouveau"/>

21. <tolite name="Enregistrer" action="Enregistrer"/>

22. <tolite name="Quitter" action="Quitter"/>

23. <separator/>

24. <tolite name="Son" action="Son"/>

25. <separator name="sep1"/>

26. <placeholder name="ElementsMod">

27. <tolite name="MA" action="MA"/>

28. <tolite name="MF" action="MF"/>

29. <tolite name="BLU" action="BLU"/>

30. <tolite name="CB" action="CB"/>

31. <tolite name="OndesCourtes" action="OndesCourtes"/>

32. </placeholder>

33. <separator/>

34. <toolitem name="Intensite" action="Intensite"/>

35. <separator/>

36. </toolbar>

Si on examine le résultat XML, on peut constater que les éléments menuitem "Nouveau" et "Enregistrer" ont été incorporés avant l'élément "Quitter". Ceci est dû à la valeur "top" de l'attribut "position" qui signifie que l'élément doit être ajouté en tête. De la même manière, les éléments tolite "Nouveau" et "Enregistrer" ont été incorporés au début de la barre d'outils "BarreOutils". Il faut remarquer que les éléments "Nouveau" et "Enregistrer" ont été inversés par le processus de fusion.

L'élément tolite "Intensite" est ajouté aux autres éléments de la barre d'outils "BarreOutils" et apparait en dernière place dans la description fusionnée même s'il n'est pas le dernier de sa propre description. Dans les deux descriptions, l'élément placeholder "ElementsMod" mêle les éléments tolite "CB" et "OndesCourtes" avec les éléments "MA", "MF", et "BLU". Si on n'avait pas utilisé un élément placeholder "ElementsMod", les éléments "CB" et "OndesCourtes" se seraient retrouvés après l'élément "Intensite".

On peut obtenir une représentation de l'Interface Utilisateur utilisée par un UIManager par la méthode :

uidesc = uimanager.get_ui()

Le programme uimerge.py illustre la fusion des descriptions précédentes UIManager. La Figure 16.14, « Exmple de fusion UIMerge » montre les Interfaces Utilisateurs séparés et fusionnés.

Figure 16.14. Exemple de fusion UIMerge

Le programme exemple utilise trois objets ActionGroup :

- des objets Action pour les menus "Fichier", "Son" et "Modulation" ;

- des objets Action pour les menus "Quitter", "Son", "MA", "MF", "BLU" et "Modulation"

- des objets Action pour les éléments "Intensité", "CB" et "Ondes Courtes".

Les widgets de choix ToggleButton "Sensible" et "Visible" contrôlent la sensibilité et la visibilité du deuxième ActionGroup uniquement.

XVI-G-9 - Signaux de l'Interface Utilisateur

L'UIManager possède une paire de signaux intéressants auxquels se connecter. Le signal "actions-changed" est émis lorsqu'un ActionGroup est ajouté ou retiré de l'UIManager. La fonction de rappel est :

def fct_rappel(gestionui, ...)

Le signal "add-widget" est émis lorsqu'un widget délégué de Toolbar ou de MenuBar est créé. La fonction de rappel est :

def fct_rappel(gestionui, widget, ...)

où le paramètre widget représente le nouveau widget créé.

XVII - Chapitre 17. Widgets non décrits

Tous ces widgets sont à la recherche d'un auteur ! :). Vous pouvez envisager de contribuer à notre tutoriel.

Si vous avez besoin d'utiliser l'un de ces widgets encore non documentés, je vous suggère fortement de jeter un coup d'œil aux fichiers .c de PyGTK. Les noms de méthode de PyGTK sont très suggestifs. Lorsque vous avez compris comment cela fonctionne, ce n'est pas difficile d'en déduire comment utiliser un widget, en lisant ses définitions de méthode. Ceci, ajouté à quelques exemples de codes d'autres personnes, et vous n'aurez pas de problèmes.

Quand vous aurez compris toutes les méthodes d'un nouveau widget sans documentation, s'il vous plaît, n'hésitez pas à envisager d'écrire un tutoriel à son sujet, de façon à ce que les autres puissent bénéficier du temps que vous y avez consacré.

XVII-A - Étiquette de raccourci

XVII-B - Option de menu

XVII-C - Éléments de menu

XVII-C-1 - Éléments de menu à cocher

XVII-C-2 - Élément de menu Radio

XVII-C-3 - Élément de menu séparateur

XVII-C-4 - Élément rattacheur de menu

XVII-D - Courbes

XVII-E - Fenêtre de dialogue de message

XVII-F - Courbe Gamma

XVIII - Chapitre 18. Définir les attributs des widgets

Ce chapitre décrit les méthodes utilisées pour agir sur les widgets (et les objets) et modifier leurs style, espacement, taille, etc.

La méthode :

widget.activate()

entraine l"émission du signal "activate" par le widget.

La méthode :

widget.set_sensitive(sensible)

définit la sensibilité d'un widget (par exemple, doit-il réagir à un événement ?). Si le paramètre sensible vaut TRUE, le widget recevra les événements ; si le paramètre vaut FALSE, le widget ne recevra pas les événements. Un widget insensible est normalement affiché en "grisé".

La méthode :

widget.set_size_request(largeur, hauteur)

définit la taille du widget à la hauteur et largeur donnée.

XVIII-A - Méthodes des drapeaux des widgets

Les méthodes :

1. widget.set_flags(drapeaux)

3. widget.unset_flags(drapeaux)

5. drapeaux = widget.flags()

définissent, annulent et récupèrent les drapeaux des gtk.Object et des gtk.Widget. Le paramètre drapeaux peut être n'importe lequel parmi les drapeaux standards :

1. IN_DESTRUCTION    # en cours de destruction

2. FLOATING          # flottant orphelin

3. RESERVED_1        # réservé

4. RESERVED_2        # réservé

5. TOPLEVEL          # de niveau supérieur (sans parent)

6. NO_WINDOW         # sans fenêtre propre

7. REALIZED          # réalisé

8. MAPPED            # peut être affiché

9. VISIBLE           # visible

10. SENSITIVE         # sensible

11. PARENT_SENSITIVE  # parent est sensible

12. CAN_FOCUS         # peut recevoir le focus

13. HAS_FOCUS         # a le focus

14. CAN_DEFAULT       # peut être le choix par défaut

15. HAS_DEFAULT       # est le choix par défaut

16. HAS_GRAB          # a la priorité

17. RC_STYLE          # style via le RC

18. COMPOSITE_CHILD   # enfant composite

19. NO_REPARENT       # non utilisé

20. APP_PAINTABLE     # directement dessinable ?

21. RECEIVES_DEFAULT  # reçoit le choix par défaut

22. DOUBLE_BUFFERED   # possède un double buffer

La méthode :

widget.grab_focus()

permet à un widget d'acquérir le focus si son drapeau CAN_FOCUS est activé.

XVIII-B - Méthodes d'affichage des widgets

Les méthodes :

1. widget.show()

2. widget.show_all()

3. widget.hide()

4. widget.hide_all()

5. widget.realize()

6. widget.unrealize()

7. widget.map()

8. widget.unmap()

gèrent l'affichage du widget.

La méthode show() permet l'affichage du widget en utilisant les méthodes realize() et map().

La méthode hide() permet de ne plus afficher le widget et de le "démapper", si nécessaire, avec la méthode unmap().

Les méthodes show_all() et hide_all() permettent d'afficher ou de cacher un widget et tous ses enfants.

La méthode realize() permet de réserver des ressources pour un widget, y compris pour sa propre fenêtre.

La méthode unrealize() libère la fenêtre du widget et des autres ressources qui lui sont associées. Elle a aussi pour effet de cacher et "démapper" le widget.

La méthode map() permet de réserver de l'espace pour le widget sur l'écran. Ceci s'applique seulement aux widgets qui doivent être gérés par le gestionnaire de fenêtre. Mapper un widget permet aussi de le "réaliser" si cela est nécessaire.

La méthode unmap() retire un widget de l'affichage et peut aussi le cacher si nécessaire.

XVIII-C - Accélérateurs pour les widgets

Les méthodes suivantes :

1. widget.add_accelerator(accel_signal, accel_group, accel_key, accel_mods, accel_flags)

3. widget.remove_accelerator(accel_group, accel_key, accel_mods)

ajoutent et suppriment des accélérateurs d'un groupe gtk.AcceleratorGroup qui doit être rattaché au widget de niveau supérieur pour gérer les accélérateurs.

Le paramètre accel_signal est un signal que le widget est en droit d'émettre.

Le paramètre accel_key est la touche du clavier à utiliser comme raccourci.

Le paramètre accel_mods est un groupe de modificateurs pouvant être associés à la touche de clavier (par exemple, Shift, Control, etc.) :

1. SHIFT_MASK

2. LOCK_MASK

3. CONTROL_MASK

4. MOD1_MASK

5. MOD2_MASK

6. MOD3_MASK

7. MOD4_MASK

8. MOD5_MASK

9. BUTTON1_MASK

10. BUTTON2_MASK

11. BUTTON3_MASK

12. BUTTON4_MASK

13. BUTTON5_MASK

14. RELEASE_MASK

Le paramètre accel_flags définit la manière dont les informations au sujet de l'accélérateur sont affichées. Les valeurs possibles sont :

1. ACCEL_VISIBLE         # montre le raccourci-clavier dans l'affichage du widget

3. ACCEL_LOCKED          # n'autorise pas la modification de l'affichage de l'accélérateur

Un groupe d'accélérateur est créé par la fonction :

accel_group = gtk.AccelGroup()

Le paramètre accel_group est lié au widget de niveau supérieur par la fonction :

window.add_accel_group(accel_group)

Un exemple d'ajout d'accélérateur :

menu_item.add_accelerator("activate", accel_group,
ord('Q'), gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE)

XVIII-D - Méthodes de nom des widgets

Les méthodes suivantes définissent et récupèrent le nom d'un widget :

1. widget.set_name(name)

3. nom = widget.get_name()

Le paramètre name est une chaine de caractères qui doit être associée au widget. Ceci est utile pour préciser des styles devant être utilisés avec des widgets particuliers dans une application. Au lieu d'utiliser la classe du widget, son nom peut être utilisé pour limiter l'application du style.

XVIII-E - Style des widgets

Les méthodes suivantes définissent et récupèrent le style associé à un widget :

1. widget.set_style(style)

3. style = widget.get_style()

La fonction :

style = get_default_style()

récupère le style par défaut.

Un style contient les informations graphiques dont a besoin un widget pour s'afficher dans les cinq états possibles :

1. STATE_NORMAL        # état pendant les opérations normales.

2. STATE_ACTIVE        # le widget est actif, (ex. : un bouton pressé).

3. STATE_PRELIGHT      # le pointeur de la souris est au-dessus du widget.

4. STATE_SELECTED      # le widget est sélectionné.

5. STATE_INSENSITIVE   # le widget est insensible.

Un style comprend les attributs suivants :

1. fg          # une liste de 5 couleurs de premier plan - une pour chaque état

2. bg          # une liste de 5 couleurs de fond

3. light       # une liste de 5 couleurs - créées dans la méthode set_style()

4. dark        # une liste de 5 couleurs - créées dans la méthode set_style()

5. mid         # une liste de 5 couleurs - créées dans la méthode set_style()

6. text        # une liste de 5 couleurs

7. base        # une liste de 5 couleurs

8. text_aa     # une liste de 5 couleurs intermédiaire entre text/base

10. black       # la couleur noire

11. white       # la couleur blanche

12. font_desc   # la description de police pango par défaut

14. xthickness  #

15. ythickness  #

17. fg_gc       # une liste de 5 contextes graphiques - créés dans la méthode set_style()

18. bg_gc       # une liste de 5 contextes graphiques - créés dans la méthode set_style()

19. light_gc    # une liste de 5 contextes graphiques - créés dans la méthode set_style()

20. dark_gc     # une liste de 5 contextes graphiques - créés dans la méthode set_style()

21. mid_gc      # une liste de 5 contextes graphiques - créés dans la méthode set_style()

22. text_gc     # une liste de 5 contextes graphiques - créés dans la méthode set_style()

23. base_gc     # une liste de 5 contextes graphiques - créés dans la méthode set_style()

24. black_gc    # une liste de 5 contextes graphiques - créés dans la méthode set_style()

25. white_gc    # une liste de 5 contextes graphiques - créés dans la méthode set_style()

27. bg_pixmap   # une liste de 5 GdkPixmaps

Chaque attribut peut être défini directement, par exemple style.black et style.fg_gc[gtk.STATE_NORMAL]. Tous les attributs sont en lecture seule exceptés style.black, style.white, style.black_gc et style.white_gc.

Un style existant peut être recopié pour être modifié ensuite par la méthode :

nouveau_style = style.copy()

qui copie les attributs de style à l'exception des listes de contexte graphique et des listes de couleurs light, dark et mid (clair, foncé et médian).

Le style actuel d'un widget peut être connu par :

style = widget.get_style()

Pour modifier le style d'un widget (par exemple, sa couleur de premier plan), il faut utiliser la méthode :

1. widget.modify_fg(etat, couleur)

2. widget.modify_bg(etat, couleur)

3. widget.modify_text(etat, couleur)

4. widget.modify_base(etat, couleur)

5. widget.modify_font(font_desc)

6. widget.set_style(style)

En définissant le paramètre style, on crée le contexte graphique et on alloue les couleurs. La plupart des widgets se redessinent automatiquement lorsque le style a été modifié. Si le paramètre style vaut None, le widget reviendra à son apparence par défaut.

Un widget n'est pas affecté par tous les changements de style. Par exemple, modifier la couleur de fond d'une étiquette (Label) ne changera pas la couleur de fond affichée, car le widget Label ne possède pas son propre gtk.gdk.Window. L'arrière-plan de l'étiquette dépend de la couleur de fond de son parent. Utiliser un EventBox pour contenir l'étiquette (Label) permet de définir sa couleur de fond. Se reporter à la section 10.1, “The EventBox” pour voir un exemple.

XIX - Chapitre 19. Temporisations, Entrées/Sorties et fonctions d'inactivité

XIX-A - Temporisations

On peut se demander comment faire effectuer un travail utile à GTK quand on est dans la fonction main(). Eh bien, on dispose de plusieurs options. En utilisant la fonction suivante du module gobject, on peut créer une fonction de temporisation qui sera appelée tous les "intervalle" millisecondes.

source_id = gobject.timeout_add(intervalle, fonction, ...)

Le paramètre intervalle représente le nombre de millisecondes entre deux appels à notre fonction. Le paramètre fonction est la fonction de rappel que l'on souhaite appeler. Tout argument suivant le deuxième est transmis à la fonction comme données. La valeur de retour "source_id" est un nombre entier qui peut être utilisé pour supprimer la temporisation en appelant :

gobject.source_remove(source_id)

Il est aussi possible d'empêcher un nouvel appel de la temporisation en renvoyant zéro ou FALSE depuis le rappel. Si on veut que le rappel soit réutilisé, il faut retourner TRUE.

Le rappel doit ressembler à ceci :

def rappel_temporisateur(...):

Le nombre d'arguments de la fonction de rappel doit être égal au nombre d'arguments de données indiqués dans la fonction timeout_add().

XIX-B - Contrôler les Entrées/Sorties

On peut vérifier la capacité à lire ou écrire un fichier (un fichier Python ou un fichier de bas niveau du système d'exploitation) et invoquer alors automatiquement un rappel. Ceci est surtout utile pour les applications réseau. La fonction de module gobject :

source_id = gobject.io_add_watch(source, condition, rappel)

où le premier argument (source) représente le fichier ouvert (un entier, descripteur du fichier Python ou du fichier système de bas niveau) que l'on veut surveiller. La fonction gobject.io_add_watch() utilise l'entier descripteur du fichier de bas niveau de manière interne, mais l'extraira de l'objet fichier Python par la méthode fileno() si nécessaire. Le deuxième argument (condition) précise ce que l'on veut contrôler. Ce peut être :

1. gobject.IO_IN - Des données sont disponibles en lecture.

3. gobject.IO_OUT - Le fichier est prêt pour une écriture.

5. gobject.IO_PRI - Il y a des données urgentes à lire dans le fichier.

7. gobject.IO_ERR - Condition d'erreur.

9. gobject.IO_HUP - Blocage (la connexion est rompue, habituellement pour les

10. pipes et sockets).

Celles-ci sont définies dans le module gobject. Comme vous l'avez déjà compris, le troisième argument, rappel, est la fonction que l'on souhaite appeler lorsque les conditions précédentes sont remplies.

La valeur de retour source_id peut être utilisée pour arrêter le contrôle du fichier grâce à la fonction suivante :

gobject.source_remove(source_id)

La fonction de rappel doit ressembler à :

def rappel_entree(source, condition):

où source et condition sont tels que décrits précédemment. La valeur de source sera le descripteur de fichier de bas niveau et non l'objet fichier Python (c.-à-d. la valeur retournée par la méthode de fichier Python fileno()).

On peut aussi empêcher la fonction de rappel d'être à nouveau utilisée en renvoyant zéro ou FALSE depuis le rappel. Si on veut que le "callback" soit réutilisé, il doit retourner TRUE.

XIX-C - Les fonctions d'inactivité

Que faire si on souhaite appeler une fonction quand rien d'autre ne se passe ? Il faut utiliser la fonction :

source_id = gobject.idle_add(rappel, ...)

Tous les arguments suivants le premier (indiqué par ...) sont transmis au rappel dans le même ordre. La valeur de retour source_id nous fournit une référence au gestionnaire.

Cette fonction entraine l'appel de la fonction indiquée rappel par GTK quand rien d'autre ne se passe.

Le rappel a la forme :

def rappel(...):

où les arguments transmis à la fonction rappel sont les mêmes que ceux qui ont été indiqués dans la fonction gobject.idle_add(). Comme pour les autres fonctions de rappel, renvoyer FALSE empêchera le rappel d'inactivité d'être appelé et renvoyer TRUE permet à la fonction de rappel d'être rappelée à la prochaine période d'inactivité.

Une fonction d'inactivité peut être supprimée de la queue par la fonction :

gobject.source_remove(source_id)

où le paramètre source_id est la valeur retournée par la fonction gobject.idle_add().

XX - Chapitre 20. Gestion avancée des événements et signaux

XX-A - Méthodes des signaux

Les méthodes des signaux sont des méthodes de gobject.GObject qui sont héritées par le gtk.Object inclus dans tous les widgets GTK+.

XX-A-1 - Connecter et déconnecter les gestionnaires de signaux

1. id_gestionnaire = objet.connect(nom, rappel, args_rappel)

3. id_gestionnaire = objet.connect_after(nom, rappel, args_rappel)

5. id_gestionnaire = objet.connect_object(nom, rappel, slot_object, args_rappel)

7. id_gestionnaire = objet.connect_object_after(nom, rappel, connecteur_objet, args_rappel)

9. objet.disconnect(id_gestionnaire)

Les quatre premières méthodes connectent un gestionnaire de signal (rappel) à un gtk.Object (objet) pour le signal spécifié par nom et retournent une valeur id_gestionnaire qui identifie la connexion. Le paramètre args_rappel consiste en zéro ou plusieurs arguments qui sont transmis dans l'ordre à rappel. Les méthodes connect_after() et connect_object_after() voient leurs gestionnaires de signal appelés après les autres gestionnaires (y compris les gestionnaires implicites) connectés au même objet et même signal. Chaque gestionnaire de signal d'objet possède son propre ensemble d'arguments auquel il s'attend. Il faut se référer à la documentation GTK+ pour trouver quels arguments sont nécessaires pour un gestionnaire de signal, bien que l'information pour les widgets courants soit disponible dans l'Annexe A, Les signaux GTK. Le gestionnaire de signal classique ressemble à :

def gest_signal(objet, ...., args_rappel):

Les gestionnaires de signaux qui appartiennent à une classe Python (indiqués dans les méthodes connect() comme self.rappel) possèdent un argument supplémentaire en première position - l'instance d'objet self :

gest_signal(self, objet, ...., args_rappel)

Les méthodes connect_object() et connect_object_after() appellent le gestionnaire de signal avec, en premier argument, le paramètre connecteur_objet au lieu de objet :

1. def gest_signal(connecteur_objet, ...., args_fonc):

3. def gest_signal(self, connecteur_objet, ...., args_fonc):

La méthode disconnect() supprime la connexion entre un gestionnaire de signal et un signal d'objet. Le paramètre id_gestionnaire indique quelle connexion supprimer.

XX-A-2 - Bloquer et libérer les gestionnaires de signaux

Les méthodes suivantes :

1. objet.handler_block(id_gestionnaire)

3. objet.handler_unblock(id_gestionnaire)

bloquent et débloquent le gestionnaire de signal indiqué par id_gestionnaire. Lorsqu'un gestionnaire de signal est bloqué, il ne peut être appelé lorsque le signal se produit.

XX-A-3 - Émettre et stopper les signaux

Les méthodes suivantes :

1. objet.emit(nom, ...)

3. objet.emit_stop_by_name(nom)

émettent et stoppent le signal nom. Émettre le signal provoque l'exécution du gestionnaire implicite et des gestionnaires définis par l'utilisateur. La méthode emit_stop_by_name() fait avorter l'émission actuelle du signal de ce nom.

XX-B - Émission et propagation du signal

L'émission du signal est le processus par lequel GTK+ exécute tous les gestionnaires d'un objet et signal spécifiques.

Tout d'abord, il faut noter que la valeur de retour d'une émission de signal est la valeur de retour du dernier gestionnaire exécuté. Comme les signaux d'événements sont tous du type RUN_LAST, ce sera celle du gestionnaire implicite (fourni par GTK+), sauf si on utilise la méthode connect_after().

Voici la manière dont un événement (par exemple, "button_press_event") est géré :

- démarrer avec le widget où survient l'événement ;

- émettre le signal générique "event". Si le gestionnaire de signal renvoie la valeur TRUE, arrêter le traitement ;

- autrement, émettre un signal spécifique "button_press_event". S'il renvoie la valeur TRUE, arrêter le traitement ;

- autrement, aller au parent du widget et répéter les deux étapes précédentes ;

- continuer jusqu'à ce qu'un des gestionnaires de signal renvoie TRUE, ou jusqu'à ce qu'on ait atteint le widget de plus haut niveau.

Quelques conséquences de ce qui précède :

- la valeur de retour de notre gestionnaire peut n'avoir aucun effet s'il existe un gestionnaire implicite à moins de le connecter avec connect_after() ;

- pour empêcher le gestionnaire implicite d'être exécuté, il faut se connecter avec connect() et utiliser la méthode emit_stop_by_name() - la valeur de retour n'affecte que si le signal est propagé, pas l'émission en cours.

XXI - Chapitre 21. Gérer les sélections

XXI-A - Généralités

Les sélections sont un des types d'inter-processus compris par X et GTK+. Une sélection identifie un morceau de données, par exemple, une portion de texte, choisi par l'utilisateur, en glissant avec la souris. Une seule application par affichage (variable display de X), l'application propriétaire peut posséder une sélection déterminée à un moment donné, donc lorsqu'une sélection est réclamée par une application, le précédent propriétaire doit signifier à l'utilisateur que la sélection a été cédée. Les autres applications peuvent demander le contenu d'une sélection sous différentes formes, appelées cibles. Il peut y avoir plusieurs sélections, mais la plupart des applications X n'en utilisent qu'une, le sélection primaire.

La plupart du temps, il n'est pas nécessaire qu'une application PyGTK gère elle-même les sélections. Les widgets classiques, tel le widget Entry (voir la Section 9.9, « Les champs de saisie ») possèdent déjà la capacité de réclamer la sélection si nécessaire (par exemple, quand l'utilisateur glisse la souris sur le texte), et de récupérer le contenu de la sélection possédée par un autre widget ou une autre application (par exemple, lorsque l'utilisateur clique sur le bouton 2 de la souris). Cependant, il peut exister des circonstances dans lesquelles on souhaite donner à d'autres widgets la capacité de fournir la sélection, ou on souhaite récupérer des cibles normalement non disponibles.

Un concept fondamental pour comprendre la gestion des sélections est celui d'atome (atom). Un atome est un nombre entier qui identifie de façon unique une chaine (sur un affichage donné). Certains atomes sont prédéterminés par le serveur X et par GTK.

XXI-B - Récupérer la sélection

Récupérer la sélection est un processus asynchrone. Pour démarrer le processus, on appelle :

result = widget.selection_convert(selection, target, time=0)

Ceci convertit la sélection selection au format indiqué par le paramètre cible target. La selection est un atome conforme au type de sélection ; les sélections courantes sont les chaines de caractères :

PRIMARY

SECONDARY

Si c'est possible, le champ temps time devrait être le moment où se produit l'événement qui a déclenché la sélection. Cela aide à s'assurer que les événements se produisent dans l'ordre dans lequel l'utilisateur les a demandés. Cependant, si ce n'est pas disponible (par exemple, si la conversion a été déclenchée par un signal "clicked"), on peut utiliser la valeur 0 qui indique l'utilisation du moment actuel. Le paramètre result vaut TRUE si la conversion réussit, FALSE dans le cas contraire.

Quand le propriétaire de la sélection répond à la requête, un signal "selection_received" est envoyé à votre application. Le gestionnaire de ce signal reçoit un objet gtk.SelectionData qui possède les attributs suivants :

selection
target
type
format
data

Les paramètres selection et target sont les valeurs que l'on a données dans la méthode selection_convert().

Le paramètre type est un atome qui identifie le type de données renvoyées par le propriétaire de la sélection. Quelques-unes des valeurs possibles sont "STRING", une chaine de caractères en latin-1, "ATOM", une série d'atomes, "INTEGER", un nombre entier, "image/x-xpixmap", etc. la plupart des cibles ne peuvent renvoyer qu'un seul type.

Voici la liste des atomes standards dans X et GTK+ :

PRIMARY
SECONDARY
ARC
ATOM
BITMAP
CARDINAL
COLORMAP
CURSOR
CUT_BUFFER0
CUT_BUFFER1
CUT_BUFFER2
CUT_BUFFER3
CUT_BUFFER4
CUT_BUFFER5
CUT_BUFFER6
CUT_BUFFER7
DRAWABLE
FONT
INTEGER
PIXMAP
POINT
RECTANGLE
RESOURCE_MANAGER
RGB_COLOR_MAP
RGB_BEST_MAP
RGB_BLUE_MAP
RGB_DEFAULT_MAP
RGB_GRAY_MAP
RGB_GREEN_MAP
RGB_RED_MAP
STRING
VISUALID
WINDOW
WM_COMMAND
WM_HINTS
WM_CLIENT_MACHINE
WM_ICON_NAME
WM_ICON_SIZE
WM_NAME
WM_NORMAL_HINTS
WM_SIZE_HINTS
WM_ZOOM_HINTS
MIN_SPACE
NORM_SPACE
MAX_SPACE END_SPACE,
SUPERSCRIPT_X
SUPERSCRIPT_Y
SUBSCRIPT_X
SUBSCRIPT_Y
UNDERLINE_POSITION
UNDERLINE_THICKNESS
STRIKEOUT_ASCENT
STRIKEOUT_DESCENT
ITALIC_ANGLE
X_HEIGHT
QUAD_WIDTH
WEIGHT
POINT_SIZE
RESOLUTION
COPYRIGHT
NOTICE
FONT_NAME
FAMILY_NAME
FULL_NAME
CAP_HEIGHT
WM_CLASS
WM_TRANSIENT_FOR
CLIPBOARD

Le paramètre format fournit la longueur des unités (par exemple, les caractères) en bits. En général, on ne s'en soucie pas lorsqu'on reçoit les données.

Le paramètre data représente les données renvoyées sous la forme d'une chaine de caractères.

PyGTK organise toutes les données reçues dans une chaine de caractères. Ceci rend facile la manipulation des cibles chaines. Pour récupérer les autres types (par exemple, ATOM ou INTEGER), le programme doit extraire l'information de la chaine renvoyée. PyGTK fournit deux méthodes pour récupérer le texte et une liste de cibles à partir des données de la sélection.

1. text = selection_data.get_text()

3. targets = selection_data.get_targets()

où le paramètre text est une chaine de caractères qui contient le texte de la sélection et targets est une liste des cibles reconnues par la sélection.

Étant donné une gtk.SelectionData contenant une liste de cibles, la méthode :

has_text = selection_data.targets_include_text()

doit renvoyer TRUE si une ou plusieurs cibles peuvent fournir un texte.

Le programme getselection.py illustre la récupération de cibles "STRING" ou "TARGETS" à partir de la sélection primaire et écrit les données correspondantes sur la console quand on clique sur le bouton associé. La Figure 21.1, « Exemple de récupération de sélection » montre un aperçu du programme  :

Figure 21.1. Exemple de récupération de sélection

Voici le code du programme getselection.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple getselection.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

9. class GetSelectionExample:

10. # Gestionnaire de signal invoqué quand l'utilisateur

11. # clique sur le bouton "Contenu de la cible"

12. def get_stringtarget(self, widget):

13. # Et demande la cible "STRING" pour la sélection primaire

14. ret = widget.selection_convert("PRIMARY", "STRING")

15. return

17. # Gestionnaire de signal invoqué quand l'utilisateur clique sur le bouton "Liste des cibles"

18. def get_targets(self, widget):

19. # Et demande la cible "TARGETS" pour la sélection primaire

20. ret = widget.selection_convert("PRIMARY", "TARGETS")

21. return

23. # Gestionnaire de signal appelé quand le propriétaire de la sélection retourne les données

24. def selection_received(self, widget, selection_data, data):

25. # On s'assure que les données sont au bon format

26. if str(selection_data.type) == "STRING":

27. # On affiche la chaîne reçue

28. print "STRING TARGET: %s" % selection_data.get_text()

30. elif str(selection_data.type) == "ATOM":

31. # On affiche la liste des cibles que l'on reçoit

32. targets = selection_data.get_targets()

33. for target in targets:

34. name = str(target)

35. if name != None:

36. print "%s" % name

37. else:

38. print "(mauvaise cible)"

39. else:

40. print "La sélection n'est pas un \"STRING\" ou un \"ATOM\" !"

42. return False

45. def __init__(self):

46. # Création de la fenêtre de niveau supérieur

47. window = gtk.Window(gtk.WINDOW_TOPLEVEL)

48. window.set_title("Get Selection")

49. window.set_border_width(10)

50. window.connect("destroy", lambda w: gtk.main_quit())

52. vbox = gtk.VBox(False, 0)

53. window.add(vbox)

54. vbox.show()

56. # Création du bouton pour obtenir le contenu chaine de la cible

57. button = gtk.Button("Contenu de la cible")

58. eventbox = gtk.EventBox()

59. eventbox.add(button)

60. button.connect_object("clicked", self.get_stringtarget, eventbox)

61. eventbox.connect("selection_received", self.selection_received)

62. vbox.pack_start(eventbox)

63. eventbox.show()

64. button.show()

66. # Création du bouton pour obtenir les formats de cible acceptés

67. button = gtk.Button("Liste des cibles")

68. eventbox = gtk.EventBox()

69. eventbox.add(button)

70. button.connect_object("clicked", self.get_targets, eventbox)

71. eventbox.connect("selection_received", self.selection_received)

72. vbox.pack_start(eventbox)

73. eventbox.show()

74. button.show()

76. window.show()

78. def main():

79. gtk.main()

80. return 0

82. if __name__ == "__main__":

83. GetSelectionExample()

84. main()

Les lignes 30-38 se chargent d'obtenir les données de sélection des "TARGETS" et impriment la liste des noms de cibles. Les boutons sont inclus dans leur propre boite à événements, car une sélection doit être associée à un gtk.gdkWindow alors que les boutons sont des widgets sans fenêtre dans GTK+2.0.

XXI-C - Alimenter la sélection

Alimenter la sélection est un peu plus compliqué. Il faut enregistrer des gestionnaires que l'on appellera lorsque la sélection sera réclamée. Pour chaque paire de sélection/cible à gérer, on fait un appel à :

widget.selection_add_target(selection, target, info)

où les paramètres widget, selection et target identifient les requêtes dont doit s'occuper ce gestionnaire. Quand une demande pour une sélection est reçue, on appellera le signal "selection_get". Le paramètre info est un nombre entier qui peut être utilisé comme un identifiant pour la cible spécifique dans le rappel.

La fonction de rappel a la forme :

def selection_get(widget, selection_data, info, time):

Le gtk.SelectionData est le même que précédemment, mais cette fois-ci, on est chargé de remplir les champs type, format et data. Le paramètre format est important ici - le serveur X l'utilise pour savoir si data nécessite un changement de l'ordre de ses bytes ou non. Habituellement la valeur est 8, c'est-à-dire un caractère, ou 32 pour un entier). Ceci est réalisé en appelant la méthode :

selection_data.set(type, format, data)

Cette méthode PyGTK peut seulement manipuler des chaines de caractères, donc le paramètre data doit être inclus dans une chaine Python, mais le format sera de la dimension appropriée (par exemple 32 pour les atomes et les nombres entiers, 8 pour les chaines). Les modules Python struct ou StringIO peuvent être utilisés pour convertir des données non-chaine en chaine de caractères. Par exemple, vous pouvez convertir une liste de nombres entiers vers une chaine et renseigner le selection_data par :

1. ilist = [1, 2, 3, 4, 5]

3. data = apply(struct.pack, ['%di'%len(ilist)] + ilist)

5. selection_data.set("INTEGER", 32, data)

La méthode suivante établit les données de la sélection à partir de la chaine indiquée :

selection_data.set_text(str, len)

À la demande de l'utilisateur, on réclame la propriété de la sélection en appelant :

result = widget.selection_owner_set(selection, time=0L)

où result vaut TRUE si le programme a réclamé la sélection selection avec succès. Si une autre application réclame la possession de la sélection, on obtiendra une réponse "selection_clear_event".

Comme exemple de fourniture de sélection, le programme setselection.py ajoute une fonction de sélection à un bouton interrupteur compris dans une boite gtk.EventBox. Le gtk.Eventbox est nécessaire, car la sélection doit être associée à une gtk.gdk.Window alors que le gtk.Button est un widget "sans fenêtre" dans GTK+ 2.0. Quand le bouton interrupteur est enfoncé, le programme réclame la propriété de la sélection primaire. La seule cible reconnue (à part certaines cibles comme "TARGETS" fournies par GTK+ lui-même) est la cible "STRING". Lorsque cette cible est demandée, une représentation en chaine de caractères du temps actuel est renvoyée. La Figure 21.2, « Exemple de création de sélection » montre le programme au moment où il prend possession de la sélection primaire.

Figure 21.2. Exemple de création de sélection

Voici le code du programme setselection.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple setselection.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

8. import time

10. class SetSelectionExample:

11. # Fonction de rappel quand l'utilisateur modifie la sélection.

12. def selection_toggled(self, widget, window):

13. if widget.get_active():

14. self.have_selection = window.selection_owner_set("PRIMARY")

15. # si réclamer la sélection échoue, on remet le bouton

16. # dans l'état inactif.

17. if not self.have_selection:

18. widget.set_active(False)

19. else:

20. if self.have_selection:

21. # Impossible de libérer la sélection en PyGTK,

22. # on indique juste que l'on ne la possède pas.

23. self.have_selection = False

24. return

26. # Appelé lorsqu'une autre application réclame la sélection.

27. def selection_clear(self, widget, event):

28. self.have_selection = False

29. widget.set_active(False)

30. return True

32. # Fournit le temps actuel comme sélection.

33. def selection_handle(self, widget, selection_data, info, time_stamp):

34. current_time = time.time()

35. timestr = time.asctime(time.localtime(current_time))

37. # Quand on renvoie une chaine unique, elle ne doit pas se terminer

38. # par une valeur nulle. Ceci le fait pour nous.

39. selection_data.set_text(timestr, len(timestr))

40. return

42. def __init__(self):

43. self.have_selection = False

44. # Création de la fenêtre de niveau supérieur

45. window = gtk.Window(gtk.WINDOW_TOPLEVEL)

46. window.set_title("Set Selection")

47. window.set_border_width(10)

48. window.connect("destroy", lambda w: gtk.main_quit())

49. self.window = window

50. # Création d'une boite événement pour contenir le bouton

51. # car il n'a pas sa propre GdkWindow.

52. eventbox = gtk.EventBox()

53. eventbox.show()

54. window.add(eventbox)

56. # Création d'un bouton interrupteur pour agir avec la sélection

57. selection_button = gtk.ToggleButton("Réclamer sélection")

58. eventbox.add(selection_button)

60. selection_button.connect("toggled", self.selection_toggled, eventbox)

61. eventbox.connect_object("selection_clear_event", self.selection_clear,

62. selection_button)

64. eventbox.selection_add_target("PRIMARY", "STRING", 1)

65. eventbox.selection_add_target("PRIMARY", "COMPOUND_TEXT", 1)

66. eventbox.connect("selection_get", self.selection_handle)

67. selection_button.show()

68. window.show()

70. def main():

71. gtk.main()

72. return 0

74. if __name__ == "__main__":

75. SetSelectionExample()

76. main()

XXII - Chapitre 22. Le glisser-déposer (DND)

PyGTK possède un ensemble de fonctions de haut niveau de communication inter-processus via le système glisser-déposer. PyGTK peut utiliser le glisser-déposer par-dessus les protocoles de bas niveau de Xdnd et le glisser-déposer Motif.

XXII-A - Aperçu du DND

Une application capable du glisser-déposer doit d'abord définir et configurer le(s) widget(s) pour le glisser-déposer. Chacun des widgets peut être l'origine et/ou la destination du glisser-déposer. Il faut noter que ces widgets doivent posséder une fenêtre X associée.

Les widgets origine peuvent exporter des données du glisser, en permettant ainsi à l'utilisateur de tirer des objets à partir d'elles, pendant que les widgets de destination peuvent recevoir des données du glisser. Les destinations du glisser-déposer peuvent limiter de qui ils acceptent des données du glisser, par exemple la même application ou n'importe quelle application (y compris eux-mêmes).

Envoyer et recevoir des données du déposer utilisent des signaux. Déposer un élément sur un widget de destination exige à la fois une demande de données (pour le widget origine) et un gestionnaire de signal de réception de données (pour le widget destination). Des gestionnaires de signaux supplémentaires peuvent être connectés si on souhaite savoir quand un glisser commence (à l'instant même du début), jusqu'à la réalisation du déposer, et quand la procédure glisser-déposer entière s'est terminée (avec succès ou pas).

Votre application aura besoin de fournir des données aux widgets origine quand ce sera demandé, ce qui implique d'avoir un gestionnaire de signal de requête de données du glisser. Pour les widgets de destination, ils auront besoin d'un gestionnaire de signal de données du déposer reçues.

Un cycle de glisser-déposer typique se présentera ainsi :

- le glisser commence. Le widget origine peut recevoir le signal "drag-begin". Il peut configurer une icône de glisser, etc. ;

- le glisser se déplace au-dessus d'une zone de déposer. Le widget destination peut recevoir le signal "drag-motion" ;

- le déposer est réalisé. Le widget destination peut recevoir le signal "drag-drop". Il doit demander les données de l'origine ;

- requête des données du glisser (quand le déposer a lieu). Le widget origine peut recevoir le signal "drag-data-get" ;

- données du déposer reçues (de la même application ou d'une autre). Le widget destination peut recevoir le signal "drag-data-received" ;

- données du glisser supprimées (si le glisser était un déplacement). Le widget origine peut recevoir le signal "drag-data-delete". ;

- procédure glisser-déposer effectuée. Le widget origine peut recevoir le signal "drag-end".

Il existe quelques étapes mineures qui peuvent s'intercaler ici où là, mais nous entrerons dans les détails plus tard.

XXII-B - Propriétés du glisser-déposer

Les données du glisser ont les propriétés suivantes :

- type d'action de glisser (par exemple : ACTION_COPY, ACTION_MOVE) ;

- type de glisser-déposer spécifique d'un client (un nom et une paire de nombres) ;

- type de format des données envoyées et reçues.

Les actions de glisser sont relativement évidentes, elles indiquent si le widget peut effectuer le glisser avec l'action ou les actions indiquées, par exemple gtk.gdk.ACTION_COPY et/ou gtk.gdk.ACTION_MOVE. Un gtk.gdk.ACTION_COPY serait un glisser-déposer typique sans que les données de la source soient supprimées, alors qu'un gtk.gdk.ACTION_MOVE serait juste comme un gtk.gdk.ACTION_COPY,mais avec une "suggestion" de supprimer les données de la source après l'appel du gestionnaire de signal de réception. Il existe d'autres actions de glisser, comme gtk.gdk.ACTION_LINK, qu'il sera possible d'examiner quand on sera plus avancé dans la connaissance du glisser-déposer.

Le type de glisser-déposer indiqué pour le client est beaucoup plus flexible, parce que c'est l'application qui le définira et le vérifiera spécifiquement. Il sera nécessaire de définir les widgets de destination pour recevoir certains types de glisser-déposer en indiquant un nom et/ou un nombre. Il serait plus sûr d'utiliser un nom puisqu'une autre application peut juste utiliser le même nombre dans un sens entièrement différent.

Les types de formats de données envoyées et reçues (selection target : cible de sélection) entrent en jeu uniquement dans leurs fonctions de gestion des données réclamées et reçues. Le terme selection target (cible de sélection) induit en erreur. C'est un terme qui vient de la sélection GTK+ (couper/copier et coller). Ce que recouvre cible de sélection est le type de format des données (par exemple un gtk.gdk.Atom, un entier ou une chaine de caractères) qui est envoyé ou reçu. La fonction de gestion des données demandées exige de préciser le type (cible de sélection) des données qui sont envoyées et le gestionnaire des données reçues doit pouvoir gérer le type cible de sélection des données reçues.

XXII-C - Méthodes du glisser-déposer

XXII-C-1 - Configurer le widget origine

La méthode drag_source_set() indique un ensemble de types de cibles pour une opération de glisser sur un widget.

widget.drag_source_set(start_button_mask, targets, actions)

Les paramètres ont la signification suivante :

- le paramètre widget indique le widget origine du glisser ;

- le paramètre start_button_mask indique un masque de bits des boutons qui peuvent débuter le glisser (par exemple BUTTON1_MASK) ;

- le paramètre targets indique une liste de types de données cibles gérées par le glisser ;

- le paramètre actions indique un masque de bits des actions possibles pour un glisser depuis cette fenêtre.

Le paramètre targets est une liste de tuples semblable à :

(target, flags, info)

où target est une chaine de caractères représentant le type de glisser.

Où flags limite la portée du glisser. Il peut prendre la valeur 0 (pas de limitation de la portée) ou la valeur des constantes suivantes :

1. gtk.TARGET_SAME_APP    # La destination peut être sélectionnée pour un glisser à

2. # l'intérieur de la même application

4. gtk.TARGET_SAME_WIDGET # La destination peut être sélectionnée pour un glisser à

5. # l'intérieur du même widget

où info est un identifiant entier assigné à l'application.

S'il n'est plus nécessaire qu'un widget continue d'être l'origine d'une opération de glisser-déposer, la méthode drag_source_unset() peut être utilisée pour supprimer un ensemble de types de glisser-déposer.

widget.drag_source_unset()

XXII-C-2 - Les signaux du widget origine

Les signaux suivants sont envoyés au widget origine pendant une opération de glisser-déposer :

Tableau 22.1. Signaux du widget origine

drag_begin (début du glisser)

def drag_begin_cb(widget, drag_context, data):

drag_data_get (obtention des données du glisser)

def drag_data_get_cb(widget, drag_context, selection_data, info, time, data):

drag_data_delete (suppression des données du glisser)

def drag_data_delete_cb(widget, drag_context, data):

drag_end (fin du glisser)

def drag_end_cb(widget, drag_context, data):

Le gestionnaire du signal "drag-begin" peut être utilisé pour définir des conditions de départ telle que l'icône du glisser en utilisant une des méthodes suivantes de la classe Widget : drag_source_set_icon(), drag_source_set_icon_pixbuf(), drag_source_set_icon_stock(). Le gestionnaire du signal "drag-end" peut servir à annuler les actions du gestionnaire du signal "drag-begin".

Le gestionnaire du signal "drag-data-get" devrait renvoyer les données de glisser correspondantes à la destination indiquée par le paramètre info. Il fournit à gtk.gdk.SelectionData les données du glisser.

Le gestionnaire du signal "drag-delete" est utilisé pour supprimer les données du glisser d'une action gtk.gdk.ACTION_MOVE une fois la copie des données effectuée.

XXII-C-3 - Configurer un widget destination

La méthode drag_dest_set() indique que ce widget peut recevoir le déposer et précise les types de déposer qu'il peut accepter.

La méthode drag_dest_unset() indique que ce widget ne peut plus maintenant recevoir de déposer.

1. widget.drag_dest_set(flags, targets, actions)

3. widget.drag_dest_unset()

où le paramètre flags indique quelles actions GTK+ doit réaliser pour le widget pour y effectuer le dépôt. Voici les valeurs possibles pour flags :

gtk.DEST_DEFAULT_MOTION

Si activé pour un widget, GTK+ vérifiera, pendant le survol d'un glisser au-dessus de ce widget, si le glisser appartient à la liste des cibles et actions possibles de ce widget. Alors, GTK+ appellera la méthode drag_status() appropriée.

gtk.DEST_DEFAULT_HIGHLIGHT

Si activé pour un widget, GTK+ mettra en évidence ce widget pendant la durée du survol par le glisser, si l'action et le format du widget sont compatibles.

gtk.DEST_DEFAULT_DROP

Si activé pour un widget, au moment du déposer, GTK+ vérifiera si le glisser concorde avec la liste des cibles et action possibles de ce widget ; si oui, GTK+ appellera la méthode drag_get_data() pour le widget. Que le déposer soit réussi ou non, GTK+ appellera la méthode drag_finish(). Si l'action était un déplacement et que le glisser est réussi, le valeur TRUE sera attribuée au paramètre delete de la méthode drag_finish().

gtk.DEST_DEFAULT_ALL

Si activé, indique que toutes les actions précédentes doivent être réalisées.

Le paramètre targets est une liste de tuples d'information de cibles décrites précédemment.

Le paramètre actions est un masque de bits des actions possibles à réaliser lors d'un glisser sur ce widget. Les valeurs possibles, combinables avec l'opérateur OR, sont les suivantes :

1. gtk.gdk.ACTION_DEFAULT

2. gtk.gdk.ACTION_COPY

3. gtk.gdk.ACTION_MOVE

4. gtk.gdk.ACTION_LINK

5. gtk.gdk.ACTION_PRIVATE

6. gtk.gdk.ACTION_ASK

Les paramètres targets et actions sont ignorés si le paramètre flags ne contient pas gtk.DEST_DEFAULT_MOTION ou gtk.DEST_DEFAULT_DROP. Dans ce cas, l'application doit gérer les signaux "drag-motion" et "drag-drop".

Le gestionnaire du signal "drag-motion" doit déterminer si les données du glisser sont appropriées en comparant les cibles de destination avec les cibles gtk.gdk.DragContext. Il peut aussi étudier les données du glisser en appelant la méthode drag_get_data(). Il faut utiliser la méthode gtk.gdk.DragContext.drag_status() pour mettre à jour le statut du paramètre drag_context.

Le gestionnaire du signal "drag-drop" doit déterminer la cible qui correspond en utilisant la méthode drag_dest_find_target() du widget et ensuite réclamer les données du glisser en utilisant la méthode drag_get_data(). Les données seront disponibles dans le gestionnaire du signal "drag-data-received".

Le programme dragtargets.py affiche les cibles possibles d'une opération de glisser dans un widget étiquette.

1. #!/usr/local/env python

3. import pygtk

4. pygtk.require('2.0')

5. import gtk

7. def motion_cb(wid, context, x, y, time):

8. context.drag_status(gtk.gdk.ACTION_COPY, time)

9. return True

11. def drop_cb(wid, context, x, y, time):

12. l.set_text('\n'.join([str(t) for t in context.targets]))

13. context.finish(True, False, time)

14. return True

16. w = gtk.Window()

17. w.set_size_request(200, 150)

18. w.drag_dest_set(0, [], 0)

19. w.connect('drag_motion', motion_cb)

20. w.connect('drag_drop', drop_cb)

21. w.connect('destroy', lambda w: gtk.main_quit())

22. l = gtk.Label()

23. w.add(l)

24. w.show_all()

26. gtk.main()

Le programme crée une fenêtre et ensuite l'utilise comme destination d'un glisser sans cible, ni action en fixant les drapeaux à zéro. Les gestionnaires motion_cb() et drop_cb() sont connectés respectivement aux signaux "drag-motion" et "drag-drop". Le gestionnaire motion_cb() se contente de configurer l'état du glisser pour permettre un déposer. Le gestionnaire drop_cb() modifie le texte de l'étiquette pour indiquer les cibles du glisser et termine le déposer en laissant intacte la source des données.

XXII-C-4 - Les signaux du widget destination

Pendant une opération de glisser-déposer, les signaux suivants sont transmis au widget destination.

Tableau 22.2. Signaux du widget destination

drag_motion (mouvement du glisser)

def drag_motion_cb(widget, drag_context, x, y, time, data):

drag_drop (glisser-déposer)

def drag_drop_cb(widget, drag_context, x, y, time, data):

drag_data_received (données reçues)

def drag_data_received_cb(widget, drag_context, x, y, selection_data, info, time, data):

Le programme dragndrop.py illustre l'utilisation du glisser-déposer dans une application. Un bouton avec une image (gtkxpm.py) constitue la source du glisser, il fournit en même temps un texte et une donnée xpm. Un widget layout constituera la destination du déposer du xpm, un bouton, la destination du texte. La Figure 22.1, « Exemple de glisser-déposer » montre le programme après le déposer du xpm et du texte, respectivement sur le layout et sur le bouton.

Figure 22.1. Exemple de glisser-déposer

Voici le code de dragndrop.py :

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple dragndrop.py

5. import pygtk

6. pygtk.require('2.0')

7. import gtk

8. import string, time

10. import gtkxpm

12. class ExempleDragNDrop:

13. HEIGHT = 600

14. WIDTH = 600

15. TARGET_TYPE_TEXT = 80

16. TARGET_TYPE_PIXMAP = 81

17. fromImage = [ ( "text/plain", 0, TARGET_TYPE_TEXT ),

18. ( "image/x-xpixmap", 0, TARGET_TYPE_PIXMAP ) ]

19. toButton = [ ( "text/plain", 0, TARGET_TYPE_TEXT ) ]

20. toCanvas = [ ( "image/x-xpixmap", 0, TARGET_TYPE_PIXMAP ) ]

22. def layout_resize(self, widget, event):

23. x, y, width, height = widget.get_allocation()

24. if width > self.lwidth or height > self.lheight:

25. self.lwidth = max(width, self.lwidth)

26. self.lheight = max(height, self.lheight)

27. widget.set_size(self.lwidth, self.lheight)

29. def makeLayout(self):

30. self.lwidth = self.WIDTH

31. self.lheight = self.HEIGHT

32. box = gtk.VBox(False,0)

33. box.show()

34. table = gtk.Table(2, 2, False)

35. table.show()

36. box.pack_start(table, True, True, 0)

37. layout = gtk.Layout()

38. self.layout = layout

39. layout.set_size(self.lwidth, self.lheight)

40. layout.connect("size-allocate", self.layout_resize)

41. layout.show()

42. table.attach(layout, 0, 1, 0, 1, gtk.FILL|gtk.EXPAND,

43. gtk.FILL|gtk.EXPAND, 0, 0)

44. # créer les barres de défilement et les placer dans une table

45. vScrollbar = gtk.VScrollbar(None)

46. vScrollbar.show()

47. table.attach(vScrollbar, 1, 2, 0, 1, gtk.FILL|gtk.SHRINK,

48. gtk.FILL|gtk.SHRINK, 0, 0)

49. hScrollbar = gtk.HScrollbar(None)

50. hScrollbar.show()

51. table.attach(hScrollbar, 0, 1, 1, 2, gtk.FILL|gtk.SHRINK,

52. gtk.FILL|gtk.SHRINK,

53. 0, 0)

54. # utiliser les ajustements du layout par les barres de défilement

55. vAdjust = layout.get_vadjustment()

56. vScrollbar.set_adjustment(vAdjust)

57. hAdjust = layout.get_hadjustment()

58. hScrollbar.set_adjustment(hAdjust)

59. layout.connect("drag_data_received", self.receiveCallback)

60. layout.drag_dest_set(gtk.DEST_DEFAULT_MOTION |

61. gtk.DEST_DEFAULT_HIGHLIGHT |

62. gtk.DEST_DEFAULT_DROP,

63. self.toCanvas, gtk.gdk.ACTION_COPY)

64. self.addImage(gtkxpm.gtk_xpm, 0, 0)

65. button = gtk.Button("Text Target")

66. button.show()

67. button.connect("drag_data_received", self.receiveCallback)

68. button.drag_dest_set(gtk.DEST_DEFAULT_MOTION |

69. gtk.DEST_DEFAULT_HIGHLIGHT |

70. gtk.DEST_DEFAULT_DROP,

71. self.toButton, gtk.gdk.ACTION_COPY)

72. box.pack_start(button, False, False, 0)

73. return box

75. def addImage(self, xpm, xd, yd):

76. hadj = self.layout.get_hadjustment()

77. vadj = self.layout.get_vadjustment()

78. style = self.window.get_style()

79. pixmap, mask = gtk.gdk.pixmap_create_from_xpm_d(

80. self.window.window, style.bg[gtk.STATE_NORMAL], xpm)

81. image = gtk.Image()

82. image.set_from_pixmap(pixmap, mask)

83. button = gtk.Button()

84. button.add(image)

85. button.connect("drag_data_get", self.sendCallback)

86. button.drag_source_set(gtk.gdk.BUTTON1_MASK, self.fromImage,

87. gtk.gdk.ACTION_COPY)

88. button.show_all()

89. # s'adapter au défilement du layout - la localisation de l'événement

90. # est relative à la zone visible et non à la taille du layout

91. self.layout.put(button, int(xd+hadj.value), int(yd+vadj.value))

92. return

94. def sendCallback(self, widget, context, selection, targetType, eventTime):

95. if targetType == self.TARGET_TYPE_TEXT:

96. now = time.time()

97. str = time.ctime(now)

98. selection.set(selection.target, 8, str)

99. elif targetType == self.TARGET_TYPE_PIXMAP:

100. selection.set(selection.target, 8,

101. string.join(gtkxpm.gtk_xpm, '\n'))

103. def receiveCallback(self, widget, context, x, y, selection, targetType,

104. time):

105. if targetType == self.TARGET_TYPE_TEXT:

106. label = widget.get_children()[0]

107. label.set_text(selection.data)

108. elif targetType == self.TARGET_TYPE_PIXMAP:

109. self.addImage(string.split(selection.data, '\n'), x, y)

111. def __init__(self):

112. self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)

113. self.window.set_default_size(300, 300)

114. self.window.connect("destroy", lambda w: gtk.main_quit())

115. self.window.show()

116. layout = self.makeLayout()

117. self.window.add(layout)

119. def main():

120. gtk.main()

122. if __name__ == "__main__":

123. ExempleDragNDrop()

124. main()

XXIII - Chapitre 23. Les fichiers de style rc GTK

GTK+ possède sa propre manière de traiter avec les paramètres par défaut des applications, en utilisant des fichiers rc. Ceux-ci peuvent servir pour définir les couleurs de n'importe quel widget, ils peuvent aussi, pour quelques widgets, servir à définir un arrière-plan avec un pixmap en mosaïque.

XXIII-A - Les fonctions pour le fichier rc

Au début de l'application, il faut inclure un appel à :

rc_parse(nom_du_fichier)

Le paramètre nom_du_fichier correspond au fichier rc. Ceci force GTK+ à analyser ce fichier et à utiliser les styles définis pour les types de widgets qui y sont décrits.

Si l'on souhaite avoir un ensemble spécifique de widgets possédant un style différent des autres ou réaliser un quelconque groupement particulier de widgets, il faut utiliser :

widget.set_name(nom)

Le nom choisi sera assigné au nouveau widget créé. Ceci permet de modifier les attributs de ce widget dans le fichier rc en utilisant son nom.

Si l'on utilise un appel comme celui-ci :

1. button = gtk.Button("Special Button")

3. button.set_name("special button")

alors ce bouton button reçoit le nom "special button" et on peut y faire référence dans le fichier rc par le nom "special button.GtkButton". [--- Vérifiez-le !]

La Section 23.3, « Exemple de fichier rc » ci-dessous définit les propriétés de la fenêtre principale, tous les enfants de cette fenêtre héritent du style décrit par le style "bouton principal". Voici le code utilisé :

1. window = gtk.Window(gtk.WINDOW_TOPLEVEL)

3. window.set_name("main window")

et le style est défini, dans le fichier rc, par :

widget "main window.*GtkButton*" style "main_button"

qui applique à tous les widgets Button (voir le Chapitre 6, Les boutons) de la fenêtre principale "main window" le style "main_buttons" tel qu'il est défini dans le fichier rc.

Ainsi qu'on peut le constater, c'est un système assez souple et puissant. Utilisez votre imagination pour en profiter au maximum.

XXIII-B - Format du fichier rc GTK

Le format du fichier rc GTK+ est illustré dans la Section 23.3, « Exemple de fichier rc » ci-dessous. C'est le fichier testgtkrc de la distribution GTK+, avec quelques commentaires et ajouts. Il est possible d'ajouter cette explication dans son application pour permettre à l'utilisateur de parfaire ses réglages.

Plusieurs commandes permettent de modifier les attributs d'un widget.

- fg - Définit la couleur de premier plan d'un widget.

- bg - Définit la couleur d'arrière-plan d'un widget.

- bg_pixmap - Compose l'arrière-plan d'un widget avec un pixmap en mosaïque.

- font - Définit la police utilisée pour le widget indiqué.

- De plus, un widget possède plusieurs états, il est possible de définir différentes couleurs, mosaïques de pixmap et polices pour chaque état. Ces états sont les suivants :

NORMAL

L'état normal d'un widget, la souris n'est pas au-dessus, il n'est pas enfoncé, etc.

PRELIGHT

Lorsque le widget est survolé par la souris, les couleurs définies pour cet état seront utilisées.

ACTIVE

Lorsque le widget est cliqué ou enfoncé, il devient actif et les attributs définis pour cet état seront utilisés.

INSENSITIVE

Lorsqu'un widget est marqué "insensible", et ne peut être activé, il prendra ces attributs

SELECTED

Quand un objet est sélectionné, il prend ces attributs.

Quand on utilise les mots-clé "fg" et "bg" pour définir les couleurs des widgets, il faut respecter ce format :

fg[<STATE>] = { Rouge, Vert, Bleu }

où STATE est un des états ci-dessus (PRELIGHT, ACTIVE, etc.), Rouge, Vert et Bleu sont des valeurs dans l'intervalle 0 - 1.0 ; {1.0,1.0,1.0} est le blanc. Elles doivent être au format décimal, sinon leur valeur est mise à 0, ainsi un simple "1" n'est pas correct, ce doit être "1.0". Un simple "0" fonctionne parce que les valeurs non reconnues sont mises à 0.

Le paramètre bg_pixmap est très semblable au précédent, mais les couleurs sont remplacées par un nom de fichier.

pixmap_path est une liste de chemins séparés par un ":". Ces chemins sont ceux où seront recherchés les pixmaps indiqués.

La commande "font" est simple :

font = "<font name>"

La partie la plus difficile est de trouver le contenu de la chaine font. Utiliser le programme xfontsel ou un utilitaire équivalent devrait faciliter la tâche.

La commande "widget_class" définit le style d'une classe de widgets. Ces classes sont énumérées dans la Section 5.1, « Hiérarchie des widgets » de la vue d'ensemble des widgets.

La commande "widget" applique un style donné à un ensemble de widgets portant un nom spécifique, quel que soit le style normal de cette classe de widgets. Ces widgets sont enregistrés dans l'application avec la méthode set_name(). Ceci permet de modifier les attributs d'un widget au coup par coup plutôt que de définir les attributs d'une toute une classe de widgets. Je vous recommande de documenter chacun de ces widgets spécifiques, permettant ainsi à l'utilisateur de les personnaliser.

Quand on utilise le mot-clé parent comme attribut, le widget utilise les attributs de son parent dans l'application.

Lorsque l'on définit un style, on peut attribuer les éléments d'un style déjà défini à ce nouveau style.

1. style "main_button" = "button"

2. {

3. font = "-adobe-helvetica-medium-r-normal--*-100-*-*-*-*-*-*"

4. bg[PRELIGHT] = { 0.75, 0, 0 }

5. }

Cet exemple utilise le style "button" et crée un nouveau style "main_button" simplement en modifiant la police et la couleur d'arrière-plan de l'état présélectionné (PRELIGHT) du style "button".

Naturellement, beaucoup de ces attributs ne s'appliquent pas à tous les widgets. C'est une simple question de bon sens. Ce qui peut s'appliquer devrait fonctionner.

XXIII-C - Exemple de fichier rc

1. # -*- coding:utf-8 -*-

2. # pixmap_path "<dir 1>:<dir 2>:<dir 3>:..."

3. #

4. pixmap_path "/usr/include/X11R6/pixmaps:/home/images/pixmaps"

5. #

6. # style <name> [= <name>]

7. # {

8. #   <option>

9. # }

10. #

11. # widget <widget_set> style <style_name>

12. # widget_class <widget_class_set> style <style_name>

14. # Voici la liste de tous les états possibles.

15. # Certains ne s'appliquent pas à tous les widgets

16. #

17. # NORMAL - L'état normal d'un widget, la souris n'est pas

18. # au-dessus, il n'est pas enfoncé, etc.

19. #

20. # PRELIGHT - Lorsque le widget est survolé par la souris,

21. # les couleurs définies pour cet état seront utilisées.

22. #

23. # ACTIVE - Lorsque le widget est cliqué ou enfoncé, il devient

24. # actif et les attributs définis pour cet état seront utilisés.

25. #

26. # INSENSITIVE - Lorsqu'un widget est marqué "insensible",

27. # et ne peut être activé, il prendra ces attributs

28. #

29. # SELECTED - Quand un objet est sélectionné, il prend ces attributs.

30. #

31. # À partir de ces états, on peut définir les attributs des widgets pour

32. # chacun d'eux avec les commandes suivantes :

33. #

34. # fg - indique la couleur de premier plan d'un widget.

35. # fg - indique la couleur d'arrière-plan d'un widget.

36. # bg_pixmap - l'arrière-plan du widget est une image en mosaïque.

37. # font - définit la police utilisée avec ce widget.

38. #

40. # Ceci crée un style appelé "button". Le nom n'a pas réellement

41. # d'importance, il est assigné aux widgets en cours à la fin du fichier.

43. style "window"

44. {

45. # Ceci remplit l'espacement autour de la fenêtre avec l'image indiquée.

46. #bg_pixmap[<STATE>] = "<pixmap filename>"

47. bg_pixmap[NORMAL] = "warning.xpm"

48. }

50. style "scale"

51. {

52. # Donne la valeur rouge à la couleur de premier plan (la couleur

53. # de police) dans l'état "NORMAL".

55. fg[NORMAL] = { 1.0, 0, 0 }

57. # Donne à ce widget la même image de fond que celle de son parent.

58. bg_pixmap[NORMAL] = "<parent>"

59. }

61. style "button"

62. {

63. # Ceci montre tous les états possibles d'un bouton. Le seul

64. # qui ne s'applique pas dans ce cas est l'état "SELECTED".

66. fg[PRELIGHT] = { 0, 1.0, 1.0 }

67. bg[PRELIGHT] = { 0, 0, 1.0 }

68. bg[ACTIVE] = { 1.0, 0, 0 }

69. fg[ACTIVE] = { 0, 1.0, 0 }

70. bg[NORMAL] = { 1.0, 1.0, 0 }

71. fg[NORMAL] = { .99, 0, .99 }

72. bg[INSENSITIVE] = { 1.0, 1.0, 1.0 }

73. fg[INSENSITIVE] = { 1.0, 0, 1.0 }

74. }

76. # Dans cet exemple, le style "main_button" hérite des attributs

77. # du style "button" et ensuite, on remplace les couleurs de police

78. # et d'arrière-plan de l'état "PRELIGHT"

80. style "main_button" = "button"

81. {

82. font = "-adobe-helvetica-medium-r-normal--*-100-*-*-*-*-*-*"

83. bg[PRELIGHT] = { 0.75, 0, 0 }

84. }

86. style "toggle_button" = "button"

87. {

88. fg[NORMAL] = { 1.0, 0, 0 }

89. fg[ACTIVE] = { 1.0, 0, 0 }

91. # Ceci définit comme image d'arrière-plan du bouton à bascule, celle

92. # de son parent (tel qu'il est défini dans l'application).

93. bg_pixmap[NORMAL] = "<parent>"

94. }

96. style "text"

97. {

98. bg_pixmap[NORMAL] = "marble.xpm"

99. fg[NORMAL] = { 1.0, 1.0, 1.0 }

100. }

102. style "ruler"

103. {

104. font = "-adobe-helvetica-medium-r-normal--*-80-*-*-*-*-*-*"

105. }

107. # pixmap_path "~/.pixmaps"

109. # Les lignes suivantes indiquent aux widgets d'utiliser les styles

110. # définis ci-dessus.

111. # Les types de widget sont énumérés dans la hiérarchie des classes,

112. #, mais on pourrait aussi les rappeler dans ce document pour servir

113. # de référence aux utilisateurs.

115. widget_class "GtkWindow" style "window"

116. widget_class "GtkDialog" style "window"

117. widget_class "GtkFileSelection" style "window"

118. widget_class "*Gtk*Scale" style "scale"

119. widget_class "*GtkCheckButton*" style "toggle_button"

120. widget_class "*GtkRadioButton*" style "toggle_button"

121. widget_class "*GtkButton*" style "button"

122. widget_class "*Ruler" style "ruler"

123. widget_class "*GtkText" style "text"

125. # Cette ligne donne à tous les boutons, enfants de la fenêtre

126. # principale ("main window"), le style main_button. Ceux-ci doivent

127. # être renseignés pour en profiter.

128. widget "main window.*GtkButton*" style "main_button"

XXIV - Chapitre 24. Scribble, Un programme simple de dessin

XXIV-A - Aperçu de Scribble

Dans cette section, nous construirons un programme simple de dessin. Ce faisant, nous examinerons comment gérer les événements souris, comment dessiner dans une fenêtre, et comment mieux dessiner en utilisant un pixmap en arrière-plan.

XXIV-B - Gestion des événements

Les signaux GTK+ que nous avons déjà vus concernent les actions de haut niveau, comme la sélection du choix d'un menu. Cependant, il est utile quelquefois de connaître des possibilités de plus bas niveau, comme le déplacement de la souris, ou l'appui sur une touche. Il existe aussi des signaux GTK+ correspondants à ces événements de bas niveau. Les gestionnaires de ces signaux possèdent un paramètre supplémentaire : un objet gtk.gdk.Event contenant des informations sur l'événement. Par exemple, les gestionnaires des événements de déplacement reçoivent un paramètre gtk.gdk.Event contenant une information GdkEventMotion qui comporte (en partie) ces attributs :

1. type    # type

2. window  # fenêtre

3. time    # temps

4. x

5. y

6. ...

7. state   # état

8. ...

window est la fenêtre dans laquelle l'événement est survenu.

x et y fournissent les coordonnées de l'événement.

type sera initialisé avec le type de l'événement, ici MOTION_NOTIFY. Ces types (du module gtk.gdk) sont :

1. NOTHING                un code spécial pour indiquer un événement nul.

3. DELETE                 le gestionnaire de fenêtre a demandé que la fenêtre de plus haut niveau soit

4. cachée ou détruite, d'habitude quand l'utilisateur clique sur

5. une icône spéciale dans la barre de titre.

7. DESTROY                la fenêtre a été détruite.

9. EXPOSE                 tout ou partie de la fenêtre est devenu visible et doit être

10. redessiné.

12. MOTION_NOTIFY          le pointeur (d'habitude une souris) a bougé.

14. BUTTON_PRESS           on a effectué un clic sur un bouton de la souris.

16. _2BUTTON_PRESS         on a effectué un double-clic sur un bouton de la souris

17. (deux clics en un bref temps) sur un bouton de la souris.

18. Note : chaque clic génère aussi un événement BUTTON_PRESS.

20. _3BUTTON_PRESS         on a effectué un triple-clic sur un bouton de la souris sur

21. un temps bref. Note : chaque clic génère aussi un

22. événement BUTTON_PRESS.

24. BUTTON_RELEASE         le bouton de la souris a été relâché.

26. KEY_PRESS              on a appuyé sur une touche.

28. KEY_RELEASE            la touche a été relâchée.

30. ENTER_NOTIFY           le pointeur est entré dans la fenêtre.

32. LEAVE_NOTIFY           le pointeur est sorti de la fenêtre.

34. FOCUS_CHANGE           le focus de clavier est dans ou a quitté la fenêtre.

36. CONFIGURE              la taille, position ou ordre d'empilage a changé. Noter que GTK+

37. n'utilisera pas ces événements pour les fenêtres enfants GDK_WINDOW_CHILD.

39. MAP                    la fenêtre a été mappée.

41. UNMAP                  la fenêtre n'est plus mappée.

43. PROPERTY_NOTIFY        une propriété de la fenêtre a été modifiée ou supprimée.

45. SELECTION_CLEAR        l'application a perdu la propriété d'une sélection.

47. SELECTION_REQUEST      une autre application a réclamé la sélection.

49. SELECTION_NOTIFY       une sélection a été reçue.

51. PROXIMITY_IN           un dispositif d'entrée a bougé en contact avec une surface sensible

52. (par ex. un écran tactile ou tablette graphique).

54. PROXIMITY_OUT          un dispositif d'entrée a bougé en coupant le contact avec une surface sensible

56. DRAG_ENTER             la souris est entrée dans la fenêtre pendant une opération glisser.

58. DRAG_LEAVE             la souris est sortie de la fenêtre pendant une opération glisser.

60. DRAG_MOTION            la souris a bougé dans la fenêtre pendant une opération glisser.

62. DRAG_STATUS            l'état de l'opération glisser démarrée par la fenêtre a changé.

64. DROP_START             une opération déposer sur la fenêtre a démarré.

66. DROP_FINISHED          une opération déposer initiée par la fenêtre est accomplie.

68. CLIENT_EVENT           un message d'une autre application a été reçu.

70. VISIBILITY_NOTIFY      l'état de visibilité de la fenêtre a changé.

72. NO_EXPOSE              indique que la région source est totalement disponible lorsque des extraits du

73. dessinable sont copiés. Ceci ne présente pas d'intérêt.

75. SCROLL                 ?

77. WINDOW_STATE           ?

79. SETTING                ?

Le paramètre state indique l'état du modificateur lorsque l'événement s'est produit (c'est-à-dire quelles sont les touches de modification et les boutons de souris qui ont été pressés). Il s'agit d'une opération de bit OR de certaines des valeurs (du module gtk.gdk) suivantes :

1. SHIFT_MASK    # masque de majuscules

2. LOCK_MASK     # masque de majuscules bloquées

3. CONTROL_MASK  # masque de contrôle

4. MOD1_MASK     # masque du modificateur 1

5. MOD2_MASK     # masque du modificateur 2

6. MOD3_MASK     # masque du modificateur 3

7. MOD4_MASK     # masque du modificateur 4

8. MOD5_MASK     # masque du modificateur 5

9. BUTTON1_MASK  # masque du bouton 1

10. BUTTON2_MASK  # masque du bouton 2

11. BUTTON3_MASK  # masque du bouton 3

12. BUTTON4_MASK  # masque du bouton 4

13. BUTTON5_MASK  # masque du bouton 5

Comme pour les autres signaux, on appelle la méthode connect() pour déterminer ce qui se passe lorsqu'un événement survient. Mais on doit aussi faire en sorte que GTK+ sache de quels événements nous voulons être avertis. Pour ce faire, on appelle la méthode :

widget.set_events(events)

où events définit les événements qui nous intéressent. Il s'agit d'une opération de bit OR de constantes qui indiquent différents types d'événements. Pour référence ultérieure, les types d'événements (du module gtk.gdk) sont :

1. EXPOSURE_MASK

2. POINTER_MOTION_MASK

3. POINTER_MOTION_HINT_MASK

4. BUTTON_MOTION_MASK

5. BUTTON1_MOTION_MASK

6. BUTTON2_MOTION_MASK

7. BUTTON3_MOTION_MASK

8. BUTTON_PRESS_MASK

9. BUTTON_RELEASE_MASK

10. KEY_PRESS_MASK

11. KEY_RELEASE_MASK

12. ENTER_NOTIFY_MASK

13. LEAVE_NOTIFY_MASK

14. FOCUS_CHANGE_MASK

15. STRUCTURE_MASK

16. PROPERTY_CHANGE_MASK

17. VISIBILITY_NOTIFY_MASK

18. PROXIMITY_IN_MASK

19. PROXIMITY_OUT_MASK

20. SUBSTRUCTURE_MASK

Il y a quelques points subtils qui doivent être observés lorsque l'on appelle la méthode set_events(). D'abord, elle doit être appelée avant que la fenêtre X d'un widget GTK soit créée. En pratique, cela signifie que l'on doit l'appeler immédiatement après avoir créé le widget. Ensuite, le widget doit faire partie de ceux réalisés avec une fenêtre X associée. Pour des raisons d'efficacité, de nombreux types de widgets n'ont pas de fenêtre propre, mais se dessinent dans la fenêtre de leur parent. Ces widgets sont :

1. gtk.Alignment

2. gtk.Arrow

3. gtk.Bin

4. gtk.Box

5. gtk.Image

6. gtk.Item

7. gtk.Label

8. gtk.Layout

9. gtk.Pixmap

10. gtk.ScrolledWindow

11. gtk.Separator

12. gtk.Table

13. gtk.AspectFrame

14. gtk.Frame

15. gtk.VBox

16. gtk.HBox

17. gtk.VSeparator

18. gtk.HSeparator

Pour capturer les événements pour ces widgets, on doit utiliser un widget EventBox. Voir la Section 10.1, « La boite à événement (EventBox) » pour plus de détails.

Voici les attributs d'événement qui sont définis par PyGTK pour chaque type d'événement :

1. événement               type              # type

2. window            # fenêtre

3. send_event        # événement transmis

5. NOTHING

6. DELETE

7. DESTROY                                   # pas d'attribut supplémentaire

9. EXPOSE                 area               # zone

10. count              # nombre

12. MOTION_NOTIFY          time               # temps

13. x

14. y

15. pressure           # pression

16. xtilt              # inclinaison en x

17. ytilt              # inclinaison en y

18. state              # état

19. is_hint            # est indice

20. source             # source

21. deviceid           # identifiant de dispositif

22. x_root             # racine x

23. y_root             # racine y

25. BUTTON_PRESS

26. _2BUTTON_PRESS

27. _3BUTTON_PRESS

28. BUTTON_RELEASE         time               # temps

29. x

30. y

31. pressure           # pression

32. xtilt              # inclinaison en x

33. ytilt              # inclinaison en y

34. state              # état

35. button             # bouton

36. source             # source

37. deviceid           # identifiant de dispositif

38. x_root             # racine x

39. y_root             # racine y

41. KEY_PRESS

42. KEY_RELEASE            time               # temps

43. state              # état

44. keyval             # valeur de clé

45. string             # chaine de caractères

47. ENTER_NOTIFY

48. LEAVE_NOTIFY           subwindow          # sous-fenêtre

49. time               # temps

50. x

51. y

52. x_root             # racine x

53. y_root             # racine y

54. mode               # mode

55. detail             # détail

56. focus              # focus

57. state              # état

59. FOCUS_CHANGE           _in                # dans

61. CONFIGURE              x

62. y

63. width              # largeur

64. height             # hauteur

66. MAP

67. UNMAP                                     # pas d'attribut supplémentaire

69. PROPERTY_NOTIFY        atom               # atome

70. time               # temps

71. state              # état

73. SELECTION_CLEAR

74. SELECTION_REQUEST

75. SELECTION_NOTIFY       selection          # sélection

76. target             # cible

77. property           # propriété

78. requestor          # demandeur

79. time               # temps

81. PROXIMITY_IN

82. PROXIMITY_OUT          time               # temps

83. source             # source

84. deviceid           # identifiant de dispositif

86. DRAG_ENTER

87. DRAG_LEAVE

88. DRAG_MOTION

89. DRAG_STATUS

90. DROP_START

91. DROP_FINISHED          context            # contexte

92. time               # temps

93. x_root             # racine x

94. y_root             # racine x

96. CLIENT_EVENT           message_type       # type de message

97. data_format        # format de données

98. data               # données

100. VISIBILTY_NOTIFY       state              # état

102. NO_EXPOSE                                 # pas d'attribut supplémentaire

XXIV-B-1 - Gestion des événements dans Scribble

Pour notre programme de dessin, on veut savoir quand le bouton de la souris est pressé et quand la souris est déplacée, nous indiquons donc POINTER_MOTION_MASK et BUTTON_PRESS_MASK. On veut aussi savoir quand il est nécessaire de redessiner notre fenêtre, on indique donc EXPOSURE_MASK. Bien que nous voulions être avertis via un événement Configure, d'un redimensionnement de la fenêtre, on n'a pas besoin de préciser le drapeau STRUCTURE_MASK correspondant, car il est automatiquement signalé pour chaque fenêtre.

Il peut cependant y avoir un problème en indiquant seulement POINTER_MOTION_MASK. Cela fera que le serveur ajoutera un nouvel événement de déplacement à la file des événements à chaque fois que l'utilisateur déplace la souris. Imaginons que cela prenne 0,1 seconde pour gérer un événement de déplacement, mais si le serveur X ajoute un nouvel événement de déplacement dans la queue toutes les 0,05 seconde, nous serons vite à la traîne de l'utilisateur. Si l'utilisateur dessine pendant 5 secondes, cela nous prendra 5 secondes de plus pour le traiter après qu'il ait relâché le bouton de la souris ! Ce que l'on voudrait, c'est ne récupérer qu'un événement de déplacement pour chaque événement que l'on traite. Pour cela, il faut préciser POINTER_MOTION_HINT_MASK.

Quand nous indiquons POINTER_MOTION_HINT_MASK, le serveur nous envoie un événement de déplacement la première fois que le pointeur se déplace après son entrée dans la fenêtre, ou après un événement d'appui ou de relâchement d'un bouton. Les événements de déplacement suivants seront supprimés jusqu'à ce que l'on demande explicitement la position du pointeur en utilisant la méthode gtk.gdk.Window :

x, y, mask = window.get_pointer()

où window est un objet gtk.gdk.Window, les paramètres x et y sont les coordonnées du pointeur et mask est le masque modificateur pour détecter les touches pressées. Il existe une méthode get_pointer() pour gtk.Widget qui fournit la même information que la méthode gtk.gdk.Window.get_pointer(), mais ne retourne pas l'indication de masque des touches

Le programme exemple scribblesimple.py montre l'utilisation de base des événements et des gestionnaires d'événements. La Figure 24.2, « Exemple de Scribble simple » illustre le programme :

Les gestionnaires d'événements sont connectés à la zone de dessin grâce aux ligne suivantes :

1. # Signaux utilisés pour gérer le pixmap hors écran

2. zone_dessin.connect("expose_event", expose_event)

3. zone_dessin.connect("configure_event", configure_event)

5. # Signaux d'événements

6. zone_dessin.connect("motion_notify_event", motion_notify_event)

7. zone_dessin.connect("button_press_event", bouton_press_event)

9. zone_dessin.set_events(gtk.gdk.EXPOSURE_MASK

10. | gtk.gdk.LEAVE_NOTIFY_MASK

11. | gtk.gdk.BUTTON_PRESS_MASK

12. | gtk.gdk.POINTER_MOTION_MASK

13. | gtk.gdk.POINTER_MOTION_HINT_MASK)

Les gestionnaires d'événements button_press_event() et motion_notify_event() dans scribblesimple.py sont ainsi :

1. def bouton_press_event(widget, event):

2. if event.button == 1 and pixmap != None:

3. brosse_dessin(widget, event.x, event.y)

4. return True

6. def motion_notify_event(widget, event):

7. if event.is_hint:

8. x, y, etat = event.window.get_pointer()

9. else:

10. x = event.x

11. y = event.y

12. etat = event.state

14. if etat & gtk.gdk.BUTTON1_MASK and pixmap != None:

15. brosse_dessin(widget, x, y)

17. return True

Les gestionnaires expose_event() et configure_event() seront décrits plus tard.

XXIV-C - Le widget zone de dessin (DrawingArea) et le dessin

Passons au processus de dessin sur l'écran. Le widget que l'on utilise pour cela est le widget DrawingArea (voir le Chapitre 12, La zone de dessin (Drawing Area)). Un tel widget est essentiellement une fenêtre X et rien de plus. Il s'agit d'une toile vide sur laquelle nous pouvons dessiner ce que nous voulons. On crée ce widget par l'appel à :

darea = gtk.DrawingArea()

On peut lui donner une taille par défaut par l'appel :

darea.set_size_request(largeur, hauteur)

Cette taille par défaut peu être surchargée, comme pour tous les widgets, en appelant la méthode set_size_request() et celle-ci, à son tour, peut être surchargée si l'utilisateur modifie manuellement la taille de la fenêtre contenant la zone de dessin.

Il faut noter que lorsque l'on crée un widget DrawingArea, on est complètement responsable du dessin du contenu. Si la fenêtre est cachée puis redécouverte, on reçoit un événement d'exposition et on doit redessiner ce qui avait été caché auparavant.

Devoir se rappeler tout ce qui a été dessiné à l'écran pour pouvoir correctement le redessiner peut s'avérer, et c'est un euphémisme, pénible. De plus, cela peut être visible si des portions de la fenêtre sont effacées puis redessinées étape par étape. La solution à ce problème est d'utiliser un pixmap d'arrière-plan hors écran. Au lieu de dessiner directement sur l'écran, on dessine sur une image stockée dans la mémoire du serveur et non affichée puis, lorsque l'image change ou lorsque de nouvelles parties de l'image sont affichées, on copie les parties correspondantes sur l'écran.

Pour créer un pixmap hors écran, on appelle la fonction :

pixmap = gtk.gdk.Pixmap(fenêtre, largeur, hauteur, profondeur=-1)

Le paramètre fenêtre désigne une fenêtre gtk.gdk.Window d'où ce pixmap tire certaines de ses propriétés. Les paramètres largeur et hauteur précisent la taille du pixmap, profondeur précise la profondeur de couleur (c'est-à-dire le nombre de bits par pixel) de la nouvelle fenêtre. Si cette profondeur vaut -1 ou n'est pas indiquée, elle correspondra à celle de fenêtre.

On crée le pixmap dans notre gestionnaire "configure_event". Cet événement est généré à chaque fois que la fenêtre change de taille, y compris lors de sa création initiale.

1. # Création d'un nouveau pixmap d'arrière-plan de la taille voulue

2. def configure_event(widget, event):

3. global pixmap

5. x, y, largeur, hauteur = widget.get_allocation()

6. pixmap = gtk.gdk.Pixmap(widget.window, largeur, hauteur)

7. pixmap.draw_rectangle(widget.get_style().white_gc,

8. True, 0, 0, largeur, hauteur)

10. return True

L'appel à draw_rectangle() initialise le pixmap à blanc. Nous en dirons plus tout à l'heure.

Le gestionnaire d'événement d'exposition copie alors simplement la partie utile du pixmap sur la zone de dessin (widget) en utilisant la méthode draw_pixmap(). On détermine la zone à redessiner en utilisant l'attribut event.area de l'événement d'exposition) :

1. # Redessine l'écran à partir du pixmap d'arrière-plan

2. def expose_event(widget, event):

3. x , y, largeur, hauteur = event.area

4. widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL],

5. pixmap, x, y, x, y, largeur, hauteur)

6. return False

On a vu comment garder l'écran à jour avec notre pixmap, mais comment dessine-t-on réellement ce que l'on veut dans le pixmap ? Il existe un grand nombre d'appels dans PyGTK pour dessiner sur des "dessinables". Un "dessinable" est simplement quelque chose sur lequel on peut dessiner. Cela peut être une fenêtre, un pixmap, ou un bitmap (une image en noir et blanc). On a déjà vu plus haut deux de ces appels, draw_rectangle() et draw_pixmap(). En voici La liste complète :

1. drawable.draw_point(gc, x, y)

3. drawable.draw_line(gc, x1, y1, x2, y2)

5. drawable.draw_rectangle(gc, fill, x, y, width, height)

7. drawable.draw_arc(gc, fill, x, y, width, height, angle1, angle2)

9. drawable.draw_polygon(gc, fill, points)

11. drawable.draw_drawable(gc, src, xsrc, ysrc, xdest, ydest, width, height)

13. drawable.draw_points(gc, points)

15. drawable.draw_lines(gc, points)

17. drawable.draw_segments(gc, segments)

19. drawable.draw_rgb_image(gc, x, y, width, height, dither, buffer, rowstride)

21. drawable.draw_rgb_32_image(gc, x, y, width, height, dither, buffer, rowstride)

23. drawable.draw_gray_image(gc, x, y, width, height, dither, buffer, rowstride)

Les méthodes des zones de dessin sont identiques à celles des "dessinables", ainsi on peut se reporter aux méthodes décrites dans la Section 12.2, « Les méthodes pour dessiner » pour plus de détails sur celles-ci. Toutes ces méthodes partagent les mêmes premiers arguments, le premier étant le contexte graphique (gc).

Un contexte graphique encapsule l'information sur des éléments comme la couleur de premier et d'arrière-plan et la largeur de ligne. PyGTK possède un ensemble complet de fonctions pour créer et manipuler les contextes graphiques, mais pour faire simple, nous n'utiliserons que les contextes graphiques prédéfinis. Reportez-vous à la Section 12.1, « Le contexte graphique » pour plus d'informations sur les contextes graphiques. Chaque widget possède un style associé (qui peut être modifié dans un fichier gtkrc, voir la Chapitre 23, Les fichiers de style rc GTK. Celui-ci, entre autres choses, stocke plusieurs contextes graphiques. Quelques exemples d'accès à ces contextes graphiques :

1. widget.get_style().white_gc

3. widget.get_style().black_gc

5. widget.get_style().fg_gc[STATE_NORMAL]

7. widget.get_style().bg_gc[STATE_PRELIGHT]

Les champs fg_gc, bg_gc, dark_gc et light_gc sont indexés par un paramètre qui peut prendre les valeurs suivantes :

1. STATE_NORMAL,

2. STATE_ACTIVE,

3. STATE_PRELIGHT,

4. STATE_SELECTED,

5. STATE_INSENSITIVE

Par exemple, pour STATE_SELECTED, la couleur de premier plan par défaut est le blanc, la couleur d'arrière-plan par défaut est le bleu foncé.

La fonction draw_brush(), qui réalise le dessin sur le pixmap est alors :

1. # Dessine un rectangle sur l'écran

2. def brosse_dessin(widget, x, y):

3. rect = (int(x-5), int(y-5), 10, 10)

4. pixmap.draw_rectangle(widget.get_style().black_gc, True,

5. rect[0], rect[1], rect[2], rect[3])

6. widget.queue_draw_area(rect[0], rect[1], rect[2], rect[3])

Après avoir dessiné le rectangle représentant la brosse sur le pixmap, on appelle la fonction :

widget.queue_draw_area(x, y, width, height)

qui indique à X que cette zone nécessite d'être mise à jour. X générera éventuellement un événement d'exposition (en combinant peut-être les zones passées dans plusieurs appels à draw()) ce qui forcera le gestionnaire d'événement d'exposition à recopier les parties adéquates à l'écran.

Nous avons maintenant couvert entièrement le programme de dessin, sauf quelques détails banals comme la création de la fenêtre principale.

XXV - Tips For Writing PyGTK Applications

XXVI - Contributing

XXVII - Credits

XXVII-A - PyGTK Credits

XXVII-B - Original GTK+ Credits

XXVIII - Chapitre 28. Copyright du tutoriel et licence d'utilisation

Le tutoriel PyGTK est Copyright (C) 2001-2005 John Finlay.

Le tutoriel GTK est Copyright (C) 1997 Ian Main.

Copyright (C) 1998-1999 Tony Gale.

Vous pouvez copier et distribuer ce document sous condition que la mention de Copyright et cette notice de permission soient reproduites dans toutes les copies.

Vous pouvez copier et distribuer une version modifiée de ce document sous les mêmes conditions qu'au paragraphe précédent, que la notice de Copyright originale soit intégralement incluse et que la version modifiée soit diffusée sous la même licence.

Vous pouvez copier et diffuser des traductions de ce document dans une autre langue sous les mêmes conditions que pour une version modifiée.

Si vous avez l'intention d'inclure ce document dans une publication, veuillez contacter l'auteur, nous ferons l'effort de vous assurer l'information disponible la plus plus récente.

Ce document est distribué dans l'espoir qu'il sera utile, mais sans aucune garantie. Il est simplement fourni comme une ressource libre. Les auteurs et les mainteneurs de l'information fournie ne garantissent en rien que l'information soit même juste.

Ce document est distribué dans l'espoir qu'il sera utile, mais sans aucune garantie. Il est simplement fourni comme une ressource libre. Le traducteur de l'information ne garantit en rien que l'information soit bien traduite.

XXIX - Annexe A - Les signaux GTK

XXIX-A - GtkObject

destroy(object, data)

XXIX-B - GtkWidget

1. show(GtkWidget, data)

3. hide(widget, data)

5. map(widget, data)

7. unmap(widget, data)

9. realize(widget, data)

11. unrealize(widget, data)

13. draw(widget, area, data)

15. draw-focus(widget, data)

17. draw-default(widget, data)

19. size-request(widget, requisition, data)

21. size-allocate(widget, allocation, data)

23. state-changed(widget, state, data)

25. parent-set(widget, object, data)

27. style-set(widget, style, data)

29. add-accelerator(widget, accel_signal_id, accel_group, accel_key, accel_mods,

30. accel_flags, data)

32. remove-accelerator(widget, accel_group, accel_key, accel_mods, data)

34. bool = event(widget, event, data)

36. bool = button-press-event(widget, event, data)

38. bool = button-release-event(widget, event, data)

40. bool = motion-notify-event(widget, event, data)

42. bool = delete-event(widget, event, data)

44. bool = destroy-event(widget, event, data)

46. bool = expose-event(widget, event, data)

48. bool = key-press-event(widget, event, data)

50. bool = key-release-event(widget, event, data)

52. bool = enter-notify-event(widget, event, data)

54. bool = leave-notify-event(widget, event, data)

56. bool = configure-event(widget, event, data)

58. bool = focus-in-event(widget, event, data)

60. bool = focus-out-event(widget, event, data)

62. bool = map-event(widget, event, data)

64. bool = unmap-event(widget, event, data)

66. bool = property-notify-event(widget, event, data)

68. bool = selection-clear-event(widget, event, data)

70. bool = selection-request-event(widget, event, data)

72. bool = selection-notify-event(widget, event, data)

74. selection-get(widget, selection_data, info, time, data)

76. selection-received(widget, selection_data, time, data)

78. bool = proximity-in-event(widget, event, data)

80. bool = proximity-out-event(widget, event, data)

82. drag-begin(widget, context, data)

84. drag-end(widget, context, data)

86. drag-data-delete(widget, context, data)

88. drag-leave(widget, context, time, data)

90. bool = drag-motion(widget, context, x, y, time, data)

92. bool = drag-drop(widget, context, x, y, time, data)

94. drag-data-get(widget, context, selection_data, info, time, data)

96. drag-data-received(widget, context, info, time, selection_data,

97. info, time, data)

99. bool = client-event(widget, event, data)

101. bool = no-expose-event(widget, event, data)

103. bool = visibility-notify-event(widget, event, data)

105. debug-msg(widget, string, data)

XXIX-C - GtkData

disconnect(data_obj, data)

XXIX-D - GtkContainer

1. add(container, widget, data)

3. remove(container, widget, data)

5. check-resize(container, data)

7. direction = focus(container, direction, data)

9. set-focus-child(container, widget, data)

XXIX-E - GtkCalendar

1. month-changed(calendar, data)

3. day-selected(calendar, data)

5. day-selected-double-click(calendar, data)

7. prev-month(calendar, data)

9. next-month(calendar, data)

11. prev-year(calendar, data)

13. next-year(calendar, data)

XXIX-F - GtkEditable

1. changed(editable, data)

3. insert-text(editable, new_text, text_length, position, data)

5. delete-text(editable, start_pos, end_pos, data)

7. activate(editable, data)

9. set-editable(editable, is_editable, data)

11. move-cursor(editable, x, y, data)

13. move-word(editable, num_words, data)

15. move-page(editable, x, y, data)

17. move-to-row(editable, row, data)

19. move-to-column(editable, column, data)

21. kill-char(editable, direction, data)

23. kill-word(editable, drirection, data)

25. kill-line(editable, direction, data)

27. cut-clipboard(editable, data)

29. copy-clipboard(editable, data)

31. paste-clipboard(editable, data)

XXIX-G - GtkNotebook

switch-page(noteboook, page, page_num, data)

XXIX-H - GtkList

1. selection-changed(list, data)

3. select-child(list, widget, data)

5. unselect-child(list, widget, data)

XXIX-I - GtkMenuShell

1. deactivate(menu_shell, data)

3. selection-done(menu_shell, data)

5. move-current(menu_shell, direction, data)

7. activate-current(menu_shell, force_hide, data)

9. cancel(menu_shell, data)

XXIX-J - GtkToolbar

1. orientation-changed(toolbar, orientation, data)

3. style-changed(toolbar, toolbar_style, data)

XXIX-K - GtkButton

1. pressed(button, data)

3. released(button, data)

5. clicked(button, data)

7. enter(button, data)

9. leave(button, data)

XXIX-L -  GtkItem

1. select(item, data)

3. deselect(item, data)

5. toggle(item, data)

XXIX-M - GtkWindow

set-focus(window, widget, data)

XXIX-N - GtkHandleBox

1. child-attached(handle_box, widget, data)

3. child-detached(handle_box, widget, data)

XXIX-O - GtkToggleButton

toggled(toggle_button, data)

XXIX-P - GtkMenuItem

1. activate(menu_item, data)

3. activate-item(menu_item, data)

XXIX-Q - GtkCheckMenuItem

toggled(check_menu_item, data)

XXIX-R - GtkInputDialog

1. enable-device(input_dialog, deviceid, data)

3. disable-device(input_dialog, deviceid, data)

XXIX-S - GtkColorSelection

color-changed(color_selection, data)

XXIX-T - GtkStatusBar

1. text-pushed(statusbar, context_id, text, data)

3. text-popped(statusbar, context_id, text, data)

XXIX-U - GtkCurve

curve-type-changed(curve, data)

XXIX-V - GtkAdjustment

1. changed(adjustment, data)

3. value-changed(adjustment, data)

Annexe B - Code Examples

scribblesimple.py

1. #!/usr/bin/env python

2. # -*- coding:utf-8 -*-

3. # exemple scribblesimple.py

5. # GTK - The GIMP Toolkit

6. # Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald

7. # Copyright (C) 2001-2004 John Finlay

8. #

9. # This library is free software; you can redistribute it and/or

10. # modify it under the terms of the GNU Library General Public

11. # License as published by the Free Software Foundation; either

12. # version 2 of the License, or (at your option) any later version.

13. #

14. # This library is distributed in the hope that it will be useful,

15. # but WITHOUT ANY WARRANTY; without even the implied warranty of

16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

17. # Library General Public License for more details.

18. #

19. # You should have received a copy of the GNU Library General Public

20. # License along with this library; if not, write to the

21. # Free Software Foundation, Inc., 59 Temple Place - Suite 330,

22. # Boston, MA 02111-1307, USA.

25. import pygtk

26. pygtk.require('2.0')

27. import gtk

29. # Pixmap d'arrière-plan pour la zone de dessin

30. pixmap = None

32. # Création d'un nouveau pixmap d'arrière-plan de la taille voulue

33. def configure_event(widget, event):

34. global pixmap

36. x, y, largeur, hauteur = widget.get_allocation()

37. pixmap = gtk.gdk.Pixmap(widget.window, largeur, hauteur)

38. pixmap.draw_rectangle(widget.get_style().white_gc,

39. True, 0, 0, largeur, hauteur)

41. return True

43. # Redessine l'écran à partir du pixmap d'arrière-plan

44. def expose_event(widget, event):

45. x , y, largeur, hauteur = event.area

46. widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL],

47. pixmap, x, y, x, y, largeur, hauteur)

48. return False

50. # Dessine un rectangle sur l'écran

51. def brosse_dessin(widget, x, y):

52. rect = (int(x-5), int(y-5), 10, 10)

53. pixmap.draw_rectangle(widget.get_style().black_gc, True,

54. rect[0], rect[1], rect[2], rect[3])

55. widget.queue_draw_area(rect[0], rect[1], rect[2], rect[3])

57. def bouton_press_event(widget, event):

58. if event.button == 1 and pixmap != None:

59. brosse_dessin(widget, event.x, event.y)

60. return True

62. def motion_notify_event(widget, event):

63. if event.is_hint:

64. x, y, etat = event.window.get_pointer()

65. else:

66. x = event.x

67. y = event.y

68. etat = event.state

70. if etat & gtk.gdk.BUTTON1_MASK and pixmap != None:

71. brosse_dessin(widget, x, y)

73. return True

75. def main():

76. fenêtre = gtk.Window(gtk.WINDOW_TOPLEVEL)

77. fenetre.set_name ("Test Input")

79. boitev = gtk.VBox(False, 0)

80. fenetre.add(boitev)

81. boitev.show()

83. fenetre.connect("destroy", lambda w: gtk.main_quit())

85. # Création de la zone de dessin

86. zone_dessin = gtk.DrawingArea()

87. zone_dessin.set_size_request(200, 200)

88. boitev.pack_start(zone_dessin, True, True, 0)

90. zone_dessin.show()

92. # Signaux utilisés pour gérer le pixmap hors écran

93. zone_dessin.connect("expose_event", expose_event)

94. zone_dessin.connect("configure_event", configure_event)

96. # Signaux d'événements

97. zone_dessin.connect("motion_notify_event", motion_notify_event)

98. zone_dessin.connect("button_press_event", bouton_press_event)

100. zone_dessin.set_events(gtk.gdk.EXPOSURE_MASK

101. | gtk.gdk.LEAVE_NOTIFY_MASK

102. | gtk.gdk.BUTTON_PRESS_MASK

103. | gtk.gdk.POINTER_MOTION_MASK

104. | gtk.gdk.POINTER_MOTION_HINT_MASK)

106. #. Et un bouton quitter

107. bouton = gtk.Button("Quit")

108. boitev.pack_start(bouton, False, False, 0)

110. bouton.connect_object("clicked", lambda w: w.destroy(), fenêtre)

111. bouton.show()

113. fenetre.show()

115. gtk.main()

117. return 0

119. if __name__ == "__main__":

120. main()

Annexe C - ChangeLog

Annexe D - À propos de la traduction

Marc Verney a démarré la traduction de ce tutoriel en se basant sur la version 1.3 de John Finlay (chapitres 1 à 9, 11 et 13).

Je la continue en me basant sur la version 2.4 (chapitres 14 à 16) et sur la version 2.5 pour la suite.

Les différences sont minimes et la mise à jour des premiers chapitres se fera parallèlement à la suite de la traduction.

Les exemples de la version française ont été réalisés avec une Mandrake 9.2 et ensuite avec :

- Slackware 10.1 ;

- Python 2.4 ;

- GTK+ 2.6.1 ;

- PyGTK 2.6.1.

Si vous avez remarqué des problèmes, des erreurs, si vous avez une meilleure traduction à proposer pour un mot ou une expression merci de m'en faire part à l'adresse suivante : .
J'accepte toute suggestion de correction.

Mises à jour :

2006 3/11
Chap. 3 : remise à niveau du tutoriel version 2.5. et indication de l'encodage dans les exemples.

2006 22/10
Chap. 1 et 2 : remise à niveau du tutoriel version 2.5.
Chap. 1 et 2 : indication de l'encodage dans les exemples (coding: utf8) pour disposer des minuscules accentuées dans les affichages.
À partir du chap 15 : indication de l'encodage dans les exemples (coding: utf8).

2006 10/10
Dans les exemples : remplacement de gtk.TRUE, gtk.FALSE et gtk.mainquit() par False, True et gtk.main_quit().

(1)NdT : depuis PyGTK 2.4., il y a aussi gtk.Alignment.set_padding() et gtk.Alignment.get_padding().

(2)NdT : pris dans le manuel de référence gtk.Alignment.