Lorsque vous utilisez le copier-coller pour transmettre des informations entre deux programmes, comme nous l'avons découvert dans l'étude précédente, le Presse-papiers sert d'intermédiaire. La méthaphore déplacer et lâcher supprime cet intermédiaire et permet à deux programmes de communiquer directement.
La plate-forme Java fournit un support simple du déplacer-lâcher, entre des applications Java et des applications natives. Cette étude vous montre comment écrire une application Java qui sert de cible (pour le "lâcher"), avec une autre application servant de source (pour le "déplacer").
Pour revenir sur le fonctionnement du Presse-papiers, je vous invite à revoir l'étude correspondante.
§
Avant d'étudier plus en détail le support de la plate-forme Java pour le déplacer et lâcher, examinons rapidement son interface utilisateur. Nous utiliserons comme exemples l'explorateur de Windows et le programme WordPad.
Sur une autre plate-forme, vous pouvez faire les mêmes expériences sur des programmes natifs disposant des capacités de déplacer-lâcher.
§
Une opération "déplacer" est initiée par un mouvement à l'intérieur d'une source de déplacement. Traditionnellement, cela revient à sélectionner un ou plusieurs éléments, puis à les éloigner de leur position d'origine.
Les autres plates-formes se servent éventuellement d'autres combinaisons de touches pour ces opérations.
§
Par conséquent, il existe trois types d'actions, correspondant à trois mouvements :
Le but d'une création de raccourci est d'établir une référence vers l'élément sélectionné. Ces liens ont typiquement besoin d'un support du système d'exploitation hôte (comme des liens symboliques pour des fichiers, ou des liens objets pour les composants d'un document). De plus, ces liens ne sont généralement pas très intéressants pour des programmes travaillant sur plusieurs plates-formes. Pour cette étude, nous nous concentrerons sur l'utilisation du déplacer-lâcher pour copier et déplacer des données.
Tous les programmes ne permettent pas ces types d'actions. Par exemple, si vous amenez un fichier sur WordPad, ce dernier ignore le type d'action et se contente d'ouvrir le fichier.
Il existe généralement un indice visuel indiquant le type d'opération en cours. Au minimum, le curseur de la souris changera. Lorsque le curseur passe au-dessus de cibles éventuelles, sa forme indique si l'opération de lâcher est posible ou non. Si un lâcher est possible, la forme du curseur indique également le type d'action :
Il est également possible de déplacer des éléments en dehors des icônes de fichiers. Par exemple, vous pouvez sélectionner du texte dans WordPad et le déplacer. Essayer de déplacer des fragments de texte dans des cibles acceptant ces fragments et observez leur réaction.
Cette expérience montre un inconvénient du déplacer-lâcher comme mécanisme d'une interface utilisateur. Il peut être difficile de deviner quels éléments vous pouvez déplacer, les endroits sur lesquels vous pouvez les lâcher et ce qui se passe lorsque vous le faites. Comme l'action par défaut "déplacer" peut supprimer l'original, il est normal que la plupart des utilisateurs soient méfiants quant aux expériences de déplacer-lâcher.
Dans ce chapitre, nous allons construire une application Java simple qui sera la cible d'un lâcher. Le programme d'exemple ne fait rien de très élaboré, il démontre simplement comment détecter qu'un utilisateur veut initier une action de lâcher, comment accepter cette action et comment analyser les données lâchées.
N'importe quel composant AWT ou Swing peut être la cible d'un lâcher.
§
Pour cela, vous devez construire un objet java.awt.dnd.DropTarget et passer au constructeur le composant ainsi qu'un écouteur de cible de lâcher. Le constructeur enregistre alors l'objet avec le système de déplacer-lâcher :
DropTarget cible = new DropTarget(composant, écouteur);
Vous pouvez activer ou désactiver la cible avec la méthode setActive() de la classe DropTarget. Par défaut, la cible est active :
cible. setActive(true);
Vous pouvez appeler la méthode setDefaultActions() de la classe DropTarget pour définir les opérations de lâcher que vous souhaitez accepter par défaut. Les opérations sont les suivantes :
Par défaut, toutes les opérations sont autorisées.
§
Vous devez maintenant implémenter un écouteur de cible de lâcher. L'interface DropTargetListener possède cinq méthodes :
Notez que cette méthode est appelée, que vous ayez indiqué précédemment ou non que vous acceptiez le lâcher.
§
Examinez attentivement les paramètres des méthodes de l'interface DropTargetListener. Trois d'entre eux sont un DropTargetDragEvent. Cependant, la méthode drop() reçoit un DropTargetDroptEvent et la méthode dragExit() reçoit un DropTargetEvent.
Vous avez besoin de ces méthodes pour déterminer si vous pouvez autoriser un déplacement ou permettre un lâcher et pour fournir un indice graphique.
Malheureusement, comme ces méthodes ne sont pas disponibles dans la superclasse commune, vous devrez implémenter la logique de test deux fois, une première fois pour le déplacement et une seconde pour le lâcher.
événement.acceptDrop(DndConstants.ACTION_MOVE);
événement.acceptDrop(DndConstants.ACTION_COPY);
Par ailleurs, si vous souhaitez réellement effectuer une distinction entre une copie et un déplacement, appelez simplement :événement.acceptDrop(événement. getDropEvent());
Voici un résumé d'un écouteur de cible de lâcher typique :
class EcouteurLâcher implements DropTargetListener { // méthodes pratiques public boolean isDragAcceptable(DropTargetDragEvent événement) { // examine l'action de lâcher et les types de données disponibles } public boolean isDropAcceptable(DropTargetDropEvent événement) { // effectue le même test que dans isDragAcceptable() } // méthodes spécifiques de l'écouteur public void dragEnter(DropTargetDragEvent événement) { if (!isDragAcceptable(événement)) { événement.rejectDrag(); return; } } public void dragOver(DropTargetDragEvent événement) { // les indices visuels peuvent être ajoutés ici } public void dropActionChanged(DropTargetDragEvent événement) { if (!isDragAcceptable(événement)) { événement.rejectDrag(); return; } } public void dragExit(DropTargetEvent événement) { } public void drop(DropTargetDropEvent événement) { if (!isDropAcceptable(événement)) { événement.rejectDrop(); return; } événement.acceptDrop(actionRéelle); // traite les données de la source de déplacement événement.dropComplete(true); } }
Une fois que vous avez accepté un lâcher, vous devez analyser les données de la source de déplacement. La méthode getTransferable() de la classe DropTargetDropEvent renvoie une référence vers un objet Transferable. C'est exactement la même interface que celle que nous avons utilisée pour le copier-coller.
Pour revenir sur le fonctionnement du Presse-papiers, je vous invite à revoir l'étude correspondante.
§
DataFlavor[] types = transférable.getTransferDataFlavors(); DataFlavor type = types[i]; if (type.equals(DataFlavor.javaFileListFlavor)) { java.util.List listeFichiers = (java.util.List)transférable.getTransferData(type); Iterator itérateur = listeFichiers.iterator(); while (itérateur.hasNext()) { File fichier = (File)itérateur.next(); // utilisation de fichier } }
DataFlavor[] types = transférable.getTransferDataFlavors(); DataFlavor type = types[i]; if (type.isMimeTypeEqual("text/html") && type.getRepresentationClass()==InputStream.class) { String charset = type.getParameter("charset"); InputStream flux = new InputStreamReader(transférable.getTransferData(type, charset); // lire les données du flux }
Notre programme d'exemple ne fait rien de très élaboré. Il se contente de vous permettre de lâcher des éléments dans une zone de texte. Lorsque vous commencez à déplacer du texte au-dessus de la zone de texte, l'action de lâcher est affichée. Une fois que vous avez initié un lâcher, les données lâchées sont affichées. Si les données sont dans un format de texte, le programme lit à la fois les encodages ascii et unicode.
Dans notre programme d'exemple, nous ne fournissons aucun indice visuel, à part un curseur d'avertissement qui apparaît automatiquement lorsque vous appelez la méthode rejectDrag(). Par ailleurs, la zone de texte est implémentée par la classe JTextArea qui affiche automatiquement une icône spécifique au lâcher si les données sont acceptées, sans avoir à écrire une seule ligne de code particulière.
Le but de ce programme est simplement de vous fournir une cible de lâcher pour vos expérimentations. Essayez de lâcher un ensemble de fichiers texte ou un fragment de texte à partir d'un autre éditeur. Observez aussi comment une tentative de création de lien est rejetée. Si vous relâchez les touches "Maj" et "Ctrl", l'icône d'avertissement apparaît lorsque vous déplacez un élément vers la zone de texte.
A noter que la classe JTexteArea est déjà capable de faire un lâcher de texte issue d'un autre éditeur sans avoir rien à écrire de spécial. Par contre, le lâcher d'un fichier texte au-dessus de la zone d'édition n'est pas pris en compte.
package déplacer; import java.awt.datatransfer.*; import java.awt.dnd.*; import java.io.*; import java.util.*; import javax.swing.*; public class Lâcher extends JFrame { private LâcherTexte éditeur = new LâcherTexte(); public Lâcher() { super("Lâcher"); add(new JScrollPane(éditeur)); new DropTarget(éditeur, éditeur); setSize(400, 300); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new Lâcher(); } private class LâcherTexte extends JTextArea implements DropTargetListener { public boolean isDragAcceptable(DropTargetDragEvent événement) { return (événement.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) != 0; } public boolean isDropAcceptable(DropTargetDropEvent événement) { return (événement.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) != 0; } public void dragEnter(DropTargetDragEvent événement) { if (!isDragAcceptable(événement)) événement.rejectDrag(); } public void dragOver(DropTargetDragEvent événement) { // les indices visuels peuvent être ajoutés ici } public void dropActionChanged(DropTargetDragEvent événement) { if (!isDragAcceptable(événement)) événement.rejectDrag(); } public void dragExit(DropTargetEvent événement) { } public void drop(DropTargetDropEvent événement) { if (!isDropAcceptable(événement)) { événement.rejectDrop(); return; } événement.acceptDrop(DnDConstants.ACTION_COPY); Transferable transférable = événement.getTransferable(); DataFlavor[] types = transférable.getTransferDataFlavors(); for (DataFlavor type : types) { try { if (type.equals(DataFlavor.javaFileListFlavor)) { List listeFichiers = (List) transférable.getTransferData(type); Iterator itérateur = listeFichiers.iterator(); while (itérateur.hasNext()) { File fichier = (File) itérateur.next(); read(new FileReader(fichier), null); } } else if (type.equals(DataFlavor.stringFlavor)) { String chaîne = (String) transférable.getTransferData(type); append(chaîne+ ); } } catch (Exception ex) { setTitle("Coller impossible"); } } événement.dropComplete(true); } } }
Un deuxième exemple permet de placer un fichier image sur la zone centrale de la fenêtre. Cette zone centrale est représentée par un JLabel qui est capable de gérer une icône, et par là une image.
package déplacer; import java.awt.datatransfer.*; import java.awt.dnd.*; import java.io.*; import java.util.*; import javax.swing.*; public class Lâcher extends JFrame { private LâcherImage photo = new LâcherImage(); public Lâcher() { super("Lâcher des photos"); add(new JScrollPane(photo)); new DropTarget(photo, photo); setSize(400, 300); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new Lâcher(); } private class LâcherImage extends JLabel implements DropTargetListener { public void dragEnter(DropTargetDragEvent événement) { if (!événement.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) événement.rejectDrag(); } public void dropActionChanged(DropTargetDragEvent événement) { if (!événement.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) événement.rejectDrag(); } public void dragExit(DropTargetEvent événement) { } public void dragOver(DropTargetDragEvent événement) { } public void drop(DropTargetDropEvent événement) { if (!événement.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { événement.rejectDrop(); return; } événement.acceptDrop(DnDConstants.ACTION_COPY); Transferable transférable = événement.getTransferable(); DataFlavor[] types = transférable.getTransferDataFlavors(); DataFlavor type = types[0]; try { if (type.equals(DataFlavor.javaFileListFlavor)) { List listeFichiers = (List) transférable.getTransferData(type); File fichier = (File) listeFichiers.get(0); setIcon(new ImageIcon(fichier.getPath())); } } catch (Exception ex) { setTitle("Récupération photo impossible"); } événement.dropComplete(true); } } }
Maintenant que nous savons comment implémenter un programme contenant une cible de lâcher, nous allons découvrir comment implémenter une source de déplacement.
DragSource source = DragSource.getDefaultDragSource(); source.createDefaultDragGestureReconizer(composant, DnDConstants.ACTION_COPY_OR_MOVE, écouteurGesture);
L'interface DragGestureListener possède une seule méthode, dragGestureReconized(). L'identificateur de geste appelle cette méthode dès qu'il a remarqué que l'utilisateur souhaite initier une opération de déplacement.
événement.startDrag(null, tranférable, écouteurSource);
Vous devez effectuer les opérations habituelles de définition d'une enveloppe Transferable.
§
L'écouteur de la source du déplacement est averti à plusieurs reprises au cours de l'opération de déplacement. L'interface DragSourceListener possède cinq méthodes :
public void dragDropEnd(DragSourceDropEvent événement) { if (événement.getDropSuccess()) { int action = événement.getDropAction(); if (action == DndConstanst.ACTION_MOVE) ... } }
Attention : L'action par défaut est un déplacement. Si vous déplacez, par exemple, un fichier d'un composant de liste et que vous le lachiez dans l'explorateur, ce dernier déplace le fichier dans le répertoire cible et n'existe plus dans la source.
Java introduit une classe d'aide intitulé DragSourceAdapter qui implémente toutes les méthodes de l'interface DragSourceListener comme des méthodes inactives.
§
Je vous propose de reprendre l'ossature de l'application précédente. Cette fois-ci, il est possible de récupérer l'image qui se trouve sur la partie centrale de la fenêtre afin qu'elle puisse être utilisée par un autre programme capable de prendre en compte une image par glisser-déposer.
package déplacer; import java.awt.Cursor; import java.awt.datatransfer.*; import java.awt.dnd.*; import java.io.*; import javax.swing.*; public class Déplacer extends JFrame implements DragGestureListener { private PanneauImage photo = new PanneauImage(new ImageIcon("C:/Photos/Papillon.jpg")); public Déplacer() { super("Lâcher des photos"); add(new JScrollPane(photo)); DragSource source = DragSource.getDefaultDragSource(); source.createDefaultDragGestureRecognizer(photo, DnDConstants.ACTION_COPY, this); setSize(400, 300); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new Déplacer(); } public void dragGestureRecognized(DragGestureEvent événement) { événement.startDrag(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR), photo); } private class PanneauImage extends JLabel implements Transferable { public PanneauImage(Icon image) { super(image); } @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] { DataFlavor.imageFlavor }; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return flavor.equals(DataFlavor.imageFlavor); } @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (flavor.equals(DataFlavor.imageFlavor)) { ImageIcon icône = (ImageIcon) getIcon(); return icône.getImage(); } else { throw new UnsupportedFlavorException(flavor); } } } }
Les composants Swing possèdent un support intégré pour le transfert de données qui permet de libérer les programmeurs d'une grande partie de la tâche consistant à implémenter le copier-coller ou le déplacer-lâcher.
Le tableau ci-dessous résume les composants Swing qui sont sources et cibles du transfert de données. Les raccourcis clavier standards pour "Couper", "Copier" et "Coller" sont activés pour tous les composants à l'exception de JColorChooser. Le déplacement n'est pas activé par défaut. Vous devez appeler la méthode setDragEnabled() pour l'activer.
Composant | Source de transfert | Cible de transfert |
---|---|---|
JFileChooser | Exporte la liste des fichiers | Non admis. |
JColorChooser | Exporte la référence locale à l'objet Color | Accepte n'importe quel objet Color |
JTextField JFormattedTextField |
Exporte le texte sélectionné | Accepte le texte |
JPasswordField | Non admis (pour des raisons de sécurité) | Accepte le texte |
JTextArea JTextPane JEditorPane |
Exporte le texte sélectionné | Accepte le texte et les listes de fichiers. Le texte est inséré. Les fichiers sont ouverts. |
JList | Exporte la description HTML d'une sélection | Non admis. |
void setName(Type t)
Vous la transformez ensuite en cible de lâcher pour les types de données avec une classe de représentation Type en appelant simplement la méthode :
composant.setTransferHandler("nom"); // "nom" représente le nom de la propriété du composant que l'on désire utiliser
Au moment du lâcher, le gestionnaire vérifie alors si l'un des types de données possède une classe de représentation Type. Si c'est le cas, il appelle la méthode setName().
void setBackground(Color couleur)
lorsqu'un objet Color est déplacé sur le champ de texte. Appelez simplement :champTexte.setTransferHandler(new TransferHandler("background"));
Attention, en installant ce gestionnaire de transfert dans le champ de texte, vous désactivez le gestionnaire de transfert standard. Vous ne pouvez plus couper, copier, déplacer ou lâcher du texte dans le champ de texte. Un composant Swing peut uniquement disposer d'un gestionnaire de transfert.
L'exemple suivant montre ce comportement. Comme nous pouvons le voir, le haut du bloc contient un sélecteur de couleur, alors qu'un champ de texte se trouve en bas du bloc contenant le message "Faites glisser la couleur ici". Vous devez faire glisser la couleur depuis l'intérieur du panneau "Aperçu", et non à partir des nuances des couleurs. Lorsque vous la faites glisser dans le champ de texte, son fond change de couleur.
package déplacer; import java.awt.*; import javax.swing.*; public class Lâcher extends JFrame { private JTextField saisie = new JTextField("Faites glisser la couleur ici"); private JColorChooser choix = new JColorChooser(); public Lâcher() { super("Lâcher de couleur"); choix.setDragEnabled(true); add(choix); saisie.setDragEnabled(true); saisie.setTransferHandler(new TransferHandler("background")); add(saisie, BorderLayout.SOUTH); pack(); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } public static void main(String[] args) { new Lâcher(); } }