杰瑞科技汇

如何用Python开发一款Python编辑器?

我们将使用Python最流行的GUI库之一——Tkinter来构建界面,并集成tkinter.scrolledtext作为代码编辑区域,为了实现语法高亮,我们将使用一个名为pygments的第三方库。

如何用Python开发一款Python编辑器?-图1
(图片来源网络,侵删)

这个编辑器将具备以下核心功能:

  1. 代码编辑区:带行号和滚动条。
  2. 语法高亮:对Python代码进行颜色标记。
  3. 文件操作:新建、打开、保存文件。
  4. 运行代码:在编辑器内执行Python脚本并显示输出。
  5. 简单的错误提示:捕获并显示运行时错误。

第一步:安装必要的库

Tkinter是Python的标准库,通常已经预装,但我们需要安装pygments库来处理语法高亮。

打开你的终端或命令行,运行以下命令:

pip install pygments

第二步:项目结构

我们将所有代码写在一个名为 python_editor.py 的文件中。

如何用Python开发一款Python编辑器?-图2
(图片来源网络,侵删)
# python_editor.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import subprocess
import sys
import os
from pygments import lex
from pygments.lexers import PythonLexer
from pygments.styles import get_style_by_name
from pygments.token import Token

第三步:创建主应用程序类

我们将使用面向对象的方式,创建一个PythonEditorApp类来管理整个应用程序。

class PythonEditorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Python 编辑器")
        self.root.geometry("800x600")
        # --- 核心组件 ---
        self.text_area = None
        self.line_numbers = None
        self.output_area = None
        # --- 文件路径 ---
        self.file_path = None
        # --- 配置语法高亮样式 ---
        self.lexer = PythonLexer()
        self.style = get_style_by_name('monokai') # 你可以换成 'vim', 'github', 'igor' 等
        # --- 创建UI ---
        self.create_widgets()
        # --- 绑定事件 ---
        self.bind_events()
    def create_widgets(self):
        # 1. 创建菜单栏
        self.create_menu()
        # 2. 创建主框架 (用于放置编辑区和输出区)
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        # 3. 创建上半部分:编辑区
        editor_frame = ttk.Frame(main_frame)
        editor_frame.pack(fill=tk.BOTH, expand=True)
        # 3.1 行号区域
        self.line_numbers = tk.Text(editor_frame, width=4, padx=3, takefocus=0, border=0, 
                                   background='#2b2b2b', foreground='#999999', font=('Consolas', 10))
        self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)
        # 3.2 代码编辑区域
        self.text_area = scrolledtext.ScrolledText(editor_frame, wrap=tk.NONE, font=('Consolas', 10))
        self.text_area.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        # 初始化行号
        self.update_line_numbers()
        # 4. 创建下半部分:输出区
        output_frame = ttk.LabelFrame(main_frame, text="输出")
        output_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))
        self.output_area = scrolledtext.ScrolledText(output_frame, height=10, font=('Consolas', 10))
        self.output_area.pack(fill=tk.BOTH, expand=True)
    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)
        file_menu.add_command(label="打开...", command=self.open_file)
        file_menu.add_command(label="保存", command=self.save_file)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.root.quit)
        # 运行菜单
        run_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="运行", menu=run_menu)
        run_menu.add_command(label="运行脚本", command=self.run_script)
    def bind_events(self):
        # 绑定文本修改事件,用于更新行号和语法高亮
        self.text_area.bind('<KeyRelease>', self.on_text_change)
        self.text_area.bind('<Button-1>', self.on_text_change)
        self.text_area.bind('<MouseWheel>', self.on_text_change) # Windows
        self.text_area.bind('<Button-4>', self.on_text_change)  # Linux, scroll up
        self.text_area.bind('<Button-5>', self.on_text_change)  # Linux, scroll down
    def on_text_change(self, event=None):
        """当文本内容改变时,更新行号和语法高亮"""
        self.update_line_numbers()
        self.highlight_syntax()
    def update_line_numbers(self):
        """更新行号"""
        final_line = self.text_area.index('end-1c').split('.')[0]
        line_numbers_content = '\n'.join(str(i) for i in range(1, int(final_line) + 1))
        self.line_numbers.config(state='normal')
        self.line_numbers.delete('1.0', tk.END)
        self.line_numbers.insert('1.0', line_numbers_content)
        self.line_numbers.config(state='disabled')
        # 同步滚动条
        self.line_numbers.yview_moveto(self.text_area.yview()[0])
        self.line_numbers.config(yscrollcommand=self.text_area.yview)
        self.text_area.config(yscrollcommand=self.line_numbers.yview)
    def highlight_syntax(self):
        """使用pygments进行语法高亮"""
        content = self.text_area.get('1.0', tk.END)
        self.text_area.tag_remove("Token", '1.0', tk.END)
        for token, value in lex(content, self.lexer):
            # 为每种token类型设置前景色
            fg_color = self.style.styles_for_token(token).get('color', None)
            if fg_color:
                # Tkinter的颜色格式是 #RRGGBB
                tag_name = f"fg_{fg_color}"
                self.text_area.tag_config(tag_name, foreground=f'#{fg_color}')
                self.text_area.insert(tk.END, value, tag_name)
            else:
                self.text_area.insert(tk.END, value)
        # 重新定位光标,防止闪烁
        cursor_pos = self.text_area.index("insert")
        self.text_area.mark_set("insert", cursor_pos)
        self.text_area.see("insert")
    # --- 文件操作方法 ---
    def new_file(self):
        """新建文件"""
        self.root.title("未命名 - Python 编辑器")
        self.text_area.delete('1.0', tk.END)
        self.file_path = None
        self.update_line_numbers()
    def open_file(self):
        """打开文件"""
        file_path = filedialog.askopenfilename(
            filetypes=[("Python Files", "*.py"), ("All Files", "*.*")]
        )
        if file_path:
            self.file_path = file_path
            self.root.title(f"{os.path.basename(file_path)} - Python 编辑器")
            with open(file_path, 'r', encoding='utf-8') as f:
                self.text_area.delete('1.0', tk.END)
                self.text_area.insert('1.0', f.read())
            self.update_line_numbers()
    def save_file(self):
        """保存文件"""
        if not self.file_path:
            self.save_as()
        else:
            with open(self.file_path, 'w', encoding='utf-8') as f:
                f.write(self.text_area.get('1.0', tk.END))
            messagebox.showinfo("保存成功", "文件已保存。")
    def save_as(self):
        """另存为"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".py",
            filetypes=[("Python Files", "*.py"), ("All Files", "*.*")]
        )
        if file_path:
            self.file_path = file_path
            self.root.title(f"{os.path.basename(file_path)} - Python 编辑器")
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(self.text_area.get('1.0', tk.END))
            messagebox.showinfo("保存成功", "文件已保存。")
    # --- 运行代码方法 ---
    def run_script(self):
        """运行脚本并显示输出"""
        # 清空之前的输出
        self.output_area.delete('1.0', tk.END)
        self.output_area.insert(tk.END, "正在运行...\n")
        # 如果文件未保存,先保存
        if not self.file_path:
            self.save_as()
            if not self.file_path: # 用户取消了保存
                return
        # 获取代码内容
        code = self.text_area.get('1.0', tk.END)
        try:
            # 使用subprocess来运行脚本,这样可以捕获stdout和stderr
            # 使用临时文件来保存代码,避免在命令行中传递复杂代码可能带来的问题
            with open('temp_script.py', 'w', encoding='utf-8') as f:
                f.write(code)
            result = subprocess.run(
                [sys.executable, 'temp_script.py'],
                capture_output=True,
                text=True,
                check=True # 如果返回码不为0,则抛出CalledProcessError
            )
            # 打印标准输出
            self.output_area.insert(tk.END, result.stdout)
        except subprocess.CalledProcessError as e:
            # 打印标准错误
            self.output_area.insert(tk.END, "运行出错:\n", "error")
            self.output_area.insert(tk.END, e.stderr, "error")
        except Exception as e:
            # 捕获其他可能的异常
            self.output_area.insert(tk.END, f"发生未知错误: {e}\n", "error")
        finally:
            # 删除临时文件
            if os.path.exists('temp_script.py'):
                os.remove('temp_script.py')
if __name__ == "__main__":
    root = tk.Tk()
    app = PythonEditorApp(root)
    root.mainloop()

第四步:代码解释

  1. __init__(self, root):

    • 初始化主窗口,设置标题和大小。
    • 定义核心组件(text_area, line_numbers, output_area)和文件路径变量。
    • 配置pygments的词法分析器(PythonLexer)和样式(monokai)。
    • 调用create_widgetsbind_events来构建界面和绑定事件。
  2. create_widgets(self):

    • 菜单栏 (create_menu): 创建了“文件”和“运行”两个菜单,并关联了相应的命令函数。
    • 主框架 (main_frame): 使用ttk.Frame作为容器,方便布局。
    • 编辑区 (editor_frame):
      • line_numbers: 一个只读的Text组件,用于显示行号,背景色设置为深色,以配合代码编辑区的主题。
      • text_area: scrolledtext.ScrolledText,这是核心的代码输入区,支持滚动。
    • 输出区 (output_frame): 另一个scrolledtext.ScrolledText,用于显示脚本的运行结果或错误信息。
  3. bind_events(self):

    如何用Python开发一款Python编辑器?-图3
    (图片来源网络,侵删)
    • 绑定了text_area<KeyRelease>(按键释放)、<Button-1>(鼠标点击)和滚动事件到on_text_change方法,这意味着每当用户输入、点击或滚动时,都会触发这个方法。
  4. on_text_change(self, event=None):

    • 这是实现动态效果的关键,它被触发时,会调用update_line_numbershighlight_syntax,确保行号和语法高亮始终与代码内容同步。
  5. update_line_numbers(self):

    • 获取text_area的总行数。
    • 生成从1到总行数的数字字符串。
    • 更新line_numbers,并同步两个组件的滚动条。
  6. highlight_syntax(self):

    • 这是实现语法高亮的核心。
    • text_area获取所有代码。
    • 使用pygments.lex()函数将代码分解成一个个的tokenToken.Keyword, Token.String, Token.Comment等)。
    • 根据pygments的样式(monokai),为每种token设置一个tag(标签),并指定其前景色。
    • 将代码和对应的tag重新插入到text_area中。tkinter会根据tag来应用颜色。
  7. 文件操作方法 (new_file, open_file, save_file, save_as):

    • 这些方法使用tkinter.filedialog来与用户交互,实现文件的创建、打开和保存逻辑,它们会更新窗口标题和self.file_path变量。
  8. run_script(self):

    • 这是运行代码的功能。
    • 它会检查文件是否已保存,如果没有则调用save_as
    • 它将text_area中的代码写入一个临时文件temp_script.py,这样做是为了避免在命令行中直接执行可能包含特殊字符的代码,更加安全。
    • 使用subprocess.run()来调用系统的Python解释器(sys.executable)执行这个临时文件。
    • capture_output=Truetext=True参数用于捕获标准输出和标准错误,并以文本形式返回。
    • check=True参数会在脚本执行失败(返回非零退出码)时抛出CalledProcessError异常。
    • 将捕获到的stdoutstderr输出到output_area中。
    • 删除临时文件。

第五步:运行和改进

  1. 运行: 将以上代码保存为python_editor.py,然后在终端中运行:

    python python_editor.py

    你应该能看到一个简单的Python编辑器界面。

  2. 可以进一步改进的方向:

    • 自动补全: 这是编辑器的一大难点,可以使用jediparso库来分析代码,并提供补全建议。
    • 错误检查: 在用户输入时(或通过一个“检查”按钮),使用pyflakesflake8等静态分析工具来检查代码中的语法错误和潜在问题,并用波浪线标出。
    • 主题切换: 允许用户在不同的pygments主题之间切换。
    • 更多编辑功能: 添加查找/替换、转到行号、代码折叠等功能。
    • 更好的错误提示: 在编辑器内直接用红色下划线标记出错误的位置,而不是只在输出区显示。

这个项目为你提供了一个坚实的基础,你可以根据自己的兴趣和需求,不断地为其添加新功能,打造一个属于你自己的强大编辑器。

分享:
扫描分享到社交APP
上一篇
下一篇