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

2) Screen after scrolling

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