Vous êtes à peu près ici : Accueil  »   tutoriel PyGTK  »   PyGTK : sommaire

14.9. Glisser-déposer dans un TreeView

14.9.1. Reclasser avec un glisser-déposer

Reclasser les lignes d'un TreeView (et les lignes sous-jacentes d'un tree model) 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.

14.9.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-a-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 :

  treeview.enable_model_drag_dest([('text/plain', 0, 0)],
                  gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
  treeview.connect("drag-data-received", rappel_donnees_du__deposer)
  ...
  ...
  def rappel_donnees_du__deposer(treeview, contexte, x, y, selection, info, dateur):
      depot_info = treeview.get_dest_row_at_pos(x, y)
      if depot_info:
          modele = treeview.get_model()
          chemin, position = depot_info
          donnees = selection.data
          # faire quelquechose avec les donnees et le modele
          ...
      return
  ...

Si une ligne est utilisé 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 :

  ...
  treestore = gtk.TreeStore(str, str)
  ...
  treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
                  [('text/plain', 0, 0)],
                  gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
  treeview.connect("drag-data-get", rappel_donnees_du_glisser)
  ...
  ...
  def rappel_donnees_du_glisser(treeview, contexte, selection, info, dateur):
      treeselection = treeview.get_selection()
      modele, iter = treeselection.get_selected()
      texte = modele.get_value(iter, 1)
      selection.set('text/plain', 8, texte)
      return
  ...

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

14.9.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
    2   
    3   # exemple treeviewdnd.py
    4   
    5   import pygtk
    6   pygtk.require('2.0')
    7   import gtk
    8   
    9   class TreeViewDnDExemple:
   10   
   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 fenetre et quitter
   18       def ferme_event(self, widget, event, donnees=None):
   19           gtk.main_quit()
   20           return False
   21   
   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
   28   
   29       def __init__(self):
   30           # Creer une nouvelle fenetre
   31           self.fenetre = gtk.Window(gtk.WINDOW_TOPLEVEL)
   32   
   33           self.fenetre.set_title("Cache URL")
   34   
   35           self.fenetre.set_size_request(250, 200)
   36   
   37           self.fenetre.connect("delete_event", self.ferme_event)
   38   
   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)
   48   
   49           # pour modele, creer une liste avec une colonne contenant une chaine
   50           self.modeleliste = gtk.ListStore(str)
   51   
   52           # creer la vue arborescente utilisant le modeleliste
   53           self.treeview = gtk.TreeView(self.modeleliste)
   54   
   55           # creer un CellRenderer pour preparer les donnees
   56           self.cell = gtk.CellRendererText()
   57   
   58           # creer unTreeViewColumn pour afficher les donnees
   59           self.colonneTV = gtk.TreeViewColumn('URL', self.cell, text=0)
   60   
   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)
   67   
   68           # autoriser le tri pour la colonne
   69           self.colonneTV.set_sort_column_id(0)
   70   
   71           # Autoriser le glisser-deposer y compris interne a 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)
   78   
   79           self.treeview.connect("drag_data_get", self.donnees_du_glisser)
   80           self.treeview.connect("drag_data_received",
   81                                 self.donnees_du_deposer)
   82   
   83           self.fen_deroule.add(self.treeview)
   84           self.fenetre.add(self.vboite)
   85           self.fenetre.show_all()
   86   
   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)
   93   
   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
  112   
  113   def main():
  114       gtk.main()
  115   
  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

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