news 2026/9/9 16:38:37

Java Swing完整版记事本实战:从GUI布局到文件IO与查找替换

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing完整版记事本实战:从GUI布局到文件IO与查找替换

简介:Java开发记事本(完整版)是一个基于Java Swing的文本编辑器项目,面向初学Java GUI编程的开发者,用于理解窗口组件、菜单栏、事件监听与文件读写等核心机制,也能作为课程设计或练手项目参考。压缩包共36个文件,大小约103KB,内部包含java源码、class编译文件、可直接运行的jar包、txt说明及工程配置文件,源码与运行产物分开存放,便于对照阅读。当前已有4192人学习/下载。源码覆盖打开保存文件、剪切复制粘贴、查找替换、撤销重做、状态栏与快捷键等功能模块,可看到JFileChooser、JTextArea、TransferHandler、Action接口等典型用法;文件读写中演示了缓冲流与异常处理,撤销重做则通过操作历史栈实现。通过阅读和运行该项目,能完整掌握Java桌面应用的模块划分、事件处理与异常处理思路,可直接修改扩展,适合Java学习者深入实践。

1. 项目概述与整体设计思路

做Java开发绕不过去的一个经典练手项目就是“记事本”。这玩意儿看起来简单,但真要做出一个“完整版”,涉及的知识点一点不少:GUI布局、事件监听、文件IO、文本处理、剪贴板交互、快捷键绑定,甚至还能扯上编码转换和正则匹配。正因为它麻雀虽小五脏俱全,我特别建议Java初学者把这个项目完完整整做一遍——它比刷一百道面试题更能帮你理解Swing的运作机制和面向对象的设计思想。

1.1 为什么选择Swing而不是JavaFX

现在很多人一上来就推荐JavaFX,理由是界面更现代、支持CSS样式。但我的看法比较直接:如果你是刚学完Java基础语法,Swing是更合适的选择。原因有三:

第一,Swing是JDK自带的库,不需要额外配置任何依赖,装好JDK就能跑。JavaFX从JDK 11开始已经从JDK中剥离了,需要单独引入模块,这对于刚入门的同学来说就是一道不必要的坎。

第二,Swing的API设计非常直观,JFrame、JMenuBar、JTextArea这些类的命名和用途完全对应,阅读理解成本低。你用Swing写完一个记事本,再去学JavaFX,很多概念是相通的,不会白学。

第三,Swing的组件模型是MVC架构的经典体现——JTextArea是视图,Document是模型,监听器是控制器。搞清楚这层关系,对理解后面Spring MVC这类框架的设计思想也大有帮助。

所以我的建议是:用Swing把记事本做扎实,之后再去进阶JavaFX完全来得及。

1.2 完整版记事本的功能范围界定

网上搜“Java记事本”能找到一堆版本,但大部分都是残废的——只能打几个字,连文件都存不了。既然叫“完整版”,功能上至少要覆盖以下几个模块:

  • 文件操作:新建、打开、保存、另存为、退出
  • 编辑操作:剪切、复制、粘贴、全选、删除
  • 查找与替换:可以输入关键词查找,支持替换单个和替换全部
  • 格式设置:字体选择、字号调整、加粗斜体
  • 视图辅助:自动换行开关、状态栏显示行号和列号
  • 快捷键支持:Ctrl+S保存、Ctrl+O打开、Ctrl+F查找等常规组合键

这些功能全部实现下来,代码量大概在600到900行之间,分文件组织的话正好能练到面向对象设计。而且每一个功能背后都对应一个Java核心知识点,做成简历项目也有底气。

2. 核心技术点解析

写记事本看起来简单,但要把每一个功能都做得不露破绽,有几个技术点必须吃透。这些点也是你在面试时能跟面试官聊出深度的关键。

2.1 JTextArea组件与Document模型的关系

记事本的核心组件就是JTextArea,但这个组件有个比较隐蔽的设计:它本质上只是一个“视图”,真正的文本内容存放在Document对象里。你调用getText()方法和setText()方法,其实都是在和Document交互。

这个分离的设计有什么实际意义呢?因为你做查找替换时,可以直接操作Document对象来实现局部文本替换,这样比拿整个字符串操作再整体setText回去要高效得多。尤其是当你打开一个几十万行的大文件时,用getText()获取整个文档再replace、再setText回去,内存开销极大,界面还会明显卡顿。

完整版记事本肯定要支持大文件的,所以正确的做法是先用Document对象做局部更新,或者用JTextArea内置的replaceSelection()方法配合选区操作来实现替换。

2.2 Swing中的事件监听机制

Swing是典型的事件驱动模型。用户点击按钮、选择菜单项、按键输入,都会产生对应的事件对象。你在开发记事本时,要操作的事件类型无非就这几类:ActionEvent(菜单点击、按钮点击)、KeyEvent(键盘按键)、WindowEvent(窗口关闭)。

这里要特别注意一个点:很多人写监听器喜欢一坨全塞在构造器里,代码又臭又长。更好的做法是定义一个Config类或者在内部类里做统一管理,把菜单名称、快捷键、监听器注册都集中在一个初始化方法里。这样代码结构清晰,后续加功能也容易。

还需要注意的是,Swing的事件处理是在事件分发线程(EDT)上执行的。如果你在事件回调里做了耗时的文件操作,界面会直接卡死。解决思路是引入SwingWorker或SwingUtilities.invokeLater(),但考虑到记事本这种轻量级应用场景,只要文件不会太大,直接同步处理问题不大,真要支持超大文件再上异步方案。

2.3 文本编码处理的底层逻辑

这个东西必须展开讲,因为凡是做记事本,没有不踩编码坑的。

Java内部字符串使用UTF-16编码,但磁盘上的文件可能是GBK、UTF-8、ISO-8859-1等等。使用记事本打开一个GBK编码的txt文件,如果你按UTF-8去读,就会出现乱码,而且这种乱码一旦显示出来,你是无法通过界面操作恢复的。

解决方案是:在打开文件时,先读取文件的原始字节流,按照指定的字符集解码成字符串,或者直接指定字符集创建InputStreamReader。保存文件时同理,按照用户选择的编码方式把字符串转成字节数组写入文件。

还有一个细节你可能没注意到:Windows系统自带的记事本在保存UTF-8文件时会写入BOM头(EF BB BF),而Linux下创建的UTF-8文件通常没有BOM。如果你的Java记事本不处理这个差异,用Windows记事本打开你保存的文件时,第一行最前面会出现一个肉眼不可见但确实存在的字符,导致第一行无法被正确匹配查找。解决方法是保存时默认使用UTF-8并写入BOM,或者打开时自动跳过BOM头。

2.4 剪贴板交互与系统级数据传递

剪切、复制、粘贴这三个功能本质上是和系统剪贴板交互。Java提供了Toolkit.getDefaultToolkit().getSystemClipboard()来获取系统剪贴板实例。

这里有一个使用细节:当用户按Ctrl+C时,如果你没有自己处理这个快捷键,JTextArea自身会完成复制动作。但如果你在菜单项上绑定了Accelerator,并且菜单项同时关联了复制动作,有可能出现快捷键被菜单消费掉、JTextArea收不到复制命令的情况。所以正确做法是:不要为剪切复制粘贴这些菜单项绑定Ctrl+C/X/V,让JTextArea默认处理这些快捷键,菜单只是调用textArea.cut()、copy()、paste()方法。

这个细节虽然小,但很多人做出来的记事本会出现“快捷键失灵”的bug,排查半天发现是Accelerator和默认行为冲突了。

3. 核心功能实现

这一节重点讲各个功能模块应该怎么实现,以及实现过程中容易踩的坑。为了方便你参考,我按模块来说。

3.1 界面布局与主框架构建

我建议主框架用一个自定义的NotepadFrame类继承JFrame,构造器里完成所有初始化逻辑。顶部用JMenuBar,中间是JScrollPane包裹的JTextArea,底部是JPanel做的状态栏。整体布局用BorderLayout,菜单放NORTH,编辑区放CENTER,状态栏放SOUTH。

状态栏的设计有讲究。它需要实时显示当前光标所在的行列号。实现方式是给JTextArea添加CaretListener接口,光标位置变化时触发事件处理。这里要注意,行列号不是直接从光标位置算出来的,需要用JTextArea的getLineOfOffset()方法把字符偏移量换算成行号,再计算当前行起始字符的偏移量来求得列号。

字体的设置会影响整个界面的观感,推荐默认字体选Monospaced或者Consolas,这样中英文混合时对齐效果最好,符合记事本的调性。字体大小默认12到14号,中文显示比较清晰。

3.2 文件读写与目录记忆功能

文件操作是这个项目的灵魂,实现起来却有不少细节。

打开文件的推荐代码思路是:通过JFileChooser选择文件后,先读取整个文件为字节数组,然后根据用户选择的编码格式构造字符串,最后setText到文本区。保存文件则是相反的过程,把文本区内容按指定编码写入文件。

JFileChooser有一些值得优化的地方。默认状态下,用户每次打开对话框都得手动找到上次的目录。优化做法是给JFileChooser设置一个静态变量记录当前目录,每次打开对话框前先setCurrentDirectory到上次的目录,体验会好很多。

新建文件时还有个细节:如果当前文本区内容已经被修改过了,直接新建会丢失数据。所以每次新建前要检查是否有未保存的修改,如果有,弹出一个确认对话框让用户选择保存、不保存或取消。这个逻辑同样要应用在关闭窗口和打开新文件时。

3.3 查找替换功能的实现方案

查找替换是整个项目里最有含金量的模块,因为它涉及正则表达式和Document操作。

查找功能的实现思路是:记录上一次查找的位置,调用textarea的getText()获取全文,用indexOf或正则匹配找下一个匹配位置,找到后用setCaretPosition把光标移到那里,并用select选中匹配的文本。第二次查找时需要注意,要基于上一次的位置向后继续查,而不是每次都从头开始。

替换功能的实现思路是用textArea.getSelectionStart()和getSelectionEnd()获取当前选中的文本范围,如果选中的文本和查找目标一致,就用replaceSelection()替换。替换全部则简单一些,遍历全文替换即可。

这里有个值得优化的地方:当我们用getText()获取全文来找匹配位置时,如果文件很大,性能会是个问题。更优雅的方案是直接操作Document对象,通过javax.swing.text.Utilities的getWordStart和getWordEnd来定位单词边界,不过实现复杂度会高不少。对记事本这个项目而言,用getText()拿全文处理已经够用了,代码还更好理解。

3.4 字体设置与状态栏显示的完整实现

字体设置模块需要用到JFontChooser,但这个类不包含在JDK里。有两个选择:一是自己写一个简易的字体选择对话框,用JList列出系统支持的字体、字号和样式;二是用JDialog直接做一个动态预览的字体选择器,效果更好。

做字体选择器时注意一个体验问题:字体列表是异步加载的,因为GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()在部分Windows系统上会卡顿半秒到一秒。优化方式是首次调用后缓存到静态变量里,后续打开对话框直接读缓存。

状态栏显示行列号的技术点在前面已经说过,再补充一个体验优化:如果文本区内容为空时,光标位置显示为第1行第1列,比第0行第0列更符合用户习惯。

4. 进阶功能与界面体验优化

基础版本做完以后,千万别停下,加上几个进阶功能,项目质感会完全不一样,面试讲起来也更有亮点。

4.1 关键字高亮功能实现

给Java记事本加上关键字高亮,这个效果非常能打。实现思路是给JTextArea的Document添加DocumentListener,每次文档内容变化时,对全文做一次词法分析,把Java关键字用高亮颜色标出来。

技术上,高亮用的API是JTextPane的StyledDocument,它比JTextArea强大很多,支持对文本的不同区间设置不同样式。当然,你也可以继续用JTextArea配合Highlighter来实现高亮,但Highlighter只支持背景色高亮,不能改文字颜色,效果会打些折扣。

建议用JTextPane来做支持高亮的记事本版本,注意它的setText和JTextArea有所不同,Document类型需要切换成DefaultStyledDocument。关键字匹配用正则表达式,把Java所有关键字列出来,用|分隔构建一个正则模式,匹配到的区间设置蓝色加粗。再进一步,字符串常量可以设置成绿色,数字设置成橙色,接近IDE的效果。

4.2 多个文档的标签页支持

单文档的记事本实现简单,但用起来总觉得不够方便。进阶版可以把主界面改成JTabbedPane布局,每个标签页对应一个文档。

实现思路是,把原来的JTextArea和状态栏打包成一个自定义的EditorPanel类,每新建一个文档就实例化一个EditorPanel放进新的标签页。菜单操作要跟当前选中的标签页联动——你得跟踪当前激活的Tab,所有事件监听都发给这个活跃的EditorPanel处理。

这个改动涉及到整个架构的调整,如果你前期的代码是把所有内容都写在JFrame类里的,这步改造会很痛苦。所以一开始就建议按模块分文件,把一个完整的文档编辑界面封装成独立类,这是面向对象设计的老生常谈,但真正理解它的价值恰好就是在这种扩展需求出现的时候。

4.3 近期文件列表与用户偏好记忆

在菜单里加一个“最近打开的文件”子菜单,记录最近打开的5到10个文件路径。这个功能用java.util.prefs.Preferences来实现持久化最方便,它可以在Windows注册表或系统配置文件中存储小量数据,不需要自己搞配置文件。

用户偏好记忆同理,比如用户上次设定的字体大小、是否自动换行,以及窗口的位置和大小——用Preferences把这个信息存进去,下次启动时自动恢复。这些细节非常提升好感度,很多开源记事本都会忽略,做出来之后体验会和系统自带工具拉开明显差距。

5. 常见问题与排查技巧实录

这部分是我做这个项目时实际踩过的坑,以及帮别人debug时经常遇到的问题,整理成速查表,遇到可以直接定位。

5.1 中文乱码问题的三种场景

乱码问题基本都出在编码错配,排查思路很固定。

第一种,代码里直接写了中文字符串,编译时如果没有指定-encoding参数,在中文Windows环境下源码是GBK编码,但IDE默认用UTF-8编译,就会乱码。解决办法是IDE里统一把项目编码设为UTF-8,并在编译时指定-encoding UTF-8。

第二种,打开文件乱码。这是文件实际编码和读取时指定的编码不一致导致的。解决办法是打开时让用户选择编码,或者更智能一点,自动检测文件编码。检测原理是:先看有没有UTF-8的BOM头,没有的话用字节序列的统计特征去猜是GBK还是UTF-8。

第三种,保存后再用其他编辑器打开乱码。一般是保存时没有加BOM头,Windows记事本对无BOM的UTF-8文件会按GBK解析,从而出现乱码。解决办法是在保存UTF-8时,把字节数组前加上EF BB BF三个字节。

5.2 界面卡死与事件线程阻塞

如果你在打开一个大文件时,界面窗口突然变成“未响应”状态,问题出在文件读取操作阻塞了事件分发线程(EDT)。解决思路是:把文件读取放到子线程里执行,读取完成后再通过SwingUtilities.invokeLater回调到EDT更新界面。

在实际开发中,我建议给打开文件操作加一个文件大小判断:如果文件超过5MB,提示用户文件较大,是否继续。超过50MB,建议用子线程读取并在左下角状态栏显示“加载中…”的进度提示。这样既不复杂,又能覆盖掉绝大多数卡死场景。

5.3 快捷键冲突与菜单焦点问题

很多人做出记事本后发现,Ctrl+C复制偶尔失灵,有时第一次好用第二次没反应。这种情况十有八九是菜单项的Accelerator和JTextArea的默认快捷键发生了冲突。菜单拥有焦点时,按下快捷键菜单会优先响应,如果菜单里没绑定复制动作,事件就被吞掉了。

解决方案在前面提过:复制剪切粘贴这几个菜单项不要绑定Accelerator,或者绑定自定义KeyStroke并接一个“派发给当前焦点组件”的动作。我实测下来最稳的方案就是第一种——让JTextArea自己处理剪贴板快捷键,菜单里只放点击触发的动作。

5.4 保存后文件内容为空的问题

有一个典型bug:保存文件时,如果用户选择的是“另存为”而且还没有输入文件名就点了取消,文件会被意外清空。原因是你在获取到用户选择的文件路径之前就执行了写入操作,而此时file对象为null,写入会把内容写到了默认路径或者直接跳过了。

正确的实现顺序是:先通过JFileChooser获取用户选择的目标文件,判断返回结果是否为APPROVE_OPTION,如果是则执行写入,不是则直接return,不做任何操作。另外,如果是已有文件路径的保存操作,不要弹文件对话框,直接覆盖原文件即可。

6. 实操过程与核心环节实现

最后带大家从头到尾走一遍关键实现流程。我默认你已经配置好了Java开发环境,也掌握基本的Swing组件概念。

6.1 搭建项目骨架

项目结构建议如下:

  • src/com/notepad/Main.java —— 程序入口
  • src/com/notepad/ui/NotepadFrame.java —— 主窗体
  • src/com/notepad/ui/EditorPanel.java —— 单个文档的编辑面板
  • src/com/notepad/ui/StatusBar.java —— 状态栏
  • src/com/notepad/util/FileUtil.java —— 文件读写工具类
  • src/com/notepad/util/PreferencesUtil.java —— 偏好设置工具类

Main类就一行代码:

public class Main { public static void main(String[] args) { SwingUtilities.invokeLater(() -> new NotepadFrame().setVisible(true)); } }

注意一定要用invokeLater创建界面,保证所有组件初始化和操作都在EDT上执行,这是Swing线程模型的硬性规定。

6.2 文件保存流程带编码选择

保存的完整流程是先判断当前文件有没有路径。如果有且用户没有主动触发“另存为”,直接写文件;如果没有路径,弹出保存对话框。下面是一个带编码选择的保存逻辑:

public void saveFile() { if (currentFilePath == null) { showSaveFileDialog(); } else { writeFile(currentFilePath, currentEncoding); } } private void writeFile(File file, String encoding) { try { String content = textArea.getText(); byte[] bytes = content.getBytes(encoding); if ("UTF-8".equals(encoding)) { byte[] bom = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; byte[] withBom = new byte[bom.length + bytes.length]; System.arraycopy(bom, 0, withBom, 0, bom.length); System.arraycopy(bytes, 0, withBom, bom.length, bytes.length); bytes = withBom; } try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(bytes); } saved = true; } catch (UnsupportedEncodingException | IOException e) { JOptionPane.showMessageDialog(this, "保存失败: " + e.getMessage()); } }

这段代码里我特意加了BOM头处理的逻辑,就是为了避免前面说的Windows记事本打开乱码的问题。

6.3 查找替换核心逻辑

查找功能的准确实现,重点在于处理“从当前光标位置向后找”这个需求。如果每次都从文档开头找,用户会觉得很别扭,明明后面还有匹配项却跳到前面去了。

我的做法是记录一个lastSearchIndex变量。查找时从该位置开始,找到匹配后把位置更新为匹配项末尾,下一次查找继续往后走。如果搜到文末还没找到,就提示“未找到”,并重置为从头开始搜。

public void search(String query, boolean caseSensitive) { String content = textArea.getText(); if (!caseSensitive) { content = content.toLowerCase(); query = query.toLowerCase(); } int start = Math.max(lastSearchIndex, 0); int index = content.indexOf(query, start); if (index >= 0) { textArea.setCaretPosition(index); textArea.select(index, index + query.length()); lastSearchIndex = index + query.length(); } else { lastSearchIndex = 0; JOptionPane.showMessageDialog(this, "未找到匹配内容"); } }

6.4 打包为可运行程序

项目做完之后,打包成双击就能运行的程序是最后一个必要步骤。用IDEA自带的Artifact功能,把项目构建成带主类的JAR包,然后在命令行里用jpackage工具(JDK 14+提供)生成一个免安装的exe或文件夹。

jpackage的使用比较简单,以Windows为例:

jpackage --name Notepad --input lib --main-jar mynotepad.jar --main-class com.notepad.Main --type exe

这样打出来的exe自带JRE运行环境,用户不需要单独安装Java就能运行。整个项目到此才算真正交付,可以发给同学试用,或者放进自己的作品集里。

做完整这一个项目,你对Swing的掌握程度、对事件驱动编程的理解、对IO和编码的熟练度,都会有一个质的提升。Java面试八股文里那些抽象的概念,在这个项目里都能找到对应的实际应用。动手做一遍,比死记硬背强太多了。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 16:38:23

TypeScript 基础类型详解:从原理到实战的必备指南

TypeScript 基础类型——这个东西听起来好像就是“给变量加个类型”而已,但等你真正上手写一段时间,会发现它其实是整个 TypeScript 世界里最值得花时间啃透的部分。你可以不背住每个工具类型的实现,但基础类型如果理解得不扎实,后…

作者头像 李华
网站建设 2026/9/9 16:37:52

4.14 也能 root?KernelSU 旧内核适配实战,老设备先别划走

4.14 也能 root?KernelSU 旧内核适配实战,老设备先别划走 【免费下载链接】KernelSU A Kernel based root solution for Android 项目地址: https://gitcode.com/GitHub_Trending/ke/KernelSU 你的设备内核太老,装 KernelSU 直接显示 …

作者头像 李华
网站建设 2026/9/9 16:31:41

JAVA毕设选题推荐:基于SpringBoot+Vue的医患在线问诊交互平台的设计与实现 基于SpringBoot+Vue的智慧门诊问诊服【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

作者头像 李华