Adapt TableView menu button

2019-01-14 18:59发布

Problem

The setTableMenuButtonVisible of a TableView provides a mechanism to change the visibility of a table column. However that functionality leaves a lot to be desired:

  • The menu should remain open. I have e. g. 15 table columns and it's a pain to click menu open -> click column -> click menu open -> click next column -> ... It's a pain to change the visibility of multiple columns

  • There should be a select all / deselect all functionality

  • There should be a way to extend the menu with custom items

  • After you deselected all columns there's no way to reselect a column because the header is gone and with it the table menu

In other words: The current implementation of the table menu is rather useless.

Question

Does anyone know of a way about how to replace the existing tableview menu with a proper one? I've seen a solution with a ".show-hide-columns-button" style lookup and adding an event filter. However that was 2 years ago, maybe things changed.

Thank you very much!

This is how I'd like to have it, demonstrated via ContextMenu (i. e. right mouse button click on table):

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();
    }
}

5条回答
啃猪蹄的小仙女
2楼-- · 2019-01-14 19:37

Update

Concerning the fact that when you deselect all the columns, the header is still visible and so do the menu button. JDK 8u72

查看更多
Melony?
3楼-- · 2019-01-14 19:42

I have a table (actually a bunch of tables) where the columns aren't fixed. Every time the columns are changed the above solution was re-setting the list of columns. So if a column called "Collar Size" was hidden it would appear again when the table was refreshed with a new set of data.

This may be crude but I've added a Set to store the names of columns that were hidden last time and then re-hide them this time.

The gist is a Set:

private Set<String> turnedOff = new HashSet<>();

and then the management of adding and removing items from the set. I needed to add a listener on the table columns to hide new columns that matched a name previously hidden.

Other ideas on how to accomplish this will be appreciated.

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;

import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
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.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;

public class TableViewContextMenuHelper {

  private Set<String> turnedOff = new HashSet<>();

  private TableView<?> tableView;
  private ContextMenu columnPopupMenu;

  private boolean showAllColumnsOperators = true;

  private List<MenuItem> additionalMenuItems = new ArrayList<>();

  // Default key to show menu: Shortcut + Shift + Space
  private Function<KeyEvent, Boolean> showMenuByKeyboardCheck = 
      ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();


  public TableViewContextMenuHelper(TableView<?> tableView) {
      super();
      this.tableView = tableView;

      tableView.skinProperty().addListener((a, b, newSkin) -> {
        tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
            if (n == true) {
                registerListeners();
            }
        });
        if (tableView.isTableMenuButtonVisible()) {
            registerListeners();
        }
    });

  }

  /**
   * Registers the listeners.
   */
  private void registerListeners() {
      final Node buttonNode = findButtonNode();

      // Keyboard listener on the table
      tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
          if (showMenuByKeyboardCheck.apply(ke)) {
              showContextMenu();
              ke.consume();
          }
      });

      // replace mouse listener on "+" node
      buttonNode.setOnMousePressed(me -> {
          showContextMenu();
          me.consume();

      });

      tableView.getColumns().addListener(new ListChangeListener<TableColumn<?,?>>(){

        @Override
        public void onChanged(javafx.collections.ListChangeListener.Change<? extends TableColumn<?, ?>> c) {
          while(c.next()){
          if(c.getAddedSize()>0){
            // hide "turned off" columns
            for(TableColumn<?, ?> tc:c.getAddedSubList()){              
              if(turnedOff.contains(tc.getText())){
                tc.setVisible(false);
              }

            }
          }
        }
        }
      });

  }

  protected void showContextMenu() {
      final Node buttonNode = findButtonNode();

      setFixedHeader();

      // When the menu is already shown clicking the + button hides it.
      if (columnPopupMenu != null) {
          columnPopupMenu.hide();
      } else {
          // Show the menu
          final ContextMenu newColumnPopupMenu = createContextMenu();
          newColumnPopupMenu.setOnHidden(ev -> {
              columnPopupMenu = null;
          });
          columnPopupMenu = newColumnPopupMenu;
          columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
          // Repositioning the menu to be aligned by its right side (keeping inside the table view)
          columnPopupMenu.setX(
              buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX() 
              - columnPopupMenu.getWidth());
      }
  }



  private void setFixedHeader() {
      // 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)
      Region tableHeaderRow = getTableHeaderRow();
      double defaultHeight = tableHeaderRow.getHeight();
      tableHeaderRow.setPrefHeight(defaultHeight);
  }

  private Node findButtonNode() {
      TableHeaderRow tableHeaderRow = getTableHeaderRow();
      if (tableHeaderRow == null) {
          return null;
      }

      for (Node child : tableHeaderRow.getChildren()) {

          // child identified as cornerRegion in TableHeaderRow.java
          if (child.getStyleClass().contains("show-hide-columns-button")) {
              return child;
          }
      }
      return null;
  }

  private TableHeaderRow getTableHeaderRow() {
      TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
      if (tableSkin == null) {
          return null;
      }

      // 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;
  }


  /**
   * 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 ContextMenu createContextMenu() {

      ContextMenu cm = new ContextMenu();

      // create new context menu
      CustomMenuItem cmi;

      if (showAllColumnsOperators) {
          // select all item
          Label selectAll = new Label("Select all");
          selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));

          cmi = new CustomMenuItem(selectAll);
          cmi.setOnAction(e -> doSelectAll(e));
          cmi.setHideOnClick(false);
          cm.getItems().add(cmi);

          // deselect all item
          Label deselectAll = new Label("Deselect all");
          deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));

          cmi = new CustomMenuItem(deselectAll);
          cmi.setOnAction(e -> doDeselectAll(e));
          cmi.setHideOnClick(false);
          cm.getItems().add(cmi);

          // separator
          cm.getItems().add(new SeparatorMenuItem());
      }

      // menu item for each of the available columns
      for (Object obj : tableView.getColumns()) {

          TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;

          CheckBox cb = new CheckBox(tableColumn.getText());
          cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());

          cmi = new CustomMenuItem(cb);
          if(turnedOff.contains(cb.getText())){
            cb.setSelected(false);
          }
          cmi.setOnAction(e -> {
              cb.setSelected(!cb.isSelected());
              if(cb.isSelected()){
                turnedOff.remove(cb.getText());
              } else {
                turnedOff.add(cb.getText());
              }
              e.consume();
          });
          cmi.setHideOnClick(false);

          cm.getItems().add(cmi);
      }

      if (!additionalMenuItems.isEmpty()) {
          cm.getItems().add(new SeparatorMenuItem());
          cm.getItems().addAll(additionalMenuItems);
      }

      return cm;
  }

  protected void doDeselectAll(Event e) {
      for (TableColumn<?, ?> obj : tableView.getColumns()) {
        turnedOff.add(obj.getText());
          obj.setVisible(false);
      }
      e.consume();
  }

  protected void doSelectAll(Event e) {
      for (TableColumn<?, ?> obj : tableView.getColumns()) {        
        turnedOff.remove(obj.getText());
        obj.setVisible(true);        
      }
      e.consume();
  }

  public boolean isShowAllColumnsOperators() {
      return showAllColumnsOperators;
  }

  /**
   * Sets whether the Select all/Deselect all buttons are visible
   *
   * @param showAllColumnsOperators
   */
  public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
      this.showAllColumnsOperators = showAllColumnsOperators;
  }

  public List<MenuItem> getAdditionalMenuItems() {
      return additionalMenuItems;
  }

  public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
      return showMenuByKeyboardCheck;
  }

  /**
   * Overrides the keypress check to show the menu. Default is Shortcut +
   * Shift + Space.
   *
   * <p>
   * To disable keyboard shortcut use the <code>e -> false</code> function.
   * </p>
   *
   * @param showMenuByKeyboardCheck
   */
  public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
      this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
  }

}
查看更多
Juvenile、少年°
4楼-- · 2019-01-14 19:45

Thank you, Roland for your solution. That was great. I generalized your solution for a little bit to solve some problems:

  • avoid to have to assign the TableView with the new context menu after the window shown (it could cause difficulty when showAndWait() should be used. It solves the problem by registering the onShown event of the containing Window.
  • corrects the mispositioning bug when clicking the + button while the menu is already on. (Clicking the + while
    the menu is visible will hide the menu.)
  • works using keyboard
  • possibility to add additional menu items

Usage:

contextMenuHelper = new TableViewContextMenuHelper(tableView);
// Adding additional menu options
MenuItem exportMenuItem = new MenuItem("Export...");
contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);

Maybe someone find it useful, here is my implementation:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;

import javafx.collections.ObservableList;
import javafx.event.Event;
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.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;

/**
 * Helper class to replace default column selection popup for TableView.
 *
 * <p>
 * The original idea credeted to Roland and was found on
 * {@link http://stackoverflow.com/questions/27739833/adapt-tableview-menu-button}
 * </p>
 * <p>
 * This improved version targets to solve several problems:
 * <ul>
 * <li>avoid to have to assign the TableView with the new context menu after the
 * window shown (it could cause difficulty when showAndWait() should be used. It
 * solves the problem by registering the onShown event of the containing Window.
 * </li>
 * <li>corrects the mispositioning bug when clicking the + button while the menu
 * is already on.</li>
 * <li>works using keyboard</li>
 * <li>possibility to add additional menu items</li>
 * </ul>
 * </p>
 * <p>
 * Usage from your code:
 *
 * <pre>
 * contextMenuHelper = new TableViewContextMenuHelper(this);
 * // Adding additional menu items
 * MenuItem exportMenuItem = new MenuItem("Export...");
 * contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
 * </pre>
 * </p>
 *
 * @author Roland
 * @author bvissy
 *
 */
public class TableViewContextMenuHelper {

    private TableView<?> tableView;
    private ContextMenu columnPopupMenu;

    private boolean showAllColumnsOperators = true;

    private List<MenuItem> additionalMenuItems = new ArrayList<>();

    // Default key to show menu: Shortcut + Shift + Space
    private Function<KeyEvent, Boolean> showMenuByKeyboardCheck = 
        ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();


    public TableViewContextMenuHelper(TableView<?> tableView) {
        super();
        this.tableView = tableView;

        // Hooking at the event when the whole window is shown 
        // and then implementing the event handler assignment
        tableView.sceneProperty().addListener(i -> {

            tableView.getScene().windowProperty().addListener(i2 -> {
                tableView.getScene().getWindow().setOnShown(i3 -> {
                    tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
                        if (n == true) {
                            registerListeners();
                        }
                    });
                    if (tableView.isTableMenuButtonVisible()) {
                        registerListeners();
                    }

                });

            });
        });
    }

    /**
     * Registers the listeners.
     */
    private void registerListeners() {
        final Node buttonNode = findButtonNode();

        // Keyboard listener on the table
        tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
            if (showMenuByKeyboardCheck.apply(ke)) {
                showContextMenu();
                ke.consume();
            }
        });

        // replace mouse listener on "+" node
        buttonNode.setOnMousePressed(me -> {
            showContextMenu();
            me.consume();

        });

    }

    protected void showContextMenu() {
        final Node buttonNode = findButtonNode();

        setFixedHeader();

        // When the menu is already shown clicking the + button hides it.
        if (columnPopupMenu != null) {
            columnPopupMenu.hide();
        } else {
            // Show the menu
            final ContextMenu newColumnPopupMenu = createContextMenu();
            newColumnPopupMenu.setOnHidden(ev -> {
                columnPopupMenu = null;
            });
            columnPopupMenu = newColumnPopupMenu;
            columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
            // Repositioning the menu to be aligned by its right side (keeping inside the table view)
            columnPopupMenu.setX(
                buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX() 
                - columnPopupMenu.getWidth());
        }
    }



    private void setFixedHeader() {
        // 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)
        Region tableHeaderRow = getTableHeaderRow();
        double defaultHeight = tableHeaderRow.getHeight();
        tableHeaderRow.setPrefHeight(defaultHeight);
    }

    private Node findButtonNode() {
        TableHeaderRow tableHeaderRow = getTableHeaderRow();
        if (tableHeaderRow == null) {
            return null;
        }

        for (Node child : tableHeaderRow.getChildren()) {

            // child identified as cornerRegion in TableHeaderRow.java
            if (child.getStyleClass().contains("show-hide-columns-button")) {
                return child;
            }
        }
        return null;
    }

    private TableHeaderRow getTableHeaderRow() {
        TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
        if (tableSkin == null) {
            return null;
        }

        // 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;
    }


    /**
     * 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 ContextMenu createContextMenu() {

        ContextMenu cm = new ContextMenu();

        // create new context menu
        CustomMenuItem cmi;

        if (showAllColumnsOperators) {
            // select all item
            Label selectAll = new Label("Select all");
            selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));

            cmi = new CustomMenuItem(selectAll);
            cmi.setOnAction(e -> doSelectAll(e));
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);

            // deselect all item
            Label deselectAll = new Label("Deselect all");
            deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));

            cmi = new CustomMenuItem(deselectAll);
            cmi.setOnAction(e -> doDeselectAll(e));
            cmi.setHideOnClick(false);
            cm.getItems().add(cmi);

            // separator
            cm.getItems().add(new SeparatorMenuItem());
        }

        // menu item for each of the available columns
        for (Object obj : tableView.getColumns()) {

            TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;

            CheckBox cb = new CheckBox(tableColumn.getText());
            cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());

            cmi = new CustomMenuItem(cb);
            cmi.setOnAction(e -> {
                cb.setSelected(!cb.isSelected());
                e.consume();
            });
            cmi.setHideOnClick(false);

            cm.getItems().add(cmi);
        }

        if (!additionalMenuItems.isEmpty()) {
            cm.getItems().add(new SeparatorMenuItem());
            cm.getItems().addAll(additionalMenuItems);
        }

        return cm;
    }

    protected void doDeselectAll(Event e) {
        for (Object obj : tableView.getColumns()) {
            ((TableColumn<?, ?>) obj).setVisible(false);
        }
        e.consume();
    }

    protected void doSelectAll(Event e) {
        for (Object obj : tableView.getColumns()) {
            ((TableColumn<?, ?>) obj).setVisible(true);
        }
        e.consume();
    }

    public boolean isShowAllColumnsOperators() {
        return showAllColumnsOperators;
    }

    /**
     * Sets whether the Select all/Deselect all buttons are visible
     *
     * @param showAllColumnsOperators
     */
    public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
        this.showAllColumnsOperators = showAllColumnsOperators;
    }

    public List<MenuItem> getAdditionalMenuItems() {
        return additionalMenuItems;
    }

    public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
        return showMenuByKeyboardCheck;
    }

    /**
     * Overrides the keypress check to show the menu. Default is Shortcut +
     * Shift + Space.
     *
     * <p>
     * To disable keyboard shortcut use the <code>e -> false</code> function.
     * </p>
     *
     * @param showMenuByKeyboardCheck
     */
    public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
        this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
    }

}
查看更多
霸刀☆藐视天下
5楼-- · 2019-01-14 19:49

Inspired by the solution of ControlsFX I solved the problem myself using reflection. If someone has a better idea and cleaner way without reflection, I'm all ears. I created a utils class in order to distinguish from the sample code.

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;
    }

}

Example usage:

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);
        }
    }

}

Screenshots:

Custom table menu in action, the menu remains open while you click the buttons:

Custom table menu in action

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

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

Edit: And here's a version that instead of reflection uses some heuristic and replaces the internal mouse event handler (see the source of JavaFX's TableHeaderRow class if you want to know more):

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;
    }
}
查看更多
劫难
6楼-- · 2019-01-14 19:57

I tried to implement Balage1551's solution.

For my Application I had to change the listeners in TableViewContextMenuHelper(...).

Without this changes I received a NullPointerException every time i changed the actual Scene and returned afterwards to my Screen containing the tableview.

I hope someone else might find this helpful!

    // Hooking at the event when the whole window is shown 
    // and then implementing the event handler assignment
    /*tableView.sceneProperty().addListener(i -> {

        tableView.getScene().windowProperty().addListener(i2 -> {
            tableView.getScene().getWindow().setOnShown(i3 -> {
                tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
                    if (n == true) {
                        registerListeners();
                    }
                });
                if (tableView.isTableMenuButtonVisible()) {
                    registerListeners();
                }

            });

        });
    });*/ 

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^OLD!^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvNEW!vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv

    tableView.skinProperty().addListener((a, b, newSkin) -> {
        tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
            if (n == true) {
                registerListeners();
            }
        });
        if (tableView.isTableMenuButtonVisible()) {
            registerListeners();
        }
    });

This adaptation allows to initialize the TableViewContextMenuHelper again when you open an other scene with:

javafx.stage.Stage.setScreen(...);
查看更多
登录 后发表回答