资讯动态

Java Swing记事本源码精讲:编码换行、查找高亮与打包避坑

发布时间:2026/10/23 1:43:47 来源:尧图企业网站定制
简介「Java开发记事本完整版」面向具备Java基础、希望理解桌面应用开发流程的学习者以可运行的小型文本编辑器为例串联Swing界面搭建、事件监听、文件读写与异常处理等知识点。压缩包共36个文件约103KB包含25个class文件、5个java源码及jar可执行包并辅以txt说明和项目配置便于直接运行并对照源码阅读。已有4194人学习下载适合作为课程设计参考或GUI入门练习。项目覆盖文件打开保存、文本编辑、剪切复制粘贴、查找替换、撤销重做、菜单栏与快捷键、状态栏提示等常规功能读者可借此观察JTextArea、JFileChooser、TransferHandler等组件的协作方式理解操作历史栈与异常捕获的组织思路。借助完整源码与可执行产物能够较快建立从界面布局到事件响应、再到文件操作的完整认识并积累模块拆分与代码组织经验。1. 拿到一份 Java 开发记事本完整版源码先别急着改界面拿到一份 Java 开发记事本完整版源码时多数人的第一反应是——记事本还能有什么难度真正上手改过一次就知道难的从来不是那个 JTextArea而是它背后那些看不见的边角料打开一个 Windows 下保存的 UTF-8 文件第一个字符变成方块是因为 BOM 没剥保存后在系统自带工具里打开只剩一行是因为换行符没归一化CtrlF 弹了窗但高亮不出现是因为偏移量算错在了文本而不是 Document 上。这份资源的价值就在这些地方——文件读写、查找替换、字体缩放、自动换行、撤销重做、状态栏行列号一整套交互都落在了代码里。它适合两类人刚学完 Swing 想找一个能改得动的完整工程练手的人以及需要一个轻量文本底座、打算二开成配置编辑器或日志查看器的从业者。下面按「先跑起来、再拆模块、最后打包和踩坑」的顺序走一遍。2. 把工程跑起来JDK 选型、目录结构和第一条启动命令Swing 记事本这类工程跑不起来九成原因不在业务代码而在 JDK 版本和源文件编码。我拿到一份 Java 开发记事本完整版源码后习惯先确认两件事工程是按哪个 JDK 写的源文件是不是 UTF-8。这两点定下来后面的编译、打包、分发才有意义否则你会在一堆「找不到符号」和「编码 GBK 的不可映射字符」里浪费一整个下午。2.1 为什么这类记事本用 Swing 而不是 JavaFX选型上这份资源走的是 Swing 路线这个决定是合理的。记事本的形态是「一个窗口 一个文本区 若干菜单 一个状态栏」JTextArea 配合 Document 模型已经完全够用UndoManager、DefaultHighlighter、JFileChooser 这些零件全是 JDK 自带不需要任何外部依赖。JavaFX 从 JDK 11 起被剥离成独立模块你得单独引 openjfx 或者配 Maven 坐标一旦网络抽风工程连编译都过不去。维度SwingJavaFX依赖JDK 内置零外部包JDK 11 需单独引入模块文本组件JTextArea / JTextPaneDocument 模型成熟TextArea / TextFlowAPI 更现代打包体积小几 MB 的 jar 即可分发需带 JavaFX 运行时体积明显更大中文资料多但偏旧坑基本都被踩过相对少遇到问题排查成本高适合场景内部小工具、单文件分发、二次开发底座需要动画、CSS 化主题的现代 UI我一般会跟人说如果你要做的是一个「双击就能开、不装运行时也能跑」的小工具Swing 是更稳的选择如果你要做的是带主题切换、动效的产品级客户端那才轮到 JavaFX 上场。记事本显然属于前者。另外还有一点容易被忽略Swing 的 JTextArea 在撤销栈、高亮、文档过滤这些能力上打磨了二十多年很多边界情况已经被别人踩平了你自己从零搭一套反而容易漏。2.2 工程目录与编译运行的三条命令一份结构清晰的记事本工程目录大致长这样。如果你拿到的源码目录名不同按职责对应过去即可重点是「界面 / 核心逻辑 / 配置」三层要分开否则改一个保存逻辑得在 Frame 里翻八百行。notepad/ ├── src/ │ └── com/example/notepad/ │ ├── Main.java # 启动入口只负责 EDT 和外观 │ ├── ui/NotepadFrame.java # 主窗口管布局和组件装配 │ ├── ui/MenuBuilder.java # 菜单与 Action 定义 │ ├── ui/StatusBar.java # 状态栏监听光标与文档变化 │ ├── core/FileService.java # 读写、编码、换行符归一化 │ ├── core/SearchService.java # 查找匹配与高亮 │ └── core/EditorConfig.java # 字体、Tab 宽度、自动换行开关 ├── resources/ │ └── icons/ # 图标走 classpath不要用绝对路径 └── out/ # 编译输出目录提交时忽略编译和运行我一般用三条命令搞定不依赖 IDE。第一条命令里的-encoding UTF-8必须显式写因为部分平台上 javac 的默认编码跟着系统区域走源码里有中文注释或中文常量时就会直接报错。# 1. 编译把所有 java 文件编到 out 目录显式指定源文件编码 javac -encoding UTF-8 -d out $(find src -name *.java) # 2. 运行-cp 指向编译输出后面跟全限定类名 java -cp out com.example.notepad.Main # 3. 打成可执行 jar清单里写入 Main-Class双击即可启动 jar --create --file notepad.jar --main-class com.example.notepad.Main -C out .三条命令分别对应的参数含义-d out决定 class 文件的落盘位置后面-cp必须和它对齐否则会报 NoClassDefFoundError--main-class写的是全限定名写成Main这种简名打出来的 jar 双击只会闪退-C out .的意思是「切到 out 目录再把里面所有东西打进包」这一步搞错了会出现 jar 里套 jar 的经典翻车现场。Windows 上没有find命令把第一条换成dir /s /b src\*.java sources.txt再javac -encoding UTF-8 -d out sources.txt效果一样。提示编译前先确认java -version和javac -version是同一个大版本。见过太多次 PATH 里 java 是 17、javac 是 8 的情况报错信息完全不指向真正原因。2.3 启动入口与主窗口的职责边界入口类只干三件事切到事件分发线程、设置系统外观、实例化主窗口。这三件事顺序不能乱尤其是外观设置必须发生在任何 Swing 组件实例化之前否则部分组件已经按默认主题渲染完了再改就会出现一半新一半旧的割裂感。public final class Main { public static void main(String[] args) { // Swing 组件不是线程安全的所有 UI 构建必须回到 EDT 上做 SwingUtilities.invokeLater(() - { try { // 跟随系统外观避免默认 Metal 主题在高分屏上发虚 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ignored) { // 外观设置失败不应该阻断启动退回默认主题即可 } NotepadFrame frame new NotepadFrame(); frame.setLocationRelativeTo(null); // 居中null 表示相对屏幕而非父窗口 frame.setVisible(true); }); } }职责边界这块我建议主窗口只负责「装配」菜单、文本区、状态栏三块拼起来具体行为委托给 core 包。比如保存动作调用FileService.write(path, textArea.getText())查找动作调用SearchService.findAll(...)窗口本身不碰 IO。这样做的好处很直接——想写单元测试的时候你可以直接 new 一个 FileService 跑编码和换行符的用例不用启动整个 GUI。很多教学版记事本把所有逻辑塞进一个上千行的 Frame 类里改一个功能要通读全文这种代码拿来练手可以拿来二开就是灾难。3. 编辑器核心JTextArea、Document 与文件读写的参数细节文本编辑器看着简单实际是一个「字符串 ↔ 文档模型 ↔ 文件字节」三层映射的问题。中间任何一层处理错表现出来就是乱码、丢行、高亮错位。这一章按「组件怎么搭 → 文件怎么读 → 内容怎么找 → 编辑怎么撤」的顺序拆每一段都对应源码里可以改的参数。3.1 最小可用编辑器JTextArea 套 JScrollPane文本区本身不带滚动条必须套一层 JScrollPane这是新手最容易漏的一步——不套的话内容超出就看不见了也没有任何提示。下面这几行是我在任何一个文本编辑器项目里都会先写的初始化代码。JTextArea textArea new JTextArea(); // 用逻辑字体族具体字形由系统兜底避免硬编码字体名在别的机器上找不到 textArea.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 16)); textArea.setLineWrap(true); // 按可用宽度折行看长日志必须开 textArea.setWrapStyleWord(true); // 按单词边界折行纯中文/日志场景可以关掉 textArea.setTabSize(4); // Tab 的显示宽度默认 8 在代码场景下太宽 textArea.setMargin(new Insets(8, 10, 8, 10)); // 内边距文字贴边很难看 JScrollPane scrollPane new JScrollPane(textArea); // 始终显示纵向滚动条避免内容增多时界面跳动 scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);参数上要留意两个联动关系setWrapStyleWord(true)只在setLineWrap(true)时才有意义关了自动换行它就是个摆设setTabSize只影响显示宽度不改内容里的\t字符本身所以它对文件读写没有任何副作用。另外如果你的记事本要显示行号不要在 JTextArea 里塞文本正确做法是给 JScrollPane 设置一个自定义的 RowHeaderView让它跟着文本区一起滚动。注意JTextArea 默认没有撤销能力也没有语法高亮它是纯文本组件。需要富文本或高亮文本时应该换 JTextPane两者的 Document 类型不同迁移成本不低一开始就要选对。3.2 打开与保存编码、换行符、BOM 三个参数文件读写是记事本最容易被低估的部分。用FileReader/FileWriter是最省事也最容易翻车的写法因为它们用的是平台默认编码同一个人写的文件换台机器打开就是一堆问号。我的做法是全程走Files 显式字符集把编码、BOM、换行符三件事分别处理。public static String read(Path path) throws IOException { byte[] bytes Files.readAllBytes(path); // 剥掉 UTF-8 BOM否则第一个字符会变成不可见的 \uFEFF int offset 0; if (bytes.length 3 (bytes[0] 0xFF) 0xEF (bytes[1] 0xFF) 0xBB (bytes[2] 0xFF) 0xBF) { offset 3; } return new String(bytes, offset, bytes.length - offset, StandardCharsets.UTF_8); } public static void write(Path path, String content) throws IOException { // 先统一成 \n再替换成平台换行符。顺序反了会得到 \r\r\n String normalized content.replace(\r\n, \n).replace(\r, \n); String withSeparator normalized.replace(\n, System.lineSeparator()); Files.write(path, withSeparator.getBytes(StandardCharsets.UTF_8)); }逐段说明。BOM 判断那段用 0xFF是因为 Java 的 byte 是有符号的0xEF直接比较会得到负数永远不相等这是很多人写了半天 BOM 检测却不生效的真正原因。换行符归一化必须先\r\n → \n再\r → \n顺序反了会把一个\r\n变成两个换行文件凭空多出一倍空行。保存时用System.lineSeparator()是为了让 Windows 用户用系统自带工具打开时能正常显示如果这份文件要进 Git 或跨平台流转那就反过来固定写\n并配一个.gitattributes。还有个容易被漏掉的点Files.readAllBytes会把整个文件读进内存十几 MB 的日志文件还好上百 MB 就会直接 OOM。真要处理大文件正确做法是换成BufferedReader逐行读、边读边追加到 Document同时给 JTextArea 设一个行数上限超了之后做只读预览。教学版可以不考虑但你要拿它当日常工具用这条边界得心里有数。3.3 查找替换从 indexOf 到高亮偏移查找功能的核心是拿到所有匹配位置的字符偏移然后交给高亮器画出来。偏移必须基于 Document 而不是屏幕坐标这一点是所有高亮错位的根源。public static Listint[] findAll(String text, String keyword, boolean caseSensitive) { Listint[] hits new ArrayList(); if (keyword null || keyword.isEmpty()) { return hits; } // 大小写不敏感时统一降小写必须用 Locale.ROOT 防土耳其语 i 问题 String haystack caseSensitive ? text : text.toLowerCase(Locale.ROOT); String needle caseSensitive ? keyword : keyword.toLowerCase(Locale.ROOT); int from 0; while (true) { int idx haystack.indexOf(needle, from); if (idx 0) { break; } hits.add(new int[]{idx, idx keyword.length()}); // 步进至少 1防止空串导致死循环按关键词长度走避免重叠匹配 from idx Math.max(1, keyword.length()); } return hits; }拿到偏移后画高亮需要先把旧的清掉再逐个添加。JTextArea 的默认高亮器是 DefaultHighlighter可以直接强转使用。DefaultHighlighter highlighter (DefaultHighlighter) textArea.getHighlighter(); highlighter.removeAllHighlights(); // 每次查找先清否则高亮会越积越多 Highlighter.HighlightPainter painter new DefaultHighlighter.DefaultHighlightPainter(new Color(0xFFF59D)); for (int[] range : hits) { highlighter.addHighlight(range[0], range[1], painter); }参数解释range[0]和range[1]是左闭右开的字符偏移不是行号也不是像素DefaultHighlightPainter的颜色带 alpha 通道时会自动和背景混合想要半透明效果传一个带透明度的 Color 即可。Locale.ROOT那个细节值得单独记一笔——土耳其语环境下I.toLowerCase()会得到带点的ı导致大小写不敏感的查找莫名其妙漏匹配这个坑我踩过一次排查了半天。替换逻辑在查找基础上多一步从后往前替换这样前面的偏移不会被后面的替换影响。如果是「全部替换」记得替换完成后重新跑一次查找刷新高亮否则界面上还留着旧位置的高亮看着像没生效。3.4 撤销重做与编辑菜单的联动UndoManager 挂到 Document 上就能自动收集编辑操作不需要你手动记录每一步。挂载方式很简单但初始化和菜单可用态刷新这两块经常写错。UndoManager undoManager new UndoManager(); // 默认只保留 100 条编辑记录长文本编辑建议调大 undoManager.setLimit(500); textArea.getDocument().addUndoableEditListener(undoManager); undoItem.addActionListener(e - { if (undoManager.canUndo()) { undoManager.undo(); } }); redoItem.addActionListener(e - { if (undoManager.canRedo()) { undoManager.redo(); } });要点setLimit控制的是编辑记录条数不是字符数一次大段粘贴算一条记录所以调大它并不会显著增加内存占用。canUndo()/canRedo()的检查不能省虽然 UndoManager 内部也做了判断但菜单项的可用状态需要你在每次编辑后主动刷新否则会出现「菜单是灰的但其实能撤销」的错觉。刷新时机一般挂在 DocumentListener 的insertUpdate和removeUpdate里两者都要覆盖只监听插入会导致删除操作后撤销按钮不亮。还有一处需要提前想到打开新文件时调用textArea.setText(...)本身会产生一条可撤销编辑如果不处理用户打开文件后按 CtrlZ 会把内容撤成空白看起来像文件被清空了。常见做法是在 setText 之后调一次undoManager.discardAllEdits()把历史清干净。4. 菜单、快捷键与状态栏把散装功能焊成完整交互功能都实现之后还有一层工作是把它们接起来菜单项、工具栏按钮、快捷键、右键菜单这四样其实是同一个动作的四种入口。如果每个入口写一遍逻辑后面改一次行为要改四处。这一章讲怎么用 Action 收口以及快捷键失效这种玄学问题怎么定位。4.1 菜单项抽成 Action一处定义四处复用Swing 的 Action 接口天生就是为这件事设计的它同时承载了名称、图标、快捷键、启用状态和执行逻辑。把「保存」抽成一个 Action 之后菜单项、工具栏按钮、快捷键绑定全部指向同一个实例启用状态也只需要维护一处。Action saveAction new AbstractAction(保存, saveIcon) { Override public void actionPerformed(ActionEvent e) { // 真正逻辑委托给 core窗口层只负责拿当前路径和文本 FileService.write(currentPath, textArea.getText()); setDirty(false); } }; // 菜单项和工具栏按钮都从同一个 Action 构造行为天然一致 JMenuItem saveItem new JMenuItem(saveAction); JButton saveButton new JButton(saveAction);这样做还有个附带好处setEnabled(false)一旦作用在 Action 上所有由它构造出来的组件会同步变灰不需要你挨个去设置。对于「保存」这种需要判断是否有未保存修改的动作这一点能省掉大量同步代码。4.2 快捷键绑定的两条路径与冲突排查Swing 里绑快捷键有两条路给 JMenuItem 设setAccelerator或者往组件的 InputMap 里绑 KeyStroke。前者依赖菜单层级只在菜单可见时生效后者直接绑在组件上不受菜单影响。记事本这类应用我的习惯是全局动作走 Accelerator跟文本区强相关的比如 Tab 缩进、Ctrl滚轮缩放走 InputMap。// 路径一菜单加速键适合打开/保存/退出这类全局动作 saveItem.setAccelerator(KeyStroke.getKeyStroke( KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); // 路径二InputMap 绑定适合只在编辑区生效的动作 textArea.getInputMap().put( KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.CTRL_DOWN_MASK), fontZoomIn); textArea.getActionMap().put(fontZoomIn, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { Font f textArea.getFont(); textArea.setFont(f.deriveFont(Math.min(f.getSize() 1f, 48f))); } });冲突排查有一条很实用的判断方法按下快捷键没反应时先看这个键是不是被 JTextArea 自带的 InputMap 消费了。CtrlC、CtrlV、CtrlX、CtrlA 这些默认就有绑定你如果再绑一次同组合键新绑定会覆盖旧的表现为「复制突然不好使了」。定位方式是打印textArea.getInputMap().allKeys()看当前所有绑定一眼就能发现重复。提示macOS 上习惯用 Command 键代码里用Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()取掩码不要硬编码 CTRL_DOWN_MASK否则跨平台体验会很难受。4.3 状态栏行列号、字符数与当前编码状态栏看起来是装饰其实是最容易暴露实现质量的地方。行列号要跟光标实时同步字符数要跟文档长度同步编码和换行符要跟当前文件同步——三个数据源三个监听器。textArea.addCaretListener(e - { try { int caret e.getDot(); int line textArea.getLineOfOffset(caret) 1; // 行号从 1 开始更符合习惯 int col caret - textArea.getLineStartOffset(line - 1) 1; positionLabel.setText(行 line 列 col); } catch (BadLocationException ignored) { // 光标位置在文档变更瞬间可能越界忽略即可 } }); textArea.getDocument().addDocumentListener(new DocumentListener() { private void refresh() { countLabel.setText(字符数 textArea.getDocument().getLength()); } Override public void insertUpdate(DocumentEvent e) { refresh(); } Override public void removeUpdate(DocumentEvent e) { refresh(); } Override public void changedUpdate(DocumentEvent e) { refresh(); } });这里有个细节getLineOfOffset和getLineStartOffset在文档正在变更时可能抛 BadLocationException必须捕获否则用户快速打字的时候控制台会刷一堆异常栈。字符数用getLength()拿的是字符数不是字节数中英文都算一个如果需求方要的是「文件大小」那应该用Files.size(path)拿字节数两者概念不同。4.4 字体缩放与自动换行的实时切换字体缩放最自然的交互是 Ctrl滚轮这个需要监听鼠标滚轮事件并判断修饰键。自动换行则是一个开关菜单项切换时同步更新 JTextArea 的两个属性。textArea.addMouseWheelListener(e - { // 只在按住 Ctrl 时接管滚轮否则交还给 JScrollPane 正常滚动 if (!e.isControlDown()) { return; } Font current textArea.getFont(); float size current.getSize2D() (e.getWheelRotation() 0 ? 1f : -1f); size Math.max(8f, Math.min(size, 72f)); // 夹住上下限防止缩到看不见或撑爆 textArea.setFont(current.deriveFont(size)); e.consume(); // 消费掉事件避免滚动条同时滚动 });deriveFont而不是 new Font是因为前者保留了字体族的其他属性粗体、斜体后者会把样式重置成 PLAIN。上下限夹取那两行别嫌啰嗦没有它的话用户把滚轮一直往下滚字号会变成 0 甚至负数界面上文字直接消失看起来像程序崩了。e.consume()同样重要不消费的话滚轮会同时触发缩放和滚动体验很怪。5. 避坑与排查五个把记事本跑崩的真实场景这一章的东西基本都是从翻车里攒出来的。有些问题看一眼报错就能定位有些则完全没有异常、只是行为不对后者才最耗时间。5.1 现象中文保存后重新打开变乱码原因几乎总是编码没显式指定。FileWriter和FileReader用的是file.encoding这个系统属性同一份代码在不同机器上跑结果不一样。解决方式是所有 IO 都改成Files.newBufferedReader(path, StandardCharsets.UTF_8)这类显式字符集的写法并且把这个约定写进工程的检查清单。如果文件来源不确定还得加一层编码探测——先按 UTF-8 解码遇到非法字节序列再退回 GBK探测逻辑不要做得太聪明猜错了比报错更麻烦。5.2 现象文件在系统自带工具里打开只有一行原因在换行符。你写出去的是\n而那个工具早期版本只认\r\n。解决办法是根据目标平台做归一化或者干脆提供一个「另存为 Windows 换行」的菜单项让用户自己选。反过来也有坑从 Windows 拷贝过来的文本里混着\r\n直接塞进 JTextArea 会在行尾显示一个不可见字符光标移动时会多停一格看起来像卡了一下这时候也需要在加载时统一成\n。5.3 现象CtrlZ 撤销之后内容全没了原因是打开文件时setText产生的那条编辑记录没有被丢弃用户第一次按撤销撤的其实是「整个文档被替换成新内容」这一步。解决办法是在加载完成、光标复位之后立刻调undoManager.discardAllEdits()。这条我建议直接写成一个loadFile(Path)方法把读文件、setText、清撤销栈、重置 dirty 标记四步封在一起任何人调都不会漏。5.4 现象打包成 jar 后图标全丢控制台不报错原因是用new File(resources/icons/save.png)这种方式加载资源。打成 jar 之后资源在压缩包里文件系统路径根本不存在。正确写法是Main.class.getResourceAsStream(/icons/save.png)走 classpath。排查方法很简单在 IDE 里跑正常、双击 jar 就丢图标基本可以锁定是资源加载路径问题。顺带一提getResourceAsStream返回 null 时不会抛异常你得自己判空并在启动时打一行日志否则就是静默失败。5.5 现象关闭窗口时未保存的修改直接蒸发原因是没有实现关闭确认。JFrame 默认的关闭行为是直接 dispose不会问你。解决方式是setDefaultCloseOperation(DO_NOTHING_ON_CLOSE)然后在 WindowListener 的windowClosing里判断 dirty 标记弹一个「保存 / 不保存 / 取消」的三选项对话框选「取消」就 return 不关闭。这里还有个连带问题dirty 标记需要在每次文档变更时置 true在保存成功后置 false两个时机漏掉任何一个用户都会遇到「明明保存了还提示未保存」或者「改了没提示直接退出」。6. 从能跑到能发打包、回归验证和一份自检清单工程能跑只是起点真正交付给别人的时候还得解决「双击就能开」这件事。JDK 14 之后自带了 jpackage可以直接生成自带运行时的安装包不用再教用户装 JDK。# 先按 2.2 的方式打出 notepad.jar 放进 out 目录再执行打包 # --type app-image 生成免安装目录换成 msi/exe 则生成安装程序 jpackage --type app-image \ --name SimpleNotepad \ --input out \ --main-jar notepad.jar \ --main-class com.example.notepad.Main \ --java-options -Dfile.encodingUTF-8参数上要留意--input指向的是包含 jar 的目录而不是 jar 本身--main-jar是相对--input的文件名--java-options里那句-Dfile.encodingUTF-8是给运行时兜底的虽然代码里已经全程显式字符集但第三方库不一定这么老实。生成出来的目录可以直接压缩发给别人双击里面的可执行文件就能开。打包完之后我会固定跑一遍下面这张自检表。这张表是从几次「本地好好的、到别人机器上就出问题」的经历里总结出来的逐条过一遍大概五分钟能挡掉大部分返工。检查项验证方式期望结果中文读写打开含中文的 UTF-8 文件另存为再打开内容与编码均不变BOM 处理打开带 BOM 的文件看第一个字符无不可见字符、无方块换行符保存后在系统自带工具打开行数与原文件一致查找高亮查找高频词逐个跳转高亮位置准确不偏移撤销边界打开新文件后按 CtrlZ内容不被清空大文件打开一个几十 MB 的日志不卡死或给出明确提示资源加载双击 jar 运行图标、关于框图片正常显示关闭确认修改内容后点关闭弹出三选项对话框最后说一个我自己的习惯。这个习惯是被坑出来的任何时候我往工程里加一个新的 IO 入口——不管是「导入」「导出」还是「最近打开」——我都会强制自己先手写一个最小的测试用例拿一个带 BOM 的中文文件走一遍完整链路确认读出来的第一个字符和最后一个换行都对再回去接界面。因为编码和换行符的问题在 GUI 上表现出来永远是「看起来不太对但说不上哪不对」等你靠肉眼发现的时候用户早就反馈过来了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑