Le Drag and Drop pour tous

Le tutoriel sun sur le drag and drop est plutôt pratique pour les objets prédéfinis. Par contre, il est singulièrement elliptique pour les cas plus complexes.

Nous expliquons ici comment proposer du drag and drop pour un type non standard et un objet graphique non standard

Drag and Drop d'objets de classe Personne

Le type que nous allons exporter par drag and drop est la classe Personne. Une personne est simplement définie par un nom et un prenom. Cependant, nous verrons que la complexité de la classe à exporter n'a que très peu d'importance.

Nous créerons un objet graphique swing capable d'afficher des personnes et de les exporter ou importer par drag and drop.

Les classes utilisées

  1. Personne : représente nos données. C'est une classe toute bête. Le seul point important, pour que le drag and drop fonctionne entre applications java, c'est qu'elle implémente serializable.
  2. PersonnePanel : une classe qui affiche les données d'une personne (son nom et son prénom, en fait). PersonnePanel est dotée des méthodes setPersonne() et getPersonne().
  3. PersonneTransfertHandler : un objet de cette classe est associé aux PersonnePanel. C'est le gestionnaire du drag and drop (tant comme source que comme destination).
  4. PersonneTransferable : quand un objet de classe Personne sera exporté par drag and drop, on le mettra dans un PersonneTransferable. Cet objet servira à communiquer avec les cibles potentielles du drag and drop. En effet, le "drop" peut être effectué sur un objet qui "sait" gérer des Personnes (en clair : un PersonnePanel), mais aussi par exemple dans un éditeur de texte (etc.). Le PersonneTransferable négocie avec la cible pour savoir quels formats elle comprend, et tente de fournir les données correspondantes.

La classe PersonneTransferable

Le but de cette classe est de proposer un objet de classe Personne sous différents formats. En pratique, nous proposerons deux formats : la classe personne elle même, et le format chaîne de caractères. Un nouveau transferable est créé à chaque drag and drop.

Les formats sont identifiés par des DataFlavor. Un DataFlavor est juste un objet java qui représente un type MIME. Ce n'est qu'un identifiant, par lui-même il ne sait rien faire.

Pour information : les types MIME permettent à des logiciels (comme les lectueurs de mail, les navigateurs web...) de reconnaître les types des fichiers. Par exemple, un fichier html sera identifié comme : "text/html" ; un fichier image pourra être décrit comme "image/png", et un fichier pdf comme "application/pdf".

La classe DataFlavor elle même contient des constantes pour les types les plus courants, comme DataFlavor.stringFlavor. Pour identifier notre type Personne, nous utiliserons un DataFlavor défini ainsi :

String MIME= DataFlavor.javaJVMLocalObjectMimeType +
    ";class=" + Personne.class.getName();
    
DataFlavor personneFlavor = new DataFlavor(MIME);
Voici intégralement le texte de notre classe :
public class PersonneTransferable implements Transferable {

    /**
     * La personne à transférer.
     */
    private Personne personne;
    
    /**
     * @param p
     */
    public PersonneTransferable(Personne p) {
       personne= p;
    }


    /**
     * La liste des flavor supportées, pour que les cibles potentielles sachent si elles 
     * peuvent recevoir la donnée.
     *
     *

Les flavor sont données par ordre de préférence. */ public DataFlavor[] getTransferDataFlavors() { DataFlavor[] result= new DataFlavor[2]; // D'abord, la PersonneFlavor (on l'a stockée comme constante dans PersonneFlavorFactory, // qui ne sert qu'à ça. result[0]= PersonneFlavorFactory.getPersonneFlavor(); // Ensuite, le texte bête. result[1]= DataFlavor.stringFlavor; return result; } /** * Dit si nous acceptons de retourner une flavor particulière. */ public boolean isDataFlavorSupported(DataFlavor flavor) { return PersonneFlavorFactory.getPersonneFlavor().equals(flavor) || DataFlavor.stringFlavor.equals(flavor); } /** * La récupération des données proprement dite. *

Renvoie un objet selon la flavor demandée (Personne ou String). */ public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (PersonneFlavorFactory.getPersonneFlavor().equals(flavor)) return personne; else if (DataFlavor.stringFlavor.equals(flavor)) { return personne.getNom().toUpperCase()+ " " + personne.getPrenom(); } else throw new UnsupportedFlavorException(flavor); } }

Le drag

Pour que notre PersonnePanel sache faire du drag, il faut deux choses : Le premier point se traite en créant une classe étendant TransferHandler, et en appelant setTransferHandler . Le second point demande d'écrire un mouseListener qui appellera la méthode exportAsDrag du TransferHandler.

Écriture du TransferHandler

Deux méthodes suffisent pour implémenter le drag : createTransferable et getSourceActions
public class PersonneTransferHandler extends TransferHandler {

  /**
   * Crée un objet transférable adapté, donc un PersonneTransferable.
   * Reçoit comme paramètre la source du drag and drop
   * (ce qui permet de partager des PersonneTransferHandler)
   */

  protected Transferable createTransferable(JComponent c) {        
        PersonnePanel panel= (PersonnePanel) c;
        Personne p= panel.getPersonne();
        return new PersonneTransferable(p);
  }

  /**
   * Permet de dire si le drag est possible. Dans notre cas, il l'est toujours.
   * cette méthode peut renvoyer COPY, NONE, MOVE, COPY_OR_MOVE.
   * Si NONE est renvoyé, le drag and drop sera impossible.
   */   
  public int getSourceActions(JComponent c) {
        return COPY;
  }
}
Notez que la classe TransferHandler a une méthode getVisualPresentation. Cette dernière n'est jamais appelée.

Attachement du TransferHandler

Il s'effectue dans le constructeur de PersonnePanel :
 setTransferHandler(new PersonneTransferHandler());

Exemple de listener pour le drag

Le "drag" démarre à la suite d'un mouvement de souris, traité par un listener. Pour nous, celui-ci sera très simple : il suffit de presser la souris pour démarrer. L'important est l'appel à exportAsDrag.
public class DragAndDropListener extends MouseAdapter {
  
    public void mousePressed(MouseEvent e) {
        PersonnePanel panel= (PersonnePanel) e.getSource();
        panel.getTransferHandler().exportAsDrag(panel, e,
                TransferHandler.COPY);
        e.consume(); // empêche que l'événement soit transmis au container du PersonnePanel.
    }
}
Et c'est tout pour le drag ! Avec ce code là, une personne peut déjà être recopiée dans openoffice par drag and drop. Elle apparaîtra comme du texte.

Le drop

L'appel du drop est automatique. Il n'y a pas besoin de prévoir de MouseListener pour ça.

Par contre, lors du drop, la cible va devoir dire à l'objet transferable qui lui est présenté quels types de données elle peut comprendre. Tout cela se fait dans le TransferHandler, qui est doté des méthodes supplémentaires :

    /**
     * Teste si les données proposée comportent une PersonneFlavor.
     * On pourrait aussi accepter des données de mode texte, par exemple.
     */
    public boolean canImport(JComponent c, DataFlavor[] flavors) {

        for (int i = 0; i < flavors.length; i++) {
            if (PersonneFlavorFactory.getPersonneFlavor().equals(flavors[i])) {
                return true;
            }
        }
        return false;
    }

    /**
     * L'importation de données proprement dite.
     * 
     * @param c : la cible du transfert.
     * @param t : données à transférer
     */

    public boolean importData(JComponent c, Transferable t) {
        if (canImport(c,PersonneFlavorFactory.getPersonneFlavor())) {
            try {
	    	// On extrait l'objet Personne du transferable 
                Personne p= (Personne) 
			t.getTransferData(PersonneFlavorFactory.getPersonneFlavor());
		// On le range dans la cible.
                ((PersonnePanel)c).setPersonne(p);	     
                return true;
            } catch (UnsupportedFlavorException ufe) {
                ufe.printStackTrace();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
        return false;
    }

Le logiciel complet est téléchargeable ici (archive ici)

Drag and Drop partiels

Si l'on veut effectuer un drag and drop sur des parties d'un composant java (par exemple les noeuds d'un graphe dans un éditeur), il y a plusieurs solutions.

La première est que les éléments graphiques qui réalisent l'affichage soient des composants swing. Pour cela, il faudra écrire un layoutManager adapté.

La seconde est de jouer sur les classes de java.awt.dnd. Pour la partie "drag", les classes de swing sont déjà utilisables : en effet, au moment du drag, on sait où se trouve la souris. Il suffit de créer le Transferable en fonction des besoins (ce qui est éventuellement faisable en surchargeant exportAsDrag dans TransferHandler. Pour le drop, par contre, la version fournie par défaut est globale. Il faudra donc passer par les bibliothèques de bas niveau (ou alors avoir un système en deux temps : sélection de la zone cible, puis drag and drop. C'est ce que font les éditeurs de texte, par exemple : on "drop" à l'emplacement du curseur.

Pour les fonctionalités drag and drop de bas niveau, regarder http://www.javaworld.com/javaworld/jw-03-1999/jw-03-dragndrop_p.html, http://java.sun.com/products/jfc/tsc/articles/dragndrop/#adding_d_n_d et http://www.javaworld.com/javatips/jw-javatip97_p.html

(Documentation à venir.) Un exemple : la classe TargetedDragAndDrop.

Et les images ?

Pour l'instant, le drag and drop des images fonctionne plus ou moins selon les plates-formes, et uniquement avec des images bitmap.
Serge ROSMORDUC