4

I have a simple tornadoFX program that generates some circles in random locations on the screen. However, none of the circles get drawn. I've added some debug code to print a line when a circle is drawn, and it only prints once.

I would expect circles to appear at 100ms intervals, as well as when I click the "Add actor" button.

private const val WINDOW_HEIGHT = 600
private const val WINDOW_WIDTH = 1024

fun main(args: Array<String>) {
    Application.launch(MainApp::class.java, *args)
}

class MainApp : App(WorldView::class, Stylesheet::class)

data class Actor(val x: Double, val y: Double)

class WorldView: View("Actor Simulator") {
    override val root = VBox()
    private val actors = ArrayList<Actor>(0)

    init {
        tornadofx.runAsync {
            (0..100).forEach {
                val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
                val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
                actors.add(Actor(x, y))
                Thread.sleep(100)
            }
        }
    }

    init {
        with(root) {
            stackpane {
                group {
                    bindChildren(actors.observable()) {
                        circle {
                            centerX = it.x
                            centerY = it.y
                            radius = 10.0
                            also {
                                println("drew circle")
                            }
                        }
                    }
                }
                button("Add actor") {
                    action {
                        actors.add(Actor(0.0, 0.0))
                    }
                }
            }
        }
    }
}

Oddly, if I put a breakpoint during the circle draw code, circles will draw and the debug line will print.

Malcolm Crum
  • 4,345
  • 4
  • 30
  • 49

1 Answers1

8

Some observations:

  1. Calling someList.observable() will create an observable list backed by the underlying list, but mutations on the underlying list will not emit events. You should instead initialize actors as an observable list right away.

  2. Access to an observable list must happen on the UI thread, so you need to wrap mutation calls in runLater.

  3. For people trying to run your example - you didn't include a stylesheet, but references one in your App subclass, so the IDEA will most probably import the TornadoFX Stylesheet class. This will not end well :)

  4. The also call has no effect, so I removed it.

  5. I updated your code to best practices here and there, for example with regards to how to create the root node :)

Updated example taking these points into account looks like this:

private const val WINDOW_HEIGHT = 600.0
private const val WINDOW_WIDTH = 1024.0

class MainApp : App(WorldView::class)

data class Actor(val x: Double, val y: Double)

class WorldView : View("Actor Simulator") {

    private val actors = FXCollections.observableArrayList<Actor>()

    override fun onDock() {
        runAsync {
            (0..100).forEach {
                val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
                val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
                runLater {
                    actors.add(Actor(x, y))
                }
                Thread.sleep(100)
            }
        }
    }

    override val root = stackpane {
        setPrefSize(WINDOW_WIDTH, WINDOW_HEIGHT)
        group {
            bindChildren(actors) {
                circle {
                    centerX = it.x
                    centerY = it.y
                    radius = 10.0
                    println("drew circle")
                }
            }
        }
        button("Add actor") {
            action {
                actors.add(Actor(0.0, 0.0))
            }
        }
    }
}
Edvin Syse
  • 7,267
  • 18
  • 24
  • 1
    This answer is great! But it makes me wonder... There is very little search results for `bindChildren` and yet it feels like a totally core functionality. Pretty much when the default table or listviews are not sufficient and I want some fancy rendering for each item, there is no other choice but use `bindChildren`. Or is there another way? – tobik Jul 28 '19 at 16:07