[javaFX] CellSpanTableView for javaFX 11

Programming/javaFX 2021. 2. 26. 02:25 Posted by 생각하는로뎅
반응형

javaFX TableView에 합치기(병합) 기능이 없다. 그래서 TableVIew를 분석해서 만들어야했다.

 

많은 공개 소스가 있었지만, javaFX 11 이상 대응하는 코드가 없었고, 또한 버그가 너무 많았다.

 

이곳에서 소스를 보고, 수정하였다.

 

SpanTableRowSkin.java에서 Cell들을 복사해서 다시 TableVIew Cell에 넣어주는 방식이었는데, 이렇게하면 Cell이 정상적으로 표현이 안되었다. 또한, TableView를 Reflush 해야하는 상황이 빈번하다면, Cell이 중복해서 나타나는 현상도 있었다.

 

javaFX11에서 쓰도록하고, Cell들을 복제하지 않고 쓰도록 수정하였다.

 

 

1. CellSpan.java

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 + " ] ";
   }
}

 

2. SpanModel.java

public interface SpanModel {
   public CellSpan getCellSpanAt(int rowIndex, int columnIndex);
   
   public boolean isCellSpanEnabled();
}

 

3. SpanTableRowSkin.java

/*
 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableRowSkin;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.Group;

public class SpanTableRowSkin<T> extends TableRowSkin<T> {

    private static enum SpanType {
        NONE,
        COLUMN,
        ROW,
        BOTH,
        UNSET;
    }


    private boolean showColumns = true;

    private TableView<T> tableView;
    
    private SpanTableView<T> cellSpanTableView;
    private SpanModel spanModel;

    // supports variable row heights
    public static double getTableRowHeight(int index, TableRow tableRow) {
        if (index < 0) {
            return DEFAULT_CELL_SIZE;
        }
        
        Group virtualFlowSheet = (Group) tableRow.getParent();
        Node node = tableRow.getParent().getParent().getParent();
        if (node instanceof VirtualFlow) {
            ObservableList<Node> children = virtualFlowSheet.getChildren();
            
            if (index < children.size()) {
                return children.get(index).prefHeight(tableRow.getWidth());
            }
        }
        
        return DEFAULT_CELL_SIZE;
    }
    
    /**
     * Used in layoutChildren to specify that the node is not visible due to spanning.
     */
    private void hide(Node node) {
        node.setManaged(false);
        node.setVisible(false);
    }
    
    /**
     * Used in layoutChildren to specify that the node is now visible.
     */
    private void show(Node node) {
        node.setManaged(true);
        node.setVisible(true);
    }

    private SpanType getSpanType(final int row, final int column) {
        SpanType[][] spanTypeArray;

        int rowCount = tableView.getItems().size();
        int columnCount = tableView.getVisibleLeafColumns().size();
        spanTypeArray = new SpanType[rowCount][columnCount];

        // initialise the array to be SpanType.UNSET
        for (int _row = 0; _row < rowCount; _row++) {
            for (int _column = 0; _column < columnCount; _column++) {
                spanTypeArray[_row][_column] = SpanType.UNSET;
            }
        }
        
        if (spanModel == null || ! spanModel.isCellSpanEnabled()) {
            spanTypeArray[row][column] = SpanType.NONE;
            return SpanType.NONE;
        }

        int distance = 0;
        for (int _col = column - 1; _col >= 0; _col--) {
            distance++;
            CellSpan cellSpan = spanModel.getCellSpanAt(row, _col);
            if (cellSpan == null) continue;
            if (cellSpan.getColumnSpan() > distance) {
                spanTypeArray[row][column] = SpanType.COLUMN;
                return SpanType.COLUMN;
            }
        }
        
        distance = 0;
        for (int _row = row - 1; _row >= 0; _row--) {
            distance++;
            CellSpan cellSpan = spanModel.getCellSpanAt(_row, column);
            if (cellSpan == null) continue;
            if (cellSpan.getRowSpan() > distance) {
                spanTypeArray[row][column] = SpanType.ROW;
                return SpanType.ROW;
            }
        }
        
        int rowDistance = 0;
        int columnDistance = 0;
        for (int _col = column - 1, _row = row - 1; _col >= 0 && _row >= 0; _col--, _row--) {
            rowDistance++;
            columnDistance++;
            CellSpan cellSpan = spanModel.getCellSpanAt(_row, _col);
            if (cellSpan == null) continue;
            if (cellSpan.getRowSpan() > rowDistance && 
                cellSpan.getColumnSpan() > columnDistance) {
                    spanTypeArray[row][column] = SpanType.BOTH;
                    return SpanType.BOTH;
            }
        }
        
        spanTypeArray[row][column] = SpanType.NONE;
        return SpanType.NONE;
    }

    public SpanTableRowSkin(TableRow<T> tableRow) {
    	super(tableRow);
        
        getSkinnable().setPickOnBounds(false);
        this.tableView = tableRow.getTableView();

        cellSpanTableView = (SpanTableView<T>) tableView;
        spanModel = cellSpanTableView.getSpanModel();

    }


    @Override
    protected void layoutChildren(double x, final double y,
            final double w, final double h) {

        if (tableView == null) return;
        
        if (showColumns && ! tableView.getVisibleLeafColumns().isEmpty()) {

            double width;
            double height;
            
            Insets insets = this.getSkinnable().getInsets();
            
            double verticalPadding = insets.getTop() + insets.getBottom();
            double horizontalPadding = insets.getLeft() + insets.getRight();
           
            int row = getSkinnable().getIndex();
            
            if (row < 0 || row >= tableView.getItems().size()) return;
            
            for (int column = 0; column < getChildren().size(); column++) {

            	Node node = getChildren().get(column);
                show(node);
                
                width = snapSize(node.prefWidth(-1)) - snapSize(horizontalPadding);
                height = Math.max(this.getSkinnable().getHeight(), node.prefHeight(-1));
                height = snapSize(height) - snapSize(verticalPadding);
                
                ///////////////////////////////////////////
                // cell spanning code starts here
                ///////////////////////////////////////////
               
                if (spanModel != null && spanModel.isCellSpanEnabled()) {

                    SpanType spanType = getSpanType(row, column);
                    switch (spanType) {
                        case ROW:
                        case BOTH: x += width; // fall through is on purpose here
                        case COLUMN:
                        case NONE:
                        case UNSET:            // fall through and carry on
                    }
                
                    CellSpan cellSpan = spanModel.getCellSpanAt(row, column);

                    if (cellSpan != null) {
                        if (cellSpan.getColumnSpan() > 1) {
                            for (int i = 1, 
                                    colSpan = cellSpan.getColumnSpan(), 
                                    max = getChildren().size() - column; 
                                    i < colSpan && i < max; i++) {
                                Node adjacentNode = getChildren().get(column + i);
                                width += snapSize(adjacentNode.prefWidth(-1));
                            }
                        }
                        
                        if (cellSpan.getRowSpan() > 1) {
                            for (int i = 1; i < cellSpan.getRowSpan(); i++) {
                                double rowHeight = getTableRowHeight(row + i, getSkinnable());
                                height += snapSize(rowHeight);
                            }
                        }
                    } else {
                    	for (int i = 0; i < getChildren().size(); i++) {
                            // calculate the width
                            Node adjacentNode = getChildren().get(i);
                           
                        }
                    }
                } 
                ///////////////////////////////////////////
                // cell spanning code ends here
                ///////////////////////////////////////////
                
                
                node.resize(width, height);
                node.relocate(x, insets.getTop());
                getSkinnable().setPrefHeight(height);
                x += width;

            }
            
        } else {
        	super.layoutChildren(x,y,w,h);
        }
        
    }
    
    private static final double DEFAULT_CELL_SIZE = 24.0;
    
}

 

4. SpanTableView.java

/*
 * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableView;
import javafx.scene.control.skin.TableRowSkin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;

public class SpanTableView<S> extends TableView<S> {

	public SpanTableView() {
		super();
		getStyleClass().add(DEFAULT_STYLE_CLASS);
		//this.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
		//registerCellListener();
	}

	public SpanTableView(ObservableList<S> items) {
		super(items);
		getStyleClass().add(DEFAULT_STYLE_CLASS);
		//this.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
		//registerCellListener();
	}
    
    // --- Span Model
    private ObjectProperty<SpanModel> spanModel 
            = new SimpleObjectProperty<SpanModel>(this, "spanModel");

    public final ObjectProperty<SpanModel> 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<String> stylesheets = scene.getStylesheets();
                String cssPath = SpanTableView.class.getResource("cell-span.css")
						.toExternalForm();
                if (! stylesheets.contains(cssPath)) {
                    stylesheets.add(cssPath);
                    isFirstRun = false;
                }
            }
        }
        
        super.layoutChildren();
    }
    
    
    public void registerCellListener() {
		final TableView.TableViewSelectionModel<S> sm = this.getSelectionModel();
		sm.getSelectedCells().addListener(new ListChangeListener<TablePosition>() {
			@Override
			public void onChanged(Change<? extends TablePosition> change) {
				final SpanTableView<S> tableView = SpanTableView.this;
				if (sm.getSelectedCells().isEmpty()) {
					return;
				}
				final TablePosition tp = sm.getSelectedCells().get(0);
				if (tp == null) {
					return;
				}
				CellSpan cellSpan = tableView.getSpanModel().getCellSpanAt(
						tp.getRow(), tp.getColumn());
				if(null == cellSpan) {
					return;
				}
				final int rowSpan = cellSpan.getRowSpan();
				if(rowSpan < 2) {
					return;
				}
				for(int row = tp.getRow(); row < tp.getRow() + rowSpan; row++) {
					if(sm.isSelected(row)) {
						continue;
					}
					tableView.getSelectionModel().select(row);
				}
			}
		});
	}
    
}

 

5. cell-span.css

.cell-span-table-view > .virtual-flow > .clipped-container > .sheet > .table-row-cell {
    -fx-skin: "{패키지 경로}.SpanTableRowSkin"; /* ex application.tableview.SpanTableRowSkin */
}

 

6. FXML

<SpanTableView  fx:id="tbVideoList" prefHeight="668.0" VBox.vgrow="ALWAYS">

</SpanTableView>

 

7. tableVIew set span

....
tbVideoList.setSpanModel(new SpanModel() {
			int i = 0;
			@Override
			public boolean isCellSpanEnabled() {
				// TODO Auto-generated method stub
				return true;
			}
			
			@Override
			public CellSpan getCellSpanAt(int rowIndex, int columnIndex) {
				// TODO Auto-generated method stub
				
				CellSpan cellSpan = null;
				
				if (tableRowList.get(rowIndex).isHeader()) {
                    // 1 ~ 6 CELL 합침
					cellSpan = new CellSpan(1, 6);
				} else {
					cellSpan = null; // 합치지 않음
				}
				
				return cellSpan;
			}
		});
 ....
반응형