java - 两个 TableView 的同步滚动条不适用于空表
问题描述
在此处使用此示例:
https://stackoverflow.com/a/30509195
非常适合为汇总行创建多个表。我还需要底部表格上可见的滚动条。但是,底部表格的滚动条与主表格不同步(起初内容为空)。当有数据时,滚动条会正确同步。
当您将数据添加到表中,然后删除数据时,滚动条会再次正确同步。所以我知道它们仍然可以与内容为空的表格同步。
这是示例代码(顶部有两个按钮用于添加和清除项目)
package testsummary;
import java.text.Format;
import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableCell;
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.HBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Callback;
/**
* Table with a summary table. The summary table is a 2nd table which is
* synchronized with the primary table.
*
* TODO: + always show vertical bars for both the primary and the summary table,
* otherweise the width of both tables wouldn't be the same + hide the
* horizontal scrollbar of the summary table
*
*/
public class SummaryTableDemo extends Application
{
private TableView<Data> mainTable = new TableView<>();
private TableView<SumData> sumTable = new TableView<>();
private final ObservableList<Data> data
= FXCollections.observableArrayList();
// TODO: calculate values
private final ObservableList<SumData> sumData
= FXCollections.observableArrayList(
new SumData("Sum", 0.0, 0.0, 0.0),
new SumData("Min", 0.0, 0.0, 0.0),
new SumData("Max", 0.0, 0.0, 0.0)
);
final HBox hb = new HBox();
public static void main(String[] args)
{
launch(args);
}
@Override
public void start(Stage stage)
{
Scene scene = new Scene(new Group());
// load css
// scene.getStylesheets().addAll(getClass().getResource("application.css").toExternalForm());
stage.setTitle("Table View Sample");
stage.setWidth(250);
stage.setHeight(550);
// setup table columns
setupMainTableColumns();
setupSumTableColumns();
// fill tables with data
mainTable.setItems(data);
sumTable.setItems(sumData);
// set dimensions
sumTable.setPrefHeight(90);
// bind/sync tables
for (int i = 0; i < mainTable.getColumns().size(); i++)
{
TableColumn<Data, ?> mainColumn = mainTable.getColumns().get(i);
TableColumn<SumData, ?> sumColumn = sumTable.getColumns().get(i);
// sync column widths
sumColumn.prefWidthProperty().bind(mainColumn.widthProperty());
// sync visibility
sumColumn.visibleProperty().bindBidirectional(mainColumn.visibleProperty());
}
// allow changing of column visibility
//mainTable.setTableMenuButtonVisible(true);
// hide header (variation of jewelsea's solution: http://stackoverflow.com/questions/12324464/how-to-javafx-hide-background-header-of-a-tableview)
sumTable.getStyleClass().add("tableview-header-hidden");
// hide horizontal scrollbar via styles
// sumTable.getStyleClass().add("sumtable");
// create container
BorderPane bp = new BorderPane();
Button addButton = new Button("+");
Button clearButton = new Button("X");
addButton.setOnAction((ActionEvent c) ->
{
data.add(new Data(LocalDate.of(2015, Month.JANUARY, 11), 40.0, 50.0, 60.0));
});
clearButton.setOnAction((ActionEvent c) ->
{
data.clear();
});
HBox buttonBar = new HBox(clearButton, addButton);
bp.setTop(buttonBar);
bp.setCenter(mainTable);
bp.setBottom(sumTable);
// fit content
bp.prefWidthProperty().bind(scene.widthProperty());
bp.prefHeightProperty().bind(scene.heightProperty());
((Group) scene.getRoot()).getChildren().addAll(bp);
stage.setScene(scene);
stage.show();
// synchronize scrollbars (must happen after table was made visible)
ScrollBar mainTableHorizontalScrollBar = findScrollBar(mainTable, Orientation.HORIZONTAL);
ScrollBar sumTableHorizontalScrollBar = findScrollBar(sumTable, Orientation.HORIZONTAL);
mainTableHorizontalScrollBar.valueProperty().bindBidirectional(sumTableHorizontalScrollBar.valueProperty());
}
/**
* Primary table column mapping.
*/
private void setupMainTableColumns()
{
TableColumn<Data, LocalDate> dateCol = new TableColumn<>("Date");
dateCol.setPrefWidth(120);
dateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
TableColumn<Data, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setPrefWidth(90);
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setPrefWidth(90);
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setPrefWidth(90);
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
mainTable.getColumns().addAll(dateCol, value1Col, value2Col, value3Col);
}
/**
* Summary table column mapping.
*/
private void setupSumTableColumns()
{
TableColumn<SumData, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<SumData, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
sumTable.getColumns().addAll(textCol, value1Col, value2Col, value3Col);
}
/**
* Find the horizontal scrollbar of the given table.
*
* @param table
* @return
*/
private ScrollBar findScrollBar(TableView<?> table, Orientation orientation)
{
// this would be the preferred solution, but it doesn't work. it always gives back the vertical scrollbar
// return (ScrollBar) table.lookup(".scroll-bar:horizontal");
//
// => we have to search all scrollbars and return the one with the proper orientation
Set<Node> set = table.lookupAll(".scroll-bar");
for (Node node : set)
{
ScrollBar bar = (ScrollBar) node;
if (bar.getOrientation() == orientation)
{
return bar;
}
}
return null;
}
/**
* Data for primary table rows.
*/
public static class Data
{
private final ObjectProperty<LocalDate> date;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public Data(LocalDate date, double value1, double value2, double value3)
{
this.date = new SimpleObjectProperty<LocalDate>(date);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final ObjectProperty<LocalDate> dateProperty()
{
return this.date;
}
public final LocalDate getDate()
{
return this.dateProperty().get();
}
public final void setDate(final LocalDate date)
{
this.dateProperty().set(date);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Data for summary table rows.
*/
public static class SumData
{
private final SimpleStringProperty text;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public SumData(String text, double value1, double value2, double value3)
{
this.text = new SimpleStringProperty(text);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final SimpleStringProperty textProperty()
{
return this.text;
}
public final java.lang.String getText()
{
return this.textProperty().get();
}
public final void setText(final java.lang.String text)
{
this.textProperty().set(text);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Formatter for table cells: allows you to align table cell values
* left/right/center
*
* Example for alignment form
* http://docs.oracle.com/javafx/2/fxml_get_started/fxml_tutorial_intermediate.htm
*
* @param <S>
* @param <T>
*/
public static class FormattedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>>
{
private TextAlignment alignment = TextAlignment.LEFT;
private Format format;
public FormattedTableCellFactory()
{
}
public FormattedTableCellFactory(TextAlignment alignment)
{
this.alignment = alignment;
}
public TextAlignment getAlignment()
{
return alignment;
}
public void setAlignment(TextAlignment alignment)
{
this.alignment = alignment;
}
public Format getFormat()
{
return format;
}
public void setFormat(Format format)
{
this.format = format;
}
@Override
@SuppressWarnings("unchecked")
public TableCell<S, T> call(TableColumn<S, T> p)
{
TableCell<S, T> cell = new TableCell<S, T>()
{
@Override
public void updateItem(Object item, boolean empty)
{
if (item == getItem())
{
return;
}
super.updateItem((T) item, empty);
if (item == null)
{
super.setText(null);
super.setGraphic(null);
} else if (format != null)
{
super.setText(format.format(item));
} else if (item instanceof Node)
{
super.setText(null);
super.setGraphic((Node) item);
} else
{
super.setText(item.toString());
super.setGraphic(null);
}
}
};
cell.setTextAlignment(alignment);
switch (alignment)
{
case CENTER:
cell.setAlignment(Pos.CENTER);
break;
case RIGHT:
cell.setAlignment(Pos.CENTER_RIGHT);
break;
default:
cell.setAlignment(Pos.CENTER_LEFT);
break;
}
return cell;
}
}
}
解决方案
没有解决方案(这可能需要在 VirtualFlow 和/或 TableViewSkin 的内部进行一些实际工作),而是一个肮脏的技巧:在连接滚动条后添加/删除数据
addButton.fire();
Platform.runLater(( ) -> {
clearButton.fire();
});
缺点是短暂但可察觉的闪烁......
更新
经过一番挖掘(“geht-nicht-gibt's-nicht” - 不知道英文成语,sry)我找到了一种即使没有项目也可以强制 VirtualFlow 进入初始布局通道的方法:基本思想是即使没有项目,也要临时设置流的 cellCount > 0。棘手的部分是在皮肤生命中的正确时间执行此操作:仅一次,有时早,但仅在正常布局发生之后。
下面的实现
- 有一个标志来指示是否伪造 itemCount
- 将侦听器中的标志设置为包含窗口的显示属性:这假定在添加到场景图中时默认皮肤的正常设置
- 如果设置了标志,则覆盖 getItemCount 以返回至少 1
- 如果设置了标志,则强制使用假布局的覆盖 layoutChildren
- 强制布局是通过两次调用 updateItemCount 来实现的:一次有标志,一次没有标志
仍然很脏,但更有趣:)
public static class TweakedTableSkin<T> extends TableViewSkin<T> {
private boolean forceNotEmpty = false;
ChangeListener showingListener = (src, ov, nv) -> {
initForceNotEmpty(src);
};
public TweakedTableSkin(TableView<T> control) {
super(control);
Window window = getSkinnable().getScene().getWindow();
if (window != null)
window.showingProperty().addListener(showingListener);
}
/**
* Overridden to force a re-layout with faked itemCount after calling
* super if the fake flag is true.
*/
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (forceNotEmpty) {
forceNotEmptyLayout();
}
}
/**
* Callback from listener installed on window's showing property.
* Implemented to set the forceNotEmpty flag and remove the listener.
*/
private void initForceNotEmpty(ObservableValue src) {
forceNotEmpty = true;
src.removeListener(showingListener);
}
/**
* Enforces a layout pass on the flow with at least one row.
* Resets the forceNotEmpty flag and triggers a second
* layout pass with the correct count.
*/
private void forceNotEmptyLayout() {
if (!forceNotEmpty) return;
updateItemCount();
forceNotEmpty = false;
updateItemCount();
}
/**
* Overridden to return at least 1 if forceNotEmpty is true.
*/
@Override
protected int getItemCount() {
int itemCount = super.getItemCount();
if (forceNotEmpty && itemCount == 0) {
itemCount = 1;
}
return itemCount;
}
}
通过使用覆盖的 createDefaultSkin 扩展 TableView 来使用:
private TableView<Data> mainTable = new TableView<>() {
@Override
protected Skin<?> createDefaultSkin() {
return new TweakedTableSkin<>(this);
}
};
推荐阅读
- javascript - 不要让用户导航或缩小区域 ARCGIS
- php - Laravel:使用带有 where 子句的数据透视表获取单个模型
- javascript - 如何将 3 维张量传递到密集层(tensorflow.js)
- django - 在 Heroku 上部署 Django 后 Chart.js 无法正常工作
- python - 如何使用 tweepy 3.10.0 从特定消息 id 读取 dm 的 twitter 列表
- selenium - Robotframework - Pycharm 没有显示建议
- python - Django:将“名称”传递给 Client.post()
- python - 错误消息:无法对切片索引
使用这些索引器 [True] - javascript - 等待使用 puppeteer 显示 Google Captcha
- ios - Swift IOS UIDatepicker - UIDatepicker 显示不正确