×

Search anything:

Simple Text Editor in Python

Binary Tree book by OpenGenus

Open-Source Internship opportunity by OpenGenus for programmers. Apply now.

In this article, we have created a simple text editor using Python. A text editor is a computer that lets users to edit or manipulate text according to their needs. Users can open files to read or edit. The files need to be the types supported on the text editor.

This text editor is created using the Tkinter library of Python. It is used for its ease of use and for the graphical representation and addition of functionalities.

This is a strong Python project for SDE Portfolio. Get the code running using the following command:

git clone https://github.com/OpenGenus/text-editor-python.git
cd text-editor-python
python main.py

Introduction

We have added simple functionalities for the text editor to perform, such as, creating new file, opening a file, and saving a file. They are included in the file menu and we can also use them with keyboard shortcuts.

This text editor can perform cut, copy, and paste actions. These functions are included in the edit menu and shortcuts can be applied for these as well.
The texts can be bold, italic, strikethrough, underlined and we can change the color of the texts. The toolbar contains these options. It also contains alignment actions. The texts can be aligned to the left, right, center, and justified.
A format menu is added for some extra functions. These are changing background color, changing all text color, a checkbutton for text wrap, and a search option for finding text.

Creating the Frame

We start by importing necessary libraries and declaring some variables. We are using Tkinter and PIL library. PIL is only used for handling images used for some icons. We set the title and the size of the frame.

from tkinter import *
from PIL import Image, ImageTk
from tkinter import filedialog
from tkinter import font
from tkinter import colorchooser

root = Tk()
root.title("Simple Text Editor")
root.geometry("1000x600")
root.resizable(True,True)

Here, we set some variable names that can be accessed globally and the values can be changed. Next we create a toolbar that will hold our buttons, a main frame, a textbox, vertical and horizontal scrollbars for the frame, a menu, and at the bottom a status update that will let us know the status of the file.

# set variable for open file name
global open_status_name
open_status_name = False

global selected
selected = False

# create a toolbar
toolbar_frame = Frame(root)
toolbar_frame.pack(fill=X, pady=5)

# create a main frame
my_frame = Frame(root)
my_frame.pack(pady=5)

# vertical scrollbar
ver_scroll = Scrollbar(my_frame)
ver_scroll.pack(side=RIGHT, fill=Y)

# horizontal scrollbar
hor_scroll = Scrollbar(my_frame, orient='horizontal')
hor_scroll.pack(side=BOTTOM, fill=X)

# create text box
my_text = Text(my_frame, width=80, height=20, font=("Helvetica", 16), selectbackground="lightgrey",
               selectforeground="black", undo=True,
               yscrollcommand=ver_scroll.set, xscrollcommand=hor_scroll.set, wrap="none")
my_text.pack()

# configure scrollbar
ver_scroll.config(command=my_text.yview)
hor_scroll.config(command=my_text.xview)

# create menu
my_menu = Menu(root)
root.config(menu=my_menu)

# add status bar to bottom
status_bar = Label(root, text='Ready        ', anchor=E)
status_bar.pack(fill=X, side=BOTTOM, ipady=15)

The scrollbars, textbox, and toolbar is set to the main frame. The scrollbars are configured to the built in command for vertical and horizontal view.

The Text Editor:

texteditor

File Menu

In the file menu, these functionalities are available; new file, open file, save, save as, and exit. For each function, a separate method is defined.

New File
For a new file, the title becomes 'Untitled' and the status bar gets updated to 'New File'.

# ***************************** File Menu ********************************

# create new file function
def new_file(e):
    # Delete previous file
    my_text.delete("1.0", END)
    # Update status bar
    root.title("Untitled")
    status_bar.config(text="New File        ")

    global open_status_name
    open_status_name = False

Open File
For the open file, we first delete the previous texts from the textbox. We define the file path and the types of files our text editor will open. Then we open the file to read. With that we update the status bar and the global variable that is needed. This global variable keeps the opened file
stored so that we can use it for other methods such as saving the file.

# open files
def open_file(e):
    my_text.delete("1.0", END)
    # Grab filename
    text_file = filedialog.askopenfilename(initialdir="E:/", title="Open File",
                filetypes=(
                    ("Text Files", "*.txt"),("HTML Files", "*.html"),
                    ("Python Files", "*.py"),("All Files", "*.*")
                ))
    # check to see if there is a file name
    if text_file:
        global open_status_name
        open_status_name = text_file

    # update status bar
    name = text_file
    status_bar.config(text=f'{name}        ')
    name = name.replace("E:/", "")
    root.title(f'{name}')

    # open file to read
    text_file = open(text_file, 'r')
    lines = text_file.read()
    # add file to textbox
    my_text.insert(END, lines)
    # close opened file
    text_file.close()

Save File
Before saving a file we first check if the file is already saved, otherwise we assign a name to the file. To save the file, we add the directory path and open the file to write so that we can save the changes. The global variable is passed through to check if a file is new or it already exists and is open.

def save_as_file(e):
    text_file = filedialog.asksaveasfilename(defaultextension=".*", initialdir="E:/", title="Save File", filetypes=(
                       ("Text Files", "*.txt"), ("HTML Files", "*.html"),
                       ("Python Files", "*.py"), ("All Files", "*.*")
                                    ))
    if text_file:
        name = text_file
        status_bar.config(text=f'Saved: {name}        ')
        name = name.replace("E:/", "")
        root.title(f'{name}')

    # save the file
    text_file = open(text_file, 'w')
    text_file.write(my_text.get(1.0, END))
    # close the file
    text_file.close()

def save_file(e):
    global open_status_name
    if open_status_name:
        text_file = open(open_status_name, 'w')
        text_file.write(my_text.get(1.0, END))
        # close the file
        text_file.close()

        status_bar.config(text=f'Saved: {open_status_name}        ')

    else:
        save_as_file(e)

Add File Menu and Keyboard Shortcuts
Adding these functions to the command of the File Menu functions. We are also binding them to keyboard shortcuts. An event is passed through every method to store the keyboard actions.

# add file menu
file_menu = Menu(my_menu, tearoff=False)
my_menu.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="New", command=new_file, accelerator="(Ctrl+n)")
file_menu.add_command(label="Open", command=open_file, accelerator="(Ctrl+o)")
file_menu.add_command(label="Save", command=save_file, accelerator="(Ctrl+s)")
file_menu.add_command(label="Save As", command=save_as_file)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=root.quit)

# file bindings
root.bind('<Control-Key-n>', new_file)
root.bind('<Control-Key-o>', open_file)
root.bind('<Control-Key-s>', save_file)

The filemenu also has the option to exit the program. It is added at the end of the menu and a quit command of the library is used for this action. We are adding a separator to separate this section from the rest.

File Menu:

filemenu

Edit Menu

In the edit menu, we have only included cut, copy, paste, undo, redo and select all function. A global variable is passed through the editmenu functions to store and access the selected text. Also, these methods have an event value that stores the keyboard actions.

Cut Text
For the 'cut' method, we select the text and then delete it. The clipboard is then cleared just to make sure there wasn't anything else stored. Then the selected text is added to the clipboard. For the key shortcut, a library method is used for the same function.

# ************************* Edit Menu *************************

# cut text
def cut_text(e):
    global selected
    # check keyboard shortcuts action
    if e:
        selected = root.clipboard_get()
    else:
        if my_text.selection_get():
            # grab selected text
            selected = my_text.selection_get()
            # delete selected text
            my_text.delete("sel.first", "sel.last")
            # clear the clipboard then append
            root.clipboard_clear()
            root.clipboard_append(selected)

Copy Text
The same library method as the 'cut' function is used in the 'copy' function for key shortcut. All the rest of the process is same as the 'cut' method, except for deleting the text. For 'copy' method, we don't delete the text, we just copy it to the clipboard and clear the board before that so that no previous value is stored.

# copy text
def copy_text(e):
    global selected
    # check keyboard shortcuts action
    if e:
        selected = root.clipboard_get()

    if my_text.selection_get():
        # grab selected text
        selected = my_text.selection_get()
        # clear the clipboard then append
        root.clipboard_clear()
        root.clipboard_append(selected)

Paste Text and Select All
The global value carries all the text stored in the clipboard. So we use this global value for the 'paste' method. We find the position our cursor is and where we want the text to be, then we insert the text.

We are defining a method to select all the text altogether. The 'sel' tag is added to select the text from start to end.

# paste text
def paste_text(e):
    global selected
    # check keyboard shortcuts action
    if e:
        selected = root.clipboard_get()

    else:
        if selected:
            position = my_text.index(INSERT)
            my_text.insert(position, selected)

# Select all
def select_all(e):
    # add sel tag to select all text
    my_text.tag_add('sel', '1.0', 'end')

Add Edit Menu and Keyboard Shortcuts
We are adding each methods as commands of the menu. A lamba function is used for passing events. For undo and redo, we are using built-in library methods. We are using a separator for separating the 'select all' section.

# add edit menu
edit_menu = Menu(my_menu, tearoff=False)
my_menu.add_cascade(label="Edit", menu=edit_menu)
edit_menu.add_command(label="Cut", command=lambda: cut_text(False), accelerator="(Ctrl+x)")
edit_menu.add_command(label="Copy", command=lambda: copy_text(False), accelerator="(Ctrl+c)")
edit_menu.add_command(label="Paste", command=lambda: paste_text(False), accelerator="(Ctrl+v)")
edit_menu.add_separator()
edit_menu.add_command(label="Undo", command=my_text.edit_undo, accelerator="(Ctrl+z)")
edit_menu.add_command(label="Redo", command=my_text.edit_redo, accelerator="(Ctrl+y)")
edit_menu.add_separator()
edit_menu.add_command(label="Select All", command=lambda: select_all(True), accelerator="(Ctrl+a)")

# edit bindings
root.bind('<Control-Key-x>', cut_text)
root.bind('<Control-Key-c>', copy_text)
root.bind('<Control-Key-v>', paste_text)

# select binding
root.bind('Control-A', select_all)
root.bind('Control-a', select_all)

We are binding the keyboard actions to shortcuts for cut, copy, paste, and select all.

Edit Menu:

editmenu

Format Menu

In the format menu, we are keeping changing the background color, changing all text color, wrapping text, and the search function.

Change Color
We are defining two functions to change the background color of the textbox and the color of all the text added to the textbox. The ColorChooser library is used for picking color of choice. We are adding '[1]' to generalize the hex value of the color.

# **************************** Format Menu ******************************

# change bg color
def bg_color():
    my_color = colorchooser.askcolor()[1]
    if my_color:
        my_text.config(bg=my_color)

# change all text color
def all_text_color():
    my_color = colorchooser.askcolor()[1]
    if my_color:
        my_text.config(fg=my_color)

Wrap Text
AS there is a value we can assign to for wrap, we don't need to put in much effort. We just need to configure the textbox to wrap= "word" or "none".

def wrap():
    if word_wrap.get() == True:
        my_text.config(wrap="word")
    else:
        my_text.config(wrap="none")

Find Text
To find a text, we use a different window over the frame. A box appears where we can put the word or words we want to find, and the found values are painted red. We set an event value for keyboard shortcut for this function. The search method searches from start to end to find the values. Two buttons are added for search and cancel.

# find text
def check(value, e):
    my_text.tag_remove('found', "1.0", "end")
    my_text.tag_config('found', foreground='red')
    list_of_words = value.split(' ')
    for word in list_of_words:
        index = "1.0"
        while index:
            index = my_text.search(word, index, nocase=1, stopindex=END)
            if index:
                lastIndex = '%s+%dc' % (index, len(word))
                my_text.tag_add('found', index, lastIndex)
                index = lastIndex

# cancel search
def cancel_search(value):
    my_text.tag_remove('found', "1.0", "end")
    value.destroy()
    return "break"

# create search box
def find_text(e):
    search = Toplevel(root)
    search.title(' Find Text ')
    search.transient(root)
    search.resizable(False, False)
    Label(search, text=' Find All:').grid(row=0, column=0, sticky='e')
    x = search.winfo_x()
    y = search.winfo_y()
    search.geometry("+%d+%d" % (x+500, y+300))
    entry_widget = Entry(search, width=25)
    entry_widget.grid(row=0, column=1, padx=2, pady=2, sticky='we')
    entry_widget.focus_set()
    
    Button(search, text='Search', underline=0,
           command=lambda: check(entry_widget.get(), e)).grid(row=0, column=2, sticky='e'+'w', padx=2, pady=5)
           
    Button(search, text='Cancel', underline=0,
           command=lambda: cancel_search(search)).grid(row=0, column=4, sticky='e'+'w', padx=2, pady=5)

Add Format Menu and Keyboard Shortcut
Creating the format menu by adding the commands of every function. The word wrap is set after a separator. We are using the checkbutton for word wrap. We are also binding the key for using the shortcut.

# add format menu
format_menu = Menu(my_menu, tearoff=False)
my_menu.add_cascade(label="Format", menu=format_menu)
format_menu.add_command(label="All Text Color", command=all_text_color)
format_menu.add_command(label="Background Color", command=bg_color)
format_menu.add_command(label="Search", command=find_text, accelerator="(Ctrl+f)")

format_menu.add_separator()
word_wrap = BooleanVar()
format_menu.add_checkbutton(label="Word Wrap", onvalue=True, offvalue=False, variable=word_wrap, command=wrap)

root.bind('<Control-Key-f>', find_text)

Format Menu:

formatmenu

Toolbar

Now we set the toolbar. We are adding the text styles, alignments and undo redo buttons on the toolbar. Images are added for the icons of these functions.

Bold
For bold, we configure the font weight to "bold". We use tag to bold the selected text.

# ***************************** ToolBar *****************************

# bold text
def bold():
    # create a font
    bold_font = font.Font(my_text, my_text.cget("font"))
    bold_font.configure(weight="bold")

    # configure a tag
    my_text.tag_configure("bold", font=bold_font)
    current_tags = my_text.tag_names("sel.first")

    # If state to see if a tag has been set
    if "bold" in current_tags:
        my_text.tag_remove("bold", "sel.first", "sel.last")

    else:
        my_text.tag_add("bold", "sel.first", "sel.last")

Italic
For Italic, we configure the font to slant = "italic". We use tag to make the selected text italic.

# italic text
def italic():
    # create a font
    italic_font = font.Font(my_text, my_text.cget("font"))
    italic_font.configure(slant="italic")

    # configure a tag
    my_text.tag_configure("italic", font=italic_font)
    current_tags = my_text.tag_names("sel.first")

    # If state to see if a tag has been set
    if "italic" in current_tags:
        my_text.tag_remove("italic", "sel.first", "sel.last")

    else:
        my_text.tag_add("italic", "sel.first", "sel.last")

Text Color
For changing the color, we use the same process. But we add a ColorChooser for picking the color of choice. Using tag we add color to the selected text.

# change text color
def text_color():
    # pick a color
    my_color = colorchooser.askcolor()[1]
    if my_color:
        status_bar.config(text=my_color)

        # color text
        color_font = font.Font(my_text, my_text.cget("font"))

        # configure a tag
        my_text.tag_configure("colored", font=color_font, foreground=my_color)
        current_tags = my_text.tag_names("sel.first")

        # If state to see if a tag has been set
        if "colored" in current_tags:
            my_text.tag_remove("colored", "sel.first", "sel.last")

        else:
            my_text.tag_add("colored", "sel.first", "sel.last")

Color Chooser:

color

Underline and Strikethrough text
For underlining and overstriking text, we only need to set the underline and overstrike variable of the font to True. The rest of the process is same as others.

# underline text
def underline():
    # create a font
    underline_font = font.Font(my_text, my_text.cget("font"))
    underline_font.configure(underline=True)

    # configure a tag
    my_text.tag_configure("underline", font=underline_font)
    current_tags = my_text.tag_names("sel.first")

    # If state to see if a tag has been set
    if "underline" in current_tags:
        my_text.tag_remove("underline", "sel.first", "sel.last")

    else:
        my_text.tag_add("underline", "sel.first", "sel.last")

# strike text
def strike():
    # create a font
    strike_font = font.Font(my_text, my_text.cget("font"))
    strike_font.configure(overstrike=True)

    # configure a tag
    my_text.tag_configure("overstrike", font=strike_font)
    current_tags = my_text.tag_names("sel.first")

    # If state to see if a tag has been set
    if "overstrike" in current_tags:
        my_text.tag_remove("overstrike", "sel.first", "sel.last")

    else:
        my_text.tag_add("overstrike", "sel.first", "sel.last")

Toolbar Functions:

toolbartext

Align Text
First of all, we define a function to remove all the align tags to make sure there is no allignment defined. This means the lines are justified. For every alignment method, we need to remove any alignment there may be and configure the tag to the alignment we want.

def remove_align_tags():
    current_tags = my_text.tag_names("sel.first")
    if "left" in current_tags:
        my_text.tag_remove("left", "sel.first", "sel.last")
    if "right" in current_tags:
        my_text.tag_remove("right", "sel.first", "sel.last")
    if "center" in current_tags:
        my_text.tag_remove("center", "sel.first", "sel.last")

def align_left():
    remove_align_tags()
    my_text.tag_configure("left", justify='left')
    my_text.tag_add("left", "sel.first", "sel.last")

def align_right():
    remove_align_tags()
    my_text.tag_configure("right", justify='right')
    my_text.tag_add("right", "sel.first", "sel.last")

def align_middle():
    remove_align_tags()
    my_text.tag_configure("center", justify='center')
    my_text.tag_add("center", "sel.first", "sel.last")

def align_justify():
    remove_align_tags()

Text Alignment(Left, Right, Center):

toolbartext-1
rightalign
centeralign

Creating Toolbar Buttons

All of the methods of toolbar are set on buttons. We use icon images to show these buttons and resize all of them to the same size. We are using grid instead of pack to have better control over the position of the buttons. We are adding the same amount of padding around the buttons to create some space among them.

# **************** Create Toolbar buttons *********************

# undo & redo button
undo_icon = ImageTk.PhotoImage(Image.open("icons/undo.png").resize((15,15), Image.LANCZOS))
undo_button = Button(toolbar_frame, borderwidth=0., image=undo_icon, command=my_text.edit_undo)
undo_button.grid(row=0, column=0, sticky=W, padx=8, pady=2)

redo_icon = ImageTk.PhotoImage(Image.open("icons/redo.png").resize((15,15), Image.LANCZOS))
redo_button = Button(toolbar_frame, borderwidth=0., image=redo_icon, command=my_text.edit_redo)
redo_button.grid(row=0, column=1, sticky=W, padx=8, pady=2)

# bold button
bold_icon = ImageTk.PhotoImage(Image.open("icons/bold.png").resize((15,15), Image.LANCZOS))
bold_button = Button(toolbar_frame, borderwidth=0., image=bold_icon, command=bold)
bold_button.grid(row=0, column=3, sticky=W, padx=8, pady=2)

# italic button
italic_icon = ImageTk.PhotoImage(Image.open("icons/italics.png").resize((15,15), Image.LANCZOS))
italic_button = Button(toolbar_frame, borderwidth=0., image=italic_icon, command=italic)
italic_button.grid(row=0, column=4, sticky=W, padx=8, pady=2)

# underline button
underline_icon = ImageTk.PhotoImage(Image.open("icons/underline.png").resize((15,15), Image.LANCZOS))
underline_button = Button(toolbar_frame, borderwidth=0., image=underline_icon, command=underline)
underline_button.grid(row=0, column=5, sticky=W, padx=8, pady=2)

# overstrike button
strike_icon = ImageTk.PhotoImage(Image.open("icons/strike.png").resize((15,15), Image.LANCZOS))
strike_button = Button(toolbar_frame, borderwidth=0., image=strike_icon, command=strike)
strike_button.grid(row=0, column=6, sticky=W, padx=8, pady=2)

# text color
color_icon = ImageTk.PhotoImage(Image.open("icons/color.png").resize((18,18), Image.LANCZOS))
color_text_button = Button(toolbar_frame, borderwidth=0., image=color_icon, command=text_color)
color_text_button.grid(row=0, column=7, padx=8, pady=2)

# align left
left_icon = ImageTk.PhotoImage(Image.open("icons/align_left.png").resize((18,18), Image.LANCZOS))
left_button = Button(toolbar_frame, borderwidth=0., image=left_icon, command=align_left)
left_button.grid(row=0, column=8, padx=8, pady=2)

# align right
right_icon = ImageTk.PhotoImage(Image.open("icons/align_right.png").resize((18,18), Image.LANCZOS))
right_button = Button(toolbar_frame, borderwidth=0., image=right_icon, command=align_right)
right_button.grid(row=0, column=9, padx=8, pady=2)

# align center
center_icon = ImageTk.PhotoImage(Image.open("icons/align_middle.png").resize((18,18), Image.LANCZOS))
center_button = Button(toolbar_frame, borderwidth=0., image=center_icon, command=align_middle)
center_button.grid(row=0, column=10, padx=8, pady=2)

# align justify
justify_icon = ImageTk.PhotoImage(Image.open("icons/align_justify.png").resize((18,18), Image.LANCZOS))
justify_button = Button(toolbar_frame, borderwidth=0., image=justify_icon, command=align_justify)
justify_button.grid(row=0, column=11, padx=8, pady=2)

At the end, we call the mainloop() function to keep the window running and listen to the events or actions we perform.

root.mainloop()

With this article at OpenGenus, you must have the complete idea of how to develop a Python project of a Simple Text Editor.

Simple Text Editor in Python
Share this