代码实战项目4:简单的文本编辑器¶
项目概述¶
文本编辑器是每个程序员必备的工具。本项目将使用 Python 的 Tkinter 库实现一个功能完整的文本编辑器,包含文件操作、文本编辑、格式设置、查找替换等基本功能。
学习目标¶
- 掌握 GUI 编程的基本概念
- 学会使用 Tkinter 创建窗口和控件
- 理解事件驱动编程模型
- 掌握文件读写操作
功能需求¶
核心功能¶
- 文件操作
- 新建文件
- 打开文件
- 保存文件
-
另存为
-
编辑功能
- 撤销/重做
- 剪切/复制/粘贴
- 全选
-
查找/替换
-
格式设置
- 设置字体
- 设置字号
- 设置颜色
-
加粗/斜体/下划线
-
其他功能
- 显示行号
- 自动换行
- 状态栏显示
- 快捷键支持
系统设计¶
界面布局¶
┌─────────────────────────────────────────────────────┐
│ 菜单栏 (File, Edit, Format, Help) │
├──────────────┬──────────────────────────────────────┤
│ 行号栏 │ 文本编辑区 │
│ │ │
│ │ │
├──────────────┴──────────────────────────────────────┤
│ 状态栏 (行:列, 文件名, 编码) │
└─────────────────────────────────────────────────────┘
类结构设计¶
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter.font import Font
class TextEditor:
"""文本编辑器主类"""
def __init__(self, root):
self.root = root
self.current_file = None
self.modified = False
self.create_widgets()
self.create_menu()
self.setup_shortcuts()
代码实现¶
完整代码(text_editor.py)¶
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
from tkinter.font import Font
import os
class TextEditor:
"""文本编辑器类"""
def __init__(self, root):
"""
初始化文本编辑器
参数:
root: Tkinter 根窗口
"""
self.root = root
self.root.title("简易文本编辑器")
self.root.geometry("1000x700")
# 当前文件信息
self.current_file = None
self.modified = False
self.encoding = 'utf-8'
# 创建界面
self.create_widgets()
self.create_menu()
self.setup_shortcuts()
# 更新状态栏
self.update_status()
def create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True)
# 行号栏和文本编辑区
text_frame = tk.Frame(main_frame)
text_frame.pack(fill=tk.BOTH, expand=True)
# 行号栏
self.line_numbers = tk.Text(text_frame, width=4, padx=3, takefocus=0,
border=0, background='#f0f0f0',
state='disabled', wrap='none')
self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)
# 文本编辑区
self.text = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD,
undo=True, maxundo=-1)
self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# 配置标签样式
self.text.tag_config('found', background='yellow')
self.text.tag_config('current_line', background='#e6f3ff')
# 绑定事件
self.text.bind('<KeyRelease>', self.on_key_release)
self.text.bind('<Button-1>', self.on_click)
self.text.bind('<MouseWheel>', self.on_scroll)
# 状态栏
self.status_var = tk.StringVar()
self.status_bar = tk.Label(self.root, textvariable=self.status_var,
bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 工具栏
toolbar = tk.Frame(self.root, bd=1, relief=tk.RAISED)
toolbar.pack(side=tk.TOP, fill=tk.X)
# 工具栏按钮
tk.Button(toolbar, text="新建", command=self.new_file).pack(side=tk.LEFT, padx=2)
tk.Button(toolbar, text="打开", command=self.open_file).pack(side=tk.LEFT, padx=2)
tk.Button(toolbar, text="保存", command=self.save_file).pack(side=tk.LEFT, padx=2)
tk.Frame(toolbar, width=2).pack(side=tk.LEFT, padx=5) # 分隔符
tk.Button(toolbar, text="撤销", command=self.text.edit_undo).pack(side=tk.LEFT, padx=2)
tk.Button(toolbar, text="重做", command=self.text.edit_redo).pack(side=tk.LEFT, padx=2)
tk.Frame(toolbar, width=2).pack(side=tk.LEFT, padx=5) # 分隔符
tk.Button(toolbar, text="查找", command=self.find_dialog).pack(side=tk.LEFT, padx=2)
tk.Button(toolbar, text="替换", command=self.replace_dialog).pack(side=tk.LEFT, padx=2)
def create_menu(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 文件菜单
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="新建", command=self.new_file, accelerator="Ctrl+N")
file_menu.add_command(label="打开", command=self.open_file, accelerator="Ctrl+O")
file_menu.add_command(label="保存", command=self.save_file, accelerator="Ctrl+S")
file_menu.add_command(label="另存为", command=self.save_as_file, accelerator="Ctrl+Shift+S")
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.quit_editor, accelerator="Ctrl+Q")
# 编辑菜单
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="编辑", menu=edit_menu)
edit_menu.add_command(label="撤销", command=self.text.edit_undo, accelerator="Ctrl+Z")
edit_menu.add_command(label="重做", command=self.text.edit_redo, accelerator="Ctrl+Y")
edit_menu.add_separator()
edit_menu.add_command(label="剪切", command=self.text.cut, accelerator="Ctrl+X")
edit_menu.add_command(label="复制", command=self.text.copy, accelerator="Ctrl+C")
edit_menu.add_command(label="粘贴", command=self.text.paste, accelerator="Ctrl+V")
edit_menu.add_separator()
edit_menu.add_command(label="全选", command=self.select_all, accelerator="Ctrl+A")
edit_menu.add_separator()
edit_menu.add_command(label="查找", command=self.find_dialog, accelerator="Ctrl+F")
edit_menu.add_command(label="替换", command=self.replace_dialog, accelerator="Ctrl+H")
# 格式菜单
format_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="格式", menu=format_menu)
format_menu.add_command(label="字体", command=self.font_dialog)
format_menu.add_command(label="颜色", command=self.color_dialog)
format_menu.add_separator()
format_menu.add_checkbutton(label="自动换行", command=self.toggle_wrap)
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="关于", command=self.about_dialog)
def setup_shortcuts(self):
"""设置快捷键"""
self.root.bind('<Control-n>', lambda e: self.new_file())
self.root.bind('<Control-o>', lambda e: self.open_file())
self.root.bind('<Control-s>', lambda e: self.save_file())
self.root.bind('<Control-Shift-S>', lambda e: self.save_as_file())
self.root.bind('<Control-q>', lambda e: self.quit_editor())
self.root.bind('<Control-f>', lambda e: self.find_dialog())
self.root.bind('<Control-h>', lambda e: self.replace_dialog())
self.root.bind('<Control-a>', lambda e: self.select_all())
# 文件操作
def new_file(self):
"""新建文件"""
if self.modified:
if not self.check_save():
return
self.current_file = None
self.text.delete(1.0, tk.END)
self.modified = False
self.root.title("简易文本编辑器 - 未命名")
self.update_line_numbers()
self.update_status()
def open_file(self):
"""打开文件"""
if self.modified:
if not self.check_save():
return
filetypes = [
("文本文件", "*.txt"),
("所有文件", "*.*")
]
filename = filedialog.askopenfilename(
title="打开文件",
filetypes=filetypes
)
if filename:
try:
with open(filename, 'r', encoding=self.encoding) as f:
content = f.read()
self.text.delete(1.0, tk.END)
self.text.insert(1.0, content)
self.current_file = filename
self.modified = False
self.root.title(f"简易文本编辑器 - {os.path.basename(filename)}")
self.update_line_numbers()
self.update_status()
except Exception as e:
messagebox.showerror("错误", f"无法打开文件: {e}")
def save_file(self):
"""保存文件"""
if self.current_file:
self._save_to_file(self.current_file)
else:
self.save_as_file()
def save_as_file(self):
"""另存为"""
filetypes = [
("文本文件", "*.txt"),
("所有文件", "*.*")
]
filename = filedialog.asksaveasfilename(
title="另存为",
filetypes=filetypes,
defaultextension=".txt"
)
if filename:
self._save_to_file(filename)
def _save_to_file(self, filename):
"""保存到文件"""
try:
with open(filename, 'w', encoding=self.encoding) as f:
f.write(self.text.get(1.0, tk.END))
self.current_file = filename
self.modified = False
self.root.title(f"简易文本编辑器 - {os.path.basename(filename)}")
self.update_status()
except Exception as e:
messagebox.showerror("错误", f"无法保存文件: {e}")
def check_save(self):
"""检查是否需要保存"""
if not self.modified:
return True
answer = messagebox.askyesnocancel(
"保存文件",
"文件已修改,是否保存?"
)
if answer is True: # 是
self.save_file()
return True
elif answer is False: # 否
return True
else: # 取消
return False
def quit_editor(self):
"""退出编辑器"""
if self.check_save():
self.root.quit()
# 编辑功能
def select_all(self):
"""全选"""
self.text.tag_add(tk.SEL, "1.0", tk.END)
self.text.mark_set(tk.INSERT, "1.0")
self.text.focus_set()
def find_dialog(self):
"""查找对话框"""
dialog = tk.Toplevel(self.root)
dialog.title("查找")
dialog.geometry("400x150")
tk.Label(dialog, text="查找内容:").pack(pady=5)
find_entry = tk.Entry(dialog, width=40)
find_entry.pack(pady=5)
def find():
keyword = find_entry.get()
if keyword:
self.find_text(keyword)
tk.Button(dialog, text="查找", command=find).pack(pady=5)
# 绑定回车键
find_entry.bind('<Return>', lambda e: find())
# 设置焦点
find_entry.focus_set()
def replace_dialog(self):
"""替换对话框"""
dialog = tk.Toplevel(self.root)
dialog.title("替换")
dialog.geometry("400x200")
tk.Label(dialog, text="查找内容:").pack(pady=5)
find_entry = tk.Entry(dialog, width=40)
find_entry.pack(pady=5)
tk.Label(dialog, text="替换为:").pack(pady=5)
replace_entry = tk.Entry(dialog, width=40)
replace_entry.pack(pady=5)
button_frame = tk.Frame(dialog)
button_frame.pack(pady=10)
def replace():
find_text = find_entry.get()
replace_text = replace_entry.get()
if find_text:
self.replace_text(find_text, replace_text)
def replace_all():
find_text = find_entry.get()
replace_text = replace_entry.get()
if find_text:
self.replace_all_text(find_text, replace_text)
tk.Button(button_frame, text="替换", command=replace).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="全部替换", command=replace_all).pack(side=tk.LEFT, padx=5)
# 设置焦点
find_entry.focus_set()
def find_text(self, keyword):
"""查找文本"""
self.text.tag_remove('found', '1.0', tk.END)
if not keyword:
return
idx = '1.0'
count = 0
while True:
idx = self.text.search(keyword, idx, nocase=1, stopindex=tk.END)
if not idx:
break
end_idx = f"{idx}+{len(keyword)}c"
self.text.tag_add('found', idx, end_idx)
self.text.see(idx)
count += 1
idx = end_idx
if count == 0:
messagebox.showinfo("查找", f"未找到 '{keyword}'")
else:
messagebox.showinfo("查找", f"找到 {count} 处 '{keyword}'")
def replace_text(self, find_text, replace_text):
"""替换文本"""
# 获取当前选中的文本
try:
selection = self.text.get(tk.SEL_FIRST, tk.SEL_LAST)
if selection.lower() == find_text.lower():
self.text.delete(tk.SEL_FIRST, tk.SEL_LAST)
self.text.insert(tk.INSERT, replace_text)
self.find_text(find_text)
else:
self.find_text(find_text)
except:
self.find_text(find_text)
def replace_all_text(self, find_text, replace_text):
"""全部替换"""
content = self.text.get(1.0, tk.END)
count = content.lower().count(find_text.lower())
if count == 0:
messagebox.showinfo("替换", f"未找到 '{find_text}'")
return
new_content = content.replace(find_text, replace_text)
self.text.delete(1.0, tk.END)
self.text.insert(1.0, new_content)
messagebox.showinfo("替换", f"已替换 {count} 处 '{find_text}'")
self.set_modified()
# 格式功能
def font_dialog(self):
"""字体对话框"""
dialog = tk.Toplevel(self.root)
dialog.title("字体设置")
dialog.geometry("300x200")
# 字体选择
tk.Label(dialog, text="字体:").grid(row=0, column=0, padx=5, pady=5)
font_var = tk.StringVar(value="Arial")
font_combo = tk.Entry(dialog, textvariable=font_var)
font_combo.grid(row=0, column=1, padx=5, pady=5)
# 字号选择
tk.Label(dialog, text="字号:").grid(row=1, column=0, padx=5, pady=5)
size_var = tk.StringVar(value="12")
size_entry = tk.Entry(dialog, textvariable=size_var)
size_entry.grid(row=1, column=1, padx=5, pady=5)
def apply_font():
try:
font_name = font_var.get()
font_size = int(size_var.get())
self.text.config(font=(font_name, font_size))
self.line_numbers.config(font=(font_name, font_size))
dialog.destroy()
except ValueError:
messagebox.showerror("错误", "请输入有效的字号")
tk.Button(dialog, text="应用", command=apply_font).grid(row=2, column=0, columnspan=2, pady=10)
def color_dialog(self):
"""颜色对话框"""
from tkinter import colorchooser
color = colorchooser.askcolor(title="选择颜色")[1]
if color:
self.text.config(fg=color)
def toggle_wrap(self):
"""切换自动换行"""
current_wrap = self.text.cget('wrap')
new_wrap = tk.NONE if current_wrap == tk.WORD else tk.WORD
self.text.config(wrap=new_wrap)
# 事件处理
def on_key_release(self, event):
"""键盘释放事件"""
self.set_modified()
self.update_line_numbers()
self.update_status()
def on_click(self, event):
"""鼠标点击事件"""
self.update_status()
def on_scroll(self, event):
"""滚动事件"""
self.update_line_numbers()
def set_modified(self):
"""设置修改标记"""
if not self.modified:
self.modified = True
if self.current_file:
self.root.title(f"简易文本编辑器 - {os.path.basename(self.current_file)} *")
def update_line_numbers(self):
"""更新行号"""
# 删除所有行号
self.line_numbers.config(state='normal')
self.line_numbers.delete(1.0, tk.END)
# 添加新的行号
line_count = int(self.text.index('end-1c').split('.')[0])
for i in range(1, line_count + 1):
self.line_numbers.insert(tk.END, f"{i}\n")
self.line_numbers.config(state='disabled')
# 同步滚动
self.line_numbers.yview_moveto(self.text.yview()[0])
def update_status(self):
"""更新状态栏"""
# 获取当前光标位置
cursor_pos = self.text.index(tk.INSERT)
line, col = cursor_pos.split('.')
# 更新状态栏
if self.current_file:
filename = os.path.basename(self.current_file)
else:
filename = "未命名"
status_text = f"行: {line} 列: {col} 文件: {filename} 编码: {self.encoding}"
if self.modified:
status_text += " (已修改)"
self.status_var.set(status_text)
def about_dialog(self):
"""关于对话框"""
messagebox.showinfo(
"关于",
"简易文本编辑器\n\n"
"版本: 1.0\n"
"作者: 菩提网络\n\n"
"一个简单的文本编辑器,支持基本的\n"
"文件操作、编辑和格式设置功能。"
)
def main():
"""主函数"""
root = tk.Tk()
editor = TextEditor(root)
root.mainloop()
if __name__ == '__main__':
main()
运行指南¶
1. 安装依赖¶
本项目使用 Python 标准库,无需安装额外依赖。
2. 运行程序¶
3. 使用说明¶
- 文件操作:使用菜单栏的"文件"菜单或工具栏按钮进行文件操作
- 编辑文本:直接在文本编辑区输入或编辑文本
- 查找替换:使用 Ctrl+F 打开查找对话框,Ctrl+H 打开替换对话框
- 格式设置:使用"格式"菜单设置字体、颜色等
- 快捷键:支持常用快捷键操作
扩展功能建议¶
基础扩展¶
- 语法高亮
- 支持多种编程语言的语法高亮
-
可自定义高亮规则
-
多标签页
- 支持同时打开多个文件
-
标签页切换
-
自动保存
- 定时自动保存
- 防止数据丢失
进阶扩展¶
- 代码折叠
- 支持代码块的折叠和展开
-
提高代码可读性
-
行操作
- 复制/删除当前行
-
移动行
-
编码支持
- 支持多种文件编码
- 自动检测编码
高级扩展¶
- 插件系统
- 支持第三方插件
-
扩展编辑器功能
-
宏录制
- 录制操作宏
-
自动重复执行
-
集成终端
- 内置终端窗口
- 直接运行代码
常见问题¶
Q: 如何修改字体大小?¶
A: 选择"格式"→"字体"菜单,在弹出的对话框中设置字体和字号。
Q: 支持哪些文件格式?¶
A: 主要支持纯文本文件(.txt),但也可以打开和保存其他格式的文本文件。
Q: 如何快速定位到某一行?¶
A: 使用 Ctrl+G 可以跳转到指定行(需要自行实现该功能)。
Q: 支持撤销/重做吗?¶
A: 支持,可以使用工具栏按钮或 Ctrl+Z/Ctrl+Y 快捷键。
项目总结¶
本项目实现了一个功能完整的文本编辑器,涵盖了:
- GUI 编程:使用 Tkinter 创建图形界面
- 文件操作:完整的文件读写功能
- 文本编辑:基本的编辑功能
- 查找替换:强大的文本搜索和替换
- 格式设置:字体、颜色等格式控制
- 快捷键:便捷的键盘操作
- 状态显示:实时显示编辑状态
通过本项目,你将掌握: - Tkinter GUI 编程 - 事件驱动编程 - 文件 I/O 操作 - 字符串处理 - 用户界面设计
下一步¶
尝试添加更多功能,如: - 语法高亮 - 多标签页支持 - 自动保存功能 - 代码折叠 - 插件系统 - 更丰富的编辑功能
祝你使用愉快!