# 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()