Toni Epple works as a consultant for Eppleton (http://www.eppleton.de) in Munich, Germany. In his spare time he's an active member of the Open Source community as a community leader for JavaTools community (http://community.java.net/javatools/), moderator of the XING NetBeans User Group (http://www.xing.com/group-20148.82db20), founder of the NetBeans User Group Munich (http://tinyurl.com/5b8tuu), member of the NetBeans Dream Team (http://wiki.netbeans.org/NBDTCurrentMembers) and blogger (http://www.eppleton.de/blog). Toni is a DZone MVB and is not an employee of DZone and has posted 51 posts at DZone. You can read more from them at their website. View Full User Profile

Creating a DateChooser Control with JavaFX 2.0

12.26.2011
| 45227 views |
  • submit to reddit

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!
AttachmentSize
datechooser.PNG22.5 KB
DateChooser.zip26.65 KB
Published at DZone with permission of Toni Epple, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Sergey Surikov replied on Mon, 2011/12/26 - 11:35pm

Look to this JavaFX datechooser:

http://java.dzone.com/tips/javafx-component-creation

This is JabaFX v1.0 from far 2008. Small code, beautifull graphics, localization.

Why there aren't such nice JavaFX components like this one?

JavaFX ruined all promises 8(

 

 

Sergey Surikov replied on Mon, 2011/12/26 - 11:38pm in response to: Sergey Surikov

ops, i looked to comments in my code. This is 23.09.2007.

Four years! Four years ago!

Toni Epple replied on Mon, 2012/01/02 - 11:33am in response to: Sergey Surikov

Hi Sergey, it's funny how different our impressions are :-). In my opinion JavaFX is on the right track for the first time. JavaFX will be Open Source and available on all major platforms in 2012. The CSS-styling is excellent. With FXML we've got a declarative way of defining UIs. And finally the integration with Swing gives developers a nice path of adoption and a way to provide missing functionality (Window Systems, etc.).

The only thing missing is decent tooling and a couple of controls. But the number ofavailable controls has increased significantly already and a DateChooser will probably be available soon ("This is a control that is planned for a future release of JavaFX").

Manish Chowdhary replied on Mon, 2012/04/02 - 7:54pm

This article needs to be updated for the latest version of JavaFX. GoECart

Rahul Chirumamilla replied on Tue, 2012/05/01 - 9:38pm

Hey the datechooser is really helpful. but my question is, I have a javafx project using FXML. I am trying to in corporate this date chooser functionaly in to it. The problem I can across is how do I add this to FXML(in terms of tags) and call the DateChooser. Can you please eloberate a little on that.

Thanks a ton in advance. 

Valentin Vrinceanu replied on Mon, 2012/05/07 - 9:18am in response to: Rahul Chirumamilla

I tried to incorporate it in the FXML Controller with a pop-up but i failded. If anyone has a solution i will be greatful!

 Thanks a lot in advance!

Matt Coleman replied on Fri, 2012/06/22 - 1:53am

Netbeans please update this soon...im having problems with the controls

 buffalo web design

Mateo Gomez replied on Mon, 2012/08/20 - 1:51am

a date picker is such a wonderful feature especially if u can pick the colors

chicken burrito recipe

Sheraz Butt replied on Mon, 2012/09/24 - 6:01am

Custom dress shirts; designed and stitched according to the fit you want at Nattyshirts.com

Johan Botha replied on Wed, 2013/04/03 - 5:14am

styrofoam  If you live in a wealthy suburb and attend a nice affluent church every sunday and you work in a sterile office where all your coworkers are well educated and affluent, 

Ray Naa replied on Mon, 2014/09/15 - 3:36am in response to: Rahul Chirumamilla

 

Thank you again for all the knowledge you distribute,Good post. I was very interested in the article, it's quite inspiring I should admit. I like visiting you site since I always come across interesting articles like this one.Great Job, I greatly appreciate that.Do Keep sharing! Regards,
 
love spells

Ray Naa replied on Mon, 2014/09/15 - 7:38pm

 

Thank you very much for writing such an interesting article on this topic.  This has really made me think and I hope to read more.
 
Strategic Campaigns Inc

Ray Naa replied on Sun, 2014/09/21 - 1:31pm in response to: Ray Naa

 

Wonderful illustrated information. I thank you about that. No doubt it will be very useful for my future projects. Would like to see some other posts on the same subject!
toyota innova 2015

Ray Naa replied on Mon, 2014/10/06 - 3:19pm

 

Wonderful illustrated information. I thank you about that. No doubt it will be very useful for my future projects. Would like to see some other posts on the same subject!
 
Make money online

Ray Naa replied on Fri, 2014/10/10 - 2:42pm

 

I found that site very usefull and this survey is very cirious, I ' ve never seen a blog that demand a survey for this actions, very curious...
 
http://www.slideshare.net/gfasreviews/

Ray Naa replied on Sat, 2014/10/11 - 5:30am

 

Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. Any way I'll be subscribing to your feed and I hope you post again soon. Big thanks for the useful info.
 
Vbulletin

Ray Naa replied on Wed, 2014/10/15 - 1:51pm

 

This is a great inspiring article.I am pretty much pleased with your good work.You put really very helpful information. Keep it up. Keep blogging. Looking to reading your next post.
 
www.rebelmouse.com

Ray Naa replied on Mon, 2014/10/20 - 5:57pm

 

I checked Google for real estate data, and I came across this housing market news web page, <a href="http://ochousingnews.com/">OC Housing News</a>. The analysis of real estate issues on this website has great depth, the cartoons and writing are entertaining and insightful, and the many user guides are extremely valuable. I urge exploring this website to tour what it has to offer.
 
Orange County Housing News

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.