menu - 适配 TableView 菜单按钮

标签 menu javafx tableview

问题

TableView 的 setTableMenuButtonVisible 提供了一种机制来更改表列的可见性。然而,该功能还有很多不足之处:

  • 菜单应保持打开状态。我有 e。 G。 15个表格列,点击菜单打开->点击列->点击菜单打开->点击下一列->...改变多列的可见性很痛苦
  • 应该有一个全选/取消全选功能
  • 应该有一种方法可以使用自定义项目扩展菜单
  • 取消选择所有列后,无法重新选择列,因为标题消失了,表格菜单也随之消失

  • 换句话说:表格菜单的当前实现是相当无用的。

    问题

    有谁知道如何用适当的菜单替换现有的 tableview 菜单?我见过一个带有“.show-hide-columns-button”样式查找并添加事件过滤器的解决方案。然而那是两年前的事了,也许事情发生了变化。

    非常感谢!

    这就是我想要的方式,通过 ContextMenu 演示(即鼠标右键单击表格):
    public class TableViewSample extends Application {
    
        private final TableView table = new TableView();
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage stage) {
            Scene scene = new Scene(new Group());
            stage.setTitle("Table View Sample");
            stage.setWidth(300);
            stage.setHeight(500);
    
            // create table columns
            TableColumn firstNameCol = new TableColumn("First Name");
            TableColumn lastNameCol = new TableColumn("Last Name");
            TableColumn emailCol = new TableColumn("Email");
    
            table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
    
            // add context menu
            CustomMenuItem cmi;
            ContextMenu cm = new ContextMenu();
    
            // select all item
            Label selectAll = new Label( "Select all");
            selectAll.addEventHandler( MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
                    for( Object obj: table.getColumns()) {
                        ((TableColumn) obj).setVisible(true);
                    }           }
    
            });
    
            cmi = new CustomMenuItem( selectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add( cmi);
    
            // deselect all item
            Label deselectAll = new Label("Deselect all");
            deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
                    for (Object obj : table.getColumns()) {
                        ((TableColumn) obj).setVisible(false);
                    }
                }
    
            });
    
            cmi = new CustomMenuItem( deselectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add( cmi);
    
            // separator
            cm.getItems().add( new SeparatorMenuItem());
    
            // menu item for all columns
            for( Object obj: table.getColumns()) {
    
                TableColumn tableColumn = (TableColumn) obj; 
    
                CheckBox cb = new CheckBox( tableColumn.getText());
                cb.selectedProperty().bindBidirectional( tableColumn.visibleProperty());
    
                cmi = new CustomMenuItem( cb);
                cmi.setHideOnClick(false);
    
                cm.getItems().add( cmi);
            }
    
            // set context menu
            table.setContextMenu(cm);
    
            final VBox vbox = new VBox();
            vbox.setSpacing(5);
            vbox.setPadding(new Insets(10, 0, 0, 10));
            vbox.getChildren().addAll(table);
    
            ((Group) scene.getRoot()).getChildren().addAll(vbox);
    
            stage.setScene(scene);
            stage.show();
        }
    }
    

    最佳答案

    受到 ControlsFX 解决方案的启发,我自己使用反射解决了这个问题。如果有人有更好的想法和更清洁的方法而无需反射(reflection),我会全力以赴。我创建了一个 utils 类以区别于示例代码。

    import java.lang.reflect.Field;
    
    import javafx.collections.ObservableList;
    import javafx.event.EventHandler;
    import javafx.scene.Node;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.ContextMenu;
    import javafx.scene.control.CustomMenuItem;
    import javafx.scene.control.Label;
    import javafx.scene.control.SeparatorMenuItem;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.input.MouseEvent;
    
    import com.sun.javafx.scene.control.skin.TableHeaderRow;
    import com.sun.javafx.scene.control.skin.TableViewSkin;
    
    public class TableViewUtils {
    
        /**
         * Make table menu button visible and replace the context menu with a custom context menu via reflection.
         * The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
         * IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
         * @param tableView
         */
        public static void addCustomTableMenu( TableView tableView) {
    
            // enable table menu
            tableView.setTableMenuButtonVisible(true);
    
            // get the table  header row
            TableHeaderRow tableHeaderRow = getTableHeaderRow((TableViewSkin) tableView.getSkin());
    
            // get context menu via reflection
            ContextMenu contextMenu = getContextMenu(tableHeaderRow);
    
            // setting the preferred height for the table header row
            // if the preferred height isn't set, then the table header would disappear if there are no visible columns
            // and with it the table menu button
            // by setting the preferred height the header will always be visible
            // note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
            double defaultHeight = tableHeaderRow.getHeight();
            tableHeaderRow.setPrefHeight(defaultHeight);
    
            // modify the table menu
            contextMenu.getItems().clear();
    
            addCustomMenuItems( contextMenu, tableView);
    
        }
    
        /**
         * Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
         * @param cm
         * @param table
         */
        private static void addCustomMenuItems( ContextMenu cm, TableView table) {
    
            // create new context menu
            CustomMenuItem cmi;
    
            // select all item
            Label selectAll = new Label("Select all");
            selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
                    for (Object obj : table.getColumns()) {
                        ((TableColumn<?, ?>) obj).setVisible(true);
                    }
                }
    
            });
    
            cmi = new CustomMenuItem(selectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);
    
            // deselect all item
            Label deselectAll = new Label("Deselect all");
            deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
    
                    for (Object obj : table.getColumns()) {
                        ((TableColumn<?, ?>) obj).setVisible(false);
                    }
                }
    
            });
    
            cmi = new CustomMenuItem(deselectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);
    
            // separator
            cm.getItems().add(new SeparatorMenuItem());
    
            // menu item for each of the available columns
            for (Object obj : table.getColumns()) {
    
                TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
    
                CheckBox cb = new CheckBox(tableColumn.getText());
                cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
    
                cmi = new CustomMenuItem(cb);
                cmi.setHideOnClick(false);
    
                cm.getItems().add(cmi);
            }
    
        }
    
        /**
         * Find the TableHeaderRow of the TableViewSkin
         * 
         * @param tableSkin
         * @return
         */
        private static TableHeaderRow getTableHeaderRow(TableViewSkin<?> tableSkin) {
    
            // get all children of the skin
            ObservableList<Node> children = tableSkin.getChildren();
    
            // find the TableHeaderRow child
            for (int i = 0; i < children.size(); i++) {
    
                Node node = children.get(i);
    
                if (node instanceof TableHeaderRow) {
                    return (TableHeaderRow) node;
                }
    
            }
            return null;
        }
    
        /**
         * Get the table menu, i. e. the ContextMenu of the given TableHeaderRow via
         * reflection
         * 
         * @param headerRow
         * @return
         */
        private static ContextMenu getContextMenu(TableHeaderRow headerRow) {
    
            try {
    
                // get columnPopupMenu field
                Field privateContextMenuField = TableHeaderRow.class.getDeclaredField("columnPopupMenu");
    
                // make field public
                privateContextMenuField.setAccessible(true);
    
                // get field
                ContextMenu contextMenu = (ContextMenu) privateContextMenuField.get(headerRow);
    
                return contextMenu;
    
            } catch (Exception ex) {
                ex.printStackTrace();
            }
    
            return null;
        }
    
    }
    

    用法示例:
    import javafx.application.Application;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.PropertyValueFactory;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.VBox;
    import javafx.scene.text.Text;
    import javafx.stage.Stage;
    
    public class CustomTableMenuDemo extends Application {
    
        private final ObservableList<Person> data =
                FXCollections.observableArrayList( 
                new Person("Jacob", "Smith", "jacob.smith@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                new Person("Ethan", "Williams", "ethan.williams@example.com"),
                new Person("Emma", "Jones", "emma.jones@example.com"),
                new Person("Michael", "Brown", "michael.brown@example.com"));
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage stage) {
    
            stage.setTitle("Table Menu Demo");
            stage.setWidth(500);
            stage.setHeight(550);
    
            // create table columns
            TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
            firstNameCol.setMinWidth(100);
            firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
    
            TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
            lastNameCol.setMinWidth(100);
            lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
    
            TableColumn<Person, String> emailCol = new TableColumn<Person, String>("Email");
            emailCol.setMinWidth(180);
            emailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
    
    
            TableView<Person> tableView = new TableView<>();
            tableView.setPlaceholder(new Text("No content in table"));
            tableView.setItems(data);
            tableView.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
    
            final VBox vbox = new VBox();
            vbox.setSpacing(5);
            vbox.setPadding(new Insets(10, 10, 10, 10));
    
            BorderPane borderPane = new BorderPane();
            borderPane.setCenter( tableView);
    
            vbox.getChildren().addAll( borderPane);
    
            Scene scene = new Scene( vbox);
    
    
            stage.setScene(scene);
            stage.show();
    
            // enable table menu button and add a custom menu to it
            TableViewUtils.addCustomTableMenu(tableView);
        }
    
    
        public static class Person {
    
            private final SimpleStringProperty firstName;
            private final SimpleStringProperty lastName;
            private final SimpleStringProperty email;
    
            private Person(String fName, String lName, String email) {
                this.firstName = new SimpleStringProperty(fName);
                this.lastName = new SimpleStringProperty(lName);
                this.email = new SimpleStringProperty(email);
            }
    
            public String getFirstName() {
                return firstName.get();
            }
    
            public void setFirstName(String fName) {
                firstName.set(fName);
            }
    
            public String getLastName() {
                return lastName.get();
            }
    
            public void setLastName(String fName) {
                lastName.set(fName);
            }
    
            public String getEmail() {
                return email.get();
            }
    
            public void setEmail(String fName) {
                email.set(fName);
            }
        }
    
    }
    

    截图:

    自定义表格菜单正在运行,单击按钮时菜单保持打开状态:

    Custom table menu in action

    自定义表格菜单仍然可用,即使没有列可见:

    Custom table menu still available, even though no columns are visible

    编辑:这是一个版本,而不是反射使用一些启发式并替换内部鼠标事件处理程序(如果您想了解更多信息,请参阅 JavaFX 的 TableHeaderRow 类的来源):
    import javafx.collections.ObservableList;
    import javafx.event.EventHandler;
    import javafx.geometry.Side;
    import javafx.scene.Node;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.ContextMenu;
    import javafx.scene.control.CustomMenuItem;
    import javafx.scene.control.Label;
    import javafx.scene.control.SeparatorMenuItem;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.input.MouseEvent;
    
    import com.sun.javafx.scene.control.skin.TableHeaderRow;
    import com.sun.javafx.scene.control.skin.TableViewSkin;
    
    public class TableViewUtils {
    
        /**
         * Make table menu button visible and replace the context menu with a custom context menu via reflection.
         * The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
         * IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
         * @param tableView
         */
        public static void addCustomTableMenu( TableView tableView) {
    
            // enable table menu
            tableView.setTableMenuButtonVisible(true);
    
            // replace internal mouse listener with custom listener 
            setCustomContextMenu( tableView);
    
        }
    
        private static void setCustomContextMenu( TableView table) {
    
            TableViewSkin<?> tableSkin = (TableViewSkin<?>) table.getSkin();
    
            // get all children of the skin
            ObservableList<Node> children = tableSkin.getChildren();
    
            // find the TableHeaderRow child
            for (int i = 0; i < children.size(); i++) {
    
                Node node = children.get(i);
    
                if (node instanceof TableHeaderRow) {
    
                    TableHeaderRow tableHeaderRow = (TableHeaderRow) node;
    
                    // setting the preferred height for the table header row
                    // if the preferred height isn't set, then the table header would disappear if there are no visible columns
                    // and with it the table menu button
                    // by setting the preferred height the header will always be visible
                    // note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
                    double defaultHeight = tableHeaderRow.getHeight();
                    tableHeaderRow.setPrefHeight(defaultHeight);
    
                    for( Node child: tableHeaderRow.getChildren()) {
    
                        // child identified as cornerRegion in TableHeaderRow.java
                        if( child.getStyleClass().contains( "show-hide-columns-button")) {
    
                            // get the context menu
                            ContextMenu columnPopupMenu = createContextMenu( table);
    
                            // replace mouse listener
                            child.setOnMousePressed(me -> {
                                // show a popupMenu which lists all columns
                                columnPopupMenu.show(child, Side.BOTTOM, 0, 0);
                                me.consume();
                            });
                        }
                    }
    
                }
            }
        }
    
        /**
         * Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
         * @param cm
         * @param table
         */
        private static ContextMenu createContextMenu( TableView table) {
    
            ContextMenu cm = new ContextMenu();
    
            // create new context menu
            CustomMenuItem cmi;
    
            // select all item
            Label selectAll = new Label("Select all");
            selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
                    for (Object obj : table.getColumns()) {
                        ((TableColumn<?, ?>) obj).setVisible(true);
                    }
                }
    
            });
    
            cmi = new CustomMenuItem(selectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);
    
            // deselect all item
            Label deselectAll = new Label("Deselect all");
            deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
    
                    for (Object obj : table.getColumns()) {
                        ((TableColumn<?, ?>) obj).setVisible(false);
                    }
                }
    
            });
    
            cmi = new CustomMenuItem(deselectAll);
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);
    
            // separator
            cm.getItems().add(new SeparatorMenuItem());
    
            // menu item for each of the available columns
            for (Object obj : table.getColumns()) {
    
                TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
    
                CheckBox cb = new CheckBox(tableColumn.getText());
                cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
    
                cmi = new CustomMenuItem(cb);
                cmi.setHideOnClick(false);
    
                cm.getItems().add(cmi);
            }
    
            return cm;
        }
    }
    

    关于menu - 适配 TableView 菜单按钮,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27739833/

    相关文章:

    c++添加操作菜单

    java - 用于文本输入/javafx 的矩形光标

    java - 为什么我的 JavaFX TableView 是空的?

    JavaFX:向 TableView 数据添加后缀并保持正确的排序

    iOS:如何从 UIMenuController 获取选定的 UIMenuItem

    c# - WPF。如何使分隔符拉伸(stretch)到菜单宽度?

    html - 菜单放大和缩小宽度问题

    javafx - 如何在输入时在微调文本字段中选择数字或文本?

    JavaFX - 如何将字符串从 'webEngine createWebEngine(Stage stage)' 显示到其他类中的 Pane

    java - 在 JavaFX 中编辑 TableView