在桌面应用或 Web 界面开发中,日期选择(Date Picker)是一项非常常见的需求,用于让用户便捷地输入或选择日期,避免手工输入错误,并且能够灵活地限制可选范围和日期格式。Java 生态中,虽然有第三方组件(如 JCalendar、JDatePicker、SwingX 的 JXDatePicker 等),但在某些项目中出于依赖精简、定制化 UI、或学习算法与组件设计原理的需求,我们往往需要手动实现日期选择工具。
一个完整的 Java 日期选择工具,应当具备以下特点:
图形界面友好:在 Swing 或 JavaFX 中以日历形式展示,用户点击即可选定日期;
格式灵活:支持多种日期格式(例如 yyyy-MM-dd
、MM/dd/yyyy
等);
区域和范围限制:可设置最小/最大可选日期;
国际化:依系统语言显示月份和星期;
易于集成:提供简单的 API,能够嵌入任意 Swing 窗口;
键盘与鼠标操作:可通过方向键、回车键选择;
高度可定制:样式、配色、字体大小等可配置;
单元测试:测试日期计算逻辑、界面事件响应;
性能:对于跨年、跨月快速切换依然流畅。
本项目以纯 Java Swing为基础,不依赖任何第三方库,完整实现一个可复用的 DatePicker
组件,涵盖从日期数据模型、日历面板渲染、事件监听与回调、格式化与解析、最小/最大日期约束到测试与示例的全流程,适合作为技术博客和课堂案例。
日期模型
LocalDate
或使用 java.util.Calendar
存储当前选中日期;
支持最小和最大可选日期边界;
日历面板
显示当月日历网格,含周标题行和日格;
支持上一月/下一月按钮,快速切换月份;
选择交互
点击日期单元格选中该日期;
支持键盘左右、上下移动;回车键确认;
格式化显示
在文本框中显示选中日期,支持自定义 DateTimeFormatter
;
手动输入日期字符串并解析;解析错误给予用户提示;
回调监听
提供 DateChangeListener
接口,用户注册后可在日期改变时收到通知;
范围限制
对超过 minDate
/maxDate
的日期格禁止点击;灰显;
国际化与本地化
按系统默认 Locale 显示月份名和星期名;
支持动态切换 Locale;
嵌入与独立使用
既可作为独立对话框弹出,也可嵌入任意 JPanel
;
单元测试
测试日期计算(当月第一天星期几、当月天数)、边界逻辑、格式化与解析;
使用 Jemmy 或 Fest-Swing 简易模拟 UI 事件;
可维护性:模块化 MVC 设计,注释详尽;
性能:瞬时渲染,跨年快速切换不卡顿;
易用性:API 简洁,用户仅需一行代码即可嵌入;
兼容性:Java8+,纯 Swing,无额外依赖;
可定制性:暴露样式类可覆盖 UI 颜色与字体;
JComponent:所有自定义组件基类;
布局管理器:使用 BorderLayout
、GridLayout
布局日期面板;
绘制与渲染:可重写 paintComponent
做自定义绘制;
事件机制:鼠标与键盘事件监听;
java.time.LocalDate(Java8+)或 java.util.Calendar
:用于日期计算;
DateTimeFormatter:格式化/解析日期字符串;
Model:存储当前选中日期、最小/最大日期、Locale、Formatter;
View:DatePickerPanel
负责绘制日历表、输入框与按钮;
Controller:DatePickerController
处理用户交互事件并更新 Model 与 View;
Locale
与 DateTimeFormatter.ofPattern(pattern, locale)
配合获取月份与星期名称;
支持运行时切换 Locale;
JUnit 5:测试 Model 逻辑与格式化;
Swing 测试:使用 Jemmy/Fest-Swing 模拟点击与键入;
Model 设计
DateModel
类:封装 LocalDate selectedDate
、LocalDate displayMonth
、LocalDate minDate
、LocalDate maxDate
、Locale locale
、DateTimeFormatter formatter
;
提供 addDateChangeListener
与通知机制;
View 设计
DatePicker
继承 JComponent
或 JPanel
,内部包含:
顶部:上一月 (<
)、月份标题(combo 或 label)、下一月 (>
) 按钮;
中部:7 列 6 行 JButton
或自定义标签网格显示每个日期;
底部:JTextField
用于显示和手动输入日期;
Controller 设计
监听按钮点击切换月份;
监听日期单元格点击选中日期;
监听 JTextField
的 ActionListener
,解析输入字符串并更新 Model;
监听键盘方向键在日历网格中移动焦点;
日期计算
使用 LocalDate.withDayOfMonth(1)
获取当月第一天,getDayOfWeek()
确定第一格偏移;
lengthOfMonth()
获取当月天数,迭代填充日格;
样式与定制
通过 UIManager
或 DatePickerStyle
类提供字体和颜色定制接口;
支持高亮当前选中日期、灰显不可选日期;
国际化
model.setLocale(locale)
后重新渲染 View;
命令行示例
提供 DatePickerDemo
类带 main
方法,弹出对话框展示组件;
测试
测试 DateModel
:当切换月份后 displayMonth
正确;
测试格式化与解析:不同 pattern
与 locale
;
模拟 UI:点击日期按钮后 selectedDate
更新;
打包与集成
使用 Maven 或 Gradle 打包,可发布到私服;
// File: DateModel.java
package com.example.datepicker;
import java.time.*;
import java.time.format.*;
import java.util.*;
/**
* 日期模型,封装当前选择与显示,以及最小/大可选日期和格式化
*/
public class DateModel {
private LocalDate selectedDate;
private LocalDate displayMonth;
private LocalDate minDate, maxDate;
private Locale locale;
private DateTimeFormatter formatter;
private final List listeners = new ArrayList<>();
public DateModel(LocalDate initialDate, LocalDate minDate, LocalDate maxDate, Locale locale, String pattern) {
this.selectedDate = initialDate;
this.displayMonth = initialDate.withDayOfMonth(1);
this.minDate = minDate;
this.maxDate = maxDate;
this.locale = locale;
this.formatter = DateTimeFormatter.ofPattern(pattern, locale);
}
public LocalDate getSelectedDate() { return selectedDate; }
public LocalDate getDisplayMonth() { return displayMonth; }
public void setDisplayMonth(LocalDate firstOfMonth) { this.displayMonth = firstOfMonth; fireDateChange(); }
public boolean isInRange(LocalDate date) {
return (minDate == null || !date.isBefore(minDate)) && (maxDate == null || !date.isAfter(maxDate));
}
public void setSelectedDate(LocalDate date) {
this.selectedDate = date; this.displayMonth = date.withDayOfMonth(1); fireDateChange();
}
public String formatDate(LocalDate date) { return date.format(formatter); }
public LocalDate parseDate(String text) { return LocalDate.parse(text, formatter); }
public void setLocale(Locale locale) {
this.locale = locale;
this.formatter = DateTimeFormatter.ofPattern(formatter.toString(), locale);
fireDateChange();
}
public void addDateChangeListener(DateChangeListener l) { listeners.add(l); }
private void fireDateChange() {
for (DateChangeListener l : listeners) l.dateChanged();
}
public interface DateChangeListener { void dateChanged(); }
}
// File: DatePicker.java
package com.example.datepicker;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.time.*;
import java.time.format.*;
import java.util.Locale;
/**
* DatePicker 组件,基于 Swing 实现
*/
public class DatePicker extends JPanel {
private final DateModel model;
private final JButton prevButton = new JButton("<");
private final JButton nextButton = new JButton(">");
private final JLabel monthLabel = new JLabel("", SwingConstants.CENTER);
private final JPanel calendarPanel = new JPanel(new GridLayout(7, 7));
private final JTextField dateField = new JTextField(10);
private final JButton[] dayButtons = new JButton[42];
public DatePicker(DateModel model) {
this.model = model;
setLayout(new BorderLayout());
initHeader();
initCalendar();
initFooter();
model.addDateChangeListener(this::updateView);
updateView();
}
private void initHeader() {
JPanel header = new JPanel(new BorderLayout());
header.add(prevButton, BorderLayout.WEST);
header.add(monthLabel, BorderLayout.CENTER);
header.add(nextButton, BorderLayout.EAST);
add(header, BorderLayout.NORTH);
prevButton.addActionListener(e -> navigateMonth(-1));
nextButton.addActionListener(e -> navigateMonth(1));
}
private void initCalendar() {
// 增加星期标题行
DateTimeFormatter dayFmt = DateTimeFormatter.ofPattern("E", model.locale);
for (DayOfWeek dow : DayOfWeek.values()) {
calendarPanel.add(new JLabel(dow.getDisplayName(TextStyle.SHORT, model.locale), SwingConstants.CENTER));
}
// 日按钮格
for (int i = 0; i < dayButtons.length; i++) {
JButton b = new JButton();
b.setMargin(new Insets(2,2,2,2));
b.addActionListener(this::dayButtonClicked);
dayButtons[i] = b;
calendarPanel.add(b);
}
add(calendarPanel, BorderLayout.CENTER);
}
private void initFooter() {
JPanel footer = new JPanel();
footer.add(dateField);
JButton parseBtn = new JButton("Set Date");
footer.add(parseBtn);
add(footer, BorderLayout.SOUTH);
parseBtn.addActionListener(e -> parseDateField());
}
private void navigateMonth(int delta) {
model.setDisplayMonth(model.getDisplayMonth().plusMonths(delta));
}
private void dayButtonClicked(ActionEvent e) {
JButton src = (JButton)e.getSource();
if (!src.isEnabled()) return;
int day = Integer.parseInt(src.getText());
LocalDate date = model.getDisplayMonth().withDayOfMonth(day);
model.setSelectedDate(date);
}
private void parseDateField() {
try {
LocalDate d = model.parseDate(dateField.getText());
if (model.isInRange(d)) model.setSelectedDate(d);
} catch (DateTimeParseException ex) {
JOptionPane.showMessageDialog(this, "无效日期格式", "错误", JOptionPane.ERROR_MESSAGE);
}
}
private void updateView() {
LocalDate first = model.getDisplayMonth();
monthLabel.setText(first.getMonth().getDisplayName(TextStyle.FULL, model.locale) + " " + first.getYear());
// 更新日期字段
dateField.setText(model.formatDate(model.getSelectedDate()));
// Calendar grid
int startDow = first.getDayOfWeek().getValue() % 7; // Sunday=0
int days = first.lengthOfMonth();
for (int i = 0; i < 42; i++) {
JButton b = dayButtons[i];
int dayNum = i - startDow + 1;
if (dayNum >= 1 && dayNum <= days) {
b.setText(String.valueOf(dayNum));
LocalDate d = first.withDayOfMonth(dayNum);
b.setEnabled(model.isInRange(d));
b.setBackground(d.equals(model.getSelectedDate()) ? Color.CYAN : Color.WHITE);
} else {
b.setText("");
b.setEnabled(false);
b.setBackground(Color.LIGHT_GRAY);
}
}
}
}
// File: DatePickerDemo.java
package com.example.datepicker;
import javax.swing.*;
import java.time.*;
import java.util.Locale;
/**
* 演示主程序,展示 DatePicker 组件
*/
public class DatePickerDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
DateModel model = new DateModel(
LocalDate.now(),
LocalDate.of(1900,1,1),
LocalDate.of(2100,12,31),
Locale.getDefault(),
"yyyy-MM-dd");
DatePicker picker = new DatePicker(model);
model.addDateChangeListener(() ->
System.out.println("Selected Date: " + model.getSelectedDate()));
JFrame frame = new JFrame("DatePicker Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(picker);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
// File: DateModelTest.java
package com.example.datepicker;
import org.junit.jupiter.api.*;
import java.time.*;
import java.time.format.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* JUnit 测试 DateModel 逻辑
*/
public class DateModelTest {
private DateModel model;
@BeforeEach
void setup() {
model = new DateModel(
LocalDate.of(2020,2,15),
LocalDate.of(2020,1,1),
LocalDate.of(2020,12,31),
Locale.US,
"MM/dd/yyyy");
}
@Test
void testFormatParse() {
String s = model.formatDate(LocalDate.of(2020,5,10));
assertEquals("05/10/2020", s);
LocalDate d = model.parseDate(s);
assertEquals(LocalDate.of(2020,5,10), d);
}
@Test
void testRange() {
assertTrue(model.isInRange(LocalDate.of(2020,6,1)));
assertFalse(model.isInRange(LocalDate.of(2019,12,31)));
}
@Test
void testListener() {
final boolean[] called = {false};
model.addDateChangeListener(() -> called[0]=true);
model.setSelectedDate(LocalDate.of(2020,3,1));
assertTrue(called[0]);
}
}
DateModel:MVC 中的 Model 层,封装选中日期、显示月份、最小/最大约束、Locale 与格式化器,提供监听机制通知 View 更新。
DatePicker:View+Controller 层,基于 Swing 构建日历面板,包含上一月/下一月按钮、星期标题行、42 个日按钮、日期文本框和解析按钮;响应用户点击、键盘输入、月份导航,并调用 Model 更新,最后刷新视图。
DatePickerDemo:演示用主程序,初始化 Model 与 DatePicker 并添加到 JFrame
,展示组件。
DateModelTest:JUnit 5 测试 Model 层功能,包括格式化/解析、范围判断与监听通知。
本项目基于纯 Java Swing 实现了一个功能完备、可定制化的 DatePicker
日期选择工具,包含:
MVC 设计:清晰分离 Model、View 与 Controller;
国际化支持:根据 Locale 显示月份和星期名称;
范围限制:禁用不可选日期;
格式化与解析:支持自定义 DateTimeFormatter
;
用户交互:鼠标点击、键盘导航、文本输入多种方式;
易于集成:提供简单的组件和监听接口;
测试覆盖:完整测试 Model 层核心逻辑;
Q1:如何嵌入到已有 Swing 窗口?
A:将 DatePicker
实例添加到任意容器(如 JPanel
、JDialog
)即可。
Q2:如何更改日期格式?
A:构造 DateModel
时传入不同的 pattern
,并支持运行时 setLocale
。
Q3:如何处理键盘选择?
A:可在 DatePicker
中为 dayButtons
添加 KeyListener
,监听方向键和回车。
Q4:如何美化样式?
A:可调用 UIManager.put(...)
或在 DatePicker
提供样式接口覆写按钮背景、字体等。
Q5:支持时间选择吗?
A:当前仅日期,如需时间,可扩展 TimePicker
或在底部加入时间输入字段。
JavaFX 实现:使用 JavaFX 的 DatePicker
控件或自定义 CalendarView
;
无阻塞 UI:异步加载大量事件(如标记节假日)不会卡顿;
Web 集成:将组件打包为 SwingApplet 或基于 Vaadin/JavaFX WebView 实现 Web 版;
高级定制:支持多日期区间选择、范围高亮、禁用周末/节假日;
脚本化样式:引入 CSS-like 样式表自定义组件外观;
单元与 UI 测试:使用 Fest-Swing 或 Jemmy 自动化验证组件交互;
移动端适配:使用 JavaFXPorts 或 Gluon Mobile 在移动端渲染日期选择控件。