不幸的是,旧问题在 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 的背景下重新审视了这些黑客行为。发生了变化,好的和坏的:
- 皮肤移动到一个公共包中,现在允许在不访问隐藏类的情况下对它们进行子类化(好)
- 此举引入了一个不允许安装自定义 VirtualFlow 的错误(已在 fx10 中修复)
- 将来某个时候将强烈禁止对隐藏成员的反射访问(阅读:不可能)
在挖掘的过程中,我注意到这些黑客的小故障(注意:我没有针对 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());
}