Licence CC BY-NC-SA

Le Drag'n Drop

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Cette notion est somme toute assez importante à l'heure actuelle : beaucoup de gens l'utilisent, ne serait-ce que pour déplacer des fichiers dans leur système d'exploitation ou encore faire des copies sur une clé USB.

Pour rappel, le Drag'n Drop - traduit par « Glisser-Déposer » - revient à sélectionner un élément graphique d'un clic gauche, à le déplacer grâce à la souris tout en maintenant le bouton enfoncé et à le déposer à l'endroit voulu en relâchant le bouton. En Java, cette notion est arrivée avec JDK 1.2, dans le système graphique awt. Nous verrons comment ceci était géré car, même si ce système est fondu et simplifié avec swing, vous devrez utiliser l'ancienne gestion de ce comportement, version awt.

Je vous propose de commencer par un exemple simple, en utilisant swing, puis ensuite de découvrir un cas plus complet en utilisant tous les rouages de ces événements, car il s'agit encore et toujours d'événements.

Présentation

La première chose à faire en swing pour activer le drag'n drop, c'est d'activer cette fonctionnalité dans les composants concernés. Voici un code de test :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Test1 extends JFrame{

  public Test1(){
    super("Test de Drag'n Drop");
    setSize(300, 200);

    JPanel pan = new JPanel();
    pan.setBackground(Color.white);
    pan.setLayout(new BorderLayout());

    //Notre textearea avec son contenu déplaçable
    JTextArea label = new JTextArea("Texte déplaçable !");
    label.setPreferredSize(new Dimension(300, 130));
    //--------------------------------------------------
    //C'est cette instruction qui permet le déplacement de son contenu
    label.setDragEnabled(true);
    //--------------------------------------------------

    pan.add(new JScrollPane(label), BorderLayout.NORTH);

    JPanel pan2 = new JPanel();
    pan2.setBackground(Color.white);
    pan2.setLayout(new BorderLayout());

    //On crée le premier textfield avec contenu déplaçable
    JTextField text = new JTextField();
    //--------------------------------------------------
    text.setDragEnabled(true);
    //--------------------------------------------------
    //Et le second, sans
    JTextField text2 = new JTextField();

    pan2.add(text2, BorderLayout.SOUTH);
    pan2.add(text, BorderLayout.NORTH);

    pan.add(pan2, BorderLayout.SOUTH);
    add(pan, BorderLayout.CENTER);

    setVisible(true);
  }

  public static void main(String[] args){
    new Test1();
  }  
}

Vous avez pu constater que le drag'n drop était vraiment très simple à activer…

Récapitulons. Nous avons une fenêtre contenant trois composants : un JTextArea avec le drag'n drop activé et deux JTextField dont seul celui du dessus a l'option activée.

La figure suivante vous montre ce que donne ce code.

Lancement du programme

La figure suivante donne le résultat après avoir sélectionné une portion de texte et l'avoir glissée dans le JTextField n° 1.

Texte cliqué-glissé

Enfin, vous trouverez à la figure suivante le résultat d'un déplacement du contenu du JTextField n° 1 vers le JTextField n° 2.

Changement de JTextField

Étant donné que ce dernier JTextField est dépourvu de l'option désirée, vous ne pouvez plus déplacer le texte.

J'ai essayé de faire la même chose avec un JLabel et ça n'a pas fonctionné !

C'est tout à fait normal. Par défaut, le drag'n drop n'est disponible que pour certains composants. D'abord, il ne faut pas confondre l'action « drag » et l'option « drop ». Certains composants autorisent les deux alors que d'autres n'autorisent que le drag. Voici un tableau récapitulatif des actions autorisées par composant :

Composant

Drag

Drop

JEditorPane

X

X

JColorChooser

X

X

JFileChooser

X

.

JTextPane

X

X

JTextField

X

X

JTextArea

X

X

JFormattedTextField

X

X

JPasswordTextField

.

X

JLabel

.

.

JTable

X

.

JTree

X

.

JList

X

.

Certains composants de ce tableau autorisent soit l'export de données, soit l'import de données, soit les deux, soit aucun des deux. Certains composants n'ont aucun comportement lorsque nous y déposons des données… Ceci est dû à leur complexité et à leurs modes de fonctionnement. Par exemple, donner un comportement par défaut à un JTree n'est pas une mince affaire. Lors d'un drop, doit-il :

  • ajouter l'élément ?
  • ajouter l'élément en supprimant celui sur lequel nous le déposons ?
  • ajouter un noeud mère ?
  • ajouter un noeud fille ?

De ce fait, le comportement est laissé aux bons soins du développeur, en l'occurrence, vous.

Par contre, il faut que vous gardiez en mémoire que lorsqu'on parle de « drag », il y a deux notions implicites à prendre en compte : le « drag déplacement » et le « drag copie ».

En fait, le drag'n drop peut avoir plusieurs effets :

  • la copie ;
  • le déplacement.

Par exemple, sous Windows, lorsque vous déplacez un fichier avec un drag'n drop dans un dossier sans changer de disque dur, ce fichier est entièrement déplacé : cela revient à faire un couper/coller. En revanche, si vous effectuez la même opération en maintenant la touche Ctrl, l'action du drag'n drop devient l'équivalent d'un copier/coller.

L'action « drag déplacement » indique donc les composants autorisant, par défaut, l'action de type couper/coller, l'action « drag copie » indique que les composants autorisent les actions de type copier/coller. La finalité, bien sûr, étant de déposer des données à l'endroit souhaité.

Gardez bien en tête que ce sont les fonctionnalités activées par défaut sur ces composants.

Tu veux dire que nous pourrions ajouter cette fonctionnalité à notre JLabel ?

Pour répondre à cette question, nous allons devoir mettre le nez dans le fonctionnement caché de cette fonctionnalité.

Fonctionnement

Comme beaucoup d'entre vous ont dû le deviner, le transfert des informations entre deux composants se fait grâce à trois composantes essentielles :

  • un composant d'origine ;
  • des données transférées ;
  • un composant cible.

Cette vision, bien qu'exacte dans la théorie, se simplifie dans la pratique, pas de panique. Pour schématiser ce que je viens de vous dire, voici un petit diagramme en figure suivante.

Fonctionnement du drag'n drop

Ce dernier est assez simple à comprendre : pendant l'opération de drag'n drop, les données transitent d'un composant à l'autre via un objet. Dans l'API Swing, le mécanisme de drag'n drop est encapsulé dans l'objet JComponent dont tous les objets graphiques héritent, ce qui signifie que tous les objets graphiques peuvent implémenter cette fonctionnalité.

Afin d'activer le drag'n drop sur un composant graphique qui ne le permet pas par défaut, nous devons utiliser la méthode setTransferHandler(TransferHandler newHandler) de l'objet JComponent. Cette méthode prend un objet TransferHandler en paramètre : c'est celui-ci qui lance le mécanisme de drag'n drop.

Les composants du tableau récapitulatif (hormis le JLabel) ont tous un objet TransferHandler par défaut. Le drag'n drop s'active par la méthode setDragEnabled(true) sur la plupart des composants, mais comme vous avez pu le constater, pas sur le JLabel… Afin de contourner cela, nous devons lui spécifier un objet TransferHandler réalisé par nos soins.

Attention, toutefois ! Vous pouvez définir un TransferHandler pour un objet possédant déjà un comportement par défaut, mais cette action supplantera le mécanisme par défaut du composant : redéfinissez donc les comportements avec prudence !

Retournons à notre JLabel. Afin de lui ajouter les fonctionnalités voulues, nous devons lui affecter un nouveau TransferHandler. Une fois que ce nouvel objet lui sera assigné, nous lui ajouterons un événement souris afin de lancer l'action de drag'n drop : je vous rappelle que l'objet TransferHandler ne permet que le transit des données, il ne gère pas les événements ! Dans notre événement, nous avons juste à récupérer le composant initiateur du drag, récupérer son objet TransferHandler et invoquer sa méthode exportAsDrag(JComponent comp, InputEvent event, int action).

Voici un code permettant de déplacer le texte d'un JLabel dans un JTextField :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class LabelContentDemo extends JFrame{

  public LabelContentDemo(){
    setTitle("Drag'n Drop avec un JLabel !");
    setSize(300, 100);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel pan = new JPanel();
    pan.setLayout(new GridLayout(2,2));
    pan.setBackground(Color.white);

    JLabel srcLib = new JLabel("Source de drag : ", JLabel.RIGHT);
    JLabel src = new JLabel("Texte à déplacer !");

    //-------------------------------------------------------------------
    //On crée le nouvel objet pour activer le drag'n drop 
    src.setTransferHandler(new TransferHandler("text"));

    //On spécifie au composant qu'il doit envoyer ses données via son objet TransferHandler
    src.addMouseListener(new MouseAdapter(){
      //On utilise cet événement pour que les actions soient visibles dès le clic de souris…
      //Nous aurions pu utiliser mouseReleased, mais, niveau IHM, nous n'aurions rien vu
      public void mousePressed(MouseEvent e){
        //On récupère le JComponent            
        JComponent lab = (JComponent)e.getSource();
        //Du composant, on récupère l'objet de transfert : le nôtre
        TransferHandler handle = lab.getTransferHandler();
        //On lui ordonne d'amorcer la procédure de drag'n drop
        handle.exportAsDrag(lab, e, TransferHandler.COPY);
      }
    });
    //-------------------------------------------------------------------

    JLabel destLib = new JLabel("Destination de drag : ", JLabel.RIGHT);
    JTextField dest = new JTextField();
    //On active le comportement par défaut de ce composant
    dest.setDragEnabled(true);

    pan.add(srcLib);
    pan.add(src);
    pan.add(destLib);
    pan.add(dest);

    setContentPane(pan);
    setVisible(true);
  }

  public static void main(String[] args){
    new LabelContentDemo();
  }   
}

Sur la figure suivante, on déplace le contenu de notre source vers le champ texte.

Avant le drag

Sur la figure suivante, on voit que le contenu est déplacé.

Texte déplacé

Enfin, sur la figure suivante, on déplace un fragment du contenu de notre champ texte vers notre JLabel.

Après le déplacement de la chaîne « déplacer » vers le JLabel

Vous devez avoir plusieurs questions. Déjà, pour ceux qui ne l'auraient pas remarqué (ou essayé), l'objet de transfert n'a pas de constructeur sans argument ! Cette instruction ne compilera pas : TransferHandler trans = new TransferHandler();. Par contre, le constructeur utilisé fonctionne parfaitement pour un JLabel TransferHandler trans = new TransferHandler("text");. Pourquoi ? Tout simplement parce que la chaîne de caractères passée en paramètre correspond à une propriété JavaBean utilisable par l'objet.

Un JavaBean est un objet Java répondant à certains critères de construction :

  • la classe doit être Serializable pour pouvoir sauvegarder et restaurer l'état des instances de cette classe ;
  • la classe doit posséder un constructeur sans arguments (constructeur par défaut) ;
  • les propriétés privées de la classe (variables d'instance) doivent être accessibles publiquement via des méthodes accesseurs (get ou set) suivies du nom de la propriété avec la première lettre transformée en majuscule ;
  • la classe doit contenir les méthodes d'interception d'événements nécessaires.

En fait, notre objet de transfert va utiliser la propriété « text » de notre objet JLabel, ceci afin de récupérer son contenu et de le faire transiter. Nous verrons plus tard comment faire pour les cas où nous ne connaissons pas le nom de la propriété…

Ensuite, nous avons récupéré l'objet TransferHandler depuis notre composant : nous le lui avions affecté avec un setter, nous pouvons le récupérer avec un getter.

Là où les choses deviennent intéressantes, c'est lorsque nous invoquons la méthode handle.exportAsDrag(lab, e, TransferHandler.COPY);. C'est cette instruction qui amorce réellement le drag'n drop. Les trois paramètres servent à initialiser les actions à effectuer et à déterminer quand et sur qui les faire :

  • le premier paramètre indique le composant qui contient les données à déplacer ;
  • le second paramètre indique à notre objet l'événement sur lequel il doit déclencher le transfert ;
  • le dernier indique l'action qui doit être effectuée : copie, déplacement, rien…

Comme je vous l'avais dit, il existe plusieurs types d'actions qui peuvent être effectuées lors du drop, celles-ci sont paramétrables via l'objet TransferHandle :

  • TransferHandler.COPY : n'autorise que la copie des données vers le composant cible ;
  • TransferHandler.MOVE : n'autorise que le déplacement des données vers le composant cible ;
  • TransferHandler.LINK : n'autorise que l'action lien sur les données du composant cible ; cela revient à créer un raccourci ;
  • TransferHandler.COPY_OR_MOVE : autorise la copie ou le déplacement ;
  • TransferHandler.NONE : n'autorise rien.

Attention, l'objet TransferHandler n'accepte que les actions COPY lorsqu'il est instancié avec le paramètre « text » : si vous modifiez la valeur ici, votre drag'n drop ne fonctionnera plus.

Alors, même si nous avons réussi à faire un JLabel avec l'option drag'n drop, celui-ci sera restreint ?

Non, mais si nous sommes parvenus à créer un nouveau TranferHandler, pour arriver à débrider notre composant, nous allons devoir encore approfondir…

Créer son propre TransferHandler

Afin de personnaliser le drag'n drop pour notre composant, nous allons devoir mettre les mains dans le cambouis. La classe TransferHandler fait pas mal de choses dans votre dos et, tout comme les modèles de composants (cf. JTree, JTable), dès lors que vous y mettez les mains, tout sera à votre charge !

Voici une représentation simplifiée de la classe en question en figure suivante.

La classe TransferHandler

Nous y retrouvons nos types de transferts, la méthode exportAsDrag(…) et tout plein de nouveautés… C'est aussi dans cette classe que se trouvent les méthodes pour la gestion du copier/coller traditionnel.

Le but est maintenant de déplacer les données du JLabel vers notre zone de texte façon « couper/coller ». Vous vous en doutez, nous allons devoir redéfinir le comportement de certaines des méthodes de notre objet de transfert. Ne vous inquiétez pas, nous allons y aller en douceur. Voici la liste des méthodes que nous allons utiliser pour arriver à faire ce que nous cherchons :

 1
 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
import javax.swing.TransferHandler;

public class MyTransferHandler extends TransferHandler{

  /**
  * Méthode permettant à l'objet de savoir si les données reçues
  * via un drop sont autorisées à être importées
  * @param info
  * @return boolean
  */
  public boolean canImport(TransferHandler.TransferSupport info) {}

  /**
  * C'est ici que l'insertion des données dans notre composant est réalisée
  * @param support
  * @return boolean
  */
  public boolean importData(TransferHandler.TransferSupport support){}

  /**
  * Cette méthode est invoquée à la fin de l'action DROP
  * Si des actions sont à faire ensuite, c'est ici qu'il faudra coder le comportement désiré
  * @param c
  * @param t
  * @param action
  */
  protected void exportDone(JComponent c, Transferable t, int action){}

  /**
  * Dans cette méthode, nous allons créer l'objet utilisé par le système de drag'n drop
  * afin de faire circuler les données entre les composants
  * Vous pouvez voir qu'il s'agit d'un objet de type Transferable
  * @param c
  * @return
  */
  protected Transferable createTransferable(JComponent c) {}

  /**
  * Cette méthode est utilisée afin de déterminer le comportement 
  * du composant vis-à-vis du drag'n drop : nous retrouverons
  * nos variables statiques COPY, MOVE, COPY_OR_MOVE, LINK ou NONE 
  * @param c
  * @return int
  */
  public int getSourceActions(JComponent c) {}

}

Commençons par définir le comportement souhaité pour notre composant : le déplacement. Cela se fait via la méthode public int getSourceActions(JComponent c). Nous allons utiliser les variables statiques de la classe mère pour définir l'action autorisée :

1
2
3
4
public int getSourceActions(JComponent c) {
  //Nous n'autorisons donc que le déplacement ici
  return MOVE;
}

Maintenant, assurons-nous qu'il sera toujours possible d'importer des données d'un autre composant en les déposant dessus. Pour cela, nous allons redéfinir les méthodes d'import de données public boolean canImport(TransferHandler.TransferSupport info) et public boolean importData(TransferHandler.TransferSupport support). Remarquez ce paramètre bizarre : TransferHandler.TransferSupport.

Rappelez-vous les classes internes : la classe TransferSupport est à l'intérieur de la classe TransferHandler. Cet objet a un rôle très important : la communication entre les composants. C'est lui qui véhicule l'objet encapsulant nos données. C'est aussi lui, pour des composants plus complexes tels qu'un tableau, un arbre ou une liste, qui fournit l'emplacement où a eu lieu l'action drop.

Voici ce que vont contenir nos méthodes :

1
2
3
4
5
6
7
public boolean canImport(TransferHandler.TransferSupport info) {
  //Nous contrôlons si les données reçues sont d'un type autorisé, ici String       
  if (!info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
    return false;
  }
  return true;
}

L'objet TransferSupport nous offre une méthode permettant de contrôler le type de données supportées par notre drag'n drop. Une liste de « type MIME » (signifie Multipurpose Internet Mail Extensions. C'est une façon de typer certains fichiers comme les images, les PDF, etc.) est disponible dans l'objet DataFlavor. Ici, nous avons utilisé DataFlavor.stringFlavor, qui signifie « chaîne de caractères », comme vous avez pu le deviner. Voici la liste des types d'éléments disponibles via l'objet DataFlavor :

  • DataFlavor.javaSerializedObjectMimeType : autorise un objet Java sérialisé correspondant au type MIME « application/x-java-serialized-object » ;
  • DataFlavor.imageFlavor : autorise une image, soit la classe java.awt.Image correspondant au type MIME « image/x-java-image » ;
  • DataFlavor.javaFileListFlavor : autorise un objet java.util.List contenant des objets java.io.File ;
  • DataFlavor.javaJVMLocalObjectMimeType : autorise n'importe quel objet Java ;
  • DataFlavor.javaRemoteObjectMimeType : autorise un objet distant utilisant l'interface Remote ;
  • DataFlavor.stringFlavor : autorise soit une chaîne de caractères, soit la classe java.lang.String correspondant au type MIME « application/x-java-serialized-object ».

La seconde étape de notre démarche consiste à autoriser l'import de données vers notre composant grâce à la méthode public boolean importData(TransferHandler.TransferSupport support) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public boolean importData(TransferHandler.TransferSupport support){
  //Nous contrôlons si les données reçues sont d'un type autorisé
  if(!canImport(support))
    return false;

  //On récupère notre objet Transferable, celui qui contient les données en transit
  Transferable data = support.getTransferable();
  String str = "";

  try {
    //Nous récupérons nos données en spécifiant ce que nous attendons       
    str = (String)data.getTransferData(DataFlavor.stringFlavor);
  } catch (UnsupportedFlavorException e){
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }

  //Via le TRansferSupport, nous pouvons récupérer notre composant
  JLabel lab = (JLabel)support.getComponent();
  //Afin de lui affecter sa nouvelle valeur
  lab.setText(str);

  return true;
}

Voilà : à ce stade, nous avons redéfini la copie du champ de texte vers notre JLabel. Voici notre objet en l'état :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class LabelContentDemo extends JFrame{

  public LabelContentDemo(){
    setTitle("Drag'n Drop avec un JLabel !");
    setSize(300, 100);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel pan = new JPanel();
    pan.setLayout(new GridLayout(2,2));
    pan.setBackground(Color.white);

    JLabel srcLib = new JLabel("Source de drag : ", JLabel.RIGHT);
    JLabel src = new JLabel("Texte à déplacer !");

    //--------------------------------------------------------
    //On utilise notre nouvel objet MyTransferHandle 
    src.setTransferHandler(new MyTransferHandler());      

    src.addMouseListener(new MouseAdapter(){

      public void mousePressed(MouseEvent e){
        System.out.println("EVENT !");
        JComponent lab = (JComponent)e.getSource();
        TransferHandler handle = lab.getTransferHandler();
        handle.exportAsDrag(lab, e, TransferHandler.COPY);
      }
    });
    //--------------------------------------------------------

    JLabel destLib = new JLabel("Destination de drag : ", JLabel.RIGHT);
    JTextField dest = new JTextField();

    dest.setDragEnabled(true);

    pan.add(srcLib);
    pan.add(src);
    pan.add(destLib);
    pan.add(dest);

    setContentPane(pan);
    setVisible(true);
  }

  public static void main(String[] args){
    new LabelContentDemo();
  }   
}

Et maintenant, le plus dur : effacer le contenu de notre objet une fois la copie des données effectuée.

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class MyTransferHandler extends TransferHandler{

  public boolean canImport(TransferHandler.TransferSupport info) {
    if (!info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
      return false;
    }
    return true;
  }

  public boolean importData(TransferHandler.TransferSupport support){
    if(!canImport(support))
      return false;

    Transferable data = support.getTransferable();
    String str = "";

    try {
      str = (String)data.getTransferData(DataFlavor.stringFlavor);
    } catch (UnsupportedFlavorException e){
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }

    JLabel lab = (JLabel)support.getComponent();
    lab.setText(str);

    return false;
  }

  protected void exportDone(JComponent c, Transferable t, int action){
    //Une fois le drop effectué nous effaçons le contenu de notre JLabel
    if(action == MOVE)
      ((JLabel)c).setText("");
  }

  protected Transferable createTransferable(JComponent c) {
    //On retourne un nouvel objet implémentant l'interface Transferable
    //StringSelection implémente cette interface,  nous l'utilisons donc
    return new StringSelection(((JLabel)c).getText());
  }

  public int getSourceActions(JComponent c) {
    return MOVE;
  }   
}

Vous pouvez tester à nouveau votre code, cette fois le rendu est conforme à nos attentes. Vous venez de recréer la fonction drag'n drop pour un composant. Bravo !

Activer le drop sur un JTree

Vous vous doutez de la marche à suivre : cependant, comme je vous l'avais dit au début de ce chapitre, vous allez être confrontés au problème du positionnement du drop sur votre composant. Cependant, votre boîte à outils dispose d'un nouvel objet dont le rôle est d'informer sur la position du drop : l'objet TransferSupport.

Avant de poursuivre dans cette voie, rappelez-vous qu'il faut définir l'action que doit effectuer notre composant lors du dépôt de nos données. C'est possible grâce à l'objet DropMode que nous pouvons utiliser via la méthode setDropMode(DropMode dropMode). Voici la liste des modes disponibles :

  • USE_SELECTION
  • ON
  • INSERT
  • ON_OR_INSERT
  • INSERT_COLS
  • INSERT_ROWS
  • ON_OR_INSERT_COLS
  • ON_OR_INSERT_ROWS

Vous l'aurez compris : certains modes sont utilisables par des tableaux et d'autres non… Afin que vous puissiez vous faire votre propre idée sur le sujet, je vous invite à les essayer dans l'exemple qui va suivre. C'est grâce à cela que nous allons spécifier le mode de fonctionnement de notre arbre.

Maintenant que nous savons comment spécifier le mode de fonctionnement, il ne nous reste plus qu'à trouver comment, et surtout où insérer le nouvel élément. C'est là que notre ami le TransfertSupport entre en jeu. Cet objet permet de récupérer un objet DropLocation contenant toutes les informations nécessaires au bon positionnement des données dans le composant cible.

En fait, par l'objet TransfertSupport, vous pourrez déduire un objet DropLocation propre à votre composant, par exemple :

1
2
3
4
5
6
//Pour récupérer les infos importantes sur un JTree
JTree.DropLocation dl = (JTree.DropLocation)myTransfertSupport.getDropLocation();
//Pour récupérer les infos importantes sur un JTable
JTable.DropLocation dl = (JTable.DropLocation)myTransfertSupport.getDropLocation();
//Pour récupérer les infos importantes sur un JList
JList.DropLocation dl = (JList.DropLocation)myTransfertSupport.getDropLocation();

L'avantage de ces spécifications, c'est qu'elles permettent d'avoir accès à des informations fort utiles :

JList.DropLocation

JTree.DropLocation

JTable.DropLocation

isInsert getIndex

getChildIndex getPath

isInsertRow isInsertColumn getRow getColumn

Maintenant que je vous ai présenté la marche à suivre et les objets à utiliser, je vous propose un exemple qui, je pense, parle de lui-même et est assez commenté pour que vous puissiez vous y retrouver. Voici les classes utilisées.

MyTransferHandler.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class MyTransferHandler extends TransferHandler{

  public boolean canImport(TransferHandler.TransferSupport info) {
    if (!info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
      return false;
    }
    return true;
  }

  public boolean importData(TransferHandler.TransferSupport support){
    if(!canImport(support))
      return false;

    Transferable data = support.getTransferable();
    String str = "";

    try {
      str = (String)data.getTransferData(DataFlavor.stringFlavor);
    } catch (UnsupportedFlavorException e){
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }

    JLabel lab = (JLabel)support.getComponent();
    lab.setText(str);

    return false;
  }

  protected void exportDone(JComponent c, Transferable t, int action){
    if(action == MOVE){
      JLabel lab = (JLabel)c; 
      String text = lab.getText();
      int indice = Integer.parseInt(text.substring(text.length()-1, text.length()));
      lab.setText(text.substring(0, text.length()-1) + (++indice));
    }
  }

  protected Transferable createTransferable(JComponent c) {
    return new StringSelection(((JLabel)c).getText());
  }

  public int getSourceActions(JComponent c) {
    return MOVE;
  }   
}

TreeTransferHandler.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class TreeTransferHandler extends TransferHandler{

  JTree tree;
  public TreeTransferHandler(JTree tree){
    this.tree = tree;
  }

  public boolean canImport(TransferHandler.TransferSupport info) {
    if (!info.isDataFlavorSupported(DataFlavor.stringFlavor))
      return false;

    return true;
  }

  public boolean importData(TransferHandler.TransferSupport support){
    if(!canImport(support))
      return false;

    //On récupère l'endroit du drop via un objet approprié
    JTree.DropLocation dl = (JTree.DropLocation)support.getDropLocation();
    //Les informations afin de pouvoir créer un nouvel élément
    TreePath path = dl.getPath();
    int index = dl.getChildIndex();

    //Comme pour le JLabel, on récupère les données
    Transferable data = support.getTransferable();
    String str = "";

    try {
      str = (String)data.getTransferData(DataFlavor.stringFlavor);
    } catch (UnsupportedFlavorException e){
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }   

    //On peut maintenant ajouter le nœud
    DefaultMutableTreeNode nouveau = new DefaultMutableTreeNode(str);
    //On déduit le nœud parent via le chemin
    DefaultMutableTreeNode parent = (DefaultMutableTreeNode)path.getLastPathComponent();

    DefaultTreeModel model = (DefaultTreeModel)this.tree.getModel();
    index = (index == -1) ? model.getChildCount(path.getLastPathComponent()) : index ;
    model.insertNodeInto(nouveau, parent, index);

    tree.makeVisible(path.pathByAddingChild(nouveau));
    tree.scrollPathToVisible(path);

    return true;
  }

  public int getSourceActions(JComponent c) {
    return COPY_OR_MOVE;
  }   
}

TreeDragDemo.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class TreeDragDemo extends JFrame{
  JTree tree;
  public TreeDragDemo(){
    setTitle("Drag'n Drop avec un JLabel !");
    setSize(400, 200);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel pan = new JPanel();
    pan.setLayout(new GridLayout(1, 1));
    pan.setBackground(Color.white);

    JLabel srcLib = new JLabel("Source de drag : ", JLabel.RIGHT);
    JLabel src = new JLabel("Noeud 1");

    //------------------------------------------------------
    //On utilise notre nouvel objet MyTransferHandle 
    src.setTransferHandler(new MyTransferHandler());      
    src.addMouseListener(new MouseAdapter(){

      public void mousePressed(MouseEvent e){
        JComponent lab = (JComponent)e.getSource();
        TransferHandler handle = lab.getTransferHandler();
        handle.exportAsDrag(lab, e, TransferHandler.MOVE);
      }
    });
    //------------------------------------------------------

    JLabel destLib = new JLabel("Destination de drag : ", JLabel.RIGHT);
    JTextField dest = new JTextField();

    dest.setDragEnabled(true);
    tree = new JTree(getModel());
    tree.setDragEnabled(true);
    tree.setTransferHandler(new TreeTransferHandler(tree));

    pan.add(src);

    pan.add(new JScrollPane(tree));

    //Pour le choix des actions
    JComboBox combo = new JComboBox();
    combo.addItem("USE_SELECTION");
    combo.addItem("ON");
    combo.addItem("INSERT");
    combo.addItem("ON_OR_INSERT");

    combo.addItemListener(new ItemListener(){

      public void itemStateChanged(ItemEvent event) {
        String value = event.getItem().toString();

        if(value.equals("USE_SELECTION"))
          tree.setDropMode(DropMode.USE_SELECTION);

        if(value.equals("ON"))
          tree.setDropMode(DropMode.ON);            

        if(value.equals("INSERT"))
          tree.setDropMode(DropMode.INSERT);

        if(value.equals("ON_OR_INSERT"))
          tree.setDropMode(DropMode.ON_OR_INSERT);

      }         
    });

    add(pan, BorderLayout.CENTER);
    add(combo, BorderLayout.SOUTH);
    setVisible(true);
  }

  private TreeModel getModel(){

    DefaultMutableTreeNode root = new DefaultMutableTreeNode("SDZ");

    DefaultMutableTreeNode forum = new DefaultMutableTreeNode("Forum");
    forum.add(new DefaultMutableTreeNode("C++"));
    forum.add(new DefaultMutableTreeNode("Java"));
    forum.add(new DefaultMutableTreeNode("PHP"));

    DefaultMutableTreeNode tuto = new DefaultMutableTreeNode("Tutoriel");
    tuto.add(new DefaultMutableTreeNode("Tutoriel"));
    tuto.add(new DefaultMutableTreeNode("Programmation"));
    tuto.add(new DefaultMutableTreeNode("Mapping"));

    root.add(tuto);
    root.add(forum);

    return new DefaultTreeModel(root);
  }

  public static void main(String[] args){
    new TreeDragDemo();
  }   
}

La figure suivante vous montre ce que j'ai obtenu après quelques manipulations.

Ajout de nœud via drag'n drop

Effet de déplacement

À la lecture de tous ces chapitres, vous devriez être à même de comprendre et d'assimiler le fonctionnement du code qui suit. Son objectif est de simuler le déplacement de vos composants sur votre IHM, un peu comme sur les trois figures suivantes.

Déplacement d'un bouton sur un autre composant

Après avoir relâché le bouton sur un autre composant

Déplacement d'un JLabel

En fait, le principe revient à définir un GlassPane à votre fenêtre, composant personnalisé que nous avons fait hériter de JPanel. C'est lui qui va se charger de dessiner les images des composants sur sa surface, dont nous aurons au préalable défini la transparence. Sur chaque composant, nous allons devoir définir les actions à effectuer à chaque événement souris : deux classes sont codées à cet effet… Ensuite, il ne reste plus qu'à faire notre test.

Voilà les codes sources promis.

MyGlassPane.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class MyGlassPane extends JPanel{

  //L'image qui sera dessinée
  private BufferedImage img;
  //Les coordonnées de l'image
  private Point location;
  //La transparence de notre glace
  private Composite transparence;

  public MyGlassPane(){
    //Afin de ne peindre que ce qui nous intéresse
    setOpaque(false);
    //On définit la transparence
    transparence = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.55f);
  }   

  public void setLocation(Point location){
    this.location = location;        
  }

  public void setImage(BufferedImage image){
    img = image;
  }

  public void paintComponent(Graphics g){
    //Si on n'a pas d'image à dessiner, on ne fait rien…
    if(img == null)
      return;

    //Dans le cas contraire, on dessine l'image souhaitée
    Graphics2D g2d = (Graphics2D)g;
    g2d.setComposite(transparence);
    g2d.drawImage(img, (int) (location.getX() - (img.getWidth(this)  / 2)), (int) (location.getY() - (img.getHeight(this) / 2)), null);
  }   
}

MouseGlassListener.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class MouseGlassListener extends MouseAdapter{

  private MyGlassPane myGlass;
  private BufferedImage image;

  public MouseGlassListener(MyGlassPane glass){
    myGlass = glass;
  }

  public void mousePressed(MouseEvent event) {
    //On récupère le composant pour en déduire sa position
    Component composant = event.getComponent();
    Point location = (Point)event.getPoint().clone();

    //Les méthodes ci-dessous permettent, dans l'ordre, 
    //de convertir un point en coordonnées d'écran
    //et de reconvertir ce point en coordonnées fenêtres
    SwingUtilities.convertPointToScreen(location, composant);
    SwingUtilities.convertPointFromScreen(location, myGlass);

    //Les instructions ci-dessous permettent de redessiner le composant
    image = new BufferedImage(composant.getWidth(), composant.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics g = image.getGraphics();
    composant.paint(g);

    //On passe les données qui vont bien à notre GlassPane
    myGlass.setLocation(location);
    myGlass.setImage(image);

    //On n'oublie pas de dire à notre GlassPane de s'afficher
    myGlass.setVisible(true);
  }

  public void mouseReleased(MouseEvent event) {
    //---------------------------------------------------------------------
    //On implémente le transfert lorsqu'on relâche le bouton de souris
    //Ceci afin de ne pas supplanter le fonctionnement du déplacement
    JComponent lab = (JComponent)event.getSource();
    TransferHandler handle = lab.getTransferHandler();
    handle.exportAsDrag(lab, event, TransferHandler.COPY);
    //---------------------------------------------------------------------

    //On récupère le composant pour en déduire sa position
    Component composant = event.getComponent();
    Point location = (Point)event.getPoint().clone();
    //Les méthodes ci-dessous permettent, dans l'ordre, 
    //de convertir un point en coordonnées d'écran
    //et de reconvertir ce point en coordonnées fenêtre
    SwingUtilities.convertPointToScreen(location, composant);
    SwingUtilities.convertPointFromScreen(location, myGlass);

    //On passe les données qui vont bien à notre GlassPane
    myGlass.setLocation(location);
    myGlass.setImage(null);
    //On n'oublie pas de ne plus l'afficher
    myGlass.setVisible(false);

  }
}

MouseGlassMotionListener.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//CTRL + SHIFT + O pour générer les imports
public class MouseGlassMotionListener extends MouseAdapter{

  private MyGlassPane myGlass;

  public MouseGlassMotionListener(MyGlassPane glass){
    myGlass = glass;
  }

  /**
  * Méthode fonctionnant sur le même principe que la classe précédente
  * mais cette fois sur l'action de déplacement
  */
  public void mouseDragged(MouseEvent event) {
    //Vous connaissez maintenant…
    Component c = event.getComponent();

    Point p = (Point) event.getPoint().clone();
    SwingUtilities.convertPointToScreen(p, c);
    SwingUtilities.convertPointFromScreen(p, myGlass);
    myGlass.setLocation(p);
    myGlass.repaint();
  }
}

Fenetre.java

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame{

  private MyGlassPane glass = new MyGlassPane();

  public Fenetre(){
    super("Test de GlassPane");
    setSize(400, 200);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel pan = new JPanel();
    JPanel pan2 = new JPanel();

    //On crée un composant
    JButton bouton1 = new JButton("Bouton N°1");
    //On y attache les écouteurs qui auront pour rôle
    //d'initialiser notre glace et d'y affecter les données
    //qui permettront de simuler le déplacement
    bouton1.addMouseListener(new MouseGlassListener(glass));
    bouton1.addMouseMotionListener(new MouseGlassMotionListener(glass));
    //On affecte maintenant un TranferHandler spécifique
    //initialisé avec la propriété JavaBean "text" 
    bouton1.setTransferHandler(new TransferHandler("text"));

    JButton bouton2 = new JButton("Bouton N°2");
    bouton2.addMouseListener(new MouseGlassListener(glass));
    bouton2.addMouseMotionListener(new MouseGlassMotionListener(glass));
    bouton2.setTransferHandler(new TransferHandler("text"));

    JLabel text = new JLabel("Deuxième texte statique");
    text.addMouseListener(new MouseGlassListener(glass));
    text.addMouseMotionListener(new MouseGlassMotionListener(glass));
    text.setTransferHandler(new TransferHandler("text"));

    JLabel label = new JLabel("Texte statique !");
    label.addMouseListener(new MouseGlassListener(glass));
    label.addMouseMotionListener(new MouseGlassMotionListener(glass));
    label.setTransferHandler(new TransferHandler("text"));

    pan.add(bouton1);
    pan.add(label);
    add(pan, BorderLayout.NORTH);

    pan2.add(text);
    pan2.add(bouton2);
    add(pan2, BorderLayout.SOUTH);

    setGlassPane(glass);
    setLocationRelativeTo(null);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    setVisible(true);
  }

  public static void main(String[] args){
    new Fenetre();
  }   
}

Pour des composants comme les JTree, JTable ou autres, vous aurez certainement à faire des modifications pour que ça fonctionne !

Et voilà : j'espère que ça vous a plu ! Vous devriez désormais aborder le drag'n drop avec plus de sérénité. Il vous reste encore des choses à explorer, mais rien qui devrait vous bloquer : vous n'êtes plus des Zéros !


  • Le drag'n drop n'est disponible via la méthode setDragEnabled(true); que pour certains composants.
  • Plusieurs comportements sont possibles pour les déplacements de données : la copie ou le déplacement.
  • Le drag'n drop permet de récupérer des données d'un composant source pour les transmettre à un composant cible, le tout via un objet : l'objet TransferHandler.
  • Vous pouvez activer le drag'n drop sur un composant en utilisant la méthode setTransferHandler(TransferHandler newHandler) héritée de JComponent.
  • La procédure de drag'n drop est réellement lancée lors de l'appel à la méthode handle.exportAsDrag(lab, e, TransferHandler.COPY);, qui permet de déterminer qui lance l'action, sur quel événement, ainsi que l'action qui doit être effectuée.
  • Afin d'avoir le contrôle du mécanisme de drag'n drop, vous pouvez réaliser votre propre TransferHandler.
  • Ce dernier dispose d'une classe interne permettant de gérer la communication entre les composants (l'objet TransferHandler.TransferSupport) et permet aussi de s'assurer que les données reçues sont bien du type attendu.