In my last post I showed how to create custom JavaFX components with Scene Builder and FXML and how to add the custom components to a new Scene. The screenshot below displays the custom TableView component where each row represents data about a shipping container, with a custom “Add Plan” component on the right side of the UI to add a specified container to a plan.
The functionality of this test application will now be extended so that the user can select a row from the table and drag it over to the “Add Plan” component where they can drop it. When the drop event occurs the application will show a dialog displaying the value of the “VFC #” field from the row that was dragged from the container table.
Additionally, when the user selects the row and begins dragging the cursor, we would like the application to display a translucent image of a shipping container (pictured to the left). Once the cursor is over the Add Plan component, the component will be set with a drop shadow effect, in addition to changing the cursor to indicate to the user that the Add Plan component will accept the drop
The Drop Source (TestTable class)
The drop source for this example is the TestTable class from the previous post. I will show sections from this class below, and then will include the entire class at the end of this post for reference.
Below is the TestTable class which contains a TableView with the shipping container information. The first modification to this class was to add a parentProperty listener so that we know when this component is added to its parent node. We will need the parent node to register for drag events that will occur when the user drags outside of the bounds of this container. Once we have the parent we can register the drag listeners.
public class TestTable extends AnchorPane { @FXML private TableView myTableView; private ImageView dragImageView; private Parent root; public TestTable() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestTable.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); parentProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue<? extends Parent> ov, Parent oldP, Parent newP) { root = newP; registerDragEvent(); } }); } catch (IOException exception) { throw new RuntimeException(exception); } }
The first step in the registerDragEvent() method is to read in the image of the shipping container that we want to display to the user and scale it down to 100×100. Next, we can register the OnDragDetectedEvent for the TableView. Once this listener detects a DragEvent it adds the ImageView to the Scene if it hasn’t already been added. It then sets the opacity, sets MouseTransparent to true, which allows the mouse events to be passed to the components underneath it, and then displays the image.
The DragBoard is then fetched for the TableView, at which time we determine what row was selected, and put the value from the VFC# column as the content of the DragBoard.
protected void registerDragEvent() { Image image = new Image(getClass().getResourceAsStream("/com/lynden/planning/ui/container2.png")); dragImageView = new ImageView(image); dragImageView.setFitHeight(100); dragImageView.setFitWidth(100); myTableView.setOnDragDetected(new EventHandler() { @Override public void handle(MouseEvent t) { AnchorPane anchorPane = (AnchorPane) myTableView.getScene().getRoot(); if (!anchorPane.getChildren().contains(dragImageView)) { anchorPane.getChildren().add(dragImageView); } dragImageView.setOpacity(0.5); dragImageView.toFront(); dragImageView.setMouseTransparent(true); dragImageView.setVisible(true); dragImageView.relocate( (int) (t.getSceneX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (t.getSceneY() - dragImageView.getBoundsInLocal().getHeight() / 2)); Dragboard db = myTableView.startDragAndDrop(TransferMode.ANY); ClipboardContent content = new ClipboardContent(); InboundBean inboundBean = (InboundBean) myTableView.getSelectionModel().getSelectedItem(); content.putString(inboundBean.getVfcNumber()); db.setContent(content); t.consume(); } });
Next, we add the OnDragOver event handler to this component’s Parent. This listener will re-position the shipping container image as the mouse is dragged around the UI. If we just add the event handler to the TableView component, the position of the shipping container image will not be updated once the user drags the cursor outside of the TableView’s bounds.
//Add the drag over listener to this component's PARENT, so that the drag over events will be processed even //after the cursor leaves the bounds of this component. root.setOnDragOver(new EventHandler() { public void handle(DragEvent e) { Point2D localPoint = myTableView.getScene().getRoot().sceneToLocal(new Point2D(e.getSceneX(), e.getSceneY())); dragImageView.relocate( (int) (localPoint.getX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (localPoint.getY() - dragImageView.getBoundsInLocal().getHeight() / 2)); e.consume(); } });
The last step for the drop source is to add the OnDragDone event handler so that the shipping container image disappears when the user releases the mouse button.
myTableView.setOnDragDone(new EventHandler() { public void handle(DragEvent e) { dragImageView.setVisible(false); e.consume(); } });
The Drop Target (TestButton class)
The TestButton class is initialized in a similar fashion to the TestTable class. The main difference is that the TestButton doesn’t care about its parent node, and we can just register for the drop events straight from the constructor without having to wait for the component’s parent to be set.
public class TestButton extends AnchorPane { @FXML private AnchorPane myTestButton; public TestButton() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestButton.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } registerDropEvent(); }
The first thing we would like to do is to register for the OnDragOver event so that we can give feedback to the user that this component is a valid drop target. The acceptTransferModes(TransferMode.ANY) will change the cursor to indicate the component accepts a drop event as well as adding a drop shadow to the component to provide a bit of highlighting
private void registerDropEvent() { myTestButton.setOnDragOver(new EventHandler() { @Override public void handle(DragEvent t) { t.acceptTransferModes(TransferMode.ANY); DropShadow dropShadow = new DropShadow(); dropShadow.setRadius(5.0); dropShadow.setOffsetX(3.0); dropShadow.setOffsetY(3.0); dropShadow.setColor(Color.color(0.4, 0.5, 0.5)); myTestButton.setEffect(dropShadow); //Don't consume the event. Let the layers below process the DragOver event as well so that the //translucent container image will follow the cursor. //t.consume(); } });
If the user exits the drop target area without releasing the mouse button the drop shadow effect is cleared from the component by setting the test button’s effect to null.
myTestButton.setOnDragExited(new EventHandler() { @Override public void handle(DragEvent t) { myTestButton.setEffect(null); t.consume(); } });
Finally, if the user does release the mouse button over the target, we will read the content from the Dragboard and display the results in a JOptionPane (used for simplicity sake).
myTestButton.setOnDragDropped(new EventHandler() { @Override public void handle(DragEvent t) { Dragboard db = t.getDragboard(); final String string = db.getString(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JOptionPane.showMessageDialog(null, "Creating a New Plan For Container #: " + string); } }); t.setDropCompleted(true); } });
Results
Below are some screenshots of the drag and drop in action.
Selecting a row and begin dragging displays translucent the shipping container image.
Dragging cursor over the Add Plan component causes the component to be highlighted with a DropShadow effect.
Releasing the mouse button over the Add Plan component will display a dialog containing the VFC# of the container row that was selected from the TableView.
For reference I have included both the TestTable and TestButton classes below.
twitter: @RobTerp
TestTable class
/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package com.lynden.fx.test; import com.lynden.fx.InboundBean; import java.io.IOException; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.geometry.Point2D; import javafx.scene.Parent; import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ClipboardContent; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; /** * * @author ROBT */ public class TestTable extends AnchorPane { @FXML private TableView myTableView; private ImageView dragImageView; private Parent root; public TestTable() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestTable.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); parentProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue<? extends Parent> ov, Parent oldP, Parent newP) { root = newP; registerDragEvent(); } }); } catch (IOException exception) { throw new RuntimeException(exception); } } protected void registerDragEvent() { Image image = new Image(getClass().getResourceAsStream("/com/lynden/planning/ui/container2.png")); dragImageView = new ImageView(image); dragImageView.setFitHeight(100); dragImageView.setFitWidth(100); myTableView.setOnDragDetected(new EventHandler() { @Override public void handle(MouseEvent t) { AnchorPane anchorPane = (AnchorPane) myTableView.getScene().getRoot(); if (!anchorPane.getChildren().contains(dragImageView)) { anchorPane.getChildren().add(dragImageView); } dragImageView.setOpacity(0.5); dragImageView.toFront(); dragImageView.setMouseTransparent(true); dragImageView.setVisible(true); dragImageView.relocate( (int) (t.getSceneX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (t.getSceneY() - dragImageView.getBoundsInLocal().getHeight() / 2)); Dragboard db = myTableView.startDragAndDrop(TransferMode.ANY); ClipboardContent content = new ClipboardContent(); InboundBean inboundBean = (InboundBean) myTableView.getSelectionModel().getSelectedItem(); content.putString(inboundBean.getVfcNumber()); db.setContent(content); t.consume(); } }); //Add the drag over listener to this component's PARENT, so that the drag over events will be processed even //after the cursor leaves the bounds of this component. root.setOnDragOver(new EventHandler() { public void handle(DragEvent e) { Point2D localPoint = myTableView.getScene().getRoot().sceneToLocal(new Point2D(e.getSceneX(), e.getSceneY())); dragImageView.relocate( (int) (localPoint.getX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (localPoint.getY() - dragImageView.getBoundsInLocal().getHeight() / 2)); e.consume(); } }); myTableView.setOnDragDone(new EventHandler() { public void handle(DragEvent e) { dragImageView.setVisible(false); e.consume(); } }); } }
TestButton Class
/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package com.lynden.fx.test; import java.io.IOException; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.effect.DropShadow; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; /** * * @author ROBT */ public class TestButton extends AnchorPane { @FXML private AnchorPane myTestButton; public TestButton() { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/lynden/planning/ui/TestButton.fxml")); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch (IOException exception) { throw new RuntimeException(exception); } registerDropEvent(); } private void registerDropEvent() { myTestButton.setOnDragOver(new EventHandler() { @Override public void handle(DragEvent t) { t.acceptTransferModes(TransferMode.ANY); DropShadow dropShadow = new DropShadow(); dropShadow.setRadius(5.0); dropShadow.setOffsetX(3.0); dropShadow.setOffsetY(3.0); dropShadow.setColor(Color.color(0.4, 0.5, 0.5)); myTestButton.setEffect(dropShadow); //Don't consume the event. Let the layers below process the DragOver event as well so that the //translucent container image will follow the cursor. //t.consume(); } }); myTestButton.setOnDragExited(new EventHandler() { @Override public void handle(DragEvent t) { t.acceptTransferModes(TransferMode.ANY); myTestButton.setEffect(null); t.consume(); } }); myTestButton.setOnDragDropped(new EventHandler() { @Override public void handle(DragEvent t) { Dragboard db = t.getDragboard(); final String string = db.getString(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JOptionPane.showMessageDialog(null, "Creating a New Plan For Container #: " + string); } }); t.setDropCompleted(true); } }); } }
Pingback: Developing a Drag-and-Drop UI in JavaFX, Part I – Skeleton Application | Joel Graff
Pingback: Drag-and-Drop in JavaFX (LinkingNodes with Cubic Curves), Part 2 | Joel Graff