Friday, September 14, 2012

A JavaFX weekly scheduler

Hi there!

This post pretends to be a detailed tutorial of how you can use custom JavaFX controls to make a nice UI, taking advantage of the JavaFX Scene Builder

As custom control I'll use a slightly modified DoubleSlider control, which originally was developed by Altuğ Uzunali by modifying the Slider Control in OpenJFX project. You can see his blog here, and download his code here. Nice work, Altuğ!

As UI I'll try to develop a weekly scheduler for working or bussines hours, using one double slider to set beginning and ending hours for each day of the week.

Here you can see in advance the result. If you find it interesting and want to learn how can you do it yourself, please, keep reading!



Step 1. Create the project

For starters, let's make a new project in NetBeans 7.2, selecting the JavaFX FXML Application type. Let's call the project WeeklySchedulerFX. Also, the FXML file will be called scheduler:

At end of the wizard three files will be created:

Step 2. Create the UI

By double clicking in scheduler.fxml the JavaFX Scene Builder will be open. Now we have to delete what it contains by default (a button and a label) and keep the AnchorPane, and start designing our own scene, by first resizing it to 600x450 dragging it or in the Layout panel, setting Pref width to 600 and Pref height to 450.

Now, let's add the label for the title. It has the following settings: 

Properties panel: 
  • Text: Weekly Scheduler  - Working Hours
  • Alignment: CENTER
  • Effect: DropShadow, spread 0.20, color #956e07,
  • Font: System 16px (Bold)
 Layout panel:
  • AnchorPane Constraints: Top 30, Right 20, Left 20.

Now, we need 7 rows for the doubleSlider controls, and a place to locate the labels (day and range of hours selected), so let's use a GridPane with 7 rows and 2 columns. In the Properties panel, first of all, we set the fx:id for this control, so we can use it in our code later.

Properties panel: 
  • fx:id: grid
  • Alignment: CENTER
  • Vgap: 2
 Layout panel:
  • AnchorPane Constraints: Top 70, Right 20, Bottom 20, Left 20.
  • add Row[3], Row [4], Row [5] and Row [6]
  • Column [0], Pref Width=100, Hgrow NEVER
  • Column [1], Pref Width=460, Max Width: USE_COMPUTED_SIZE, Hgrow ALWAYS

This is what we have for now:

Although the GridPane is a container and we could just add the controls to the specified row and column, we can add to each cell another container, so we can insert later on the custom control or other controls from code, just looping through the gridPane children. 

Let's add a VBox to each cell, and set for all of them Alignment: CENTER. For the first column, also we set Spacing: 5 and Padding 5-5-5-5. For the second column, set Padding: 10-10-10-10.

Now, in the first column, we can add a label with the day of the week. For the first one:

Properties panel: 
  • Text: MONDAY
  • Effect: DropShadow
and so on. 

That's all for the Java Scenic Builder. We can close it and move on to the code part. You should have something like this:

Step 3. DoubleSlider custom control

First of all, we download Altuğ Uzunali's code, and in our project we create a package called doubleSlider and insert DoubleSlider.java, DoubleSliderBehavior.java, DoubleSliderSkin.java and double_slider.css files there. Make sure you change the package declaration to weeklyschedulerfx.doubleSlider  in the java files, and set the -fx-skin property:

.double-slider
{
    -fx-skin: "weeklyschedulefx.doubleSlider.DoubleSliderSkin";
}

in the css one. 

Now we are going to make a few changes in his code. (For shortness sake I won't comment here every change I make, but there are very few; please have a look at the code you can download below)

In order to display time formats instead of numbers in the ticks of the slider, in DoubleSliderSkin class, in the setShowTickMarks method, insert this code just after the tickLine.setMinorTickCount line:


if (doubleSlider.getLabelFormatter() != null){
    tickLine.setTickLabelFormatter(new StringConverter(){

        @Override
        public String toString(Number object) {
            return getSkinnable().getLabelFormatter().toString((Double)object);
        }

        @Override
        public Number fromString(String string) {
            return getSkinnable().getLabelFormatter().fromString(string);
        }
    });
}


Let's add now a node to the control in order to highlight the range of hours between the two thumbs. In DoubleSliderSkin class, declare it:


private StackPane range;


Initialize it (in initialize method) and add it with the other stackpanes (both in initialize and setShowTickMarks methods) to the scene: 

range = new StackPane();
range.getStyleClass().setAll("range");

getChildren().clear();
getChildren().addAll(track, thumb1, thumb2, range);


Now we have to set the range layout. Every time the thumb's layout change the method positionThumb is called. So there insert the following code:

if(horizontal){
    range.setLayoutX(lx1+thumbWidth/2);
    range.setLayoutY(track.getLayoutY());
    range.setPrefWidth(lx2-lx1);
    range.resize(lx2-lx1, track.getHeight());
} else{
    range.setLayoutX(track.getLayoutX());
    range.setLayoutY(ly2+3*thumbHeight/4);
    range.setPrefHeight(Math.abs(ly2-ly1+thumbHeight/2));
    range.resize(track.getWidth(),Math.abs(ly2-ly1+thumbHeight/2));
}

In the layoutChildren method call positionThumb after the track is resized and relocated, not before:

// layout track
track.resizeRelocate(trackStart - trackRadius, trackTop , trackLength + trackRadius + trackRadius, trackHeight);
// layout thumbs and range
positionThumb();

Finally we add the styling properties to the css file (a few more are in the file below):

.double-slider Text {
    -fx-text-fill: #303030;
}

.double-slider .range {
    -fx-base: #956e07;
    -fx-background-color: -fx-base,
        derive(-fx-base,-22%),
        linear-gradient(to bottom, derive(-fx-base,-15.5%), derive(-fx-base,34%) 30%, derive(-fx-base,68%));
    -fx-background-insets: 1 0 -1 0, 0, 1;
    -fx-padding: 0.208333em; /* 2.5 */
}

.double-slider .axis {
    -fx-tick-label-fill: #303030;
}

And that's it!

Step 4. Application setup

Back to our application, we need to add to the Stylesheets the custom control one, and set the stage title:


Scene scene = new Scene(root);
scene.getStylesheets().addAll(DoubleSlider.class.getResource("double_slider.css").toExternalForm());

stage.setScene(scene);
stage.setTitle("JavaFX Tutorial");

Step 5. The Controller

First of all, we add the GridPane container defined in the fxml file:

@FXML
private GridPane grid;

In the initialize method now we loop through the grid children. In the first seven nodes, related to the first column, we add to each VBox a new label which will show the selected range in the format [00:00 - 24:00]. 

final Label[] rangeLabel=new Label[7];
int cont=0;
for(Node n:grid.getChildren()){
    if(n instanceof VBox){
        VBox cell=(VBox)n;
        if(cont<7){                   
            rangeLabel[cont]=new Label();
            rangeLabel[cont].textProperty().set("[00:00 - 00:00]");
            cell.getChildren().add(rangeLabel[cont]);                    
        } 
        ...

In the last seven nodes, we add the doubleSlider controls. We want to show a range of hours and half hours, so we have 2*24+ 1 tick marks, and that's why we set the min value to 0 and the max value to 48. 

Also, the unit distance between major tick marks is set to 2 (hours), and the number of minor ticks between any two major ticks is specified as 1 (half hour). The setSnapToTicks method is set to true to keep the slider's value aligned with the tick marks.
 
...
else {
    final int day=cont-7;

    final DoubleSlider doubleSlider1 = new DoubleSlider();
    doubleSlider1.setPrefWidth(300);                
    doubleSlider1.setShowTickMarks(true);
    doubleSlider1.setShowTickLabels(true);
    doubleSlider1.setMajorTickUnit(2);
    doubleSlider1.setMinorTickCount(1);
    doubleSlider1.setSnapToTicks(true);
    doubleSlider1.setMin(0);
    doubleSlider1.setMax(48);
    doubleSlider1.setLabelFormatter(new StringConverter(){              
        @Override
        public String toString(Double object) {
            if(object==null){
                return null;
            }
            return toTime(object.doubleValue());
        }

        @Override
        public Double fromString(String string) {
            return (string!=null?new Double(string):new Double(0));
        }
    });
    doubleSlider1.value1Property().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue arg0,
                        Number arg1, Number arg2) {
            rangeLabel[day].textProperty().set("["+toTime(arg2.intValue())+" - "+toTime(doubleSlider1.getValue2())+"]");
        }
    });
    doubleSlider1.value2Property().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue arg0,
                        Number arg1, Number arg2) {
            rangeLabel[day].textProperty().set("["+toTime(doubleSlider1.getValue1())+" - "+toTime(arg2.intValue())+"]");
        }
    });
}

You can see that we set the labelFormatter to map the double value of the thumb position to a time string. For this, a simple string formatter is used:
 
private String toTime(double value){
    return String.format("%02d:%02d", (int)(value/2), (int)(30*(value%2)));
}

Also, we add a valueProperty() listener for each thumb so we can reflect their position changes in the proper rangeLabel.

Finally, we decorate the cells, taking here the advices of  Jasper Potts in his the excellent post Styling FX Buttons with CSS:
 
cell.setStyle("-fx-background-color: #ecebe9,rgba(0,0,0,0.05),linear-gradient(#dcca8a, #c7a740),"
        + "linear-gradient(#f9f2d6 0%, #f4e5bc 20%, #e6c75d 80%, #e2c045 100%),"
        + "linear-gradient(#f6ebbe, #e6c34d);"
        + "-fx-background-radius: 4;-fx-background-insets: 0,2 2 1 2,2,3,4;");

End

And this is pretty much everything you need to do to have a cool control and a really nice UI up and running in a few hours! Even minutes if you grab the code here!

Now it's up to you to make any changes you may have missed here.

Please let me know if you have any comment or suggestion. They would be much appreciated!

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.