Daten aus JTable per Drag 'n Drop in ein JLabel verpacken und auf einem JPanel platzieren

#1

Moin,

ich versuche, dem Thread-Titel entsprechend, Daten, die ich in einer JTable zur Auswahl habe, per Drag 'n Drop in ein JPanel zu bugsieren.
Die JTable habe ich in einem javawx.swing.Popup-Objekt untergebracht, das sich ereignisgesteuert über der Oberfläche darstellt. Ein entsprechendes Beispiel konnte ich dazu auf Creating Popup Components finden.
Aus der JTable möchte ich nun durch Ziehen (drag) mit der Maus ein Objekt aus der JTable selektieren und dessen Daten mit einem JLabel visualisieren, das am Ende in einem JPanel in meiner GUI eingebaut werden soll (drop).

Bisher habe ich nur brauchbare Beispiele im Netz gefunden, die die Standardkomponenten abdecken, die den Mechanismus bereits implementiert haben. Oder Beispiele, die den entscheidenden Schritt des “Droppens” auf ein JPanel nicht dabei haben.

Kann mir jemand mit einem einfachen nachvollziehbaren KSKB auf die Sprünge helfen?
Im Moment experimentiere ich mit einem MouseListener an der JTable und einem DropListener am Panel. Mal sehen, wo das hinführt.

#2

Kürzlich hatte ich aus gegebenem Anlass mal ein Beispiel zusammengeschustert, wo aus einer JTreeTable ein String in ein JLabel geschoben wird: https://github.com/javagl/JTreeTable/blob/master/src/test/java/de/javagl/treetable/test/JTreeTableDragAndDropExample.java Vielleicht könnte einiges davon als “Inspiration” helfen.

So ganz ist mir das im Detail aber noch nicht klar:

Aus der JTable möchte ich nun durch Ziehen (drag) mit der Maus ein Objekt aus der JTable selektieren und dessen Daten mit einem JLabel visualisieren, das am Ende in einem JPanel in meiner GUI eingebaut werden soll (drop).

Im verlinkten Beispiel wird einfach die String-Repräsentation einer Zelle verschoben. Sollen bei dir mehrere Zellen verschoben werden können, und da vielleicht auch die spezifischen Daten, die in der Tabelle liegen? (Z.B.: In der Tabelle liegt ein Person-Objekt, von dem der getName mit einem passenden cell renderer angezeigt wird - da sollte dann wohl nicht der Name, sondern das Person-Objekt gezogen werden, oder?)

#3

Hallo Marco, Vielen Dank für deine Antwort. Ich freue mich immer, wenn ich Antworten von dir bekomme.:+1:

Genau!

In der JTable sind je Zeile Daten eines Bauteils einer Maschine gespeichert. Ich hatte gehofft, das ausgewählte Objekt aus der JTable in ein JLabel transferieren, um dort bspw. Name und Grafik, die im Bauteil-Objekt gespeichert sind, zu visualisieren. Dieses JLabel muss also noch vor dem Drop ins JPanel erzeugt werden. Ich möchte also Daten aus einer JTable in ein JPanel übertragen, mit einem JLabel als notwendige Zwischenstation.
Also sinngemäß: JTable -> Drag-Daten -> JLabel -> Drop-JPanel

Ich muss erst mal den Mechanismus verstehen, damit ich irgendwann das auch mal selbst implementieren kann.

#4

Wenn du sagst

In der JTable sind je Zeile Daten eines Bauteils einer Maschine gespeichert.

dann ist nicht ganz klar, wie der “Rückweg” aussehen kann. Wenn man auf eine Tabellenzelle klickt, kommt man dann noch ohne weiteres an das eigentliche Objekt dran, was dahintersteckt? Z.B. wenn man sowas hat wie (pseudocode)

List<Part> parts = ...;
for (int i=0; i<10; i++) {
    Part part = parts.get(i);
    tableModel.addRow(part.getName(), part.getID(), part.getSize());
}

Dann stünden in der Tabelle ja ggf. nur Strings und Integers, und man käme nur “indirekt” an die Part-Objekte ran - nämlich über den Index (Tabellenzeile - und da halt immer aufpassen, falls man einen Table-Sorter drin hat :wink: ).

Wenn man an die Part-Objekte drankommt, wäre es noch praktisch zu wissen, ob die Serializable sind. Soweit ich weiß kann man sich dann das erstellen einer eigenen Transferable-Implementierung sparen. Aber in sooo vielen Varianten hab’ ich DnD selbst noch nicht benutzt, da müßt ich auch nochmal reinschauen. (Ein eigenes Transferable ist nicht wirklich aufwändig, aber eine einfache(re) Lösung wäre ja vielleicht praktisch)

Dass das JLabel da so explizit auftaucht wundert mich etwas. Das ist ja vermutlich nicht “zwingend”, in dem Sinne, dass man ja vermutlich einfach eine Methode

class DropPanel extends JPanel {
    void acceptDroppedStuff(Part part) {

        // Might create a JLabel here.... 
    }
}

erstellen würde - ob da nun ein JLabel verwendet wird, wäre für die “Schnittstelle” dann ja egal. (Es könnte auch ein CloseablePanel sein :wink: )

#5

Hi Marco, danke für deine Antwort.

Die Sache ist eigentlich eine Einbahnstraße. Es sollen nur Daten aus der Tabelle ins Panel übernommen und dargestellt werden. Aus dem Panel soll nichts in die Tabelle geschoben werden.
Das TableModel sammelt in einer Liste die Bauteile einer Maschine. Dazu bekommt das Model eine zuvor zusammengebaute Liste übergeben, die aus Bauteil-Objekten besteht und in der JTable dargestellt werden.
Das Model ist so gebaut, dass es nicht nur die String-Repräsentationen der Daten ausgibt, es kann auch ein selektiertes Bauteil zurückgeben.

Am Ende soll dann das Bauteil, das aus der Tabelle gezogen wurde, im Panel symbolisch dargestellt werden. Ich wollte schon, dass das Panel das für die Darstellung benötigten JLabel erstellt. Da ich die JLabel auf dem Panel positionieren will, sollte das JLabel bereits nach dem Verlassen der Tabelle über dem Panel zu sehen sein.
Ich habe heute ins Oracle Tutorial zum DnD geguckt. Es hat aber erst Mal noch mehr Fragen aufgeworfen, als es beantwortet hat.

Schöne Grüße
Gernot

#6

Das mit dem “Rückweg” bezog sich darauf: In der JTable wird auf irgendeine Tabellenzelle gedrückt, um das DnD zu starten. Kommt man von der angeklickten Tabellenzelle noch auf das echte, dahinter liegende Part-Objekt?

So, wie es jetzt formuliert ist, klingt das ja so, als könnte es da eine Stelle geben, wo man alles beisammen hat (Pseudocode)

List<Part> parts = ...;
TableModel model = createModelFrom(parts);
JTable table = new JTable(model);

// Grob wie im verlinkten Beispiel:
DragGestureListener dragGestureListener = new DragGestureListener() {
    @Override
    public void dragGestureRecognized(DragGestureEvent dge) {
        int row = getSelectedRow(table);
            
        Part part = parts.get(row); // HIER hat man das relevante Part-Objekt!
        machDamitIrgendwas(part);
    }
}

(Es hätte ja sein können, dass man “irgendwo” eine JTable bekommt, und die Information, in welcher Zeile welches Part-Objekt steht, durch Zwischen- und Abstraktionsschritte verloren gegangen ist. Eigentlich muss die row noch von der View ins Modell konvertiert werden, falls die Tabelle z.B. sortiert sein kann…)

Aber falls das ungefähr so ist, kommt man an der “Quelle” schonmal ohne Probleme an das Part-Objekt.


Der “Empfänger-Teil” des Part-Objekts ist dann das Panel.

Dort soll dann, wenn ich das richtig verstanden habe, das JLabel angezeigt werden, wenn man droppt, aber auch schon während man das Bauteil nur über das Ziel-Panel drüberzieht.

Das klingt ein bißchen wie etwas, was ich auch schon mal gemacht habe. Aaaber: Das habe ich bisher nur einmal gemacht, und in einem recht “komplizierten” Zusammenhang, und ich habe mich da auch ein bißchen zum Ziel hingestolpert :flushed: D.h. vielleicht gibt es auch eine vieeel einfachere Lösung, die ich nur nicht gefunden habe…

Das ganze ist Teil von https://github.com/javagl/Flow :

gif

(Dass das so halbtransparent-grün ist, ist natürlich nur eye candy ;-))

Die relevantesten Teile sind hier wohl…

Das ist alles im Rahmen einer “komplexeren” Applikation, und es gibt sicher Konstellationen, wo das ganze “einfacher” gelöst werden kann. Bezogen auf das Mini-Beispiel: Dort ist eben nur die drop-Methode (ziemlich trivial) implementiert. Um das JLabel auch schon beim Drüberziehen anzuzeigen, müßte dort dragOver passend implementiert werden: https://github.com/javagl/JTreeTable/blob/master/src/test/java/de/javagl/treetable/test/JTreeTableDragAndDropExample.java#L151


Allgemein: DnD ist ziemlich kompliziert, das stimmt. Und alles, was ich hier sage, ist genau unter diesem Vorbehalt zu sehen: Vielleicht gibt es einfachere Lösungen.

Was ich bei DnD schwierig fand: Es gibt zwei verschiedene “Ebenen”, an denen man ansetzen kann: Bei manchen Sachen muss man nur wenige Zeilen Code schreiben, und sowas sagen wie

source.enableDrag(true);
new DropTarget(target);

und dann passiert die ganze Magie automatisch (das ist der “Drafult Support”, der unter https://docs.oracle.com/javase/tutorial/uiswing/dnd/defaultsupport.html beschrieben ist).

Aber sobald man auch nur einen Hauch weiter runter muss, muss man sich mit DragGestureListener, Transferable, DataFlavor & Co rumschlagen. So gesehen ist klar, dass das kompliziert ist, wenn man bedenkt, dass man ja auch komplexe Objekte in die Anwendung rein und aus der Anwendung raus draggen können will. Z.B. “Drag-And-Drop einer Datei aus dem Windows-Explorer in die Anwendung, um die Datei zu öffnen”. Aber es gibt einige Tutorials und Beispiele, an denen man sich entlanghangeln kann (auch wenn das Gefühl etwas unbehaglich ist, das ganze nicht in aller Tiefe “verstanden” zu haben…)

1 Like
#7

Nebenbei: Wenn du ein KSKB mit einem Dummy-Part und einer einfachen JTable hast, schau’ ich da vielleicht mal.

#8

Hallo Marco, danke, dass du dir die Zeit genommen und dir die Mühe gemacht hast, mir bei der Lösung meines Problems zu helfen!

Ja, in etwas so, wie du das in deinem Pseudocode-Fragment skizziert hast.

Wow, die Animation trifft den Nagel auf den Kopf! Eine etwas angepasste Variante könnte schon die Lösung sein. Danke für die Links zu den Teilen deiner Projekte. Die schaue ich mir auf jeden Fall an. Ich gehe davon aus, das ich da einiges lernen werde. :+1:

Ja, darüber habe ich mir Gedanken gemacht. In einer ersten Testversion, die ich mal gebaut habe, wurden alle “gültigen” Bauteile am oberen Rand des Panels aufgereiht. Da war es möglich, die Bauteile in einen Bereich zu verschieben, den man mit der Maus nicht mehr “erreichen” konnte.
Mein Gedanke dazu war, einen Bereich (Rectangle) festzulegen, in dem die Bauteile nach dem Verschieben liegen müssen. Anderenfalls werden sie aus dem Panel entfernt, bzw. zur Auswahl der Positionierung (DnD) hinzugefügt.

Auch zu diesem Teil habe ich bereits eine Lösung vorbereitet. An den JLabels, in denen die Bauteile dargestellt werden sollen, ist ein MouseListener registriert, der anbietet, ein Bauteil per Rechtsklick-Kontextmenü u.a. aus dem Panel zu löschen. Nach dem Löschen wird das Bauteil wieder Auswahl der Positionierung (DnD) hinzugefügt.

Oh ja, wie sehr du mir da aus der Seele sprichst…

Habe ich bisher nicht, werde da aber wohl nicht herum kommen.

Danke erst mal bis hier her! Ich schaue mir erst mal die Code-Fragmente aus deinen Links an, vielleicht komme ich ja damit bereits weiter. Dafür brauche ich etwas Zeit. Ich melde mich bei Erfolg oder Misserfolg.

Schöne Grüße

Gernot

#9

Das sollte dann fokussierter möglich sein, indem man dragExit (neben dragOver und drop) passend implementiert.

Ja, da gibt es sicher verschiedene Optionen. Je nach Szenario ja vielleicht auch dedizierte Buttons “Remove last” oder “Clear”…

#10

Ich habe mir ein paar Beispiel-Tutorials angesehen. Die einen mehr, die anderen weniger komplex.
Ich weiß jetzt nicht so richtig, wo ich nun anfangen soll und so richtig weiß ich auch nicht, “wo” nun “was” genau “hin” muss.
Ich habe mal ein kleines KSKB geschrieben, das meiner Situation recht nahe kommt. Über Hilfe mit einem verständlichen Beispiel würde ich mich freuen.

dnd.zip (7,1 KB)

#11

Mit dem üblichen Vorbehalt: Ich hab’ auch noch nicht sooo viel mit DnD gemacht, und es könnte sein, dass das alles vieeel einfacher geht, hier ein kleines Päckchen:

dnd src 2019-05-05.zip (8,3 KB)

Die eigentliche Funktionalität steckt praktisch ausschließlich in der Klasse DnD. Die ist unten nochmal gepostet. In der Main sind die relevanten Stellen mit Kommentaren mit XXX markiert.

Die Frage

“wo” nun “was” genau “hin” muss

würde ich schreiben als

“wo” nun “was” genau “hin” “muss”

also mit Anführungzeichen beim “muss” :wink: Man hat da viele Möglichkeiten. Du hattest z.B. eine Klasse DropPanel erstellt, die schon das DropTargetListener interface implementiert hat. In dem Beispiel ist die jetzt weggefallen. Stattdessen wird ein einfaches JPanel verwendet, und der DropTargetListener als anonyme innere Klasse geschrieben. Aber das ist natürlich dem “KSKB-Charakter” des ganzen geschuldet: So ein DropPanel macht in der realen Anwendung sicher Sinn. Da könnte man dann DropTargetListener implementieren, und in den Methoden nur private Methoden mir sprechenden Namen aufrufen, grob

class DropPanel implements DropTargetListener {
    @Override
    public void drop(DropTargetDropEvent dtde) {
        Transferable transferable = dtde.getTransferable();
        Part part = extractPart(transferable);
        insertPartAt(part, dtde.getLocation());
    }

    private void insertPartAt(Part part, Point location) {
        PartPanel partPanel = new PartPanel();
        partPanel.setLocation(location);
        partPanel.setPart(part);
        add(partPanel);
        repaint();
    }

   ...

Ensprechend auch sowas wie showDropPreview(part, location) usw. Sinngemäß: Die Methoden, die das Panel als Funktionalität anbieten soll, genau so implementieren, und sie dann von den DropTargetListener-Methoden (d.h. dem “Controller”) nur noch “ansteuern” lassen, um das ganze etwas zu trennen.

Die Details hängen dann aber vermutlich auch davon ab, ob die “Vorschau” und das Ergebnis des droppens wirklich das gleiche sein sollen. Ich hab’ da jetzt mal so eine Dummy-Klasse PartPanel erstellt, die für beides verwendet wird…

Hier ist das jetzt alles, etwas “zusammengeklatscht”, aber mit ein paar Kommentaren und Sysos:

package dnd;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.io.IOException;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.table.TableModel;

import dnd.model.Part;
import dnd.model.PartTableModel;

public class DnD {

    // Erzeuge den DataFlavor für "Part"-Objekte. Der magische
    // Teil mit "javaJVMLocalObjectMimeType" bewirkt, dass die
    // Objekte NICHT Serialiable sein müssen. (Symbol, z.B.,
    // ist nicht serialiable)
    private static DataFlavor createPartFlavor() {
        try {
            return new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=\"" + Part.class.getName() + "\"");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    // Eine Implementierung von Transferable, die direkt
    // ein "Part"-Objekt einwickelt
    private static class TransferablePart implements Transferable {

        private static final DataFlavor partFlavor = createPartFlavor();

        private final Part part;

        public TransferablePart(Part part) {
            this.part = part;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return new DataFlavor[] { partFlavor };
        }

        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(partFlavor);
        }

        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
            if (flavor.equals(partFlavor)) {
                return part;
            }
            throw new UnsupportedFlavorException(flavor);
        }
    }
    
    static void installDragHandling(JTable table) {
        
        // Wenn man auf der Table ein Drag-And-Drop anfängt, wird
        // das passende Part aus der Tabelle geholt, und das
        // passende TransferablePart erstellt
        DragGestureListener dragGestureListener = new DragGestureListener() {
            @Override
            public void dragGestureRecognized(DragGestureEvent dge) {
                if (table.getSelectedRowCount() != 1) {
                    return;
                }
                int row = table.getSelectedRow();
                TableModel tableModel = table.getModel();
                PartTableModel<?> partTableModel = (PartTableModel<?>) tableModel;
                Part part = partTableModel.getPart(row);
                Transferable transferable = new TransferablePart(part);

                System.out.println("dragGestureRecognized with " + part);

                dge.startDrag(null, transferable);
            }
        };
        DragSource source = DragSource.getDefaultDragSource();
        source.createDefaultDragGestureRecognizer(table, DnDConstants.ACTION_COPY, dragGestureListener);
    }

    // Holt ein Part-Objekt aus einem TransferablePart raus
    private static Part extractPart(Transferable transferable) {
        try {
            Object result = transferable.getTransferData(TransferablePart.partFlavor);
            Part resultPart = (Part) result;
            return resultPart;
        } catch (UnsupportedFlavorException | IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // Ein Panel, das einen "Part" anzeigen kann. Das wird als Vorschau
    // verwendet, während man dragt: In dragEnter wird es erzeugt, in
    // dragOver wird es veschoben, und beim dragDropEnd wird es entfernt.
    // (Bei einem drop wird so eine Instanz auch erstellt, um das
    // gedroppte Part anzuzeigen...)
    static class PartPanel extends JPanel
    {
        PartPanel()
        {
            super(new BorderLayout(5, 0));
            setBorder(BorderFactory.createLineBorder(Color.BLACK));
        }
        void setPart(Part part)
        {
            removeAll();
            add(new JLabel(part.getIcon()), BorderLayout.WEST);
            add(new JLabel(part.getName()), BorderLayout.CENTER);
            add(new JLabel(part.getCode()), BorderLayout.EAST);
            setSize(getPreferredSize());
            doLayout();
        }
    }
    
    static void installDropHandling(JPanel dropPanel) {
        PartPanel draggedPartPanel = new PartPanel();
        
        DropTargetListener dropTargetListener = new DropTargetAdapter() {
            
            @Override
            public void drop(DropTargetDropEvent dtde) {
                
                Transferable transferable = dtde.getTransferable();
                Part part = extractPart(transferable);
                
                // Erzeuge ein neues PartPanel um das gedroppte
                // Part anzuzeigen
                PartPanel partPanel = new PartPanel();
                partPanel.setLocation(dtde.getLocation());
                partPanel.setPart(part);
                dropPanel.add(partPanel);
                dropPanel.repaint();
                
            }

            @Override
            public void dragOver(DropTargetDragEvent dtde) {
                
                System.out.println("dragOver to "+dtde.getLocation());

                // Aktualisiere die Position der Vorschau
                draggedPartPanel.setLocation(dtde.getLocation());
                dropPanel.repaint();
            }

            @Override
            public void dragExit(DropTargetEvent dte) {
                dropPanel.remove(draggedPartPanel);
            }

            @Override
            public void dragEnter(DropTargetDragEvent dtde) {
                
                Transferable transferable = dtde.getTransferable();
                Part part = extractPart(transferable);
                
                System.out.println("dragEnter with "+part);
                
                // Füge das Panel mit der Vorschau hinzu
                draggedPartPanel.setLocation(dtde.getLocation());
                draggedPartPanel.setPart(part);
                dropPanel.add(draggedPartPanel);
                dropPanel.repaint();
            }
        };
        new DropTarget(dropPanel, dropTargetListener);
        
        DragSource source = DragSource.getDefaultDragSource();
        source.addDragSourceListener(new DragSourceAdapter() {
            
            @Override
            public void dragDropEnd(DragSourceDropEvent dsde) {
                
                System.out.println("dragDropEnd");
                
                // Entferne die Vorschau (egal, ob das draggen
                // mit einem drop beendet oder abgebrochen wurde)
                dropPanel.remove(draggedPartPanel);
                dropPanel.repaint();
            }
        });
        
    }
}
1 Like
#12

Was ich in 14 Tagen (neben der Arbeit in meiner Freizeit) versucht habe hin zubekommen, hast du mal eben in 2 Stunden gebastelt? Ich bin platt… :flushed:
Das Ergebnis entspricht genau dem, was ich machen möchte. :+1: Danke Marco! Auch für Deine Anmerkungen zu meinem Code und die Erklärungen in deiner DnD-Klasse.
Dann muss ich “nur noch” Deine Umsetzung verstehen… :thinking:

Und wenn ich mir das so ansehe, hätte ich da noch ein paar Wochen ohne brauchbarem Ergebnis dran gesessen. :scream:

Schöne Grüße

Gernot

#13

Hmja, wo genau die Funktionen dann am besten verortet werden, muss man dann halt sehen - und wie z.B. die Frage beantwortet wird, ob die “Vorschau-Komponente” und die “Gedroppte Komponente” gleich oder unterschiedlich sind.

Nochmal der disclaimer: Das geht vielleicht auch alles einfacher.

(Aber ich persönlich finde einen Stand, der “funktioniert”, praktisch: Der Zyklus aus "Ändern - Testen (läuft noch) - Ändern - Testen (läuft noch) … " ist irgendwie entspannter als die Alternative: "Das ist genau so, wie in dem Tutorial! Lauf’ endlich!!!111 :tired_face: " :wink:

#14

Auf dem DropPanel soll nur das grafische Symbol zu sehen sein, abgelegt und verschoben werden können. Der ganze Rest der Funktionalität für das Bauteil auf diesem Teil der GUI wird dann per Kontextmenü angeboten.

Das mag sein, für den Augenblick bin ich happy, dass das so weit funktioniert und es endlich Fortschritte gibt. Nichts ist frustrierender, als wochenlang an einem Teilproblem auf der Stelle zu treten.
Für deine Hilfe danke ich dir nochmal.

Ich habe noch eine Frage zur Vorschau. In deinem oben verlinkten Beispiel und in in deinem KSKB-Ausbau sieht es so aus, als ziehe man die zu droppende Komponente unter dem Drag-Point hervor.
In den mir bekannten Betriebssystemen ist das DnD optisch so gebaut, dass die zu droppende Komponente halb transparent über dem Drag-Point erscheint.
Hast du eine Idee dazu, wie man die Drop-Komponente über die Drag-Komponente bringt?

#15

Hmja, das habe ich “wahrgenommen”, und wenn ich zurückdenke, denke ich auch, dass es mich “gestört” hat, aber irgendwie habe ich das wohl verdrängt :roll_eyes: Spontan weiß ich nicht auswendig, wie das geht. Ich könnte mir ein paar Ansätze vorstellen, aber … DnD ist im Detail recht kompliziert, da muss ich nochmal schauen (heute Abend wohl eher nicht, aber morgen vielleicht).

Nebenbei: Die Gedroppten Komponenten dann noch verschieben zu können, kann auch tricky sein. In anderen Zusammenhängen hatte ich sowas schonmal probiert, und das Problem dabei ist/war: Wo hängt der MouseMotionListener dran? An der eigentlichen Komponente? Wenn man die mit der Maus zusammen verschiebt, bewegt sich die Maus “relativ” dazu nicht mehr - oder doch… oder wie viel? Was passiert, wenn man so schnell zieht, dass die Maus die gedraggte Kompnente kurz verläßt? (Nur als Vorwarnung: Da gibt es einige Stolpersteine. Aber dazu findet man sicher viele Beispiele online).

#16

Direkt am JLabel, das das Symbol aufnimmt. In diesem Kontext also das JLabel, das erzeugt wird, um die Part-Daten aufzunehmen. In einer früheren Version ohne DnD hat das funktioniert. Wie das nun wird, muss ich erst mal sehen, wenn ich eine Adaptierung und Implementierung deiner DnD-Klasse vorgenommen habe. Ich bin gespannt, ob ich das hinbekomme…

Das ging trotzdem, sollte also keinen negativen Einfluss auf die Funktion haben. Die Koordinaten der Maus wurden ja ermittelt, lediglich das Zeichnen hinkt dann etwas hinterher.

Ich wollte nicht drängen, es war nur eine Mitteilung meiner Beobachtungen.

#17

Ha!

Schön, wenn eine zusätzliche Anforderung einen “zwingt”, eine Lösung zu suchen, die nicht nur “besser”, sondern auch einfacher ist.

Statt die “Vorschau”-Komponente echt zum DropPanel hinzuzufügen und mit dragEnter/dragOver/dragExit zu hantieren um diese Vorschau zu verwalten, kann man auch der drag-and-drop-Aktion auch ein Image zuweisen. Dieses image ersetzt die Vorschau-Komponente, und klebt direkt am Cursor. D.h. es wird direkt beim Beginn des draggens angezeigt - auch über der Popup-Komponente mit der Table.

Die DnD-Klasse aus dem letzten Paket muss nur hiermit ersetzt werden:

package dnd;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableModel;

import dnd.model.Part;
import dnd.model.PartTableModel;

public class DnD {

	// Erzeuge den DataFlavor für "Part"-Objekte. Der magische
	// Teil mit "javaJVMLocalObjectMimeType" bewirkt, dass die
	// Objekte NICHT Serialiable sein müssen. (Symbol, z.B.,
	// ist nicht serialiable)
	private static DataFlavor createPartFlavor() {
		try {
			return new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=\"" + Part.class.getName() + "\"");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			return null;
		}
	}

	// Eine Implementierung von Transferable, die direkt
	// ein "Part"-Objekt einwickelt
	private static class TransferablePart implements Transferable {

		private static final DataFlavor partFlavor = createPartFlavor();

		private final Part part;

		public TransferablePart(Part part) {
			this.part = part;
		}

		@Override
		public DataFlavor[] getTransferDataFlavors() {
			return new DataFlavor[] { partFlavor };
		}

		@Override
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return flavor.equals(partFlavor);
		}

		@Override
		public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
			if (flavor.equals(partFlavor)) {
				return part;
			}
			throw new UnsupportedFlavorException(flavor);
		}
	}

	static void installDragHandling(JTable table) {

		// Wenn man auf der Table ein Drag-And-Drop anfängt, wird
		// das passende Part aus der Tabelle geholt, und das
		// passende TransferablePart erstellt
		DragGestureListener dragGestureListener = new DragGestureListener() {
			@Override
			public void dragGestureRecognized(DragGestureEvent dge) {
				if (table.getSelectedRowCount() != 1) {
					return;
				}
				int row = table.getSelectedRow();
				TableModel tableModel = table.getModel();
				PartTableModel<?> partTableModel = (PartTableModel<?>) tableModel;
				Part part = partTableModel.getPart(row);
				Transferable transferable = new TransferablePart(part);

				System.out.println("dragGestureRecognized with " + part);

				BufferedImage dragImage = createPartPanelImage(part);
				dge.startDrag(null, dragImage, new Point(0, 0), transferable, null);
			}
		};
		DragSource source = DragSource.getDefaultDragSource();
		source.createDefaultDragGestureRecognizer(table, DnDConstants.ACTION_COPY, dragGestureListener);
	}

	private static BufferedImage createPartPanelImage(Part part) {
		PartPanel partPanel = new PartPanel();
		partPanel.setPart(part);
		Dimension dim = partPanel.getPreferredSize();
		int w = dim.width;
		int h = dim.height;
		BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = image.createGraphics();
		JPanel container = new JPanel(new GridLayout(1, 1));
		SwingUtilities.paintComponent(g, partPanel, container, 0, 0, w, h);
		g.dispose();
		return image;
	}

	// Holt ein Part-Objekt aus einem TransferablePart raus
	private static Part extractPart(Transferable transferable) {
		try {
			Object result = transferable.getTransferData(TransferablePart.partFlavor);
			Part resultPart = (Part) result;
			return resultPart;
		} catch (UnsupportedFlavorException | IOException e) {
			e.printStackTrace();
			return null;
		}
	}

	// Ein Panel, das einen "Part" anzeigen kann. Das wird als Vorschau
	// verwendet, während man dragt: In dragEnter wird es erzeugt, in
	// dragOver wird es veschoben, und beim dragDropEnd wird es entfernt.
	// (Bei einem drop wird so eine Instanz auch erstellt, um das
	// gedroppte Part anzuzeigen...)
	static class PartPanel extends JPanel {
		PartPanel() {
			super(new BorderLayout(5, 0));
			setBorder(BorderFactory.createLineBorder(Color.BLACK));
		}

		void setPart(Part part) {
			removeAll();
			add(new JLabel(part.getIcon()), BorderLayout.WEST);
			add(new JLabel(part.getName()), BorderLayout.CENTER);
			add(new JLabel(part.getCode()), BorderLayout.EAST);
			setSize(getPreferredSize());
			doLayout();
		}
	}

	static void installDropHandling(JPanel dropPanel) {

		DropTargetListener dropTargetListener = new DropTargetAdapter() {

			@Override
			public void drop(DropTargetDropEvent dtde) {

				Transferable transferable = dtde.getTransferable();
				Part part = extractPart(transferable);

				// Erzeuge ein neues PartPanel um das gedroppte
				// Part anzuzeigen
				PartPanel partPanel = new PartPanel();
				partPanel.setLocation(dtde.getLocation());
				partPanel.setPart(part);
				dropPanel.add(partPanel);
				dropPanel.repaint();

			}
		};
		new DropTarget(dropPanel, dropTargetListener);
	}

}
#18

Einfacher gefällt mir auch, auch wenn ich immer noch kaum etwas von dem durchschaue, was du gebaut hast. :thinking:
Ich habe eben deine neue Klasse ausprobiert. Sieht bei mir so aus:

Bei mir wird keine Vorschau mehr angezeigt. Ich bin nutze Linux, kann das etwas damit zu tun haben?

#19

Wär’ ja auch zu schön gewesen :expressionless:

Schnelle Websuche liefert https://stackoverflow.com/questions/51381575/swing-drag-image-not-working-on-linux?noredirect=1&lq=1 , ich nehme an dieses isDragImageSupported gibt bei dir auch false zurück.

Die eigentliche Vorschau könnte man dann ja noch wie in der ersten Version anzeigen. Wie man ihn dazu bewegen könnte, die Vorschau auch “über der Table” anzuzeigen, weiß ich sponan nicht. Die liegt ja in einem Popup, und das ist “per Definition” über allen anderen Komponenten.

Vermutlich könnte man irgendwas dengeln. Eine spontane, nicht durchdachte (!) grobe (!!!) Idee wäre

  • die Table nicht in ein Popup legen, sondern in die normale Container-Hierarchie (vielleicht mit einem Overlay-Layout)
  • Beim Anfang des draggens eine GlassPane über das ganze Fenster legen, um die Vorschau-Komponente “überall” anzeigen zu können
  • Beim droppen oder einem Abbruch das Part “echt” hinzufügen und vor allem die GlassPane wegnehmen

Da wären ein paar Umbauten notwendig, und wie gut/einfach das funktionieren könnte, ist schwer zu sagen. Vielleicht schau’ ich nochmal, wenn du meinst, dass das ein gangbarer Weg sein könnte (zeitliche Zusagen sind schwierig, aber spätestens am Wochenende sollte etwas Zeit sein).

#20

Danke Marco, für deine Antwort.

So ist es. So was in der Art habe ich schon vermutet.

Ich habe mir heute mal deine Arbeit unter einem Windows-System angesehen. Sieht wirklich sehr gut aus. Genauso, wie es sein sollte.

Wenn dir da noch was halbwegs Sauberes einfällt…
Anderseits hat das nicht wirklich Priorität. Das ist nur ein Detail, mit einem Nice-To-Have-Charakter.
Ich könnte eine System-Weiche schreiben, die das neue Fragment aufruft, wenn das Host-System ein Windows ist, anderenfalls eben die erste Variante.

Als ich mir das KSKB unter Windows ansah, kam mir für Linux der Gedanke, einfach einen anderen Cursor (Mauszeiger) zu basteln, für den Fall, wenn ein Part-Objekt gedragged wird.
Würde sowas funktionieren?