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 :
2.
3.
4.
5.
gtk.TARGET_SAME_APP # La destination peut être sélectionnée pour un glisser à
# l'intérieur de la même application
gtk.TARGET_SAME_WIDGET # La destination peut être sélectionnée pour un glisser à
# 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 :
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.
2.
3.
widget.drag_dest_set
(
flags, targets, actions)
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 :
2.
3.
4.
5.
6.
gtk.gdk.ACTION_DEFAULT
gtk.gdk.ACTION_COPY
gtk.gdk.ACTION_MOVE
gtk.gdk.ACTION_LINK
gtk.gdk.ACTION_PRIVATE
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.
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.
#!/usr/local/env python
import
pygtk
pygtk.require
(
'2.0'
)
import
gtk
def
motion_cb
(
wid, context, x, y, time):
context.drag_status
(
gtk.gdk.ACTION_COPY, time)
return
True
def
drop_cb
(
wid, context, x, y, time):
l.set_text
(
'
\n
'
.join
(
[str(
t) for
t in
context.targets]))
context.finish
(
True
, False
, time)
return
True
w =
gtk.Window
(
)
w.set_size_request
(
200
, 150
)
w.drag_dest_set
(
0
, [], 0
)
w.connect
(
'drag_motion'
, motion_cb)
w.connect
(
'drag_drop'
, drop_cb)
w.connect
(
'destroy'
, lambda
w: gtk.main_quit
(
))
l =
gtk.Label
(
)
w.add
(
l)
w.show_all
(
)
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.
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.
Voici le code de dragndrop.py :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# exemple dragndrop.py
import
pygtk
pygtk.require
(
'2.0'
)
import
gtk
import
string, time
import
gtkxpm
class
ExempleDragNDrop:
HEIGHT =
600
WIDTH =
600
TARGET_TYPE_TEXT =
80
TARGET_TYPE_PIXMAP =
81
fromImage =
[ (
"text/plain"
, 0
, TARGET_TYPE_TEXT ),
(
"image/x-xpixmap"
, 0
, TARGET_TYPE_PIXMAP ) ]
toButton =
[ (
"text/plain"
, 0
, TARGET_TYPE_TEXT ) ]
toCanvas =
[ (
"image/x-xpixmap"
, 0
, TARGET_TYPE_PIXMAP ) ]
def
layout_resize
(
self, widget, event):
x, y, width, height =
widget.get_allocation
(
)
if
width >
self.lwidth or
height >
self.lheight:
self.lwidth =
max(
width, self.lwidth)
self.lheight =
max(
height, self.lheight)
widget.set_size
(
self.lwidth, self.lheight)
def
makeLayout
(
self):
self.lwidth =
self.WIDTH
self.lheight =
self.HEIGHT
box =
gtk.VBox
(
False
,0
)
box.show
(
)
table =
gtk.Table
(
2
, 2
, False
)
table.show
(
)
box.pack_start
(
table, True
, True
, 0
)
layout =
gtk.Layout
(
)
self.layout =
layout
layout.set_size
(
self.lwidth, self.lheight)
layout.connect
(
"size-allocate"
, self.layout_resize)
layout.show
(
)
table.attach
(
layout, 0
, 1
, 0
, 1
, gtk.FILL|
gtk.EXPAND,
gtk.FILL|
gtk.EXPAND, 0
, 0
)
# créer les barres de défilement et les placer dans une table
vScrollbar =
gtk.VScrollbar
(
None
)
vScrollbar.show
(
)
table.attach
(
vScrollbar, 1
, 2
, 0
, 1
, gtk.FILL|
gtk.SHRINK,
gtk.FILL|
gtk.SHRINK, 0
, 0
)
hScrollbar =
gtk.HScrollbar
(
None
)
hScrollbar.show
(
)
table.attach
(
hScrollbar, 0
, 1
, 1
, 2
, gtk.FILL|
gtk.SHRINK,
gtk.FILL|
gtk.SHRINK,
0
, 0
)
# utiliser les ajustements du layout par les barres de défilement
vAdjust =
layout.get_vadjustment
(
)
vScrollbar.set_adjustment
(
vAdjust)
hAdjust =
layout.get_hadjustment
(
)
hScrollbar.set_adjustment
(
hAdjust)
layout.connect
(
"drag_data_received"
, self.receiveCallback)
layout.drag_dest_set
(
gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
gtk.DEST_DEFAULT_DROP,
self.toCanvas, gtk.gdk.ACTION_COPY)
self.addImage
(
gtkxpm.gtk_xpm, 0
, 0
)
button =
gtk.Button
(
"Text Target"
)
button.show
(
)
button.connect
(
"drag_data_received"
, self.receiveCallback)
button.drag_dest_set
(
gtk.DEST_DEFAULT_MOTION |
gtk.DEST_DEFAULT_HIGHLIGHT |
gtk.DEST_DEFAULT_DROP,
self.toButton, gtk.gdk.ACTION_COPY)
box.pack_start
(
button, False
, False
, 0
)
return
box
def
addImage
(
self, xpm, xd, yd):
hadj =
self.layout.get_hadjustment
(
)
vadj =
self.layout.get_vadjustment
(
)
style =
self.window.get_style
(
)
pixmap, mask =
gtk.gdk.pixmap_create_from_xpm_d
(
self.window.window, style.bg[gtk.STATE_NORMAL], xpm)
image =
gtk.Image
(
)
image.set_from_pixmap
(
pixmap, mask)
button =
gtk.Button
(
)
button.add
(
image)
button.connect
(
"drag_data_get"
, self.sendCallback)
button.drag_source_set
(
gtk.gdk.BUTTON1_MASK, self.fromImage,
gtk.gdk.ACTION_COPY)
button.show_all
(
)
# s'adapter au défilement du layout - la localisation de l'événement
# est relative à la zone visible et non à la taille du layout
self.layout.put
(
button, int(
xd+
hadj.value), int(
yd+
vadj.value))
return
def
sendCallback
(
self, widget, context, selection, targetType, eventTime):
if
targetType ==
self.TARGET_TYPE_TEXT:
now =
time.time
(
)
str =
time.ctime
(
now)
selection.set(
selection.target, 8
, str)
elif
targetType ==
self.TARGET_TYPE_PIXMAP:
selection.set(
selection.target, 8
,
string.join
(
gtkxpm.gtk_xpm, '
\n
'
))
def
receiveCallback
(
self, widget, context, x, y, selection, targetType,
time):
if
targetType ==
self.TARGET_TYPE_TEXT:
label =
widget.get_children
(
)[0
]
label.set_text
(
selection.data)
elif
targetType ==
self.TARGET_TYPE_PIXMAP:
self.addImage
(
string.split
(
selection.data, '
\n
'
), x, y)
def
__init__
(
self):
self.window =
gtk.Window
(
gtk.WINDOW_TOPLEVEL)
self.window.set_default_size
(
300
, 300
)
self.window.connect
(
"destroy"
, lambda
w: gtk.main_quit
(
))
self.window.show
(
)
layout =
self.makeLayout
(
)
self.window.add
(
layout)
def
main
(
):
gtk.main
(
)
if
__name__
==
"__main__"
:
ExempleDragNDrop
(
)
main
(
)