程式碼檢視器 – A-Notepad-v1.py

← 返回清單
# notepad.py
# Python 3.x + Tkinter 記事本(讀寫 .txt)
import os
import tkinter as tk
from tkinter import filedialog, messagebox

APP_NAME = "記事本"

class SimpleNotepad:
    def __init__(self, root):
        self.root = root
        self.current_file = None
        self.dirty = False

        # 視窗基本設定
        self.root.title(f"未命名 - {APP_NAME}")
        self.root.geometry("800x600")

        # 文字編輯區 + 捲軸
        self.text = tk.Text(root, undo=True, wrap="word")
        self.text.pack(fill="both", expand=True, side="left")
        yscroll = tk.Scrollbar(root, command=self.text.yview)
        yscroll.pack(fill="y", side="right")
        self.text.configure(yscrollcommand=yscroll.set)

        # 監聽編輯變動
        self.text.bind("<<Modified>>", self._on_text_modified)

        # 功能表
        menubar = tk.Menu(root)

        filemenu = tk.Menu(menubar, tearoff=0)
        filemenu.add_command(label="新增 (Ctrl+N)", command=self.new_file)
        filemenu.add_command(label="開啟… (Ctrl+O)", command=self.open_file)
        filemenu.add_separator()
        filemenu.add_command(label="儲存 (Ctrl+S)", command=self.save_file)
        filemenu.add_command(label="另存新檔… (Ctrl+Shift+S)", command=self.save_as)
        filemenu.add_separator()
        filemenu.add_command(label="離開 (Ctrl+Q)", command=self.on_exit)
        menubar.add_cascade(label="檔案", menu=filemenu)

        editmenu = tk.Menu(menubar, tearoff=0)
        editmenu.add_command(label="復原 (Ctrl+Z)", command=lambda: self.text.event_generate("<<Undo>>"))
        editmenu.add_command(label="取消復原 (Ctrl+Y)", command=lambda: self.text.event_generate("<<Redo>>"))
        editmenu.add_separator()
        editmenu.add_command(label="剪下 (Ctrl+X)", command=lambda: self.text.event_generate("<<Cut>>"))
        editmenu.add_command(label="複製 (Ctrl+C)", command=lambda: self.text.event_generate("<<Copy>>"))
        editmenu.add_command(label="貼上 (Ctrl+V)", command=lambda: self.text.event_generate("<<Paste>>"))
        editmenu.add_separator()
        editmenu.add_command(label="全選 (Ctrl+A)", command=self.select_all)
        menubar.add_cascade(label="編輯", menu=editmenu)

        viewmenu = tk.Menu(menubar, tearoff=0)
        self.word_wrap = tk.BooleanVar(value=True)
        viewmenu.add_checkbutton(label="自動換行", onvalue=True, offvalue=False,
                                 variable=self.word_wrap, command=self.toggle_wrap)
        menubar.add_cascade(label="檢視", menu=viewmenu)

        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(
            label="關於",
            command=lambda: messagebox.showinfo("關於", "軟體開發 Kelvin Huang")
        )
        menubar.add_cascade(label="說明", menu=helpmenu)

        root.config(menu=menubar)

        # 快捷鍵
        root.bind("<Control-n>", lambda e: self.new_file())
        root.bind("<Control-o>", lambda e: self.open_file())
        root.bind("<Control-s>", lambda e: self.save_file())
        root.bind("<Control-S>", lambda e: self.save_as())  # Shift+Ctrl+S
        root.bind("<Control-q>", lambda e: self.on_exit())
        root.bind("<Control-a>", lambda e: self.select_all())

        # 關閉視窗攔截
        root.protocol("WM_DELETE_WINDOW", self.on_exit)

    # ===== 狀態/標題 =====
    def update_title(self):
        name = os.path.basename(self.current_file) if self.current_file else "未命名"
        prefix = "* " if self.dirty else ""
        self.root.title(f"{prefix}{name} - {APP_NAME}")

    def _on_text_modified(self, _event=None):
        # Tk 的 edit_modified flag 在觸發後需要手動清除
        self.text.edit_modified(False)
        self.dirty = True
        self.update_title()

    # ===== 變更確認 =====
    def _maybe_save_changes(self) -> bool:
        if self._text_changed_since_save():
            ans = messagebox.askyesnocancel("提示", "檔案尚未儲存,是否要儲存?")
            if ans is None:   # 取消
                return False
            if ans:           # 是 → 儲存
                return self.save_file()
        return True

    def _text_changed_since_save(self) -> bool:
        if not self.dirty:
            return False
        # 進一步比對內容(保險)
        current = self.text.get("1.0", "end-1c")
        if self.current_file and os.path.exists(self.current_file):
            try:
                with open(self.current_file, "r", encoding="utf-8", errors="replace") as f:
                    return current != f.read()
            except Exception:
                return True
        else:
            return current != ""

    # ===== 檔案動作 =====
    def new_file(self):
        if not self._maybe_save_changes():
            return
        self.text.delete("1.0", "end")
        self.current_file = None
        self.dirty = False
        self.update_title()

    def open_file(self):
        if not self._maybe_save_changes():
            return
        path = filedialog.askopenfilename(
            title="開啟檔案",
            filetypes=[("文字檔案", "*.txt"), ("所有檔案", "*.*")]
        )
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8", errors="replace") as f:
                content = f.read()
            self.text.delete("1.0", "end")
            self.text.insert("1.0", content)
            self.current_file = path
            self.dirty = False
            self.text.edit_modified(False)
            self.update_title()
        except Exception as e:
            messagebox.showerror("錯誤", f"無法開啟檔案:\n{e}")

    def save_file(self):
        if not self.current_file:
            return self.save_as()
        return self._write_to_path(self.current_file)

    def save_as(self):
        path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("文字檔案", "*.txt"), ("所有檔案", "*.*")],
            title="另存新檔"
        )
        if not path:
            return False
        self.current_file = path
        return self._write_to_path(path)

    def _write_to_path(self, path) -> bool:
        try:
            text = self.text.get("1.0", "end-1c")
            with open(path, "w", encoding="utf-8") as f:
                f.write(text)
            self.dirty = False
            self.text.edit_modified(False)
            self.update_title()
            return True
        except Exception as e:
            messagebox.showerror("錯誤", f"無法儲存檔案:\n{e}")
            return False

    # ===== 其他動作 =====
    def select_all(self):
        self.text.tag_add("sel", "1.0", "end-1c")
        return "break"

    def toggle_wrap(self):
        self.text.configure(wrap="word" if self.word_wrap.get() else "none")

    def on_exit(self):
        if not self._maybe_save_changes():
            return
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    app = SimpleNotepad(root)
    root.mainloop()