设计目标
设计的这个日历既可以捕捉当天的日期,并且使用红色圆圈圈起来以及实时具体时间,而且将月份和年份设计了下拉框,可以自行选择具体的月份年份,也可以通过<和>两个按键实现对月份的转变,同时年份和月份的显示会随着按键转变而转变,本月日期用黑色标记清楚,其他月份日期用灰色标出。
实现功能
一、日历展示功能
- 可视化日历:创建一个可视化的日历窗口,展示特定月份的日历布局,包括星期几的标题行和日期格子。
- 月份切换:通过 “<” 和 “>” 按钮可以切换到上一个月或下一个月的日历。同时,也可以通过年和月的下拉框选择特定的年份和月份来显示相应的日历。
- 突出显示今天:在日历中,用红色圆形突出显示当天的日期,方便用户快速识别。
- 显示月份名称:在窗口顶部显示当前月份的中文名称,如 “一月”“二月” 等。
- 日期范围调整:对于当前月份之前的日期(上一个月的日期),以灰色字体显示在日历的开头部分;对于当前月份之后的日期(下一个月的占位符),也以灰色字体显示在日历的结尾部分。
二、年份选择功能
年下拉框:创建一个包含当前年份前后各 20 年的年份下拉框,用户可以从下拉框中选择特定的年份,选择后会自动更新显示相应年份的日历。
三、月份选择功能
月下拉框:创建一个包含十二个中文月份名称的下拉框,用户可以选择不同的月份来显示对应月份的日历。
四、实时时间显示功能
时间更新:使用定时器每秒钟更新一次窗口标题,显示当前的时间,格式为 “yyyy-MM-dd HH:mm:ss”,并带有时区设置为 “GMT+8”。
五、窗口布局和定位功能
- 窗口布局:使用
BorderLayout
布局将窗口分为顶部和中央两部分,顶部放置年、月下拉框、按钮和年份标签,中央放置日历面板。 - 窗口定位:在程序启动时,自动计算窗口在屏幕中的位置,使其居中显示。
设计类的思想
- 可视化日历应用:CalendarMain类旨在创建一个可视化的日历应用程序,提供用户交互功能,允许用户通过年、月下拉框和按钮切换不同的月份和年份,同时实时显示当前时间。
- 模块化设计:将不同的功能模块分离,如年、月下拉框的创建和事件处理、日历面板的生成、时间更新等分别封装在不同的方法中,提高代码的可读性和可维护性。
- 用户交互性:通过提供直观的用户界面,包括按钮和下拉框,使用户能够方便地浏览不同的日期。同时,通过自定义的圆形标签突出显示今天的日期,增强用户体验。
CalendarMain
类
整体设计目标
CalendarMain
类的主要目标是创建一个具有图形用户界面(GUI)的日历应用程序。它允许用户查看指定月份和年份的日历信息,能够切换月份,并且在窗口标题中实时显示当前时间。
成员变量设计思想
monthLabel
:用于直观地向用户展示当前所查看日历的月份信息。通过JLabel
来实现,初始化为特定的月份字符串(如 "October 2023"),后续会根据用户的操作(如切换月份)动态更新其显示内容。yearLabel
:与monthLabel
类似,用于显示当前年份。同样是JLabel
类型,其显示内容会随着用户在年下拉框中的选择或月份切换等操作而更新。calendarPanel
:这是整个日历应用程序的核心显示区域,通过JPanel
实现,并设置为GridLayout(7, 7)
的网格布局。该布局用于以一种规则的方式展示日历中的星期标题以及每个月的日期信息等内容。currentMonth
和currentYear
:分别用于记录当前所显示日历的月份索引(0 - 11 分别对应一月到十二月)和年份信息。这两个变量在程序运行过程中会根据用户的操作(如切换月份、选择年份)不断更新,以便准确显示相应的日历内容。currentTime
:用于存储当前时间的字符串表示。通过定时器定期更新这个变量的值,并将其用于更新窗口标题,以实现实时显示当前时间的功能。yearComboBox
和yearComboBox
:这两个下拉框组件分别用于用户选择年份和月份。yearComboBox
包含了从当前年份前后各 20 年的年份选项,方便用户在较大时间范围内选择想要查看日历的年份。monthComboBox
则包含了中文月份名称数组中的各个月份名称,使用户能够以更直观的方式选择月份。
构造函数设计思想
- 在
CalendarMain
的构造函数中,首先进行了一系列窗口基本设置:- 创建并初始化
monthLabel
和yearLabel
,设置窗口标题、大小以及关闭操作等,为整个应用程序的窗口外观和基本行为奠定基础。 - 设置窗口布局为
BorderLayout
,这种布局方式便于将不同功能的组件放置在窗口的不同区域(如顶部、中央等),使得界面布局更加清晰和有条理。 - 通过获取屏幕尺寸并计算,将窗口设置为在屏幕居中显示,提供更好的用户体验。
- 创建并初始化
- 接着,获取当前时间的日历对象,并从中提取当前月份索引和年份信息,用于初始化
currentMonth
和currentYear
变量,确保应用程序一开始显示的是当前时间对应的日历信息。 - 然后,分别创建并设置年下拉框和月下拉框:
- 对于年下拉框
yearComboBox
,通过循环添加从当前年份前后各 20 年的年份选项,设置初始选中项为当前年份,并为其添加动作监听器。当用户选择不同年份时,监听器会触发相应的操作,更新currentYear
变量并重新显示日历内容。 - 对于月下拉框
monthComboBox
,创建包含中文月份名称的数组,并将各个月份名称添加到下拉框中,设置初始选中项为当前月份对应的中文名称。同样为其添加动作监听器,当用户选择不同月份时,监听器会触发更新currentMonth
变量并重新显示日历的操作。
- 对于年下拉框
- 之后,创建顶部面板
topPanel
,并设置其布局为FlowLayout
,用于放置上一个月按钮、年下拉框、月下拉框、年份标签以及下一个月按钮等组件。为上一个月按钮和下一个月按钮分别添加动作监听器,点击按钮时会调用changeMonth
方法来实现月份的切换操作。 - 最后,创建用于显示日历内容的
calendarPanel
,将其添加到窗口的中央区域,并调用displayCalendar
方法首次显示日历内容。同时,创建一个定时器,每 1000 毫秒(1 秒)触发一次updateTime
方法,用于更新窗口标题显示的时间。
CircleLabel
类(内部静态类)
整体设计目标
CircleLabel
类是一个自定义的标签类,继承自 JLabel
。其主要设计目标是在日历应用程序中,针对表示当天日期的标签进行特殊绘制,使其呈现出圆形边框的效果,以便用户能够更直观地识别当天日期。
成员变量设计思想
isToday
:这个布尔类型的变量用于标记该标签所表示的日期是否为今天。通过这个变量,在绘制标签时可以根据其值来决定是否绘制红色圆形边框,从而实现对今天日期的特殊标识。
构造函数设计思想
- 在构造函数
CircleLabel(String text, boolean isToday)
中:- 首先调用父类
JLabel
的构造函数,设置文本居中显示,确保日期文本在标签内的显示效果良好。 - 然后初始化
isToday
变量,将传入的参数值赋给它,以便后续绘制操作能够根据这个值进行相应的处理。 - 最后设置标签的字体为
Arial
,普通样式,字号为 20,统一了日期标签的字体格式,使整个日历的显示更加规范和美观。
- 首先调用父类
paintComponent
方法设计思想
- 该方法重写了父类
JLabel
的paintComponent
方法。 - 首先,调用父类的
paintComponent
方法进行默认绘制,保证标签的基本显示内容(如日期文本)能够正常显示。 - 然后,根据
isToday
变量的值来决定是否进行特殊绘制:如果isToday
为true
,则将绘制图形的上下文转换为Graphics2D
,并设置抗锯齿渲染提示,使绘制的圆形更平滑。接着,通过获取标签的宽度和高度,计算出合适的圆形直径以及圆心坐标,设置绘制颜色为红色,最后绘制出红色圆形边框,从而实现了对今天日期的特殊标识效果。
displayCalendar
方法设计思想
- 该方法的主要目的是根据当前的
currentYear
和currentMonth
变量的值,准确地在calendarPanel
上显示相应月份的日历信息。 - 首先,清除
calendarPanel
上的所有组件,为重新显示新的日历内容做好准备。 - 然后,创建当前月份的日历对象和上一个月的日历对象,用于获取相关的日历信息,如当前月的第一天是星期几、当前月和上一个月的总天数等。
- 接着,更新
monthLabel
和yearLabel
的显示内容,使其准确反映当前所查看的月份和年份信息。 - 之后,添加星期标题行,通过循环将星期的中文简称(如 “一”、“二” 等)添加到
calendarPanel
上作为日历的标题行。 - 再根据获取到的当前月的第一天是星期几以及当前月和上一个月的总天数等信息,分三步添加日历中的日期信息:
- 添加上一个月的空白日期(灰色显示):通过循环,根据当前月第一天是星期几的信息,计算出需要添加上一个月的哪些空白日期,并将这些日期以灰色显示的方式添加到
calendarPanel
上。 - 添加当前月的日期:通过循环遍历当前月的所有日期,对于每个日期,判断其是否为今天,如果是则使用
CircleLabel
类创建带有圆形边框标识的标签,否则使用普通的JLabel
创建标签,然后将这些标签添加到calendarPanel
上。 - 填充后续的空白日期(灰色显示):根据日历布局的规则以及当前月的总天数和第一天是星期几等信息,计算出需要填充的后续空白日期,并将这些日期以灰色显示的方式添加到
calendarPanel
上。
- 添加上一个月的空白日期(灰色显示):通过循环,根据当前月第一天是星期几的信息,计算出需要添加上一个月的哪些空白日期,并将这些日期以灰色显示的方式添加到
- 最后,重新验证和重绘
calendarPanel
,确保新添加的组件能够正确显示在窗口中,并且显示效果良好。
changeMonth
方法设计思想
- 该方法用于实现月份的切换功能。当用户点击上一个月按钮或下一个月按钮时,会传入正负 1 的参数值来表示切换到上一个月或下一个月。
- 根据传入的参数值对
currentMonth
变量进行相应的调整:如果currentMonth
小于 0,说明要切换到上一年的 12 月份,此时将currentMonth
设为 11,同时将currentYear
减 1;如果currentMonth
大于 11,说明要切换到下一年的 1 月份,此时将currentMonth
设为 0,同时将currentYear
增 1。 - 然后,更新年下拉框和月下拉框的选中项,使其与新的
currentYear
和currentMonth
相匹配。 - 最后,调用
displayCalendar
方法重新显示日历内容,以反映出月份切换后的新情况。
main
方法设计思想
- 在
main
方法中,使用SwingUtilities.invokeLater
方法来确保在事件分发线程(EDT)中创建和显示窗口。这是因为 Swing 组件必须在 EDT 中进行创建和操作,以避免出现线程相关的问题。 - 在
invokeLater
方法内部,创建了CalendarMain
类的一个实例,并设置其可见,从而启动了整个日历应用程序,使得用户可以通过图形界面查看日历信息、切换月份以及看到实时更新的时间显示。
updateTime
方法设计思想
- 该方法的主要目的是更新窗口标题显示的时间。
- 首先,创建一个
SimpleDateFormat
格式器,设置其格式为 "yyyy-MM-dd HH:mm:ss",并将其时区设置为 "GMT+8"(东八区,符合中国等多数地区的时间标准)。 - 然后,通过调用
SimpleDateFormat
的format
方法,将当前日期和时间(通过new java.util.Date()
获取)格式化为指定格式的字符串,并将其存储到currentTime
变量中。 - 最后,将窗口标题更新为 "日历" +
currentTime
,实现了在窗口标题中实时显示当前时间的功能。
getMonthName
方法设计思想
- 该方法的主要目的是根据月份索引(0 - 11)获取对应的中文月份名称。
- 通过创建一个包含中文月份名称的数组,然后根据传入的月份索引从数组中获取相应的中文月份名称并返回,为在其他方法(如
changeMonth
方法中更新月下拉框选中项)中提供了方便的获取中文月份名称的方式。
设计原理
.窗口布局和组件创建:
- 使用JFrame作为主窗口,设置标题、大小、关闭操作和布局。通过BorderLayout布局将窗口分为顶部和中央两部分,顶部放置年、月下拉框和按钮,中央放置日历面板。
- 创建JLabel作为月份和年份标签,以及JPanel作为顶部面板和日历面板。使用FlowLayout布局管理顶部面板,GridLayout布局管理日历面板。
- 创建年、月下拉框,并添加相应的动作监听器,以便在用户选择不同的年份和月份时更新日历显示。
2.日历生成:
- 使用GregorianCalendar类获取当前时间和特定月份的日历信息。通过计算当前月的第一天是星期几以及该月的总天数,生成日历的布局。
- 对于上一个月和下一个月的空白日期,使用灰色字体显示上一个月的日期或下一个月的占位符。对于当前月的日期,使用自定义的圆形标签突出显示今天的日期。
3.用户交互和时间更新:
- 为上一个月和下一个月按钮添加动作监听器,实现月份的切换功能。在切换月份时,更新当前月份和年份,并重新显示日历。
- 使用Timer类创建定时器,每秒钟触发一次时间更新操作,更新窗口标题显示的当前时间。
功能实现
设计源码
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.awt.RenderingHints;
// 定义主类 CalendarMain,继承自 JFrame 以创建可视化窗口
public class CalendarMain extends JFrame {
// 月份标签,用于显示当前月份
private final JLabel monthLabel;
// 年份标签,用于显示当前年份
private final JLabel yearLabel;
// 日历面板,用于显示日历内容
private final JPanel calendarPanel;
// 当前月份的索引
private int currentMonth;
// 当前年份
private int currentYear;
// 当前时间的字符串表示
private static String currentTime;
// 年下拉框组件
private JComboBox<Integer> yearComboBox;
// 月下拉框组件
private JComboBox<String> monthComboBox;
public CalendarMain() {
// 创建月份标签,初始显示为 "October 2023"
monthLabel = new JLabel("October 2023");
// 设置窗口标题为 "日历"
setTitle("日历");
// 设置窗口大小为 500x450 像素
setSize(500, 450);
// 设置窗口关闭操作,当关闭窗口时退出程序
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置窗口布局为 BorderLayout(边框布局)
setLayout(new BorderLayout());
// 获取屏幕尺寸
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
// 计算窗口在屏幕中的位置,使其居中显示
int x = (screenWidth - getWidth()) / 2;
int y = (screenHeight - getHeight()) / 2;
setLocation(x, y);
// 获取当前时间的日历对象
Calendar calendar = new GregorianCalendar();
// 获取当前月份的索引
currentMonth = calendar.get(Calendar.MONTH);
// 获取当前年份
currentYear = calendar.get(Calendar.YEAR);
// 创建年下拉框
yearComboBox = new JComboBox<>();
// 循环添加从当前年份前后各 20 年的年份选项到年下拉框
for (int i = currentYear - 20; i <= currentYear + 20; i++) {
yearComboBox.addItem(i);
}
// 设置年下拉框的初始选中项为当前年份
yearComboBox.setSelectedItem(currentYear);
// 为年下拉框添加动作监听器,当选择年份变化时更新当前年份并重新显示日历
yearComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
currentYear = (Integer) yearComboBox.getSelectedItem();
displayCalendar();
}
});
// 创建月下拉框
monthComboBox = new JComboBox<>();
// 创建包含中文月份名称的数组
String[] monthNames = new String[]{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"};
// 循环添加中文月份名称到月下拉框
for (int i = 0; i < 12; i++) {
monthComboBox.addItem(monthNames[i]);
}
// 设置月下拉框的初始选中项为当前月份对应的中文名称
monthComboBox.setSelectedItem(monthNames[currentMonth]);
// 为月下拉框添加动作监听器,当选择月份变化时更新当前月份并重新显示日历
monthComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
currentMonth = monthComboBox.getSelectedIndex();
displayCalendar();
}
});
// 创建顶部面板,用于放置年、月下拉框和按钮等
JPanel topPanel = new JPanel();
topPanel.setLayout(new FlowLayout());
// 创建上一个月按钮,点击时调用 changeMonth(-1) 方法切换到上一个月
JButton prevButton = new JButton("<");
prevButton.addActionListener(e -> changeMonth(-1));
topPanel.add(prevButton);
// 将年下拉框添加到顶部面板
topPanel.add(yearComboBox);
// 将月下拉框添加到顶部面板
topPanel.add(monthComboBox);
// 创建年份标签(这里暂时保留原有的年份标签显示方式)
yearLabel = new JLabel();
topPanel.add(yearLabel);
// 创建下一个月按钮,点击时调用 changeMonth(1) 方法切换到下一个月
JButton nextButton = new JButton(">");
nextButton.addActionListener(e -> changeMonth(1));
topPanel.add(nextButton);
// 将顶部面板添加到窗口的北侧(BorderLayout.NORTH)
add(topPanel, BorderLayout.NORTH);
// 创建一个 6 行 7 列的网格布局的面板,用于显示日历内容
calendarPanel = new JPanel(new GridLayout(7, 7));
// 将日历面板添加到窗口的中央(BorderLayout.CENTER)
add(calendarPanel, BorderLayout.CENTER);
// 显示日历内容
displayCalendar();
// 创建一个定时器,每 1000 毫秒(1 秒)触发一次更新时间的操作
Timer timer = new Timer(1000, this::updateTime);
timer.start();
}
// 自定义的圆形标签类,继承自 JLabel
private static class CircleLabel extends JLabel {
// 表示是否为今天的标志
private boolean isToday;
public CircleLabel(String text, boolean isToday) {
// 调用父类构造方法,设置文本居中显示
super(text, SwingConstants.CENTER);
this.isToday = isToday;
// 设置字体为 Arial,普通样式,字号为 20
setFont(new Font("Arial", Font.PLAIN, 20));
}
@Override
protected void paintComponent(Graphics g) {
// 调用父类的 paintComponent 方法进行默认绘制
super.paintComponent(g);
// 如果是今天,则绘制红色圆形
if (isToday) {
Graphics2D g2d = (Graphics2D) g;
// 设置抗锯齿渲染提示,使绘制的圆形更平滑
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int width = getWidth();
int height = getHeight();
int diameter = Math.min(width, height);
int x = (width - diameter) / 2;
int y = (height - diameter) / 2;
// 设置绘制颜色为红色
g2d.setColor(Color.RED);
// 绘制圆形
g2d.drawOval(x, y, diameter, diameter);
}
}
}
private void displayCalendar() {
// 清除日历面板上的所有组件
calendarPanel.removeAll();
// 创建当前月份的日历对象
Calendar calendar = new GregorianCalendar(currentYear, currentMonth, 1);
// 创建上一个月的日历对象
Calendar prevMonth = new GregorianCalendar();
if (currentMonth == 0) {
// 如果当前月份是一月,则上一个月为去年的十二月
prevMonth.set(currentYear - 1, 11, 1);
} else {
// 否则,上一个月为当前月份减一
prevMonth.set(currentYear, currentMonth - 1, 1);
}
// 更新月份标签为当前月份的长名称
monthLabel.setText(calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, getLocale()));
// 更新年份标签为当前年份
yearLabel.setText(String.valueOf(currentYear));
// 添加星期标题行
String[] days = {"一", "二", "三", "四", "五", "六", "天"};
for (String day : days) {
// 创建星期标题标签并添加到日历面板
calendarPanel.add(new JLabel(day, SwingConstants.CENTER));
}
// 获取当前月的第一天是星期几(星期日为 1,星期六为 7)
int startDay = calendar.get(Calendar.DAY_OF_WEEK) - 1;
if (startDay == 0) startDay = 7;
// 获取当前月的总天数
int daysInMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
// 获取上一个月的总天数
int prevDaysInMonth = prevMonth.getActualMaximum(Calendar.DAY_OF_MONTH);
// 获取当前日期的日历对象
Calendar todayCalendar = new GregorianCalendar();
int todayDay = todayCalendar.get(Calendar.DAY_OF_MONTH);
int todayMonth = todayCalendar.get(Calendar.MONTH);
int todayYear = todayCalendar.get(Calendar.YEAR);
// 添加上一个月的空白日期(灰色显示)
for (int i = startDay - 1; i >= 1; i--) {
JLabel label = new JLabel(String.valueOf(prevDaysInMonth - i + 1), SwingConstants.CENTER);
label.setForeground(Color.GRAY);
label.setFont(new Font("Arial", Font.PLAIN, 20));
calendarPanel.add(label);
}
// 添加当前月的日期
for (int i = 1; i <= daysInMonth; i++) {
boolean isToday = (i == todayDay) && (currentMonth == todayMonth) && (currentYear == todayYear);
// 创建日期标签,如果是今天则用圆形标签显示
calendarPanel.add(new CircleLabel(String.valueOf(i), isToday));
}
// 填充后续的空白日期(灰色显示)
for (int i = 1; i <= 42 - daysInMonth - (startDay - 1); i++) {
JLabel dayLabel = new JLabel(String.valueOf(i), SwingConstants.CENTER);
dayLabel.setForeground(Color.GRAY);
dayLabel.setFont(new Font("Arial", Font.PLAIN, 20));
calendarPanel.add(dayLabel);
}
// 重新验证和重绘日历面板
calendarPanel.revalidate();
calendarPanel.repaint();
}
// 切换月份的方法,delta 为正负 1 表示切换到上一个月或下一个月
private void changeMonth(int delta) {
currentMonth += delta;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
} else if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
// 更新年下拉框的选中项
yearComboBox.setSelectedItem(currentYear);
// 更新月下拉框的选中项
monthComboBox.setSelectedItem(getMonthName(currentMonth));
// 重新显示日历
displayCalendar();
}
public static void main(String[] args) {
// 使用 SwingUtilities.invokeLater 确保在事件分发线程中创建和显示窗口
SwingUtilities.invokeLater(() -> {
CalendarMain app = new CalendarMain();
app.setVisible(true);
});
}
// 更新时间的方法,每秒钟更新一次窗口标题显示的时间
private void updateTime(ActionEvent e) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT+8"));
currentTime = sdf.format(new java.util.Date());
setTitle("日历" + currentTime);
}
// 根据月份索引获取中文月份名称的方法
private String getMonthName(int monthIndex) {
String[] monthNames = new String[]{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"};
return monthNames[monthIndex];
}
}