☰
Java Swing实现文本编辑器:文件操作、剪贴板与字符统计
2026/10/14 3:06:16 网站建设 项目流程

1. 项目需求拆解:标题里每一项功能背后对应什么

1.1 从用户视角分清“浏览”和“编辑”的差别

收到这个标题时,我第一反应是:这不就是我当年练手时做的那个记事本吗。标题看起来只是把几个功能列在了一起,但真去实现一遍,就会发现每一项都不是孤立的。拿“创建、浏览、编辑文件”这句话来说,它其实隐含了三个完全不同的交互流程。

创建文件不是简单地在内存里开一块空文本,它要解决“当前文档还没保存过,怎么给用户一个可识别的名字”这个问题。浏览文件意味着要有打开文件对话框,要处理编码,要能把磁盘里的内容完整读进编辑区。编辑文件又意味着用户会有大量连续输入,编辑区必须响应够快,还要能处理选中、替换、撤销这些细操作。一个文本编辑器真正麻烦的地方,不是单个功能写不出来,而是这些功能之间要共用同一份文档状态。

剪贴、复制、粘贴这三个动作,表面上是三个菜单项,实际上是同一套系统剪贴板能力。复制是把选中内容放进剪贴板,剪切是复制后把原内容删除,粘贴是把剪贴板内容插入光标处。如果只依赖Java文本组件自带的行为,那确实很省事,但要让菜单在无选中内容时置灰、要让工具栏按钮和菜单动作共享同一套逻辑,就需要我们自己封装一套剪贴板操作。

保存、另存为这一组功能更典型。保存是“把当前内容写回当前文件”,另存为是“把当前内容写到一个新位置,并把它设为当前文件”。很多新手代码把这两个功能写成了完全独立的两段逻辑,结果另存为之后点保存,内容又莫名其妙存回旧文件了。项目虽小,但组织的思路从一开始就要对。

字符统计和自动换行则是对用户友好的收尾功能。自动换行解决的是“一段很长的文本要不要横向滚动才能看完”的体验问题,字符统计解决的是“我到底写了多少字”这样的实用需求。这两项也是测试一个文本编辑器是否“能用来干正经事”的参考指标,而不是可有可无的装饰。

1.2 技术选型:为什么Java Swing比JavaFX更适合这个规模

做文本编辑器,摆在我面前的有两条主流路线:Java Swing和JavaFX。标题里列的功能其实属于典型的“教学级桌面工具”,我更推荐Swing作为主力方案。

原因不是Swing比JavaFX先进,恰恰相反,Swing老,但老有老的好处。第一,Swing是JDK自带的,不需要额外引入任何第三方库,也不需要额外配置模块路径,环境折腾成本几乎为零。第二,Swing的组件体系对“文本编辑器”这种场景覆盖得很完整,JTextArea、JScrollPane、JFileChooser、JMenuBar都是现成的,直接拼装就行。第三,Swing的资料和案例特别多,遇到界面布局或者事件监听的问题,搜索一下基本都能找到对应写法,对初学者非常友好。

JavaFX当然也能做,但如果只为实现标题上的这些功能,JavaFX要额外处理FXML、样式表、平台插件之类的东西,这会让简单项目被工程结构拖累,反而偏离了“用Java实现文本编辑器”这个核心目标。如果哪一天你想做的不是普通文本编辑器,而是要带复杂视觉效果、动画、皮肤切换的高级文档工具,再考虑JavaFX不迟。

界面布局上,我选择最简单的BorderLayout。中心区域放JScrollPane包着的JTextArea,北侧放菜单栏,南侧放状态栏。这个结构一眼能看到头,也方便后续添加工具栏。

1.3 程序包结构规划

虽然这是一个单文件工程,我也建议从一开始就把代码按职责拆开。真正写起来不是把所有逻辑都堆在JFrame里,而是分三块:

  • 界面层:负责创建窗口、菜单、编辑区、状态栏,只关心排版和事件转发。
  • 文件操作层:负责打开、保存、判断扩展名、处理编码,不关心界面长什么样。
  • 剪贴板与工具层:负责复制剪切粘贴、字符统计、自动换行状态切换。

这样做的好处是,以后你想把文本编辑器升级成带插件的版本,或者给界面换一套皮肤,只需要改某一层,其他部分不会跟着崩。我在实践中发现,很多人的失败不是因为功能写不出来,而是把所有变量都塞在一个类里,几百行之后自己都分不清谁在用哪个状态。

2. 搭主窗口:布局、菜单栏、编辑区和状态栏

2.1 窗口框架和编辑区初始化

主界面是整个项目的入口。我建议继承JFrame来做,然后初始化一个JTextArea放在JScrollPane里。JTextArea是文本编辑器的核心,它有默认的编辑能力,能用键盘输入、能选中文本、能根据内容判断是否需要滚动条。

初始化代码大概长这样:

public class TextEditorFrame extends JFrame { private final JTextArea editorArea = new JTextArea(); private final JLabel statusLabel = new JLabel("就绪"); private File currentFile = null; private boolean documentChanged = false; public TextEditorFrame() { super("未命名 - Java文本编辑器"); setLayout(new BorderLayout()); setSize(900, 650); setLocationRelativeTo(null); JScrollPane scrollPane = new JScrollPane(editorArea); add(scrollPane, BorderLayout.CENTER); add(createStatusBar(), BorderLayout.SOUTH); setJMenuBar(createMenuBar()); editorArea.getDocument().addDocumentListener(createDocumentListener()); } }

这里有几个容易被忽略的点:JTextArea默认是没有滚动条的,直接往窗口里丢一个JTextArea,内容一旦超出可视区域就永远看不到了。用JScrollPane包一层之后,还要设置好滚动策略,让长文档既能上下滚,也能左右滚,而自动换行开启之后又可以禁用水平滚动条。

另一个点是关闭窗口的事件。直接设置EXIT_ON_CLOSE会无提示丢失未保存内容,所以文本编辑器必须用DO_NOTHING_ON_CLOSE,再自己监听windowClosing事件,弹确认框之后才能真正退出。

2.2 菜单与快捷键绑定

菜单是这个编辑器最直接的操作入口。我规划了三个菜单:文件、编辑、格式。文件菜单里放新建、打开、保存、另存为和退出;编辑菜单里放剪切、复制、粘贴;格式菜单里放自动换行的开关。

绑定快捷键时,用的是KeyStroke。注意不要用KeyEvent.CTRL_MASK,它已经被标记为废弃,应该用InputEvent.CTRL_DOWN_MASK。以新建为例:

JMenuItem newItem = new JMenuItem("新建"); newItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); newItem.addActionListener(e -> createNewFile());

这里有个经验:菜单项的快捷键标签要跟实际触发动作对齐。如果菜单项写得清楚“新建(Ctrl+N)”,用户就不需要在界面上摸索。另外,我还要提一句,中文本地化菜单里,助记符setMnemonic有时会跟输入法抢按键,造成按Alt+字母没有反应。更稳妥的做法是不要依赖助记符,直接靠快捷键和鼠标操作,省得在特定系统上踩坑。

2.3 状态栏的“脏标记”设计

状态栏不要只放一个静态文字,它至少应该展示三样东西:当前文件是否已保存、当前文件路径、字符统计结果。我的方案是:状态栏左侧放一个有“脏标记”含义的提示标签,右侧放字符数。

“脏标记”这个名字听起来玄乎,其实就是一个布尔变量,表示文档是否被修改过但没有保存。任何插入、删除、替换操作都会把它置为true,同时把窗口标题加一个星号。比如:

private boolean documentChanged = false; private DocumentListener createDocumentListener() { return new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { markDocumentChanged(); } @Override public void removeUpdate(DocumentEvent e) { markDocumentChanged(); } @Override public void changedUpdate(DocumentEvent e) { markDocumentChanged(); } }; } private void markDocumentChanged() { documentChanged = true; updateTitle(); updateStatus(); }

标题栏显示“未命名 *”而不是“未命名”,就是靠这个状态。保存之后,再把documentChanged改成false,标题栏的星号去掉。这个设计虽然很简单,却是所有后续文件操作判断的基础:新建、打开、退出之前都要先问一句,当前文档是不是改了还没存。

3. 文件操作链路:新建、打开、保存、另存为

3.1 文件选择器的可选项与过滤规则

打开和另存为都依赖JFileChooser。标题里说“浏览文件”,所以文件选择器要能过滤出常见文本文件,但不能把用户挡在门外。我的做法是提供两组文件过滤器:一组是“文本文件(*.txt)”,一组是“所有文件”。默认选文本文件过滤器,用户也可以切到所有文件去打开配置文件、日志文件或者无扩展名文件。

JFileChooser还有一个常被忽略的参数:APPROVE_OPTION。点“打开”按钮后返回的状态码要跟对话框选择结果区分开。如果用户点了“取消”,就不能继续执行读取逻辑。

JFileChooser chooser = new JFileChooser(); chooser.setFileFilter(new FileNameExtensionFilter("文本文件(*.txt)", "txt")); if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) { return; } File file = chooser.getSelectedFile(); loadFile(file);

3.2 读写时统一UTF-8编码

这是整个项目里最容易出问题的地方。旧代码经常用FileReader和FileWriter,它们走的是系统默认字符编码。同一段代码,在你电脑上正常,复制到别人电脑上可能打开文件全是乱码。因为Windows中文环境默认编码是GBK,而Linux通常用UTF-8。

所以我建议所有文件读写都明确指定StandardCharsets.UTF_8。读写代码分别长这样:

private void loadFile(File file) throws IOException { String content = Files.readString(file.toPath(), StandardCharsets.UTF_8); editorArea.setText(content); currentFile = file; documentChanged = false; updateTitle(); updateStatus(); } private void saveFile(File file) throws IOException { String content = editorArea.getText(); Files.writeString(file.toPath(), content, StandardCharsets.UTF_8); currentFile = file; documentChanged = false; updateTitle(); updateStatus(); }

Files.readString和Files.writeString是JDK 11才有的方便方法,如果还在用JDK 8,可以用Files.readAllBytes加new String的方式替代,效果一致。重点是“编码必须显式指定”,不要把这个决定权交给系统。文本编辑器一旦乱码,用户对它的信任基本就归零了。

3.3 新建、覆盖和未保存提醒

新建文件不是简单地把编辑器内容清空。如果当前文档有未保存的修改,直接清空就是灾难。正确顺序是:先判断documentChanged是否为true,是的话弹确认框,用户选择保存、不保存、取消三种结果。

确认框我建议用JOptionPane.showConfirmDialog,选项按钮设为YES_NO_CANCEL_OPTION。返回值的处理逻辑是:点是就执行保存,保存成功才继续新建;点否就丢弃修改,继续新建;点取消就什么都不做,回到编辑状态。新建之后,要把currentFile置为null,标题栏显示“未命名”。

保存和另存为的关联也要理清。保存时先看currentFile是不是null,如果当前文档根本没有关联到磁盘文件,保存动作就要转调另存为逻辑。另存为弹文件选择器,用户选好目标文件后,再调用写入逻辑,并把currentFile更新到新位置。这两个菜单如果各写各的,很容易出现“保存到A文件后,另存为B文件,再点保存还是写进A文件”的怪现象。

覆盖已有文件时,最好增加一个人类可读的确认步骤。JFileChooser有的版本默认会在选择已存在文件时询问,但不同版本行为有差异。安全起见可以在写入前自己判断一次,文件存在就弹“是否覆盖”对话框。

3.4 外部文件被修改的检查思路

这个需求比较进阶,但标题既然说“浏览文件”,就值得考虑。我遇到过一种场景:用编辑器打开一个配置文件,然后另一个程序修改了它,我这边还在继续编辑,最后保存时把别人的修改覆盖了。

更稳妥的做法是打开文件时记录文件的最后修改时间和大小,在保存之前重新检查一遍。如果外部修改时间比本次打开还晚,就提醒用户“文件已被其他程序修改,是否仍然保存”。虽然这个检查只需要几行代码,却能从根上防止很多工作丢失:

private long lastModifiedTime = 0L; private void loadFile(File file) throws IOException { // 读取文件内容并更新界面 lastModifiedTime = file.lastModified(); } private void checkExternalModification(File file) { if (file.lastModified() != lastModifiedTime) { int result = JOptionPane.showConfirmDialog( this, "文件已被外部程序修改,是否仍然覆盖?", "警告", JOptionPane.YES_NO_OPTION); if (result != JOptionPane.YES_OPTION) { return; } } }

3.5 文件读写过程中的异常处理

读写文件不能只写正常路径。用户可能选了没有读取权限的文件、磁盘满了、目标目录不存在、文件正被其他程序占用。这些情况如果不用try-catch兜住,程序会直接抛异常给用户看到一堆堆栈信息,体验很差。

我的习惯是把读写操作包在try-catch里,统一弹一个简洁的错误提示,同时在状态栏显示失败原因。文件读写的错误信息越具体越好,最好能看出是“文件不存在”还是“没有权限”,但不要把堆栈直接贴给用户。

4. 剪贴板功能:从快捷键到系统级剪贴板

4.1 文本组件的内置剪贴板行为

JTextArea自带了一套剪贴板行为,只要编辑区拥有焦点,按Ctrl+C、Ctrl+X、Ctrl+V就能工作。这是因为Swing的文本组件内部处理了TransferHandler。如果只是做一个自己能用的私人小工具,直接用这套默认行为就够了,不用写一行剪贴板代码。

但标题里把“剪贴、复制、粘贴”作为独立功能列出来,意味着要有菜单项和按钮,还要有操作反馈。直接调用editorArea.copy()当然可以,但它有一个问题:它只管编辑器自己的逻辑,不关心当前焦点在哪里,也不会更新你状态栏里的提示。如果你希望复制完成之后状态栏显示“已复制N个字符”,就需要自己接管。

4.2 手动接系统剪贴板的实现

接入系统剪贴板的规范做法是使用Toolkit的Clipboard配合StringSelection。复制操作是从编辑器取选中文字,放到剪贴板;粘贴操作是从剪贴板拿文字,插入编辑器。

复制:

private void copySelection() { String selected = editorArea.getSelectedText(); if (selected == null || selected.isEmpty()) { statusLabel.setText("没有选中文本"); return; } Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(selected), null); statusLabel.setText("已复制 " + selected.length() + " 个字符"); }

粘贴:

private void pasteFromClipboard() { Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); Transferable contents = clipboard.getContents(this); if (contents != null && contents.isDataFlavorSupported(DataFlavor.stringFlavor)) { try { String text = (String) contents.getTransferData(DataFlavor.stringFlavor); editorArea.replaceSelection(text); } catch (Exception ex) { statusLabel.setText("粘贴失败"); } } }

这里有一个关键点:粘贴不要只是把剪贴板内容插入到光标处,而应该调用replaceSelection。如果当前有选中的文字,粘贴时应该替换选中内容,这才是所有文本编辑器的通用行为。新手最容易踩的坑就是先删除选中内容再插入文字,结果操作拆成两步,撤销历史和文档变化会多算一次。

剪切操作是复制选中内容后删除选中内容,这里要注意顺序。正常情况是先复制后删除,这样即使剪贴板写入失败,原文本也不至于被破坏。删掉之后,编辑器不会自动产生撤销的合并记录,因为复制过程不涉及文档变化,所以最后用户看到的是一次剪切行为。

4.3 菜单置灰与焦点切换的问题

剪贴板菜单最好有个状态管理:没有选中文字时,剪切和复制是不可用的;剪贴板为空时,粘贴是不可用的。实现方法是在每次编辑器光标位置变化、选区变化时刷新菜单项状态。

Swing里监听选区变化比较隐蔽,没有像DocumentListener那样直接的SelectionListener。实际可行的办法是监听Caret事件:

editorArea.addCaretListener(e -> updateMenuStates()); private void updateMenuStates() { String selected = editorArea.getSelectedText(); boolean hasSelection = selected != null && !selected.isEmpty(); cutMenuItem.setEnabled(hasSelection); copyMenuItem.setEnabled(hasSelection); pasteMenuItem.setEnabled(true); }

关于粘贴菜单是否置灰,我有一个更实际的做法:不写死,因为剪贴板数据随时可能被其他程序修改。每次刷新状态时都读取系统剪贴板,检查是否包含文本格式。这个操作很轻量,频繁调用也不会卡界面。

焦点切换也需要处理。如果剪贴板操作键绑定在菜单加速器上,而焦点不在编辑器里,按Ctrl+V可能不会触发粘贴。最稳妥的方式是让菜单始终作用于editorArea,而不是当前的焦点组件。把触发动作绑定到editorArea的ActionMap上会更可控,但那样代码复杂度会上升。对轻量编辑器来说,合理的做法是在菜单动作里临时请求焦点:editorArea.requestFocusInWindow(),然后再执行粘贴或复制,避免用户看到菜单灰着却不知道焦点跑到哪去了。

5. 自动换行与字符统计的实现细节

5.1 换行开关为什么不是默认值

JTextArea默认是不自动换行的,一行文字超出可视区域就继续向右延伸,水平滚动条随之出现。这个默认行为对写代码的人来说其实是合理的,代码一行很长,换行后反而看不清结构。但普通用户编辑散文或笔记时,他们期望的是到窗口边缘自动折行,不出现水平滚动条。

实现自动换行只需要两行设置:

editorArea.setLineWrap(true); editorArea.setWrapStyleWord(true);

第一行开启换行,第二行设置“按单词折行”。按单词折行的意思是,如果当前行放不下一整个英文单词,就把整个单词移到下一行,而不是把单词从中间硬截断。对中文来说,每个汉字之间都有换行机会,所以看起来差别不大,但英文文本差别非常明显。

我遇到过一个问题:开启自动换行之后,窗口缩放时文本重排非常频繁,某些环境中会感受到轻微闪烁。这是Swing渲染的固有行为,改进空间有限。如果用户在意这个,可以考虑关闭按单词折行,代价是英文单词会被折断换行,视觉上粗糙很多。实际使用中,多数人更接受按单词折行。

5.2 自动换行和水平滚动条的联动

开启lineWrap之后,还要同步处理滚动条策略。默认的JScrollPane水平滚动条是AS_NEEDED,自动换行开启后,逻辑上就不应该再出现水平滚动条。如果不去设置,就会出现一种很矛盾的状态:内容已经折行了,但水平滚动条仍然存在,用户拖到最右边发现右边是空的。

正确做法是换行开启时,把水平滚动条策略设为NEVER,关闭换行时再改回AS_NEEDED:

private void updateWrapMode() { boolean wrap = wrapMenuItem.isSelected(); editorArea.setLineWrap(wrap); editorArea.setWrapStyleWord(wrap); scrollPane.setHorizontalScrollBarPolicy( wrap ? ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER : ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); }

工具栏上放一个“自动换行”的复选框按钮,用户随时可见,这个体验比藏在菜单里更好。标题栏要求里明确写了“自动换行”,我建议不仅要有菜单项,还要有一个默认开启的开关,让普通用户一打开就能用。

5.3 字符统计为什么不能直接用length()

字符统计看起来是最简单的功能:把编辑区文本取出来,数一下长度。但这里有一个非常典型的Unicode问题。

Java里String.length()返回的是UTF-16代码单元的数量,不是用户真实看到的字符数量。大部分中文、英文、数字都对应一个代码单元,所以length()看起来没问题。可一旦出现表情符号或者生僻字,情况就变了。一个普通的Emoji表情在Java字符串里占两个代码单元,length()会把它算成2,而用户觉得那就是一个字符。

例如“你好”长度为2,但“你好😀”在你的直觉里是3个字符,用length()统计出来却是4。这在标题功能“字符统计”中就属于统计错误。

正确做法是使用codePointCount:

private void updateCharacterCount() { String text = editorArea.getText(); int codePoints = text.codePointCount(0, text.length()); statusLabel.setText("字符数: " + codePoints); }

如果要做得更讲究,可以进一步区分“不含空白字符”“包含换行”“按字节计算”等多种口径。我的经验是在状态栏默认显示按代码点计算的字符数,再在字符统计菜单里提供详细统计弹窗,把字符数、字数、行数、字节数分别列出。

换行符的统计口径也要统一。有些人写字符统计时,把换行符也当作字符,有些人则不把换行算进去。没有一个绝对正确的答案,我采取的办法是:字符数统计的是“非换行字符”,行数统计的是“换行符数量加一”。这样用户在写文章时看到的字符数跟在线编辑器吻合度更高。

6. 实测踩坑:编码、行尾和大文件

6.1 打开旧日志文件时行尾全部丢失

这是我在测试时碰到的第一件怪事。用编辑器打开一个Windows下生成的日志文件,内容能显示,但把所有行保存回去之后,用另一个工具看,发现行尾从CRLF变成了LF。如果团队里有人在做二进制比对,整个文件会显示成“每个文件都被改动过”。

原因在于Files.readAllLines方法会把换行符吃掉,再把文本拼接起来时统一使用系统换行符。这个行为对一般文本编辑需求影响不大,但如果你的编辑器要处理配置文件、脚本文件,行尾样式变掉会引发很多不必要的困扰。

要保留原始行尾,就不能用readAllLines,改用Files.readString直接读取整个文件内容,再把内容原样放進编辑区。保存时也原样写回,不要额外做行尾转换。如果你确实需要提供“转换为LF”的功能,那应该做成一个显式的菜单,而不是在读写过程中偷偷修改。

6.2 编码不对导致满屏乱码

有一回我打开一个用旧版中文Windows保存的文本文件,编辑区里全是乱码。原因很清晰:文件是GBK编码,而程序固定用UTF-8读取。把GBK字节按UTF-8解码,自然会产生大量不可识别的字符。

我的解决方案很朴素:打开文件时默认按UTF-8读,但如果发现解码结果里包含大量替换字符或问号,就提示用户可以选择其他编码。实际编程里,检测编码是个很复杂的课题,没有百分之百可靠的办法。轻量编辑器没必要做全自动编码识别,给用户一个手动选择的入口就够了。

我放在工具栏的编码选择是UTF-8、GBK、ISO-8859-1三种。每次选择后,程序会记住之前读到的原始字节,然后用新编码重新解码一次,把编辑区刷新为新内容。这里要特别注意:重新解码之前必须先保存原始字节,不能从编辑区已有的乱码文本再去做二次解码,那样信息已经丢了。

6.3 十几兆的文件打开后界面卡死

文件打开卡死的主要原因是我一开始用了Files.readString,它会把整个文件一次性读进内存。文本编辑器面对几百KB的文件毫无压力,但面对十几MB的日志文件,主线程会明显卡顿。

方案有两个层次。第一层,先判断文件大小,文件超过一定阈值就提示用户“文件较大,可能需要稍等”。第二层,用SwingWorker在后台线程读取文件,读完后通过EventQueue回到界面线程更新编辑器。SwingWorker虽然写起来稍微多几行,但对提升程序稳定性非常有价值。

SwingWorker<String, Void> worker = new SwingWorker<>() { @Override protected String doInBackground() throws Exception { return Files.readString(file.toPath(), charset); } @Override protected void done() { try { editorArea.setText(get()); } catch (Exception ex) { statusLabel.setText("读取失败: " + ex.getMessage()); } } }; worker.execute();

对于教学项目,我建议至少加上文件大小判断,因为很多用户会拿它去打开各种奇怪的文件,没有防护的代码很容易当场崩溃。

6.4 关闭窗口时数据丢失

还有一次,我写完一段内容直接按右上角关闭,程序没有任何提示就退出了,写下的话全没了。这是因为我把关闭逻辑写成了EXIT_ON_CLOSE,没有接windowClosing事件。

正确的关闭流程分三步:第一步,检查documentChanged;第二步,为true时弹出保存确认;第三步,根据选择执行保存、不保存、取消。保存过程如果失败,不要急着退出,先弹错误提示,等用户确认后再决定。这些逻辑都要写在windowClosing里,才算真正完成了一个文本编辑器的生命周期。

如果用户点的是“取消”,程序应该什么都不做,保持原窗口状态。这一点不难,但很容易写漏。漏了之后的表现是:用户点取消,程序还是退出了,这跟没做确认框没有任何区别。

7. 扩展方向与实际使用感想

7.1 还能加什么功能

如果照着标题做,做到这里已经算完整交付了。离“能用多年”还有不少距离,我会建议进一步扩展这几个功能:

第一是撤销与重做。JTextArea的文档默认其实保留了UndoManager能力的入口,但默认没有开启。自己接入一个UndoManager,把编辑动作记录起来,就能实现Ctrl+Z和Ctrl+Y。这是一个性价比极高的增强,对文本编辑器来说几乎是刚需。

第二是搜索替换。实现KMP或正则搜索都不难,难点在“搜索结果的跳转和定位”。做得简单的话,用一个对话框接受关键词,再循环查找下一次出现的位置即可。替换功能要先定位选中,再调用replaceSelection。

第三是字体和主题设置。文本编辑器绕不开“等宽字体”和“暗色主题”。如果面向写代码的用户,菜单里放一个字体选择框,再放两套主题,体验会提升一个档次。

7.2 我在实际使用中的几点体会

做完这个项目之后,我最大的感受是:文本编辑器是“麻雀虽小,五脏俱全”的典型代表。你以为只是几个菜单,真正串起来才发现,文件、剪贴板、文档状态、界面组件之间全都是关联的。

我建议读者不要只是把代码抄一遍,而是先跑起来,然后试着把某个功能改成自己的写法。比如,把字符统计改成“字数统计”,把自动换行改成“按窗口宽度重新折行”,这些都不是什么高深改动,但每改一次,你对Swing事件模型和组件协作的理解就深一层。

这个项目也适合拿来做面试练手。整个代码量控制在800行上下,却能覆盖文件IO、事件监听、菜单构建、异常处理、线程使用这些高频知识点。如果你能把每个菜单背后的设计决策都讲清楚,比背一百个面试八股有用得多。

最后再分享一个小技巧:开发时让状态栏常显“字符数”和“编码”,这不仅是给用户看的,也是给自己调试用的。遇到乱码时,先看状态栏编码是什么;遇到统计不实时时,先看DocumentListener是不是忘接了。这个小习惯帮我省下了大量排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询