position of TableColumn in Scene

1.6k Views Asked by At

I have a TableView with several TableColumns and I want to place a Node below a certain TableColumn. How do I get the exact position (x,y-coordinates) of the TableColumn so I can bind the translate properties of my node?

Here is a snippet of how I placed a button on the top right corner of my TableView:

button.translateXProperty().unbind();
button.translateXProperty().bind(tableView.widthProperty().divide(2.0).subtract(button.getWidth() / 2.0 + 2.0) + tableView.localToScene(0.0, 0.0).getX());

This works fine, but obviously only for the TableView. The TableColumns don't have those translate properties or the localToScene methods, so I can't directly get the position to which I would like to bind my Node.

My current solution (which doesn't really work that well) is to do the following: I read out the position of my TableView in the Scene (PointA) and then go through the list of all columns (tableView.getColumns()) and check if each of them is visible, and if so, add their width to the X-value of PointA. I do this until I find the actual column that I want to place the Node below. Now the problem is, that I can't really just bind the Nodes position to this point, because when I change the order of the columns, or make one of them invisible, my column changes position on the screen. I would have to add a listener to the column order and visibility...

Is there any more efficient way to do what I want? :D

2

There are 2 best solutions below

4
On BEST ANSWER

If you are allowed to use non-public api, you might consider to access the TableColumnHeader via its skin, provided it's of type TableViewSkinBase: it has api to access the TableRowHeader which is the container of all TableColumnHeaders and has api to find the header for any column it contains.

Code snippet (the width/location binding is copied from James' example, just to the header instead of the label)

private void buttonsPerHeader(TableView<Person> table, Pane root) {
    if (!(table.getSkin() instanceof TableViewSkinBase)) return;
    TableViewSkinBase skin = (TableViewSkinBase) table.getSkin();
    TableHeaderRow headerRow = skin.getTableHeaderRow();
    for (TableColumn col : table.getColumns()) {
        TableColumnHeader header = headerRow.getColumnHeaderFor(col);
        Button button = new Button(col.getText());

        button.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> 
            header.getBoundsInLocal().getWidth(), header.boundsInLocalProperty()));
        button.minWidthProperty().bind(button.prefWidthProperty());
        button.maxWidthProperty().bind(button.prefWidthProperty());

        button.layoutXProperty().bind(Bindings.createDoubleBinding(() -> 
            header.getLocalToSceneTransform().transform(header.getBoundsInLocal()).getMinX(),
            header.boundsInLocalProperty(), header.localToSceneTransformProperty()));

        button.layoutYProperty().bind(Bindings.createDoubleBinding(() ->
            table.getBoundsInParent().getMaxY() ,table.boundsInParentProperty()));

        root.getChildren().add(button);
    }

}
2
On

I generally dislike using lookups, but you can retrieve the label that is used to display the column header using the lookup .table-view .column-header .label and then bind your button's layout properties using the bounds of that label.

Example:

import java.util.Optional;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class TableColumnLocationExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Person> table = new TableView<>();
        table.getColumns().add(column("First Name", Person::firstNameProperty, 120));
        table.getColumns().add(column("Last Name", Person::lastNameProperty, 120));
        table.getColumns().add(column("Email", Person::emailProperty, 250));

        table.getItems().addAll(
                new Person("Jacob", "Smith", "[email protected]"),
                new Person("Isabella", "Johnson", "[email protected]"),
                new Person("Ethan", "Williams", "[email protected]"),
                new Person("Emma", "Jones", "[email protected]"),
                new Person("Michael", "Brown", "[email protected]")        
        );

        Pane root = new Pane(table);
        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();


        for (TableColumn<Person, ?> col : table.getColumns()) {
            Optional<Label> header = findLabelForTableColumnHeader(col.getText(), root);
            header.ifPresent(label ->  {
                Button button = new Button(col.getText());

                button.prefWidthProperty().bind(Bindings.createDoubleBinding(() -> 
                    label.getBoundsInLocal().getWidth(), label.boundsInLocalProperty()));
                button.minWidthProperty().bind(button.prefWidthProperty());
                button.maxWidthProperty().bind(button.prefWidthProperty());

                button.layoutXProperty().bind(Bindings.createDoubleBinding(() -> 
                    label.getLocalToSceneTransform().transform(label.getBoundsInLocal()).getMinX(),
                    label.boundsInLocalProperty(), label.localToSceneTransformProperty()));

                button.layoutYProperty().bind(Bindings.createDoubleBinding(() ->
                    table.getBoundsInParent().getMaxY() ,table.boundsInParentProperty()));

                root.getChildren().add(button);

            });
        }


    }

    private Optional<Label> findLabelForTableColumnHeader(String text, Parent root) {
        return root.lookupAll(".table-view .column-header .label")
                .stream()
                .map(Label.class::cast)
                .filter(label -> label.getText().equals(text))
                .findAny(); // assumes all columns have unique text...
    }



    private <S,T> TableColumn<S,T> column(String title, Function<S,ObservableValue<T>> property, double width) {
        TableColumn<S,T> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        col.setPrefWidth(width);
        return col ;
    }

    public static class Person {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();
        private StringProperty email = new SimpleStringProperty();

        public Person(String firstName, String lastName, String email) {
            setFirstName(firstName);
            setLastName(lastName);
            setEmail(email);
        }

        public final StringProperty firstNameProperty() {
            return this.firstName;
        }

        public final String getFirstName() {
            return this.firstNameProperty().get();
        }

        public final void setFirstName(final String firstName) {
            this.firstNameProperty().set(firstName);
        }

        public final StringProperty lastNameProperty() {
            return this.lastName;
        }

        public final String getLastName() {
            return this.lastNameProperty().get();
        }

        public final void setLastName(final String lastName) {
            this.lastNameProperty().set(lastName);
        }

        public final StringProperty emailProperty() {
            return this.email;
        }

        public final String getEmail() {
            return this.emailProperty().get();
        }

        public final void setEmail(final String email) {
            this.emailProperty().set(email);
        }


    }

    public static void main(String[] args) {
        launch(args);
    }
}