调整 TableView 菜单按钮

Posted

技术标签:

【中文标题】调整 TableView 菜单按钮【英文标题】:Adapt TableView menu button 【发布时间】:2015-02-28 15:32:36 【问题描述】:

问题

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

菜单应保持打开状态。我有e。 G。 15个表格列,单击菜单打开->单击列->单击菜单打开->单击下一列-> ...更改多个列的可见性很痛苦

应该有全选/取消全选功能

应该有一种方法可以使用自定义项来扩展菜单

取消选择所有列后,无法重新选择列,因为标题已消失,表格菜单也随之消失

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

问题

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

非常感谢!

这就是我想要的,通过 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();
    

【问题讨论】:

【参考方案1】:

我将上面的代码调整为更通用,以便同时使用 TreeTableView 和 TableView。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javafx.beans.property.BooleanProperty;
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.Control;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TableView;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import org.jetbrains.annotations.NotNull;

/**
 * Helper class to replace default column selection popup for TableView.
 *
 * <p>
 * The original idea credeted to Roland and was found on https://***.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>
 * <p>
 * https://***.com/questions/27739833/adapt-tableview-menu-button
 *
 * @author Roland
 * @author bvissy
 */
public class TreeColumnMenuHelper 

  private final Control tableView;
  private final List<MenuItem> additionalMenuItems = new ArrayList<>();
  private ContextMenu columnPopupMenu;
  private boolean showAllColumnsOperators = true;
  // Default key to show menu: Shortcut (CTRL on windows) + Shift + Space
  private Function<KeyEvent, Boolean> showMenuByKeyboardCheck = ke ->
      ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
  
  public TreeColumnMenuHelper(TableView tableView) 
   this((Control) tableView);
  

  public TreeColumnMenuHelper(TreeTableView tableView) 
    this((Control) tableView);
  

  private TreeColumnMenuHelper(Control tableView) 
    super();
    this.tableView = tableView;

    if (tableView.getSkin() != null) 
      registerListeners();
      return;
    

    // listen to skin change - this should happen once the table is shown
    tableView.skinProperty().addListener((a, b, newSkin) -> 
      final BooleanProperty tableMenuButtonVisibleProperty = getTableMenuButtonVisibleProperty(tableView);
      tableMenuButtonVisibleProperty.addListener((ob, o, n) -> 
        if (n) 
          registerListeners();
        
      );
      if (tableMenuButtonVisibleProperty.get()) 
        registerListeners();
      
    );
  

  /**
   *
   * @return property that controls the menu button in the corner of the table 
   */
  private BooleanProperty getTableMenuButtonVisibleProperty(@NotNull Control tableView) 

    if(tableView instanceof TableView tab) 
      return tab.tableMenuButtonVisibleProperty();
    
    if(tableView instanceof TreeTableView tree) 
      return tree.tableMenuButtonVisibleProperty();
    
    throw new IllegalArgumentException("Argument is no TableView or TreeTableView. Actual class: "+tableView.getClass().getName());
  

  /**
   * Get columns of the table or treetable
   * @return list of columns
   */
  private static List<? extends TableColumnBase> getColumns(Control table) 
    if (table instanceof TableView tab) 
      return tab.getColumns();
     else if (table instanceof TreeTableView tree) 
      return tree.getColumns();
     else 
      throw new IllegalArgumentException(
          "Table argument is no TreeTableView or TableView. Actual class: " + table.getClass()
              .getName());
    
  

  /**
   * 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
    assert buttonNode != null;
    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() 
    TableViewSkinBase tableSkin = (TableViewSkinBase) tableView.getSkin();
    if (tableSkin == null) 
      return null;
    

    // get all children of the skin
    ObservableList<Node> children = tableSkin.getChildren();

    // find the TableHeaderRow child
    for (Node node : children) 

      if (node instanceof TableHeaderRow header) 
        return header;
      
    
    return null;
  

  /**
   * Create a menu with custom items. The important thing is that the menu remains open while you
   * click on the menu items.
   */
  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, this::doSelectAll);

      cmi = new CustomMenuItem(selectAll);
      cmi.setOnAction(this::doSelectAll);
      cmi.setHideOnClick(false);
      cm.getItems().add(cmi);

      // deselect all item
      Label deselectAll = new Label("Deselect all");
      deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, this::doDeselectAll);

      cmi = new CustomMenuItem(deselectAll);
      cmi.setOnAction(this::doDeselectAll);
      cmi.setHideOnClick(false);
      cm.getItems().add(cmi);

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

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

    // menu item for each of the available columns
    for (TableColumnBase col : getColumns(tableView)) 

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

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

      cm.getItems().add(cmi);
    

    return cm;
  

  protected void setAllVisible(boolean visible) 
    for (TableColumnBase col : getColumns(tableView)) 
      col.setVisible(visible);
    
  

  protected void doDeselectAll(Event e) 
    setAllVisible(false);
    e.consume();
  

  protected void doSelectAll(Event e) 
    setAllVisible(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;
  


【讨论】:

【参考方案2】:

如果您只想监听来自 Table Menu Button 的事件(并将状态保存/恢复到 java.util.Preferences),则将监听器添加到 table 的 VisibleLeafColumns [getColumns 中的 ObservableList 不会随选择]。

【讨论】:

【参考方案3】:

我有一个表(实际上是一堆表),其中列不固定。每次更改列时,上述解决方案都会重新设置列列表。因此,如果隐藏了一个名为“Collar Size”的列,当用一组新数据刷新表格时,它会再次出现。

这可能很粗略,但我添加了一个 Set 来存储上次隐藏的列的名称​​,然后这次重新隐藏它们。

要点是一个集合:

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

然后管理从集合中添加和删除项目。我需要在表列上添加一个侦听器以隐藏与以前隐藏的名称匹配的新列。

将不胜感激有关如何实现此目的的其他想法。

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;
  


【讨论】:

您已将其发布为对现有问题的回答。请将其作为自己的问题发布,以便您获得答案。【参考方案4】:

我尝试实现 Balage1551 的解决方案。

对于我的应用程序,我必须更改 TableViewContextMenuHelper(...) 中的侦听器。

如果没有这些更改,我每次更改实际场景并返回到包含 tableview 的屏幕时都会收到 NullPointerException。

我希望其他人会觉得这很有帮助!

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

            );

        );
    );*/ 

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^旧!^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv新!vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv

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

这种适配允许在您打开另一个场景时再次初始化 TableViewContextMenuHelper:

javafx.stage.Stage.setScreen(...);

【讨论】:

【参考方案5】:

更新

当您取消选择所有列时,标题仍然可见,菜单按钮也是如此。 JDK 8u72

【讨论】:

【参考方案6】:

感谢 Roland 的解决方案。 那很棒。为了解决一些问题,我对您的解决方案进行了概括:

避免之后必须为 TableView 分配新的上下文菜单 显示的窗口(当 showAndWait() 应该 使用。它通过注册的 onShown 事件解决了这个问题 包含窗口。 更正了点击时的错误定位错误 菜单已打开时的 + 按钮。 (点击 + while 菜单可见将隐藏菜单。) 使用键盘工作 可以添加其他菜单项

用法:

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

也许有人觉得它有用,这是我的实现:

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://***.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;
    


【讨论】:

【参考方案7】:

受 ControlsFX 解决方案的启发,我自己使用反射解决了这个问题。如果有人在没有反思的情况下有更好的想法和更清洁的方式,我会全力以赴。为了与示例代码区分开来,我创建了一个 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);
        
    


截图:

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

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

编辑:这是一个版本,它使用一些启发式方法代替反射并替换了内部鼠标事件处理程序(如果您想了解更多信息,请参阅 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;
    

【讨论】:

以上是关于调整 TableView 菜单按钮的主要内容,如果未能解决你的问题,请参考以下文章

调整 TableView 大小时,Swift 3.0 表格单元格未填充

查看 tableView

单击下拉菜单时如何调整tableView的高度?

协助区分 URL 和 Tableview 数据

在单元格中单击时,tableView:indexPathForCell 返回 nil

UIRefreshControl 导致不正确的 TableView 偏移量