How can I bypass the JavaFX's TableView “placeholder”?

匿名 (未验证) 提交于 2019-12-03 01:08:02

问题:

JavaFX's TableView has a placeholder property that is basically a Node that gets displayed in the TableView whenever it is empty. If this property is set to null (its default value), it appears as a Label or some other text based Node that says "There is no content in the table."

But if there are any rows of data in the table, then the placeholder Node disappears and the entire vertical space in the TableView gets filled with rows, including empty rows if there isn't enough data to fill the whole table.

These empty rows are what I want, even when the table is empty. In other words, I don't want to use the placeholder at all. Does anyone know how I can do this?

I'd rather not do something kludgey like put a empty-looking row in the TableView whenever it's supposed to be actually empty.

回答1:

I think I found a solution. It is definitely not nice, since it is accessing the API in a not wanted way, and I'm probably also making undesired use of the visibleProperty, but here you go:

You can try to hack the TableViewSkin. Basically do this to retrieve a hacked Skin:

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

For the TableViewSkin you then need to override following method:

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

And for the skin using reflection stop showing the placeholder:

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

Maybe you can change the visibility of the flow in the same method to make the code shorter... But I think you get the concept



回答2:

Unfortunately, the old issue is still not fixed in fx9 nor fx10. So revisited the hacks in the context of fx9. There had been changes, good and bad ones:

  • Skins moved into a public package which now allows to subclass them without accessing hidden classes (good)
  • the move introduced a bug which doesn't allow to install a custom VirtualFlow (fixed in fx10)
  • reflective access to hidden members will be strongly disallowed (read: not possible) sometime in future

While digging, I noticed ever so slight glitches with the hacks (note: I did not run them against fx8, so these might be due differences in fx8 vs fx9!)

  • the forced in-/visibility of placeholder/flow worked fine except when starting up with an empty table (placeholder was shown) and enlarging the table while empty (the "new" region looks empty)
  • faking the itemCount to not-empty lets the rows dissappear on pressing navigation keys (which probably is not a big problem because users tend to not navigate an empty table) - this is definitely introduced in fx9, working fine in fx8

So I decided to go with the visibility enforcement: the reason for the slight glitches is that layoutChildren doesn't layout the flow if it thinks the placeholder is visible. That's handled by including the flow in the layout if super didn't.

The custom skin:

/**  * 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).  * 

* * Updated for fx9 plus ensure to update the layout of the flow as * needed. * * @author Jeanette Winzenburg, Berlin */ public class NoPlaceHolderTableViewSkin extends TableViewSkin{ private VirtualFlow> flowAlias; private TableHeaderRow headerAlias; private Parent placeholderRegionAlias; private ChangeListener visibleListener = (src, ov, nv) -> visibleChanged(nv); private ListChangeListener childrenListener = c -> childrenChanged(c); /** * Instantiates the skin. * @param table the table to skin. */ public NoPlaceHolderTableViewSkin(TableView 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 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}

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

Usage example:

public class EmptyPlaceholdersInSkin extends Application {      private Parent createContent() {         // initially populated         //TableView table = new TableView(Person.persons()) {         // initially empty         TableView table = new TableView() {              @Override             protected Skin> createDefaultSkin() {                 return new NoPlaceHolderTableViewSkin(this);             }          };         TableColumn 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());  } 


回答3:

Here is a tricky way to perform your task,

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


回答4:

I found a solution for javafx8. It makes use of the non-public api, but it uses no reflection (luckly). Basically you need to set (or replace) the skin of the TableView and return a non-zero value in the method getItemCount(). Like so:

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

This method can also be used to add an extra row at the bottom of your last item (for if you want to include an add button for example). Basically return always one higher than the actual item-count.

Eventhough this is an old question, hopefully this was helpfull to someone.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!