自定义的文本编辑器

自定义的文本编辑器#

最终的效果图:

自定义的文本编辑器

代码:

from tkinter import filedialog, Toplevel, Menu, Text, Tk, messagebox
from tkinter import ttk, PhotoImage, IntVar, StringVar, BooleanVar
from pathlib import Path


class WindowMeta(Tk):
    '''Create a text editor.'''

    def __init__(self):
        super().__init__()
        self.geometry('350x350')
        self.show_line_number = IntVar()
        self.show_line_number.set(1)
        self.create_widgets()
        self.layout()
        self.content_text.bind('<Any-KeyPress>', self.on_content_changed)
        self.content_text.tag_configure('active_line', background='ivory2')
        self.protocol('WM_DELETE_WINDOW', self.exit_editor)

    def get_line_numbers(self):
        output = ''
        if self.show_line_number.get():
            row, _ = self.content_text.index("end").split('.')
            for k in range(1, int(row)):
                output += str(k) + '\n'
        return output

    def update_line_numbers(self, event=None):
        line_numbers = self.get_line_numbers()
        self.line_number_bar.config(state='normal')
        self.line_number_bar.delete('1.0', 'end')
        self.line_number_bar.insert('1.0', line_numbers)
        self.line_number_bar.config(state='disabled')

    def _create_content_text(self):
        '''Create a scrollable text box.'''
        self.content_text = Text(wrap='word', undo=1)
        self.scroll_bar = ttk.Scrollbar(self.content_text)
        self.content_text.configure(yscrollcommand=self.scroll_bar.set)
        self.scroll_bar.config(command=self.content_text.yview)

    def _create_shortbar(self):
        style = ttk.Style()
        style.configure('TFrame', background='light sea green')
        self.shortcut_bar = ttk.Frame(height=25)

    def create_widgets(self):
        self._create_shortbar()
        self.line_number_bar = Text(width=4, padx=3, takefocus=0,  border=0,
                                    background='khaki', state='disabled',  wrap='none')
        self._create_content_text()
        self.cursor_info_bar = ttk.Label(
            self.content_text, text='Row: 0 | Column: 0')

    def update_cursor_info_bar(self, event=None):
        row, col = self.content_text.index('insert').split('.')
        infotext = f"Row: {row} | Column: {int(col)+1}"
        self.cursor_info_bar.config(text=infotext)

    def on_content_changed(self, event=None):
        self.update_line_numbers()
        self.update_cursor_info_bar()

    def layout(self):
        '''Initialize the style that sets the Window.'''
        self.shortcut_bar.pack(expand='no', fill='x')
        self.line_number_bar.pack(side='left',  fill='y')
        self.content_text.pack(expand='yes', fill='both')
        self.scroll_bar.pack(side='right', fill='y')
        self.cursor_info_bar.pack(
            expand='no', fill=None, side='right', anchor='se')

    def exit_editor(self):
        if messagebox.askokcancel("Quit?", "Really quit?"):
            self.destroy()


class Window(WindowMeta):

    file_name_params = {
        'defaultextension': ".txt",
        'filetypes': [("Text Documents", "*.txt"),
                      ("Images", "*.jpg *.gif *.png"),
                      ("All Files", "*.*")]
    }

    def __init__(self, program_name="Notebook Editor"):
        super().__init__()
        self.program_name = program_name
        self.temp_file_name = StringVar()
        self.temp_file_name.trace_add('write', self.update_title)
        self.file_name = StringVar()
        self.update_title()
        self.bind_content_text()

    def update_title(self, *args):
        file_name = self.temp_file_name.get()
        if file_name:
            file_name = Path(file_name)
            base_name = file_name.parts[-1]
            self.title(f"{base_name}-{self.program_name}")
        else:
            self.title(self.program_name)

    def display_about_messagebox(self, event=None):
        text = "\nTkinter GUI Application\n Development Blueprints"
        messagebox.showinfo("About", f"{self.program_name}{text}")

    def layout(self):
        '''Initialize the style that sets the Window.'''
        self.shortcut_bar.pack(expand='no', fill='x')
        icons = ('new_file', 'open_file', 'save', 'cut', 'copy', 'paste',
                 'undo', 'redo', 'find_text')
        for icon in icons:
            tool_bar_icon = PhotoImage(file=f'icons/{icon}.gif')
            cmd = eval(f'self.{icon}')
            tool_bar = ttk.Button(
                self.shortcut_bar, image=tool_bar_icon, command=cmd)
            tool_bar.image = tool_bar_icon
            tool_bar.pack(side='left')
        self.line_number_bar.pack(side='left',  fill='y')
        self.content_text.pack(expand='yes', fill='both')
        self.scroll_bar.pack(side='right', fill='y')
        self.cursor_info_bar.pack(
            expand='no', fill=None, side='right', anchor='se')

    def new_file(self, event=None):
        self.temp_file_name.set('Untitled')
        self.file_name.set('')
        self.content_text.delete(1.0, 'end')
        self.on_content_changed()

    def open_file(self):
        input_file_name = filedialog.askopenfilename(**self.file_name_params)
        self.temp_file_name.set(input_file_name)
        if input_file_name:
            self.file_name.set(input_file_name)
            self.content_text.delete(1.0, 'end')
            with open(input_file_name, encoding='utf-8') as _file:
                self.content_text.insert(1.0, _file.read())
            self.on_content_changed()

    def write_to_file(self, file_name):
        try:
            content = self.content_text.get(1.0, 'end')
            with open(file_name, 'w', encoding='utf-8') as the_file:
                the_file.write(content)
        except IOError:
            messagebox.showwarning("Save", "Could not save the file.")

    def save_as(self, event=None):
        input_file_name = filedialog.asksaveasfilename(**self.file_name_params)
        self.temp_file_name.set(input_file_name)
        if input_file_name:
            self.write_to_file(input_file_name)
        return "break"

    def save(self, event=None):
        file_name = self.file_name.get()
        if file_name:
            self.write_to_file(file_name)
        else:
            self.save_as()
        return "break"

    def find_text(self, event=None):
        search_toplevel = Toplevel(self.master)
        search_toplevel.title('Find Text')
        search_toplevel.transient(self.master)

        ttk.Label(search_toplevel, text="Find All:").grid(
            row=0, column=0, sticky='e')

        search_entry_widget = ttk.Entry(
            search_toplevel, width=25)
        search_entry_widget.grid(row=0, column=1, padx=2, pady=2, sticky='we')
        search_entry_widget.focus_set()
        ignore_case_value = IntVar()
        ttk.Checkbutton(search_toplevel, text='Ignore Case', variable=ignore_case_value).grid(
            row=1, column=1, sticky='e', padx=2, pady=2)
        button = ttk.Button(search_toplevel, text="Find All", underline=0,
                            command=lambda: self.search_output(search_entry_widget.get(),
                                                               ignore_case_value.get(),
                                                               search_toplevel, search_entry_widget))
        button.grid(row=0,
                    column=2, sticky='e' + 'w', padx=2, pady=2)

        def close_search_window():
            self.content_text.tag_remove('match', '1.0', 'end')
            search_toplevel.destroy()
        search_toplevel.protocol('WM_DELETE_WINDOW', close_search_window)
        return "break"

    def search_output(self, needle, if_ignore_case, search_toplevel, search_box):
        self.content_text.tag_remove('match', '1.0', 'end')
        matches_found = 0
        if needle:
            start_pos = '1.0'
            while True:
                start_pos = self.content_text.search(needle, start_pos,
                                                     nocase=if_ignore_case, stopindex='end')
                if not start_pos:
                    break
                end_pos = f'{start_pos}+{len(needle)}c'
                self.content_text.tag_add('match', start_pos, end_pos)
                matches_found += 1
                start_pos = end_pos
            self.content_text.tag_config(
                'match', foreground='red', background='yellow')
        search_box.focus_set()
        search_toplevel.title(f'{matches_found} matches found')

    def cut(self):
        self.content_text.event_generate("<<Cut>>")
        self.on_content_changed()
        return "break"

    def copy(self):
        self.content_text.event_generate("<<Copy>>")
        return "break"

    def paste(self):
        self.content_text.event_generate("<<Paste>>")
        self.on_content_changed()
        return "break"

    def undo(self):
        self.content_text.event_generate("<<Undo>>")
        self.on_content_changed()
        return "break"

    def redo(self, event=None):
        self.content_text.event_generate("<<Redo>>")
        self.on_content_changed()
        return 'break'

    def display_help_messagebox(self, event=None):
        messagebox.showinfo(
            "Help", "Help Book: \nTkinter GUI Application\n Development Blueprints",
            icon='question')

    def select_all(self, event=None):
        self.content_text.tag_add('sel', '1.0', 'end')
        return "break"

    def bind_content_text(self):
        self.content_text.bind('<KeyPress-F1>', self.display_help_messagebox)
        self.content_text.bind('<Control-N>', self.new_file)
        self.content_text.bind('<Control-n>', self.new_file)
        self.content_text.bind('<Control-O>', self.open_file)
        self.content_text.bind('<Control-o>', self.open_file)
        self.content_text.bind('<Control-S>', self.save)
        self.content_text.bind('<Control-s>', self.save)
        self.content_text.bind('<Control-f>', self.find_text)
        self.content_text.bind('<Control-F>', self.find_text)
        self.content_text.bind('<Control-A>', self.select_all)
        self.content_text.bind('<Control-a>', self.select_all)
        self.content_text.bind('<Control-y>', self.redo)
        self.content_text.bind('<Control-Y>', self.redo)


class Popup:
    def __init__(self, master):
        self.master = master
        self.generate_menu()
        # bind right mouse click to show pop up and set focus to text widget on launch
        self.master.content_text.bind('<Button-3>', self.show_popup_menu)
        self.master.content_text.focus_set()

    def generate_menu(self):
        # set up the pop-up menu
        self.popup_menu = Menu(self.master.content_text, tearoff=0)
        for m in ('cut', 'copy', 'paste', 'undo', 'redo'):
            cmd = eval(f'self.master.{m}')
            self.popup_menu.add_command(label=m, compound='left', command=cmd)
        self.popup_menu.add_separator()
        self.popup_menu.add_command(
            label='Select All', underline=7, command=self.master.select_all)

    def show_popup_menu(self, event):
        self.popup_menu.tk_popup(event.x_root, event.y_root)


class File(Menu):
    def __init__(self, master, action, **kw):
        super().__init__(master, **kw)
        self.master = master
        self.action = action
        self.generate_menu()

    def _load_icon(self):
        self._new_file_icon = PhotoImage(file='icons/new_file.gif')
        self._open_file_icon = PhotoImage(file='icons/open_file.gif')
        self._save_file_icon = PhotoImage(file='icons/save.gif')

    def _create_menu(self):
        self.add_command(label='New', accelerator='Ctrl+N', command=self.action.new_file,
                         compound='left', image=self._new_file_icon, underline=0)
        self.add_command(label='Open', accelerator='Ctrl+O', command=self.action.open_file,
                         compound='left', image=self._open_file_icon, underline=0)
        self.add_command(label='Save', accelerator='Ctrl+S', command=self.action.save,
                         compound='left', image=self._save_file_icon, underline=0)
        self.add_command(
            label='Save as', accelerator='Shift+Ctrl+S', command=self.action.save_as)
        self.add_separator()
        self.add_command(label='Exit', accelerator='Alt+F4',
                         command=self.action.exit_editor)

    def generate_menu(self):
        self._load_icon()
        self._create_menu()


class Edit(Menu):
    def __init__(self, master, action, **kw):
        super().__init__(master, **kw)
        self.action = action
        self.generate_menu()

    def _load_icon(self):
        self._cut_icon = PhotoImage(file='icons/cut.gif')
        self._copy_icon = PhotoImage(file='icons/copy.gif')
        self._paste_icon = PhotoImage(file='icons/paste.gif')
        self._undo_icon = PhotoImage(file='icons/undo.gif')
        self._redo_icon = PhotoImage(file='icons/redo.gif')

    def _create_menu(self):
        self.add_command(label='Undo', accelerator='Ctrl+Z',
                         compound='left', image=self._undo_icon, command=self.action.undo)
        self.add_command(label='Redo', accelerator='Ctrl+Y',
                         compound='left', image=self._redo_icon, command=self.action.redo)
        self.add_separator()
        self.add_command(label='Cut', accelerator='Ctrl+X',
                         compound='left', image=self._cut_icon, command=self.action.cut)
        self.add_command(label='Copy', accelerator='Ctrl+C',
                         compound='left', image=self._copy_icon, command=self.action.copy)
        self.add_command(label='Paste', accelerator='Ctrl+V',
                         compound='left', image=self._paste_icon, command=self.action.paste)
        self.add_separator()
        self.add_command(
            label='Find', underline=0, accelerator='Ctrl+F', command=self.action.find_text)
        self.add_separator()
        self.add_command(label='Select All', underline=7,
                         accelerator='Ctrl+A', command=self.action.select_all)

    def generate_menu(self):
        self._load_icon()
        self._create_menu()


class View(Menu):
    color_schemes = {
        'Default': '#000000.#FFFFFF',
        'Greygarious': '#83406A.#D1D4D1',
        'Aquamarine': '#5B8340.#D1E7E0',
        'Bold Beige': '#4B4620.#FFF0E1',
        'Cobalt Blue': '#ffffBB.#3333aa',
        'Olive Green': '#D1E7E0.#5B8340',
        'Night Mode': '#FFFFFF.#000000',
    }

    def __init__(self, master, action, **kw):
        super().__init__(master, **kw)
        self.master = master
        self.action = action
        self.show_cursor_info = IntVar()
        self.show_cursor_info.set(1)
        self.generate_menu()

    def highlight_line(self, interval=100):
        self.action.content_text.tag_remove("active_line", 1.0, "end")
        self.action.content_text.tag_add(
            "active_line", "inset linestart", "insert lineend+1c")
        self.action.content_text.after(interval, self.toggle_highlight)

    def undo_highlight(self):
        self.action.content_text.tag_remove("active_line", 1.0, "end")

    def toggle_highlight(self, event=None):
        if self.to_highlight_line.get():
            self.highlight_line()
        else:
            self.undo_highlight()

    def change_theme(self, event=None):
        selected_theme = self.theme_choice.get()
        fg_bg_colors = self.color_schemes.get(selected_theme)
        foreground_color, background_color = fg_bg_colors.split('.')
        self.action.content_text.config(
            background=background_color, fg=foreground_color)

    def show_cursor_info_bar(self):
        show_cursor_info_checked = self.show_cursor_info.get()
        if show_cursor_info_checked:
            self.action.cursor_info_bar.pack(
                expand='no', fill=None, side='right', anchor='se')
        else:
            self.action.cursor_info_bar.pack_forget()

    def generate_menu(self):
        self.add_checkbutton(
            label='Show Line Number', variable=self.action.show_line_number, command=self.action.update_line_numbers)
        self.add_checkbutton(
            label='Show Cursor Location at Bottom', variable=self.show_cursor_info, command=self.show_cursor_info_bar)
        self.to_highlight_line = BooleanVar()
        self.add_checkbutton(label='Highlight Current Line', onvalue=1, offvalue=0,
                             variable=self.to_highlight_line, command=self.toggle_highlight)
        self.theme_choice = StringVar()
        self.theme_choice.set('Default')
        self.themes_menu = Menu(self.master, tearoff=0)
        for k in sorted(self.color_schemes):
            self.themes_menu.add_radiobutton(
                label=k, variable=self.theme_choice, command=self.change_theme)
        self.add_cascade(label='Themes', menu=self.themes_menu)


class About(Menu):
    def __init__(self, master, action, **kw):
        super().__init__(master, **kw)
        self.action = action
        self.generate_menu()

    def generate_menu(self):
        self.add_command(
            label='Help', command=self.action.display_help_messagebox)
        self.add_command(
            label='About', command=self.action.display_about_messagebox)


def test():
    root = Window()
    menu_bar = Menu(root)
    root['menu'] = menu_bar

    file_menu = File(menu_bar, root, tearoff=0)
    menu_bar.add_cascade(label='File', menu=file_menu)

    edit_menu = Edit(menu_bar, root, tearoff=0)
    menu_bar.add_cascade(label='Edit', menu=edit_menu)

    view_menu = View(menu_bar, root, tearoff=0)
    menu_bar.add_cascade(label='View', menu=view_menu)

    about_menu = About(menu_bar, root, tearoff=0)
    menu_bar.add_cascade(label='About',  menu=about_menu)

    Popup(root)
    root.mainloop()


if __name__ == '__main__':
    test()