Search code examples
scalauser-interfacebindingscalafx

ScalaFX. Live binding


I have a simple UI window with just one Text node, witch shows current time and binded to the model:

import model.Clock

import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.StringProperty
import scalafx.geometry.Insets
import scalafx.scene.Scene
import scalafx.scene.layout.HBox
import scalafx.scene.text.Text
import scalafx.Includes._

object Main extends JFXApp {
    val clock = new Text()
    clock.textProperty().bind( new StringProperty(Clock.curTime) )

    stage = new PrimaryStage {
        onShowing = handle { Clock.startClock }
        title = "ScalaFX clock"
        scene = new Scene {
            content = new HBox {
                padding = Insets(50, 80, 50, 80)
                children = clock
            }
        }
    }
}

And a model:

import java.util.Date
import java.text.SimpleDateFormat

object Clock {
    var curTime = ""
    private lazy val dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss")

    def startClock = {
        /*A code, witch runs in another Thread and 
          changes curTime every 1 second skipped here.
          Only one change of curTime for simplicity.
        */

        curTime = dateFormat format new Date
    }
}

My problem is: the text node in UI doesn't change, when curTime variable change.

I guess it renderes once on stratup. How to make text node show new value of curTime every time, curTime changed?

Sorry for my English :(


Solution

  • The problem is that Clock.curTime is just a variable whose value gets updated periodically - it's not an observed property, so nothing happens when it is changed. In particular, there's no link between that variable and your Text element's contents

    What you have done is to initialize a StringProperty with that value - but since the property itself is never updated, neither is the label. I think what you wanted to do was to make curTime a StringProperty instead of just a String. Now, whenever you change that property's value, the Text element's value will change accordingly.

    You should note that interactions with JavaFX/ScalaFX can only happen on the JavaFX Application Thread, so if you try updating curTime from a another thread, you're going to run into problems. One way to achieve that would be to pass the code that actually updates curTime to Platform.runLater.

    However, a simpler approach is to use a timed ScalaFX event to update curTime periodically from within JavaFX/ScalaFX, as follows:

    import model.Clock
    import scalafx.application.JFXApp
    import scalafx.application.JFXApp.PrimaryStage
    import scalafx.geometry.Insets
    import scalafx.scene.Scene
    import scalafx.scene.layout.HBox
    import scalafx.scene.text.Text
    import scalafx.Includes._
    
    object Main
    extends JFXApp {
    
      val clock = new Text()
    
      // Clock.curTime is now a StringProperty, which we bind to clock's text.  Since clock is
      // created lazily, it starts the timer event, so we do not need startClock either.
      clock.text <== Clock.curTime
    
      // Create the stage & scene.
      stage = new PrimaryStage {
        title = "ScalaFX clock"
        scene = new Scene {
          content = new HBox {
            padding = Insets(50, 80, 50, 80)
            children = clock
          }
        }
      }
    }
    

    In Clock.scala:

    import java.util.Date
    import java.text.SimpleDateFormat
    import scalafx.animation.PauseTransition
    import scalafx.application.Platform
    import scalafx.beans.property.StringProperty
    import scalafx.util.Duration
    import scalafx.Includes._
    
    // Object should be constructed lazily on the JFX App Thread. Verify that...
    object Clock {
      assert(Platform.isFxApplicationThread)
    
      private val dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss")
    
      val curTime = new StringProperty(getDate())
    
      // Update the curTime property every second, using a timer.
      // Note: 1,000 ms = 1 sec
      val timer = new PauseTransition(Duration(1000))
      timer.onFinished = {_ =>
        curTime.value = getDate()
        timer.playFromStart() // Wait another second, or you can opt to finish instead.
      }
    
      // Start the timer.
      timer.play()
    
      private def getDate() = dateFormat.format(new Date())
    }