Home > OS >  JavaFx TableView shows long string representation of columns
JavaFx TableView shows long string representation of columns

Time:05-28

I just start JavaFx and am a bit stuck with TableView, it shows very long string representation of each column like:

StringProperty [bean: com.plcsim2.PlcSimModel$ErpSheet@2c1ffe7b, name:name, value: big]
IntegerProperty [bean: com.plcsim2.PlcSimModel$ErpSheet@2c1ffe7b, name:sheet_long, value: 5000]
IntegerProperty [bean: com.plcsim2.PlcSimModel$ErpSheet@2c1ffe7b, name:sheet_short, value: 3000]

while I am expecting only "big", "5000", "3000" to appear in the cells.

Here is my model:

object PlcSimModel {
    class ErpSheet {
        val name = SimpleStringProperty(this, "name")
        val sheet_long = SimpleIntegerProperty(this, "sheet_long")
        val sheet_short = SimpleIntegerProperty(this, "sheet_short")
    }
    val erpSheets = ArrayList<ErpSheet>()
}

The fxml:

<VBox alignment="CENTER" prefHeight="562.0" prefWidth="812.0" spacing="20.0"
  xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1"
  fx:controller="com.plcsim2.PlcSimController">
<padding>
    <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<TableView fx:id="table_1" prefHeight="400.0" prefWidth="200.0">
</TableView>
<Button onAction="#onHelloButtonClick" text="Hello!" />
</VBox>

And finally the controller:

@FXML
private fun onHelloButtonClick() {
    val rs = DB.populateSql("select name, sheet_long, sheet_short from erp_sheet")
    PlcSimModel.erpSheets.clear()
    if (rs != null) {
        while (rs.next()) {
            val sheet = PlcSimModel.ErpSheet()
            sheet.name.set(rs.getString("name"))
            sheet.sheet_long.set(rs.getInt("sheet_long"))
            sheet.sheet_short.set(rs.getInt("sheet_short"))
            PlcSimModel.erpSheets.add(sheet)
        }
    }
    table_1.columns.clear()
    val col0 = TableColumn<PlcSimModel.ErpSheet, String>("name")
    col0.cellValueFactory = PropertyValueFactory("name")
    table_1.columns.add(col0)
    val col1 = TableColumn<PlcSimModel.ErpSheet, Int>("sheet_long")
    col1.cellValueFactory = PropertyValueFactory("sheet_long")
    table_1.columns.add(col1)
    val col2 = TableColumn<PlcSimModel.ErpSheet, Int>("sheet_short")
    col2.cellValueFactory = PropertyValueFactory("sheet_short")
    table_1.columns.add(col2)
    table_1.items = FXCollections.observableArrayList(PlcSimModel.erpSheets)
}

It seems controller is good, it is able to get the values from database and add rows to TableView, but why TableView shows Property object's string representation, instead of just show the value?

Thanks a lot!

CodePudding user response:

JavaFX Properties

When a class exposes a JavaFX property, it should adhere to the following pattern:

import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty

public class Foo {

  // a field holding the property
  private final StringProperty name = new SimpleStringProperty(this, "name");
  
  // a setter method (but ONLY if the property is writable)
  public final void setName(String name) {
    this.name.set(name);
  }

  // a getter method
  public final String getName() {
    return name.get();
  }

  // a "property getter" method
  public final StringProperty nameProperty() {
    return name;
  }
}

Notice that the name of the property is name, and how that is used in the names of the getter, setter, and property getter methods. The method names must follow that format.

The PropertyValueFactory class uses reflection to get the needed property. It relies on the method naming pattern described above. Your ErpSheet class does not follow the above pattern. The implicit getter methods (not property getter methods) return the property objects, not the values of the properties.


Kotlin & JavaFX Properties

Kotlin does not work especially well with JavaFX properties. You need to create two Kotlin properties, one for the JavaFX property object, and the other as a delegate (manually or via the by keyword) for the JavaFX property's value.

Here is an example:

import javafx.beans.property.IntegerProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty

class Person(name: String = "", age: Int = 0) {

    @get:JvmName("nameProperty")
    val nameProperty: StringProperty = SimpleStringProperty(this, "name", name)
    var name: String
        get() = nameProperty.get()
        set(value) = nameProperty.set(value)

    @get:JvmName("ageProperty")
    val ageProperty: IntegerProperty = SimpleIntegerProperty(this, "age", age)
    var age: Int
        get() = ageProperty.get()
        set(value) = ageProperty.set(value)
}

You can see, for instance, that the name Kotlin property delegates its getter and setter to the nameProperty Kotlin property.

The @get:JvmName("nameProperty") annotation is necessary for Kotlin to generate the correct "property getter" method on the Java side (the JVM byte-code). Without that annotation, the getter would be named getNameProperty(), which does not match the pattern for JavaFX properties. You can get away without using the annotation if you never plan to use your Kotlin code from Java, or use any class that relies on reflection (e.g., PropertyValueFactory) to get the property.

See the Kotlin documentation on delegated properties if you want to use the by keyword instead of manually writing the getter and setter (e.g., var name: String by nameProperty). You can write extension functions for ObservableValue / WritableValue (and ObservableIntegerValue / WritableIntegerValue, etc.) to implement this.

Runnable Example

Here is a runnable example using the above Person class. It also periodically increments the age of each Person so you can see that the TableView is observing the model items.

import javafx.animation.PauseTransition
import javafx.application.Application
import javafx.beans.property.IntegerProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.scene.Scene
import javafx.scene.control.TableColumn
import javafx.scene.control.TableView
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import javafx.util.Duration

fun main(args: Array<String>) = Application.launch(App::class.java, *args)

class App : Application() {

    override fun start(primaryStage: Stage) {
        val table = TableView<Person>()
        table.columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
        table.items.addAll(
            Person("John Doe", 35),
            Person("Jane Doe", 42)
        )

        val nameCol = TableColumn<Person, String>("Name")
        nameCol.cellValueFactory = PropertyValueFactory("name")
        table.columns  = nameCol

        val ageCol = TableColumn<Person, Number>("Age")
        ageCol.cellValueFactory = PropertyValueFactory("age")
        table.columns  = ageCol

        primaryStage.scene = Scene(table, 600.0, 400.0)
        primaryStage.show()

        PauseTransition(Duration.seconds(1.0)).apply {
            setOnFinished {
                println("Incrementing age of each person...")
                table.items.forEach { person -> person.age  = 1 }
                playFromStart()
            }
            play()
        }
    }
}

class Person(name: String = "", age: Int = 0) {

    @get:JvmName("nameProperty")
    val nameProperty: StringProperty = SimpleStringProperty(this, "name", name)
    var name: String
        get() = nameProperty.get()
        set(value) = nameProperty.set(value)

    @get:JvmName("ageProperty")
    val ageProperty: IntegerProperty = SimpleIntegerProperty(this, "age", age)
    var age: Int
        get() = ageProperty.get()
        set(value) = ageProperty.set(value)
}

Avoid PropertyValueFactory

With all that said, you should avoid using PropertyValueFactory, whether you're writing your application in Java or Kotlin. It was added when lambda expressions were not yet part of Java to help developers avoid writing verbose anonymous classes everywhere. However, it has two disadvantages: it relies on reflection and, more importantly, you lose compile-time validations (e.g., whether the property actually exists).

You should replace uses of PropertyValueFactory with lambdas. For example, from the above code, replace:

val nameCol = TableColumn<Person, String>("Name")
nameCol.cellValueFactory = PropertyValueFactory("name")
table.columns  = nameCol

val ageCol = TableColumn<Person, Number>("Age")
ageCol.cellValueFactory = PropertyValueFactory("age")
table.columns  = ageCol

With:

val nameCol = TableColumn<Person, String>("Name")
nameCol.setCellValueFactory { it.value.nameProperty }
table.columns  = nameCol

val ageCol = TableColumn<Person, Number>("Age")
ageCol.setCellValueFactory { it.value.ageProperty }
table.columns  = ageCol

CodePudding user response:

Now I know PropertyValueFactory uses reflection to find the property. I thought it is the key defined to IntegerProperty or StringProperty. So simply changing the model class to following fixed the problem:

class ErpSheet {
    var name = ""
    var sheet_long = 0
    var sheet_short = 0
}

The member variable name is the key to PropertyVaueFactory.

  • Related