Make notepad with PyQt5
2021/04/18 categories:TOOL| tags:TOOL|PyQt5|Python|
I made a notepad as a coding practice for PyQt5.
Functions to implement
The following four functions are implemented.
- Open and save the file
- Search text
- Text replacement
- Font settings
Show QMainWindow
First, create a QMainWindow class with QTextEdit set and display the window as shown in the code below. QTextEdit seems to implement functions such as context menu, undo, and redo as standard, so it seems that you do not need to implement these functions yourself.
import sys
from PyQt5 import QtWidgets, QtCore
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
super(MemoPad, self).__init__()
self.resize(400, 400)
self.setWindowTitle('Memo pad')
self.textEdit = QtWidgets.QTextEdit(self)
self.setCentralWidget(self.textEdit)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
memoPad = MemoPad()
memoPad.show()
app.exec()
Add menu bar
Add menus for files, edits, etc. to the menu bar. It implements the basic functions of a notepad, such as opening and saving files, and searching and replacing.
import sys
from PyQt5 import QtWidgets, QtCore
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
super(MemoPad, self).__init__()
self.resize(400, 400)
self.setWindowTitle('Memo pad')
self.textEdit = QtWidgets.QTextEdit(self)
self.setCentralWidget(self.textEdit)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
memoPad = MemoPad()
memoPad.show()
app.exec()
Implementation of file save and open function
Connect the save and open methods to the QAction triggered signal so that you can save and open the file when you click the Save or Open action.
Use QFileDialog.getOpenFileName to get the path of the file to open.
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
self.saveAction.triggered.connect(self.saveFile)
def saveFile(self):
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(None, 'Save file', '', 'Text file(*.txt)')
if fileName == '':
return
with open(fileName, mode='w') as f:
f.write( self.textEdit.toPlainText() )
Use QFileDialog.getSaveFileName to get the destination path.
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
self.openAction.triggered.connect(self.openFile)
def openFile(self):
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(None, 'Open file', '', 'Text file(*.txt)')
if fileName == '':
return
with open(fileName, mode='r') as f:
self.textEdit.setPlainText( f.read() )
Search text
To implement the text search function, create a FindDialog class that inherits the Dialog class and create a search window. The reason for inheriting Dialog is that it also uses this class as a replacement window. The Dialog class implements all the UI needed for search and replace, and the FindDialog class hides the UI for features not needed for search.
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.label_0 = QtWidgets.QLabel('Find what')
self.label_1 = QtWidgets.QLabel('Replace with')
self.lineEdit_0 = QtWidgets.QLineEdit()
self.lineEdit_1 = QtWidgets.QLineEdit()
self.button_0 = QtWidgets.QPushButton('Find next')
self.button_1 = QtWidgets.QPushButton('Find beafore')
self.button_2 = QtWidgets.QPushButton('Replace')
self.button_3 = QtWidgets.QPushButton('Replace all')
self.button_4 = QtWidgets.QPushButton('Cancel')
self.setLayout( QtWidgets.QGridLayout() )
self.layout().addWidget( self.label_0, 0, 0, 1, 1 )
self.layout().addWidget( self.label_1, 1, 0, 1, 1 )
self.layout().addWidget( self.lineEdit_0, 0, 1, 1, 1 )
self.layout().addWidget( self.lineEdit_1, 1, 1, 1, 1 )
self.layout().addWidget( self.button_0, 0, 2, 1, 1 )
self.layout().addWidget( self.button_1, 1, 2, 1, 1 )
self.layout().addWidget( self.button_2, 2, 2, 1, 1 )
self.layout().addWidget( self.button_3, 3, 2, 1, 1 )
self.layout().addWidget( self.button_4, 4, 2, 1, 1 )
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.button_0.clicked.connect(lambda : self.parent().findText(self.lineEdit_0.text()))
self.button_1.clicked.connect(lambda : self.parent().findText(self.lineEdit_0.text(), True))
self.button_2.clicked.connect(lambda : self.parent().replace( self.lineEdit_0.text(), self.lineEdit_1.text() ))
self.button_3.clicked.connect(lambda : self.parent().textEdit.replaceAll( self.lineEdit_0.text(), self.lineEdit_1.text() ))
self.button_4.clicked.connect(lambda : self.reject())
class FindDialog(Dialog):
def __init__(self, parent=None):
super(FindDialog, self).__init__(parent)
self.setWindowTitle('Find')
self.label_1.hide()
self.lineEdit_1.hide()
self.button_2.hide()
self.button_3.hide()
The search process is executed by findText of MemoPad class. Process the text data acquired by textEdit.toPlainText () and use str.find to search in the forward order and str.rfind to search in the reverse order. By storing the search result in self.findIndex of MemoPad class, it is possible to search from the text data after the position of the previous search result. Also, by getting the text edit cursor with textEdit.textCursor () and setting the position of the search result with setPosition, the search result is reflected in the UI.
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
self.findIndex = -1
self.replaceAction.triggered.connect(lambda : ReplaceDialog(self).show())
def findText(self, findText, reverse=False):
if findText == '':
return
text = self.textEdit.toPlainText()
if reverse:
self.findIndex = text.rfind( findText, 0, self.findIndex )
else:
self.findIndex = text.find( findText, self.findIndex + 1 )
if self.findIndex == -1:
return
textCursor = self.textEdit.textCursor()
textCursor.setPosition(self.findIndex)
textCursor.setPosition(self.findIndex + len(findText), QtGui.QTextCursor.KeepAnchor)
self.textEdit.setTextCursor(textCursor)
self.activateWindow()
Text replacement
The replacement UI inherits the Dialog class as well as the search, and implements to hide the UI unnecessary for replacement.
class ReplaceDialog(Dialog):
def __init__(self, parent=None):
super(ReplaceDialog, self).__init__(parent)
self.setWindowTitle('Replace')
self.button_1.hide()
Text replacement is a process that replaces the selected text if it matches the search string. Since the process of searching for text is performed after that process, the text to be replaced next will be selected in the search process.
class MemoPad(QtWidgets.QMainWindow):
def replace(self, findText, replaceText):
text = self.textEdit.toPlainText()
if findText == self.textEdit.textCursor().selectedText():
index = self.textEdit.textCursor().selectionStart()
replaced = text[ : index ] + replaceText + text[index + len(findText) : ]
self.textEdit.setPlainText(replaced)
self.findText(findText)
def replaceAll(self, findText, replaceText):
self.textEdit.setPlainText(
self.textEdit.toPlainText().replace(findText, replaceText)
)
Font settings
QFontDialog is provided for font settings, so implementation is easy. It can be implemented simply by displaying the QFontDialog and setting the specified font in TextEdit when OK is clicked.
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
self.fontAction.triggered.connect(self.changeFont)
def changeFont(self):
font, ok = QtWidgets.QFontDialog.getFont(self.textEdit.currentFont(), self)
if not ok:
return
self.textEdit.setFont(font)
Impressions
I think PyQt5 is convenient because you can create such a GUI application with only 133 lines. It would be nice to have dialogs as standard, such as file and font dialogs.
Source code
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
class MemoPad(QtWidgets.QMainWindow):
def __init__(self):
super(MemoPad, self).__init__()
self.findIndex = -1
self.resize(400, 400)
self.setWindowTitle('Memo pad')
self.textEdit = QtWidgets.QTextEdit(self)
self.setCentralWidget(self.textEdit)
self.fileMenu = self.menuBar().addMenu('File')
self.openAction = self.fileMenu.addAction('Open')
self.saveAction = self.fileMenu.addAction('Save')
self.editMenu = self.menuBar().addMenu('Edit')
self.findAction = self.editMenu.addAction('Find')
self.replaceAction = self.editMenu.addAction('Replace')
self.formatMenu = self.menuBar().addMenu('Format')
self.fontAction = self.formatMenu.addAction('Font')
self.openAction.triggered.connect(self.openFile)
self.saveAction.triggered.connect(self.saveFile)
self.findAction.triggered.connect(lambda : FindDialog(self).show())
self.replaceAction.triggered.connect(lambda : ReplaceDialog(self).show())
self.fontAction.triggered.connect(self.changeFont)
def openFile(self):
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(None, 'Open file', '', 'Text file(*.txt)')
if fileName == '':
return
with open(fileName, mode='r') as f:
self.textEdit.setPlainText( f.read() )
def saveFile(self):
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(None, 'Save file', '', 'Text file(*.txt)')
if fileName == '':
return
with open(fileName, mode='w') as f:
f.write( self.textEdit.toPlainText() )
def findText(self, findText, reverse=False):
if findText == '':
return
text = self.textEdit.toPlainText()
if reverse:
self.findIndex = text.rfind( findText, 0, self.findIndex )
else:
self.findIndex = text.find( findText, self.findIndex + 1 )
if self.findIndex == -1:
return
textCursor = self.textEdit.textCursor()
textCursor.setPosition(self.findIndex)
textCursor.setPosition(self.findIndex + len(findText), QtGui.QTextCursor.KeepAnchor)
self.textEdit.setTextCursor(textCursor)
self.activateWindow()
def replace(self, findText, replaceText):
text = self.textEdit.toPlainText()
if findText == self.textEdit.textCursor().selectedText():
index = self.textEdit.textCursor().selectionStart()
replaced = text[ : index ] + replaceText + text[index + len(findText) : ]
self.textEdit.setPlainText(replaced)
self.findText(findText)
def replaceAll(self, findText, replaceText):
self.textEdit.setPlainText(
self.textEdit.toPlainText().replace(findText, replaceText)
)
def changeFont(self):
font, ok = QtWidgets.QFontDialog.getFont(self.textEdit.currentFont(), self)
if not ok:
return
self.textEdit.setFont(font)
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.label_0 = QtWidgets.QLabel('Find what')
self.label_1 = QtWidgets.QLabel('Replace with')
self.lineEdit_0 = QtWidgets.QLineEdit()
self.lineEdit_1 = QtWidgets.QLineEdit()
self.button_0 = QtWidgets.QPushButton('Find next')
self.button_1 = QtWidgets.QPushButton('Find beafore')
self.button_2 = QtWidgets.QPushButton('Replace')
self.button_3 = QtWidgets.QPushButton('Replace all')
self.button_4 = QtWidgets.QPushButton('Cancel')
self.setLayout( QtWidgets.QGridLayout() )
self.layout().addWidget( self.label_0, 0, 0, 1, 1 )
self.layout().addWidget( self.label_1, 1, 0, 1, 1 )
self.layout().addWidget( self.lineEdit_0, 0, 1, 1, 1 )
self.layout().addWidget( self.lineEdit_1, 1, 1, 1, 1 )
self.layout().addWidget( self.button_0, 0, 2, 1, 1 )
self.layout().addWidget( self.button_1, 1, 2, 1, 1 )
self.layout().addWidget( self.button_2, 2, 2, 1, 1 )
self.layout().addWidget( self.button_3, 3, 2, 1, 1 )
self.layout().addWidget( self.button_4, 4, 2, 1, 1 )
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.button_0.clicked.connect(lambda : self.parent().findText(self.lineEdit_0.text()))
self.button_1.clicked.connect(lambda : self.parent().findText(self.lineEdit_0.text(), True))
self.button_2.clicked.connect(lambda : self.parent().replace( self.lineEdit_0.text(), self.lineEdit_1.text() ))
self.button_3.clicked.connect(lambda : self.parent().textEdit.replaceAll( self.lineEdit_0.text(), self.lineEdit_1.text() ))
self.button_4.clicked.connect(lambda : self.reject())
class FindDialog(Dialog):
def __init__(self, parent=None):
super(FindDialog, self).__init__(parent)
self.setWindowTitle('Find')
self.label_1.hide()
self.lineEdit_1.hide()
self.button_2.hide()
self.button_3.hide()
class ReplaceDialog(Dialog):
def __init__(self, parent=None):
super(ReplaceDialog, self).__init__(parent)
self.setWindowTitle('Replace')
self.button_1.hide()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
memoPad = MemoPad()
memoPad.show()
app.exec()