尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java可视化日历实战:从控制台到Swing完整开发指南

Java可视化日历实战:从控制台到Swing完整开发指南 1. 这个可视化日历到底在做什么以及为什么从零开始写先直接把话说透Java可视化日历就是用Java自带的GUI工具包Swing把你平时在手机、电脑上看到的月历界面自己动手做出来。它不是一个只能在控制台打印数字的玩具而是一个真正有窗口、有按钮、能翻月、能标注日期的桌面程序。很多朋友学Java学到面向对象和集合就卡住了天天在控制台里跑System.out.println感觉自己学了一堆语法却做不出一个“看得见摸得着”的东西。可视化日历这个项目恰好能把Java基础语法、面向对象思想、集合框架、日期时间API、事件监听机制一条线串起来做完这个项目你对Java的理解会有一个质的提升。这个项目的适用人群很明确刚学完Java基本语法、想找个项目练手的初学者以及在准备面试但简历上缺少项目经验的朋友。它的知识密度刚好不会难到让人放弃又能让你接触到真实桌面应用开发的大致流程。我见过很多培训机构把这套东西包装成“企业级项目”其实它的本质就是一个综合性练手项目但千万别小看它因为把日历做“对”需要处理很多细节比如某月第一天是星期几、这个月有几天、闰年怎么算、点击事件怎么绑定、界面布局怎么不挤在一起。这些细节才是真正考验你代码功底的地方。我建议的学习路径分三步走第一步先用纯控制台把日历的“计算逻辑”跑通第二步用JFrame和JButton做出一版“能动”的图形界面第三步再给它加上月份切换、事件标记、双击查看详情这些进阶功能。下面所有的内容就按这个路径展开每一步我都会把我自己当时踩过的坑和后来总结出的正确做法一起写出来你可以直接照着做。2. 动手之前环境准备和基础认知必须到位2.1 JDK环境配置别在这上面浪费时间但也不能完全不管做Java项目第一件事就是把JDK装好、配好环境变量。这里我不展开讲太多JDK的安装细节只把关键操作说清楚。去Oracle官网下载JDK注意选对版本我建议直接上JDK 17或者JDK 21这些LTS版本不要追最新版本也不要死守Java 8。Java 8经典是经典但很多新项目已经不用了而且换版本之后你会发现有些API用起来确实更顺手。JDK装完之后需要配置三个环境变量JAVA_HOME指向JDK安装目录Path新增一个条目指向%JAVA_HOME%\binClassPath这个变量JDK 5之后其实已经不需要手动配了网上很多旧教程还在让你配那是时代遗留问题。配置完成后一定要验证。在命令行窗口输入java -version能正常输出版本号就是成功。如果提示“不是内部或外部命令”先检查JAVA_HOME是不是配错了再检查Path里的路径有没有写对尤其注意Windows系统如果装了多个JDK优先级顺序会被Path里的先后顺序影响。我自己的经历是明明装好了JDK结果控制台里调用的还是系统自带的旧Java一脸懵。后来发现是Path里旧版本的路径排在前面把新版路径挪到最前面就解决了。这个细节务必记住。2.2 Swing和AWT到底是个什么关系可视化日历的“可视化”部分靠的是Swing这套GUI工具包。很多新手一听GUI就头大其实Swing的底层逻辑很简单就是往窗口里放组件。窗口是JFrame按钮是JButton标签是JLabel文本输入框是JTextField布局要靠布局管理器如BorderLayout、GridLayout来安排位置。Swing和AWT的关系一句话解释AWT是早期版本自带的图形界面工具依赖操作系统的原生组件不同系统上长啥样不完全一样Swing是在AWT基础上发展出来的纯Java实现不依赖系统原生组件界面由Java自己绘制所以跨平台表现一致。你用Swing写日历本质上用到的还是从AWT继承来的顶层窗口、事件机制这些底层能力但所有显示组件都用Swing的这样界面好看又可控。2.3 日历项目里需要的Java核心知识点清单写代码之前心里先有个知识地图知道自己会用到什么。我把这个项目涉及到的知识点列了一张表你对照着自查缺哪块补哪块。知识点在这个项目里的具体用处重要程度数组和集合List存储一个月的日期数据、存储事件列表高条件判断与循环计算每月天数、判断星期、遍历日期格子高面向对象封装把日历逻辑封装成类界面逻辑跟业务逻辑分离高日期时间API新版java.time获取当前年份月份、计算星期几、处理日期加减高事件监听ActionListener点击按钮切换月份、双击日期查看事件高布局管理器把日期格子按7列排成日历表格形态中异常处理处理非法日期输入、日期解析失败等场景中日期时间API我特别要强调一点不用再学SimpleDateFormat和Calendar这些老古董了。Java 8引入的java.time包LocalDate、DayOfWeek、YearMonth这些类比旧API好用太多代码写出来又短又不容易出错而且天然线程安全。从零开始学直接上新API别走回头路。3. 先从控制台日历说起把隐藏的计算逻辑彻底搞清楚3.1 控制台日历第一步判断某年某月有多少天很多新手一上来就想着直接画界面结果界面画了一堆按钮日期数据却不会算最后做出来一个空壳。正确思路是先在控制台把逻辑打通再考虑显示。日历最核心的一个函数就是给定一个年份和一个月份这个月总共有多少天。这个问题的传统解法用switch语句处理1月、3月、5月、7月、8月、10月、12月是31天4月、6月、9月、11月是30天2月特殊闰年是29天平年是28天。注意闰年的判断规则不能被400整除能整除400的也是闰年。完整的闰年判断条件有两层能整除4且不能整除100的是闰年能整除400的也是闰年。如果用新版API这个逻辑一行代码就搞定了YearMonth.of(year, month).lengthOfMonth()它内部已经把所有规则都处理好了。这就是我为什么要强调用新API你少写几十行代码不说还不会踩那种“1990年不是闰年”之类的边界坑。但如果只是调用API你很难理解日历背后的规则面试时也容易被人追问到懵。所以我建议你在写项目的时候自己动手实现一遍那个传统逻辑然后再用API替换掉两相对照你才知道API到底帮你省了什么活。3.2 控制台日历第二步这个月的第一天到底是星期几有了总天数第二个核心问题就是这个月第一天是星期几。只有知道第一天是星期几才能在打印日历时把前面的空位补齐。传统做法是选定一个已知的基准日期比如1900年1月1日是星期一然后计算从基准日期到你查询的这个月第一天一共隔了多少天再用天数模7得到星期几。这个逻辑本身不复杂但容易在求总天数时出错因为要考虑中间的每个闰年。新版API的做法是LocalDate.of(year, month, 1).getDayOfWeek().getValue()会自动返回从1到7的数字分别代表星期一至星期日拿到这个数字直接就能用。这里有一个小细节值得说一说中国习惯上把星期一当作一周的开始但西方很多国家把星期天当作一周的开始。可视化日历默认按星期日作为第一列来排但我们的计算逻辑里要清楚内部数字代表什么不能界面和逻辑各想各的最后日期串位。3.3 控制台日历第三步按周排列输出完整月历把前两步连起来就可以在控制台打印出完整月历了。打印格式通常是周一 周二 周三 周四 周五 周六 周日 1 2 3 4 5 6 7 8 9 10 11 12 13 ...实现思路是先把第一天的星期几对应的空格打印出来然后从1开始打印日期每打印7个日期就换一行最后一天打完后结束。注意控制格式对齐数字不足两位数时前面补一个空格或者其他占位符。这个阶段你不需要考虑界面好不好看只要数字排列是对的说明你的计算逻辑就没问题。有很多初学者在控制台打印这一步能卡很久常见bug是当月第一天是星期日时前面的空格数量不对或者换行位置不对导致后面的日期串到了下一行。遇到这种问题别慌这就是在锻炼你调试代码的能力。你可以单独写一个测试方法故意传几个特殊的年月进去比如2024年2月2023年12月2050年1月把输出结果跟手机日历对比一下哪里不对就改哪里。4. 可视化升级从控制台到Swing图形界面的完整跨越4.1 搭建主窗口JFrame是你的画板控制台逻辑跑通之后终于可以开始做界面了。第一步是创建主窗口用JFrame实现。基本代码就四五行实例化一个JFrame设置标题设置大小设置关闭时退出进程设置可见。我建议窗体尺寸直接设为800x600或者700x500太小了日期格子挤在一起很难看太大了又显得空。这里有个容易被忽略的功能给窗口设置一个合适的初始居中位置。用setLocationRelativeTo(null)可以让窗口在屏幕正中间弹出不设置的话窗口默认在左上角体验感差很多。另外可以给窗口设置一个默认的关闭操作WindowConstants.EXIT_ON_CLOSE就是点右上角叉号时直接结束进程如果你的程序里还有别的资源要释放可以考虑用DISPOSE_ON_CLOSE但在这个练手项目里用EXIT_ON_CLOSE就够了。4.2 布局设计日历界面的骨架子怎么搭做界面不能想到哪写到哪随手add一个组件上去就完事。日历界面的经典布局是这样的窗口整体用BorderLayout北边放一个工具面板里面放着“上一月”“下一月”的按钮和显示当前年月信息的标签中间放一个7列的网格面板每一行代表一周第一行固定是星期几的标题行后面的行放日期按钮。中间这个日期网格最适合用GridLayout来排。GridLayout(6, 7)表示6行7列所有格子大小一致正好适配一个月的日期分布需求。为什么是6行7列因为一个月最多横跨6个自然周也就是说最多有6行星期行所以预留6行永远不会不够用。日期不够时剩下的格子就留空或者放其他月份的日子。还有一个布局细节最外面的大框架用BorderLayout就可以不要嵌套太多层否则代码维护起来很痛苦。嵌套布局是新手容易犯的毛病一个面板套一个面板套了五六层最后自己都找不到组件在哪了。我见过最夸张的代码光面板就有十几个嵌套想改一个按钮的位置要找半天。这种时候就需要停下来重构了。4.3 动态生成日期按钮核心代码逐行拆解日期网格里的每个日期本质上是一个JButton按钮。为什么用按钮而不是标签因为按钮天然支持点击事件后面扩展“点一下日期就填写事件”这个功能时就不用额外折腾。网格里总共有42个格子也就是6行7列。生成日期按钮的核心思路是清空原有网格面板的所有组件。获取当前显示的年月计算出这个月的总天数以及第一天是星期几。按7列的顺序遍历42个位置编号从0到41。如果当前位置前面需要空出offset个空白格就放一个不可用的占位标签否则按顺序放日期按钮。offset的计算方式要理解清楚如果星期一是一周的第一天offset等于第一天星期几的数字减去1这里我结合实际代码解释。假设你的第一列是星期一当LocalDate.getDayOfWeek().getValue()返回1表示星期一那么它就要放到第1列不需要留空offset0。返回3表示星期三它需要放到第3列所以前面要空出2个位置offset2。所以offset dayOfWeekValue - 1。很多教程会把这一点讲得很绕其实就是算出第一天前面要空几个格子。在循环里日期编号从1递增到getTotalDays。如果编号的索引位置小于offset说明是空白格如果编号大于总天数说明这个月的日期已经放完剩下的格子也是空。处理空白的时候有两种做法放一个空标签或者放一个disabled的按钮。我推荐放一个空标签这样视觉上更干净。这里的代码核心逻辑我放一段伪代码你可以参考这个思路去写自己的版本int offset firstDayOfWeekValue - 1; int totalDays YearMonth.of(currentYear, currentMonth).lengthOfMonth(); for (int i 0; i 42; i) { int dayNumber i - offset 1; if (dayNumber 1 || dayNumber totalDays) { gridPanel.add(new JLabel()); } else { JButton dayButton new JButton(String.valueOf(dayNumber)); gridPanel.add(dayButton); } }这段代码看起来很简洁但它已经解决了日历显示最核心的三个问题空白补齐、日期数量限制、网格固定。多运行几次用不同月份测试确保数字排列跟真实日历对得上。4.4 翻月功能点击按钮切换年月日历比静态图片高级的地方在于能翻月。翻月的逻辑本质上就是改年月数据然后刷新界面。点击“下一月”按钮时月份加1如果月份加到13说明已经跨年年份加1月份重置为1。点击“上一月”按钮时月份减1如果月份减到0说明上一年12月年份减1月份重置为12。这个逻辑用if判断几行代码就能写完关键是写完要刷新界面。刷新有两个动作更新中央网格面板的日期按钮更新顶部标签的年月文字。刷新的时候调用一个refreshCalendar()方法先调用gridPanel.removeAll()清掉旧的日期按钮再重新生成所有按钮并add进面板最后调用gridPanel.revalidate()和gridPanel.repaint()让界面真正重绘。特别要注意removeAll之后如果不调revalidate界面经常不会立即刷新点按钮没反应这是Swing的经典陷阱之一。翻月功能的按钮事件监听写法也有讲究。最传统的是匿名内部类方式nextMonthButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { currentMonth; if (currentMonth 13) { currentMonth 1; currentYear; } refreshCalendar(); } });这个写法在新版Java里的等价简写是lambda表达式一行搞定nextMonthButton.addActionListener(e - { ... })。新手阶段建议先写一遍匿名内部类体会一下事件监听的机制再改成lambda这样你既懂原理又掌握了新写法。5. 功能进阶让日历从“能看”变成“能用”5.1 显示事件给日期格子挂上小标记一个能翻页的日历其实已经达到了入门标准但如果你想让这个项目在面试或者开源作品集里拿得出手建议再加上事件标记功能。事件标记的常见效果是某天有事件时日期按钮上显示一个醒目的圆点、星号或数字角标鼠标放上去还能看到事件内容。具体实现思路是维护一个MapLocalDate, List 键是日期值是对应的事件描述列表。生成日期按钮时每生成一个日期就去查一下这个Map里有没有该日期的事件记录如果有就在按钮文字中拼接一个标识符比如“12日·会”或者直接改变按钮的背景色。从实现成本来看后者更简单直观。选中日期后弹出一个输入框让用户录入事件这一步涉及JOptionPane的用法。JOptionPane是Swing里现成的对话框工具直接showInputDialog就能弹出输入框读取返回的字符串。把输入内容存进Map然后调用刷新逻辑。整个过程不需要自己写窗口代码量很精简。事件存储这块我建议先只存在内存Map里写清楚注释说明“此版本数据不持久化重启程序后事件丢失”。如果你有兴趣可以后续用文件IO或者SQLite把事件持久化但那已经超出可视化日历本身的知识点范畴。作为日志型项目先把界面和交互做好比把数据存储做得复杂但界面一塌糊涂要重要得多。5.2 当前日期高亮一眼看出今天是几号高亮今天的日期是一个很加分的细节。实现起来也很简单在生成日期按钮时用LocalDate.now()获取今天然后对比当前渲染的年份月份和日期。如果LocalDate.of(currentYear, currentMonth, dayNumber).equals(LocalDate.now())就把这个按钮的背景色设置为一个醒目的颜色比如橙色或者浅蓝色。一些课程或项目还会把今天的按钮设为不可点击理由是“今天已经过去了”我认为这反而限制了功能。日历应用里很多人都要记录今天的事你应该让今天和其他日期一样可以正常点击只是视觉上特殊化。高亮颜色的选择上注意避开红绿这种容易刺激视觉的颜色蓝色和橙色都偏中性视觉效果较好。5.3 双击日期查看事件更贴近真实桌面应用的交互如果每个日期的事件不只一条录入完还要能查看。最简单的查看方式是在日期按钮上绑定鼠标监听器双击出现提示框展示事件。Swing里双击检测需要用到MouseAdapter类重写mouseClicked方法判断e.getClickCount() 2说明是双击。双击日期后从Map里取出这个日期关联的事件列表拼成一个字符串用JOptionPane.showMessageDialog弹窗展示。如果列表为空就提示“当天暂无事件”。这个功能做完后整个日历的应用完整性就上了一个台阶不再只是一个展示工具而是一个能记录日程的小助手。我在这里给一个整理好的伪代码示例展示整个监听器数据读取弹窗的组合逻辑dayButton.addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() 2) { LocalDate selectedDate LocalDate.of(currentYear, currentMonth, dayNumber); ListString events eventMap.get(selectedDate); if (events null || events.isEmpty()) { JOptionPane.showMessageDialog(frame, selectedDate 暂无事件); } else { JOptionPane.showMessageDialog(frame, String.join(\n, events)); } } } });6. 常见问题与排查技巧实录我把新手反复踩的坑一次说完6.1 日期错位日历的第一天总是对不上这是出现频率排第一的问题。症状是日历上第一天显示的位置跟真实星期对不上比如实际1号是星期三界面上却排到了星期五。排查思路分两步走。先确认计算逻辑在控制台打印出offset值看看跟预期是否一致再确认布局顺序注意你的日期网格是从第1列星期一/星期日开始排的有没有在循环里把编号对应错。我当年犯过的一个典型错误把offset和dayNumber判断写反了写成if (i % 7 offset)。这种错误属于“逻辑没理清就开写”的结果后来我把需求拆解成“第一个有效日期前要放几个空白”代码就一下子清晰了。6.2 界面刷新没反应removeAll之后必须revalidate翻月按钮点了年月标签变了但日期格子还是老的。这个问题的原因几乎都是因为没有调用revalidate和repaint。Swing的组件在移除之后不会立即重绘需要向布局管理器发出“重新计算布局”的通知。很多人只在paintComponent等重绘方法里做文章却不知道removeAll之后需要两步走。正确的刷新顺序是removeAll - 重新add组件 - revalidate - repaint。前两步保证组件集合的新内容就位后两步保证界面真正刷新显示。6.3 数组越界、字符串比较踩坑基础功不扎实导致的连带问题写这个项目时有的朋友用字符串拼接日期时直接拼接一个空字符串这在Java里没问题但如果要在循环里频繁拼接请多用StringBuilder否则数据量大时性能下降明显。另外判断日期字符串是否相等绝对不能写必须用equals方法。比较的是引用地址equals比较的是内容这是Java新手最容易踩的坑在日历这种大量使用字符串的场景里被无限放大。还有一个小坑就是日期日期的int值计算。3月明明有31天却计算总天数时写成30。遇到“少了一天”的这种问题就用YearMonth.lengthOfMonth()来对照自己的手写逻辑两个结果对齐了就说明算法没问题。6.4 我整理的一个常见问题速查表现象可能原因解决方案日期第一天位置不对offset计算错误或布局顺序错位打印offset值对照确认第一列代表星期几点击翻月后界面不变跳过revalidate/repaint刷新时依次调用removeAll、add、revalidate、repaint今天不显示高亮年份或月份没有参与equals判断必须用完整LocalDate对象比较不能只比day值事件录不上用了字符串拼接但没正确处理空值判断null之后再用Map存储窗口关闭了进程还在关闭操作设置不对显式设置EXIT_ON_CLOSE按钮点击无响应没有注册ActionListener给按钮添加监听器或检查事件代码是否抛异常6.5 调试日历的几个小技巧多准备几个测试日期样例比如2024年2月这个闰年2月、2026年1月、2030年12月这几个日期跨越多周、跨闰年、跨大月小月基本能覆盖大多数边界情况。在控制台同时打印一份纯文本格式的当月日历跟界面并排对照只要控制台输出正确界面的问题就集中在GUI层排查范围能缩小一半。不要一次性把所有功能写完再调试功能拆细先把静态月份显示跑通再加翻月再加事件每一步都验证过再继续。7. 代码组织与工程化为什么我不建议你写一个巨型类很多新手做这个小项目喜欢把所有代码堆在main方法或者一个JFrame子类里一个类几千行。这样写虽然能运行但后续改bug和加功能都会变得非常痛苦。我建议至少把代码拆成三个层次日历实体类负责日期数据计算界面框架类负责创建窗口与布局工具方法可以单独放一个类比如日期格式转换、按钮样式设置等。这里有一个具体的经验把年和月封装成一个YearMonth对象传给界面不要在界面里散落着currentYear和currentMonth两个int变量无限传递。这样不仅代码整洁还能直接调用YearMonth里的plusMonths方法完成翻月计算逻辑更不容易出错。翻月的逻辑优化后上一月下一月的代码量能压缩一半currentYearMonth currentYearMonth.plusMonths(1);这个写法传负数也能往前翻一行就搞定原本好几个if判断的活。这就是封装以及API设计带来的力量你在写项目时也能慢慢品味到Java这门语言的设计哲学。工具方法独立出来还有一个额外的好处当你想给日期加上农历、节假日、周数等功能时不需要改动原有的日历框架只需要在对应的工具类里加方法然后在渲染时调用即可。这种低耦合结构是真实项目开发的基本要求从小项目就开始培养是特别宝贵的能力。8. 项目扩展思路一门入门项目如何变成简历上的亮点日历本身是一个足够经典的项目但它能不能在简历上成为亮点取决于你往哪个方向扩展。我根据自己的经验列出四个比较有价值的方向按实现难度从低到高排序第一个方向是把界面风格做好。Swing原生控件确实不够漂亮但有方法给它“化妆”。你可以用自定义渲染器也可以引入FlatLaf这类第三方外观库几行配置就能让界面从“远古系统风格”变成“现代扁平风格”。视觉上的提升在展示时效果非常直观但技术含量不高适合用来增加项目完成度。第二个方向是完善日程管理功能。事件标记目前只是存储在内存里你可以用文件IO把事件保存到本地txt或JSON文件实现“重启程序数据不丢”。以此为基础再扩展出提醒功能用javax.swing.Timer定时检查事件时间到达时间就弹窗提醒。这个方向能锻炼数据持久化和多线程调度能力。第三个方向是联动网络抓取节假日数据。通过HTTP请求调用公开的节假日API在日历上用颜色标识周末和法定节假日这样项目就具备了一定的实用价值而知乎、CSDN这类平台上很多人对这类教程的需求很高说明这是大家普遍认可的方向。第四个方向是引入数据库。用SQLite或H2内嵌数据库存储事件数据按日期索引查询。这时候你已经是在用一个“半个真实项目”的架构来写日历了。给事件增加优先级、分类、完成状态日历就从练习项目变成一个待办事项管理工具简历上的描述也可以升级为“开发并实现一个具备增删改查功能的桌面日程管理系统”。扩展功能的原则是一定要在原有架构上“长”出来而不是推翻重写。如果你在前面已经把逻辑和界面拆开那么这四个方向的扩展都只需要增加类或方法不涉及大改这也是前面强调代码组织的重要性所在。9. 最后再分享几个我在这个项目里学到的通用经验这个项目做完之后除了日期计算和Swing组件之外我最大的收获其实是调试思路的转变。以前我在控制台写练习代码出了错就看红色报错文字改完就算完。写日历之后我发现最大的坑往往不是编译错误而是“编译通过了运行结果不对”的逻辑错误比如日期错位、事件显示不出来、按钮点击没反应。这些错误控制台不会告诉你你得自己想办法“看穿”程序运行时的内部状态。我养成了一个习惯在关键的节点用System.out.println打印中间值比如刷新日历前打印要渲染的年份月份点击按钮后打印目标年月这样能很快定位是哪一步出了问题。很多人觉得打印日志很土其实这是最实用高效的调试手段。等你以后用IDE的断点调试功能或者将来在服务器上排查线上问题时你会发现“能在关键节点输出日志”本身就是一项核心能力。还有一点体会是关于学习节奏的。这个项目不需要一口气写完一天写一点反而效果更好。第一天只做控制台日历第二天把静态界面搭出来第三天加翻月第四天加事件中间每天留出时间回顾和重构代码。不要追求一步到位复杂的项目都是一步步迭代出来的。最后我强烈建议你把做好的项目拿给身边学Java的朋友看一眼或者发到技术社区里。哪怕你的界面还不是很好看功能也还有瑕疵但这个动手过程已经能证明你的代码能力比只看教程强得多。编程这事看十遍不如自己写一遍。
返回列表