Search code examples
javajavafxtablecolumn

Prevent TableColumn from being resized by user only


Is there a way to prevent any or at least all JavaFX TableColumns in a TableView to be resized by the user, while still allowing a Resize Policy to work?

There exists TableColumn::setResizable, but while preventing a user from resizing a TableColumn, it also prevents the Resize Policy from resizing a TableColumn.


Solution

  • A quick solution could be adding event filter for mouse drag on table header row. Create a custom tableView and add event fitler on header row as below:

    class CustomTableView<S> extends TableView<S>{
            private Node headerRow;
    
            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                if(headerRow ==null){
                    headerRow = (Region) lookup("TableHeaderRow");
                    headerRow.addEventFilter(MouseEvent.MOUSE_DRAGGED, MouseEvent::consume);
                }
            }
        }
    

    Obviously the side effect is you can't realign the columns now. If you are very specific to resizing only, then lookup for the nodes that are responsible for resizing and add the filter on them rather than on the entire header row.

    Below is a quick working demo with disabling the column resizing and realigning while still allowing for Resize Policy.

    import javafx.application.Application;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Node;
    import javafx.scene.Scene;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.layout.Region;
    import javafx.stage.Stage;
    
    public class TableResizeRestrictionDemo extends Application {
        @Override
        public void start(Stage primaryStage) throws Exception {
            ObservableList<Person> persons = FXCollections.observableArrayList();
            persons.add(new Person("Harry","John","LS"));
            persons.add(new Person("Mary","King","MS"));
            persons.add(new Person("Don","Bon","CAT"));
            persons.add(new Person("Pink","Wink","IND"));
    
            CustomTableView<Person> tableView = new CustomTableView<>();
            TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
            fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
    
            TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
            lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
    
            TableColumn<Person, String> cityCol = new TableColumn<>("City");
            cityCol.setCellValueFactory(param -> param.getValue().cityProperty());
    
            tableView.getColumns().addAll(fnCol, lnCol, cityCol);
            tableView.setItems(persons);
            tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
    
            Scene sc = new Scene(tableView);
            primaryStage.setScene(sc);
            primaryStage.show();
    
        }
    
        class Person{
            private StringProperty firstName = new SimpleStringProperty();
            private StringProperty lastName = new SimpleStringProperty();
            private StringProperty city = new SimpleStringProperty();
    
            public Person(String fn, String ln, String cty){
                setFirstName(fn);
                setLastName(ln);
                setCity(cty);
            }
    
            public String getFirstName() {
                return firstName.get();
            }
    
            public StringProperty firstNameProperty() {
                return firstName;
            }
    
            public void setFirstName(String firstName) {
                this.firstName.set(firstName);
            }
    
            public String getLastName() {
                return lastName.get();
            }
    
            public StringProperty lastNameProperty() {
                return lastName;
            }
    
            public void setLastName(String lastName) {
                this.lastName.set(lastName);
            }
    
            public String getCity() {
                return city.get();
            }
    
            public StringProperty cityProperty() {
                return city;
            }
    
            public void setCity(String city) {
                this.city.set(city);
            }
        }
    
        class CustomTableView<S> extends TableView<S>{
            private Node headerRow;
    
            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                if(headerRow ==null){
                    headerRow = (Region) lookup("TableHeaderRow");
                    headerRow.addEventFilter(MouseEvent.MOUSE_DRAGGED, MouseEvent::consume);
                }
            }
        }
    }
    

    Update :: After inspecting the source code in NestedTableColumnHeader class, the nodes that are responsible for resizing are indeed Rectangle(s). Unfortunately there is no style class set to this Rectangle. So assuming that all the rectangles in HeaderRow are for resize purpose, we lookup for all Rectangle nodes and set the event filter.

    Internally PRESSED-DRAGGED-RELEASED handlers are set for resizing and ENTER-EXIT handlers are set for changing the cursor. So rather than setting filters for 5 types of events, it is better to set on one super event MouseEvent.ANY. This will solve the issue of changing the cursor as well.

    class CustomTableView<S> extends TableView<S> {
            private final EventHandler<MouseEvent> consumeEvent = MouseEvent::consume;
    
            @Override
            protected void layoutChildren() {
                super.layoutChildren();
                final Set<Node> dragRects = lookup("TableHeaderRow").lookupAll("Rectangle");
                for (Node dragRect : dragRects) {
                    dragRect.removeEventFilter(MouseEvent.ANY, consumeEvent);
                    dragRect.addEventFilter(MouseEvent.ANY, consumeEvent);
                }
            }
        }
    

    The reason for not keeping the reference of Rectangles (like HeaderRow in demo) is, every time you realign a column a new set of Rectangles are generated. So you cannot rely on any Rectangle reference. To avoid duplicate filters, we create a handler reference, first we remove and then add the handler.

    UPDATE 2:

    As per one of the requirement in the comments, below is the solution to restrict the resizing only for the first and last columns.

    Note that this logic always blocks the current first/last columns in the table. So if a column is reordered, then the new first/last column is restricted for sizing.

    class CustomTableView<S> extends TableView<S> {
        private final EventHandler<MouseEvent> consumeEvent = MouseEvent::consume;
    
        InvalidationListener boundsListener = p -> recompute();
    
        @Override
        protected void layoutChildren() {
            super.layoutChildren();
            final Set<Node> dragRects = lookup("TableHeaderRow").lookupAll("Rectangle");
            // Add listener to boundsInParent property of all rectangles
            dragRects.forEach(node -> {
                node.boundsInParentProperty().removeListener(boundsListener);
                node.boundsInParentProperty().addListener(boundsListener);
            });
        }
    
        private void recompute() {
            final Set<Node> dragRects = lookup("TableHeaderRow").lookupAll("Rectangle");
            final Map<Double, Node> xMap = new HashMap<>();
            for (Node dragRect : dragRects) {
                xMap.put(dragRect.getBoundsInParent().getMinX(), dragRect);
            }
            // Sort the rectangles in the order from left to right (or other way)
            List<Double> xOffset = xMap.keySet().stream().sorted().collect(Collectors.toList());
    
            for (int i = 0; i < xOffset.size(); i++) {
                Node node = xMap.get(xOffset.get(i));
                node.removeEventFilter(MouseEvent.ANY, consumeEvent);
                // Consume event for first and last rectangle.
                if (i == 0 || i == xOffset.size() - 1) {
                    node.addEventFilter(MouseEvent.ANY, consumeEvent);
                }
            }
        }
    }