D. Marco D. Marco - 3 months ago 33
Java Question

JavaFX TableView columns fit content

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.

Answer

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.

Comments