自定义的文本编辑器#
最终的效果图:
代码:
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()