5

What exactly is the right way to create a custom ScalaFX control? I'm coming from Swing and Scala Swing, where custom components are simply created by extending Component or Panel. But when I try to extend ScalaFX's Control, I can't extend it without a JavaFX Control delegate. Should I just create custom ScalaFX components by extending the base JavFX classes instead of the ScalaFX classes?

Benedict Lee
  • 714
  • 8
  • 21

1 Answers1

6

Generally speaking, you will want to :

  • Create a custom JavaFX control.
  • Then optionally create a custom ScalaFX wrapper, on the same model as the default ones. Note that some ScalaFX features (like bindings) should work fine even without a specific ScalaFX wrapper - you can see some examples here.

To create a custom JavaFX control, a first resource to check out is this Oracle tutorial, but this blog post goes further. Open source projects like ControlsFX and JFXtras provide plenty of examples of controls.

Obviously, all those resources show how to do it in Java. I don't see any reason why you couldn't do it in Scala (as long as you use JavaFX classes rather than ScalaFX ones) - but I wasn't able to find any documentation on that, so I guess that it may be safer to create the controls in Java.

Edit: I have put up on github two examples of a simple custom JavaFX control with ScalaFX wrapper class. One version, YieldingSlider, is a single Java class that extends the Slider class; the other version, FxmlYieldingSlider, is basically the same thing, but it shows how to construct a control with an FXML file and a controller class. Note that the JAR file built from this project can be imported in Scene Builder 2.0, so that Scene Builder can use the <YieldingSlider> and <FxmlYieldingSlider> controls in FXML.

Here's what the simple version looks like.

JavaFX control:

package customjavafx.scene.control;

import javafx.scene.control.Slider;
import javafx.scene.input.MouseEvent;

public class YieldingSlider extends Slider {

    public YieldingSlider() {
        addEventFilter(MouseEvent.MOUSE_PRESSED, event -> lastTimeMousePressed = System.currentTimeMillis());
    }

    public YieldingSlider(final double min, final double max, final double value) {
        this();
        setMin(min);
        setMax(max);
        setValue(value);
    }

    private long lastTimeMousePressed = 0;

    public boolean mouseWasPressedWithinLast(final long t) {
        return (System.currentTimeMillis() - lastTimeMousePressed) <= t;
    }
}

ScalaFX wrapper:

package customscalafx.scene.control

import scala.language.implicitConversions
import customjavafx.scene.{control => jfxsc}
import scalafx.scene.control.Slider

object YieldingSlider {
  implicit def sfxSlider2jfx(v: YieldingSlider) = v.delegate
}

class YieldingSlider(override val delegate: jfxsc.YieldingSlider = new jfxsc.YieldingSlider) extends Slider {

  /** Constructs a Slider control with the specified slider min, max and current value values. */
  def this(min: Double, max: Double, value: Double) {
    this(new jfxsc.YieldingSlider(min, max, value))
  }
}

Can be used in FXML:

<?xml version="1.0" encoding="UTF-8"?>

<?import customjavafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <YieldingSlider AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" />
   </children>
</AnchorPane>

Or in ScalaFX DSL:

package guilgaly.fxtest.mp3player

import customscalafx.scene.control.YieldingSlider

import scalafx.application.JFXApp
import scalafx.scene.Scene

object TestApp extends JFXApp {
  stage = new JFXApp.PrimaryStage {
    scene = new Scene {
      content = new YieldingSlider
    }
  }
}

Finally, please note that if you use it with ScalaFXML, it will not be properly injected in the controller because ScalaFXML looks for classes whose package begins with scalafx.* (and expects the corresponding JavaFX class in the same package, but staring with javafx.*). However, if you use a package starting in javafx.*, you cannot import your control in Scene Builder. My solution was to put an ungly hack in the ScalaFXML code, so that it handles customscalafx.* like scalafx.*. But that's only a concern when using ScalaFXML.

Edit 2 : While I'm at it, here's the same JavaFX control written inScala instead of Java. It works the same, and can be wrapped in a similar ScalaFX wrapper if needed.

package customjavafx.scene.control

import javafx.event.EventHandler
import javafx.scene.control.Slider
import javafx.scene.input.MouseEvent

class ScalaYieldingSlider extends Slider{
  def this(min: Double, max: Double, value: Double) = {
    this()
    setMin(min)
    setMax(max)
    setValue(value)
  }
  // Support for Java 8 SAMs (lambdas) is still experimental in Scala 2.11.
  // I used the old-school anonymous class instead.
  addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler[MouseEvent] {
    def handle(event: MouseEvent): Unit = lastTimeMousePressed = System.currentTimeMillis
  })

  private var lastTimeMousePressed: Long = 0

  def mouseWasPressedWithinLast(t: Long): Boolean =
    (System.currentTimeMillis - lastTimeMousePressed) <= t
}
Cyäegha
  • 4,191
  • 2
  • 20
  • 36