Creating a DateChooser Control with JavaFX 2.0
Join the DZone community and get the full member experience.
Join For FreeI 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!
Opinions expressed by DZone contributors are their own.
Comments