Swing Drag and Drop in JTree aus verschiedenen Listen

  • Themenstarter Themenstarter Firephoenix
  • Beginndatum Beginndatum
F

Firephoenix

Gast
Folgendes Problem:
Ich habe 2 verschiedene JList-Objekte.
Die eine Liste enthält Objekte vom Typ A, die andere vom Typ B.
Aus beiden Listen sollen per DnD Objekte in einen JTree gezogen werden können (Copy, kein Move).
Als Wurzelelemente im Tree sollen nur Typ A erlaubt sein, als Kindelemente nur Typ B.
Die Objekte in den Listen liegen aktuell als String vor.

Kann jemand, der etwas besser in der Materie steckt als ich, mir grob erläutern wie ich das ganze aufbauen könnte (muss kein Codebeispiel sein, nur die grobe Richtung die ich einschlagen muss).
Ich sitze schon an den Tutorials für DnD und JTree von Oracle, damit kann man sich aber auch eine ganze Weile beschäftigen :rtfm:

Gruß
 
Bin mir nicht sicher ob es für deinen Fall taugt, afair gibt es diesen Transfermodus schon fertig implementiert, vielleicht reicht setDragEnabled(true) . Ansonsten musst du für die Listen und den Tree einen eigenen TransferHandler schreiben und setzten. Wenn die Daten als eine bestimmter Typ vorliegen sollen musst du dein eigenes DataFlavor definieren, sonst kannst du die schon vorhandenen verwenden, wenn sie ein String sein sollen empfiehlt sich DataFlavor.stringFlavor, dann kannst du auch Texte aus anderen Programmen reinziehen.
 
So, bin endlich dazu gekommen an der Baustelle weiterzuarbeiten.
Ich habe ein kleines Beispiel gebaut mit dem man hier besser arbeiten kann.
Der Code ist in einer Klasse im default-package, also ohne viel Aufwand einzubinden und testbar (nein das ist nicht mein normaler Programmierstil 😉 ).

Kurze Erklärung:
A sind die Objekte die als Wurzeln im Baum vorkommen, B sind die Kinder.
B kann keine Wurzel sein, A kein Kind (außer von der "imaginären" Wurzel).
In der späteren Anwendung sind beides Metaobjekte, die auf ids von meinen Anwendungsobjekten mappen.
Es soll später möglich sein im Baum hinter einem Objekt B einen Button zu klicken, der intern in meiner Anwendungslogik so etwas macht wie ApplicationObjectA.doSomethingWith(ApplicationObjectB). Das ist aber kein Problem wenn ich erstmal die Baumstruktur und das DnD habe.

Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class Gui {

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

	public Gui() {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		frame.setLayout(new BorderLayout());
		JPanel listPanel = createListPanel();
		frame.add(listPanel, BorderLayout.WEST);
		JTree tree = createTree();
		frame.add(tree, BorderLayout.CENTER);
		frame.setPreferredSize(new Dimension(500,500));
		frame.pack();
		frame.setVisible(true);
	}

	private JPanel createListPanel() {
		JPanel listPanel = new JPanel();
		listPanel.setLayout(new GridLayout(0, 1));
		//Fist list
		JList listA = new JList();
		DefaultListModel modelA = new DefaultListModel();
		modelA.addElement(new A("A1"));
		modelA.addElement(new A("A2"));
		modelA.addElement(new A("A3"));
		listA.setModel(modelA);
		ListCellRenderer cellRenderer = new ListABCellRenderer();
		listA.setCellRenderer(cellRenderer);
		listPanel.add(listA);
		//Second list
		JList listB = new JList();
		DefaultListModel modelB = new DefaultListModel();
		modelB.addElement(new B("B1"));
		modelB.addElement(new B("B2"));
		modelB.addElement(new B("B3"));
		listB.setModel(modelB);
		listB.setCellRenderer(cellRenderer);
		listPanel.add(listB);
		return listPanel;
	}

	private JTree createTree() {
		JTree tree = new JTree();
		TreeModel model = new TreeModelAB();
		tree.setModel(model);
		TreeCellRenderer cellRenderer = new TreeCellRendererAB();
		tree.setCellRenderer(cellRenderer);
		return tree;
	}

	/*
	 * Wurzeln
	 */
	private static class A {

		String name;
		List<B> childs = new ArrayList<B>();

		public A(String name) {
			this.name = name;
		}
	}

	private static class B {

		String name;

		public B(String name) {
			this.name = name;
		}
	}

	/*
	 * Renderer for the lists
	 */
	@SuppressWarnings("serial")
	private static class ListABCellRenderer extends DefaultListCellRenderer{
		@Override
		public Component getListCellRendererComponent( JList list, Object value, int index,
                boolean isSelected, boolean cellHasFocus){
			super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
			if(value instanceof A){
				setText("[A]:"+((A)value).name);
			} else if(value instanceof B){
				setText("[B]:"+((B)value).name);
			}
			return this;
		}
	}
	
	/*
	 * Renderer für den Tree
	 */
	@SuppressWarnings("serial")
	private static class TreeCellRendererAB extends DefaultTreeCellRenderer {
		public Component getTreeCellRendererComponent(JTree tree, Object value,
				boolean sel, boolean expanded, boolean leaf, int row,
				boolean hasFocus) {
			super.getTreeCellRendererComponent(tree, value, sel, expanded,
					leaf, row, hasFocus);
			setIcon(null);
			if (value instanceof A) {
				setBackgroundNonSelectionColor(Color.LIGHT_GRAY);
				setText("[A]: " + ((A) value).name);
			} else if (value instanceof B) {
				setBackgroundNonSelectionColor(Color.GRAY);
				setText("[B]: " + ((B) value).name);
			} else {
				setText("");
				setBackgroundNonSelectionColor(Color.GREEN);
			}
			return this;
		}
	}

	/*
	 * Model für den Tree
	 */
	private static class TreeModelAB implements TreeModel {

		RootNode root = new RootNode();

		List<TreeModelListener> listeners = new ArrayList<TreeModelListener>();

		public TreeModelAB() {
			// Das sollte später per DnD möglich sein
			A a1 = new A("A41");
			a1.childs.add(new B("b41"));
			root.nodes.add(a1);
			A a2 = new A("A42");
			a2.childs.add(new B("b42"));
			a2.childs.add(new B("b43"));
			root.nodes.add(a2);
			root.nodes.add(new A("A43"));
		}

		@Override
		public Object getRoot() {
			return root;
		}

		@Override
		public Object getChild(Object parent, int index) {
			if (parent == root) {
				return root.nodes.get(index);
			}
			if (parent instanceof A) {
				return ((A) parent).childs.get(index);
			}
			return null;
		}

		@Override
		public int getChildCount(Object parent) {
			if (parent == root) {
				return root.nodes.size();
			}
			if (parent instanceof A) {
				return ((A) parent).childs.size();
			}
			return 0;
		}

		@Override
		public boolean isLeaf(Object node) {
			return getChildCount(node) == 0;
		}

		@Override
		public void valueForPathChanged(TreePath path, Object newValue) {

		}

		@Override
		public int getIndexOfChild(Object parent, Object child) {
			if (parent == root) {
				return root.nodes.indexOf(child);
			}
			if (parent instanceof A) {
				return ((A) parent).childs.indexOf(child);
			}
			return -1;
		}

		@Override
		public void addTreeModelListener(TreeModelListener l) {
			listeners.add(l);
		}

		@Override
		public void removeTreeModelListener(TreeModelListener l) {
			listeners.remove(l);

		}

		private static class RootNode {
			List<A> nodes = new ArrayList<A>();
		}
	}
}

Mein nächster Schritt müsste doch so aussehen oder?
-Eigenen Transferhandler schreiben der festlegt was ich nach wo bewegen kann
-Diesen den Listen und dem Tree zuweisen
-A und B Transferable implementieren lassen (im Prinzip ist es ausreichend für mich wenn ich Strings übertragen kann, aber ich muss später wissen ob der String aus A oder B in den Baum gedroppt wird).
-Bei Änderungen im Tree entsprechende Methoden im TreeModel anbieten um Knoten einzufügen, in der Methode entsprechende Events an die Listener schicken damit sich die Gui aktualisieren kann.

Gruß
 
Zuletzt bearbeitet von einem Moderator:
Ja, das sollten die nächsten Schritte sein.

Ich denke, du brauchst nur eine Transferhandler-Implemention für beide Listen und zwei verschiedene DataFlavors für A und B. Die übergibst du dann als Parameter an die Implementation. Und wenn A und B Transferable implementieren, kannst du einfach getSelectedItem verwenden und dann casten. Möglicherweise kannst du auch vermeiden, dass A und B Transferable implementieren, wenn du toString()/eigenes Interface verwendest undd dann darauf basierend ein Transferable-Objekt erstellst mit dem entsprechenden DataFlavor.
 
Die Ecke aus Swing ist echt noch ein spanisches Dorf für mich...

Ich versuche aktuell nur den DnD von A in den Tree zu ermöglichen (Aus der Liste in die obere Ebene des Trees).
Allerdings komme ich mit den DataFlavor und dem Transferzeugs noch nicht ordentlich zurecht.
Wenn ich das richtig verstanden habe geben die DataFlavor die Typen an die ich übertrage, dannach kann ich aus dem Transferable mein Transferobjekt rausziehen.

Allerdings verhält sich das System nicht so wie ich erwarten würde, der Transferhandler im Tree wird zwar angestoßen, allerdings entspricht das DataFlavor nicht dem erwarteten (Ich habe eine Ausgabe dazu eingefügt). Entsprechend ist es nicht möglich das Objekt auf dem Tree loszulassen.

Aktueller Code:

Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.TransferHandler;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public class Gui {

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

	public Gui() {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		frame.setLayout(new BorderLayout());
		JPanel listPanel = createListPanel();
		frame.add(listPanel, BorderLayout.WEST);
		JTree tree = createTree();
		frame.add(tree, BorderLayout.CENTER);
		frame.setPreferredSize(new Dimension(500, 500));
		frame.pack();
		frame.setVisible(true);
	}

	private JPanel createListPanel() {
		JPanel listPanel = new JPanel();
		listPanel.setLayout(new GridLayout(0, 1));
		// Fist list
		JList listA = new JList();
		DefaultListModel modelA = new DefaultListModel();
		modelA.addElement(new A("A1"));
		modelA.addElement(new A("A2"));
		modelA.addElement(new A("A3"));
		listA.setModel(modelA);
		ListCellRenderer cellRenderer = new ListABCellRenderer();
		listA.setCellRenderer(cellRenderer);
		listA.setTransferHandler(new TransferHandlerA());
		listA.setDragEnabled(true);
		listPanel.add(listA);
		// Second list
		JList listB = new JList();
		DefaultListModel modelB = new DefaultListModel();
		modelB.addElement(new B("B1"));
		modelB.addElement(new B("B2"));
		modelB.addElement(new B("B3"));
		listB.setModel(modelB);
		listB.setCellRenderer(cellRenderer);
		listB.setTransferHandler(new TransferHandlerB());
		listB.setDragEnabled(true);
		listPanel.add(listB);
		return listPanel;
	}

	private JTree createTree() {
		JTree tree = new JTree();
		TreeModelAB model = new TreeModelAB();
		tree.setModel(model);
		TreeCellRenderer cellRenderer = new TreeCellRendererAB();
		tree.setCellRenderer(cellRenderer);
		tree.setTransferHandler(new TransferHandlerTreeAB(model));
		return tree;
	}

	/*
	 * Wurzeln
	 */
	private static class A {

		String name;
		List<B> childs = new ArrayList<B>();

		public A(String name) {
			this.name = name;
		}
	}

	private static class B {

		String name;

		public B(String name) {
			this.name = name;
		}
	}

	/*
	 * Renderer for the lists
	 */
	@SuppressWarnings("serial")
	private static class ListABCellRenderer extends DefaultListCellRenderer {
		@Override
		public Component getListCellRendererComponent(JList list, Object value,
				int index, boolean isSelected, boolean cellHasFocus) {
			super.getListCellRendererComponent(list, value, index, isSelected,
					cellHasFocus);
			if (value instanceof A) {
				setText("[A]:" + ((A) value).name);
			} else if (value instanceof B) {
				setText("[B]:" + ((B) value).name);
			}
			return this;
		}
	}

	/*
	 * Renderer für den Tree
	 */
	@SuppressWarnings("serial")
	private static class TreeCellRendererAB extends DefaultTreeCellRenderer {
		public Component getTreeCellRendererComponent(JTree tree, Object value,
				boolean sel, boolean expanded, boolean leaf, int row,
				boolean hasFocus) {
			super.getTreeCellRendererComponent(tree, value, sel, expanded,
					leaf, row, hasFocus);
			setIcon(null);
			if (value instanceof A) {
				setBackgroundNonSelectionColor(Color.LIGHT_GRAY);
				setText("[A]: " + ((A) value).name);
			} else if (value instanceof B) {
				setBackgroundNonSelectionColor(Color.GRAY);
				setText("[B]: " + ((B) value).name);
			} else {
				setText("");
				setBackgroundNonSelectionColor(Color.GREEN);
			}
			return this;
		}
	}

	/*
	 * Model für den Tree
	 */
	private static class TreeModelAB implements TreeModel {

		RootNode root = new RootNode();

		List<TreeModelListener> listeners = new ArrayList<TreeModelListener>();

		public TreeModelAB() {
			// Das sollte später per DnD möglich sein
			A a1 = new A("A41");
			a1.childs.add(new B("b41"));
			root.nodes.add(a1);
			A a2 = new A("A42");
			a2.childs.add(new B("b42"));
			a2.childs.add(new B("b43"));
			root.nodes.add(a2);
			root.nodes.add(new A("A43"));
		}

		@Override
		public Object getRoot() {
			return root;
		}

		@Override
		public Object getChild(Object parent, int index) {
			if (parent == root) {
				return root.nodes.get(index);
			}
			if (parent instanceof A) {
				return ((A) parent).childs.get(index);
			}
			return null;
		}

		@Override
		public int getChildCount(Object parent) {
			if (parent == root) {
				return root.nodes.size();
			}
			if (parent instanceof A) {
				return ((A) parent).childs.size();
			}
			return 0;
		}

		@Override
		public boolean isLeaf(Object node) {
			return getChildCount(node) == 0;
		}

		@Override
		public void valueForPathChanged(TreePath path, Object newValue) {

		}

		@Override
		public int getIndexOfChild(Object parent, Object child) {
			if (parent == root) {
				return root.nodes.indexOf(child);
			}
			if (parent instanceof A) {
				return ((A) parent).childs.indexOf(child);
			}
			return -1;
		}

		@Override
		public void addTreeModelListener(TreeModelListener l) {
			listeners.add(l);
		}

		@Override
		public void removeTreeModelListener(TreeModelListener l) {
			listeners.remove(l);

		}

		private static class RootNode {
			List<A> nodes = new ArrayList<A>();
		}
	}

	/*
	 * 
	 * Transferhandler für die liste von A
	 */
	@SuppressWarnings("serial")
	private static class TransferHandlerA extends TransferHandler {

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

		protected Transferable createTransferable(JComponent c) {
			TransferableA transferable = new TransferableA();
			for (Object o : ((JList) c).getSelectedValues()) {
				transferable.values.add((A) o);
			}
			return transferable;
		}

		protected void exportDone(JComponent c, Transferable t, int action) {
			// Nothing to do, lists should not change after export
		}
	}

	/*
	 * Transferhandler für die liste von B
	 */
	@SuppressWarnings("serial")
	private static class TransferHandlerB extends TransferHandler {

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

		protected Transferable createTransferable(JComponent c) {
			TransferableB transferable = new TransferableB();
			for (Object o : ((JList) c).getSelectedValues()) {
				transferable.values.add((B) o);
			}
			return transferable;
		}

		protected void exportDone(JComponent c, Transferable t, int action) {
			// Nothing to do, lists should not change after export
		}
	}

	/*
	 * Transferobjekt für mehrere instanzen von A
	 */
	private static class TransferableA implements Transferable {

		List<A> values = new ArrayList<A>();

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			System.out.println("transferFlavors (TransferableA.class)");
			return new DataFlavor[] { new DataFlavor(TransferableA.class,
					"A-Klasse") };
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return true;
		}

		@Override
		public Object getTransferData(DataFlavor flavor)
				throws UnsupportedFlavorException, IOException {
			return this;
		}

	}

	/*
	 * Transferobjekt für mehrere instanzen von B
	 */
	private static class TransferableB implements Transferable {

		List<B> values = new ArrayList<B>();

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return new DataFlavor[] { new DataFlavor(TransferableB.class,
					"B-Klasse") };
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return true;
		}

		@Override
		public Object getTransferData(DataFlavor flavor)
				throws UnsupportedFlavorException, IOException {
			return this;
		}
	}

	/*
	 * Transferhandler für den tree
	 */
	@SuppressWarnings("serial")
	private static class TransferHandlerTreeAB extends TransferHandler {

		TreeModelAB treeModel;

		public TransferHandlerTreeAB(TreeModelAB treeModel) {
			this.treeModel = treeModel;
		}

		public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
			System.out.println(transferFlavors[0]
					.getDefaultRepresentationClass());
			return transferFlavors.length > 0
					&& transferFlavors[0].getDefaultRepresentationClass()
							.equals(A.class);

		}

		public boolean importData(JComponent comp, Transferable t) {
			System.out.println("importing");
			if (t instanceof TransferableA) {
				for (A a : ((TransferableA) t).values) {
					treeModel.root.nodes.add(a);
				}
				return true;
			}
			return false;
		}

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

	}
}

[EDIT]Danke übrigens für die Hilfe - macht bei Gui-Code sicherlich nur begrenzt Spaß... aber kleiner kriege ich das Beispiel nicht, zumindest nicht ohne jeweils 8-10 seperate Klassen zu posten, was den importaufwand für das Beispiel wieder unangenehm erhöhen würde.[/EDIT]

Gruß
 
Was mir als erstes aufgefallen ist: Du erzeugst immer ein neues DataFlavor. Ich würde immer die selbe Instanz verwenden, die in einer public static final Variable gespeichert ist. Die Verwendung von repsentationClass als Vergleichspunkt in canImport war Grund für die Ablehnung.

Hab das ganze mal ein bischen umgebaut, import wird auch aufgerufen, aber nicht angezeigt, k. A. warum das Model nicht richtig angezeigt wird. Auserdem hab ich mit Generics redundanten Code reduziert:

Java:
//Irgenwo zentral definieren, hier in GUI
public static final DataFlavor AFlavor=new DataFlavor(TransferableA.class,"A");
public static final DataFlavor BFlavor=new DataFlavor(TransferableB.class,"B");

//Setzten der TransferHandler verändert:
listA.setTransferHandler(new ListTransferHandler<A>(AFlavor,A.class));
listB.setTransferHandler(new ListTransferHandler<B>(BFlavor,B.class));

private static class ListTransferHandler<T> extends TransferHandler {
        private DataFlavor df;
        
        public ListTransferHandler(DataFlavor _df,Class<T> representingClass)
        {
            df=_df;
            //Check auf Generische Richtigkeit des DataFlavors:  
            if(df.getDefaultRepresentationClass().isArray() && 
               df.getDefaultRepresentationClass().getComponentType()!=representingClass)
                throw new IllegalArgumentException();
        }
        
        public int getSourceActions(JComponent c) {
            return TransferHandler.MOVE;
        }
 
        protected Transferable createTransferable(JComponent c) {
            //geht natürlich auch mit generischem cast und getSelectedItems()
            ListTransferable<T> transferable = new ListTransferable<T>(df);
            for (T o : ((JList<T>) c).getSelectedValuesList()) {
                transferable.values.add(o);
            }
            return transferable;
        }
 
        protected void exportDone(JComponent c, Transferable t, int action) {
            // Nothing to do, lists should not change after export
        }
    }
 
     private static class ListTransferable<T> implements Transferable {
        private DataFlavor df;
        List<T> values = new ArrayList<T>();

        public ListTransferable(DataFlavor _df)
        {
            df=_df;
        }
 
        @Override
        public DataFlavor[] getTransferDataFlavors() {
            System.out.println("transferFlavors (TransferableA.class)");
            return new DataFlavor[] { df};
        }
 
        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(df);
        }
 
        @Override
        public Object getTransferData(DataFlavor flavor)
                throws UnsupportedFlavorException, IOException {
            if(!isDataFlavorSupported(flavor)) throw new UnsupportedFlavorException(flavor);
            return values.toArray();
        }
 
    }
 
    private static class TransferHandlerTreeAB extends TransferHandler {
 
        TreeModelAB treeModel;
 
        public TransferHandlerTreeAB(TreeModelAB treeModel) {
            this.treeModel = treeModel;
        }
 
        public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
            System.out.println(transferFlavors[0]
                    .getDefaultRepresentationClass());
            //isDataFlavorSupported nutzen:
            return Arrays.asList(transferFlavors).contains(AFlavor); //Später BFlavor noch behandeln
 
        }
 
        public boolean importData(JComponent comp, Transferable t) {
            System.out.println("importing");
            if (t.isDataFlavorSupported(AFlavor)) {
                try {
                    //WICHTIG: nicht Transferable casten sondern getTransferData und isDataFlavorSupported nutzen
                    A[] data=(A[]) t.getTransferData(AFlavor);
                    for (A a : data) {
                        treeModel.root.nodes.add(a);
                    }
                    //TODO model+updates in Ordnung bringen.
                    return true;
                }
                catch (UnsupportedFlavorException | IOException e) {}
            }
            //BFlavor
            return false;
        }
 
        public int getSourceActions(JComponent c) {
            return NONE;
        }
 
    }
 
Danke für den super Ansatz,
das Model wird derzeit nicht aktualisiert da noch keine Methode zum hinzufügen von Knoten vorhanden ist und auch die Listener noch nicht benachrichtigt werden. Die Punkte sind aber eher Kleinigkeiten solange ich meine Daten einmal von der Liste zum Tree geschoben habe 😉

Ich markiere das hier auch mal als erledigt, mein Hauptproblem ist damit nämlich gelöst, der Rest wird dann spaßiges TreeModel-Gefummel 😀

Gruß
 

Zurück
Oben