如何绕过JavaFX的TableView“占位符”?

java javafx-2 javafx tableview javafx-8

2837 观看

5回复

983 作者的声誉

JavaFX TableView有一个占位符属性,它基本上是NodeTableView空的时候显示的。如果此属性设置为null(其默认值),则它显示为一个Label或其他一些基于Node“表中没有内容”的文本。

但是如果表中有任何数据行,则占位符将Node消失,并且整个垂直空间TableView将填充行,如果没有足够的数据填充整个表,则包括空行。

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

我宁愿不做什么事情,就像TableView在它应该实际上是空的时候放一个空洞的行。

作者: Xanatos 的来源 发布者: 2013 年 6 月 7 日

回应 (5)


1

2633 作者的声誉

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

    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);
作者: Shreyas Dave 发布者: 18.12.2013 06:10

2

163 作者的声誉

我想我找到了解决方案。这绝对不是很好,因为它以一种不想要的方式访问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);
});

也许你可以用同样的方法改变流的可见性,使代码更短......但我认为你得到的概念

作者: terix2k11 发布者: 18.12.2014 09:59

2

985 作者的声誉

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

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

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

虽然这是一个老问题,但希望这对某人有帮助。

作者: n247s 发布者: 24.01.2018 11:08

2

44348 作者的声誉

不幸的是,旧的问题仍未在fx9和fx10中修复。所以在fx9的背景下重新审视了黑客攻击。有变化,好的和坏的:

  • 皮肤移动到一个公共包,现在允许子类化它们而不访问隐藏的类(好)
  • 此举引入了一个不允许安装自定义VirtualFlow 的bug(在fx10中修复)
  • 将来某个时候强烈禁止对隐藏成员的反思性访问(阅读:不可能)

在挖掘的时候,我发现黑客的故障很少(注意:我没有针对fx8运行它们,所以这些可能是由于fx8与fx9的差异!)

  • 占位符/流的强制输入/可见性工作正常,除非在启动时使用空表(显示占位符)并在空时放大表(“新”区域看起来为空)
  • 伪造itemCount为非空,让行按下导航键消失(这可能不是一个大问题,因为用户往往不会导航空表) - 这绝对是在fx9中引入的,在fx8中运行正常

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

自定义皮肤:

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

}
作者: kleopatra 发布者: 25.01.2018 03:02

0

569 作者的声誉

如果任何人仍在寻找除了其他人提供的替代解决方案之外的其他解决方案,下面是我与之合作的解决方案。就我而言,这是我可以采用的最简单的方法。

使用类似于备用行着色的自定义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。

在此输入图像描述

作者: Sai Dandem 发布者: 18.11.2018 10:21
32x32