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!| Attachment | Size |
|---|---|
| datechooser.PNG | 22.5 KB |
| DateChooser.zip | 26.65 KB |
(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
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
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
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,