Develop a text editor app with Python

Develop a text editor app with Python

Learn Tkinter by making a Text editor app

If you have come to this blog and are completely new to tkinter or a beginner in Python, then this blog is exactly for you.

What are we going to learn today -

  • Today we will create a basic text editor using the tkinter module in Python.

  • The editor offers a menu bar with the functionalities such as:

    • opening files

    • saving files

    • text editing commands like cut, copy, paste, undo, redo

    • font customization

    • theme switching - Dark and Light mode

This is how the end result would look like:

Three reasons why you should use Tkinter

1. Cross-platform

Applications developed with tkinter work on Windows, macOS, and Linux without any modifications.

2. Rich Widget Set

Tkinter provides an array of widgets (like buttons, labels, text areas, and canvases) and utilities for event handling, geometry management, etc.

3. Easy to Use

For beginners, tkinter offers a more gentle introduction to GUI programming than some other libraries because of its simplicity.

Three reasons when to Use Tkinter?

1. Simple Tools and Applications

If you're developing a small utility or a tool for personal use, or you're prototyping something quickly, tkinter is a good fit.

2. Educational Purpose

For teaching GUI programming or Python programming in general, tkinter serves as a beginner-friendly option.

3. Cross-platform Desktop Applications

If you aim to develop a lightweight application that should run on multiple platforms without much hassle, tkinter can be beneficial.

Three reasons when Not to Use Tkinter?

1. Modern Look and Feel

If you want an application with a very modern UI, you might find tkinter a bit lacking, as its widgets have a more traditional appearance.

2. Intensive Graphics

For applications that require advanced graphics, animations, or games, libraries like PyQt, Kivy, or Pygame might be better suited.

3. Complex Applications

For larger, more complex applications with a lot of features, more comprehensive frameworks like PyQt or wxPython might be more appropriate.

Before we begin - Best Practices

  1. Believe in yourself, and don't give up mid-way, if there is a new concept, research about it or simply contact me - Mustafa Saifee

  2. Complete the project app and upload it to your own GitHub repo and add it to your resume with the link to the repo

  3. And most importantly, type everything manually without copy-pasting the code.

Let's dive into action

Installing tkinter

tkinter is normally bundled with most standard Python installations, so there's usually no need to install it separately. However, in some scenarios, if it's missing, here is the command -

Windows Installation steps

pip install tk

Linux Installation steps

For Debian-based:

sudo apt-get install python3-tk

For Red Hat-based Linux:

sudo yum install python3-tkinter # For older versions with yum

Importing the libraries

We will start the development of the application, in your preferred text editor, create a new file called app.py and open the file for further development.

import tkinter as tk
from tkinter import filedialog, messagebox

We have imported the necessary modules, and the other imports (filedialog and messagebox) help in dialog box functionalities like opening files or showing alerts.

Calling the main function:

This code block initializes the root window, creates an instance of the TextEditor, and then enters the main event loop, waiting for user input.

if __name__ == "__main__":
    root = tk.Tk()
    editor = TextEditor(root)
    root.mainloop()

This is how your code should look now:

import tkinter as tk
from tkinter import filedialog, messagebox

# All the rest of the code will be here
# We will write more classes and functions which will be between the two code pieces

if __name__ == "__main__":
    root = tk.Tk()
    editor = TextEditor(root)
    root.mainloop()

Creating the class and menu_bar

    class TextEditor:
        def __init__(self, root):
            self.root = root
            self.root.title("Tkinter Text Editor")

            # Menu Bar
            self.menu_bar = tk.Menu(root)
            self.root.config(menu=self.menu_bar)
  • TextEditor class will represent the main application or the text editor itself.
  • self.root.title("Tkinter Text Editor") - This line sets the title of the main window (root) to "Tkinter Text Editor". When you run the application, the title bar of the window will display this text.
  • self.menu_bar = tk.Menu(root) - This creates a new Menu widget, which is the main menu bar of the application. This menu bar will be placed at the top of the root window and will later have various menu items (like File, Edit, etc.) added to it.
  • self.root.config(menu=self.menu_bar) - This line associates the newly created menu bar (self.menu_bar) with the main window (self.root). The config function is used to configure widget properties, and in this case, it's setting the main window's menu to be self.menu_bar.

Everything we code henceforth should be inside class TextEditor. So maintain the indentation and write the code one after the other as instructed.

Designing the File Menu components:

We want the file submenu to have New, Open, Save, SaveAs, and Exit feature options.

NOTE: This is inside the def __init__(self, root): function.

    # File Menu
    self.file_menu = tk.Menu(self.menu_bar, tearoff=0)
    self.menu_bar.add_cascade(label="File", menu=self.file_menu)
    self.file_menu.add_command(label="New", command=self.new_file)
    self.file_menu.add_command(label="Open", command=self.open_file)
    self.file_menu.add_command(label="Save", command=self.save_file)
    self.file_menu.add_command(label="Save As", command=self.save_as_file)
    self.file_menu.add_separator()
    self.file_menu.add_command(label="Exit", command=root.quit)
  • tk.Menu(self.menu_bar, tearoff=0)- This menu will become a dropdown list under the main menu bar (self.menu_bar). The tearoff=0 argument prevents the menu from being separated into its own window when dragged (a behavior seen in some older systems).
  • self.menu_bar.add_cascade(label="File", menu=self.file_menu) - Here, the "File" submenu (self.file_menu) is added to the main menu bar (self.menu_bar). The add_cascade function is used to add a submenu to a menu. The label "File" is what you'll see in the main menu bar.
  • self.file_menu.add_command(label="xxx", command=self.xxx_file) - This adds a menu item labeled "xxx" to the "File" submenu. When this "xxx" option is selected, the function self.xxx_file will be called. The add_command function is used to add an actionable menu item. Replacing xxx with New or Open will give the desired output
  • self.file_menu.add_separator() - This adds a separator line in the "File" submenu. Just a visual element.
  • self.file_menu.add_command(label="Exit", command=root.quit) - Lastly, this adds an "Exit" option to the "File" submenu. When this option is selected, the root.quit function will be called, closing the application.

Designing the Edit Menu components:

The edit menu can have functions like Cut, Copy, Paste, Undo and Redo.

NOTE: This is inside the def __init__(self, root): function.

    # Edit Menu
    self.edit_menu = tk.Menu(self.menu_bar, tearoff=0)
    self.menu_bar.add_cascade(label="Edit", menu=self.edit_menu)
    self.edit_menu.add_command(label="Cut", command=self.cut_text)
    self.edit_menu.add_command(label="Copy", command=self.copy_text)
    self.edit_menu.add_command(label="Paste", command=self.paste_text)
    self.edit_menu.add_separator()
    self.edit_menu.add_command(label="Undo", command=self.undo_text)
    self.edit_menu.add_command(label="Redo", command=self.redo_text)
  • As discussed previously, self.menu_bar.add_cascade() will add the Edit submenu to the Menu bar.
  • self.edit_menu.add_command() will add respective Cut, Copy, Paste, Undo and Redo functions to the Edit menu
  • add_separator() funtion is a visual line separator.

Designing the View Menu and it's Submenu components:

TheView Menu would be a little special than the other menus', we would want to add two more expandable submenus to the View Menu. They are Font Submenu and Theme Submenu.

For the Font Submenu, we will add font options like "Times", "Arial", "Helvetica".

For the Theme Submenu, we will add two options to switch between Light theme and Dark theme.

NOTE: This is inside the def __init__(self, root): function.

    # View Menu
    self.view_menu = tk.Menu(self.menu_bar, tearoff=0)
    self.menu_bar.add_cascade(label="View", menu=self.view_menu)

    # Font submenu
    self.font_var = tk.StringVar(value="Times")
    self.fonts = ["Times", "Arial", "Helvetica"]
    self.font_menu = tk.Menu(self.view_menu, tearoff=0)
    for font in self.fonts:
        self.font_menu.add_radiobutton(label=font, variable=self.font_var, command=self.change_font)
    self.view_menu.add_cascade(label="Font", menu=self.font_menu)

    # Theme submenu
    self.theme_var = tk.StringVar(value="Light")
    self.themes = {
        "Light": {"bg": "white", "fg": "black"},
        "Dark": {"bg": "black", "fg": "white"}
    }
    self.theme_menu = tk.Menu(self.view_menu, tearoff=0)
    for theme, _ in self.themes.items():
        self.theme_menu.add_radiobutton(label=theme, variable=self.theme_var, command=self.change_theme)
    self.view_menu.add_cascade(label="Theme", menu=self.theme_menu)

Font

  • font_var = tk.StringVar(value="Times") - We set the default font to "Times".
  • self.fonts - We provide the available fonts
  • font_menu.add_radiobutton(label=font, variable=self.font_var, command=self.change_font) - For each font in the self.fonts list, a radio button is added to self.font_menu. When a particular font is chosen, self.font_var will be updated to that font, and the self.change_font function will be executed to change the editor's font.

Theme

  • theme_var = tk.StringVar(value="Light") - Setting the default theme to Light
  • self.themes= {} - A dictionary self.themes is defined with two themes, "Light" and "Dark". Each theme has background (bg) and foreground (fg) color values.
  • self.theme_menu.add_radiobutton(label=theme, variable=self.theme_var, command=self.change_theme) - For each theme in self.themes, a radio button is added to self.theme_menu. When a theme is selected, self.theme_var will be updated to that theme, and the self.change_theme function will be executed to change the editor's theme.

Designing the Help Menu

Our Help menu would simply have an About command which will trigger a window pop-up with some introductory text.

NOTE: This is inside the def __init__(self, root): function.

    # Help Menu
    self.help_menu = tk.Menu(self.menu_bar, tearoff=0)
    self.menu_bar.add_cascade(label="Help", menu=self.help_menu)
    self.help_menu.add_command(label="About", command=self.show_about)

In the above code we simply add the Help Menu to the Main Menu bar, and add a cascaded option of About to the Help menu. The self.show_about calls the show_about function.

Designing the Text Area, Scrollbar and the Status bar

Text Area and Scrollbar - This section is about creating a text widget (or text area) where users can input and edit text, and an associated vertical scrollbar to scroll through the text if it exceeds the visible area of the widget.

Status bar - A status bar is generally a small section at the bottom of a GUI application that provides quick info or status updates. In this case, it shows the current line and column position of the cursor in the text widget.

NOTE: This is inside the def __init__(self, root): function.

    # Text Area and Scrollbar
    self.text_scroll = tk.Scrollbar(root)
    self.text_scroll.pack(side=tk.RIGHT, fill=tk.Y)

    self.text_area = tk.Text(root, yscrollcommand=self.text_scroll.set, wrap=tk.WORD, undo=True, font=(self.font_var.get(), 12))
    self.text_area.pack(fill=tk.BOTH, expand=1)

    self.text_scroll.config(command=self.text_area.yview)
    self.text_area.bind('<KeyRelease>', self.update_status)

    # Status Bar
    self.status = tk.Label(root, text='Line: 1 | Column: 1', anchor=tk.W)
    self.status.pack(side=tk.BOTTOM, fill=tk.X)

Scrollbar

  • We start of by Scrollbar initialization - tk.Scrollbar(root)
  • self.text_scroll.pack() - The pack() function packs (or places) the scrollbar to the right side (tk.RIGHT) of the parent widget (root). The fill=tk.Y means that the scrollbar will fill the entire vertical space of its parent container.

Text Area

  • tk.Text(root, yscrollcommand=self.text_scroll.set, wrap=tk.WORD, undo=True, font=(self.font_var.get(), 12)) -
    • yscrollcommand=self.text_scroll.set: This links the scrollbar's movement to the text widget's vertical scrolling.
    • wrap=tk.WORD: Text will wrap at word boundaries (and not break words in half).
    • undo=True: Enables the undo feature.
    • font=(self.font_var.get(), 12): Sets the initial font of the text to the value in self.font_var (which defaults to "Times") with a font size of 12.
  • Extra space allotment for the text area using the expand=1 - self.text_area.pack(fill=tk.BOTH, expand=1)

Linking Text Area with Scrollbar

  • Then we link the scrollbar to the text area - self.text_scroll.config(command=self.text_area.yview)
  • self.text_area.bind('<KeyRelease>', self.update_status) - The text widget is bound to the self.update_status function such that every time a key is released within the text widget, the function is called. This is likely used to update the status bar with the current line and column position (as seen in the next section).

Status Bar

  • tk.Label(root, text='Line: 1 | Column: 1', anchor=tk.W) - A label widget is initialized with a default text showing the cursor is at Line 1, Column 1. This label is assigned to the variable self.status. The anchor=tk.W ensures the text aligns to the left (West).
  • pack(side=tk.BOTTOM, fill=tk.X) - This function places the label at the bottom (tk.BOTTOM) of the parent widget (root). The fill=tk.X ensures the label fills the entire horizontal space of its parent container.

Defining all the sub-components and functions

We have completeted the application's interface and visual options. Now we need to define all the functions that these components are calling.

Henceforth Rest of the code is outside the def __init__(self, root): function but inside the class TextEditor.

Defining the update_status() function for the Status Bar

NOTE: This is inside the class TextEditor as a separate function.

    def update_status(self, event=None):
        row, col = self.text_area.index(tk.INSERT).split('.')
        self.status.config(text=f'Line: {row} | Column: {col}')
  • self.text_area.index(tk.INSERT): This gets the current position of the insertion cursor in the text_area widget. The insertion cursor is the blinking cursor indicating where the next characters will be inserted when you type.
  • The position is returned in the format row.column. For example, if the insertion cursor is at the third row and fifth column, the index method would return the string "3.5".
  • .split('.'): This splits the returned string into two parts at the dot. Using the previous example, "3.5" would be split into ["3", "5"].
  • row, col = ...: The split result is then unpacked into two separate variables: row and col.

Defining the change_font() function for the View menu

NOTE: This is inside the class TextEditor as a separate function.

    def change_font(self):
        self.text_area.config(font=(self.font_var.get(), 12))
  • self.text_area.config(): This method is used to modify the configurations (or attributes) of a Tkinter widget, in this case, the text_area widget which is of type tk.Text.
  • font=(self.font_var.get(), 12): The font attribute takes a tuple, where the first element is the font name and the second element is the font size.
  • self.font_var.get(): This retrieves the current value stored in the self.font_var variable, which is of type tk.StringVar(). In the context of the program you provided, the user can choose a font from a menu, and when they do, the value of self.font_var gets set to the name of the chosen font (e.g., "Times", "Arial", or "Helvetica").
  • 12: This is the font size. The method sets the font size to 12 points regardless of which font is chosen.

Defining the change_theme() function for the View menu

NOTE: This is inside the class TextEditor as a separate function.

    def change_theme(self):
        theme = self.theme_var.get()
        bg_color, fg_color = self.themes[theme]["bg"], self.themes[theme]["fg"]
        self.text_area.config(bg=bg_color, fg=fg_color)
  • The purpose of this method is to adjust the theme of the text_area widget, specifically changing its background (bg) and foreground (fg) colors.
  • The current value of the self.theme_var is fetched and assigned to the variable theme. The self.theme_var is an instance of tk.StringVar(), which is a special type of string variable in Tkinter used for holding string values. In the provided context, self.theme_var holds the name of the currently selected theme (e.g., "Light" or "Dark").
  • self.themes is a dictionary that holds the details for each theme, specifically their background (bg) and foreground (fg) colors.
  • self.themes[theme] fetches the dictionary of the specific theme (either "Light" or "Dark" in this case).
  • ["bg"] and ["fg"] retrieve the background and foreground colors respectively for the selected theme.
  • bg=bg_color: Sets the background color of the text_area to the value of bg_color.
  • fg=fg_color: Sets the foreground (text) color of the text_area to the value of fg_color.

Defining the show_about() function for the Help menu

NOTE: This is inside the class TextEditor as a separate function.

        def show_about(self):
            messagebox.showinfo("About", "This is a simple Text Editor application using tkinter!")
  • messagebox.showinfo: This is a function from the messagebox module in tkinter that displays an informational message to the user in a pop-up dialog box.
  • "About": This is the title of the pop-up dialog box.
  • "This is a simple Text Editor application using tkinter!": This is the message/content that will be displayed within the pop-up dialog box.

Defining the new, open, save, save_as functions for the File menu

  • new_file is for starting a fresh, new document.
  • open_file is for opening an existing document.
  • save_file is for saving the current document, either updating the existing file or prompting for a filename if it's a new document.
  • save_as_file is for saving the current document under a new name or location.

NOTE: This is inside the class TextEditor as separate functions.

    def new_file(self):
        self.text_area.delete(1.0, tk.END)

    def open_file(self):
        file_path = filedialog.askopenfilename()
        if file_path:
            with open(file_path, 'r') as file:
                self.text_area.delete(1.0, tk.END)
                self.text_area.insert(tk.INSERT, file.read())

    def save_file(self):
        file_path = filedialog.asksaveasfilename(defaultextension=".txt")
        if file_path:
            with open(file_path, 'w') as file:
                file.write(self.text_area.get(1.0, tk.END))

    def save_as_file(self):
        file_path = filedialog.asksaveasfilename(defaultextension=".txt")
        if file_path:
            with open(file_path, 'w') as file:
                file.write(self.text_area.get(1.0, tk.END))

new_file()

  • self.text_area.delete(1.0, tk.END): This line clears the entire text area. The delete method of the Text widget removes content. Here, it removes content starting from the very first character (1.0 signifies the first line and the zeroth character of that line) up to the end (tk.END).

open_file()

  • filedialog.askopenfilename(): This opens a file dialog where users can select a file to open. It then returns the path to the selected file.
  • The if statement checks if a file was actually selected (i.e., the user did not cancel the file dialog).
  • If a file was selected, it's opened in reading mode ('r').
  • The current content of the text_area is then cleared using delete.
  • The content of the opened file is then read using file.read() and inserted into the text_area.

save_file()

  • save_file function: This function is for saving the content of the text editor to a file.
  • It function opens a dialog allowing the user to specify where and with what name they wish to save the file. If no extension is provided by the user, .txt is used by default.
  • If a location and filename are chosen, the content of the text_area is written to this file.

save_as_file()

  • save_as_file method: This method is similar to the save_file method but is typically used to save the file with a new name or at a different location.
  • Like the save_file method, it opens a dialog for the user to select a location and filename.
  • And like the save_file method, if a location and filename are chosen, the content of the text_area is saved to this new location.

Defining the cut, copy, paste, undo, redo functions for the Edit menu

  • cut_text cuts the selected text.
  • copy_text copies the selected text.
  • paste_text pastes the clipboard content.
  • undo_text undoes the last change.
  • redo_text redoes a previously undone change.

NOTE: This is inside the class TextEditor as separate functions.

    def cut_text(self):
        self.text_area.event_generate("<<Cut>>")

    def copy_text(self):
        self.text_area.event_generate("<<Copy>>")

    def paste_text(self):
        self.text_area.event_generate("<<Paste>>")

    def undo_text(self):
        self.text_area.edit_undo()

    def redo_text(self):
        self.text_area.edit_redo()

cut_text()

  • cut_text method: This method is designed to cut (remove and copy to clipboard) the currently selected text in the text editor.
  • self.text_area.event_generate("<<Cut>>"): The event_generate method is used to simulate a specific event on a widget. In this case, the "<<Cut>>" event is generated on the text_area, which is equivalent to performing a cut operation on the selected text.

copy_text()

  • copy_text method: This method copies the currently selected text in the text editor to the clipboard without removing it.
  • self.text_area.event_generate("<<Copy>>"): This line simulates the "<<Copy>>" event on the text_area, copying the selected text to the clipboard.

paste_text()

  • paste_text method: This method is for pasting the content currently on the clipboard into the text editor at the current cursor location.
  • self.text_area.event_generate("<<Paste>>"): This simulates the "<<Paste>>" event on the text_area, pasting the clipboard content at the cursor's position.

undo_text()

  • undo_text method: This method undoes the most recent change in the text editor.
  • self.text_area.edit_undo(): The edit_undo method of the Text widget is called to undo the last change. It's important to note that the undo attribute of the Text widget must be set to True for this method to work, which it was in the previously provided code.

redo_text()

  • redo_text method: This method redoes a change that was previously undone in the text editor.
  • self.text_area.edit_redo(): The edit_redo method of the Text widget is called to redo the previously undone change. Like the undo operation, the undo attribute of the Text widget must be enabled.

If you wish to have the complete code, it is available at - Click Here

That was fun. Finally, we have completed the text editor. So let's execute it.

Running the text editor app

Terminal

On the terminal type the following:

python app.py

If the above command doesn't work replace python with py OR python3

Visual Studio Code

From the Menu bar, click on Run > Run Without Debugging

OR

Press Ctrl + F5

I hope this tutorial was helpful and you learned the basic concepts of Tkinter library.

If you have any feedback, suggestion or request, reach out to me -

Linkedin: linkedin.com/in/saifeemustafa

Twitter: twitter.com/mustafasaifee_