在行上使用动态ContextMenu的javafx TableView

时间:2016-06-13 19:49:18

标签: javafx event-handling tableview contextmenu

我正在尝试使用DLNA Control Point创建一个java媒体播放器。

有一张包含媒体文件的表格 使用JavaFX TableView,我学到了,在setRowFactory回调中,我们可以添加由表元素属性生成的事件的侦听器。 TableView的所有事件类型仅在内部表数据更改时触发。 在一些外部事件或逻辑的情况下,我找不到一种方法来获取表行,并且例如修改每行的ContextMenu。

表格中的每一行代表一个媒体文件。 ContextMenu最初只有“播放”(本地)和“删除”菜单项。 例如,DLNA渲染器设备已出现在网络上。 DLNA发现线程已触发事件,我想在每个表行的上下文菜单中添加“播放到此设备”菜单项。一旦相应的设备关闭,我将需要删除此项目。

如何从rowFactory东西外部挂钩每行的ContextMenu?

这是表和行工厂的代码

    public FileManager(GuiController guiController) {

        gCtrl = guiController;
        gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name"));
        gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type"));
        gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size"));
        gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime"));


        gCtrl.filesTable.setRowFactory(tv -> {
            TableRow<FileTableItem> row = new TableRow<>();
            row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
                if (!isEmpty) {
                    FileTableItem file = row.getItem();
                    ContextMenu contextMenu = new ContextMenu();

                    if (file.isPlayable()) {
                        row.setOnMouseClicked(event -> {
                            if (event.getClickCount() == 2) {
                                gCtrl.playMedia(file.getAbsolutePath());
                            }
                        });

                        MenuItem playMenuItem = new MenuItem("Play");
                        playMenuItem.setOnAction(event -> {
                            gCtrl.playMedia(file.getAbsolutePath());
                        });
                        contextMenu.getItems().add(playMenuItem);
                    }

                    if (file.canWrite()) {
                        MenuItem deleteMenuItem = new MenuItem("Delete");
                        deleteMenuItem.setOnAction(event -> {
                            row.getItem().delete();
                        });
                        contextMenu.getItems().add(deleteMenuItem);
                    }
                    row.setContextMenu(contextMenu);
                }
            });
            return row;
        });
        gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
    }
    ...
    public class FileTableItem extends File {
    ...
    }

提前致谢

2 个答案:

答案 0 :(得分:2)

JavaFX通常遵循MVC / MVP类型模式。在表视图中,TableRow是视图的一部分:因此要更改表行的外观(包括在这种情况下与其关联的上下文菜单的内容),您应该让它观察某种模型,并更改上下文菜单中显示的内容,您可以更改该模型。

我不完全确定我是否正确理解了您的用例,但我想我明白表中的每个项目可能都有与之关联的不同设备集。所以你会看到类似这样的实体类:

public class FileTableItem extends File {

    private final ObservableList<Device> devices = FXCollections.observableArrayList();

    public ObservableList<Device> getDevices() {
        return devices ;
    }
}

创建表格行时,需要它来观察与其当前项目关联的设备列表;您可以使用ListChangeListener执行此操作。当然,在任何给定时间由一行显示的项目可以在超出您控制的任意时间更改(例如,当用户滚动表格时),因此您需要观察行的项目属性并确保ListChangeListener正在观察正确的项目列表。以下是一些实现此目的的代码:

TableView<FileTableItem> filesTable = new TableView<>();
filesTable.setRowFactory(tv -> {
    TableRow<FileTableItem> row = new TableRow<>();
    ContextMenu menu = new ContextMenu();
    ListChangeListener<FileTableItem> changeListener = (ListChangeListener.Change<? extends FileTableItem> c) -> 
        updateMenu(menu, row.getItem().getDevices());

    row.itemProperty().addListener((obs, oldItem, newItem) -> {
        if (oldItem != null) {
            oldItem.getDevices().removeListener(changeListener);
        }
        if (newItem == null) {
            contextMenu.getItems().clear();
        } else {
            newItem.getDevices().addListener(changeListener);
            updateMenu(menu, newItem.getDevices());
        }
    });

    row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> 
         row.setContextMenu(isNowEmpty ? null : menu));

    return row ;
});

// ...

private void updateMenu(ContextMenu menu, List<Device> devices) {
    menu.getItems().clear();
    for (Device device : devices) {
        MenuItem item = new MenuItem(device.toString());
        item.setOnAction(e -> { /* ... */ });
        menu.getItems().add(item);
    }

}

如果设备列表发生变化,现在将自动更新上下文菜单。

在您的问题下面的评论中,您说您希望表格中有getRows()方法。没有这样的方法,部分原因是设计使用了所描述的MVC方法。即使有,它也不会真正有用:假设一个项目的设备列表滚动出视图已经改变了 - 在这种情况下,没有一个TableRow对应于该项目,所以你不会能够获取对行的引用以更改其上下文菜单。相反,使用所描述的设置,您只需在代码中更新表行的位置更新模型。

如果你的菜单项不依赖于列表等,你可能需要修改它,但这应该足以表明这个想法。

这是一个SSCCE。在此示例中,表中最初有20个项目,未附加任何设备。每个的上下文菜单只显示&#34;删除&#34;删除项目的选项。而不是更新项目的后台任务,我用一些控件模仿这个。您可以选择表格中的项目,然后按&#34;添加设备&#34;添加设备。按钮:您随后将看到&#34;在设备上播放....&#34;出现在其上下文菜单中。同样&#34;删除设备&#34;将删除列表中的最后一个设备。 &#34;延迟&#34;复选框将延迟添加或删除设备两秒:这允许您按下按钮然后(快速)打开上下文菜单;您将看到上下文菜单在显示时更新。

import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;

public class DynamicContextMenuInTable extends Application {

    private int deviceCount = 0 ;

    private void addDeviceToItem(Item item) {
        Device newDevice = new Device("Device "+(++deviceCount));
        item.getDevices().add(newDevice);
    }

    private void removeDeviceFromItem(Item item) {
        if (! item.getDevices().isEmpty()) {
            item.getDevices().remove(item.getDevices().size() - 1);
        }
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Item> table = new TableView<>();
        TableColumn<Item, String> itemCol = new TableColumn<>("Item");
        itemCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
        table.getColumns().add(itemCol);

        table.setRowFactory(tv -> {
            TableRow<Item> row = new TableRow<>();
            ContextMenu menu = new ContextMenu();

            MenuItem delete = new MenuItem("Delete");
            delete.setOnAction(e -> table.getItems().remove(row.getItem()));

            menu.getItems().add(delete);

            ListChangeListener<Device> deviceListListener = c -> 
                updateContextMenu(row.getItem(), menu);

            row.itemProperty().addListener((obs, oldItem, newItem) -> {
                if (oldItem != null) {
                    oldItem.getDevices().removeListener(deviceListListener);
                }
                if (newItem != null) {
                    newItem.getDevices().addListener(deviceListListener);
                    updateContextMenu(row.getItem(), menu);
                }
            });

            row.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> 
                row.setContextMenu(isNowEmpty ? null : menu));

            return row ;
        });

        CheckBox delay = new CheckBox("Delay");

        Button addDeviceButton = new Button("Add device");
        addDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
        addDeviceButton.setOnAction(e -> {
            Item selectedItem = table.getSelectionModel().getSelectedItem();
            if (delay.isSelected()) {
                PauseTransition pause = new PauseTransition(Duration.seconds(2));
                pause.setOnFinished(evt -> {
                    addDeviceToItem(selectedItem);
                });
                pause.play();
            } else {
                addDeviceToItem(selectedItem);
            }
        });

        Button removeDeviceButton = new Button("Remove device");
        removeDeviceButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
        removeDeviceButton.setOnAction(e -> {
            Item selectedItem = table.getSelectionModel().getSelectedItem() ;
            if (delay.isSelected()) {
                PauseTransition pause = new PauseTransition(Duration.seconds(2));
                pause.setOnFinished(evt -> removeDeviceFromItem(selectedItem));
                pause.play();
            } else {
                removeDeviceFromItem(selectedItem);
            }
        });

        HBox buttons = new HBox(5, addDeviceButton, removeDeviceButton, delay);
        BorderPane.setMargin(buttons, new Insets(5));
        BorderPane root = new BorderPane(table, buttons, null, null, null);

        for (int i = 1 ; i <= 20; i++) {
            table.getItems().add(new Item("Item "+i));
        }

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void updateContextMenu(Item item, ContextMenu menu) {
        if (menu.getItems().size() > 1) {
            menu.getItems().subList(1, menu.getItems().size()).clear();
        }
        for (Device device : item.getDevices()) {
            MenuItem menuItem = new MenuItem("Play on "+device.getName());
            menuItem.setOnAction(e -> System.out.println("Play "+item.getName()+" on "+device.getName()));
            menu.getItems().add(menuItem);
        }
    }

    public static class Device {
        private final String name ;

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

        public String getName() {
            return name ;
        }

        @Override
        public String toString() {
            return getName();
        }
    }

    public static class Item {
        private final ObservableList<Device> devices = FXCollections.observableArrayList() ;

        private final String name ;

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

        public ObservableList<Device> getDevices() {
            return devices ;
        }

        public String getName() {
            return name ;
        }
    }

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

enter image description here

enter image description here

enter image description here

答案 1 :(得分:0)

根据sillyfly的建议,我得到了可行的解决方案,但它可能存在性能上的缺陷。所以找到一个更好的一个会很有趣。

class FileManager {


    private GuiController gCtrl;

    protected Menu playToSub = new Menu("Play to...");
    Map<String, MenuItem> playToItems = new HashMap<String, MenuItem>();

    public FileManager(GuiController guiController) {

        gCtrl = guiController;

        gCtrl.fileName.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Name"));
        gCtrl.fileType.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Type"));
        gCtrl.fileSize.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("Size"));
        gCtrl.fileTime.setCellValueFactory(new PropertyValueFactory<FileTableItem, String>("modifiedTime"));

        gCtrl.filesTable.setRowFactory(tv -> {
            TableRow<FileTableItem> row = new TableRow<>();
            row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
                if (!isEmpty) {
                    FileTableItem file = row.getItem();
                    ContextMenu contextMenu = new ContextMenu();

                    if (file.isPlayable()) {
                        row.setOnMouseClicked(event -> {
                            if (event.getClickCount() == 2) {
                                gCtrl.mainApp.playFile = file.getName();
                                gCtrl.playMedia(file.getAbsolutePath());
                            }
                        });

                        MenuItem playMenuItem = new MenuItem("Play");
                        playMenuItem.setOnAction(event -> {
                            gCtrl.mainApp.playFile = file.getName();
                            gCtrl.playMedia(file.getAbsolutePath());
                        });
                        contextMenu.getItems().add(playMenuItem);
                    }

                    if (file.canWrite()) {
                        MenuItem deleteMenuItem = new MenuItem("Delete");
                        deleteMenuItem.setOnAction(event -> {
                            row.getItem().delete();
                        });
                        contextMenu.getItems().add(deleteMenuItem);
                    }
                    row.setContextMenu(contextMenu);
                }
            });
            row.setOnContextMenuRequested((event) -> {

                /// Here, just before showing the context menu we can decide what to show in it
                /// In this particular case it's OK, but it may be time expensive in general
                if(! row.isEmpty()) {
                    if(gCtrl.mainApp.playDevices.size() > 0) {
                        if(! row.getContextMenu().getItems().contains(playToSub)) {
                            row.getContextMenu().getItems().add(1, playToSub);
                        }
                    }
                    else {
                        if(row.getContextMenu().getItems().contains(playToSub)) {
                            row.getContextMenu().getItems().remove(playToSub);
                        }
                    }
                }
            });
            return row;
        });
        gCtrl.filesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
    }

    /// addPlayToMenuItem and removePlayToMenuItem are run from Gui Controller
    /// which in turn is notified by events in UPNP module
    /// The playTo sub menu items are managed here
    public void addPlayToMenuItem(String uuid, String name, URL iconUrl) {
        MenuItem playToItem = new PlayToMenuItem(uuid, name, iconUrl);
        playToItems.put(uuid, playToItem);
        playToSub.getItems().add(playToItem);
    }

    public void removePlayToMenuItem(String uuid) {
        if(playToItems.containsKey(uuid)) {
            playToSub.getItems().remove(playToItems.get(uuid));
            playToItems.remove(uuid);
        }
    }

    public class PlayToMenuItem extends MenuItem {
        PlayToMenuItem(String uuid, String name, URL iconUrl) {
            super();
            if (iconUrl != null) {
                Image icon = new Image(iconUrl.toString());
                ImageView imgView = new ImageView(icon);
                imgView.setFitWidth(12);
                imgView.setPreserveRatio(true);
                imgView.setSmooth(true);
                imgView.setCache(true);
                setGraphic(imgView);
            }
            setText(name);
            setOnAction(event -> {
                gCtrl.mainApp.playFile =    gCtrl.filesTable.getSelectionModel().getSelectedItem().getName();
                gCtrl.mainApp.startRemotePlay(uuid);
            });
        }
    }

    /// Other class methods and members

}