我试图用DLNA控制点制作一个java媒体播放器。
有一个包含媒体文件的表。
通过使用JavaFX TableView,我已经了解到,在setRowFactory回调中,我们可以在由表元素属性生成的事件上添加侦听器。所有事件类型的TableView仅在内部表数据更改时触发。在某些外部事件或逻辑的情况下,我找不到一种方法来访问表行,也找不到修改每一行的ContextMenu的方法。
表中的每一行表示一个媒体文件。ContextMenu最初只有"Play“(本地)和"Delete”菜单项。例如,网络上出现了DLNA渲染器设备。DLNA发现线程触发了一个事件,我想在每个表行的上下文菜单中添加一个"Play to the device“菜单项。分别,我将需要删除这个项目,一旦相应的设备将消失。
如何从ContextMenu之外连接到每一行的rowFactory?
这是表和行工厂的代码
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 {
...
}提前感谢
发布于 2016-06-14 00:22:25
JavaFX通常遵循MVC/MVP类型模式。在表视图中,TableRow是视图的一部分:因此,要更改表行的外观(在本例中包括与其关联的上下文菜单的内容),您应该让它观察某种模型,并更改上下文菜单中显示的内容,以更改该模型。
我不完全确定我是否正确地理解了您的用例,但我认为我理解表中的每一项都可能有一组与其相关的不同设备。因此,您将有一个实体类,如下所示:
public class FileTableItem extends File {
private final ObservableList<Device> devices = FXCollections.observableArrayList();
public ObservableList<Device> getDevices() {
return devices ;
}
}创建表行时,需要它来观察与其当前项关联的设备列表;您可以使用ListChangeListener来实现这一点。当然,在任何给定时间按行显示的项都可能在超出您控制范围的任意时间发生更改(例如,当用户滚动表时),因此您需要观察该行的item属性,并确保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个项,没有附加任何设备。每个菜单的上下文菜单只显示一个"Delete“选项,该选项删除项目。而不是一个更新项目的后台任务,我用一些控件来模仿它。您可以在表中选择一个项,然后按下"Add“按钮将设备添加到该表中:您随后将看到"Play on device.”出现在它的上下文菜单中。类似地,“删除设备”将删除列表中的最后一个设备。“延迟”复选框将设备的添加或删除延迟两秒钟:这允许您按下按钮,然后(快速)打开上下文菜单;在显示时,您将看到上下文菜单的更新。
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);
}
}



发布于 2016-06-14 08:31:48
有了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
}https://stackoverflow.com/questions/37798019
复制相似问题