自定义对象从 FX 拖放到 Swing

Posted

技术标签:

【中文标题】自定义对象从 FX 拖放到 Swing【英文标题】:Custom object drag-and-drop from FX to Swing 【发布时间】:2019-01-08 22:49:00 【问题描述】:

我正在开发一个应该通过拖放与现有 Swing 应用程序交互的 JavaFX 应用程序。通过拖放进行的数据交换实际上是有效的,但我们希望重新设计这部分功能,以实际交换自定义 Java 对象,而不是简单的字符串和序列化为 JSON 的对象。问题是,如果使用自定义 MIME 类型而不是例如,Swing UI 不会接收拖动的数据。 text/plain。您可以在下面找到拖动应用程序 (JavaFX) 和放置应用程序 (Swing) 的最小示例。

FxDrag

public class FxDrag extends Application 

    private static final DataFormat format = new DataFormat("application/x-my-mime-type");

    public static void main(String[] args) 
        launch(args);
    

    @Override
    public void start(Stage stage) throws Exception 
        BorderPane root = new BorderPane();
        root.setOnDragDetected(event -> 
            Dragboard dragboard = root.startDragAndDrop(TransferMode.COPY);
            ClipboardContent content = new ClipboardContent();
            content.putString("Test");
            // content.put(format, "Test");
            dragboard.setContent(content);
            event.consume();
        );
        stage.setScene(new Scene(root, 300, 300));
        stage.setTitle("Drag");
        stage.show();
    

SwingDrop

public class SwingDrop 

    public static void main(String[] args) 
        new SwingDrop().run();
    

    private void run() 
        JPanel panel = new JPanel();
        panel.setTransferHandler(new TransferHandler() 

            @Override
            public boolean canImport(TransferSupport support) 
                return true;
            

            @Override
            public boolean importData(TransferSupport support) 
                Stream.of(support.getDataFlavors()).forEach(flavor -> 
                    System.out.println(flavor.getMimeType());
                );
                return super.importData(support);
            

        );
        JFrame frame = new JFrame();
        frame.setTitle("Drop");
        frame.add(panel);
        frame.setSize(300, 300);
        frame.setVisible(true);
    

当在 JavaFX 应用程序中通过 putStringString 放入 content 时,Swing 应用程序接收拖动并提供以下风格:

application/x-java-serialized-object; class=java.lang.String
text/plain; class=java.io.Reader; charset=Unicode
text/plain; class=java.lang.String; charset=Unicode
text/plain; class=java.nio.CharBuffer; charset=Unicode
text/plain; class="[C"; charset=Unicode
text/plain; class=java.io.InputStream; charset=unicode
text/plain; class=java.nio.ByteBuffer; charset=UTF-16
text/plain; class="[B"; charset=UTF-16
text/plain; class=java.io.InputStream; charset=UTF-8
text/plain; class=java.nio.ByteBuffer; charset=UTF-8
text/plain; class="[B"; charset=UTF-8
text/plain; class=java.io.InputStream; charset=UTF-16BE
text/plain; class=java.nio.ByteBuffer; charset=UTF-16BE
text/plain; class="[B"; charset=UTF-16BE
text/plain; class=java.io.InputStream; charset=UTF-16LE
text/plain; class=java.nio.ByteBuffer; charset=UTF-16LE
text/plain; class="[B"; charset=UTF-16LE
text/plain; class=java.io.InputStream; charset=ISO-8859-1
text/plain; class=java.nio.ByteBuffer; charset=ISO-8859-1
text/plain; class="[B"; charset=ISO-8859-1
text/plain; class=java.io.InputStream; charset=windows-1252
text/plain; class=java.io.InputStream
text/plain; class=java.nio.ByteBuffer; charset=windows-1252
text/plain; class="[B"; charset=windows-1252
text/plain; class=java.io.InputStream; charset=US-ASCII
text/plain; class=java.nio.ByteBuffer; charset=US-ASCII
text/plain; class="[B"; charset=US-ASCII

我什至可以从各种应用程序(如浏览器等)中删除不同的数据,Swing 应用程序提供了相应的数据风格(文本、图像等)。

但是,如果我使用自定义格式,则根本不会列出任何口味。 Swing 是否过滤通过拖放应用程序传输的数据类型?

【问题讨论】:

我可以通过使用最少的信息来传输 Java 对象(例如 Product 类),例如键(例如 Product Id),它是一个字符串或分隔符分隔的字符串的组合。拖放以字符串形式发生,在放置位置,Java 对象是使用键字符串值构建的;实际对象在集合中可用。这在两个窗口之间的 Swing 应用程序中有效。 当然,我总是可以以某种方式将必要的信息放入一个字符串中,然后将该字符串作为text/plain 传输,但在我看来这很丑,因为传输的数据不仅仅是一个普通的字符串。此外,通过这种方式,我可以将数据放到几乎任何地方,而不仅仅是放到目标应用程序中。 这是一个想法。但是,我可以将数据放到几乎任何地方,而不仅仅是放到目标应用程序中...。我觉得不行;至少我所尝试的不允许在应用程序内或目标以外的任何地方放置(可以限制)。 使用text/plain 将允许丢弃任何接受text/plain 的东西,例如编辑器、浏览器、大多数应用程序的文本输入。即使内容只是一个字符串,我也想使用自定义 MIME 类型来防止数据传输到这些应用程序中,当然不是出于安全原因,而是出于可用性考虑。 【参考方案1】:

旧答案在不同的应用程序之间不起作用。下面是新的尝试


我设法让这在两个方向上独立的 Swing 和 JavaFX 应用程序之间工作。如果您想查看它,我将一个工作示例上传到GitLab repository,但我将在这里介绍一些基础知识。

如果您查看存储库,您会注意到我有一个名为 model 的 Gradle 子项目,其中包含模型类 com.example.dnd.model.Doctor。这个类是Serializable,包含三个属性:firstNamelastNamenumber。该项目在 JavaFX 和 Swing 应用程序之间共享(即它们使用相同的模型)。在每个应用程序中,我都有一个表格,按这些属性显示Doctors 列表:JavaFX 中的TableView 和Swing 中的JTable

这些应用程序允许您将一行或多行拖动到另一个应用程序并将它们附加到表格的末尾。他们通过发送适当的Doctors 列表来做到这一点。

该示例需要 Java 10。GIF of example in action。


JavaFX

JavaFX 方面我发现实现起来要简单得多。真的,你唯一需要解决的就是如何配置合适的DataFormat。我使用的 MIME 类型是,

application/x-my-mime-type; class=com.example.dnd.model.Doctor

class= 参数在 Swing 端很重要;它用于反序列化。经过反复试验,我发现当您尝试将数据从 Swing 拖到 JavaFX 时,给定的 MIME 类型前面会加上 JAVA_DATAFLAVOR:,因此:

JAVA_DATAFLAVOR:application/x-my-mime-type; class=com.example.dnd.model.Doctor

我必须将它添加到 onDragDetected 处理程序中使用的 DataFormat 中,否则 Swing 无法识别数据格式。我不知道为什么会这样,也没有找到有关此的文档。在更改 Java 版本和/或平台时,我会注意这一点,以防这是依赖于实现的行为(除非您设法找到文档)。

最后,我的DataFormat 是这样声明的:

DataFormat format = new DataForamt(
    "JAVA_DATAFLAVOR:application/x-my-mime-type; class=com.example.dnd.model.Doctor",
    "application/x-my-mime-type; class=com.example.dnd.model.Doctor"
);

我添加了两个标识符,一个带有JAVA_DATAFLAVOR,一个没有,试图涵盖这两种情况(在需要和不需要的情况下)。我不知道这是否有必要,也不知道它是否有帮助。然后我将其存储在一些 static final 字段中以供全局访问。

然后您只需按照您的预期实现onDragXXX 处理程序。


摇摆

在我看来,Swing 方面的参与度更高;虽然这可能只是因为我更喜欢 JavaFX。我想提一下Oracle Tutorials 在这里非常有用。 Swing 中有三个1 与 DnD 相关的重要类:

DataFlavor Swing 的 DataFormat,但更复杂 TransferHandler 基本上将onDragXXX 处理程序作为一个类 Transferable 表示可传输的数据

1 - 还涉及其他类,但这是我发现在这种情况下最重要的三个。

为了让这个工作,我必须创建 TransferHandlerTransferable 的自定义实现。

TransferHandler

import com.example.dnd.model.Doctor;
import java.awt.datatransfer.Transferable;
import java.util.ArrayList;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.TransferHandler;

public class DoctorTransferHandler extends TransferHandler 

  @Override
  public boolean canImport(TransferSupport support) 
    return support.isDrop() && support.isDataFlavorSupported(DoctorTransferable.DOCTOR_FLAVOR);
  

  @Override
  public boolean importData(TransferSupport support) 
    if (!canImport(support)) 
      return false;
    
    JTable table = (JTable) support.getComponent();
    DoctorTableModel model = (DoctorTableModel) table.getModel();
    try 
      Transferable transferable = support.getTransferable();
      ArrayList<Doctor> list =
          (ArrayList<Doctor>) transferable.getTransferData(DoctorTransferable.DOCTOR_FLAVOR);
      model.addAll(list);
      return true;
     catch (Exception ex) 
      ex.printStackTrace();
      return false;
    
  

  @Override
  public int getSourceActions(JComponent c) 
    return COPY_OR_MOVE;
  

  @Override
  protected Transferable createTransferable(JComponent c) 
    JTable table = (JTable) c;
    DoctorTableModel model = (DoctorTableModel) table.getModel();
    return new DoctorTransferable(model.getAll(table.getSelectedRows()));
  

  @Override
  protected void exportDone(JComponent source, Transferable data, int action) 
    if (action == MOVE) 
      JTable table = (JTable) source;
      DoctorTableModel model = (DoctorTableModel) table.getModel();
      model.removeAll(model.getAll(table.getSelectedRows()));
    
  

可转让

import com.example.dnd.model.Doctor;
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.Collection;

public class DoctorTransferable implements Transferable 

  public static final DataFlavor DOCTOR_FLAVOR;

  static 
    try 
      DOCTOR_FLAVOR = new DataFlavor("application/x-my-mime-type; class=java.util.ArrayList");
     catch (ClassNotFoundException ex) 
      throw new RuntimeException(ex);
    
  

  private final ArrayList<Doctor> doctors;

  public DoctorTransferable(Collection<? extends Doctor> doctors) 
    this.doctors = new ArrayList<>(doctors);
  

  @Override
  public DataFlavor[] getTransferDataFlavors() 
    return new DataFlavor[]DOCTOR_FLAVOR;
  

  @Override
  public boolean isDataFlavorSupported(DataFlavor flavor) 
    return DOCTOR_FLAVOR.equals(flavor);
  

  @Override
  public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException 
    if (DOCTOR_FLAVOR.equals(flavor)) 
      return doctors;
    
    throw new UnsupportedFlavorException(flavor);
  


如果您查看DataFlavor 的声明,在Transferable 中,您会发现我使用的MIME 类型与JavaFX 中的相同,但减去了JAVA_DATAFLAVOR: 位。

我相信最重要的部分是创建您自己的 Transferable 来处理您的自定义对象。这个Transferable 将在受保护的TransferHandler#createTranserfable 方法中创建。直到我意识到我需要这样做,我才设法让它发挥作用。 Transferable 负责报告DataFlavor 以及如何检索对象。

接下来需要做的两件事是覆盖canImportimportData。这些方法处理是否可以成功拖放拖过的数据,如果可以,如何将其添加到 Swing 组件。我的例子很简单,将数据添加到JTable的模型末尾。

对于导出数据,您还应该覆盖exportDone。如果传输涉及移动数据而不仅仅是复制数据,此方法负责执行任何清理。


我通过大量的反复试验达到了这个解决方案。结果,再加上我想尽可能简单地做到这一点,很多“标准”行为都没有实现。例如,数据总是附加到表的底部,而不是插入到删除的位置。在 JavaFX 方面,拖动处理程序位于整个 TableView 上,而不是每个 TableCell 上(我认为这更有意义)。

我希望这对你有用。如果没有,请告诉我。

【讨论】:

所以这应该在不同的应用程序之间工作 - 遗憾的是,它没有。我刚刚启动了您的应用程序两次,当从一个实例拖动到另一个实例时,它会抛出一个IllegalArgumentException,原因是DataFormat 'application/x-my-mime-type' already exists.。我还尝试通过在启动时调用 DataFormat/DataFlavor 的创建来规避此问题,但它仍然在拖动 第二个应用程序实例时失败。 好吧,上面的异常只在拖放到应用程序实例时抛出,而之前没有在实例中成功拖放。如果已在实例中成功删除并完成了“跨实例”删除,则会抛出 IllegalArgumentException,原因为 failed to parse:DragImageBits。另外,这次堆栈跟踪没有提到我自己的代码对DataHolder 构造函数的调用,而是在javafx.embed.swing.DataFlavorUtils.getDataFlavors 中创建DataFlavor @lu.koerfer 有趣...刚试了一下,得到了同样的异常。这很奇怪,因为IllegalArgumentException 在两个应用程序实例的上下文中没有意义。我会看看我能不能解决这个问题。 @lu.koerfer 我更新了我的答案,让我知道它是否适合你。 哇,干得真好。我可以使我的最小示例与您的方法一起使用,这绝对适合我的需要。但是,我仍然不知道为什么这是必要的,也许我稍后会做一些研究。我已经在 J​​DK 中找到了 SystemFlavorMap 类,它提供了各种方法来扩展具有 JAVA_DATAFLAVOR: 前缀的 MIME 类型。【参考方案2】:

为方便起见,我将添加@Slaw 的出色解决方案和我的问题中的最小示例的组合。要获得更好的见解,请查看他的答案,因为它更详细。


FxDrag

public class FxDrag extends Application 

    public static final DataFormat FORMAT = new DataFormat(
        "JAVA_DATAFLAVOR:application/x-my-mime-type; class=java.lang.String",
        "application/x-my-mime-type; class=java.lang.String");

    public static void main(String[] args) 
        launch(args);
    

    @Override
    public void start(Stage stage) throws Exception 
        BorderPane root = new BorderPane();
        root.setOnDragDetected(event -> 
            Dragboard dragboard = root.startDragAndDrop(TransferMode.COPY);
            ClipboardContent content = new ClipboardContent();
            content.put(FORMAT, "Test123");
            dragboard.setContent(content);
            event.consume();
        );

        stage.setScene(new Scene(root, 300, 300));
        stage.setTitle("Drag");
        stage.show();
    


SwingDrop

public class SwingDrop 

    public static final DataFlavor FLAVOR;

    static 
        try 
            FLAVOR = new DataFlavor("application/x-my-mime-type; class=java.lang.String");
         catch (ClassNotFoundException ex) 
            throw new RuntimeException(ex);
        
    

    public static void main(String[] args) 
        new SwingDrop().run();
    

    private void run() 
        JPanel panel = new JPanel();
        panel.setTransferHandler(new TransferHandler() 

            @Override
            public boolean canImport(TransferSupport support) 
                return support.isDataFlavorSupported(FLAVOR);
            

            @Override
            public boolean importData(TransferSupport support) 
                if (!canImport(support)) return false;
                try 
                    String data = (String) support.getTransferable().getTransferData(FLAVOR);
                    System.out.println(data);
                    return true;
                 catch (UnsupportedFlavorException e) 
                    e.printStackTrace();
                 catch (IOException e) 
                    e.printStackTrace();
                
                return false;
            

        );
        JFrame frame = new JFrame("Drop");
        frame.getContentPane().add(panel);
        frame.setSize(300, 300);
        frame.setVisible(true);
    


使用这些示例应用程序可以实现从 FX 应用程序到 Swing 应用程序的拖放操作。即使传输的数据只是普通的String,也无法拖动到任何其他应用程序。这样做的目的是为了提高可用性。

【讨论】:

以上是关于自定义对象从 FX 拖放到 Swing的主要内容,如果未能解决你的问题,请参考以下文章

使用自定义数据将 QTreeWidgetItem 拖放到 QGraphicsView

将文件从操作系统拖放到 Java 应用程序 (Swing)

通过拖放将自定义元素的对象引用传递给另一个自定义元素

Qt学习之路(54): 自定义拖放数据对象

WinForms C#中自定义对象类型的跨进程拖放

从 Java Swing 应用程序拖放到 Windows 资源管理器