ScalaFX: Is it possible to define controls in objects other than the application object?

614 Views Asked by At

What I am trying to accomplish is: having a ScalaFX application with some nice ordered objects called Buttons, Labels, Checkboxes and so on to keep everything nice and in order.

Here a little example to show what I mean:

package ButtonsAndLabel

import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.control.{ Button, Label }
import scalafx.event.ActionEvent

object Main extends JFXApp {

  stage = new JFXApp.PrimaryStage {
    title = "Test-Program"

    scene = new Scene(300, 200) {
      val label = new Label("Nothing happened yet") {
        layoutX = 20
        layoutY = 20
      }
      val button1 = new Button("Button 1") {
        layoutX = 20
        layoutY = 50
        onAction = (e: ActionEvent) => {
          label.text = "B1 klicked"
        }
      }
      val button2 = new Button("Button 2") {
        layoutX = 20
        layoutY = 80
        onAction = (e: ActionEvent) => {
          label.text = "B2 klicked"
        }
      }

      content = List(label, button1, button2)
    }
  }
}

This code shows a window with a label and two buttons, and the buttons change the text of the label.

That works fine.

But when my code grows with a lot more controls, things get messy.

That's why I tried to transfer the controls into other objects (in different files). I've put the label into an object called Labels:

package ButtonsAndLabel

import scalafx.scene.control.Label
import scalafx.event.ActionEvent

object Labels {
  val label = new Label("Nothing happened yet") {
    layoutX = 20
    layoutY = 20
  }
}

when I import this into the main-file with

import Labels.label

everything works fine.

But then I try to put the buttons into a Buttons object:

package ButtonsAndLabel

import scalafx.scene.control.Button
import scalafx.event.ActionEvent
import Labels.label

object Buttons {
  val button1 = new Button("Button 1") {
    layoutX = 20
    layoutY = 50
    onAction = (e: ActionEvent) => {
      label.text = "B1 klicked"
    }
  }
  val button2 = new Button("Button 2") {
    layoutX = 20
    layoutY = 80
    onAction = (e: ActionEvent) => {
      label.text = "B2 klicked"
    }
  }
}

this brings the error message when I try to compile:

[error]  found   : scalafx.event.ActionEvent => Unit
[error]  required: javafx.event.EventHandler[javafx.event.ActionEvent]
[error]     onAction = (e: ActionEvent) => {

and now I am stuck, as I don't know any Java.

Does anybody know if it is even possible what I am trying to do?

So far I have not found anything about this on the net. The problem doesn't keep me from writing the program I want, but the last application I wrote was a real mess with all the controls in one file.

Am I overlooking something obvious here?

Any help would be really appreciated.

1

There are 1 best solutions below

1
On BEST ANSWER

Firstly, your approach is perfectly OK.

The error you're seeing actually has nothing to do with Java—it's output by the Scala compiler! All it's saying is that it has been supplied one type of element (in this case, a function that takes a scalafx.event.ActionEvent and that returns Unit) when it was expecting another type of element (a javafx.event.EventHandler[javafx.event.ActionEvent] instance, in this case).

ScalaFX is just a set of Scala-friendly wrappers for the JavaFX library; without the implicit conversion functions that convert between the two sets of elements, the Scala compiler will complain about finding ScalaFX elements when it needs JavaFX elements, and vice versa.

The solution is to ensure that the following import is added to each of your ScalaFX source files:

import scalafx.Includes._

(You have this at the top of your main source file, but not the others.)

This will ensure that your ScalaFX ActionEvent handler is converted into the JavaFX equivalent, thereby making your life a little easier.

This is a very common type of error with ScalaFX, which is nearly always fixed by specifying the above import. (If the import doesn't fix your problem, then you will typically have a genuine case of type confusion, in which you just plain used the wrong type of object.)

So, here's what I think your code needs to look like:

Main.scala:

import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.scene.Scene
import buttonsandlabel._

object Main extends JFXApp {

  stage = new JFXApp.PrimaryStage {
    title = "Test-Program"

    scene = new Scene(300, 200) {
      content = List(Labels.label, Buttons.button1, Buttons.button2)
    }
  }
}

buttonsandlabel/Labels.scala:

package buttonsandlabel

import scalafx.Includes._
import scalafx.scene.control.Label

object Labels {
  val label = new Label("Nothing happened yet") {
    layoutX = 20
    layoutY = 20
  }
}

buttonsandlabel/Buttons.scala:

package buttonsandlabel

import scalafx.Includes._
import scalafx.scene.control.Button
import scalafx.event.ActionEvent
import Labels.label

object Buttons {
  val button1 = new Button("Button 1") {
    layoutX = 20
    layoutY = 50
    onAction = (e: ActionEvent) => {
      label.text = "B1 klicked"
    }
  }
  val button2 = new Button("Button 2") {
    layoutX = 20
    layoutY = 80
    onAction = (e: ActionEvent) => {
      label.text = "B2 klicked"
    }
  }
}

(Note that package names, by convention, are typically all lowercase.)

One thing that you'll need to be aware of is the JavaFX Application Thread: all of your code that interacts with ScalaFX (or JavaFX) must execute on this thread. If you access ScalaFX/JavaFX from a different thread, you'll get an error exception. (This ensures that all such applications are thread-safe.) If you're unfamiliar with multi-threading, don't worry, ScalaFX initializes your application in such a way that this is fairly trivial. Usually, all that's needed is that your initialization code goes into your main application object's constructor (the object that extends JFXApp).

When you start creating ScalaFX elements in other classes and objects, you need to take extra care. An object is initialized when first referenced. If it is first referenced by code that is not executing on the JavaFX Application Thread, then you'll get thread error exceptions. One possible option is to put such code into def or lazy val members, so that they are only executed when referenced directly.

Alternatively, you may have to invoke your code via scalafx.application.Platform.runLater().

For more information on the JavaFX Application Thread, refer to the JavaFX documentation.