1

I want to resize some TableView columns, since a method to achieve this is missing from javafx i managed to find a solution exposed in MainApp.GUIUtil.fitColumns(TableView tableView).
My problem is: this solution works fine when called by a user action, but i can't find a way to run this method at startup before any user intervention is made.
I want to present the table with the column fitted as soos as the table is shown.
As you can see I intercept the Exception which is causing me headaches (PersonTableController.setMainApp line 32), print the stacktrace and then let the program continue to prove the fit method works after control is given to the user.
How can I fit the columns sizes by code as soon as the tableview is shown?

    Exception in Application start method
java.lang.reflect.InvocationTargetException
    ...
Caused by: java.lang.RuntimeException: Exception in Application start method
    ...
Caused by: java.lang.NullPointerException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at columnstofit.MainApp$GUIUtil.fitColumns(MainApp.java:120)
    at columnstofit.PersonTableController.setMainApp(PersonTableController.java:35)
    at columnstofit.MainApp.showPersonTable(MainApp.java:79)
    at columnstofit.MainApp.start(MainApp.java:65)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    ... 1 more
Exception running application columnstofit.MainApp

Here is my code:

package columnstofit;

import com.sun.javafx.scene.control.skin.TableViewSkin;
import java.awt.AWTException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class MainApp extends Application {

    private       PersonTableController controller;
    public static Stage                 primaryStage;
                  AnchorPane            personTable;

    private ObservableList<Person> personData = FXCollections.observableArrayList();

    /**
     * Constructor
     */
    public MainApp() {
        // i am entering this name just to force the resizing of the column
        personData.add(new Person("Hansgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", "Muster"));
        personData.add(new Person("Ruth", "Mueller"));
        personData.add(new Person("Heinz", "Kurz"));
        personData.add(new Person("Cornelia", "Meier"));
        personData.add(new Person("Werner", "Meyer"));
        personData.add(new Person("Lydia", "Kunz"));
        personData.add(new Person("Anna", "Best"));
        personData.add(new Person("Stefan", "Meier"));
    }

    /**
     * Returns the data as an observable list of Persons. 
     * @return
     */
    public ObservableList<Person> getPersonData() {
        return personData;
    }

    @Override
    public void start(Stage primaryStage) throws AWTException  {

        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("Names Table");

        showPersonTable();
    }

    public void showPersonTable() throws AWTException {

        try 
        {
            // Load root layout from fxml file.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(MainApp.class.getResource("PersonTable.fxml"));
            personTable = (AnchorPane) loader.load();

            // Give the controller access to the main app.
            controller = loader.getController();
            controller.setMainApp(this);

            Scene scene = new Scene(personTable);
            primaryStage.setScene(scene);
            primaryStage.show();
        } 
        catch (IOException e) { e.printStackTrace(); }
    }

    /**
     * Returns the main stage.
     * @return
     */
    public Stage getPrimaryStage() {
        return primaryStage;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }


    public static class GUIUtil {
        private static Method columnToFitMethod;

        static 
        {
            try 
            {
                columnToFitMethod = TableViewSkin.class.getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
                columnToFitMethod.setAccessible(true);
            } 
            catch (NoSuchMethodException e) {e.printStackTrace();}
        }

        public static void fitColumns(TableView tableView) {
            for (Object column : tableView.getColumns()) 
            {
                try { columnToFitMethod.invoke(tableView.getSkin(), column, -1); } 
                catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }
            }
        }
    }

    public class Person {
        private final StringProperty firstName;
        private final StringProperty lastName;

        public Person() {
            this(null, null);
        }

        public Person(String firstName, String lastName) {
            this.firstName  = new SimpleStringProperty(firstName);
            this.lastName   = new SimpleStringProperty(lastName);
        }

        public String getFirstName() {
            return firstName.get();
        }

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

        public StringProperty firstNameProperty() {
            return firstName;
        }

        public String getLastName() {
            return lastName.get();
        }

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

        public StringProperty lastNameProperty() {
            return lastName;
        }
    }
}  

And here is the FXML file:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane id="AnchorPane" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="columnstofit.PersonTableController">
    <TableView fx:id="tableView" layoutX="-39.0" layoutY="39.0" onKeyPressed="#fitColumns" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
                    <columns>
                      <TableColumn fx:id="firstNameColumn" editable="false" minWidth="-1.0" prefWidth="-1.0" text="First Name" />
                      <TableColumn fx:id="lastNameColumn" editable="false" minWidth="-1.0" prefWidth="-1.0" text="Last Name" />
                    </columns>
                     <columnResizePolicy>
                        <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
                     </columnResizePolicy>
                  </TableView>
</AnchorPane>

With his respective controller:

package columnstofit;

import java.awt.AWTException;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;

    /**
    * FXML Controller class
    */
    public class PersonTableController {

        // Reference to the main application.
        private MainApp mainApp;

        @FXML
        private TableColumn<MainApp.Person, String> firstNameColumn;
        @FXML
        private TableColumn<MainApp.Person, String> lastNameColumn;
        @FXML
        private TableView                           tableView;

        public PersonTableController() {
        }

        public void setMainApp(MainApp mainApp) throws AWTException {
           this.mainApp = mainApp;
           // Add observable list data to the table
            tableView.setItems(mainApp.getPersonData());
            try
            {
                fitColumns();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }

        @FXML
        private void initialize() throws AWTException {
            // Initialize the person table with the two columns.
            firstNameColumn.setCellValueFactory(
                    cellData -> cellData.getValue().firstNameProperty());
            lastNameColumn.setCellValueFactory(
                    cellData -> cellData.getValue().lastNameProperty());
        }

        @FXML
        private void fitColumns() {
            MainApp.GUIUtil.fitColumns(tableView);
        }
    }

My idea was to call that method at the end of the rendering so i tried with this solution: Post render event in JavaFX, but nothing happends if do I it.

Community
  • 1
  • 1
D. Marco
  • 13
  • 6
  • Works fine for me. – DVarga Aug 31 '16 at 11:50
  • The null pointer exception is because the skin has not been installed at that point. For that to happen, at a minimum, you need the table to be part of a scene and for CSS to have been applied (which you can force with `table.applyCSS()`); I still can't get it to work, even with that, though. – James_D Aug 31 '16 at 12:19
  • @DVarga I am sorry my question wasn't completely clear, the code I posted didn't throw the excepion. I have corrected it. – D. Marco Aug 31 '16 at 14:35

2 Answers2

0

So, the difference is that you have added

try
{
    fitColumns();
}
catch (Exception e) {
    e.printStackTrace();
}

in setMainApp method of your controller. As it was pointed out by James_D, the null pointer exception is being thrown because the skin has not been installed at that point.

You can workaround this with for example update setMainApp method in the controller as:

public void setMainApp(MainApp mainApp) {
    this.mainApp = mainApp;
    tableView.setItems(mainApp.getPersonData());
    if(mainApp.primaryStage.isShowing())
        fitColumns();
    else {
        mainApp.primaryStage.showingProperty().addListener((obs, oldVal, newVal) -> {
            if(newVal)
                fitColumns();
        });
    }
}

This will check that the Stage of the your Application is showing, and if so, it fits the columns. If it is not showing, it attached a listener to the showingProperty of the Stage therefore as soon as the Stage is being shown, the fitColumns method will be called.

Community
  • 1
  • 1
DVarga
  • 21,311
  • 6
  • 55
  • 60
  • ... but my troubles aren't over. When I wrapped PersonTable into a Parent layout (RootLayout) the same problem resurfaced. I've already posted a question [here](http://stackoverflow.com/questions/39327969/programmatically-resize-tablecolumns-in-javafx). Any help is appreciated thanks :) – D. Marco Sep 05 '16 at 10:04
0

Just listen to tableView.skinProperty whenever it changed and if the skin is not null, you simply invoke your fitColumns method.

Another way is to create a custom table skin and override the createDefaultSkin method of the TableView class to return an instance of the custom skin. Then you are able to fitColumns for every TableView object you instantiate.

Javafx lets you set the skin via css property -fx-skin also. This way you are able to set the skin without overriding the createDefaultSkin method to reach a cleaner code.

By extending TableViewSkin the resizeToFitContent method is at your hand without using reflection also.

Amin
  • 292
  • 2
  • 11