Tkinter开发代码编辑器

闲着无聊自己写了个简单的代码编辑器, 支持语法高亮, 符号补全, 查找, 打印等功能。有部分功能还不完善。

有点长。可以到Github下载XIAOQINGeq/PyScripter: A very simple python code editor made with python tkinter (github.com)

顺便点颗星, 谢谢。

打印和加载配置功能使用了pywin32和commentjson库, 需要另外安装。

请确保你的电脑上安装了IDLE

## main.py

import tkinter as tk
from idlelib.colorizer import color_config, ColorDelegator
from idlelib.percolator import Percolator
from tkinter import *
from tkinter import messagebox, filedialog
from tkinter.scrolledtext import ScrolledText

import commentjson as json
import win32print
import win32ui

from dialog import FindDialog, ReplaceDialog


class Editor:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('PyScripter - Untitled')
        self.root.geometry('900x600')
        self.root.protocol('WM_DELETE_WINDOW', self.exit_app)

        # 储存数据
        self.data = {
            'file_path': 'untitled'
        }

        # 加载配置文件
        self.config = self.load_config()

        # 文本编辑区
        self.text_area = ScrolledText(self.root, font=('Consolas', 12), undo=True, autoseparators=True, maxundo=-1)
        self.text_area.pack(fill=BOTH, expand=True)
        # Tab键前进4格
        self.text_area.bind("<Tab>", self.insert_tab_spaces)
        # 添加键盘输入事件处理函数
        self.text_area.bind('<Key>', self.on_key_press)

        # 应用语法高亮
        if self.config['python_syntax_highlight']:
            color_config(self.text_area)
            p = Percolator(self.text_area)
            d = ColorDelegator()
            p.insertfilter(d)

        # 状态栏
        self.status_bar = Label(self.root, text='Ln: 1 | Col: 0', bd=1, relief=SUNKEN, anchor=W)
        self.status_bar.pack(side=BOTTOM, fill=X)
        # 行数列数显示
        self.text_area.bind('<KeyRelease>', self.update_line_col)
        self.text_area.bind('<ButtonRelease-1>', self.update_line_col)

        # 菜单栏
        self.menubar = Menu(self.root)
        self.root.config(menu=self.menubar)

        # 文件菜单
        file_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='File', menu=file_menu)
        file_menu.add_command(label='New File', accelerator='Ctrl+N', command=self.new_file)
        file_menu.add_command(label='Open File', accelerator='Ctrl+O', command=self.open_file)
        file_menu.add_command(label='Save', accelerator='Ctrl+S', command=self.save_file)
        file_menu.add_command(label='Save As...', accelerator='Ctrl+Shift+S', command=self.save_as_file)
        file_menu.add_separator()
        file_menu.add_command(label='Print', accelerator='Ctrl+P', command=self.print_text)
        file_menu.add_command(label='File Statistics', command=self.file_statistics)
        file_menu.add_separator()
        file_menu.add_command(label='Exit', command=self.exit_app)

        # 编辑菜单
        edit_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='Edit', menu=edit_menu)
        edit_menu.add_command(label='Undo', accelerator='Ctrl+Z', command=self.undo)
        edit_menu.add_command(label='Redo', accelerator='Ctrl+Y', command=self.redo)
        edit_menu.add_separator()
        edit_menu.add_command(label='Cut', accelerator='Ctrl+X', command=self.cut)
        edit_menu.add_command(label='Copy', accelerator='Ctrl+C', command=self.copy)
        edit_menu.add_command(label='Paste', accelerator='Ctrl+V', command=self.paste)
        edit_menu.add_separator()
        edit_menu.add_command(label='Find All', accelerator='Ctrl+F', command=self.find_all)
        edit_menu.add_command(label='Cancel Find', accelerator='Esc', command=self.cancel_find)
        edit_menu.add_command(label='Replace...', accelerator='Ctrl+H', command=self.replace)

        # 选择菜单
        select_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='Selection', menu=select_menu)
        select_menu.add_command(label='Select All', accelerator='Ctrl+A', command=self.select_all)
        select_menu.add_command(label='Select Line', accelerator='Ctrl+L', command=self.select_line)
        select_menu.add_command(label='Select Paragraph', accelerator='Ctrl+Shift+P', command=self.select_paragraph)
        select_menu.add_separator()
        select_menu.add_command(label='Expand Selection', accelerator='Ctrl+W', command=self.expand_selection)
        select_menu.add_command(label='Shrink Selection', accelerator='Ctrl+E', command=self.shrink_selection)

        # 首选项菜单
        preference_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='Preference', menu=preference_menu)
        # 语法高亮选项
        preference_menu.add_command(label='Enable/Disable Syntax Highlight', command=self.update_syntax_highlight)

        # 帮助菜单
        help_menu = Menu(self.menubar, tearoff=0)
        self.menubar.add_cascade(label='Help', menu=help_menu)
        help_menu.add_command(label='About', command=self.about)

        self.root.mainloop()

    def load_config(self):
        """加载配置文件"""
        with open('./app/config/config.json', 'r', encoding='utf-8') as f:
            config = json.load(f)
        return config

    def update_line_col(self, event=None):
        """更新光标所在的行列数"""
        line, col = self.text_area.index('insert').split('.')
        self.status_bar.config(text=f'Ln: {line} | Col: {col}')

    def insert_tab_spaces(self, event):
        """Tab键前进4格"""
        self.text_area.insert(INSERT, ' ' * self.config['number_of_tab_indents'])
        return 'break'

    def on_key_press(self, event):
        """键盘输入事件处理函数"""
        if event.char == '"':
            self.text_area.insert(INSERT, '"')  # 在光标位置插入"
            self.text_area.mark_set("insert", "insert-1c")  # 光标位置不变
        elif event.char == "'":
            self.text_area.insert(INSERT, "'")  # 在光标位置插入'
            self.text_area.mark_set("insert", "insert-1c")  # 光标位置不变
        elif event.char in ['(', '[', '{', '<']:
            end_char = {
                '(': ')',
                '[': ']',
                '{': '}',
                '<': '>'
            }
            self.text_area.insert(INSERT, end_char[event.char])  # 在光标位置插入对应的括号
            self.text_area.mark_set("insert", "insert-1c")  # 光标位置不变

    def check_file_saved(self, file_path):
        """检查文件是否已保存"""
        if file_path == 'untitled':
            if self.text_area.get('1.0', END).strip() == '':
                return True
            else:
                return False
        else:
            with open(file_path, 'r', encoding='utf-8') as f:
                if f.read() == self.text_area.get('1.0', END).strip():
                    return True
                else:
                    return False

    def exit_app(self):
        """退出程序"""
        if self.check_file_saved(self.data['file_path']):
            self.root.destroy()
        else:
            response = messagebox.askyesnocancel('Exit', 'Do you want to save changes?')
            if response is True:
                self.save_file()
                self.root.destroy()
            elif response is False:
                self.root.destroy()
            else:
                pass

    def new_file(self):
        """新建文件"""
        if self.check_file_saved(self.data['file_path']):
            self.text_area.delete('1.0', END)
            self.data['file_path'] = 'untitled'
            self.root.title('PyScripter - Untitled')
        else:
            response = messagebox.askyesnocancel('New File', 'Do you want to save changes?')
            if response is True:
                self.save_file()
            elif response is False:
                self.text_area.delete('1.0', END)
                self.data['file_path'] = 'untitled'
                self.root.title('PyScripter - Untitled')
            else:
                pass

    def open_file(self):
        """打开文件"""
        if self.check_file_saved(self.data['file_path']):
            file_path = filedialog.askopenfilename(title='Open File', filetypes=[('Python Files', '*.py')])
            if file_path:
                with open(file_path, 'r', encoding='utf-8') as f:
                    self.text_area.delete('1.0', END)
                    self.text_area.insert('1.0', f.read())
                self.data['file_path'] = file_path
                self.root.title(f'PyScripter - {file_path}')
        else:
            response = messagebox.askyesnocancel('Open File', 'Do you want to save changes?')
            if response is True:
                self.save_file()
            elif response is False:
                file_path = filedialog.askopenfilename(title='Open File', filetypes=[('Python Files', '*.py')])
                if file_path:
                    with open(file_path, 'r', encoding='utf-8') as f:
                        self.text_area.delete('1.0', END)
                        self.text_area.insert('1.0', f.read())
                    self.data['file_path'] = file_path
                    self.root.title(f'PyScripter - {file_path}')
            else:
                pass

    def save_file(self):
        """保存文件"""
        if self.data['file_path'] == 'untitled':
            file_path = filedialog.asksaveasfilename(title='Save File', filetypes=[('Python Files', '*.py')])
            if file_path:
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(self.text_area.get('1.0', END))
                self.data['file_path'] = file_path
                self.root.title(f'PyScripter - {file_path}')
        else:
            with open(self.data['file_path'], 'w', encoding='utf-8') as f:
                f.write(self.text_area.get('1.0', END))

    def save_as_file(self):
        """另存为文件"""
        file_path = filedialog.asksaveasfilename(title='Save As', filetypes=[('Python Files', '*.py')])
        if file_path:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(self.text_area.get('1.0', END))
            self.data['file_path'] = file_path
            self.root.title(f'PyScripter - {file_path}')

    def print_text(self):
        """打印文本"""
        text_to_print = self.text_area.get("1.0", "end-1c")
        if not text_to_print.strip():
            messagebox.showerror('Print Error', 'There is no text to print.')

        # 打印文本
        printer_name = win32print.GetDefaultPrinter()
        hprinter = win32print.OpenPrinter(printer_name)
        try:
            printer_info = win32print.GetPrinter(hprinter, 2)
            hdc = win32ui.CreateDC()
            hdc.CreatePrinterDC(printer_name)
            hdc.StartDoc(f"PyScripter - {self.data['file_path']}")
            hdc.StartPage()
            hdc.TextOut(100, 100, text_to_print)
            hdc.EndPage()
            hdc.EndDoc()
            hdc.DeleteDC()
        finally:
            win32print.ClosePrinter(hprinter)

    def file_statistics(self):
        """文件统计"""
        file_path = self.data['file_path']
        text = self.text_area.get('1.0', END)
        lines = text.count('\n')
        words = len(text.split())
        characters = len(text)
        messagebox.showinfo('File Statistics',
                            f'File: {file_path}\n\nLines: {lines}\nWords: {words}\nCharacters: {characters}')

    def undo(self):
        """撤销"""
        self.text_area.event_generate("<<Undo>>")

    def redo(self):
        """重做"""
        self.text_area.event_generate("<<Redo>>")

    def cut(self):
        """剪切"""
        self.text_area.event_generate("<<Cut>>")

    def copy(self):
        """复制"""
        self.text_area.event_generate("<<Copy>>")

    def paste(self):
        """粘贴"""
        self.text_area.event_generate("<<Paste>>")

    def find_all(self):
        """查找全部匹配项"""
        self.find_dialog = FindDialog(self.root, self.text_area)
        self.find_dialog.show()

    def replace(self):
        """替换功能"""
        self.replace_dialog = ReplaceDialog(self.root, self.text_area)
        self.replace_dialog.show()

    def cancel_find(self):
        """取消查找"""
        self.text_area.tag_remove('found', '1.0', tk.END)

    def select_all(self):
        """全选"""
        self.text_area.event_generate('<<SelectAll>>')

    def select_line(self):
        """选中当前行"""
        line, col = map(int, self.text_area.index('insert').split('.'))
        self.text_area.tag_add('sel', f'{line}.0', f'{line}.end')

    def select_paragraph(self):
        """选中当前段落"""
        line, col = map(int, self.text_area.index('insert').split('.'))
        paragraph_start = self.text_area.search(r'\n\s*\n', f'{line}.0', backwards=True, regexp=True)
        paragraph_end = self.text_area.search(r'\n\s*\n', f'{line}.0', forwards=True, regexp=True)
        self.text_area.tag_add('sel', paragraph_start, paragraph_end)

    def expand_selection(self):
        """扩大选择"""
        self.text_area.tag_add('sel', 'insert linestart', 'insert lineend+1c')

    def shrink_selection(self):
        """缩小选择"""
        self.text_area.tag_remove('sel', 'insert linestart', 'insert lineend+1c')

    def update_syntax_highlight(self):
        """更新是否启用语法高亮"""
        if self.config['python_syntax_highlight']:
            self.config['python_syntax_highlight'] = False
            with open('./app/config/config.json', 'w', encoding='utf-8') as f:
                json.dump(self.config, f, indent=4)
            messagebox.showinfo('Syntax Highlight', 'Syntax Highlight Disabled, Restart PyScripter to take effect.')
        else:
            self.config['python_syntax_highlight'] = True
            with open('./app/config/config.json', 'w', encoding='utf-8') as f:
                json.dump(self.config, f, indent=4)
            messagebox.showinfo('Syntax Highlight', 'Syntax Highlight Enabled, Restart PyScripter to take effect.')

    def about(self):
        """关于"""
        with open('./app/description.txt', 'r', encoding='utf-8') as f:
            description = f.read()
        messagebox.showinfo('About', 'About PyScripter\n\n'
                                     'Version: 1.2.0\n'
                                     'Author: Xiaoqing\n'
                                     'License: MIT\n'
                                     'Built Date: 2024-05-26\n\n'
                                     f'Description:{description}\n')


if __name__ == '__main__':
    Editor()

## dialog.py

import tkinter as tk
from tkinter import ttk


class FindDialog:
    def __init__(self, parent, text_widget):
        self.parent = parent
        self.text_widget = text_widget
        self.dialog = tk.Toplevel(parent)
        self.dialog.title('Find All')

        self.search_label = ttk.Label(self.dialog, text='Find:')
        self.search_label.pack(side=tk.LEFT)

        self.search_entry = ttk.Entry(self.dialog, width=30)
        self.search_entry.pack(side=tk.LEFT)

        self.search_entry.bind('<Return>', lambda event: self.find())

        self.find_button = ttk.Button(self.dialog, text='Find All', command=self.find)
        self.find_button.pack(side=tk.LEFT)

        self.cancel_button = ttk.Button(self.dialog, text='Cancel', command=self.cancel_find)
        self.cancel_button.pack(side=tk.LEFT)

        self.close_button = ttk.Button(self.dialog, text='Close', command=self.close)
        self.close_button.pack(side=tk.LEFT)

    def find(self):
        self.text_widget.tag_remove('found', '1.0', tk.END)
        s = self.search_entry.get()
        if s:
            idx = '1.0'
            while True:
                idx = self.text_widget.search(s, idx, nocase=1, stopindex=tk.END)
                if not idx:
                    break
                lastidx = f'{idx}+{len(s)}c'
                self.text_widget.tag_add('found', idx, lastidx)
                idx = lastidx
            self.text_widget.tag_config('found', background='lightblue')

    def cancel_find(self):
        self.text_widget.tag_remove('found', '1.0', tk.END)

    def close(self):
        self.dialog.destroy()

    def show(self):
        self.dialog.deiconify()



class ReplaceDialog:
    def __init__(self, parent, text_widget):
        self.parent = parent
        self.text_widget = text_widget
        self.dialog = tk.Toplevel(parent)
        self.dialog.title('Replace')

        self.search_label = ttk.Label(self.dialog, text='Find:')
        self.search_label.pack(side=tk.LEFT)

        self.search_entry = ttk.Entry(self.dialog, width=30)
        self.search_entry.pack(side=tk.LEFT)

        self.search_entry.bind('<Return>', lambda event: self.replace())

        self.replace_label = ttk.Label(self.dialog, text='Replace with:')
        self.replace_label.pack(side=tk.LEFT)

        self.replace_entry = ttk.Entry(self.dialog, width=30)
        self.replace_entry.pack(side=tk.LEFT)

        self.replace_button = ttk.Button(self.dialog, text='Replace', command=self.replace)
        self.replace_button.pack(side=tk.LEFT)

        self.cancel_button = ttk.Button(self.dialog, text='Undo', command=self.undo)
        self.cancel_button.pack(side=tk.LEFT)

        self.close_button = ttk.Button(self.dialog, text='Close', command=self.close)
        self.close_button.pack(side=tk.LEFT)

        self.history = []

    def replace(self):
        s = self.search_entry.get()
        r = self.replace_entry.get()
        if s and r:
            text = self.text_widget.get('1.0', tk.END)
            new_text = text.replace(s, r)
            self.history.append(text)
            self.text_widget.delete('1.0', tk.END)
            self.text_widget.insert('1.0', new_text)

    def close(self):
        self.dialog.destroy()

    def show(self):
        self.dialog.deiconify()

    def undo(self):
        if self.history:
            text = self.history.pop()
            self.text_widget.delete('1.0', tk.END)
            self.text_widget.insert('1.0', text)

## ./app/description.txt

PyScripter is a simple python code editor that allows you to write Python code.
It has python syntax highlighting, find, replace, and other features.
It can also be used as a regular text editor.

## ./app/config/config.json

{
    "umber_of_tab_indents": 4,
    "python_syntax_highlight": true
}

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值