Platinum Partner
netbeans,custom control,javafx 2.0

Creating a DateChooser Control with JavaFX 2.0

I must admit I finally came to like JavaFX. The game changer was that JavaFX 2.0 offers a path of adoption for Swing developers.

I especially like the fact that it's easy to style the JavaFX user interface via CSS. And, even if you create custom controls, you can make them styleable without much extra effort. Here's a little datepicker I created as an example:

You can use NetBeans IDE 7.1 for development. Here's the complete example as a NetBeans Project:

[download]

The JavaFX controls consist of a Control and a Skin class. We'll start with the control:

 DateChooser.java

public class DateChooser extends Control{

    private static final String DEFAULT_STYLE_CLASS = "date-chooser";
    private Date date;

    public DateChooser(Date preset) {
        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
        this.date = preset;
    }

    public DateChooser() {
        this(new Date(System.currentTimeMillis()));
    }

    @Override
    protected String getUserAgentStylesheet() {
        return "de/eppleton/fxcontrols/datechooser/calendar.css";
    }

    public Date getDate() {
        return date;
    }
}

The only important thing it does is to register the stylesheet for this Control and gives access to the date picked by the user. Next we'll define a CSS file with our default styles:

calendar.css

.date-chooser {
    -fx-skin: "de.eppleton.fxcontrols.datechooser.DateChooserSkin";
}

.weekday-cell { 
    -fx-background-color: lightgray;
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-fill: darkgray;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
}

.week-of-year-cell { 
    -fx-background-color: lightgray;
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-fill: white;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
}

.calendar-cell { 
    -fx-background-color: skyblue, derive(skyblue, 25%), derive(skyblue, 50%), derive(skyblue, 75%);
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-fill: skyblue;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
}

.calendar-cell:hover { 
    -fx-background-color: skyblue; 
    -fx-text-fill: white;   
}

.calendar-cell:pressed { 
    -fx-background-color: darkblue;  
    -fx-text-fill: green;   
}

.calendar-cell-selected { 
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
    -fx-background-color: darkblue;  
    -fx-text-fill: white;   
}

.calendar-cell-inactive { 
    -fx-background-color: derive(lightgray, 75%);
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-fill: darkgray;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
}

.calendar-cell-today { 
    -fx-background-color: yellow;  
    -fx-background-radius: 5 5 5 5;
    -fx-background-insets: 2 2 2 2 ;
    -fx-text-fill: skyblue;
    -fx-text-alignment: left;
    -fx-font: 12pt "Tahoma Bold";
}

The most important part here is the -fx-skin. It defines which class should be used as the Skin for our control. The third part is the Skin itself.

DateChooserSkin.java

public class DateChooserSkin extends SkinBase<DateChooser, BehaviorBase<DateChooser>> {

    private final Date date;
    private final Label month;
    private final BorderPane content;
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMMMM yyyy");

    private static class CalendarCell extends StackPane {

        private final Date date;

        public CalendarCell(Date day, String text) {
            this.date = day;
            Label label = new Label(text);
            getChildren().add(label);
        }

        public Date getDate() {
            return date;
        }
    }

    public DateChooserSkin(DateChooser dateChooser) {
        super(dateChooser, new BehaviorBase(dateChooser));
        // this date is the selected date
        date = dateChooser.getDate();
        final DatePickerPane calendarPane = new DatePickerPane(date);


        month = new Label(simpleDateFormat.format(calendarPane.getShownMonth()));
        HBox hbox = new HBox();

        // create the navigation Buttons
        Button yearBack = new Button("<<");
        yearBack.addEventHandler(ActionEvent.ACTION, new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                calendarPane.forward(-12);

            }
        });
        Button monthBack = new Button("<");
        monthBack.addEventHandler(ActionEvent.ACTION, new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                calendarPane.forward(-1);
            }
        });
        Button monthForward = new Button(">");
        monthForward.addEventHandler(ActionEvent.ACTION, new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                calendarPane.forward(1);
            }
        });
        Button yearForward = new Button(">>");
        yearForward.addEventHandler(ActionEvent.ACTION, new EventHandler() {

            @Override
            public void handle(ActionEvent event) {
                calendarPane.forward(12);
            }
        });

        // center the label and make it grab all free space
        HBox.setHgrow(month, Priority.ALWAYS);
        month.setMaxWidth(Double.MAX_VALUE);
        month.setAlignment(Pos.CENTER);
        hbox.getChildren().addAll(yearBack, monthBack, month, monthForward, yearForward);

        // use a BorderPane to Layout the view
        content = new BorderPane();
        getChildren().add(content);
        content.setTop(hbox);
        content.setCenter(calendarPane);
    }

    /**

     @author eppleton
     */
    class DatePickerPane extends Region {

        private final Date selectedDate;
        private final Calendar cal;
        private CalendarCell selectedDayCell;
        // this is used to format the day cells
        private final SimpleDateFormat sdf = new SimpleDateFormat("d");
        // empty cell header of weak-of-year row
        private final CalendarCell woyCell = new CalendarCell(new Date(), "");
        private int rows, columns;//default

        public DatePickerPane(Date date) {
            setPrefSize(300, 300);
            woyCell.getStyleClass().add("week-of-year-cell");
            setPadding(new Insets(5, 0, 5, 0));
            this.columns = 7;
            this.rows = 5;

            // use a copy of Date, because it's mutable
            // we'll helperDate it through the month
            cal = Calendar.getInstance();
            Date helperDate = new Date(date.getTime());
            cal.setTime(helperDate);

            // the selectedDate is the date we will change, when a date is picked
            selectedDate = date;
            refresh();
        }

        /**
         Move forward the specified number of Months, move backward by using
         negative numbers

         @param i
         */
        public void forward(int i) {

            cal.add(Calendar.MONTH, i);
            month.setText(simpleDateFormat.format(cal.getTime()));
            refresh();
        }

        private void refresh() {
            super.getChildren().clear();
            this.rows = 5; // most of the time 5 rows are ok
            // save a copy to reset the date after our loop
            Date copy = new Date(cal.getTime().getTime());

            // empty cell header of weak-of-year row
            super.getChildren().add(woyCell);

            // Display a styleable row of localized weekday symbols 
            DateFormatSymbols symbols = new DateFormatSymbols();
            String[] dayNames = symbols.getShortWeekdays();

            // @TODO use static constants to access weekdays, I suspect we 
            // get problems with localization otherwise ( Day 1 = Sunday/ Monday in
            // different timezones
            for (int i = 1; i < 8; i++) { // array starts with an empty field
                CalendarCell calendarCell = new CalendarCell(cal.getTime(), dayNames[i]);
                calendarCell.getStyleClass().add("weekday-cell");
                super.getChildren().add(calendarCell);
            }

            // find out which month we're displaying
            cal.set(Calendar.DAY_OF_MONTH, 1);
            final int month = cal.get(Calendar.MONTH);

            int weekday = cal.get(Calendar.DAY_OF_WEEK);

            // if the first day is a sunday we need to rewind 7 days otherwise the 
            // code below would only start with the second week. There might be 
            // better ways of doing this...
            if (weekday != Calendar.SUNDAY) {
                // it might be possible, that we need to add a row at the end as well...

                Calendar check = Calendar.getInstance();
                check.setTime(new Date(cal.getTime().getTime()));
                int lastDate = check.getActualMaximum(Calendar.DATE);
                check.set(Calendar.DATE, lastDate);
                if ((lastDate + weekday) > 36) {
                    rows = 6;
                }

                cal.add(Calendar.DATE, -7);

            }
            cal.set(Calendar.DAY_OF_WEEK, 1);



            // used to identify and style the cell with the selected date;
            Calendar testSelected = Calendar.getInstance();
            testSelected.setTime(selectedDate);

            for (int i = 0; i < (rows); i++) {

                // first column shows the week of year
                CalendarCell calendarCell = new CalendarCell(cal.getTime(), "" + cal.get(Calendar.WEEK_OF_YEAR));
                calendarCell.getStyleClass().add("week-of-year-cell");
                super.getChildren().add(calendarCell);

                // loop through current week
                for (int j = 0; j < columns; j++) {
                    String formatted = sdf.format(cal.getTime());
                    final CalendarCell dayCell = new CalendarCell(cal.getTime(), formatted);
                    dayCell.getStyleClass().add("calendar-cell");
                    if (cal.get(Calendar.MONTH) != month) {
                        dayCell.getStyleClass().add("calendar-cell-inactive");
                    } else {
                        if (isSameDay(testSelected, cal)) {
                            dayCell.getStyleClass().add("calendar-cell-selected");
                            selectedDayCell = dayCell;
                        }
                        if (isToday(cal)) {
                            dayCell.getStyleClass().add("calendar-cell-today");
                        }

                    }
                    dayCell.setOnMouseClicked(new EventHandler() {

                        @Override
                        public void handle(MouseEvent arg0) {
                            if (selectedDayCell != null) {
                                selectedDayCell.getStyleClass().add("calendar-cell");
                                selectedDayCell.getStyleClass().remove("calendar-cell-selected");
                            }
                            selectedDate.setTime(dayCell.getDate().getTime());
                            dayCell.getStyleClass().remove("calendar-cell");
                            dayCell.getStyleClass().add("calendar-cell-selected");
                            selectedDayCell = dayCell;
                            Calendar checkMonth = Calendar.getInstance();
                            checkMonth.setTime(dayCell.getDate());

                            if (checkMonth.get(Calendar.MONTH) != month) {
                                forward(checkMonth.get(Calendar.MONTH) - month);
                            }
                        }
                    });

                    // grow the hovered cell in size  
                    dayCell.setOnMouseEntered(new EventHandler() {

                        @Override
                        public void handle(MouseEvent e) {
                            dayCell.setScaleX(1.1);
                            dayCell.setScaleY(1.1);
                        }
                    });

                    dayCell.setOnMouseExited(new EventHandler() {

                        @Override
                        public void handle(MouseEvent e) {
                            dayCell.setScaleX(1);
                            dayCell.setScaleY(1);
                        }
                    });

                    super.getChildren().add(dayCell);
                    cal.add(Calendar.DATE, 1);  // number of days to add
                }
            }
            cal.setTime(copy);
        }

        /**
         Overriden, don't add Children directly

         @return unmodifieable List
         */
        @Override
        protected ObservableList getChildren() {
            return FXCollections.unmodifiableObservableList(super.getChildren());
        }

        /**
         get the current month our calendar displays. Should always give you the
         correct one, even if some days of other mnths are also displayed

         @return
         */
        public Date getShownMonth() {
            return cal.getTime();
        }

        @Override
        protected void layoutChildren() {
            ObservableList children = getChildren();
            double width = getWidth();
            double height = getHeight();

            double cellWidth = (width / (columns + 1));
            double cellHeight = height / (rows + 1);

            for (int i = 0; i < (rows + 1); i++) {
                for (int j = 0; j < (columns + 1); j++) {
                    if (children.size() <= ((i * (columns + 1)) + j)) {
                        break;
                    }
                    Node get = children.get((i * (columns + 1)) + j);
                    layoutInArea(get, j * cellWidth, i * cellHeight, cellWidth, cellHeight, 0.0d, HPos.LEFT, VPos.TOP);
                }

            }
        }
    }
    // utility methods

    private static boolean isSameDay(Calendar cal1, Calendar cal2) {
        if (cal1 == null || cal2 == null) {
            throw new IllegalArgumentException("The dates must not be null");
        }
        return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA)
                && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
                && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
    }

    private static boolean isToday(Calendar cal) {
        return isSameDay(cal, Calendar.getInstance());
    }
}

There are a couple of inline comments explaining what the code does. Most of the visual stuff is in the DatePickerPane. DatePickerPane extends Region, which is a base class you can use for your own layout manager. You just need to override layoutChildren. I used a simple grid.

The refresh method is responsible for creating the headers and the cells that represent the days and weeks. It also assigns the style to each of the fields. Since we have a fixed number of cells, we should probably add them only once and in subsequent steps only change the labels (and save us from creating tons of Eventhandlers).

But let's keep it simple, it's just an example... The cool thing is that anyone interested in changing the appearance just needs to have a look at the CSS file to find out which styles we defined and can override them with a customized stylesheet to match the overall look and feel of their own application.

Finally here's how to use it in your code:

public class TestApplication extends Application {

    /**
     @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Hello World!");
        StackPane root = new StackPane();
        final DateChooser dateChooser = new DateChooser();
        root.getChildren().add(dateChooser);
        Scene scene = new Scene(root, 300, 250);        
        primaryStage.setScene(scene);
        primaryStage.setOnHiding(new EventHandler() {

                    public void handle(WindowEvent event) {
                        System.out.println("date " + dateChooser.getDate());
                    }
                });
        primaryStage.show();
    }
}
Have fun!
{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}