title: How to merge TableView cells in JavaFX
body: <h2> Hello ! </h2>
  
  <p>I need some help, I'm working on a project with a TableView in JavaFX. And I need to merge some cells like the 
  <a href="https://i.stack.imgur.com/vEmWM.png" rel="nofollow noreferrer">screen</a>
  .</p>
  
  <p>I already have create a Tab with a TableView in it, but I don't know how to merge the row of this TableView.</p>
  
  <p>Here is some part of my code ( But I don't know if I realy need it to merge just some cells) :</p>
  
  <p><strong>CellSpanTableView Class :</strong></p>
  
  <pre><code>public class CellSpanTableView&lt;S&gt; extends TableView&lt;S&gt; {
    public CellSpanTableView() {
      super();
      getStyleClass().add(DEFAULT_STYLE_CLASS);
    }
  
    public CellSpanTableView(ObservableList&lt;S&gt; items) {
      super(items);
      getStyleClass().add(DEFAULT_STYLE_CLASS);
    }
  
    // --- Span Model
    private ObjectProperty&lt;SpanModel&gt; spanModel 
          = new SimpleObjectProperty&lt;SpanModel&gt;(this, "spanModel");
  
    public final ObjectProperty&lt;SpanModel&gt; spanModelProperty() {
      return spanModel;
    }
    public final void setSpanModel(SpanModel value) {
      spanModelProperty().set(value);
    }
    public final SpanModel getSpanModel() {
      return spanModel.get();
    }
  
    private static final String DEFAULT_STYLE_CLASS = "cell-span-table-view";
  
    private boolean isFirstRun = true;
  
    @Override protected void layoutChildren() {
      // ugly hack to enable adding the cell-span.css file to the scenegraph
      // without requiring user intervention
      if (isFirstRun) {
        Scene scene = getScene();
        if (scene != null) {
          ObservableList&lt;String&gt; stylesheets = scene.getStylesheets();
          String cssPath = CellSpanTableView.class.getResource("cell- 
                      span.css").toExternalForm();
          if (! stylesheets.contains(cssPath)) {
            stylesheets.add(cssPath);
            isFirstRun = false;
          }
        }
      }
      super.layoutChildren();
    }
  }
  </code></pre>
  
  <p><strong>CellSpan Class :</strong></p>
  
  <pre><code>public final class CellSpan {
    private final int rowSpan;
    private final int columnSpan;
  
    public CellSpan(int rowSpan, int columnSpan) {
      this.rowSpan = rowSpan;
      this.columnSpan = columnSpan;
    }
    public int getRowSpan() {
      return rowSpan;
    }
    public int getColumnSpan() {
      return columnSpan;
    }
  
    @Override public String toString() {
      return "CellSpan: [ rowSpan: " + rowSpan + ", columnSpan: " + columnSpan + " ] ";
    }
  }
  </code></pre>
  
  <p><strong>SpanModel Class :</strong></p>
  
  <pre><code>public interface SpanModel {
    public CellSpan getCellSpanAt(int rowIndex, int columnIndex);
  
    public boolean isCellSpanEnabled();
  }
  </code></pre>
  
  <p><strong>And the main Class to build the tab :</strong></p>
  
  <pre><code>//Build the tab
  private void buildTab(Tab tab) {
    BorderPane borderpane = buildBorderPane(tab);
    CellSpanTableView tableView = (CellSpanTableView) buildBasicTableView(true);
    borderpane.setCenter(tableView);
    //Install the span model
    tableView.setSpanModel(new SpanModel() {
      private final CellSpan spanTwoRows = new CellSpan(2, 1);
      @Override public CellSpan getCellSpanAt(int rowIndex, int columnIndex) {
        return rowIndex % 3 == 0 &amp;&amp; columnIndex == 1 ? spanTwoRows : null;
      }
      @Override public boolean isCellSpanEnabled() {
        return true;
      }
    });
  }
  
  //Build the borderpane
  private BorderPane buildBorderPane(Tab tab) {
    BorderPane borderpane = new BorderPane();
    borderpane.setPadding(new Insets(10, 10, 10, 10));
    tab.setContent(borderpane);
    return borderpane;
  }
  
  //Build the tableview
  private TableView buildBasicTableView(boolean enableCellSpan) {
    //Column Section
    TableColumn&lt;Row, String&gt; columnSections = new TableColumn&lt;Row, String&gt;("Sections");
    columnSections.setPrefWidth(120);
    columnSections.setCellValueFactory(new PropertyValueFactory&lt;Row, String&gt;("section"));
    columnSections.setCellFactory(column -&gt; EditCell.createStringEditCell());
    columnSections.setOnEditCommit(
      new EventHandler&lt;TableColumn.CellEditEvent&lt;Row, String&gt;&gt;() {
      @Override public void handle(TableColumn.CellEditEvent&lt;Row, String&gt; string) {
        ((Row)string.getTableView().getItems().get(string.getTablePosition().getRow())).setSection(string.getNewValue());
      }
    });
  
    //Column Item
    TableColumn&lt;Row, String&gt; columnItems = new TableColumn&lt;Row, String&gt;("Items");
    columnItems.setPrefWidth(120);
    columnItems.setCellValueFactory(new PropertyValueFactory&lt;Row, String&gt;("item"));
    columnItems.setCellFactory(column -&gt; EditCell.createStringEditCell());
    columnItems.setOnEditCommit(new EventHandler&lt;TableColumn.CellEditEvent&lt;Row, String&gt;&gt;() {
      @Override public void handle(TableColumn.CellEditEvent&lt;Row, String&gt; string) {
        ((Row)string.getTableView().getItems().get(string.getTablePosition().getRow())).setItem(string.getNewValue());
      }
    });
  
    //Column level
    TableColumn&lt;Row, String&gt; columnLevels = new TableColumn&lt;Row, String&gt;("Niveaux");
    columnLevels.setPrefWidth(120);
  c  olumnLevels.setCellValueFactory(new PropertyValueFactory&lt;Row, String&gt;("level"));
    columnLevels.setCellFactory(column -&gt; EditCell.createStringEditCell());
    columnLevels.setOnEditCommit(
    new EventHandler&lt;TableColumn.CellEditEvent&lt;Row, String&gt;&gt;() {
      @Override public void handle(TableColumn.CellEditEvent&lt;Row, String&gt; string) {
        ((Row)string.getTableView().getItems().get(string.getTablePosition().getRow())).setLevel(string.getNewValue());
      }
    });
  
    //Create a TableView
    TableView&lt;Row&gt; tableView = enableCellSpan ? new CellSpanTableView&lt;Row&gt;() : new TableView&lt;Row&gt;();
    tableView.getColumns().addAll(columnSections, columnItems, columnLevels);
  
    // insert the test data
    tableView.setItems(FXCollections.observableArrayList(
      new Row("section 1", "item 1", "level 1"),
      new Row("section 2", "item 2", "level 2"),
      new Row("section 3", "item 3", "level 3")
    ));
  
    // for exploration, we enable single cell selection mode, and also print out
    // some debug output to show the currently selected table position
    final TableView.TableViewSelectionModel&lt;Row&gt; sm = tableView.getSelectionModel();
    sm.getSelectedCells().addListener(new ListChangeListener&lt;TablePosition&gt;() {
      @Override public void onChanged(Change&lt;? extends TablePosition&gt; change) {
        if (sm.getSelectedCells().isEmpty()) {
          System.out.println("No selection");
        } else {
          TablePosition tp = sm.getSelectedCells().get(0);
          if (tp == null) return;
          System.out.println("Selection: [ row: " + tp.getRow() + 
                 ", column: " + tp.getColumn() + 
                 ", column name: " + tp.getTableColumn().getText() + " ] ");
        }
      }
    });
    return tableView;
  }
  public static void main(String[] args) {
    launch(args);
  }
  </code></pre>
  
  <p>Best regards,
  Adalvy</p>