第18章:项目实战 - 笔记本应用

第18章:项目实战 - 笔记本应用


作者:步子哥 (steper@foxmail.com)


18.1 需求分析

功能列表

笔记本应用 = 电子笔记本
- 就像你在纸上写笔记
- 但可以在电脑上写、改、保存

类比:
笔记本应用 = 便利贴
- 你可以在便利贴上写字
- 也可以擦掉重写
- 可以贴在任何地方

核心功能:

  1. 文件操作

- 新建笔记 - 打开笔记 - 保存笔记 - 另存为 - 最近文件列表

  1. 编辑功能

- 撤销/重做 - 查找与替换 - 剪切/复制/粘贴

  1. 视图功能

- 缩放字体 - 切换主题 - 全屏模式

  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 本章小结

功能总结

功能作用类比
新建/打开/保存文件操作新建/打开/保存日记
撤销/重做编辑历史时间机器
查找/替换文本搜索查找书页
最近文件快速访问最近阅读记录

费曼测试:你能解释清楚吗?

  1. 数据模型的作用?

- 定义笔记的结构(标题、内容、文件路径、修改状态)

  1. 新建笔记的流程?

- 检查当前笔记是否需要保存 → 创建新笔记 → 更新 UI

  1. 查找功能的原理?

- 在文本中搜索关键词,高亮显示

  1. 最近文件列表的实现?

- 保存到配置文件,从配置文件加载

下一章预告

现在你已经完成了笔记本应用,
可以创建、编辑、保存笔记,
管理最近文件。

下一章,我们将学习图片浏览器,
支持打开、浏览、缩放图片,
进一步巩固所学知识。

练习题:

  1. 实现替换功能(将查找的内容替换为指定文本)
  2. 实现自动保存功能(每 30 秒自动保存)
  3. 实现字数统计的高级功能(不包含空格、中英文分别统计)

(提示:替换功能使用 String.replace()String.replaceAll(),自动保存使用 display.timerExec(),字数统计使用正则表达式)

← 返回目录