布局布线和时序分析
笔记本


2025-08-27 15:06:44基于PYTHON2.6.6的文本编辑器走来走去116.236.47.26

#!/usr/bin/python

# -*- coding: utf-8 -*-
import sys
import os
import re
import json
reload(sys)
sys.setdefaultencoding('utf-8')

try:
    from Tkinter import *
    from tkFont import Font
    import tkFileDialog
    import tkMessageBox
except Exception as e:
    print 'Import error:', str(e)
    sys.exit(1)

CONFIG_FILE = 'config.txt'

class Editor:
    def __init__(self, root, file_arg=None):
        self.root = root
        self.root.title(u'\u7F16\u8F91\u5668')

        # 默认设置
        default_config = {
            'width': 1500,
            'height': 900,
            'font_family': 'Monospace',
            'font_size': 10,
            'color_mode': 0,
            'dark_mode': False
        }

        # 读取配置
        self.config = default_config
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r') as f:
                    saved = json.load(f)
                    self.config.update(saved)
            except:
                pass

        w = self.config['width']
        h = self.config['height']
        self.root.geometry('%dx%d' % (w, h))
        self.root.minsize(600, 400)

        self.current_font = Font(
            family=self.config['font_family'],
            size=self.config['font_size']
        )
        self.file_path = None
        self.file_contents = {}
        self.file_modifies = {}

        self.color_mode = IntVar(root, value=self.config['color_mode'])
        self.dark_mode = BooleanVar(root, value=self.config['dark_mode'])
        self.current_file_var = StringVar(root)

        self._build_ui()
        self._create_menus()

        # === 快捷键绑定 ===
        # 文件
        self.root.bind('', lambda e: self._new_file())
        self.root.bind('', lambda e: self._new_file())
        self.root.bind('', lambda e: self._open_dialog())
        self.root.bind('', lambda e: self._open_dialog())
        self.root.bind('', lambda e: self._save_file())
        self.root.bind('', lambda e: self._save_file())
        self.root.bind('', lambda e: self._save_as())
        self.root.bind('', lambda e: self._save_as())
        self.root.bind('', lambda e: self._close_file())
        self.root.bind('', lambda e: self._close_file())
        self.root.bind('', lambda e: self._exit_app())
        self.root.bind('', lambda e: self._exit_app())

        # 编辑
        self.root.bind('', lambda e: self._cut())
        self.root.bind('', lambda e: self._cut())
        self.root.bind('', lambda e: self._copy())
        self.root.bind('', lambda e: self._copy())
        self.root.bind('', lambda e: self._paste())
        self.root.bind('', lambda e: self._paste())
        self.root.bind('', lambda e: self._clear())

        # 查找与替换
        self.root.bind('', lambda e: self._find_dialog())
        self.root.bind('', lambda e: self._find_dialog())
        self.root.bind('', lambda e: self._replace_dialog())
        self.root.bind('', lambda e: self._replace_dialog())

        # 帮助
        self.root.bind('', lambda e: self._show_about())

        if file_arg:
            self._open_file(file_arg)

        # ? 启动光标位置轮询
        self.last_cursor_pos = '1.0'
        self.root.after(50, self._poll_cursor)

    def _poll_cursor(self):
        try:
            current = self.text_area.index('insert')
            if current != self.last_cursor_pos:
                self.last_cursor_pos = current
                self._update_status()
        except:
            pass
        self.root.after(50, self._poll_cursor)  # 每 50ms 检查一次

    def _set_color_mode_1(self):
        self.color_mode.set(0)
        self._highlight()

    def _set_color_mode_2(self):
        self.color_mode.set(1)
        self._highlight()

    def _toggle_dark_mode(self):
        self._apply_theme()  # ? 已移除 print

    def _apply_theme(self):
        is_dark = self.dark_mode.get()

        bg_main = '#1e1e1e' if is_dark else '#ffffff'
        fg_main = '#d4d4d4' if is_dark else '#000000'
        bg_line = '#2d2d2d' if is_dark else '#f0f0f0'
        fg_line = '#888888' if is_dark else '#555555'
        insert_bg = fg_main

        self.text_area.config(
            bg=bg_main,
            fg=fg_main,
            insertbackground=insert_bg,
            selectbackground='#4a4a4a' if is_dark else '#c0c0c0',
            selectforeground=fg_main
        )
        self.line_text.config(
            bg=bg_line,
            fg=fg_line
        )

        self.root.update()

    def _build_ui(self):
        main_frame = Frame(self.root)
        main_frame.pack(fill=BOTH, expand=1)

        line_frame = Frame(main_frame, width=40)
        line_frame.pack(side=LEFT, fill=Y)
        line_frame.pack_propagate(False)

        self.line_text = Text(
            line_frame,
            width=4,
            padx=2,
            pady=0,
            takefocus=0,
            border=0,
            state=DISABLED,
            font=self.current_font,
            wrap=NONE,
            bg='#f0f0f0',
            fg='#555555',
            spacing1=0,
            spacing2=0,
            spacing3=0
        )
        self.line_text.pack(side=LEFT, fill=Y)

        text_container = Frame(main_frame)
        text_container.pack(side=LEFT, fill=BOTH, expand=1)

        self.h_scroll = Scrollbar(text_container, orient=HORIZONTAL)
        self.h_scroll.pack(side=BOTTOM, fill=X)

        text_frame = Frame(text_container)
        text_frame.pack(side=TOP, fill=BOTH, expand=1)

        self.v_scroll = Scrollbar(text_frame, orient=VERTICAL)
        self.v_scroll.pack(side=RIGHT, fill=Y)

        self.text_area = Text(
            text_frame,
            wrap=NONE,
            undo=True,
            font=self.current_font,
            padx=0,
            pady=0,
            borderwidth=0,
            highlightthickness=0,
            spacing1=0,
            spacing2=0,
            spacing3=0
        )
        self.text_area.pack(side=LEFT, fill=BOTH, expand=1)

        self.text_area.config(
            yscrollcommand=self._sync_scrollbar,
            xscrollcommand=self.h_scroll.set
        )
        self.v_scroll.config(command=self._yview_sync)
        self.h_scroll.config(command=self.text_area.xview)

        # 保留基本绑定
        self.text_area.bind('', lambda e: None)  # 轮询会处理
        self.text_area.bind('<>', self._on_modified)
        self.text_area.bind('', self._on_input)

        self.status_frame = Frame(self.root, bd=1, relief=SUNKEN)
        self.status_frame.pack(side=BOTTOM, fill=X)
        self.status_label = Label(
            self.status_frame,
            text=u'\u6587\u4EF6\uFF1A\u65E0 \u884C\uFF1A1 \u5217\uFF1A1 \u5B57\u7B26\uFF1A0',
            font=('Monospace', 9),
            anchor=W
        )
        self.status_label.pack(fill=X)

        self._setup_syntax_tags()
        self._apply_theme()
        self._update_line_numbers()

    def _setup_syntax_tags(self):
        colors = [
            ('m1_neg_deep', '#8B0000'),
            ('m1_neg_light', '#DC143C'),
            ('m1_blue', 'deepskyblue'),
            ('m1_green', 'limegreen'),
            ('m2_neg_deep', '#8B0000'),
            ('m2_neg_light', '#DC143C'),
            ('m2_blue', 'deepskyblue'),
            ('m2_green', 'limegreen'),
        ]
        for name, color in colors:
            self.text_area.tag_configure(name, foreground=color, font=self.current_font)

    def _sync_scrollbar(self, first, last):
        self.v_scroll.set(first, last)
        self.line_text.yview_moveto(first)

    def _yview_sync(self, *args):
        self.text_area.yview(*args)
        self.line_text.yview(*args)

    def _update_line_numbers(self):
        self.line_text.config(state=NORMAL)
        self.line_text.delete(1.0, END)
        content = self.text_area.get(1.0, END)
        lines = content.split('\n')
        line_numbers = '\n'.join(str(i) for i in range(1, len(lines)))
        self.line_text.insert(1.0, line_numbers)
        self.line_text.config(state=DISABLED)
        self.line_text.yview_moveto(self.text_area.yview()[0])

    def _on_input(self, event=None):
        self._update_line_numbers()

    def _on_modified(self, event=None):
        if self.text_area.edit_modified():
            self.text_area.edit_modified(False)
            self._highlight()
            self._update_line_numbers()
            self._update_status()

    def _highlight(self):
        tags = ['m1_neg_deep', 'm1_neg_light', 'm1_blue', 'm1_green',
                'm2_neg_deep', 'm2_neg_light', 'm2_blue', 'm2_green']
        for tag in tags:
            self.text_area.tag_remove(tag, '1.0', END)

        content = self.text_area.get('1.0', END)
        lines = content.split('\n')

        for row, line in enumerate(lines):
            num_matches = re.finditer(r'-?(?:0|[1-9]\d*)(?:\.\d+)?', line)
            for m in num_matches:
                try:
                    val = float(m.group())
                    start = '%d.%d' % (row + 1, m.start())
                    end = '%d.%d' % (row + 1, m.end())
                    if self.color_mode.get() == 0:
                        if val < -0.2:
                            self.text_area.tag_add('m1_neg_deep', start, end)
                        elif val < -0.1:
                            self.text_area.tag_add('m1_neg_light', start, end)
                        elif val < -0.05:
                            self.text_area.tag_add('m1_blue', start, end)
                        elif val < 0:
                            self.text_area.tag_add('m1_green', start, end)
                    else:
                        if val < -200:
                            self.text_area.tag_add('m2_neg_deep', start, end)
                        elif val < -100:
                            self.text_area.tag_add('m2_neg_light', start, end)
                        elif val < -50:
                            self.text_area.tag_add('m2_blue', start, end)
                        elif val < 0:
                            self.text_area.tag_add('m2_green', start, end)
                except:
                    continue

    def _update_status(self, event=None):
        try:
            line, col = self.text_area.index('insert').split('.')
            line = int(line)
            col = int(col) + 1
        except:
            line, col = 1, 1
        content = self.text_area.get('1.0', END)
        char_count = len(content) - 1
        filename = self.file_path if self.file_path else u'\u65E0'
        self.status_label.config(
            text=u'\u6587\u4EF6\uFF1A%s \u884C\uFF1A%d \u5217\uFF1A%d \u5B57\u7B26\uFF1A%d' % (filename, line, col, char_count)
        )

    def _create_menus(self):
        menu_bar = Menu(self.root)
        self.root.config(menu=menu_bar)

        # 文件菜单
        file_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u6587\u4EF6', menu=file_menu)
        file_menu.add_command(
            label=u'\u65B0\u5EFA',
            command=self._new_file,
            accelerator='Ctrl+N'
        )
        file_menu.add_command(
            label=u'\u6253\u5F00',
            command=self._open_dialog,
            accelerator='Ctrl+O'
        )
        file_menu.add_command(
            label=u'\u5173\u95ED\u6587\u4EF6',
            command=self._close_file,
            accelerator='Ctrl+W'
        )
        file_menu.add_separator()
        file_menu.add_command(
            label=u'\u4FDD\u5B58',
            command=self._save_file,
            accelerator='Ctrl+S'
        )
        file_menu.add_command(
            label=u'\u53E6\u5B58\u4E3A',
            command=self._save_as,
            accelerator='Ctrl+Shift+S'
        )
        file_menu.add_separator()
        file_menu.add_command(
            label=u'\u9000\u51FA',
            command=self._exit_app,
            accelerator='Ctrl+Q'
        )

        # 切换文件
        switch_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u5207\u6362\u6587\u4EF6', menu=switch_menu)
        self.switch_menu = switch_menu
        self._update_switch_menu()

        # 着色模式
        color_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u7740\u8272\u6A21\u5F0F', menu=color_menu)
        color_menu.add_radiobutton(
            label=u'\u6A21\u5F0F1: \u5C0F\u6570',
            variable=self.color_mode,
            value=0,
            command=self._set_color_mode_1
        )
        color_menu.add_radiobutton(
            label=u'\u6A21\u5F0F2: \u6574\u6570',
            variable=self.color_mode,
            value=1,
            command=self._set_color_mode_2
        )

        # 视图
        view_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u89C6\u56FE', menu=view_menu)
        view_menu.add_checkbutton(
            label=u'\u6697\u8272\u6A21\u5F0F',
            variable=self.dark_mode,
            command=self._toggle_dark_mode
        )

        # 编辑菜单
        edit_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u7F16\u8F91', menu=edit_menu)
        edit_menu.add_command(
            label=u'\u526A\u5207',
            command=self._cut,
            accelerator='Ctrl+X'
        )
        edit_menu.add_command(
            label=u'\u590D\u5236',
            command=self._copy,
            accelerator='Ctrl+C'
        )
        edit_menu.add_command(
            label=u'\u7C98\u8D34',
            command=self._paste,
            accelerator='Ctrl+V'
        )
        edit_menu.add_separator()
        edit_menu.add_command(
            label=u'\u6E05\u9664',
            command=self._clear,
            accelerator='Del'
        )
        edit_menu.add_separator()
        edit_menu.add_command(
            label=u'\u67E5\u627E',
            command=self._find_dialog,
            accelerator='Ctrl+F'
        )
        edit_menu.add_command(
            label=u'\u66FF\u6362',
            command=self._replace_dialog,
            accelerator='Ctrl+H'
        )

        # 设置
        setting_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u8BBE\u7F6E', menu=setting_menu)
        setting_menu.add_command(
            label=u'\u5B57\u4F53',
            command=self._choose_font
        )

        # 帮助
        help_menu = Menu(menu_bar, tearoff=0)
        menu_bar.add_cascade(label=u'\u5E2E\u52A9', menu=help_menu)
        help_menu.add_command(
            label=u'\u5173\u4E8E',
            command=self._show_about,
            accelerator='F1'
        )

    def _update_switch_menu(self):
        self.switch_menu.delete(0, END)
        if not self.file_contents:
            self.switch_menu.add_command(label=u'\u65E0', state=DISABLED)
        else:
            for path in self.file_contents:
                name = os.path.basename(path)
                self.switch_menu.add_radiobutton(
                    label=name,
                    variable=self.current_file_var,
                    value=path,
                    command=lambda p=path: self._switch_to(p)
                )
        if self.file_path:
            self.current_file_var.set(self.file_path)
        else:
            self.current_file_var.set('')

    def _switch_to(self, path):
        if self.file_path and self.file_modifies.get(self.file_path, False):
            if tkMessageBox.askyesno(u'\u4FDD\u5B58', u'\u672A\u4FDD\u5B58\uFF0C\u662F\u5426\u4FDD\u5B58\uFF1F'):
                self._save_current()

        self.file_path = path
        self.text_area.delete(1.0, END)
        self.text_area.insert(1.0, self.file_contents[path])
        self.text_area.edit_modified(False)
        self.root.title(u'\u7F16\u8F91\u5668 \u2014 ' + path)
        self._on_modified()
        self._update_switch_menu()

    def _close_file(self):
        if not self.file_path:
            return
        if self.file_modifies.get(self.file_path, False):
            if not tkMessageBox.askyesno(u'\u5173\u95ED', u'\u672A\u4FDD\u5B58\uFF0C\u786E\u8BA4\u5173\u95ED\uFF1F'):
                return
        del self.file_contents[self.file_path]
        del self.file_modifies[self.file_path]
        next_path = next(iter(self.file_contents)) if self.file_contents else None
        if next_path:
            self._switch_to(next_path)
        else:
            self.file_path = None
            self.text_area.delete(1.0, END)
            self.root.title(u'\u7F16\u8F91\u5668')
        self._update_switch_menu()

    def _open_dialog(self):
        path = tkFileDialog.askopenfilename(title=u'\u6253\u5F00', filetypes=[])
        if path:
            self._open_file(path)

    def _open_file(self, path):
        if path in self.file_contents:
            self._switch_to(path)
            return

        if os.path.getsize(path) > 200 * 1024 * 1024:
            tkMessageBox.showwarning(
                u'\u6587\u4EF6\u8FC7\u5927',
                u'\u6587\u4EF6\u5927\u4E8E200MB\uFF0C\u5DF2\u88AB\u62D2'
            )
            return

        try:
            if path.endswith('.gz'):
                import gzip
                f_gz = gzip.open(path, 'rb')
                try:
                    content = f_gz.read().decode('utf-8')
                finally:
                    f_gz.close()
            else:
                with open(path, 'r') as f:
                    content = f.read().decode('utf-8')

            self.file_contents[path] = content
            self.file_modifies[path] = False
            self._switch_to(path)
        except Exception as e:
            tkMessageBox.showerror(u'\u9519\u8BEF', u'\u6253\u5F00\u5931\u8D25\n' + str(e))

    def _save_file(self):
        if self.file_path:
            self._save_current()
        else:
            self._save_as()

    def _save_as(self):
        path = tkFileDialog.asksaveasfilename(filetypes=[])
        if path:
            self._do_save(path)
            self.file_path = path
            self.root.title(u'\u7F16\u8F91\u5668 \u2014 ' + path)
            self._update_switch_menu()

    def _do_save(self, path):
        try:
            content = self.text_area.get(1.0, END)
            raw_content = content.encode('utf-8')

            if path.endswith('.gz'):
                import gzip
                f_gz = gzip.open(path, 'wb')
                try:
                    f_gz.write(raw_content)
                finally:
                    f_gz.close()
            else:
                with open(path, 'w') as f:
                    f.write(raw_content)

            self.file_contents[path] = content
            self.file_modifies[path] = False
            tkMessageBox.showinfo(u'\u4FDD\u5B58', u'\u5DF2\u4FDD\u5B58')
            self._update_status()
        except Exception as e:
            tkMessageBox.showerror(u'\u9519\u8BEF', u'\u4FDD\u5B58\u5931\u8D25\n' + str(e))

    def _save_current(self):
        if self.file_path:
            self._do_save(self.file_path)

    def _new_file(self):
        if self.file_path and self.file_modifies.get(self.file_path, False):
            if tkMessageBox.askyesno(u'\u4FDD\u5B58', u'\u672A\u4FDD\u5B58\uFF0C\u662F\u5426\u4FDD\u5B58\uFF1F'):
                self._save_current()
        self.text_area.delete(1.0, END)
        self.file_path = None
        self.root.title(u'\u7F16\u8F91\u5668')
        self._on_modified()

    def _exit_app(self):
        self.config['width'] = self.root.winfo_width()
        self.config['height'] = self.root.winfo_height()
        self.config['font_family'] = self.current_font.cget('family')
        self.config['font_size'] = self.current_font.cget('size')
        self.config['color_mode'] = self.color_mode.get()
        self.config['dark_mode'] = self.dark_mode.get()

        try:
            with open(CONFIG_FILE, 'w') as f:
                json.dump(self.config, f)
        except Exception as e:
            pass

        for path in self.file_contents:
            if self.file_modifies.get(path, False):
                if tkMessageBox.askyesnocancel(u'\u9000\u51FA', u'\u6709\u672A\u4FDD\u5B58\u6587\u4EF6\uFF0C\u786E\u8BA4\u9000\u51FA\uFF1F'):
                    self._save_current()
                elif tkMessageBox.askyesnocancel is None:
                    return
        self.root.destroy()

    def _cut(self):
        self.text_area.event_generate('<>')

    def _copy(self):
        self.text_area.event_generate('<>')

    def _paste(self):
        self.text_area.event_generate('<>')

    def _clear(self):
        try:
            self.text_area.delete('sel.first', 'sel.last')
        except:
            pass

    def _choose_font(self):
        win = Toplevel(self.root)
        win.title(u'\u5B57\u4F53')
        win.geometry('300x200')
        Label(win, text=u'\u5B57\u4F53').pack(anchor=W, padx=10, pady=5)
        family_list = Listbox(win, height=5)
        families = ['Monospace', 'Courier', 'Consolas', 'Arial']
        for f in families:
            family_list.insert(END, f)
        family_list.pack(fill=X, padx=10)
        family_list.selection_set(0)
        Label(win, text=u'\u5B57\u53F7').pack(anchor=W, padx=10, pady=5)
        size_var = StringVar(value=str(self.current_font.cget('size')))
        Entry(win, textvariable=size_var).pack(fill=X, padx=10)
        def apply():
            try:
                idx = family_list.curselection()[0]
                family = families[idx]
            except:
                family = self.current_font.cget('family')
            try:
                size = int(size_var.get())
            except:
                size = 10
            self.current_font.config(family=family, size=size)
            self.text_area.config(font=self.current_font)
            self.line_text.config(font=self.current_font)
            win.destroy()
        Button(win, text=u'\u786E\u5B9A', command=apply).pack(pady=10)

    def _show_about(self):
        msg = (u'\u6B22\u8FCE\u4F7F\u7528\u672C\u7F16\u8F91\u5668\uFF01\n\n'
               u'\u529F\u80FD\uFF1A\n'
               u'\u2022 \u67E5\u627E\u66FF\u6362\n'
               u'\u2022 \u53CC\u7740\u8272\u6A21\u5F0F\n'
               u'\u2022 \u6697\u8272\u6A21\u5F0F\n'
               u'\u2022 \u8BB0\u4F4F\u8BBE\u7F6E\n'
               u'\u2022 \u5927\u6587\u4EF6\u62E6\u622A\n\n'
               u'Python 2.6.6 + Tkinter\n'
               u'Version 1.0')
        tkMessageBox.showinfo(u'\u5173\u4E8E', msg)

    def _find_dialog(self):
        if hasattr(self, 'find_window') and self.find_window.winfo_exists():
            self.find_window.lift()
            return

        self.find_window = Toplevel(self.root)
        self.find_window.title(u'\u67E5\u627E')
        self.find_window.geometry('300x140')
        self.find_window.resizable(False, False)

        Label(self.find_window, text=u'\u67E5\u627E\u5185\u5BB9\uFF1A').pack(anchor=W, padx=10, pady=(10, 0))

        find_var = StringVar()
        if hasattr(self, 'last_find'):
            find_var.set(self.last_find)
        entry = Entry(self.find_window, textvariable=find_var, width=40)
        entry.pack(padx=10, pady=5)

        case_var = BooleanVar()
        Checkbutton(self.find_window, text=u'\u533A\u5206\u5927\u5C0F\u5199', variable=case_var).pack(anchor=W, padx=10)

        btn_frame = Frame(self.find_window)
        btn_frame.pack(pady=5)

        def find_next():
            query = find_var.get()
            if not query:
                return
            self.last_find = query
            self._find_next(query, case_var.get())

        def find_prev():
            query = find_var.get()
            if not query:
                return
            self.last_find = query
            self._find_prev(query, case_var.get())

        Button(btn_frame, text=u'\u67E5\u627E\u4E0B\u4E00\u4E2A', command=find_next).pack(side=LEFT, padx=5)
        Button(btn_frame, text=u'\u67E5\u627E\u4E0A\u4E00\u4E2A', command=find_prev).pack(side=LEFT, padx=5)
        Button(btn_frame, text=u'\u5173\u95ED', command=self._close_find_dialog).pack(side=LEFT, padx=5)

        # ? 支持主回车和小键盘回车
        entry.bind('', lambda e: find_next())
        entry.bind('', lambda e: find_next())

        find_var.trace('w', lambda *args: self._clear_find_tags())

        self.find_var = find_var
        self.case_var = case_var
        self.find_entry = entry

    def _close_find_dialog(self):
        if hasattr(self, 'find_window'):
            self.find_window.destroy()

    def _find_next(self, query, match_case=False):
        self._clear_find_tags()
        self.text_area.tag_configure('find_match', background='yellow', foreground='black')

        content = self.text_area.get('1.0', END)
        start_pos = self.text_area.index('insert')
        if start_pos == '1.0':
            start_pos = '1.0 + 1c'

        found = False
        pattern = re.escape(query)
        flags = 0 if match_case else re.IGNORECASE

        lines = content.split('\n')
        start_line, start_col = map(int, start_pos.split('.'))
        start_col += 1

        for line_idx in range(start_line - 1, len(lines)):
            line = lines[line_idx]
            offset = 0
            if line_idx == start_line - 1:
                line = line[start_col:]
                offset = start_col

            matches = list(re.finditer(pattern, line, flags))
            if matches:
                for match in matches:
                    s = match.start() + offset
                    e = match.end() + offset
                    start_index = "%d.%d" % (line_idx + 1, s)
                    end_index = "%d.%d" % (line_idx + 1, e)
                    self.text_area.tag_add('find_match', start_index, end_index)
                    if not found:
                        self.text_area.mark_set('insert', end_index)
                        self.text_area.see('insert')
                        found = True

        if not found:
            tkMessageBox.showinfo(u'\u67E5\u627E', u'\u672A\u627E\u5230\u201C%s\u201D' % query)

    def _find_prev(self, query, match_case=False):
        self._clear_find_tags()
        self.text_area.tag_configure('find_match', background='yellow', foreground='black')

        content = self.text_area.get('1.0', END)
        start_pos = self.text_area.index('insert')
        if start_pos == '1.0':
            start_pos = 'end'

        pattern = re.escape(query)
        flags = 0 if match_case else re.IGNORECASE

        lines = content.split('\n')
        line_count = len(lines)

        try:
            start_line, start_col = map(int, start_pos.split('.'))
        except:
            start_line, start_col = line_count, 0

        found = False

        for line_idx in range(start_line - 1, -1, -1):
            line = lines[line_idx]
            search_line = line if line_idx < start_line - 1 else line[:start_col]
            matches = list(re.finditer(pattern, search_line, flags))
            if matches:
                match = matches[-1]
                s = match.start()
                e = match.end()
                start_index = "%d.%d" % (line_idx + 1, s)
                end_index = "%d.%d" % (line_idx + 1, e)
                self.text_area.tag_add('find_match', start_index, end_index)
                self.text_area.mark_set('insert', start_index)
                self.text_area.see('insert')
                found = True
                break

        if not found:
            for line_idx in range(len(lines) - 1, start_line - 1, -1):
                line = lines[line_idx]
                matches = list(re.finditer(pattern, line, flags))
                if matches:
                    match = matches[-1]
                    s = match.start()
                    e = match.end()
                    start_index = "%d.%d" % (line_idx + 1, s)
                    end_index = "%d.%d" % (line_idx + 1, e)
                    self.text_area.tag_add('find_match', start_index, end_index)
                    self.text_area.mark_set('insert', start_index)
                    self.text_area.see('insert')
                    found = True
                    break

        if not found:
            tkMessageBox.showinfo(u'\u67E5\u627E', u'\u672A\u627E\u5230\u201C%s\u201D' % query)

    def _clear_find_tags(self):
        self.text_area.tag_remove('find_match', '1.0', END)

    def _replace_dialog(self):
        if hasattr(self, 'replace_window') and self.replace_window.winfo_exists():
            self.replace_window.lift()
            return

        self.replace_window = Toplevel(self.root)
        self.replace_window.title(u'\u66FF\u6362')
        self.replace_window.geometry('350x180')
        self.replace_window.resizable(False, False)
        self.replace_window.transient(self.root)
        self.replace_window.grab_set()

        Label(self.replace_window, text=u'\u67E5\u627E\u5185\u5BB9\uFF1A').pack(anchor=W, padx=10, pady=(10, 0))
        find_var = StringVar()
        if hasattr(self, 'last_find'):
            find_var.set(self.last_find)
        find_entry = Entry(self.replace_window, textvariable=find_var, width=45)
        find_entry.pack(padx=10, pady=5)

        Label(self.replace_window, text=u'\u66FF\u6362\u4E3A\uFF1A').pack(anchor=W, padx=10, pady=(5, 0))
        replace_var = StringVar()
        Entry(self.replace_window, textvariable=replace_var, width=45).pack(padx=10, pady=5)

        case_var = BooleanVar()
        Checkbutton(self.replace_window, text=u'\u533A\u5206\u5927\u5C0F\u5199', variable=case_var).pack(anchor=W, padx=10)

        btn_frame = Frame(self.replace_window)
        btn_frame.pack(pady=10)

        def replace_next():
            find_text = find_var.get()
            replace_text = replace_var.get()
            if not find_text:
                return
            self.last_find = find_text
            count = self._replace_next(find_text, replace_text, case_var.get())
            if count == 0:
                tkMessageBox.showinfo(u'\u66FF\u6362', u'\u672A\u627E\u5230\u201C%s\u201D' % find_text)

        def replace_all():
            find_text = find_var.get()
            replace_text = replace_var.get()
            if not find_text:
                return
            self.last_find = find_text
            count = self._replace_all(find_text, replace_text, case_var.get())
            tkMessageBox.showinfo(u'\u66FF\u6362', u'\u5171\u66FF\u6362 %d \u5904' % count)

        Button(btn_frame, text=u'\u66FF\u6362', command=replace_next).pack(side=LEFT, padx=10)
        Button(btn_frame, text=u'\u5168\u90E8\u66FF\u6362', command=replace_all).pack(side=LEFT, padx=10)
        Button(btn_frame, text=u'\u5173\u95ED', command=self._close_replace_dialog).pack(side=LEFT, padx=10)

        self.find_var_rep = find_var
        self.replace_var = replace_var
        self.case_var_rep = case_var

        # ? 支持主回车和小键盘回车
        self.replace_window.bind('', lambda e: replace_next())
        self.replace_window.bind('', lambda e: replace_next())
        self.replace_window.bind('', lambda e: self._close_replace_dialog())
        find_var.trace('w', lambda *args: self._clear_find_tags())

        self.replace_window.update_idletasks()
        self.replace_window.focus_set()

    def _close_replace_dialog(self):
        if hasattr(self, 'replace_window'):
            self.replace_window.destroy()

    def _replace_next(self, find_str, replace_str, match_case=False):
        self._clear_find_tags()
        content = self.text_area.get('1.0', END)
        start_pos = self.text_area.index('insert')
        if start_pos == '1.0':
            start_pos = '1.0 + 1c'

        pattern = re.escape(find_str)
        flags = 0 if match_case else re.IGNORECASE

        lines = content.split('\n')
        start_line, start_col = map(int, start_pos.split('.'))
        start_col += 1

        for line_idx in range(start_line - 1, len(lines)):
            line = lines[line_idx]
            offset = 0
            if line_idx == start_line - 1:
                line = line[start_col:]
                offset = start_col

            match = re.search(pattern, line, flags)
            if match:
                s = match.start() + offset
                e = match.end() + offset
                start_index = "%d.%d" % (line_idx + 1, s)
                end_index = "%d.%d" % (line_idx + 1, e)
                self.text_area.delete(start_index, end_index)
                self.text_area.insert(start_index, replace_str)
                self.text_area.mark_set('insert', start_index)
                self.text_area.see('insert')
                self._on_modified()
                return 1
        return 0

    def _replace_all(self, find_str, replace_str, match_case=False):
        self._clear_find_tags()
        content = self.text_area.get('1.0', END)
        pattern = re.escape(find_str)
        flags = 0 if match_case else re.IGNORECASE

        if match_case:
            replaced = content.replace(find_str, replace_str)
        else:
            def replace_func(match):
                return replace_str
            replaced = re.sub(pattern, replace_func, content, flags=flags)

        if replaced != content:
            self.text_area.delete('1.0', END)
            self.text_area.insert('1.0', replaced)
            self._on_modified()
            return replaced.count(replace_str)
        return 0

if __name__ == '__main__':
    root = Tk()
    file_arg = sys.argv[1] if len(sys.argv) > 1 else None
    app = Editor(root, file_arg)
    root.mainloop()


回到首页时钟 , 联系信箱:yzbox#163.com(把#换成@) 粤ICP备18155639号