24. Chapitre 24. Scribble, Un programme simple de dessin▲
24-1. 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.
24-2. 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 :
2.
3.
4.
5.
6.
7.
8.
type #
type
window #
fenêtre
time #
temps
x
y
...
state #
état
...
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
NOTHING un code spécial pour indiquer un événement nul.
DELETE le gestionnaire de fenêtre a demandé que la fenêtre de plus haut niveau soit
cachée ou détruite, d'habitude quand l'utilisateur clique sur
une icône spéciale dans la barre de titre.
DESTROY la fenêtre a été détruite.
EXPOSE tout ou partie de la fenêtre est devenu visible et doit être
redessiné.
MOTION_NOTIFY le pointeur (d'habitude une souris) a bougé.
BUTTON_PRESS on a effectué un clic sur un bouton de la souris.
_2BUTTON_PRESS on a effectué un double-clic sur un bouton de la souris
(deux clics en un bref temps) sur un bouton de la souris.
Note : chaque clic génère aussi un évènement BUTTON_PRESS.
_3BUTTON_PRESS on a effectué un triple-clic sur un bouton de la souris sur
un temps bref. Note : chaque clic génère aussi un
évènement BUTTON_PRESS.
BUTTON_RELEASE le bouton de la souris a été relâché.
KEY_PRESS on a appuyé sur une touche.
KEY_RELEASE la touche a été relâchée.
ENTER_NOTIFY le pointeur est entré dans la fenêtre.
LEAVE_NOTIFY le pointeur est sorti de la fenêtre.
FOCUS_CHANGE le focus de clavier est dans ou a quitté la fenêtre.
CONFIGURE la taille, position ou ordre d'empilage a changé. Noter que GTK+
n'utilisera pas ces événements pour les fenêtres enfants GDK_WINDOW_CHILD.
MAP la fenêtre a été mappée.
UNMAP la fenêtre n'est plus mappée.
PROPERTY_NOTIFY une propriété de la fenêtre a été modifiée ou supprimée.
SELECTION_CLEAR l'application a perdu la propriété d'une sélection.
SELECTION_REQUEST une autre application a réclamé la sélection.
SELECTION_NOTIFY une sélection a été reçue.
PROXIMITY_IN un dispositif d'entrée a bougé en contact avec une surface sensible
(par ex. un écran tactile ou tablette graphique).
PROXIMITY_OUT un dispositif d'entrée a bougé en coupant le contact avec une surface sensible
DRAG_ENTER la souris est entrée dans la fenêtre pendant une opération glisser.
DRAG_LEAVE la souris est sortie de la fenêtre pendant une opération glisser.
DRAG_MOTION la souris a bougé dans la fenêtre pendant une opération glisser.
DRAG_STATUS l'état de l'opération glisser démarrée par la fenêtre a changé.
DROP_START une opération déposer sur la fenêtre a démarré.
DROP_FINISHED une opération déposer initiée par la fenêtre est accomplie.
CLIENT_EVENT un message d'une autre application a été reçu.
VISIBILITY_NOTIFY l'état de visibilité de la fenêtre a changé.
NO_EXPOSE indique que la région source est totalement disponible lorsque des extraits du
dessinable sont copiés. Ceci ne présente pas d'intérêt.
SCROLL ?
WINDOW_STATE ?
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
SHIFT_MASK #
masque
de
majuscules
LOCK_MASK #
masque
de
majuscules
bloquées
CONTROL_MASK #
masque
de
contrôle
MOD1_MASK #
masque
du
modificateur
1
MOD2_MASK #
masque
du
modificateur
2
MOD3_MASK #
masque
du
modificateur
3
MOD4_MASK #
masque
du
modificateur
4
MOD5_MASK #
masque
du
modificateur
5
BUTTON1_MASK #
masque
du
bouton
1
BUTTON2_MASK #
masque
du
bouton
2
BUTTON3_MASK #
masque
du
bouton
3
BUTTON4_MASK #
masque
du
bouton
4
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érent types d'évènements. Pour référence ultérieure, les types d'évènements (du module gtk.gdk) sont :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
EXPOSURE_MASK
POINTER_MOTION_MASK
POINTER_MOTION_HINT_MASK
BUTTON_MOTION_MASK
BUTTON1_MOTION_MASK
BUTTON2_MOTION_MASK
BUTTON3_MOTION_MASK
BUTTON_PRESS_MASK
BUTTON_RELEASE_MASK
KEY_PRESS_MASK
KEY_RELEASE_MASK
ENTER_NOTIFY_MASK
LEAVE_NOTIFY_MASK
FOCUS_CHANGE_MASK
STRUCTURE_MASK
PROPERTY_CHANGE_MASK
VISIBILITY_NOTIFY_MASK
PROXIMITY_IN_MASK
PROXIMITY_OUT_MASK
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
gtk.Alignment
gtk.Arrow
gtk.Bin
gtk.Box
gtk.Image
gtk.Item
gtk.Label
gtk.Layout
gtk.Pixmap
gtk.ScrolledWindow
gtk.Separator
gtk.Table
gtk.AspectFrame
gtk.Frame
gtk.VBox
gtk.HBox
gtk.VSeparator
gtk.HSeparator
Pour capturer les évènements pour ces widgets, on doit utiliser un widget EventBox. Voir la Section 10.1, « La boîte à évènement (EventBox) »La boîte à é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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
évènement type # type
window # fenêtre
send_event # évènement transmis
NOTHING
DELETE
DESTROY # pas d'attribut supplémentaire
EXPOSE area # zone
count # nombre
MOTION_NOTIFY time # temps
x
y
pressure # pression
xtilt # inclinaison en x
ytilt # inclinaison en y
state # état
is_hint # est indice
source # source
deviceid # identifiant de dispositif
x_root # racine x
y_root # racine y
BUTTON_PRESS
_2BUTTON_PRESS
_3BUTTON_PRESS
BUTTON_RELEASE time # temps
x
y
pressure # pression
xtilt # inclinaison en x
ytilt # inclinaison en y
state # état
button # bouton
source # source
deviceid # identifiant de dispositif
x_root # racine x
y_root # racine y
KEY_PRESS
KEY_RELEASE time # temps
state # état
keyval # valeur de clé
string # chaîne de caractères
ENTER_NOTIFY
LEAVE_NOTIFY subwindow # sous-fenêtre
time # temps
x
y
x_root # racine x
y_root # racine y
mode # mode
detail # détail
focus # focus
state # état
FOCUS_CHANGE _in # dans
CONFIGURE x
y
width # largeur
height # hauteur
MAP
UNMAP # pas d'attribut supplémentaire
PROPERTY_NOTIFY atom # atome
time # temps
state # état
SELECTION_CLEAR
SELECTION_REQUEST
SELECTION_NOTIFY selection # sélection
target # cible
property # propriété
requestor # demandeur
time # temps
PROXIMITY_IN
PROXIMITY_OUT time # temps
source # source
deviceid # identifiant de dispositif
DRAG_ENTER
DRAG_LEAVE
DRAG_MOTION
DRAG_STATUS
DROP_START
DROP_FINISHED context # contexte
time # temps
x_root # racine x
y_root # racine x
CLIENT_EVENT message_type # type de message
data_format # format de données
data # données
VISIBILTY_NOTIFY state # état
NO_EXPOSE # pas d'attribut supplémentaire
24-2-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 tountes les 0,05 secondes, 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
#
Signaux
utilisés
pour
gérer
le
pixmap
hors
écran
zone_dessin.connect
(
"
expose_event
"
, expose_event)
zone_dessin.connect
(
"
configure_event
"
, configure_event)
#
Signaux
d'évènements
zone_dessin.connect
(
"
motion_notify_event
"
, motion_notify_event)
zone_dessin.connect
(
"
button_press_event
"
, bouton_press_event)
zone_dessin.set_events
(
gtk.gdk.EXPOSURE_MASK
|
gtk.gdk.LEAVE_NOTIFY_MASK
|
gtk.gdk.BUTTON_PRESS_MASK
|
gtk.gdk.POINTER_MOTION_MASK
|
gtk.gdk.POINTER_MOTION_HINT_MASK)
Les gestionnaires d'évènements button_press_event() et motion_notify_event() dans scribblesimple.py sont ainsi :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
def
bouton_press_event
(
widget, event):
if
event.button =
=
1
and
pixmap !
=
None
:
brosse_dessin
(
widget, event.x, event.y)
return
True
def
motion_notify_event
(
widget, event):
if
event.is_hint:
x, y, etat =
event.window.get_pointer
(
)
else
:
x =
event.x
y =
event.y
etat =
event.state
if
etat &
gtk.gdk.BUTTON1_MASK and
pixmap !
=
None
:
brosse_dessin
(
widget, x, y)
return
True
Les gestionnaires expose_event() et configure_event() seront décrits plus tard.
24-3. 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)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
(
fenetre, largeur, hauteur, profondeur=
-
1
)
Le paramètre fenetre 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 fenetre.
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
#
Création
d'un
nouveau
pixmap
d'arrière-plan
de
la
taille
voulue
def
configure_event
(
widget, event):
global
pixmap
x, y, largeur, hauteur =
widget.get_allocation
(
)
pixmap =
gtk.gdk.Pixmap
(
widget.window, largeur, hauteur)
pixmap.draw_rectangle
(
widget.get_style
(
).white_gc,
True
, 0
, 0
, largeur, hauteur)
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) :
2.
3.
4.
5.
6.
#
Redessine
l'écran
à
partir
du
pixmap
d'arrière-plan
def
expose_event
(
widget, event):
x , y, largeur, hauteur =
event.area
widget.window.draw_drawable
(
widget.get_style
(
).fg_gc[gtk.STATE_NORMAL],
pixmap, x, y, x, y, largeur, hauteur)
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
drawable.draw_point
(
gc, x, y)
drawable.draw_line
(
gc, x1, y1, x2, y2)
drawable.draw_rectangle
(
gc, fill, x, y, width, height)
drawable.draw_arc
(
gc, fill, x, y, width, height, angle1, angle2)
drawable.draw_polygon
(
gc, fill, points)
drawable.draw_drawable
(
gc, src, xsrc, ysrc, xdest, ydest, width, height)
drawable.draw_points
(
gc, points)
drawable.draw_lines
(
gc, points)
drawable.draw_segments
(
gc, segments)
drawable.draw_rgb_image
(
gc, x, y, width, height, dither, buffer, rowstride)
drawable.draw_rgb_32_image
(
gc, x, y, width, height, dither, buffer, rowstride)
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 »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 »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 GTKChapitre 23. Les fichiers de style rc GTK. Celui-ci, entre autres choses, stocke plusieurs contextes graphiques. Quelques exemples d'accès à ces contextes graphiques :
2.
3.
4.
5.
6.
7.
widget.get_style
(
).white_gc
widget.get_style
(
).black_gc
widget.get_style
(
).fg_gc[STATE_NORMAL]
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 :
2.
3.
4.
5.
STATE_NORMAL,
STATE_ACTIVE,
STATE_PRELIGHT,
STATE_SELECTED,
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 :
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.