Python

How to Write a Simple Text Editor in PyQt5

This article will cover a guide on creating a simple text editor in Python3 and PyQt5. Qt5 is a set of cross-platform libraries written in C++, used mainly for creating rich graphical applications. PyQt5 provides Python bindings for the latest version of Qt5. All code samples in this article are tested with Python 3.8.2 and PyQt5 version 5.14.1 on Ubuntu 20.04.

Installing PyQt5 in Linux

To install PyQt5 in latest version of Ubuntu, run the command below:

$ sudo apt install python3-pyqt5

If you are using any other Linux distribution, search for the term “Pyqt5” in the package manager and install it from there. Alternatively, you can install PyQt5 from pip package manager using the command below:

$ pip install pyqt5

Note that in some distributions, you may have to use pip3 command to correctly install PyQt5.

Full Code

I am posting full code beforehand so that you can better understand context for individual code snippets explained later in the article. If you are familiar with Python and PyQt5, you can just refer to the code below and skip the explanation.

#!/usr/bin/env python3

import sys
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QHBoxLayout
from PyQt5.QtWidgets import QTextEdit, QLabel, QShortcut, QFileDialog, QMessageBox
from PyQt5.QtGui import QKeySequence
from PyQt5 import Qt

class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.file_path = None

        self.open_new_file_shortcut = QShortcut(QKeySequence('Ctrl+O'), self)
        self.open_new_file_shortcut.activated.connect(self.open_new_file)

        self.save_current_file_shortcut = QShortcut(QKeySequence('Ctrl+S'), self)
        self.save_current_file_shortcut.activated.connect(self.save_current_file)

        vbox = QVBoxLayout()
        text = "Untitled File"
        self.title = QLabel(text)
        self.title.setWordWrap(True)
        self.title.setAlignment(Qt.Qt.AlignCenter)
        vbox.addWidget(self.title)
        self.setLayout(vbox)

        self.scrollable_text_area = QTextEdit()
        vbox.addWidget(self.scrollable_text_area)

    def open_new_file(self):
        self.file_path, filter_type = QFileDialog.getOpenFileName(self, "Open new file",
                "", "All files (*)")
        if self.file_path:
            with open(self.file_path, "r") as f:
                file_contents = f.read()
                self.title.setText(self.file_path)
                self.scrollable_text_area.setText(file_contents)
        else:
            self.invalid_path_alert_message()

    def save_current_file(self):
        if not self.file_path:
            new_file_path, filter_type = QFileDialog.getSaveFileName(self, "Save this file
                        as..."
, "", "All files (*)")
            if new_file_path:
                self.file_path = new_file_path
            else:
                self.invalid_path_alert_message()
                return False
        file_contents = self.scrollable_text_area.toPlainText()
        with open(self.file_path, "w") as f:
            f.write(file_contents)
        self.title.setText(self.file_path)

    def closeEvent(self, event):
        messageBox = QMessageBox()
        title = "Quit Application?"
        message = "WARNING !!\n\nIf you quit without saving, any changes made to the file
                will be lost.\n\nSave file before quitting?"

       
        reply = messageBox.question(self, title, message, messageBox.Yes | messageBox.No |
                messageBox.Cancel, messageBox.Cancel)
        if reply == messageBox.Yes:
            return_value = self.save_current_file()
            if return_value == False:
                event.ignore()
        elif reply == messageBox.No:
            event.accept()
        else:
            event.ignore()

    def invalid_path_alert_message(self):
        messageBox = QMessageBox()
        messageBox.setWindowTitle("Invalid file")
        messageBox.setText("Selected filename or path is not valid. Please select a
                valid file."
)
        messageBox.exec()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.showMaximized()
    sys.exit(app.exec_())

Explanation

The first part of the code just imports modules that will be used throughout the sample:

import sys
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QHBoxLayout
from PyQt5.QtWidgets import QTextEdit, QLabel, QShortcut, QFileDialog, QMessageBox
from PyQt5.QtGui import QKeySequence
from PyQt5 import Qt

In the next part, a new class called “Window” is created that inherits from “QWidget” class. QWidget class provides commonly used graphical components in Qt. By using “super” you can ensure that the parent Qt object is returned.

class Window(QWidget):
    def __init__(self):
        super().__init__()

Some variables are defined in the next part. File path is set to “None” by default and shortcuts for opening a file using <CTRL+O> and saving a file using <CTRL+S> are defined using QShortcut class. These shortcuts are then connected to their respective methods that are called whenever a user presses the defined key combinations.

self.file_path = None

self.open_new_file_shortcut = QShortcut(QKeySequence('Ctrl+O'), self)
self.open_new_file_shortcut.activated.connect(self.open_new_file)

self.save_current_file_shortcut = QShortcut(QKeySequence('Ctrl+S'), self)
self.save_current_file_shortcut.activated.connect(self.save_current_file)

Using QVBoxLayout class, a new layout is created to which child widgets will be added. A center-aligned label is set for the default file name using QLabel class.

vbox = QVBoxLayout()
text = "Untitled File"
self.title = QLabel(text)
self.title.setWordWrap(True)
self.title.setAlignment(Qt.Qt.AlignCenter)
vbox.addWidget(self.title)
self.setLayout(vbox)

Next, a text area is added to the layout using a QTextEdit object. The QTextEdit widget will give you an editable, scrollable area to work with. This widget supports typical copy, paste, cut, undo, redo, select-all etc. keyboard shortcuts. You can also use a right click context menu within the text area.

self.scrollable_text_area = QTextEdit()
vbox.addWidget(self.scrollable_text_area)

The “open_new_fie” method is called when a user completes <CTRL+O> keyboard shortcut. QFileDialog class presents a file picker dialog to the user. File path is determined after a user selects a file from the picker. If file path is valid, text content is read from the file and set to QTextEdit widget. This makes text visible to the user, changes the title to the new filename and completes the process of opening a new file. If for some reason, file path cannot be determined, an “invalid file” alert box is shown to the user.

def open_new_file(self):
    self.file_path, filter_type = QFileDialog.getOpenFileName(self, "Open new file", "",
        "All files (*)")
    if self.file_path:
        with open(self.file_path, "r") as f:
            file_contents = f.read()
            self.title.setText(self.file_path)
            self.scrollable_text_area.setText(file_contents)
    else:
        self.invalid_path_alert_message()

The “save_current_file” method is called whenever a user completes <CTRL+S> keyboard shortcut. Instead of retrieving a new file path, QFileDialog now asks the user to provide a path. If file path is valid, contents visible in QTextEdit widget are written to the full file path, otherwise an “invalid file” alert box is shown. Title of the file currently being edited is also changed to the new location provided by the user.

def save_current_file(self):
    if not self.file_path:
        new_file_path, filter_type = QFileDialog.getSaveFileName(self, "Save this file
                as..."
, "", "All files (*)")
        if new_file_path:
            self.file_path = new_file_path
        else:
            self.invalid_path_alert_message()
            return False
    file_contents = self.scrollable_text_area.toPlainText()
    with open(self.file_path, "w") as f:
        f.write(file_contents)
    self.title.setText(self.file_path)

The “closeEvent” method is part of the PyQt5 event handling API. This method is called whenever a user tries to close a window using the cross button or by hitting <ALT+F4> key combination. On firing of the close event, the user is shown a dialog box with three choices: “Yes”, “No” and “Cancel”. “Yes” button saves the file and closes the application while “No” button closes the file without saving the contents. “Cancel” button closes the dialog box and takes user back to the application.

def closeEvent(self, event):
    messageBox = QMessageBox()
    title = "Quit Application?"
    message = "WARNING !!\n\nIf you quit without saving, any changes made to the file will
        be lost.\n\nSave file before quitting?"

       
    reply = messageBox.question(self, title, message, messageBox.Yes | messageBox.No |
        messageBox.Cancel, messageBox.Cancel)
    if reply == messageBox.Yes:
        return_value = self.save_current_file()
        if return_value == False:
            event.ignore()
    elif reply == messageBox.No:
        event.accept()
    else:
        event.ignore()

The “invalid file” alert box doesn’t have any bells and whistles. It just conveys the message that file path couldn’t be determined.

def invalid_path_alert_message(self):
    messageBox = QMessageBox()
    messageBox.setWindowTitle("Invalid file")
    messageBox.setText("Selected filename or path is not valid. Please select a valid file.")
    messageBox.exec()

Lastly, the main application loop for event handling and drawing of widgets is started by using the “.exec_()” method.

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.showMaximized()
    sys.exit(app.exec_())

Running the App

Just save full code to a text file, set the file extension to “.py”, mark the file executable and run it to launch the app. For instance, if the file name is “simple_text_editor.py”, you need to run following two commands:

$ chmod +x simple_text_editor.py
$ ./simple_text_editor.py

Things You can Do to Improve the Code

The code explained above works fine for a bare-bones text editor. However, it may not be useful for practical purposes as it lacks many features commonly seen in good text editors. You can improve the code by adding new features like line numbers, line highlighting, syntax highlighting, multiple tabs, session saving, toolbar, dropdown menus, buffer change detection etc.

Conclusion

This article mainly focuses on providing a starting ground for creating PyQt apps. If you find errors in the code or want to suggest something, feedback is welcome.

About the author

Nitesh Kumar

Nitesh Kumar

I am a freelancer software developer and content writer who loves Linux, open source software and the free software community. I maintain a blog that lists new Android deals everyday.