跳转至

代码实战项目4:简单的文本编辑器

项目概述

文本编辑器是每个程序员必备的工具。本项目将使用 Python 的 Tkinter 库实现一个功能完整的文本编辑器,包含文件操作、文本编辑、格式设置、查找替换等基本功能。

学习目标

  • 掌握 GUI 编程的基本概念
  • 学会使用 Tkinter 创建窗口和控件
  • 理解事件驱动编程模型
  • 掌握文件读写操作

功能需求

核心功能

  1. 文件操作
  2. 新建文件
  3. 打开文件
  4. 保存文件
  5. 另存为

  6. 编辑功能

  7. 撤销/重做
  8. 剪切/复制/粘贴
  9. 全选
  10. 查找/替换

  11. 格式设置

  12. 设置字体
  13. 设置字号
  14. 设置颜色
  15. 加粗/斜体/下划线

  16. 其他功能

  17. 显示行号
  18. 自动换行
  19. 状态栏显示
  20. 快捷键支持

系统设计

界面布局

┌─────────────────────────────────────────────────────┐
│ 菜单栏 (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. 运行程序

python text_editor.py

3. 使用说明

  1. 文件操作:使用菜单栏的"文件"菜单或工具栏按钮进行文件操作
  2. 编辑文本:直接在文本编辑区输入或编辑文本
  3. 查找替换:使用 Ctrl+F 打开查找对话框,Ctrl+H 打开替换对话框
  4. 格式设置:使用"格式"菜单设置字体、颜色等
  5. 快捷键:支持常用快捷键操作

扩展功能建议

基础扩展

  1. 语法高亮
  2. 支持多种编程语言的语法高亮
  3. 可自定义高亮规则

  4. 多标签页

  5. 支持同时打开多个文件
  6. 标签页切换

  7. 自动保存

  8. 定时自动保存
  9. 防止数据丢失

进阶扩展

  1. 代码折叠
  2. 支持代码块的折叠和展开
  3. 提高代码可读性

  4. 行操作

  5. 复制/删除当前行
  6. 移动行

  7. 编码支持

  8. 支持多种文件编码
  9. 自动检测编码

高级扩展

  1. 插件系统
  2. 支持第三方插件
  3. 扩展编辑器功能

  4. 宏录制

  5. 录制操作宏
  6. 自动重复执行

  7. 集成终端

  8. 内置终端窗口
  9. 直接运行代码

常见问题

Q: 如何修改字体大小?

A: 选择"格式"→"字体"菜单,在弹出的对话框中设置字体和字号。

Q: 支持哪些文件格式?

A: 主要支持纯文本文件(.txt),但也可以打开和保存其他格式的文本文件。

Q: 如何快速定位到某一行?

A: 使用 Ctrl+G 可以跳转到指定行(需要自行实现该功能)。

Q: 支持撤销/重做吗?

A: 支持,可以使用工具栏按钮或 Ctrl+Z/Ctrl+Y 快捷键。

项目总结

本项目实现了一个功能完整的文本编辑器,涵盖了:

  1. GUI 编程:使用 Tkinter 创建图形界面
  2. 文件操作:完整的文件读写功能
  3. 文本编辑:基本的编辑功能
  4. 查找替换:强大的文本搜索和替换
  5. 格式设置:字体、颜色等格式控制
  6. 快捷键:便捷的键盘操作
  7. 状态显示:实时显示编辑状态

通过本项目,你将掌握: - Tkinter GUI 编程 - 事件驱动编程 - 文件 I/O 操作 - 字符串处理 - 用户界面设计

下一步

尝试添加更多功能,如: - 语法高亮 - 多标签页支持 - 自动保存功能 - 代码折叠 - 插件系统 - 更丰富的编辑功能


祝你使用愉快!