首页 > 解决方案 > 两个 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;
        }
    }

}

标签: javajavafx

解决方案


没有解决方案(这可能需要在 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);
    }

};

推荐阅读