首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 簡易日曆程式實作:GregorianCalendar 與 Swing 介面整合教學

     編輯:Java 程式設計入門

雖然現代 Java 建議使用 `java.time` (JSR-310) 進行日期時間處理,但理解傳統 `java.util.Calendar` / `GregorianCalendar` 的運作邏輯,對於維護舊系統、閱讀經典教學代碼仍有必要。本文將從零構建一個具備「月份切換、今日高亮、日期點擊事件」功能的 Swing 日曆元件,並對比新舊 API 寫法,幫助開發者平滑遷移。

核心邏輯:日曆網格生成算法

日曆本質是一個 **6 行 × 7 列** 的二維網格 (最多 6 週)。生成關鍵在於確定:**該月 1 號是星期幾**、**該月總天數**。

/** 純邏輯類:生成指定年月的日曆網格數據 (與 UI 無關) */
public class CalendarModel {
    private final int year;
    private final int month; // 1-12
    
    public CalendarModel(int year, int month) {
        this.year = year;
        this.month = month;
    }
    
    /** 返回 6x7 陣列,0 代表非本月日期 (灰顯) */
    public int[][] getMonthGrid() {
        // 1. 使用 GregorianCalendar 獲取關鍵資訊
        // 注意: Calendar.MONTH 為 0-based (0=一月)
        Calendar cal = new GregorianCalendar(year, month - 1, 1);
        
        int firstDayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=星期日 ... 7=星期六
        int daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
        
        // 2. 計算上月尾巴天數 (用於填充第一行前方空格)
        Calendar prevCal = new GregorianCalendar(year, month - 1, 1);
        prevCal.add(Calendar.DAY_OF_MONTH, -1);
        int prevMonthDays = prevCal.getActualMaximum(Calendar.DAY_OF_MONTH);
        
        // 3. 填充 6x7 網格
        int[][] grid = new int[6][7];
        int day = 1;
        boolean nextMonth = false;
        
        for (int row = 0; row < 6; row++) {
            for (int col = 0; col < 7; col++) {
                // 星期對齊: col 0=星期日 ... col 6=星期六
                if (row == 0 && col < firstDayOfWeek - 1) {
                    // 上月日期 (負數標記或單獨標記)
                    grid[row][col] = -(prevMonthDays - (firstDayOfWeek - 2 - col));
                } else if (day <= daysInMonth) {
                    grid[row][col] = day++;
                } else {
                    // 下月日期 (負數或 > daysInMonth 標記)
                    grid[row][col] = -(day++ - daysInMonth); // 1, 2, 3...
                    nextMonth = true;
                }
            }
            if (nextMonth && day > daysInMonth + 7) break; // 提前結束
        }
        return grid;
    }
    
    public int getYear() { return year; }
    public int getMonth() { return month; }
    public String getMonthTitle() { 
        return String.format("%d 年 %02d 月", year, month); 
    }
}

Swing UI 實作:JPanel + GridLayout + 自訂渲染

public class CalendarPanel extends JPanel {
    private CalendarModel model;
    private JLabel titleLabel;
    private JPanel gridPanel; // 6x7 GridLayout
    private Consumer dateClickListener; // 日期點擊回調
    
    public CalendarPanel() {
        // 初始化為本月
        LocalDate now = LocalDate.now(); // 現代 API 獲取當前日期
        this.model = new CalendarModel(now.getYear(), now.getMonthValue());
        
        setLayout(new BorderLayout(5, 5));
        setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        
        // 頂部標題 + 導航按鈕
        JPanel header = new JPanel(new BorderLayout());
        JButton prevBtn = new JButton("◀");
        JButton nextBtn = new JButton("▶");
        titleLabel = new JLabel(model.getMonthTitle(), SwingConstants.CENTER);
        titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f));
        
        prevBtn.addActionListener(e -> navigate(-1));
        nextBtn.addActionListener(e -> navigate(1));
        
        header.add(prevBtn, BorderLayout.WEST);
        header.add(titleLabel, BorderLayout.CENTER);
        header.add(nextBtn, BorderLayout.EAST);
        add(header, BorderLayout.NORTH);
        
        // 中央網格
        gridPanel = new JPanel(new GridLayout(0, 7, 2, 2)); // 7列, 間距2
        // 星期標題行
        String[] weekDays = {"日", "一", "二", "三", "四", "五", "六"};
        for (String w : weekDays) {
            JLabel lbl = new JLabel(w, SwingConstants.CENTER);
            lbl.setFont(lbl.getFont().deriveFont(Font.BOLD, 12f));
            lbl.setForeground(Color.GRAY);
            gridPanel.add(lbl);
        }
        add(gridPanel, BorderLayout.CENTER);
        
        refreshGrid();
    }
    
    private void navigate(int delta) {
        // 使用現代 API 進行月份運算,再轉回模型
        LocalDate current = LocalDate.of(model.getYear(), model.getMonth(), 1);
        LocalDate target = current.plusMonths(delta);
        this.model = new CalendarModel(target.getYear(), target.getMonthValue());
        titleLabel.setText(model.getMonthTitle());
        refreshGrid();
    }
    
    private void refreshGrid() {
        // 保留星期標題 (前7個元件), 移除舊日期按鈕
        while (gridPanel.getComponentCount() > 7) {
            gridPanel.remove(7); // 總是移除第8個 (索引7)
        }
        
        int[][] grid = model.getMonthGrid();
        LocalDate today = LocalDate.now();
        boolean isCurrentMonth = (model.getYear() == today.getYear() && model.getMonth() == today.getMonthValue());
        
        for (int row = 0; row < grid.length; row++) {
            for (int col = 0; col < 7; col++) {
                int dayVal = grid[row][col];
                boolean isToday = (isCurrentMonth && dayVal == today.getDayOfMonth());
                boolean isCurrentMonthDay = (dayVal > 0);
                
                JButton btn = new JButton(String.valueOf(Math.abs(dayVal)));
                btn.setFocusPainted(false);
                btn.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
                btn.setFont(btn.getFont().deriveFont(13f));
                
                if (!isCurrentMonthDay) {
                    btn.setForeground(Color.LIGHT_GRAY); // 灰顯非本月
                    btn.setEnabled(false);
                } else {
                    btn.setForeground(Color.BLACK);
                    if (isToday) {
                        btn.setBackground(new Color(0xBBDEFB)); // 今日藍底
                        btn.setBorder(BorderFactory.createLineBorder(new Color(0x1976D2), 2));
                    }
                    final int day = dayVal;
                    btn.addActionListener(e -> {
                        if (dateClickListener != null) dateClickListener.accept(day);
                    });
                }
                gridPanel.add(btn);
            }
        }
        gridPanel.revalidate();
        gridPanel.repaint();
    }
    
    public void setDateClickListener(Consumer listener) {
        this.dateClickListener = listener;
    }
    
    // 測試主程式
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Java 日曆元件");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            CalendarPanel cal = new CalendarPanel();
            cal.setDateClickListener(day -> 
                JOptionPane.showMessageDialog(cal, "您點選了: " + cal.model.getYear() + "-" + cal.model.getMonth() + "-" + day)
            );
            frame.add(cal);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

新舊 API 對照表:遷移指南

功能 舊 API (Calendar/GregorianCalendar) 新 API (java.time, JDK 8+) 優勢
獲取當前日期 `Calendar.getInstance()` `LocalDate.now()` / `LocalDateTime.now()` 不可變、線程安全、無時區驚喜
構建指定日期 `new GregorianCalendar(y, m-1, d)` `LocalDate.of(y, m, d)` 月份 1-based 直觀,參數順序自然
月份天數 `cal.getActualMaximum(Calendar.DAY_OF_MONTH)` `date.lengthOfMonth()` / `YearMonth.of(y,m).lengthOfMonth()` 方法語義清晰
星期幾 `cal.get(Calendar.DAY_OF_WEEK)` (1=Sun) `date.getDayOfWeek()` (Enum: MONDAY=1...SUNDAY=7) 強類型 Enum,避免魔術數字
日期加減 `cal.add(Calendar.MONTH, 1)` `date.plusMonths(1)` / `date.minusDays(5)` 不可變鏈式調用,線程安全
格式化輸出 `SimpleDateFormat` (非線程安全!) `DateTimeFormatter` (線程安全、可預編譯) 解決 `SimpleDateFormat` 經典併發 Bug
時區處理 `Calendar.setTimeZone()` / `TimeZone` `ZonedDateTime` / `OffsetDateTime` / `ZoneId` 領域模型清晰分離:本地時間/瞬間/時區

現代化重寫核心邏輯 (java.time 版)

public class ModernCalendarModel {
    private final YearMonth yearMonth; // 不可變,線程安全
    
    public ModernCalendarModel(int year, int month) {
        this.yearMonth = YearMonth.of(year, month);
    }
    
    public int[][] getMonthGrid() {
        LocalDate firstDay = yearMonth.atDay(1);
        int firstDayOfWeek = firstDay.getDayOfWeek().getValue() % 7; // MON=1->1, SUN=7->0 (調整為 0=Sun)
        // 若習慣 0=Mon,可直接用 getValue()-1
        int daysInMonth = yearMonth.lengthOfMonth();
        
        // 上月尾巴
        YearMonth prevMonth = yearMonth.minusMonths(1);
        int prevMonthDays = prevMonth.lengthOfMonth();
        
        int[][] grid = new int[6][7];
        int day = 1;
        for (int row = 0; row < 6; row++) {
            for (int col = 0; col < 7; col++) {
                if (row == 0 && col < firstDayOfWeek) {
                    grid[row][col] = -(prevMonthDays - (firstDayOfWeek - 1 - col));
                } else if (day <= daysInMonth) {
                    grid[row][col] = day++;
                } else {
                    grid[row][col] = -(day++ - daysInMonth);
                }
            }
            if (day > daysInMonth + 7) break;
        }
        return grid;
    }
    // ... getYear, getMonth, getMonthTitle 同理委託 yearMonth ...
}

總結

本文實作了一個**模型與視圖分離**、支援**月份導航、今日高亮、點擊回調**的 Swing 日曆元件。關鍵收穫:

  1. 邏輯核心: 利用 `Calendar` / `YearMonth` 獲取 `firstDayOfWeek`、`daysInMonth`,填充 6×7 網格。
  2. UI 細節: `GridLayout(0,7)` 自動換行、非本月日期灰顯禁用、今日藍底加粗邊框、Lambda 回調解耦。
  3. 必然趨勢: **新專案請直接使用 `java.time` (LocalDate, YearMonth, DateTimeFormatter)**。舊 `Calendar`/`Date`/`SimpleDateFormat` 僅用於維護 Legacy 代碼,其可變性、非線程安全、月份 0-based 設計缺陷已被 JSR-310 徹底解決。

掌握日曆網格生成算法,不僅能造出日曆元件,更能應用於排程器、日期選擇器、甘特圖等所有基於時間網格的視覺化組件開發中。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved