java:实现日期选择工具(附带源码)

1. 项目背景详细介绍

在桌面应用或 Web 界面开发中,日期选择(Date Picker)是一项非常常见的需求,用于让用户便捷地输入或选择日期,避免手工输入错误,并且能够灵活地限制可选范围和日期格式。Java 生态中,虽然有第三方组件(如 JCalendar、JDatePicker、SwingX 的 JXDatePicker 等),但在某些项目中出于依赖精简、定制化 UI、或学习算法与组件设计原理的需求,我们往往需要手动实现日期选择工具

一个完整的 Java 日期选择工具,应当具备以下特点:

  • 图形界面友好:在 Swing 或 JavaFX 中以日历形式展示,用户点击即可选定日期;

  • 格式灵活:支持多种日期格式(例如 yyyy-MM-ddMM/dd/yyyy 等);

  • 区域和范围限制:可设置最小/最大可选日期;

  • 国际化:依系统语言显示月份和星期;

  • 易于集成:提供简单的 API,能够嵌入任意 Swing 窗口;

  • 键盘与鼠标操作:可通过方向键、回车键选择;

  • 高度可定制:样式、配色、字体大小等可配置;

  • 单元测试:测试日期计算逻辑、界面事件响应;

  • 性能:对于跨年、跨月快速切换依然流畅。

本项目以纯 Java Swing为基础,不依赖任何第三方库,完整实现一个可复用的 DatePicker 组件,涵盖从日期数据模型日历面板渲染事件监听与回调格式化与解析最小/最大日期约束测试与示例的全流程,适合作为技术博客和课堂案例。


2. 项目需求详细介绍

功能需求

  1. 日期模型

    • LocalDate 或使用 java.util.Calendar 存储当前选中日期;

    • 支持最小和最大可选日期边界;

  2. 日历面板

    • 显示当月日历网格,含周标题行和日格;

    • 支持上一月/下一月按钮,快速切换月份;

  3. 选择交互

    • 点击日期单元格选中该日期;

    • 支持键盘左右、上下移动;回车键确认;

  4. 格式化显示

    • 在文本框中显示选中日期,支持自定义 DateTimeFormatter

    • 手动输入日期字符串并解析;解析错误给予用户提示;

  5. 回调监听

    • 提供 DateChangeListener 接口,用户注册后可在日期改变时收到通知;

  6. 范围限制

    • 对超过 minDate/maxDate 的日期格禁止点击;灰显;

  7. 国际化与本地化

    • 按系统默认 Locale 显示月份名和星期名;

    • 支持动态切换 Locale;

  8. 嵌入与独立使用

    • 既可作为独立对话框弹出,也可嵌入任意 JPanel

  9. 单元测试

    • 测试日期计算(当月第一天星期几、当月天数)、边界逻辑、格式化与解析;

    • 使用 Jemmy 或 Fest-Swing 简易模拟 UI 事件;

非功能需求

  • 可维护性:模块化 MVC 设计,注释详尽;

  • 性能:瞬时渲染,跨年快速切换不卡顿;

  • 易用性:API 简洁,用户仅需一行代码即可嵌入;

  • 兼容性:Java8+,纯 Swing,无额外依赖;

  • 可定制性:暴露样式类可覆盖 UI 颜色与字体;


3. 相关技术详细介绍

3.1 Java Swing 基础

  • JComponent:所有自定义组件基类;

  • 布局管理器:使用 BorderLayoutGridLayout 布局日期面板;

  • 绘制与渲染:可重写 paintComponent 做自定义绘制;

  • 事件机制:鼠标与键盘事件监听;

3.2 日期 API

  • java.time.LocalDate(Java8+)或 java.util.Calendar:用于日期计算;

  • DateTimeFormatter:格式化/解析日期字符串;

3.3 MVC 设计模式

  • Model:存储当前选中日期、最小/最大日期、Locale、Formatter;

  • ViewDatePickerPanel 负责绘制日历表、输入框与按钮;

  • ControllerDatePickerController 处理用户交互事件并更新 Model 与 View;

3.4 本地化与国际化

  • LocaleDateTimeFormatter.ofPattern(pattern, locale) 配合获取月份与星期名称;

  • 支持运行时切换 Locale;

3.5 单元测试

  • JUnit 5:测试 Model 逻辑与格式化;

  • Swing 测试:使用 Jemmy/Fest-Swing 模拟点击与键入;


4. 实现思路详细介绍

  1. Model 设计

    • DateModel 类:封装 LocalDate selectedDateLocalDate displayMonthLocalDate minDateLocalDate maxDateLocale localeDateTimeFormatter formatter

    • 提供 addDateChangeListener 与通知机制;

  2. View 设计

    • DatePicker 继承 JComponentJPanel,内部包含:

      • 顶部:上一月 (<)、月份标题(combo 或 label)、下一月 (>) 按钮;

      • 中部:7 列 6 行 JButton 或自定义标签网格显示每个日期;

      • 底部:JTextField 用于显示和手动输入日期;

  3. Controller 设计

    • 监听按钮点击切换月份;

    • 监听日期单元格点击选中日期;

    • 监听 JTextFieldActionListener,解析输入字符串并更新 Model;

    • 监听键盘方向键在日历网格中移动焦点;

  4. 日期计算

    • 使用 LocalDate.withDayOfMonth(1) 获取当月第一天,getDayOfWeek() 确定第一格偏移;

    • lengthOfMonth() 获取当月天数,迭代填充日格;

  5. 样式与定制

    • 通过 UIManagerDatePickerStyle 类提供字体和颜色定制接口;

    • 支持高亮当前选中日期、灰显不可选日期;

  6. 国际化

    • model.setLocale(locale) 后重新渲染 View;

  7. 命令行示例

    • 提供 DatePickerDemo 类带 main 方法,弹出对话框展示组件;

  8. 测试

    • 测试 DateModel:当切换月份后 displayMonth 正确;

    • 测试格式化与解析:不同 patternlocale

    • 模拟 UI:点击日期按钮后 selectedDate 更新;

  9. 打包与集成

    • 使用 Maven 或 Gradle 打包,可发布到私服;


5. 完整实现代码

// 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]);
    }
}

6. 代码详细解读

  • DateModel:MVC 中的 Model 层,封装选中日期、显示月份、最小/最大约束、Locale 与格式化器,提供监听机制通知 View 更新。

  • DatePicker:View+Controller 层,基于 Swing 构建日历面板,包含上一月/下一月按钮、星期标题行、42 个日按钮、日期文本框和解析按钮;响应用户点击、键盘输入、月份导航,并调用 Model 更新,最后刷新视图。

  • DatePickerDemo:演示用主程序,初始化 Model 与 DatePicker 并添加到 JFrame,展示组件。

  • DateModelTest:JUnit 5 测试 Model 层功能,包括格式化/解析、范围判断与监听通知。


7. 项目详细总结

本项目基于纯 Java Swing 实现了一个功能完备、可定制化DatePicker 日期选择工具,包含:

  1. MVC 设计:清晰分离 Model、View 与 Controller;

  2. 国际化支持:根据 Locale 显示月份和星期名称;

  3. 范围限制:禁用不可选日期;

  4. 格式化与解析:支持自定义 DateTimeFormatter

  5. 用户交互:鼠标点击、键盘导航、文本输入多种方式;

  6. 易于集成:提供简单的组件和监听接口;

  7. 测试覆盖:完整测试 Model 层核心逻辑;


8. 项目常见问题及解答

Q1:如何嵌入到已有 Swing 窗口?
A:将 DatePicker 实例添加到任意容器(如 JPanelJDialog)即可。

Q2:如何更改日期格式?
A:构造 DateModel 时传入不同的 pattern,并支持运行时 setLocale

Q3:如何处理键盘选择?
A:可在 DatePicker 中为 dayButtons 添加 KeyListener,监听方向键和回车。

Q4:如何美化样式?
A:可调用 UIManager.put(...) 或在 DatePicker 提供样式接口覆写按钮背景、字体等。

Q5:支持时间选择吗?
A:当前仅日期,如需时间,可扩展 TimePicker 或在底部加入时间输入字段。


9. 扩展方向与性能优化

  1. JavaFX 实现:使用 JavaFX 的 DatePicker 控件或自定义 CalendarView

  2. 无阻塞 UI:异步加载大量事件(如标记节假日)不会卡顿;

  3. Web 集成:将组件打包为 SwingApplet 或基于 Vaadin/JavaFX WebView 实现 Web 版;

  4. 高级定制:支持多日期区间选择、范围高亮、禁用周末/节假日;

  5. 脚本化样式:引入 CSS-like 样式表自定义组件外观;

  6. 单元与 UI 测试:使用 Fest-Swing 或 Jemmy 自动化验证组件交互;

  7. 移动端适配:使用 JavaFXPorts 或 Gluon Mobile 在移动端渲染日期选择控件。

你可能感兴趣的:(Java,实战项目,java,开发语言)