第18章:项目实战 - 笔记本应用
作者:步子哥 (steper@foxmail.com)
18.1 需求分析
功能列表
笔记本应用 = 电子笔记本
- 就像你在纸上写笔记
- 但可以在电脑上写、改、保存
类比:
笔记本应用 = 便利贴
- 你可以在便利贴上写字
- 也可以擦掉重写
- 可以贴在任何地方
核心功能:
- 文件操作
- 新建笔记 - 打开笔记 - 保存笔记 - 另存为 - 最近文件列表
- 编辑功能
- 撤销/重做 - 查找与替换 - 剪切/复制/粘贴
- 视图功能
- 缩放字体 - 切换主题 - 全屏模式
- 辅助功能
- 字数统计 - 自动保存 - 快捷键
技术选型
技术选型 = 搬家的工具
- 钉子、锤子、锯子(GUI 库:SWT)
- 木材、钉子、油漆(依赖:SWT)
- 图纸、设计图(布局:GridLayout)
类比:
技术选型 = 烹饪工具
- 锅、铲子、菜刀(GUI 库:SWT)
- 米、菜、调料(依赖:SWT)
- 食谱(布局:GridLayout)
技术栈:
- GUI 库:SWT 3.124.200
- 构建工具:Maven
- 语言:Java 11+
- 布局管理器:GridLayout
18.2 界面设计
主窗口布局
主窗口 = 笔记本的外壳
┌─────────────────────────────────────┐
│ 菜单栏(File, Edit, View, Help) │
├─────────────────────────────────────┤
│ 工具栏(新建、打开、保存、剪贴、复制、粘贴、撤销、重做、查找、替换) │
├─────────────────────────────────────┤
│ 文本编辑区域(占大部分空间) │
│ │
│ │
├─────────────────────────────────────┤
│ 状态栏(行数、列数、字数、保存状态) │
└─────────────────────────────────────┘
费曼解释:主窗口的布局
主窗口 = 电影院
- 菜单栏 = 电影片头(显示电影名)
- 工具栏 = 控制台(播放、暂停、音量)
- 文本编辑区域 = 屏幕(显示电影)
- 状态栏 = 字幕(显示当前信息)
类比:
主窗口 = 餐厅
- 菜单栏 = 餐厅菜单(显示菜品)
- 工具栏 = 服务员(端菜、倒水、结账)
- 文本编辑区域 = 餐桌(放食物)
- 状态栏 = 账单(显示金额)
菜单栏设计
文件菜单:
- 新建(Ctrl+N)
- 打开(Ctrl+O)
- 保存(Ctrl+S)
- 另存为(Ctrl+Shift+S)
- 分隔线
- 退出(Ctrl+Q)
编辑菜单:
- 撤销(Ctrl+Z)
- 重做(Ctrl+Y)
- 分隔线
- 剪切(Ctrl+X)
- 复制(Ctrl+C)
- 粘贴(Ctrl+V)
- 分隔线
- 查找(Ctrl+F)
- 替换(Ctrl+H)
视图菜单:
- 放大字体(Ctrl++)
- 缩小字体(Ctrl+-)
- 正常字体(Ctrl+0)
- 分隔线
- 切换主题
- 分隔线
- 全屏模式(F11)
帮助菜单:
- 关于
工具栏设计
工具栏 = 快捷按钮
- 新建
- 打开
- 保存
- 分隔线
- 剪切
- 复制
- 粘贴
- 分隔线
- 撤销
- 重做
- 分隔线
- 查找
- 替换
18.3 数据模型
笔记的数据结构
class Note {
private String title; // 笔记标题
private String content; // 笔记内容
private String filePath; // 文件路径
private boolean modified; // 是否已修改
private long lastSaved; // 最后保存时间
// 构造函数
public Note() {
this.title = "未命名笔记";
this.content = "";
this.filePath = null;
this.modified = false;
this.lastSaved = 0;
}
public Note(String title, String content) {
this.title = title;
this.content = content;
this.filePath = null;
this.modified = true;
this.lastSaved = 0;
}
// Getter 和 Setter
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getFilePath() { return filePath; }
public void setFilePath(String filePath) { this.filePath = filePath; }
public boolean isModified() { return modified; }
public void setModified(boolean modified) { this.modified = modified; }
public long getLastSaved() { return lastSaved; }
public void setLastSaved(long lastSaved) { this.lastSaved = lastSaved; }
}
费曼解释:数据模型的含义
数据模型 = 产品的规格书
- 产品名称(笔记标题)
- 产品描述(笔记内容)
- 产品编号(文件路径)
- 产品状态(是否已修改)
- 产品日期(最后保存时间)
类比:
数据模型 = 个人信息卡
- 姓名(笔记标题)
- 地址(笔记内容)
- 身份证号(文件路径)
- 状态(是否已修改)
- 出生日期(最后保存时间)
文件的保存与加载
class NoteFileManager {
// 保存笔记到文件
public static void save(Note note) throws IOException {
String filePath = note.getFilePath();
if (filePath == null) {
throw new IOException("笔记没有文件路径");
}
Files.write(Paths.get(filePath), note.getContent().getBytes(StandardCharsets.UTF_8));
note.setModified(false);
note.setLastSaved(System.currentTimeMillis());
}
// 从文件加载笔记
public static Note load(String filePath) throws IOException {
String content = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
Note note = new Note();
note.setFilePath(filePath);
note.setContent(content);
note.setModified(false);
note.setLastSaved(System.currentTimeMillis());
// 从文件名提取标题
File file = new File(filePath);
note.setTitle(file.getName());
return note;
}
}
费曼解释:文件保存与加载的原理
保存 = 写日记
- 把想法(笔记内容)写在日记本(文件)上
- 合上日记本(关闭文件)
加载 = 看日记
- 打开日记本(文件)
- 阅读日记内容(读取文件)
类比:
保存 = 存钱
- 把钱(笔记内容)存入银行(文件)
- 存款单(最后保存时间)
加载 = 取钱
- 拿存款单(文件路径)
- 从银行取出钱(读取文件)
18.4 功能实现
新建、打开、保存、另存为
// 新建笔记
public void newNote() {
// 如果当前笔记已修改,询问是否保存
if (currentNote != null && currentNote.isModified()) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO | SWT.CANCEL);
messageBox.setText("保存");
messageBox.setMessage("当前笔记已修改,是否保存?");
int result = messageBox.open();
if (result == SWT.YES) {
saveCurrentNote();
} else if (result == SWT.CANCEL) {
return; // 取消新建
}
}
// 创建新笔记
currentNote = new Note();
updateUI();
updateWindowTitle();
// 清空最近文件列表
recentFiles.clear();
updateRecentFilesMenu();
}
// 打开笔记
public void openNote() {
// 如果当前笔记已修改,询问是否保存
if (currentNote != null && currentNote.isModified()) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO | SWT.CANCEL);
messageBox.setText("保存");
messageBox.setMessage("当前笔记已修改,是否保存?");
int result = messageBox.open();
if (result == SWT.YES) {
saveCurrentNote();
} else if (result == SWT.CANCEL) {
return; // 取消打开
}
}
// 选择文件
FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
fileDialog.setText("打开笔记");
fileDialog.setFilterExtensions(new String[]{"*.txt"});
fileDialog.setFilterNames(new String[]{"文本文件 (*.txt)"});
String filePath = fileDialog.open();
if (filePath != null) {
try {
// 加载笔记
currentNote = NoteFileManager.load(filePath);
updateUI();
updateWindowTitle();
// 添加到最近文件列表
addToRecentFiles(filePath);
} catch (IOException e) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法打开文件:" + e.getMessage());
messageBox.open();
}
}
}
// 保存笔记
public void saveCurrentNote() {
if (currentNote == null || currentNote.getFilePath() == null) {
// 另存为
saveAsNote();
return;
}
try {
NoteFileManager.save(currentNote);
updateStatus("已保存");
updateWindowTitle();
} catch (IOException e) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法保存文件:" + e.getMessage());
messageBox.open();
}
}
// 另存为
public void saveAsNote() {
if (currentNote == null) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK);
messageBox.setText("警告");
messageBox.setMessage("没有笔记可保存");
messageBox.open();
return;
}
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setText("另存为");
fileDialog.setFilterExtensions(new String[]{"*.txt"});
fileDialog.setFilterNames(new String[]{"文本文件 (*.txt)"});
if (currentNote.getFilePath() != null) {
fileDialog.setFileName(new File(currentNote.getFilePath()).getName());
}
String filePath = fileDialog.open();
if (filePath != null) {
currentNote.setFilePath(filePath);
try {
NoteFileManager.save(currentNote);
updateStatus("已保存");
updateWindowTitle();
// 添加到最近文件列表
addToRecentFiles(filePath);
} catch (IOException e) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法保存文件:" + e.getMessage());
messageBox.open();
}
}
}
文本编辑
// 撤销/重做
private Stack<String> undoStack = new Stack<>();
private Stack<String> redoStack = new Stack<>();
public void undo() {
if (undoStack.isEmpty()) {
return;
}
String currentContent = textArea.getText();
redoStack.push(currentContent);
String previousContent = undoStack.pop();
textArea.setText(previousContent);
currentNote.setContent(previousContent);
currentNote.setModified(true);
updateUI();
}
public void redo() {
if (redoStack.isEmpty()) {
return;
}
String currentContent = textArea.getText();
undoStack.push(currentContent);
String nextContent = redoStack.pop();
textArea.setText(nextContent);
currentNote.setContent(nextContent);
currentNote.setModified(true);
updateUI();
}
// 剪切
public void cut() {
textArea.cut();
updateClipboard();
}
// 复制
public void copy() {
textArea.copy();
updateClipboard();
}
// 粘贴
public void paste() {
textArea.paste();
updateUI();
}
查找与替换
// 查找
public void find() {
FindReplaceDialog dialog = new FindReplaceDialog(shell, textArea);
dialog.open();
}
public class FindReplaceDialog extends Dialog {
private Text textArea;
private Text findText;
private Button findNextButton;
private Button findPreviousButton;
public FindReplaceDialog(Shell parent, Text textArea) {
super(parent);
this.textArea = textArea;
}
@Override
protected Control createDialogArea(Composite parent) {
Composite composite = (Composite) super.createDialogArea(parent);
composite.setLayout(new GridLayout(3, false));
Label findLabel = new Label(composite, SWT.NONE);
findLabel.setText("查找:");
findText = new Text(composite, SWT.BORDER);
findText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1));
findNextButton = new Button(composite, SWT.PUSH);
findNextButton.setText("查找下一个");
findNextButton.addListener(SWT.Selection, event -> {
String searchText = findText.getText().trim();
if (!searchText.isEmpty()) {
findNext(searchText);
}
});
return composite;
}
private void findNext(String searchText) {
String content = textArea.getText();
int currentPosition = textArea.getCaretPosition();
int position = content.indexOf(searchText, currentPosition);
if (position == -1) {
position = content.indexOf(searchText);
}
if (position != -1) {
textArea.setSelection(position, position + searchText.length());
textArea.showSelection();
} else {
MessageBox messageBox = new MessageBox(getShell(), SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("查找");
messageBox.setMessage("未找到:" + searchText);
messageBox.open();
}
}
}
最近文件列表
private List<String> recentFiles = new ArrayList<>();
private static final int MAX_RECENT_FILES = 10;
// 添加到最近文件列表
public void addToRecentFiles(String filePath) {
// 移除已存在的
recentFiles.remove(filePath);
// 添加到最前面
recentFiles.add(0, filePath);
// 限制数量
if (recentFiles.size() > MAX_RECENT_FILES) {
recentFiles = new ArrayList<>(recentFiles.subList(0, MAX_RECENT_FILES));
}
// 保存到配置文件
saveRecentFiles();
// 更新菜单
updateRecentFilesMenu();
}
// 更新最近文件菜单
public void updateRecentFilesMenu() {
// 清除现有菜单项
MenuItem[] items = recentMenu.getItems();
for (MenuItem item : items) {
item.dispose();
}
// 添加新菜单项
for (String filePath : recentFiles) {
File file = new File(filePath);
MenuItem item = new MenuItem(recentMenu, SWT.PUSH);
item.setText(file.getName() + " [" + filePath + "]");
item.setData("filePath", filePath);
item.addListener(SWT.Selection, event -> {
openRecentFile(filePath);
});
}
}
// 打开最近文件
public void openRecentFile(String filePath) {
File file = new File(filePath);
if (!file.exists()) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_WARNING | SWT.OK);
messageBox.setText("警告");
messageBox.setMessage("文件不存在:" + filePath);
messageBox.open();
// 从列表中移除
recentFiles.remove(filePath);
saveRecentFiles();
updateRecentFilesMenu();
return;
}
try {
currentNote = NoteFileManager.load(filePath);
updateUI();
updateWindowTitle();
} catch (IOException e) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
messageBox.setText("错误");
messageBox.setMessage("无法打开文件:" + e.getMessage());
messageBox.open();
}
}
// 保存最近文件列表
public void saveRecentFiles() {
try {
Files.write(Paths.get("recentFiles.txt"), String.join("\n", recentFiles).getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
// 加载最近文件列表
public void loadRecentFiles() {
try {
if (Files.exists(Paths.get("recentFiles.txt"))) {
List<String> lines = Files.readAllLines(Paths.get("recentFiles.txt"));
recentFiles = new ArrayList<>(lines);
updateRecentFilesMenu();
}
} catch (IOException e) {
e.printStackTrace();
}
}
18.5 完整代码
public class NotepadApp {
private Display display;
private Shell shell;
private Text textArea;
private StatusLine statusLine;
private Note currentNote;
private List<String> recentFiles;
private Menu fileMenu;
private Menu editMenu;
private Menu viewMenu;
private Menu helpMenu;
private Menu recentMenu;
private Stack<String> undoStack;
private Stack<String> redoStack;
private int currentFontSize = 14;
private String currentTheme = "light";
public static void main(String[] args) {
new NotepadApp().open();
}
public void open() {
display = new Display();
// 加载最近文件
loadRecentFiles();
// 初始化
undoStack = new Stack<>();
redoStack = new Stack<>();
// 创建主窗口
shell = new Shell(display);
shell.setText("未命名笔记 - 笔记本");
shell.setLayout(new GridLayout(1, false));
// 创建菜单栏
createMenuBar();
// 创建工具栏
createToolBar();
// 创建文本编辑区域
createTextArea();
// 创建状态栏
createStatusBar();
// 监听文本修改
textArea.addModifyListener(event -> {
if (currentNote != null) {
currentNote.setContent(textArea.getText());
currentNote.setModified(true);
updateWindowTitle();
}
updateStatus();
});
// 监听窗口关闭
shell.addListener(SWT.Close, event -> {
// 如果当前笔记已修改,询问是否保存
if (currentNote != null && currentNote.isModified()) {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_QUESTION | SWT.YES | SWT.NO | SWT.CANCEL);
messageBox.setText("保存");
messageBox.setMessage("当前笔记已修改,是否保存?");
int result = messageBox.open();
if (result == SWT.YES) {
saveCurrentNote();
event.doit = true;
} else if (result == SWT.NO) {
event.doit = true;
} else {
event.doit = false; // 取消关闭
}
} else {
event.doit = true;
}
});
// 打开窗口
shell.setBounds(100, 100, 800, 600);
shell.open();
// 事件循环
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
// 释放资源
display.dispose();
}
// 创建菜单栏
private void createMenuBar() {
Menu menuBar = new Menu(shell, SWT.BAR);
shell.setMenuBar(menuBar);
// 文件菜单
fileMenu = new MenuItem(menuBar, SWT.CASCADE);
fileMenu.setText("文件(&F)");
Menu fileMenuContent = new Menu(shell, SWT.DROP_DOWN);
fileMenu.setMenu(fileMenuContent);
// 新建
MenuItem newItem = new MenuItem(fileMenuContent, SWT.PUSH);
newItem.setText("新建(&N)\tCtrl+N");
newItem.setAccelerator(SWT.MOD1 + 'N');
newItem.addListener(SWT.Selection, event -> newNote());
// 打开
MenuItem openItem = new MenuItem(fileMenuContent, SWT.PUSH);
openItem.setText("打开(&O)\tCtrl+O");
openItem.setAccelerator(SWT.MOD1 + 'O');
openItem.addListener(SWT.Selection, event -> openNote());
// 保存
MenuItem saveItem = new MenuItem(fileMenuContent, SWT.PUSH);
saveItem.setText("保存(&S)\tCtrl+S");
saveItem.setAccelerator(SWT.MOD1 + 'S');
saveItem.addListener(SWT.Selection, event -> saveCurrentNote());
// 另存为
MenuItem saveAsItem = new MenuItem(fileMenuContent, SWT.PUSH);
saveAsItem.setText("另存为(&A)\tCtrl+Shift+S");
saveAsItem.setAccelerator(SWT.MOD1 + SWT.MOD2 + 'S');
saveAsItem.addListener(SWT.Selection, event -> saveAsNote());
new MenuItem(fileMenuContent, SWT.SEPARATOR);
// 最近文件
recentMenu = new MenuItem(fileMenuContent, SWT.CASCADE);
recentMenu.setText("最近文件");
Menu recentMenuContent = new Menu(shell, SWT.DROP_DOWN);
recentMenu.setMenu(recentMenuContent);
updateRecentFilesMenu();
new MenuItem(fileMenuContent, SWT.SEPARATOR);
// 退出
MenuItem exitItem = new MenuItem(fileMenuContent, SWT.PUSH);
exitItem.setText("退出(&X)\tCtrl+Q");
exitItem.setAccelerator(SWT.MOD1 + 'Q');
exitItem.addListener(SWT.Selection, event -> {
shell.close();
});
// 编辑菜单
editMenu = new MenuItem(menuBar, SWT.CASCADE);
editMenu.setText("编辑(&E)");
Menu editMenuContent = new Menu(shell, SWT.DROP_DOWN);
editMenu.setMenu(editMenuContent);
// 撤销
MenuItem undoItem = new MenuItem(editMenuContent, SWT.PUSH);
undoItem.setText("撤销(&Z)\tCtrl+Z");
undoItem.setAccelerator(SWT.MOD1 + 'Z');
undoItem.addListener(SWT.Selection, event -> undo());
// 重做
MenuItem redoItem = new MenuItem(editMenuContent, SWT.PUSH);
redoItem.setText("重做(&Y)\tCtrl+Y");
redoItem.setAccelerator(SWT.MOD1 + 'Y');
redoItem.addListener(SWT.Selection, event -> redo());
new MenuItem(editMenuContent, SWT.SEPARATOR);
// 剪切
MenuItem cutItem = new MenuItem(editMenuContent, SWT.PUSH);
cutItem.setText("剪切(&X)\tCtrl+X");
cutItem.setAccelerator(SWT.MOD1 + 'X');
cutItem.addListener(SWT.Selection, event -> cut());
// 复制
MenuItem copyItem = new MenuItem(editMenuContent, SWT.PUSH);
copyItem.setText("复制(&C)\tCtrl+C");
copyItem.setAccelerator(SWT.MOD1 + 'C');
copyItem.addListener(SWT.Selection, event -> copy());
// 粘贴
MenuItem pasteItem = new MenuItem(editMenuContent, SWT.PUSH);
pasteItem.setText("粘贴(&V)\tCtrl+V");
pasteItem.setAccelerator(SWT.MOD1 + 'V');
pasteItem.addListener(SWT.Selection, event -> paste());
new MenuItem(editMenuContent, SWT.SEPARATOR);
// 查找
MenuItem findItem = new MenuItem(editMenuContent, SWT.PUSH);
findItem.setText("查找(&F)\tCtrl+F");
findItem.setAccelerator(SWT.MOD1 + 'F');
findItem.addListener(SWT.Selection, event -> find());
// 替换
MenuItem replaceItem = new MenuItem(editMenuContent, SWT.PUSH);
replaceItem.setText("替换(&H)\tCtrl+H");
replaceItem.setAccelerator(SWT.MOD1 + "H");
replaceItem.addListener(SWT.Selection, event -> {
// TODO: 实现替换功能
});
// 视图菜单
viewMenu = new MenuItem(menuBar, SWT.CASCADE);
viewMenu.setText("视图(&V)");
Menu viewMenuContent = new Menu(shell, SWT.DROP_DOWN);
viewMenu.setMenu(viewMenuContent);
// 放大字体
MenuItem increaseFontItem = new MenuItem(viewMenuContent, SWT.PUSH);
increaseFontItem.setText("放大字体(+)\tCtrl++");
increaseFontItem.setAccelerator(SWT.MOD1 + '+');
increaseFontItem.addListener(SWT.Selection, event -> {
currentFontSize += 2;
updateFontSize();
});
// 缩小字体
MenuItem decreaseFontItem = new MenuItem(viewMenuContent, SWT.PUSH);
decreaseFontItem.setText("缩小字体(-)\tCtrl+-");
decreaseFontItem.setAccelerator(SWT.MOD1 + '-');
decreaseFontItem.addListener(SWT.Selection, event -> {
if (currentFontSize > 8) {
currentFontSize -= 2;
updateFontSize();
}
});
// 正常字体
MenuItem normalFontItem = new MenuItem(viewMenuContent, SWT.PUSH);
normalFontItem.setText("正常字体\tCtrl+0");
normalFontItem.setAccelerator('0');
normalFontItem.addListener(SWT.Selection, event -> {
currentFontSize = 14;
updateFontSize();
});
new MenuItem(viewMenuContent, SWT.SEPARATOR);
// 切换主题
MenuItem toggleThemeItem = new MenuItem(viewMenuContent, SWT.PUSH);
toggleThemeItem.setText("切换主题\tCtrl+T");
toggleThemeItem.setAccelerator(SWT.MOD1 + 'T');
toggleThemeItem.addListener(SWT.Selection, event -> {
toggleTheme();
});
new MenuItem(viewMenuContent, SWT.SEPARATOR);
// 全屏模式
MenuItem fullScreenItem = new MenuItem(viewMenuContent, SWT.PUSH);
fullScreenItem.setText("全屏模式\tF11");
fullScreenItem.setAccelerator(SWT.F11);
fullScreenItem.addListener(SWT.Selection, event -> {
toggleFullScreen();
});
// 帮助菜单
helpMenu = new MenuItem(menuBar, SWT.CASCADE);
helpMenu.setText("帮助(&H)");
Menu helpMenuContent = new Menu(shell, SWT.DROP_DOWN);
helpMenu.setMenu(helpMenuContent);
// 关于
MenuItem aboutItem = new MenuItem(helpMenuContent, SWT.PUSH);
aboutItem.setText("关于(&A)");
aboutItem.addListener(SWT.Selection, event -> {
MessageBox messageBox = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
messageBox.setText("关于");
messageBox.setMessage("笔记本 v1.0\n\n一个简单的文本编辑器,\n使用 SWT 构建。\n\n© 2024");
messageBox.open();
});
}
// 创建工具栏(简化版,省略代码...)
private void createToolBar() {
// 创建工具栏,添加新建、打开、保存、撤销、重做、查找按钮
// 代码省略...
}
// 创建文本编辑区域
private void createTextArea() {
textArea = new Text(shell, SWT.BORDER | SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
GridData textData = new GridData();
textData.horizontalAlignment = GridData.FILL;
textData.verticalAlignment = GridData.FILL;
textData.grabExcessHorizontalSpace = true;
textData.grabExcessVerticalSpace = true;
textArea.setLayoutData(textData);
// 设置字体
Font font = new Font(display, "Courier New", currentFontSize, SWT.NORMAL);
textArea.setFont(font);
font.dispose();
}
// 创建状态栏
private void createStatusBar() {
statusLine = new StatusLine(shell, SWT.BORDER | SWT.HORIZONTAL);
statusLine.setLayoutData(new GridData(SWT.FILL, SWT.END, true, false));
}
// 更新 UI
private void updateUI() {
if (currentNote != null) {
textArea.setText(currentNote.getContent());
} else {
textArea.setText("");
}
undoStack.clear();
redoStack.clear();
updateStatus();
}
// 更新窗口标题
private void updateWindowTitle() {
if (currentNote != null) {
String title = currentNote.getTitle();
if (currentNote.isModified()) {
title += " *";
}
shell.setText(title);
} else {
shell.setText("未命名笔记 - 笔本");
}
}
// 更新状态
private void updateStatus() {
String content = textArea.getText();
int lineCount = textArea.getLineCount();
int charCount = content.length();
int wordCount = content.split("\\s+").length;
String status = String.format("行数:%d, 字数:%d, 词数:%d", lineCount, charCount, wordCount);
if (currentNote != null && currentNote.isModified()) {
status += " | 已修改";
}
statusLine.setMessage(status);
}
// 更新字体
private void updateFontSize() {
Font font = new Font(display, "Courier New", currentFontSize, SWT.NORMAL);
textArea.setFont(font);
font.dispose();
}
// 切换主题
private void toggleTheme() {
if (currentTheme.equals("light")) {
currentTheme = "dark";
textArea.setBackground(display.getSystemColor(SWT.COLOR_BLACK));
textArea.setForeground(display.getSystemColor(SWT.COLOR_WHITE));
} else {
currentTheme = "light";
textArea.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
textArea.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
}
}
// 全屏模式
private void toggleFullScreen() {
shell.setFullScreen(!shell.getFullScreen());
}
// 新建笔记(已实现)
private void newNote() {
// 代码见前文...
}
// 打开笔记(已实现)
private void openNote() {
// 代码见前文...
}
// 保存笔记(已实现)
private void saveCurrentNote() {
// 代码见前文...
}
// 另存为(已实现)
private void saveAsNote() {
// 代码见前文...
}
// 撤销(已实现)
private void undo() {
// 代码见前文...
}
// 重做(已实现)
private void redo() {
// 代码见前文...
}
// 剪切(已实现)
private void cut() {
// 代码见前文...
}
// 复制(已实现)
private void copy() {
// 代码见前文...
}
// 粘贴(已实现)
private void paste() {
// 代码见前文...
}
// 查找(已实现)
private void find() {
// 代码见前文...
}
// 更新剪贴板
private void updateClipboard() {
// 更新撤销/重做按钮状态
}
// 保存最近文件列表
private void saveRecentFiles() {
// 代码见前文...
}
// 加载最近文件列表
private void loadRecentFiles() {
// 代码见前文...
}
// 更新最近文件菜单
private void updateRecentFilesMenu() {
// 代码见前文...
}
// 打开最近文件
private void openRecentFile(String filePath) {
// 代码见前文...
}
}
18.6 测试与优化
功能测试
// 测试新建功能
public void testNewNote() {
newNote();
textArea.setText("这是测试笔记");
assert currentNote != null;
assert currentNote.isModified();
}
// 测试保存功能
public void testSave() {
currentNote.setFilePath("test.txt");
saveCurrentNote();
assert !currentNote.isModified();
}
// 测试查找功能
public void testFind() {
textArea.setText("测试文本查找功能");
findNext("测试");
Point selection = textArea.getSelection();
assert selection.x == 0;
assert selection.y == 4;
}
性能优化
// 优化:避免频繁重绘
textArea.addModifyListener(event -> {
// 不立即重绘
display.asyncExec(() -> {
updateStatus();
});
});
// 优化:延迟加载最近文件
private void loadRecentFiles() {
new Thread(() -> {
try {
if (Files.exists(Paths.get("recentFiles.txt"))) {
List<String> lines = Files.readAllLines(Paths.get("recentFiles.txt"));
recentFiles = new ArrayList<>(lines);
display.asyncExec(() -> {
updateRecentFilesMenu();
});
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
18.7 本章小结
功能总结
| 功能 | 作用 | 类比 |
|---|---|---|
| 新建/打开/保存 | 文件操作 | 新建/打开/保存日记 |
| 撤销/重做 | 编辑历史 | 时间机器 |
| 查找/替换 | 文本搜索 | 查找书页 |
| 最近文件 | 快速访问 | 最近阅读记录 |
费曼测试:你能解释清楚吗?
- 数据模型的作用?
- 定义笔记的结构(标题、内容、文件路径、修改状态)
- 新建笔记的流程?
- 检查当前笔记是否需要保存 → 创建新笔记 → 更新 UI
- 查找功能的原理?
- 在文本中搜索关键词,高亮显示
- 最近文件列表的实现?
- 保存到配置文件,从配置文件加载
下一章预告
现在你已经完成了笔记本应用,
可以创建、编辑、保存笔记,
管理最近文件。
下一章,我们将学习图片浏览器,
支持打开、浏览、缩放图片,
进一步巩固所学知识。
练习题:
- 实现替换功能(将查找的内容替换为指定文本)
- 实现自动保存功能(每 30 秒自动保存)
- 实现字数统计的高级功能(不包含空格、中英文分别统计)
(提示:替换功能使用 String.replace() 或 String.replaceAll(),自动保存使用 display.timerExec(),字数统计使用正则表达式)