6

For numeric input, it's sometimes convenient to synchronize an analog control to a text display. In this Swing example, a JSpinner and a JSlider each listen for change events, and each updates the other's model to match. A similar JavaFX program, shown below, connects a Spinner and a Slider, and these listeners keep the controls coordinated:

slider.valueProperty().addListener((Observable o) -> {
    spinner.getValueFactory().setValue(slider.getValue());
});
spinner.valueProperty().addListener((Observable o) -> {
    slider.setValue((double) spinner.getValue());
});

Unfortunately, when I added a StringConverter to the spinner's SpinnerValueFactory, the initial value was unformatted until either control was changed—even when setting the initial value explicitly again, after adding the converter:

spinner.getValueFactory().setConverter(…);
spinner.getValueFactory().setValue(INITIAL_VALUE);

Where am I going wrong?

image

import java.text.NumberFormat;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.converter.PercentageStringConverter;

/**
 * @see https://stackoverflow.com/a/6067986/230513
 */
public class SpinSlider extends Application {

    private static final double MIN = 0;
    private static final double MAX = 1;
    private static final double INITIAL_VALUE = 0.5;
    private static final double STEP_INCREMENT = 0.1;

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("SpinSlider");
        Slider slider = new Slider(MIN, MAX, INITIAL_VALUE);
        slider.setBlockIncrement(STEP_INCREMENT);
        Spinner spinner = new Spinner(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
        spinner.getValueFactory().setConverter(
            new PercentageStringConverter(NumberFormat.getPercentInstance()));
        spinner.getValueFactory().setValue(INITIAL_VALUE);
        slider.valueProperty().addListener((Observable o) -> {
            spinner.getValueFactory().setValue(slider.getValue());
        });
        spinner.valueProperty().addListener((Observable o) -> {
            slider.setValue((double) spinner.getValue());
        });
        GridPane root = new GridPane();
        root.addRow(0, slider, spinner);
        root.setPadding(new Insets(8, 8, 8, 8));
        root.setHgap(8);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Although I'm not (yet) familiar with JavaFX, it's good to know solutions for glitches like that. The workaround from the answer (a "wrong" initial value, and a change to trigger the listeners) sometimes was useful for Swing as well. As an aside: I also like the "interactivity" of a slider and the "precision"/readability of a Spinner, and therefore (some shameless self-promotion) I once created some fancy [spinner dragging functionality](https://github.com/javagl/CommonUI/blob/master/src/main/java/de/javagl/common/ui/JSpinners.java#L87) which might be an alternative - maybe also for JavaFX...? – Marco13 Mar 30 '19 at 16:18
  • 1
    @Marco13: Sadly, the glitch was mine; I forgot about the distinction between [change events and invalidation events](https://docs.oracle.com/javase/8/javafx/api/javafx/beans/value/ObservableValue.html). I hope some of my Swing-to-JavaFX [experiments](https://stackoverflow.com/search?tab=votes&q=user%3a230513%20%5bswing%5d%20%5bjavafx%5d) prove useful going forward. – trashgod Mar 30 '19 at 21:30

1 Answers1

7

Your INITIAL_VALUE is first used as the initialValue parameter to the spinner's constructor. Internally, the spinner's concrete SpinnerValueFactory is a DoubleSpinnerValueFactory that adds a ChangeListener to its value property; when the initial value is set again, the value hasn't really changed. Two approaches suggest themselves:

  1. Specify a different value to the constructor and set the desired one after adding the converter:

    Spinner spinner = new Spinner(MIN, MAX, -42, STEP_INCREMENT);
    spinner.getValueFactory().setConverter(…);
    spinner.getValueFactory().setValue(INITIAL_VALUE);
    
  2. Construct a SpinnerValueFactory with the desired initial value and use it to construct the spinner:

    SpinnerValueFactory factory = new SpinnerValueFactory
        .DoubleSpinnerValueFactory(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
    factory.setConverter(…);
    Spinner spinner = new Spinner(factory);
    

In addition, the example below replaces the two listeners with a bidirectional binding, which uses weak listeners to allow garbage collection of properties:

slider.valueProperty().bindBidirectional(
    spinner.getValueFactory().valueProperty());

Trivially, the spinner and slider can control each other. More commonly, each can stay synchronized in the course of controlling a property held by another model:

model.xProperty().bindBidirectional(slider.valueProperty());
model.xProperty().bindBidirectional(spinner.getValueFactory().valueProperty());

image

import java.text.NumberFormat;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import javafx.util.converter.PercentageStringConverter;

/**
 * @see https://stackoverflow.com/a/55427307/230513
 * @see https://stackoverflow.com/a/6067986/230513
 */
public class SpinSlider extends Application {

    private static final double MIN = 0;
    private static final double MAX = 1;
    private static final double INITIAL_VALUE = 0.5;
    private static final double STEP_INCREMENT = 0.1;

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("SpinSlider");
        Slider slider = new Slider(MIN, MAX, INITIAL_VALUE);
        slider.setBlockIncrement(STEP_INCREMENT);
        SpinnerValueFactory factory = new SpinnerValueFactory
            .DoubleSpinnerValueFactory(MIN, MAX, INITIAL_VALUE, STEP_INCREMENT);
        factory.setConverter(new PercentageStringConverter(
            NumberFormat.getPercentInstance()));
        Spinner spinner = new Spinner(factory);
        slider.valueProperty().bindBidirectional(spinner.getValueFactory().valueProperty());
        GridPane root = new GridPane();
        root.addRow(0, slider, spinner);
        root.setPadding(new Insets(8, 8, 8, 8));
        root.setHgap(8);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045