設計目標
- **零依賴第三方庫**,純 JDK (Swing/AWT) 實現
- **封裝為
JButton 子類**,直接放入任意表單/工具欄,點擊彈出模態對話框選擇日期+小時
- 支援
Application / Applet 雙模式
- 可自定義顏色、字體、年份範圍、默認格式 (
yyyy年MM月dd日HH時)
核心類結構
public class DateChooserJButton extends JButton {
private final String prefixLabel; // 按鈕前綴文本,如 "出生日期:"
private DateChooser dateChooser; // 懶加載的彈出面板 (JPanel)
// 構造器、日期獲取/設置、事件綁定...
private class DateChooser extends JPanel implements ActionListener, ChangeListener {
// 年/月/時 Spinner、日曆網格 (6x7 JButton)、顏色主題、邏輯控制
}
}
關鍵功能模塊
1. 按鈕文本與日期雙向綁定
// 設置日期 → 更新按鈕文本
public void setDate(Date date) {
super.setText(prefixLabel + defaultFormat.format(date));
}
// 獲取日期 → 解析按鈕文本
public Date getDate() {
String text = getText().substring(prefixLabel.length());
try { return defaultFormat.parse(text); }
catch (ParseException e) { return new Date(); }
}
// 禁用父類 setText(String),強制走日期邏輯
@Override public void setText(String s) { setDate(defaultFormat.parse(s)); }
2. 彈出對話框定位邏輯
private void showDateChooser(Point screenPos) {
Frame owner = (Frame) SwingUtilities.getWindowAncestor(this);
if (dialog == null || dialog.getOwner() != owner) createDialog(owner);
dialog.setLocation(getAppropriateLocation(owner, screenPos)); // 防止超出屏幕邊界
flushWeekAndDay(); // 根據當前日期刷新日曆網格
dialog.setVisible(true);
}
private Point getAppropriateLocation(Frame owner, Point pos) {
Point p = new Point(pos);
int overflowX = pos.x + width - (owner.getX() + owner.getWidth());
int overflowY = pos.y + height - (owner.getY() + owner.getHeight());
if (overflowX > 0) p.x -= overflowX;
if (overflowY > 0) p.y -= overflowY;
return p;
}
3. 日曆網格渲染與交互
- 表頭:周日~周六 (紅/藍區分)
- 日期按鈕 6x7:
ActionCommand 編碼 0~41,點擊觸發 actionPerformed 更新選中日期並高亮
- 控制行:
JSpinner (年/月/時) + ChangeListener 連動刷新網格
- 今日高亮**:橙色背景,切換月份自動重算
4. 核心邏輯方法
// 根據年/月/當前日期重繪 6x7 網格
private void flushWeekAndDay() {
Calendar c = getCalendar(); // 基於當前按鈕日期
c.set(Calendar.DAY_OF_MONTH, 1);
int maxDay = c.getActualMaximum(Calendar.DAY_OF_MONTH);
int firstWeekDay = c.get(Calendar.DAY_OF_WEEK); // 1=周日
int dayNo = 2 - firstWeekDay; // 網格起始數字 (可為負)
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 7; j++) {
String txt = (dayNo >= 1 && dayNo <= maxDay) ? String.valueOf(dayNo) : "";
daysButton[i][j].setText(txt);
dayNo++;
}
}
highlightToday(); // 標記今日
}
// Spinner 變更監聽
public void stateChanged(ChangeEvent e) {
JSpinner src = (JSpinner) e.getSource();
Calendar c = getCalendar();
if ("Hour".equals(src.getName())) {
c.set(Calendar.HOUR_OF_DAY, (Integer) src.getValue());
setDate(c.getTime());
return;
}
// 年/月變更需重繪網格
if ("Year".equals(src.getName())) c.set(Calendar.YEAR, (Integer) src.getValue());
else c.set(Calendar.MONTH, (Integer) src.getValue() - 1);
setDate(c.getTime());
flushWeekAndDay();
}
// 日期按鈕點擊
public void actionPerformed(ActionEvent e) {
JButton btn = (JButton) e.getSource();
if (btn.getText().isEmpty()) return; // 非本月日期不可選
highlightToday(); // 先清除舊高亮
btn.setForeground(todayBackColor); // 新高亮
int day = Integer.parseInt(btn.getText());
Calendar c = getCalendar();
c.set(Calendar.DAY_OF_MONTH, day);
setDate(c.getTime());
}
完整可用代碼 (精簡版,保留核心邏輯)
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Calendar;
import java.text.*;
public class DateChooserJButton extends JButton {
private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy年MM月dd日HH時");
private final String prefix;
private DateChooser popup;
public DateChooserJButton(String prefix, Date date) {
this.prefix = prefix == null ? "" : prefix;
setDate(date);
setBorderPainted(false);
setContentAreaFilled(false);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
addActionListener(e -> showPopup());
}
public DateChooserJButton() { this("", new Date()); }
public DateChooserJButton(Date d) { this("", d); }
public void setDate(Date d) { super.setText(prefix + FMT.format(d)); }
public Date getDate() {
try { return FMT.parse(getText().substring(prefix.length())); }
catch (ParseException ex) { return new Date(); }
}
@Override public void setText(String t) { setDate(FMT.parse(t)); }
@Override public void addActionListener(ActionListener l) {} // 禁用外部監聽
private void showPopup() {
if (popup == null) popup = new DateChooser();
Point p = getLocationOnScreen();
p.y += getHeight();
popup.showAt(p);
}
private class DateChooser extends JPanel implements ActionListener, ChangeListener {
private final JSpinner ySpin, mSpin, hSpin;
private final JButton[][] days = new JButton[6][7];
private final JDialog dlg;
DateChooser() {
setLayout(new BorderLayout());
setBorder(BorderFactory.createLineBorder(Color.GRAY, 2));
add(createHeader(), BorderLayout.NORTH);
add(createCalendar(), BorderLayout.CENTER);
dlg = new JDialog((Frame) SwingUtilities.getWindowAncestor(DateChooserJButton.this), "選擇日期時間", true);
dlg.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
dlg.getContentPane().add(this);
dlg.pack();
dlg.setSize(260, 240);
}
private JPanel createHeader() {
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT));
p.setBackground(new Color(0xDDDDFF));
Calendar now = Calendar.getInstance(); now.setTime(getDate());
ySpin = new JSpinner(new SpinnerNumberModel(now.get(Calendar.YEAR), 1900, 2100, 1));
ySpin.setEditor(new JSpinner.NumberEditor(ySpin, "####")); ySpin.setName("Y");
mSpin = new JSpinner(new SpinnerNumberModel(now.get(Calendar.MONTH) + 1, 1, 12, 1)); mSpin.setName("M");
hSpin = new JSpinner(new SpinnerNumberModel(now.get(Calendar.HOUR_OF_DAY), 0, 23, 1)); hSpin.setName("H");
ySpin.addChangeListener(this); mSpin.addChangeListener(this); hSpin.addChangeListener(this);
p.add(ySpin); p.add(new JLabel("年"));
p.add(mSpin); p.add(new JLabel("月"));
p.add(hSpin); p.add(new JLabel("時"));
return p;
}
private JPanel createCalendar() {
JPanel p = new JPanel(new GridLayout(7, 7));
p.setBackground(Color.WHITE);
String[] ws = {"日","一","二","三","四","五","六"};
for (int i = 0; i < 7; i++) {
JLabel l = new JLabel(ws[i], SwingConstants.RIGHT);
l.setForeground(i == 0 || i == 6 ? Color.RED : Color.BLUE);
p.add(l);
}
for (int i = 0; i < 6; i++) for (int j = 0; j < 7; j++) {
JButton b = new JButton();
b.setBorder(null); b.setHorizontalAlignment(SwingConstants.RIGHT);
b.setActionCommand(i * 7 + j + ""); b.addActionListener(this);
b.setBackground(Color.WHITE);
b.setForeground(j == 0 || j == 6 ? Color.RED : Color.BLACK);
days[i][j] = b; p.add(b);
}
return p;
}
void showAt(Point loc) {
Frame owner = (Frame) SwingUtilities.getWindowAncestor(DateChooserJButton.this);
if (dlg.getOwner() != owner) { dlg.dispose(); dlg.setOwner(owner); }
dlg.setLocation(fitOnScreen(owner, loc));
refreshGrid();
dlg.setVisible(true);
}
private Point fitOnScreen(Frame o, Point p) {
Point r = new Point(p);
int ox = p.x + dlg.getWidth() - (o.getX() + o.getWidth());
int oy = p.y + dlg.getHeight() - (o.getY() + o.getHeight());
if (ox > 0) r.x -= ox;
if (oy > 0) r.y -= oy;
return r;
}
private void refreshGrid() {
Calendar c = Calendar.getInstance(); c.setTime(getDate());
c.set(Calendar.DAY_OF_MONTH, 1);
int max = c.getActualMaximum(Calendar.DAY_OF_MONTH);
int start = 2 - c.get(Calendar.DAY_OF_WEEK);
for (int i = 0; i < 6; i++) for (int j = 0; j < 7; j++) {
days[i][j].setText((start >= 1 && start <= max) ? String.valueOf(start) : "");
start++;
}
highlightToday();
}
private void highlightToday() {
Calendar c = Calendar.getInstance(); c.setTime(getDate());
int day = c.get(Calendar.DAY_OF_MONTH);
c.set(Calendar.DAY_OF_MONTH, 1);
int idx = day - 2 + c.get(Calendar.DAY_OF_WEEK);
int r = idx / 7, col = idx % 7;
for (int i = 0; i < 6; i++) for (int j = 0; j < 7; j++)
days[i][j].setForeground((i == r && j == col) ? Color.ORANGE : (j == 0 || j == 6 ? Color.RED : Color.BLACK));
}
public void stateChanged(ChangeEvent e) {
JSpinner s = (JSpinner) e.getSource();
Calendar c = Calendar.getInstance(); c.setTime(getDate());
if ("H".equals(s.getName())) { c.set(Calendar.HOUR_OF_DAY, (Integer)s.getValue()); setDate(c.getTime()); return; }
if ("Y".equals(s.getName())) c.set(Calendar.YEAR, (Integer)s.getValue());
else c.set(Calendar.MONTH, (Integer)s.getValue() - 1);
setDate(c.getTime());
refreshGrid();
}
public void actionPerformed(ActionEvent e) {
JButton b = (JButton) e.getSource();
if (b.getText().isEmpty()) return;
highlightToday();
b.setForeground(Color.ORANGE);
Calendar c = Calendar.getInstance(); c.setTime(getDate());
c.set(Calendar.DAY_OF_MONTH, Integer.parseInt(b.getText()));
setDate(c.getTime());
}
}
// 測試主程序
public static void main(String[] args) {
JFrame f = new JFrame("測試日期選擇器");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLayout(new FlowLayout());
f.add(new DateChooserJButton("開始:", new Date()));
f.add(new DateChooserJButton("結束:", new Date()));
f.pack(); f.setLocationRelativeTo(null); f.setVisible(true);
}
}
使用示例
// 表單中直接添加
formPanel.add(new DateChooserJButton("報名截止:", new Date()));
// 獲取用戶選擇的日期
Date deadline = ((DateChooserJButton) component).getDate();
優缺點與擴展方向
| 優點 | 局限 | 擴展建議 |
| 無外部依賴、體積極小 (~8KB) | 僅支援 日期+小時,無分鐘/秒 | 增加 minuteSpin、secondSpin |
| 封裝度高,像原生組件一樣用 | 外觀固定,難以深度定制主題 | 暴露 Color/Font 設置器、支援 Look&Feel |
| 模態對話框阻塞流程,邏輯簡單 | Applet 簽名權限可能受限 | 提供非模態 JPopupMenu 版本 |