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 элементов, без подключенных устройств. В контекстном меню для каждого отображается только опция «Удалить», которая удаляет элемент. Вместо фоновой задачи, которая обновляет элементы, я имитировал это с помощью некоторых элементов управления. Вы можете выбрать элемент в таблице и добавить к нему устройства, нажав кнопку «Добавить устройство»: впоследствии вы увидите «Воспроизвести на устройстве ....» в своем контекстном меню. Аналогично «Удалить устройство» удалит последнее устройство в списке. Флажок «Задержка» задерживает добавление или удаление устройства на две секунды: это позволяет вам нажимать кнопку, а затем (быстро) открывать контекстное меню; вы увидите обновление контекстного меню во время его показа.
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);
}
}
Один из методов заключается в том, чтобы прикрепить к 'TableView' onContextMenuRequested' и изменить контекстное меню в соответствии с текущей выбранной строкой. Другой подход состоял бы в том, чтобы иметь все возможные пункты меню и привязывать каждое свойство 'disable' или' visible' к соответствующему выражению на основе выбранного элемента. – Itai
не обязательно добавлять все возможные пункты меню, например, мы не можем знать, какие DLNA-устройства появятся в сети. «onContextMenuRequested» - это интересный подход - таким образом мы можем обновлять ContextMenu каждый раз, когда он вызывается. Я боюсь, это может быть менее эффективным и более трудоемким, чем фоновое обновление на внешнем событии. Но я попробую. Какое прослушивание событий можно подключить к нему? – Liphtier
ОК, я получил его работу с row.setOnContextMenuRequested - большое спасибо! Но мне все еще интересно - это единственный возможный способ? Очень странно, если мы не можем напрямую обращаться к строкам TableView с помощью getRows – Liphtier