【问题标题】:How can I bypass the JavaFX's TableView "placeholder"?如何绕过 JavaFX 的 TableView “占位符”?
【发布时间】:2022-01-21 06:59:45
【问题描述】:

JavaFX 的TableView 有一个placeholder 属性,它基本上是一个Node,只要它为空就会显示在TableView 中。如果此属性设置为 null(其默认值),它会显示为 Label 或其他基于 Node 的文本,表示“表格中没有内容”。

但是如果表格中有任何数据行,那么占位符 Node 就会消失,TableView 中的整个垂直空间都会被行填充,如果没有足够的数据来填充整张桌子。

这些空行是我想要的,即使表格是空的。换句话说,我根本不想使用占位符。有谁知道我该怎么做?

我宁愿不做一些笨拙的事情,比如在TableView 中放置一个看起来空的行,只要它应该是空的。

【问题讨论】:

标签: java javafx-2 javafx tableview javafx-8


【解决方案1】:

不幸的是,old issue 在 fx9 和更高版本(包括 fx18)中仍未修复。

fx9 的原始皮肤修改(见水平线下方)似乎仍在 fx18 中工作。虽然由于 fx12 引入了对流和 tableHeaderRow 的访问,但可以稍微改进。

在尝试将其调整为更新的 api 时,我想出了一个 - 同样肮脏且未经测试的 - 方法。这个想法是覆盖实际的布局方法,即在布置表格的各个子项时调用的方法,注意占位符并将其替换为流:

public class NoPlaceHolderSkin<T> extends TableViewSkin<T> {

    public NoPlaceHolderSkin(TableView<T> control) {
        super(control);
    }

    @Override
    protected void layoutInArea(Node child, double areaX, double areaY, double areaWidth,
            double areaHeight, double areaBaselineOffset, HPos halignment, VPos valignment) {
        if (child.getStyleClass().contains("placeholder")) {
            child.setVisible(false);
            child = getVirtualFlow();
            child.setVisible(true);
        }
        super.layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, halignment, valignment);
    }
}

因此,我们在 fx9 的背景下重新审视了这些黑客行为。有好有坏:

  • 皮肤移入了一个公共包,现在允许在不访问隐藏类的情况下对其进行子类化(好)
  • 移动 introduced a bug 不允许安装自定义 VirtualFlow(已在 fx10 中修复)
  • 将来某个时候将强烈禁止对隐藏成员的反射访问(阅读:不可能)

在挖掘过程中,我注意到这些 hack 出现了非常轻微的故障(注意:我没有针对 fx8 运行它们,因此这些可能是 fx8 与 fx9 的差异!)

  • 占位符/流的强制不可见/可见性工作正常,除非以空表启动(显示占位符)并在空表时放大表(“新”区域看起来是空的)
  • 将 itemCount 设置为非空会使行在按下导航键时消失(这可能不是什么大问题,因为用户往往不会在空表中导航) - 这肯定是在 fx9 中引入的,在 fx8 中可以正常工作李>

所以我决定使用可见性强制执行:出现轻微故障的原因是 layoutChildren 如果认为占位符可见,则不会布局流程。如果 super 没有,则通过在布局中包含流来处理。

自定义皮肤:

/**
 * TableViewSkin that doesn't show the placeholder.
 * The basic trick is keep the placeholder/flow in-/visible at all 
 * times (similar to https://stackoverflow.com/a/27543830/203657).
 * <p> 
 * 
 * Updated for fx9 plus ensure to update the layout of the flow as
 * needed.
 * 
 * @author Jeanette Winzenburg, Berlin
 */
public class NoPlaceHolderTableViewSkin<T> extends TableViewSkin<T>{

    private VirtualFlow<?> flowAlias;
    private TableHeaderRow headerAlias;
    private Parent placeholderRegionAlias;
    private ChangeListener<Boolean> visibleListener = (src, ov, nv) -> visibleChanged(nv);
    private ListChangeListener<Node> childrenListener = c -> childrenChanged(c);
    
    /**
     * Instantiates the skin.
     * @param table the table to skin.
     */
    public NoPlaceHolderTableViewSkin(TableView<T> table) {
        super(table);
        flowAlias = (VirtualFlow<?>) table.lookup(".virtual-flow");
        headerAlias = (TableHeaderRow) table.lookup(".column-header-background");
        
        // startet with a not-empty list, placeholder not yet instantiatet
        // so add alistener to the children until it will be added
        if (!installPlaceholderRegion(getChildren())) {
            installChildrenListener();
        }
    }


    /**
     * Searches the given list for a Parent with style class "placeholder" and
     * wires its visibility handling if found.
     * @param addedSubList
     * @return true if placeholder found and installed, false otherwise.
     */
    protected boolean installPlaceholderRegion(
            List<? extends Node> addedSubList) {
        if (placeholderRegionAlias !=  null) 
            throw new IllegalStateException("placeholder must not be installed more than once");
        List<Node> parents = addedSubList.stream()
                .filter(e -> e.getStyleClass().contains("placeholder"))
                .collect(Collectors.toList());
        if (!parents.isEmpty()) {
            placeholderRegionAlias = (Parent) parents.get(0);
            placeholderRegionAlias.visibleProperty().addListener(visibleListener);
            visibleChanged(true);
            return true;
        }
        return false;
    }


    protected void visibleChanged(Boolean nv) {
        if (nv) {
            flowAlias.setVisible(true);
            placeholderRegionAlias.setVisible(false);
        }
    }


    /**
     * Layout of flow unconditionally.
     * 
     */
    protected void layoutFlow(double x, double y, double width,
            double height) {
        // super didn't layout the flow if empty- do it now
        final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
        double headerHeight = headerAlias.getHeight();
        y += headerHeight;
        double flowHeight = Math.floor(height - headerHeight);
        layoutInArea(flowAlias, x, y,
                width, flowHeight,
                baselineOffset, HPos.CENTER, VPos.CENTER);
    }


    /**
     * Returns a boolean indicating whether the flow should be layout.
     * This implementation returns true if table is empty.
     * @return
     */
    protected boolean shouldLayoutFlow() {
        return getItemCount() == 0;
    }


    /**
     * {@inheritDoc} <p>
     * 
     * Overridden to layout the flow always.
     */
    @Override
    protected void layoutChildren(double x, double y, double width,
            double height) {
        super.layoutChildren(x, y, width, height);
        if (shouldLayoutFlow()) {
            layoutFlow(x, y, width, height);
            
        }
    }

    /**
     * Listener callback from children modifications.
     * Meant to find the placeholder when it is added.
     * This implementation passes all added sublists to 
     * hasPlaceHolderRegion for search and install the 
     * placeholder. Removes itself as listener if installed.
     * 
     * @param c the change 
     */
    protected void childrenChanged(Change<? extends Node> c) {
        while (c.next()) {
            if (c.wasAdded()) {
                if (installPlaceholderRegion(c.getAddedSubList())) {
                    uninstallChildrenListener();
                    return;
                }
                
            }
        }
    }


    /**
     * Installs a ListChangeListener on the children which calls
     * childrenChanged on receiving change notification. 
     * 
     */
    protected void installChildrenListener() {
        getChildren().addListener(childrenListener);
    }
    
    /**
     * Uninstalls a ListChangeListener on the children:
     */
    protected void uninstallChildrenListener() {
        getChildren().removeListener(childrenListener);
    }
    
    
}

使用示例:

public class EmptyPlaceholdersInSkin extends Application {

    private Parent createContent() {
        // initially populated
        //TableView<Person> table = new TableView<>(Person.persons()) {
        // initially empty
        TableView<Person> table = new TableView<>() {

            @Override
            protected Skin<?> createDefaultSkin() {
                return new NoPlaceHolderTableViewSkin<>(this);
            }
            
        };
        TableColumn<Person, String> first = new TableColumn<>("First Name");
        first.setCellValueFactory(new PropertyValueFactory<>("firstName"));
        
        table.getColumns().addAll(first);
        
        Button clear = new Button("clear");
        clear.setOnAction(e -> table.getItems().clear());
        clear.disableProperty().bind(Bindings.isEmpty(table.getItems()));
        Button fill = new Button("populate");
        fill.setOnAction(e -> table.getItems().setAll(Person.persons()));
        fill.disableProperty().bind(Bindings.isNotEmpty(table.getItems()));
        BorderPane pane = new BorderPane(table);
        pane.setBottom(new HBox(10, clear, fill));
        return pane;
    }


    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.show();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(EmptyPlaceholdersInSkin.class.getName());

}

【讨论】:

  • 伪造项目计数时“行在箭头导航上消失”只能是 fx9/fx10 错误,因为我无法在 fx8 中重现它。无论如何,关于这个问题/错误的一个很好的概述!
【解决方案2】:

如果有人仍在寻找与其他人提供的解决方案不同的替代解决方案,以下是我使用的解决方案。就我而言,这是我可以采用的最简单的方法(没有自定义皮肤,没有 API 调整,也没有像 ListView 这样的繁重控件)。

使用类似于交替行着色的自定义 CSS 设置 StackPane。

StackPane placeHolder = new StackPane();
placeHolder.setStyle("-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #e8e8e8 49% , #f7f7f7 12% );");
tableView.setPlaceholder(placeHolder);

以下是实施的快速参考。左表有数据,右表无数据,显示自定义占位符。

如果您也特别想显示列线,您可以按照@Shreyas Dave 的方法构建带有边框实现的 StackPane 的 HBox。

HBox placeHolder = new HBox();
tableView.getColumns().forEach(tc->{
    StackPane colHolder = new StackPane();
    colHolder.getStyleClass().add("table-view-place-holder");
    colHolder.prefWidthProperty().bind(tc.widthProperty());
    placeHolder.getChildren().add(colHolder);
});
tableView.setPlaceholder(placeHolder);

CSS实现如下:

.table-view-place-holder{
  -fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #232A2D 49% , #3D4549 12% );
  -fx-border-width: 0px 1px 0px 0px;
  -fx-border-color: linear-gradient(from 50px 14px to 50px 40px , repeat, #3D4549 49% , #232a2d 12% );
}

我要求边框颜色与行背景形成对比。通过上述方法,我可以轻松地为占位符列设置边框颜色。

【讨论】:

    【解决方案3】:

    我找到了 javafx8 的解决方案。它使用了非公共 api,但它不使用反射(幸运的是)。基本上你需要设置(或替换)TableView 的外观并在方法getItemCount() 中返回一个非零值。像这样:

    (TableView)t.setSkin(new TableViewSkin<Object>(t)
        {
            @Override
            public int getItemCount()
            {
                int r = super.getItemCount();
                return r == 0 ? 1 : r;
            }
        });
    

    此方法还可用于在最后一项的底部添加额外的行(例如,如果您想包含添加按钮)。基本上返回总是比实际项目数高一。

    尽管这是一个老问题,但希望这对某人有所帮助。

    【讨论】:

    • 已验证这在 fx8 中看起来不错 - 我注意到的故障只是 fx9。 +1 一个简单的解决方案:)
    【解决方案4】:

    我想我找到了解决办法。这绝对不好,因为它以一种不想要的方式访问 API,而且我可能也在不希望地使用 visibleProperty,但是你去吧:

    您可以尝试破解 TableViewSkin。基本上这样做是为了找回被黑的皮肤:

    public class ModifiedTableView<E> extends TableView<E> {
        @Override
        protected Skin<?> createDefaultSkin() {
            final TableViewSkin<E> skin = new TableViewSkin<E>(this) {
              // override method here
            }
            // modifiy skin here
            return skin;
       }
    }
    

    对于 TableViewSkin,您需要重写以下方法:

    @Override
    protected VirtualFlow<TableRow<E>> createVirtualFlow() {
        final VirtualFlow<TableRow<E>> flow = new VirtualFlow<TableRow<E>>();
        // make the 'scroll-region' always visible:
        flow.visibleProperty().addListener((invalidation) -> {
            flow.setVisible(true);
        });
        return flow;
    }
    

    对于使用反射停止显示占位符的皮肤:

    final Field privateFieldPlaceholderRegion = TableViewSkinBase.class.getDeclaredField("placeholderRegion");
    privateFieldPlaceholderRegion.setAccessible(true);
    final StackPane placeholderRegion = (StackPane) privateFieldPlaceholderRegion.get(skin);
    
    // make the 'placeholder' never visible:
    placeholderRegion.visibleProperty().addListener((invalidation) -> {
        placeholderRegion.setVisible(false);
    });
    

    也许你可以用同样的方法改变流的可见性,使代码更短……但我想你明白了

    【讨论】:

      【解决方案5】:

      这是执行任务的一种棘手方法,

          HBox box = new HBox();
          box.setDisable(true);
          for (TableColumn column : patientsTable.getColumns()) {
              ListView<String> listView = new ListView<>();
              listView.getItems().add("");
              listView.setPrefWidth(column.getWidth());
              box.getChildren().add(listView);
          }
      
          tableView.setPlaceholder(box);
      

      【讨论】:

      • 哈!那是偷偷摸摸的。不幸的是,它在我的表中看起来不太“正确”,它有很多可调整大小的列。不过,对于具有静态列的简单表,这可能会起作用。
      猜你喜欢
      • 1970-01-01
      • 2014-06-15
      • 2018-03-21
      • 1970-01-01
      • 2019-11-17
      • 1970-01-01
      • 2013-07-15
      • 2023-04-09
      相关资源
      最近更新 更多