• 第二十一章《万年历》第2节:系统功能实现


    本小节将讲解万年历软件的代码设计思路及关键代码的实现过程。

    21.2.1初始化组件

    对于界面上大部分组件而言都使用常规的方式进行初始化,但对于选择年份的下拉框对象jcbYears而言,其中的选项是动态生成的。这些选项表示年份数值,它们并不是固定的选项,而是根据当前年份计算出来的前后各50年的年份值。例如,当前年如果是2022年,那么jcbYears下拉框中的第一个年份选项为“1972”,这是2022年的之前50年的年份,而下拉框中最后一个年份选项为“2072”这是2022年的之后50年的年份。此外,界面窗体对象刚被创建出来时,选择月份的下拉框jcbMonths中表示当前月份的选项会被自动选中。例如当前月份是6月,则窗体在生成时自动选中jcbMonths中的选项“6”。

    实现初始化jcbYears和jcbMonths的代码被定义到initCalendarComp()方法中,初始化的思路很简单:首先根据系统时间获取当前日期,然后得到当前日期的年份和月份数值,求出年份数值的前50年的值,以该值为起点,循环向jcbYears加入101个年份数值作为选项。之所以是101个选项,是因为前后各50年再加上当前年总共101年而不是100年。加入选项之后,再根据当前年份设置下拉框中表示当前年份和月份的选项被自动选中。下面是初始化jcbYears和jcbMonths组件initCalendarComp()方法的代码。

    1. private void initCalendarComp(){
    2.    //得到当前的年月
    3.    shownYM = YearMonth.from(curTime);
    4.    int year = shownYM.getYear();//得到年份数值
    5.    int month = shownYM.getMonthValue();//得到月份数值
    6.    int earliestYear = year-50;//earliestYear是当前年之前的50年
    7.    //以循环方式加入101年
    8.    for (int i=0;i<101;i++){
    9.       yearsData.addElement((earliestYear+i)+"");
    10.    }
    11.    //定位被选中的年和月
    12.    jcbYears.setSelectedIndex(50);
    13.    jcbMonths.setSelectedIndex(month-1);
    14. }

    21.2.2当前时间的刷新

    万年历软件从开始运行时界面上的当前时间就一直保持每秒刷新一次,因此获取并显示当前时间的操作必须每秒执行一次并且一直执行下去。获取并显示当前时间的方法是showCurTime(),方法定义过程如下:

    1. private void showCurTime(){
    2.     curTime = ZonedDateTime.now(zids[zidIndex]);//以某个时区为准获取当前时间
    3.     strCurTime = formatter.format(curTime);//格式化当前时间
    4.     lblCurTime.setText(strCurTime);//把当前时间显示到界面上
    5. }

    以上代码中,zidIndex是数组的下标,它的值默认为0,当切换城市时会根据城市选项的索引重新给它赋值,因此这个下标与用户选择的城市相对应,通过这个下标能够获取到所选城市的当前时间。

    showCurTime()方法虽然能够获取并显示当前时间,但不能每秒执行一次并一直执行下去。如果希望showCurTime()方法能够每秒执行一次,必须借助Timer类来完成。

    Timer意为“定时器”,Timer类对象可以设定一个任务每隔一段时间执行一次。例如本案例中,获取当前时间的就是一个任务,程序员可以通过Timer对象让这个任务每隔1秒钟就执行一次。Timer所设定的任务,由TimerTask类表示,这个是一个抽象类,我们要用一个匿名内部类去实现这个抽象类,程序员在实现它的时候要重写run()的抽象方法,只要把要执行的任务写在run方法当中就可以了。Timer类对象通过其schedule方法设定执行任务的参数,例如:

    timer.schedule(task,0,1000);

    以上代码中,timer是一个定时器,task是要执行的任务。也就是说,通过timer设定执行task任务,schedule()方法的第二个参数0表示表示立刻执行,延迟时间为0毫秒,第三个参数1000表示任务每隔1000毫秒执行一次,也就是每秒执行一次。

    创建并启动定时器的方法是startClock(),其源代码如下:

    1. private void startClock(){
    2.     timer = new Timer();//创建定时器
    3.     task = new TimerTask() {//以匿名内部类的形式创建TimerTask的实现类对象
    4.         @Override
    5.         public void run() {
    6.             showCurTime();
    7.         }
    8.     };
    9.     timer.schedule(task,0,1000);//启动定时器并设置任务执行频率
    10. }

    当在对窗体组件初始化的过程中同时调用startClock()方法,就能显示出当前时间并能一直保持每秒一次的刷新频率。

    21.2.3切换时区

    万年历软件被打开后默认以北京时间显示当前日期时间,当用户选择不同城市时,软件能够根据用户选择的城市切换不同时区的当前时间。本项目中,zids表示时区数组,而zidIndex表示zids数组的当前下标,因此只要让下拉框中城市选项的顺序与zids数组中各城市对应的时区元序保持一致,就能做到与每次切换城市选项时该选项的索引恰好是时区元素的下标。例如:在选择城市的下拉框中,“悉尼”选项的索引为3,在设置zids数组时,把“悉尼”对应的时区也放置在zids数组下标为3的位置,这样就保持了城市与时区的对应一致。当用户切换城市选项时,根据下拉框被选中项的索引就能确定时区,而根据时区又能得到对应的当前时间。

    在程序中,表示城市下拉框的是jcbCities,为使其选项在被切换时能够同时切换时区,应为jcbCities添加监听器,代码如下:

    1. jcbCities.addActionListener(new ActionListener() {
    2.         @Override
    3.         public void actionPerformed(ActionEvent e) {
    4.             zidIndex = jcbCities.getSelectedIndex();//根据被选中项的索引修改zidIndex
    5.             //判断被显示到界面上的年月是否与最新获取的当前年月一致
    6.             if(isCoincideCurDate (shownYM)==true){
    7.                 resetYearMonth();//重新显示日历
    8.             }
    9.             showCurTime();//重新获取并显示当前时间
    10.         }
    11.     });

    在以上代码中,监听器在处理城市选项被切换时除了根据选项所代表的时区获取并显示当前的最新的时间外,还额外检查了被显示到界面上的年月是否与最新获取的当前年月一致,如果不一致就重新显示一次日历,这是因为切换城市不仅仅切换了一个时区,还可能造成日历的变化。例如北京时间2023年1月1日早上6点,但对于纽约时间还是2022年12月31日下午6点,可以看出,在同一时刻,这两个城市不仅仅有时差,连年份、月份和日期都不一样,所以要重新绘制。关于绘制日历的操作将在21.2.4小节中讲解。

    21.2.4显示日历及本月节日

    本小节重点讲解如何把某个月的日期显示到窗体上。读者可以观察图21-1所显示的日期排布规律,可以发现:日历上从“日”到“六”总共有七个汉字来表示一周的七天。我们要做的事情就是把某个月的日期按照星期均匀的排列在这七个汉字的下面。

    从“日”到“六”这七个汉字其实就是7个大小完全相同的标签。而想排列某个月的日期也可以采用这样的方式:创建出7个大小完全相同的标签排列整齐,之后在标签上显示出代笔日期的数字就可以。一个月的天数最少有28天,最多有31天。28天的情况最少要4行就能完成排列,如图21-2所示。

    图21-2一个月只有4行的情况

    如果一个月有31天,那么最多的情况下需要6行才能把一个月的日期全部显示出来,如图21-3所示。

    图21-3一个月有6行的情况

    图21-3所示的情况下,每行7个标签,总共需要42个标签。因此:无论这个月有多少天,也无论这个月的第1天是星期几,只要窗体上有42个标签,并且按照每行7个标签的方式排列,就能把任意一个月显示到窗体上。为了编程方便,界面上固定都用42个标签来显示日期,如果某个月不需要6行来显示,就把最后面的1行或2行空出来就可以。把日期显示到界面上的方法是showCalendar(),因为每次显示的是某年某月的,因此showCalendar()方法的参数类型是YearMonth,参数表示要显示哪年哪月的日历。此外,软件界面上除了要显示本月日历外,还要显示上个月最后几天和下个月前几天,因此showCalendar()方法也要把这日期显示到界面上,并且这些日期是用灰色字体显示的,此外:无论本月有多少天,下个月的日期最多显示6天,因为大部分日历都不会把下个月前一整周都显示出来。如果一个月最后一天恰好是周六,这样能占满一整行,那么就不显示下个月的日期。

    万年历软件还能显示阳历节日,由于该软件是基于Java8新日期时间系统实现的,而这套日期时间系统并没有提供中国传统阴历的实现,因此不能显示阴历节日。显示节日的思路与显示日期一样:定义一个标签数组,并且把标签数组中的每一个标签以7个标签为一行排列到窗体上。也就是说有多少显示日期的标签,就对应有多少个显示节日的标签。这两组标签的位置关系是:上面摆放日期标签下面摆放节日标签,一一对应。本案例中设定只显示本月的节日,如果当前月日历中的上个月最后几天和下个月前几天也有节日,那么这些节日不在本月日历中显示。

    系统中把所有节日定义在一个二维数组festivals中,并且是按月份的顺序定义的,这样根据当前显示的是几月的日期就能对应找到本月份的节日数组。寻找本月节日的具体办法是:以月份值减去1为下标,就能找到对应月的节日,这些节日是一个一维数组。每次显示一个日期值时,就根据节日数组中的元素判断这个日期是不是节日,如果是节日则同时以红色字体显示节日。

    显示日期及节日的方法是showCalendar(),以下是方法定义代码。

    1.   private void showCalendar(YearMonth ym){
    2.     //清空日历
    3.     clearCalendar();
    4.     YearMonth lastYM = ym.minusMonths(1);//得到上个月
    5.     int lastLen = lastYM.lengthOfMonth();//算出上个月有多少天
    6.     LocalDate firstDay = ym.atDay(1);
    7.     int weekDay = firstDay.getDayOfWeek().getValue();//本月1号是星期几
    8.     int startIndex = weekDay;//本月第1天应该出现的位置
    9.     if(weekDay==7){
    10.         startIndex = 0;
    11.     }
    12.     int year = ym.getYear();
    13.     int month = ym.getMonthValue();
    14.     if(year<=0){
    15.        year = year-1;
    16.     }
    17.     lblShowTip.setText("日历显示年月:"+year+"年"+month+"月");
    18.     int startNum = lastLen-startIndex+1;
    19.     int date = startNum;
    20.     //打印上个月的最后几天
    21.     int i=0;
    22.     for(;i
    23.         lblDates[i].setText(""+date);
    24.         lblDates[i].setForeground(Color.LIGHT_GRAY);
    25.     }
    26.     date=1;
    27.     int monthLen = ym.lengthOfMonth();//本月的最后一天
    28.     boolean flag = isCoincideCurDate(ym);
    29.     Festival[] monthFestival = festivals[month-1];
    30.     for(;date<=monthLen;date++,i++){
    31.        lblDates[i].setText(""+date);
    32.        if(flag==true && date==curTime.getDayOfMonth()){//如果要打印的date表示今天
    33.            lblDates[i].setForeground(Color.BLUE);
    34.        }
    35.        for(int j=0;j
    36.            if(monthFestival[j].getDay()==date){
    37.                lblFestivals[i].setText(monthFestival[j].getName());
    38.           }
    39.        }
    40.    }
    41.    int maxNum;
    42.    if(startIndex+monthLen%7==0){
    43.        maxNum = startIndex+monthLen;
    44.    }else{
    45.        maxNum = (startIndex+monthLen)/7*7+7;
    46.    }
    47.    date=1;//设置下个月被打印日期的开始
    48.    //打印下个月的前几天
    49.    for(;i
    50.         lblDates[i].setText(""+date);
    51.         lblDates[i].setForeground(Color.LIGHT_GRAY);
    52.    }
    53. }

    21.2.5切换日期

    窗体上的区域4是用来切换日期的组件,总共包括2个下拉框和5个按钮,这些组件都能引起窗体上日历的切换,因此要给这些组件添加监听器以监听它们的操作。仔细观察选择年和选择月的下拉框,会发现它们所做的操作本质上是一样的,都是用两个下拉框当中的年份值和月份值组成一个新的年月,然后再把这个年月的日历显示到窗体上,因此,这两个下拉框的事件处理程序也是完全一样的,它们所做的操作可以用getNewYearMonth()方法定义,getNewYearMonth()方法的代码如下:

    //获得新的年和月

    1. private void getNewYearMonth(){
    2.     int year = Integer.parseInt((String)jcbYears.getSelectedItem());//获得新的年份值
    3.     int month = Integer.parseInt((String)jcbMonths.getSelectedItem());//获得新的月份只
    4.     shownYM = YearMonth.of(year,month);//组成新年月
    5.     showCalendar(shownYM);//把新年月显示到窗体上
    6. }

    getNewYearMonth()方法可以直接作为表示选择年份的下拉框jcbYears和表示选择月份的下拉框jcbMonths的事件响应方法。

    除这两个下拉框外,窗体上还有“前1年”、“前1月”、“后1月”、“后1年”4个按钮能够切换日期,这4个按钮切换日期的原理是一样的,都是在当前日期的基础上修改一个年或月的数值得到新年月,然后把这个年月显示到窗体上。例如单击“前1年”按钮实际上就是在当前年份值上减去1形成一个新的年份值,然后和当前月组合形成新的年月。其他按钮的操作也都类似,因此只需要定义一个能够在当前年月值上进行修改的方法就可以,同时这个方法还能够把新计算出的年月以日历形式显示到窗体上。本案例把这个方法定义为plusYearMonth(),其源代码如下:

    1. //增加年月
    2. private void plusYearMonth(int year,int month){//year是要增加的年份,month是要增加的月份
    3.    shownYM = shownYM.plusYears(year);//当前年的基础上增加year年
    4.    shownYM = shownYM.plusMonths(month);//当前月的基础上增加month月
    5.    showCalendar(shownYM);//显示新的年月
    6. }

    这个方法可以直接在4个切换年月按钮的时间处理方法中调用,只要传入不同的参数值就可以,例如“前1年”按钮的监听器应该按如下方式调用plusYearMonth()方法:

    plusYearMonth(-1,0);

    而“后1月”按钮的监听器则按如下方式调用plusYearMonth()方法:

    plusYearMonth(0,1);

    除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!

  • 相关阅读:
    AVR单片机开发4——定时器T0 中断方式
    3. 该微信用户未开启“公众号安全助手”的消息接收功能,请先开启后再绑定
    整合minio时出现的错误
    Linux——磁盘与文件系统的管理
    vue3 ts vite elementplus更改主题颜色
    【Unity入门计划】制作RubyAdventure02-处理瓦片地图&碰撞
    光纤快速连接器如何安装使用?
    LeetCode每日一题——1752. 检查数组是否经排序和轮转得到
    JAVA知识——JAVA基础
    光电柴微电网日前调度报告
  • 原文地址:https://blog.csdn.net/shalimu/article/details/128147899