*Read part two here, or start from the beginning!*
In part one of this tutorial series on Building a text editor with PyQt, we built a basic text editor skeleton and already added features for file management, list insertion, undo/redo and more. In part two, we turned our program into a rich-text editor by adding actions for text-formatting. In the third (and subsequently fourth) part of this series, we’ll be adding some slick extensions to our text editor for:
- Finding and replacing text
- Inserting an image
- Word and symbol count
- Creating and managing tables
- Inserting time and date
This part will deal with the first three extensions and in the fourth and final part I’ll discuss the remaining two.
Directory structure
For most of the above actions, we’ll be creating dialog classes in separate files, meaning we need a new folder for all of these new files. Create a folder in your working directory called “ext” (for extensions) and create an empty file called __init__.py
within it. This will turn our folder into a Python package.
Your working directory should look somewhat like this now:
writer.py
icons/
lots of icons
ext/
__init__.py
Find-and-replace
First up, we’ll handle our find-and-replace dialog. PyQt unfortunately has no methods of its own for finding and replacing text in a QTextEdit, therefore we’ll be doing a lot ourselves for this one.
In your ext
folder, create a new file called find.py
:
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
import re
class Find(QtGui.QDialog):
def __init__(self, parent = None):
QtGui.QDialog.__init__(self, parent)
self.parent = parent
self.lastMatch = None
self.initUI()
def initUI(self):
# Button to search the document for something
findButton = QtGui.QPushButton("Find",self)
findButton.clicked.connect(self.find)
# Button to replace the last finding
replaceButton = QtGui.QPushButton("Replace",self)
replaceButton.clicked.connect(self.replace)
# Button to remove all findings
allButton = QtGui.QPushButton("Replace all",self)
allButton.clicked.connect(self.replaceAll)
# Normal mode - radio button
self.normalRadio = QtGui.QRadioButton("Normal",self)
self.normalRadio.toggled.connect(self.normalMode)
# Regular Expression Mode - radio button
self.regexRadio = QtGui.QRadioButton("RegEx",self)
self.regexRadio.toggled.connect(self.regexMode)
# The field into which to type the query
self.findField = QtGui.QTextEdit(self)
self.findField.resize(250,50)
# The field into which to type the text to replace the
# queried text
self.replaceField = QtGui.QTextEdit(self)
self.replaceField.resize(250,50)
optionsLabel = QtGui.QLabel("Options: ",self)
# Case Sensitivity option
self.caseSens = QtGui.QCheckBox("Case sensitive",self)
# Whole Words option
self.wholeWords = QtGui.QCheckBox("Whole words",self)
# Layout the objects on the screen
layout = QtGui.QGridLayout()
layout.addWidget(self.findField,1,0,1,4)
layout.addWidget(self.normalRadio,2,2)
layout.addWidget(self.regexRadio,2,3)
layout.addWidget(findButton,2,0,1,2)
layout.addWidget(self.replaceField,3,0,1,4)
layout.addWidget(replaceButton,4,0,1,2)
layout.addWidget(allButton,4,2,1,2)
# Add some spacing
spacer = QtGui.QWidget(self)
spacer.setFixedSize(0,10)
layout.addWidget(spacer,5,0)
layout.addWidget(optionsLabel,6,0)
layout.addWidget(self.caseSens,6,1)
layout.addWidget(self.wholeWords,6,2)
self.setGeometry(300,300,360,250)
self.setWindowTitle("Find and Replace")
self.setLayout(layout)
# By default the normal mode is activated
self.normalRadio.setChecked(True)
def find(self):
# Grab the parent's text
text = self.parent.text.toPlainText()
# And the text to find
query = self.findField.toPlainText()
# If the 'Whole Words' checkbox is checked, we need to append
# and prepend a non-alphanumeric character
if self.wholeWords.isChecked():
query = r'\W' + query + r'\W'
# By default regexes are case sensitive but usually a search isn't
# case sensitive by default, so we need to switch this around here
flags = 0 if self.caseSens.isChecked() else re.I
# Compile the pattern
pattern = re.compile(query,flags)
# If the last match was successful, start at position after the last
# match's start, else at 0
start = self.lastMatch.start() + 1 if self.lastMatch else 0
# The actual search
self.lastMatch = pattern.search(text,start)
if self.lastMatch:
start = self.lastMatch.start()
end = self.lastMatch.end()
# If 'Whole words' is checked, the selection would include the two
# non-alphanumeric characters we included in the search, which need
# to be removed before marking them.
if self.wholeWords.isChecked():
start += 1
end -= 1
self.moveCursor(start,end)
else:
# We set the cursor to the end if the search was unsuccessful
self.parent.text.moveCursor(QtGui.QTextCursor.End)
def replace(self):
# Grab the text cursor
cursor = self.parent.text.textCursor()
# Security
if self.lastMatch and cursor.hasSelection():
# We insert the new text, which will override the selected
# text
cursor.insertText(self.replaceField.toPlainText())
# And set the new cursor
self.parent.text.setTextCursor(cursor)
def replaceAll(self):
# Set lastMatch to None so that the search
# starts from the beginning of the document
self.lastMatch = None
# Initial find() call so that lastMatch is
# potentially not None anymore
self.find()
# Replace and find until find is None again
while self.lastMatch:
self.replace()
self.find()
def regexMode(self):
# First uncheck the checkboxes
self.caseSens.setChecked(False)
self.wholeWords.setChecked(False)
# Then disable them (gray them out)
self.caseSens.setEnabled(False)
self.wholeWords.setEnabled(False)
def normalMode(self):
# Enable checkboxes (un-gray them)
self.caseSens.setEnabled(True)
self.wholeWords.setEnabled(True)
def moveCursor(self,start,end):
# We retrieve the QTextCursor object from the parent's QTextEdit
cursor = self.parent.text.textCursor()
# Then we set the position to the beginning of the last match
cursor.setPosition(start)
# Next we move the Cursor by over the match and pass the KeepAnchor parameter
# which will make the cursor select the the match's text
cursor.movePosition(QtGui.QTextCursor.Right,QtGui.QTextCursor.KeepAnchor,end - start)
# And finally we set this new cursor as the parent's
self.parent.text.setTextCursor(cursor)
And insert this line in ext/__init__.py
:
__all__ = ["find"]
Back to writer.py
. At the top of the file:
from ext import *
In initToolbar()
:
self.findAction = QtGui.QAction(QtGui.QIcon("icons/find.png"),"Find and replace",self)
self.findAction.setStatusTip("Find and replace words in your document")
self.findAction.setShortcut("Ctrl+F")
self.findAction.triggered.connect(find.Find(self).show)
Further below:
self.toolbar.addSeparator()
self.toolbar.addAction(self.findAction)
In initMenubar()
:
edit.addAction(self.findAction)
Woah! That was a lot! No worries, I’ll explain everything.
First, the easy stuff. In ext/__init__.py
, we inserted the only line this file will ever contain: __all__ = ["find"]
. This enables us to import from our ext
package using the asterix symbol (*
), which imports all modules that are inside __all__
. Therefore, at the top of writer.py
, we can now write from ext import *
, which is currently equivalent to from ext import find
, but will be a lot more efficient once we have more modules in our package.
Further down in writer.py
, more precisely in our toolbar initialization method, initToolbar()
, we — as we’ve done many times for our text editor — create a QAction
, set up a status tip as well as a shortcut, and connect the triggered
signal to a slot function. In this case, all we need to do is create an instance of the Find
class (which I’ll get to in a bit) and call its show()
method. Fortunately, this all fits into one line and doesn’t require us to create a separate method. In initMenubar()
, we add this action to the edit
menu.
Initializing the UI
Now to our Find
class in find.py
. We start out like we did for our main window. First, we import the necessary modules from PyQt as well as the re
module, which we’ll use for text search. Next, we create a class and let it inherit from one of PyQt’s GUI windows. In this case, we’re going to inherit from QDialog
instead of from QMainWindow
, because, well, it’s a dialog and not our main window. In the constructor, __init__()
, we make the parent object a member (we pass Find
‘s constructor self
in Main.initToolbar()
). Moreover, we need another class member, self.lastMatch
, which will store the last found match (more about it soon).
In initUI()
, we take care of the graphical part of our find-and-replace dialog. We’ll create three push-buttons, one for finding text, one for replacing a single occurrence and a last one for replacing all occurrence. We create non-member instances of our buttons and connect their clicked
signals to slot functions that we’ll discuss in a bit:
# Button to search the document for something
findButton = QtGui.QPushButton("Find",self)
findButton.clicked.connect(self.find)
# Button to replace the last finding
replaceButton = QtGui.QPushButton("Replace",self)
replaceButton.clicked.connect(self.replace)
# Button to remove all findings
allButton = QtGui.QPushButton("Replace all",self)
allButton.clicked.connect(self.replaceAll)
Next, we create two radio buttons that’ll enable the user to switch between regular expression finding mode and normal, plain-text, finding mode. We make them class members, because we need to access their states later on, and connect their toggled
signals to slot functions, as for the buttons above:
# Normal mode - radio button
self.normalRadio = QtGui.QRadioButton("Normal",self)
self.normalRadio.toggled.connect(self.normalMode)
# Regular Expression Mode - radio button
self.regexRadio = QtGui.QRadioButton("RegEx",self)
self.regexRadio.toggled.connect(self.regexMode)
Then, we create two text fields. One where the user inputs text that he or she would like to find and another for the text the user’d like to replace occurences with. We resize both text fields to 250×50 pixels:
# The field into which to type the query
self.findField = QtGui.QTextEdit(self)
self.findField.resize(250,50)
# The field into which to type the text to replace the
# queried text
self.replaceField = QtGui.QTextEdit(self)
self.replaceField.resize(250,50)
Almost done. We want to also provide the user with some search options, namely case-sensitivity control and a “whole word” flag, which only highlights occurrence that have non-alphanumeric characters to their left and right. For example, I like cat soup
would pass the “whole word” check for the word cat
because the word cat
is not part of another word. In I greatly enjoy concatenating strings
, the string “cat” would be highlighted if the “whole words” flag is unchecked, but would be ignored if the user only wants “whole words”. The code for this is very simple, the only important things is that these QCheckBox
es are class members so we can check their states later on. Also, we create a QLabel
that will hold the string “Options:”, just for visual clarity:
optionsLabel = QtGui.QLabel("Options: ",self)
# Case Sensitivity option
self.caseSens = QtGui.QCheckBox("Case sensitive",self)
# Whole Words option
self.wholeWords = QtGui.QCheckBox("Whole words",self)
Now we need to order all of these widgets on our dialog. We do so by creating a QGridLayout
and adding the widgets we just created using the QGridLayout
‘s addWidget()
method, which takes the widget to add, the row, column, row-span and column-span in the layout as its arguments. Note that I create a “spacer” widget which is just a plain QWidget
with a fixed size of 0 by 10 pixels. We insert this spacer to add some distance between the replace buttons and our options:
# Layout the objects on the screen
layout = QtGui.QGridLayout()
layout.addWidget(self.findField,1,0,1,4)
layout.addWidget(self.normalRadio,2,2)
layout.addWidget(self.regexRadio,2,3)
layout.addWidget(findButton,2,0,1,2)
layout.addWidget(self.replaceField,3,0,1,4)
layout.addWidget(replaceButton,4,0,1,2)
layout.addWidget(allButton,4,2,1,2)
# Add some spacing
spacer = QtGui.QWidget(self)
spacer.setFixedSize(0,10)
layout.addWidget(spacer,5,0)
layout.addWidget(optionsLabel,6,0)
layout.addWidget(self.caseSens,6,1)
layout.addWidget(self.wholeWords,6,2)
Lastly, some window settings. We set our dialog’s geometry settings, give it a window title and set our newly created layout as the dialog’s layout. Also, we want to activate our normalRadio
checkbox initially:
self.setGeometry(300,300,360,250)
self.setWindowTitle("Find and Replace")
self.setLayout(layout)
# By default the normal mode is activated
self.normalRadio.setChecked(True)
Raiders of the lost text
Now that we have an interface, we can make our dialog… do something. As a start, I’ll discuss find()
line by line. The first thing this method needs to do is get the text in which we’ll look for queries, our main window’s QTextEdit
, and find out what text the user wants to find, which we get from our findField
:
# Grab the parent's text
text = self.parent.text.toPlainText()
# And the text to find
query = self.findField.toPlainText()
Then, we need to check whether the user has ticked any options. Note that we will use Python’s regular expression engine to do our searching. If the user wants only whole words, we append and prepend a '\W'
character, which matches any non-alphanumeric character such as a space or any form of punctuation. After checking for the case-sensitivy flag, we compile our regular expression.
To find out where we need to start our search in the text, we check if the self.lastMatch
object is not None
. If it isn’t, we can use the last match’s starting position and increment it by one for our new search. If self.lastMatch
is None
, however, we re-start from index 0. Note that Python’s regex functions return None
if no match was found for a regular expression, meaning that this way of resetting the search index to 0 will work in such a way that if the user searches the text to its end, the search starts all over again, which is great. Lastly, we do the actual search:
# If the 'Whole Words' checkbox is checked, we need to append.
# and prepend a non-alphanumeric character
if self.wholeWords.isChecked():
query = r'\W' + query + r'\W'
# By default regexes are case sensitive, but usually a search isn't.
# case sensitive by default, so we need to switch this around here
flags = 0 if self.caseSens.isChecked() else re.I
# Compile the pattern
pattern = re.compile(query,flags)
# If the last match was successful, start at position after the last.
# match's start, else at 0
start = self.lastMatch.start() + 1 if self.lastMatch else 0
# The actual search
self.lastMatch = pattern.search(text,start)
If the search was successful, we need to highlight the match. We have to do this manually using our main window’s QTextEdit
‘s QTextCursor
again, but more about that in a bit. If the user had the “whole word” flag checked, this means that the match also includes the two non-alphanumeric characters that we included in the search. Would we leave the indices like this, replacing the matched text would mean also replacing the spaces or punctuation around the actual matched text, which would make our users frustrated and make them hate us, which in turn would make us very sad. To keep everyone happy and loving, we increment the starting position and decrement the ending index of our match. If the search was unsuccessful, we set the cursor to the end of the text:
if self.lastMatch:
start = self.lastMatch.start()
end = self.lastMatch.end()
# If 'Whole words' is checked, the selection would include the two
# non-alphanumeric characters we included in the search, which need
# to be removed before marking them.
if self.wholeWords.isChecked():
start += 1
end -= 1
self.moveCursor(start,end)
else:
# We set the cursor to the end if the search was unsuccessful
self.parent.text.moveCursor(QtGui.QTextCursor.End)
Highlights
Because we just used the self.moveCursor()
method in find()
, I’ll talk about that next. As commented, We retrieve the QTextCursor object from the parent’s QTextEdit and Then we set the position to the beginning of the last match. Next we move the Cursor over the match and pass the KeepAnchor parameter which will make the cursor select the match’s text. And finally we set this new cursor as the parent’s:
def moveCursor(self,start,end):
# We retrieve the QTextCursor object from the parent's QTextEdit
cursor = self.parent.text.textCursor()
# Then we set the position to the beginning of the last match
cursor.setPosition(start)
# Next we move the Cursor over the match and pass the KeepAnchor parameter
# which will make the cursor select the match's text
cursor.movePosition(QtGui.QTextCursor.Right,QtGui.QTextCursor.KeepAnchor,end - start)
# And finally we set this new cursor as the parent's
self.parent.text.setTextCursor(cursor)
Replacing
Now that we managed to find text and highlight it, we’ll want to also handle our slot functions that take care of replacing the matched text. In replace()
, we again grab our parent’s QTextCursor
object. Then, we ensure
- That the last match was successful and
self.lastMatch
is notNone
. - The cursor currently has a selection.
If those two conditions are met, we can use the cursor’s insertText()
method and retrieve the text we want to replace our match with from the replace field. Because the cursor has a selection, it will replace the selected text with the new text. Finally, we reset our cursor:
def replace(self):
# Grab the text cursor
cursor = self.parent.text.textCursor()
# Security
if self.lastMatch and cursor.hasSelection():
# We insert the new text, which will override the selected
# text
cursor.insertText(self.replaceField.toPlainText())
# And set the new cursor
self.parent.text.setTextCursor(cursor)
Replace ALL the occurences!
To replace all the occurences of a query in the text, we need to first reset our self.lastMatch
member to None and call find()
, so that the search will begin from the start of the text. Then, if the first match was successful, we enter a loop that will replace and find occurences as long as self.lastMatch
is not None
, so as long as the search doesn’t hit the end of the text.
def replaceAll(self):
# Set lastMatch to None so that the search
# starts from the beginning of the document
self.lastMatch = None
# Initial find() call so that lastMatch is
# potentially not None anymore
self.find()
# Replace and find until find is None again
while self.lastMatch:
self.replace()
self.find()
Some last slots
The last two functions we need for our Find
class are the handlers for the search mode (normal or regex):
def regexMode(self):
# First uncheck the checkboxes
self.caseSens.setChecked(False)
self.wholeWords.setChecked(False)
# Then disable them (gray them out)
self.caseSens.setEnabled(False)
self.wholeWords.setEnabled(False)
def normalMode(self):
# Enable checkboxes (un-gray them)
self.caseSens.setEnabled(True)
self.wholeWords.setEnabled(True)
Regex mode means that the search flags are unnecessary, since the user will want to input flags using regular expressions him- or herself. Therefore, we uncheck the check boxes and also disable them, which will “gray” them out.
For normalMode()
, we simply re-enable the check boxes.
So much for our find-and-replace dialog! Next up:
Image insertion
Image insertion does not require a class of its own, so we’ll stick around writer.py
for this one. In fact, all we need is a QAction
in initToolbar()
:
imageAction = QtGui.QAction(QtGui.QIcon("icons/image.png"),"Insert image",self)
imageAction.setStatusTip("Insert image")
imageAction.setShortcut("Ctrl+Shift+I")
imageAction.triggered.connect(self.insertImage)
self.toolbar.addAction(imageAction)
And a slot function, self.insertImage()
. In it, we open a getOpenFileName
dialog like we did for opening a .writer
file in the very beginning, from which we retrieve a file name. For the file dialog’s filter, we include common image formats. If we got a file name, we create a QImage
and, if it was loadable (isNull
== False), we insert it using our QTextCursor
‘s insertImage()
method. If it wasn’t loadable, we pop up a QMessageBox
. The constructor of this QMessageBox
requires an icon from the QMessageBox namespace (either a question, information, warning or “critical” icon), a window title, the message to display, a set of buttons to show and lastly a parent object:
def insertImage(self):
# Get image file name
filename = QtGui.QFileDialog.getOpenFileName(self, 'Insert image',".","Images (*.png *.xpm *.jpg *.bmp *.gif)")
# Create image object
image = QtGui.QImage(filename)
# Error if unloadable
if image.isNull():
popup = QtGui.QMessageBox(QtGui.QMessageBox.Critical,
"Image load error",
"Could not load image file!",
QtGui.QMessageBox.Ok,
self)
popup.show()
else:
cursor = self.text.textCursor()
cursor.insertImage(image,filename)
Counting words
For the next extension, a word-count dialog that’ll display the number of words and symbols in the document’s selected and total text, we’ll create a new class in a separate file again. So, in
ext/wordcount.py
:
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
class WordCount(QtGui.QDialog):
def __init__(self,parent = None):
QtGui.QDialog.__init__(self, parent)
self.parent = parent
self.initUI()
def initUI(self):
# Word count in selection
currentLabel = QtGui.QLabel("Current selection",self)
currentLabel.setStyleSheet("font-weight:bold; font-size: 15px;")
currentWordsLabel = QtGui.QLabel("Words: ", self)
currentSymbolsLabel = QtGui.QLabel("Symbols: ",self)
self.currentWords = QtGui.QLabel(self)
self.currentSymbols = QtGui.QLabel(self)
# Total word/symbol count
totalLabel = QtGui.QLabel("Total",self)
totalLabel.setStyleSheet("font-weight:bold; font-size: 15px;")
totalWordsLabel = QtGui.QLabel("Words: ", self)
totalSymbolsLabel = QtGui.QLabel("Symbols: ",self)
self.totalWords = QtGui.QLabel(self)
self.totalSymbols = QtGui.QLabel(self)
# Layout
layout = QtGui.QGridLayout(self)
layout.addWidget(currentLabel,0,0)
layout.addWidget(currentWordsLabel,1,0)
layout.addWidget(self.currentWords,1,1)
layout.addWidget(currentSymbolsLabel,2,0)
layout.addWidget(self.currentSymbols,2,1)
spacer = QtGui.QWidget()
spacer.setFixedSize(0,5)
layout.addWidget(spacer,3,0)
layout.addWidget(totalLabel,4,0)
layout.addWidget(totalWordsLabel,5,0)
layout.addWidget(self.totalWords,5,1)
layout.addWidget(totalSymbolsLabel,6,0)
layout.addWidget(self.totalSymbols,6,1)
self.setWindowTitle("Word count")
self.setGeometry(300,300,200,200)
self.setLayout(layout)
def getText(self):
# Get the text currently in selection
text = self.parent.text.textCursor().selectedText()
# Split the text to get the word count
words = str(len(text.split()))
# And just get the length of the text for the symbols
# count
symbols = str(len(text))
self.currentWords.setText(words)
self.currentSymbols.setText(symbols)
# For the total count, same thing as above but for the
# total text
text = self.parent.text.toPlainText()
words = str(len(text.split()))
symbols = str(len(text))
self.totalWords.setText(words)
self.totalSymbols.setText(symbols)
And in __init__.py
:
__all__ = ["find","wordcount"]
Back to writer.py
. In initToolbar()
:
wordCountAction = QtGui.QAction(QtGui.QIcon("icons/count.png"),"See word/symbol count",self)
wordCountAction.setStatusTip("See word/symbol count")
wordCountAction.setShortcut("Ctrl+W")
wordCountAction.triggered.connect(self.wordCount)
self.toolbar.addAction(wordCountAction)
Below initUI()
:
def wordCount(self):
wc = wordcount.WordCount(self)
wc.getText()
wc.show()
As mentioned, this dialog will show the user the number of words and symbols currently under selection (if there is a selection) and also the number of words and symbols in the entire document. The UI is fairly simple. We create labels that indicate whether the numbers shown are for the current selection or the whole text, currentLabel
and totalLabel
, as well as two labels each that hold the strings “Words:” and “Symbols:”, plus two labels each in which we’ll show the actual numbers (these must be class members):
# Word count in selection
currentLabel = QtGui.QLabel("Current selection",self)
currentLabel.setStyleSheet("font-weight:bold; font-size: 15px;")
currentWordsLabel = QtGui.QLabel("Words: ", self)
currentSymbolsLabel = QtGui.QLabel("Symbols: ",self)
self.currentWords = QtGui.QLabel(self)
self.currentSymbols = QtGui.QLabel(self)
# Total word/symbol count
totalLabel = QtGui.QLabel("Total",self)
totalLabel.setStyleSheet("font-weight:bold; font-size: 15px;")
totalWordsLabel = QtGui.QLabel("Words: ", self)
totalSymbolsLabel = QtGui.QLabel("Symbols: ",self)
self.totalWords = QtGui.QLabel(self)
self.totalSymbols = QtGui.QLabel(self)
We put them into a layout and set the dialog’s geometry and window title:
# Layout
layout = QtGui.QGridLayout(self)
layout.addWidget(currentLabel,0,0)
layout.addWidget(currentWordsLabel,1,0)
layout.addWidget(self.currentWords,1,1)
layout.addWidget(currentSymbolsLabel,2,0)
layout.addWidget(self.currentSymbols,2,1)
spacer = QtGui.QWidget()
spacer.setFixedSize(0,5)
layout.addWidget(spacer,3,0)
layout.addWidget(totalLabel,4,0)
layout.addWidget(totalWordsLabel,5,0)
layout.addWidget(self.totalWords,5,1)
layout.addWidget(totalSymbolsLabel,6,0)
layout.addWidget(self.totalSymbols,6,1)
self.setWindowTitle("Word count")
self.setGeometry(300,300,200,200)
self.setLayout(layout)
The function that will count all of these words and symbols is getText()
. First, we want to count the words and symbols of the selected text, which we get by grabbing our QTextEdit
‘s QTextCursor
and calling its selectedText()
method. We use the retrieved string’s split()
method to split the string into a list of individual words, of which we then get the length. The number of symbols is simply the length of the entire string. We then visualize the two numbers we just got using the respective labels’ setText()
method. We repeat this process for the whole text and again set the counts we retrieved to the respective labels’ text:
def getText(self):
# Get the text currently in selection
text = self.parent.text.textCursor().selectedText()
# Split the text to get the word count
words = str(len(text.split()))
# And just get the length of the text for the symbols
# count
symbols = str(len(text))
self.currentWords.setText(words)
self.currentSymbols.setText(symbols)
# For the total count, same thing as above but for the
# total text
text = self.parent.text.toPlainText()
words = str(len(text.split()))
symbols = str(len(text))
self.totalWords.setText(words)
self.totalSymbols.setText(symbols)
In writer.py
, we again create a QAction
for our word count dialog and add it to the toolbar. In the slot function, self.wordCount()
, we create an instance of our WordCount
class, call its getText()
method and finally show the dialog.
That’ll be it for this part of the series. In the next (and final!) part, we’ll be adding awesome extensions for inserting the current time and date into the text as well as a more sophisticated dialog for inserting tables. Moreover, I’ll show you how to enable custom context menus that will enable us to manipulate the tables we insert into the text (adding/deleting/merging rows and columns).
Don’t forget to check back to the project’s GitHub repository for any new updates or changes once in a while. See you next week!
Subscribe below to receive updates on new tutorials!
Continue this series with the fourth and final installment!
Author: Peter Goldsborough