Scrolling issues in JavaFX Tableview and ScrollPane

I have been using JavaFX since 2 years ago. Now I am creating a spreadsheet such as control using JavaFX. To create the control, I use TableView and ScrollPane with the ListView control for the table REST header, since JavaFX does not provide support for row headers. Everything works fine, except for one thing, when I look at the table, initially the table and scroll lines scroll synchronously, but after some scrolling of its initial inconsistency. I have attached screenshots of the same.

1) Start screen without scrolling

enter image description here

2) Screen after scrolling

enter image description here

Below is the code snippet for this control that I pasted into pastebin.

1) Cells.java

import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; public class Cells extends Application { public void start(Stage stage) { stage.setScene(new Scene(new SpreadSheet(100, 26))); stage.setTitle("Cells"); stage.setWidth(400); stage.setHeight(400); stage.show(); } public static void main(String[] args) { launch(args); } } 

2) SpreadSheet.java

 import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.HBox; public class SpreadSheet extends HBox { public SpreadSheet(int height, int width) { super(); Model model = new Model(height, width); TableView<ObservableList<Model.Cell>> table = new TableView<>(); table.setEditable(true); table.setItems(model.getCellsAsObservableList()); for (char w = 'A'; w < 'A'+width; w++) { TableColumn<ObservableList<Model.Cell>, String> column = new TableColumn<>(w+""); column.setSortable(false); column.setMinWidth(50); column.setCellFactory(TextFieldTableCell.forTableColumn()); final char w0 = w; column.setCellValueFactory(param -> param.getValue().get(w0-'A').text); column.setOnEditStart(event -> { int row = event.getTablePosition().getRow(); int col = event.getTablePosition().getColumn(); Model.Cell c = model.getCells()[row][col]; c.setShowUserData(true); }); column.setOnEditCommit(event -> { int row = event.getTablePosition().getRow(); int col = event.getTablePosition().getColumn(); Model.Cell c = model.getCells()[row][col]; System.out.println("Hello"); c.userData.set(event.getNewValue()); c.setShowUserData(false); }); table.getColumns().add(column); } ListView<String> rowHeaders = new ListView<>(); rowHeaders.getItems().add(""); for (int i = 0; i < height; i++) { rowHeaders.getItems().add(i+""); } ScrollPane scrolledRowHeaders = new ScrollPane(rowHeaders); scrolledRowHeaders.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrolledRowHeaders.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); table.getChildrenUnmodifiable().addListener((ListChangeListener<Node>) c -> { ScrollBar vbarTable = (ScrollBar) table.lookup(".scroll-bar:vertical"); ScrollBar vbarRowHeaders = (ScrollBar) scrolledRowHeaders.lookup(".scroll-bar:vertical"); if (vbarRowHeaders != null && vbarTable != null) vbarTable.valueProperty().bindBidirectional(vbarRowHeaders.valueProperty()); }); getChildren().addAll(scrolledRowHeaders, table); } } 

3) Model.java

 import java.util.List; import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; class Model { private Cell[][] cells; Model(int height, int width) { cells = new Cell[height][width]; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { cells[i][j] = new Cell(); } } } public Cell[][] getCells() { return cells; } public ObservableList<ObservableList<Cell>> getCellsAsObservableList() { ObservableList<ObservableList<Cell>> cs = FXCollections.observableArrayList(); for (int i = 0; i < cells.length; i++) { cs.add(FXCollections.observableArrayList()); for (int j = 0; j < cells[i].length; j++) { cs.get(i).add(cells[i][j]); } } return cs; } class Cell { public final StringProperty userData = new SimpleStringProperty(""); public final StringProperty text = new SimpleStringProperty(""); ObservableValue<Double>[] toArray(List<ObservableValue<Double>> l) { return l.toArray(new ObservableValue[l.size()]); } // Has same problem // public ObservableValue<Double> value = EasyBind.map(userData, Parser::parse) // .flatMap(f -> Bindings.createObjectBinding(() -> f.eval(Model.this), toArray(f.getReferences(Model.this)))); // Has same problem public ObservableValue<Double> value = Bindings.createObjectBinding(() -> { System.out.println(System.currentTimeMillis()); Formula f = Parser.parse(userData.get()); ObservableValue<Double>[] fs = toArray(f.getReferences(Model.this)); Binding<Double> d = Bindings.createObjectBinding(() -> { double v = f.eval(Model.this); // text.set(String.valueOf(v)); return v; }, fs); d.addListener((v, o, n) -> { // ??? }); return d.getValue(); }, userData); public void setShowUserData(Boolean b) { if (b) text.setValue(userData.get()); else text.setValue(String.valueOf(value.getValue())); } } } 

4) Parser.java

 import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; class Parser { private static Parser instance = new Parser(); private static Tokenizer tokenizer; static { tokenizer = new Tokenizer(); tokenizer.add("[a-zA-Z_]\\d+", Token.CELL); tokenizer.add("[a-zA-Z_]\\w*", Token.IDENT); tokenizer.add("-?\\d+(\\.\\d*)?", Token.DECIMAL); tokenizer.add("=", Token.EQUALS); tokenizer.add(",", Token.COMMA); tokenizer.add(":", Token.COLON); tokenizer.add("\\(", Token.OPEN_BRACKET); tokenizer.add("\\)", Token.CLOSE_BRACKET); } public static Formula parse(String formulaString) { return instance.parseFormula(formulaString); } String formulaString; LinkedList<Token> tokens; Token lookahead; private Parser() {} private Formula parseFormula(String formulaString) { this.formulaString = formulaString; try { tokenizer.tokenize(formulaString.replaceAll("\\s+","")); } catch (ParseError e) { System.out.println(e.getMessage()); } this.tokens = tokenizer.getTokens(); if (tokens.isEmpty()) return Formula.Empty; lookahead = this.tokens.getFirst(); return formula(); } private Formula formula() { switch(lookahead.token) { case Token.DECIMAL: String n = lookahead.sequence; nextToken(); return new Number(Double.parseDouble(n)); case Token.EQUALS: nextToken(); return expression(); case Token.EPSILON: return Formula.Empty; default: return new Textual(formulaString); } } private Formula expression() { switch(lookahead.token) { case Token.CELL: int c = lookahead.sequence.charAt(0) - 'A'; int r = Integer.parseInt(lookahead.sequence.substring(1)); nextToken(); if (lookahead.token == Token.COLON) { // Range nextToken(); if (lookahead.token == Token.CELL) { int c2 = lookahead.sequence.charAt(0) - 'A'; int r2 = Integer.parseInt(lookahead.sequence.substring(1)); nextToken(); return new Range(new Coord(r, c), new Coord(r2, c2)); } else { throw new ParseError("Incorrect Range: " + lookahead.sequence); } } else { return new Coord(r, c); } case Token.DECIMAL: Double d = Double.parseDouble(lookahead.sequence); nextToken(); return new Number(d); case Token.IDENT: return application(); default: throw new ParseError("Incorrect Expression: " + lookahead.sequence); } } private Formula application() { String opName = lookahead.sequence; nextToken(); if (lookahead.token != Token.OPEN_BRACKET) throw new ParseError("No opening bracket: " + opName); nextToken(); List<Formula> args = new ArrayList<Formula>(); while (true) { if (lookahead.token == Token.EPSILON) throw new ParseError("No closing bracket"); args.add(expression()); if (lookahead.token == Token.COMMA) nextToken(); if (lookahead.token == Token.CLOSE_BRACKET) return new Application(opName, args); } } private void nextToken() { tokens.pop(); if (tokens.isEmpty()) lookahead = new Token(Token.EPSILON, ""); else lookahead = tokens.getFirst(); } } class ParseError extends RuntimeException { ParseError(String message) { super(message); } } class Token { public static final int EPSILON = 0; public static final int EQUALS = 1; public static final int IDENT = 2; public static final int DECIMAL = 3; public static final int OPEN_BRACKET = 4; public static final int CLOSE_BRACKET = 5; public static final int COMMA = 6; public static final int COLON = 7; public static final int CELL = 8; public final int token; public final String sequence; public Token(int token, String sequence) { this.token = token; this.sequence = sequence; } } class Tokenizer { private LinkedList<TokenInfo> tokenInfos; private LinkedList<Token> tokens; public Tokenizer() { tokenInfos = new LinkedList<TokenInfo>(); tokens = new LinkedList<Token>(); } public void add(String regex, int token) { tokenInfos.add(new TokenInfo(Pattern.compile("^("+regex+")"), token)); } public void tokenize(String s) { tokens.clear(); while (!s.equals("")) { boolean match = false; for (TokenInfo info : tokenInfos) { Matcher m = info.regex.matcher(s); if (m.find()) { match = true; String tok = m.group().trim(); tokens.add(new Token(info.token, tok)); s = m.replaceFirst(""); break; } } if (!match) throw new ParseError("Unexpected char in input: " + s); } } public LinkedList<Token> getTokens() { return tokens; } private static class TokenInfo { public final Pattern regex; public final int token; public TokenInfo(Pattern regex, int token) { super(); this.regex = regex; this.token = token; } } } 

5) Formula.java

 import javafx.beans.value.ObservableValue; import javafx.scene.control.Cell; import java.util.*; abstract class Formula { public static final Formula Empty = new Textual(""); public double eval(Model env) { return 0.0; } public List<ObservableValue<Double>> getReferences(Model env) { return Collections.emptyList(); } } class Textual extends Formula { String value; public Textual(String value) { this.value = value; } public String toString() { return value; } } class Number extends Formula { double value; public Number(double value) { this.value = value; } public String toString() { return String.valueOf(value); } public double eval(Model env) { return value; } } class Coord extends Formula { int row, column; public Coord(int row, int column) { this.row = row; this.column = column; } public String toString() { return ((char)('A'+column))+""+row; } public double eval(Model env) { return env.getCells()[row][column].value.getValue(); } public List<ObservableValue<Double>> getReferences(Model env) { List<ObservableValue<Double>> result = new ArrayList<>(1); result.add(env.getCells()[row][column].value); return result; } } class Range extends Formula { Coord coord1, coord2; public Range(Coord coord1, Coord coord2) { this.coord1 = coord1; this.coord2 = coord2; } public String toString() { return String.valueOf(coord1)+":"+String.valueOf(coord2); } public double eval(Model env) { throw new RuntimeException("Range cannot be evaluated!"); } public List<ObservableValue<Double>> getReferences(Model env) { List<ObservableValue<Double>> result = new ArrayList<>(); for (int r = coord1.row; r <= coord2.row; r++) { for (int c = coord1.column; c <= coord2.column; c++) { result.add(env.getCells()[r][c].value); } } return result; } } class Application extends Formula { String function; List<Formula> arguments; public Application(String function, List<Formula> arguments) { this.function = function; this.arguments = arguments; } public String toString() { StringBuilder t = new StringBuilder(); t.append(function); t.append("("); for (int i = 0; i < arguments.size()-1; i ++) { t.append(arguments.get(i).toString()); t.append(", "); } if (!arguments.isEmpty()) t.append(arguments.get(arguments.size()-1).toString()); t.append(")"); return t.toString(); } public double eval(Model env) { try { List<Double> argvals = evalList(arguments, env); return opTable.get(function).eval(argvals); } catch(Exception e) { return Double.NaN; } } public List<ObservableValue<Double>> getReferences(Model env) { List<ObservableValue<Double>> result = new ArrayList<>(); for (Formula argument : arguments) { result.addAll(argument.getReferences(env)); } return result; } private static List<Double> evalList(List<Formula> args, Model env) { List<Double> result = new ArrayList<>(); for (Formula f : args) { if (f instanceof Range) { for (ObservableValue<Double> c : f.getReferences(env)) { result.add(c.getValue()); } } else { result.add(f.eval(env)); } } return result; } private static Map<String, Op> opTable = new HashMap<>(); static { opTable.put("add", vals -> vals.get(0) + vals.get(1)); opTable.put("sub", vals -> vals.get(0) - vals.get(1)); opTable.put("div", vals -> vals.get(0) / vals.get(1)); opTable.put("mul", vals -> vals.get(0) * vals.get(1)); opTable.put("mod", vals -> vals.get(0) % vals.get(1)); opTable.put("sum", vals -> { double accum = 0; for (Double i : vals) { accum += i; } return accum; }); opTable.put("prod", vals -> { double accum = 1; for (Double i : vals) { accum *= i; } return accum; }); } private static interface Op { public double eval(List<Double> vals); } } 
+7
java javafx
source share
1 answer

try it

For your ListView get a VBar and bind it with a TableView VBar , and your alignment will be intact.

For your ListView VBar run this code for TableView VBar

 VirtualFlow tvVF = (VirtualFlow) TableView.lookup("#virtual-flow"); for (Node n : tvVF.getChildrenUnmodifiable()) { if (n.getClass().isAssignableFrom(VirtualScrollBar.class)) { VirtualScrollBar table_vsb = (VirtualScrollBar) n; if (tvVF.getWidth() - table_vsb.getWidth() > tvVF.getWidth() / 2) { //table_vsb is your Vertical bar for the TableView ....//close braces VirtualFlow lvVF = (VirtualFlow) ListView.lookup("#virtual-flow"); // we do the same for listview for (Node c : lvVF.getChildrenUnmodifiable()) { if (c.getClass().isAssignableFrom(VirtualScrollBar.class)) { VirtualScrollBar list_vsb = (VirtualScrollBar) c; if (tvVF.getWidth() - vsb.getWidth() > tvVF.getWidth() / 2) { //list_vsb is your vbar for the listview 

now since you have two VBar , bind the ListView to the TableView as follows

 list_vsb.valueProperty().bind(table_vsb.valueProperty()); 

Please note that you need to call these codes after the full layout or when Stage.show (); called safe

Hope it is clear and help

EDIT

enter image description here [ enter image description here ] [ enter image description here ] 3

it works for me :)

0
source share

All Articles