public class AudioConfigMain extends Application {
// A reference to the model
AudioConfigModel acModel = new AudioConfigModel();
Text textDb;
Slider slider;
CheckBox mutingCheckBox;
ChoiceBox genreChoiceBox;
Color color = Color.color(0.66, 0.67, 0.69);
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Text title = new Text(65,12, "Audio Configuration");
title.setTextOrigin(VPos.TOP);
title.setFill(Color.WHITE);
title.setFont(Font.font("SansSerif", FontWeight.BOLD, 20));
Text textDb = new Text();
textDb.setLayoutX(18);
textDb.setLayoutY(69);
textDb.setTextOrigin(VPos.TOP);
textDb.setFill(Color.web("#131021"));
textDb.setFont(Font.font("SansSerif", FontWeight.BOLD, 18));
Text mutingText = new Text(18, 113, "Muting");
mutingText.setTextOrigin(VPos.TOP);
mutingText.setFont(Font.font("SanSerif", FontWeight.BOLD, 18));
mutingText.setFill(Color.web("#131021"));
Text genreText = new Text(18,154,"Genre");
genreText.setTextOrigin(VPos.TOP);
genreText.setFill(Color.web("#131021"));
genreText.setFont(Font.font("SanSerif", FontWeight.BOLD, 18));
slider = new Slider();
slider.setLayoutX(135);
slider.setLayoutY(69);
slider.setPrefWidth(162);
slider.setMin(acModel.minDecibels);
slider.setMax(acModel.maxDecibels);
mutingCheckBox = new CheckBox();
mutingCheckBox.setLayoutX(280);
mutingCheckBox.setLayoutY(113);
genreChoiceBox = new ChoiceBox();
genreChoiceBox.setLayoutX(204);
genreChoiceBox.setLayoutY(154);
genreChoiceBox.setPrefWidth(93);
genreChoiceBox.setItems(acModel.genres);
Stop[] stops = new Stop[]{new Stop(0, Color.web("0xAEBBCC")), new Stop(1, Color.web("0x6D84A3"))};
LinearGradient linearGradient = new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
Rectangle rectangle = new Rectangle(0, 0, 320, 45);
rectangle.setFill(linearGradient);
Rectangle rectangle2 = new Rectangle(0, 43, 320, 300);
rectangle2.setFill(Color.rgb(199, 206, 213));
Rectangle rectangle3 = new Rectangle(8, 54, 300, 130);
rectangle3.setArcHeight(20);
rectangle3.setArcWidth(20);
rectangle3.setFill(Color.WHITE);
rectangle3.setStroke(color);
Line line1 = new Line(9, 97, 309, 97);
line1.setStroke(color);
Line line2 = new Line(9, 141, 309, 141);
line2.setFill(color);
Group group = new Group(rectangle, title, rectangle2, rectangle3,
textDb,
slider,
line1,
mutingText,
mutingCheckBox, line2, genreText,
genreChoiceBox);
Scene scene = new Scene(group, 320, 343);
textDb.textProperty().bind(acModel.selectedDBs.asString().concat(" dB"));
slider.valueProperty().bindBidirectional(acModel.selectedDBs);
slider.disableProperty().bind(acModel.muting);
mutingCheckBox.selectedProperty().bindBidirectional(acModel.muting);
acModel.genreSelectionModel = genreChoiceBox.getSelectionModel();
acModel.addListenerToGenreSelectionModel();
acModel.genreSelectionModel.selectFirst();
stage.setScene(scene);
stage.setTitle("Audio Configuration");
stage.show();
}
}
두번째 프로그램 - "워낭소리"
If you’re familiar with the Saturday Night Live television show, you may have seen the “More Cowbell” sketch, in which Christopher Walken’s character keeps asking for “more cowbell” during a Blue Oyster Cult recording session. The following JavaFX example program covers some of the simple but powerful concepts of JavaFX in the context of an imaginary application that lets you select a music genre and control the volume. Of course, “Cowbell Metal,” shortened to “Cowbell,” is one of the available genres. Figure 1-9 shows a screenshot of this application, which has a sort of retro iPhone application look.
Figure 1-9. The Audio Configuration “워낭소리” program
The Behavior of the Audio Configuration Program
When you run the application, notice that adjusting the volume slider changes the associated decibel (dB) level displayed. Also, selecting the Muting check box disables the slider, and selecting various genres changes the volume slider. This behavior is enabled by concepts that are shown in the code that follows, such as the following:
-
Binding to a class that contains a model
-
Using change listeners
-
Creating observable lists
Understanding the Audio Configuration Program
The Audio Configuration program contains two source code files, shown in Listing 1-3 and Listing 1-4:
The AudioConfigMain.java
file in Listing 1-3 contains the main
class, and expresses the UI in a manner that you are familiar with from the Hello Earthrise
example in Listing 1-1.
The AudioConfigModel.java
file in Listing 1-4 contains a model for this program, which holds the state of the application, to which the UI is bound.
Listing 1-3. The AudioConfigMain.java Program
Take a look at the AudioConfigMain.java
source code in Listing 1-3, after which we examine it together, focusing on concepts not covered in the previous example.
Now that you’ve seen the main class in this application, let’s walk through the new concepts.
The Magic of Binding
자바FX의 가장 강력한 기능이 바인딩이다. 바인딩은 앱의 UI를 상태나 모델과 쉽게 동기화할 수 있도록 한다. 자바FX 앱의 모델은 보통 하나 이상의 클래스에 맏물려 있다. 여기에서는 AudioConfigModel
클래스와 물려 있다. 다음 코드를 보면 이 모델 클래스를 만들고 있다.
AudioConfigModel acModel = new AudioConfigModel();
여기 UI를 보면 여러가지 그래픽 노드 인스턴스가 존재한다. 다음 코드에서는 모델의 selectedDBs
속성과 바운드되고 있다.
textDb = new Text();
... code omitted
slider = new Slider();
...code omitted...
textDb.textProperty().bind(acModel.selectedDBs.asString().concat(" dB"));
slider.valueProperty().bindBidirectional(acModel.selectedDBs);
As shown in this code, the text property of the Text
object is bound to an expression. The bind
function contains an expression (that includes the selectedDBs
property), which is evaluated and becomes the value of the text property. Look at Figure 1-9 (or check the running application) to see the content value of the Text
node displayed to the left of the slider.
Notice also in the code that the value property of the Slider
node is bound to the selectedDBs
property in the model as well, but that it uses the bindBidirectional()
method. This causes the bind to be bidirectional, so in this case when the slider is moved, the selectedDBs
property in the model changes. Conversely, when the selectedDBs
property changes (as a result of changing the genre), the slider moves.
Go ahead and move the slider to demonstrate the effects of the bind expressions in the snippet. The number of decibels displayed at the left of the slider should change as the slider is adjusted.
There are other bound properties in Listing 1-3 that we point out when we walk through the model class. Before leaving the UI, we point out some color-related concepts in this example.
Colors and Gradients
The following snippet from Listing 1-3 contains an example of defining a color gradient pattern, as well as defining colors.
Stop[] stops = new Stop[]{new Stop(0, Color.web("0xAEBBCC")), new Stop(1, Color.web("0x6D84A3"))};
LinearGradient linearGradient = new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, stops);
Rectangle rectangle = new Rectangle(0, 0, 320, 45);
rectangle.setFill(linearGradient);
If the JavaFX API docs are handy, first take a look at the javafx.scene.shape.Rectangle
class and notice that it inherits a property named fill that is of type javafx.scene.paint.Paint
. Looking at the JavaFX API docs for the Paint
class, you’ll see that the Color
, ImagePattern
, LinearGradient
, and RadialGradient
classes are subclasses of Paint
. This means that the fill of any shape can be assigned a color, pattern, or gradient.
To create a LinearGradient
, as shown in the code, you need to define at least two stops, which define the location
and color
at that location. In this example, the offset value of the first stop is 0.0, and the offset value of the second stop is 1.0. These are the values at both extremes of the unit square, the result being that the gradient will span the entire node (in this case a Rectangle).
The direction of the LinearGradient
is controlled by its startX
, startY
, endX
, and endY
values, which we pass via the constructor. In this case, the direction is only vertical because the startY value is 0.0 and the endY
value is 1.0, whereas the startX
and endX
values are both 0.0.
Note that in the Hello Earthrise
example in Listing 1-1, the constant named Color.WHITE
was used to represent the color white. In the previous snippet, the web function of the Color
class is used to define a color from a hexadecimal value.
The Model Class for the Audio Configuration Example
Take a look at the source code for the AudioConfigModel
class in Listing 1-4.
Listing 1-4. The Source Code for AudioConfigModel.java
/**
* The model class that the AudioConfigMain class uses
*/
public class AudioConfigModel {
public double minDecibels = 0.0;
public double maxDecibels = 160.0;
public IntegerProperty selectedDBs = new SimpleIntegerProperty(0);
public BooleanProperty muting = new SimpleBooleanProperty(false);
public ObservableList genres = FXCollections.observableArrayList(
"Chamber",
"Country",
"Cowbell",
"Metal",
"Polka",
"Rock"
);
public SingleSelectionModel genreSelectionModel;
public void addListenerToGenreSelectionModel() {
genreSelectionModel.selectedIndexProperty().addListener((Observable o) -> {
int selectedIndex = genreSelectionModel.selectedIndexProperty().getValue();
switch(selectedIndex) {
case 0: selectedDBs.setValue(80);
break;
case 1: selectedDBs.setValue(100);
break;
case 2: selectedDBs.setValue(150);
break;
case 3: selectedDBs.setValue(140);
break;
case 4: selectedDBs.setValue(120);
break;
case 5: selectedDBs.setValue(130);
}
});
}
}
Using InvalidationListeners and Lambda Expressions
In the earlier section “The Magic of Binding,” we showed how you can use property binding for dynamically changing parameters. There is another, more low-level but also more flexible way of achieving this, using ChangeListeners
and InvalidationListeners
. These concepts are discussed in more detail in Chapter 4.
In our example, we add an InvalidationListener
to the selectedIndexProperty
of the genreSelectionModel
. When the value of the selectedIndexProperty changes, and when we didn’t retrieve it yet, the invalidated(Observable)
method on the added InvalidationListener
will be called. In the implementation of this method, we retrieve the value of the selectedIndexProperty
, and based on its value, the value of the selectedDBs
property is changed. This is achieved with the following code:
public void addListenerToGenreSelectionModel() {
genreSelectionModel.selectedIndexProperty().addListener((Observable o) -> {
int selectedIndex = genreSelectionModel.selectedIndexProperty().getValue();
switch(selectedIndex) {
case 0: selectedDBs.setValue(80);
break;
case 1: selectedDBs.setValue(100);
break;
case 2: selectedDBs.setValue(150);
break;
case 3: selectedDBs.setValue(140);
break;
case 4: selectedDBs.setValue(120);
break;
case 5: selectedDBs.setValue(130);
}
});
}
Note that we are using a Lambda expression here rather than creating a new instance of the InvalidationListener
and implementing its single abstract method invalidated
.
Tip: One of the major enhancements in JavaFX 8 is the fact that it is using Java 8. As a consequence, abstract classes with a single abstract method can easily be replaced by Lambda expressions, which clearly enhances readability of the code.
What causes selectedIndexProperty
of the genreSelectionModel
to change? To see the answer to this, we have to revisit some code in Listing 1-3. In the following code snippet, the setItems method of ChoiceBox
is used to populate the ChoiceBox
with items that each contain a genre.
genreChoiceBox = new ChoiceBox();
genreChoiceBox.setLayoutX(204);
genreChoiceBox.setLayoutY(154);
genreChoiceBox.setPrefWidth(93);
genreChoiceBox.setItems(acModel.genres);
This snippet from the model code in Listing 1-4 contains the collection to which the ComboBox
items are bound:
/**
* List of some musical genres
*/
public ObservableList genres = FXCollections.observableArrayList(
"Chamber",
"Country",
"Cowbell",
"Metal",
"Polka",
"Rock"
);
When the user chooses a different item in the ChoiceBox
, the invalidationListener
is invoked. Looking again at the code in the invalidationListener
, you’ll see that the value of the selectedDBs
property changes, which as you may recall, is bidirectionally bound to the slider. This is why the slider moves when you select a genre in the combo box. Go ahead and test this by running the Audio Config
program.
Note: Associating the items property of the ChoiceBox
with an ObservableList
causes the items in the ChoiceBox
to be automatically updated when the elements in the underlying collection are modified.