MokaByte Numero 23  -  Ottobre 98

  di 
Marco Molinari

 

Il drag & drop
in Java
 

 

 
Come utilizzare questo utilissimo strumento in Java per mezzo delle nuove api introdotte con il JDK 1.2



Uno degli elementi che danno più intuitività a un'interfaccia grafica è il drag&drop, la possibilità di prendere e trascinare oggetti grafici in giro per lo schermo ottenendo risultati diversi a seconda di dove li si lascia cadere. Uno degli utilizzi più comuni di questo meccanismo è la possibilità di copiare o spostare dei file trascinandoli da una finestra all'altra, ma molti altri software lo utilizzano: molti editor di testo, ad esempio, permettono di spostare un brano evidenziandolo e trascinandolo in un'altra locazione; i programmi di grafica si appoggiano al drag&drop per manipolare le immagini.


Il Drag & Drop

Questo articolo descrive l’API drag&drop (d’ora in avanti d&d) introdotta nel jdk 1.2; essa permetterà di utilizzare il d&d per scambiare dati tra applicazioni Java nella stessa Virtual Machine, tra applicazioni Java in Virtual Machine diverse e tra le applicazioni Java e la piattaforma nativa. L’API dovrebbe essere diventata abbastanza stabile da poterla descrivere senza il pericolo di vedere grossi stravolgimenti, come quello che è avvenuto tra la beta 3 e la beta 4 del jdk 1.2. Vedremo come funziona il meccanismo, come è descritto nella API, le sue limitazioni intrinseche e il suo utilizzo in pratica. Infine vedremo lo stato attuale delle cose nel jdk 1.2 beta 4.

Cosa è il Drag&Drop

Durante una azione di d&d si possono distinguere tre elementi:

    Una sorgente, associata a un qualche elemento grafico e capace di fornire dei dati

    Una destinazione, anch’essa associata a un elemento grafico, capace di ricevere dei dati da una sorgente

    Un utente umano che esegue il gesto di prendere, trascinare e lasciare dalla sorgente alla destinazione

Sotto quello che si vede, il meccanismo funziona perchè c’è qualcosa che sta a guardare le azioni che l’utente fa con una specifica device di input (tipicamente il mouse) e fa in modo che vengano generati eventi per tutto quello che può accadere.

Gli eventi sono vari, più di quanti uno può immaginarsi all’inizio. Si pensi a quando si trascina un file da una finestra all’altra: quando si inizia a trascinare l’icona il puntatore "si porta dietro" l’icona e cambia di forma (sotto Windows diventa un simbolo di divieto, perchè non ha senso spostare un file nella propria directory); mentre si passa sopra un’altra finestra il puntatore può assumere forme diverse a seconda dell’esito che avrebbe l’operazione (spostamento o copia): inoltre se si passa sopra all’icona di una directory questa si evidenzia (per indicare che il file verrebbe copiato lì dentro); c’è anche una funzione di autoscroll, che fa in modo che avvicinandosi al bordo la finestra scrolli in quella direzione. Infine una volta che si lascia il tasto del mouse avviene una certa azione: in generale si può dire che avviene uno spostamento, una copia o un collegamento. Mentre le prime due sono chiare, l’azione collegamento non è univocamente definita ma dipende dalla piattaforma e dalla applicazione; un esempio è quando si crea un collegamento a un file o a un URL.

java.awt.dnd.DragSource

Si noti come il d&d non sia inserito nelle Swing, anche se è considerato parte delle JFC. Questa distinzione è ovvia se si considera che le Swing sono 100% Pure Java mentre per forza di cose il d&d deve basarsi su una certa quantità di codice nativo.

Le sorgenti di trascinamento sono forse l’elemento meno intuitivo dell’API. Gli elementi fondamentali sono:

    un Component, che l’utente trascinerà;

    un oggetto che implementi Transferable, ovvero, come vedremo dopo, capace di fornire i dati in un certo formato a una destinazione;

    un oggetto che implementi DragGestureListener, che sarà il primo ad "accorgersi" dell’inizio di un’azione di d&d;

    un DragSource, che si occupa di iniziare il d&d;

    un DragGestureRecognizer, che traccia e identifica ogni gesto di trascinamento compiuto dall’utente.

Volendo si può anche avere un oggetto che implementi DragSourceListener, che reagirà a quello che sta accadendo all’oggetto trascinato, generalmente facendo cambiare forma al puntatore.

Innanzitutto bisogna ottenere un oggetto DragSource; questo oggetto si può ottenere in vari modi ma di base ogni vm ne ha uno per tutta la durata della propria vita. 

DragSource ds = DragSource.getDefaultDragSource();

Ottenuto questo, il Component lega a sè stesso a un DragGestureRecognizer; questa è una classe astratta che incapsula ogni tipo di dipendenza che c’è tra i componenti, le piattaforme e le periferiche di ingresso. Un DragGestureRecognizer standard può essere ottenuto da DragSource, da ToolKit o da altre parti. Sui PC il DragGestureRecognizer standard si basa sul mouse ed è una implementazione concreta della MouseDragGestureRecognizer, ma non è detto che su altre piattaforme sia così. Al DragGestureRecognizer si legano anche le azioni che si possono compiere sul componente (copy, move, link) e un DragGestureListener; quest’ultimo riceverà dal DragGestureRecognizer un DragGestureEvent quando l’utente inizierà un’azione di drag. 

ds.createDefaultDragGestureRecognizer(sourceComponent, actions, dragGestureListener);

DragGestureListener è una interfaccia che definisce un solo metodo, dragGestureRecognized(DragGestureEvent dge), che solitamente non farà altro che richiamare il metodo startDrag. Con startDrag il DragSource (il gestore unico delle operazioni) inizia a controllare le azioni dell’utente; DragSource si preoccupa che in un dato istante ci sia una sola azione di drag&drop. Con startDrag si dovrebbe specificare soprattutto l’oggetto Transferable; si può anche legare un DragSourceListener, che viene avvisato di quello che sta avvenendo (entrata, uscita, passaggio in un target e altro) e dovrebbe fornire effetti visivi di trascinamento all’utente, per esempio cambiando la forma del cursore a seconda che sia sopra una destinazione valida o no. Inoltre si può specificare al DragSource un’immagine per l’oggetto del trascinamento e altre cose ancora meno fondamentali.

Riassumiamo in punti quello che di solito accade in una sorgente di drag:

    • esiste un DragSource (generalmente quello standard della vm)
    • un DragGestureRecognizer (generalmente quello legato al mouse) viene legato a un Component e a un DragGestureListener
    • l’utente inizia una azione
    • il DragGestureRecognizer avvisa di questo il DragGestureListener
    • Il DragGestureListener fa partire il DragSource che inizia a tracciare e interpretare i gesti dell’utente
    • DragSource inizializza il drag e fa partire gli eventi corretti agli eventuali DragSourceListener che agiscono di conseguenza
A questo punto il drag è partito, e le destinazioni potranno ricavare i dati dal Transferable.

java.awt.dnd.DropTarget

Come ci si può aspettare una destinazione è completamente a sè stante rispetto a una sorgente: una destinazione non sa da chi arriva il dato, lo riceve e basta; se vuole può sapere se arriva dall’interno della vm o dall’esterno. Gli elementi fondamentali per creare una destinazione sono:

    • un Component, su cui l’utente trascina il Component della sorgente;
    • un oggetto che implementi DropTargetListener, che sarà quello che reagirà ai vari eventi, tra cui il drop vero e proprio;
    • un DropTarget, che lega i due generando gli eventi per il listener
Per associare un Component a un DropTarget e a un DropTargetListener ci sono vari modi, ad esempio chiamare in fase di inizializzazione un metodo nuovo di Component: setDropTarget. Fatto questo, è sufficiente implementare DropTargetListener nell’oggetto scelto per reagire agli eventi di drop. Questa interfaccia prevede 4 metodi: dragEnter, dragExit, dragOver e drop più un quinto metodo, dropActionChanged, che viene chiamato quando si passa da uno stato all’altro (ad esempio da enter a exit). Di questi, il metodo fondamentale è drop, che viene chiamato quando avviene il rilascio vero e proprio e dovrebbe occuparsi del trasferimento dei dati.Una tipica implementazione osserverà le azioni e i tipi di dati forniti dalla sorgente per determinare se ci può essere un trasferimento o no e comunicare questo al contesto del d&d (metodi acceptDrop e rejectDrop).

Transferable

Risolto questo c’è lo scambio vero e proprio di dati. La chiave di tutto è l’interfaccia Transferable di java.awt.Datatransfer. Gli oggetti che la implementano sono in grado di fornire una lista di formati supportati (i Data Flavor) e anche di fornire il dato stesso in un certo formato richiesto sotto forma di generico Object. Quest’ultima funzionalità è fornita dal metodo getTransferData. 

Il trasferimento di dati tra una applicazione Java e l’ambiente nativo è più complesso. Internamente Java usa dei tipi MIME incapsulati dentro un DataFlavor per rapprensentare i suoi tipi, mentre le varie piattaforme usano rappresentazioni diverse. Per questo c’è bisogno di un oggetto, la FlavorMap (e la sua implementazione standard SystemFlavorMap) per mappare i tipi nativi in tipi MIME interni alla VM e viceversa.

Quando si tenta uno scambio esso può avvenire se e solo se i partecipanti sono d’accordo sul tipo di dato e sulla sua codifica. Quindi in pratica i formati "nativi" della piattaforma vanno implementati a mano, in quanto l’API fornisce solo una inputstream. Fortunatamente ci sarà un meccanismo dedicato che permetterà di ottenere una lista di file dall’ambiente nativo come List (contenitore astratto del jdk 1.2) di File. 

Un po’ di codice

Come esempio riporto una versione modificata dell’esempio della prima Question of the Week sul sito di Sun (vedi bibliografia). Si apre una semplice finestra con un TextField e una Label, e si può trascinare il testo del primo nel secondo. In questo caso il TextField implementa tutte le interfaccie che girano attorno alle sorgenti (DragGestureListener, DragSourceListener e Transferable) e la Label implementa DropTargetListener. Le modifiche interessanti che ho fatto sono due: il TextField, che anche originariamente implementava DragSourceListener, viene legato al DragSource e fornisce informazioni sull’oggetto trascinato (stranamente nell’esempio originale questo non avveniva); la Label è stata modificata in modo da correggere un problema con le sorgenti dalla piattaforma nativa.

 

import java.awt.*;

import java.awt.event.*;

import java.awt.datatransfer.*;

import java.awt.dnd.*;

import java.io.*;

import java.util.mime.*;

/*

la classe di origine
*/

class DragText extends TextField 

implements Transferable, DragGestureListener, DragSourceListener {

DragText(String s) {
super(s);

DragSource ds = DragSource.getDefaultDragSource();

ds.createDefaultDragGestureRecognizer(this,DnDConstants.ACTION_COPY, this);

}
/*
l'unico metodo di DragGestureListener
*/
public void dragGestureRecognized(DragGestureEvent dge) {
System.out.println ("DragGestureEvent");

dge.startDrag(null, this, this);

}
/*
i metodi di DragSourceListener
*/
public void dragEnter(DragSourceDragEvent dsde) {
System.out.println("DragSourceListener:Enter");

dsde.getDragSourceContext().setCursor(DragSource.DefaultCopyDrop);

repaint();

}
public void dragOver(DragSourceDragEvent dsde) {
System.out.println("DragSourceListener: Over");
}

public void dragExit(DragSourceEvent dse) {

System.out.println("DragSourceListener: Exit");

dse.getDragSourceContext().setCursor(null);

}

public void dragDropEnd(DragSourceDropEvent dsde) {

System.out.println("DragSourceListener: End Drop");
}
public void dropActionChanged(DragSourceDragEvent e) {
}
/*

i metodi di Transferable

*/

public DataFlavor[] getTransferDataFlavors() {
return dfs;
}

public boolean isDataFlavorSupported(DataFlavor sdf) {

for (int i = 0 ; i < dfs.length; i++)
if (dfs[i].equals(sdf)) return true;
return false;
}

public Object getTransferData(DataFlavor tdf) 

   throws UnsupportedFlavorException , IOException {

if (!isDataFlavorSupported(tdf)) 
throw new UnsupportedFlavorException(tdf);
String text = getText();
if (DataFlavor.stringFlavor.equals(tdf)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

ObjectOutputStream oos = new ObjectOutputStream(baos);

try {
oos.writeObject(text);
} catch (Exception e) {
throw new IOException();
}
return new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()));
} else {
StringBufferInputStream sbis = new StringBufferInputStream(text);

return sbis;

}
}

/* 

i DataFlavor di questo Transferable
*/
private static DataFlavor create() {
try {
return new DataFlavor("text/plain; charset=iso8859-1", "String");
} catch (Exception e) {
e.printStackTrace();

return null;

}
}

private static DataFlavor dfs[] = new DataFlavor[] {

create(),

DataFlavor.plainTextFlavor,

DataFlavor.stringFlavor

};
}
 
 

/*

la classe di destinazione
*/

class DropText extends Label implements DropTargetListener {

DropTarget dt;

DropText(String s){

super(s);

/* 

DropTarget(Component, DropTargetListener)

per legare il Component e il listener

*/

dt = new DropTarget(this, this);

}
/* 
tutti i seguenti metodi sono l'implementazione di DropTargetListener

i nomi sono piuttosto esplicativi

*/
public void dragEnter(DropTargetDragEvent e) {
e.acceptDrag(DnDConstants.ACTION_COPY);

System.out.println("DropTargetListener: Enter");

repaint();

}
public void dragOver(DropTargetDragEvent e) {
e.acceptDrag(DnDConstants.ACTION_COPY);

System.out.println("DropTargetListener: Over");

}

public void dragExit(DropTargetEvent e) {

repaint();

System.out.println("DropTargetListener: Exit");

}

public void drop(DropTargetDropEvent dtde) {

DropTargetContext dtc = dtde.getDropTargetContext();

boolean outcome = false;

/*

accettiamo il d&d solo se l'azione è una COPY e se 

il DataFlavor è un plain/text; adesso controlla se è una COPY

*/

if ((dtde.getSourceActions() & DnDConstants.ACTION_COPY) != 0)

dtde.acceptDrop(DnDConstants.ACTION_COPY);
else {
dtde.rejectDrop();

return;

}
/*

adesso riceve i DataFlavor della sorgente e controlla se c'è plain/text

*/

DataFlavor[] dfs = dtde.getCurrentDataFlavors();

DataFlavor tdf = null;

for (int i = 0; i < dfs.length; i++) {

if (DataFlavor.plainTextFlavor.equals(dfs[i])) {
tdf = dfs[i];

break;

}
}
/*

se tra i DataFlavor c'è text/plain prendiamo i dati in quel formato

*/

if (tdf != null) {
Transferable t = dtde.getTransferable();

InputStream is = null;

try {
is = (InputStream)t.getTransferData(tdf);
} catch (IOException ioe) {
ioe.printStackTrace();

dtc.dropComplete(false);

return;

} catch (UnsupportedFlavorException ufe) {
ufe.printStackTrace();

dtc.dropComplete(false);

repaint();

return;

}
if (is != null) {
String s = getText();
try {
int len = is.available();

System.err.println("len = " + len);

byte[] string = new byte[len];

is.read(string, 0, len);

StringBuffer tmp = new StringBuffer();

/* 

prendendo i dati in questo modo dalla piattaforma nativa dalla 

piattaforma nativa arrivano intermezzati da zeri;qui li filtro

*/

for (int i = 0; i < len; i++) 
if (string[i] != 0)
tmp.append((char)string[i]);
s = new String(tmp);

outcome = true;

} catch (Exception e) {
e.printStackTrace();

dtc.dropComplete(false);

repaint();

return;

} finally {
setText(s);
}
} else outcome = false;
} else {
/*

il Transferable non supporta text/plain, 

rifiutiamo il d&d

*/

dtde.rejectDrop();

return;

}
repaint();
dtc.dropComplete(outcome);
}
/* 
il metodo chiamato quando ci sono alcuni passaggi di stato:

enter -> exit

over -> exit

*/
public void dropActionChanged(DropTargetDragEvent e) {
}
}
 
 

/*

la classe per testare
*/

public class TestText extends Frame {

public TestText() {
super();
}
public void init() { 
setLayout(new GridLayout(1,2));

DragText dragT = new DragText("Drag this");

DropText dropT = new DropText("Drop here");

add(dragT);

add(dropT);

pack();

show();

}
public static void main(String[] args) {
TestText tt = new TestText();

tt.init();

}
}
Purtroppo il d&d è stato disattivato nella beta 4 del jdk 1.2 per Solaris, quindi questo codice è compilabile solo sotto Windows. Provando questo programma si noteranno dei problemi: per esempio accade spesso che ci sia una pausa di anche 5 secondi prima che l’azione avvenga. Inoltre succede ogni tanto che l’evento non venga visto del tutto. Può essere utile testare il programma senza attivare il JIT.

Tirando le somme

Quello che segue non vuole essere una critica, ma solo un rapporto sull’effettivo stato dei lavori per chi è interessato al d&d: il jdk 1.2 è ancora in beta, e dichiaratamente non è un prodotto finito: il d&d è un chiaro esempio di questo. Chi si vuole avvicinare a questa API adesso dovrebbe farlo mettendosi nello spirito dell’esploratore ed essere ben conscio che le specifiche non sono ancora stabili, anche se non dovrebbero essere cambiate molto in futuro. Chi ha provato a cimentarsi con il d&d con jdk1.2 beta 3 si è trovato a dover modificare i programmi in maniera sostanziale con la beta 4 e la discussione per definire l’API sono ancora aperte.

In pratica attualmente il d&d non funziona sotto Solaris: è stato disattivato nella beta 4 per gli eccessivi problemi che si avevano. Sotto Windows il d&d funziona con i limiti che abbiamo visto all’interno della virtual machine, ma sotto 95 il trasferimento dei dati da ambiente ospite a vm non funziona. Questo vuol dire che tutti gli eventi vengono generati e riconosciuti, ma che poi non è possibile portare a termine un trasferimento di dati. Sotto NT invece questo trasferimento funziona. Però ci si può aspettare una buona dose di "errori strani", magari inspiegabili, dovuti sempre al carattere di beta dell’API. In ogni caso è attesa la nuova versione del jdk che dovrebbe risolvere buona parte di questi problemi.

Bibliografia

La maggiore fonte di informazioni sul d&d è ancora il testo delle specifiche; inoltre si possono trovare informazioni interessanti andando a cercare nell’archivio delle domande e dei bug, inserendo come argomento "drag&drop".


 
 
 
 


MokaByte Web  1998 - www.mokabyte.it 
MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it