Get started with this series with part one!

In the previous part of my tutorial series on Building a text editor with PyQt, we created our text editor’s basic skeleton and added some useful features for file management, printing, inserting lists and more. This part will focus on the format bar, which we’ll populate with a number of features, including actions to change the font family, background color, alignment and more.

Font

We’ll start with actions related to font, meaning the user will be able to:

Change font family

Adjust font size

Set font color

Choose background color

Now to the code. Just like last time, I’ll only show the functions that change relative to the previous code:

initFormatbar() :

fontBox = QtGui . QFontComboBox ( self )

fontBox . currentFontChanged . connect ( self . fontFamily )

fontSize = QtGui . QComboBox ( self )

fontSize . setEditable ( True )

# Minimum number of chars displayed

fontSize . setMinimumContentsLength ( 3 )

fontSize . activated . connect ( self . fontSize )

# Typical font sizes

fontSizes = [ '6' , '7' , '8' , '9' , '10' , '11' , '12' , '13' , '14' ,

'15' , '16' , '18' , '20' , '22' , '24' , '26' , '28' ,

'32' , '36' , '40' , '44' , '48' , '54' , '60' , '66' ,

'72' , '80' , '88' , '96' ]

for i in fontSizes :

fontSize . addItem ( i )

fontColor = QtGui . QAction ( QtGui . QIcon ( "icons/font-color.png" ), "Change font color" , self )

fontColor . triggered . connect ( self . fontColor )

backColor = QtGui . QAction ( QtGui . QIcon ( "icons/highlight.png" ), "Change background color" , self )

backColor . triggered . connect ( self . highlight )

self . formatbar = self . addToolBar ( "Format" )

self . formatbar . addWidget ( fontBox )

self . formatbar . addWidget ( fontSize )

self . formatbar . addSeparator ()

self . formatbar . addAction ( fontColor )

self . formatbar . addAction ( backColor )

self . formatbar . addSeparator ()

Below initUI() :

def fontFamily ( self , font ):

self . text . setCurrentFont ( font )

def fontSize ( self , fontsize ):

self . text . setFontPointSize ( int ( fontsize ))

def fontColor ( self ):

# Get a color from the text dialog

color = QtGui . QColorDialog . getColor ()

# Set it as the new text color

self . text . setTextColor ( color )

def highlight ( self ):

color = QtGui . QColorDialog . getColor ()

self . text . setTextBackgroundColor ( color )

Note that the actions we just created don’t follow the code pattern for actions I described last time. We don’t make these actions class members because we only need to create and use them within the scope of initFormatbar() . We also don’t give them tooltips or shortcuts anymore (unless you want to, of course).

We start out by creating a QFontComboBox , which is a very convenient combo box that automatically includes all the fonts available to the system. We instantiate it and connect its currentFontChanged signal to a slot function, self.fontFamily() , which we later created underneath the initUI() method. As you can see, we also give this slot function a second parameter font , so PyQt will pass the user-selected QFont object to our function, reducing our work to setting this font to the text’s current font.

Next up, we need a combo box for font sizes. PyQt itself doesn’t have such a thing, so we need to create one ourself. This is easily done by instantiating a normal combo box, here called fontSize , which we set editable, meaning the user can enter any number they want for the font. After connecting the activated signal to a slot function, we populate the combo box with some common font sizes. For the slot function, we again set a second parameter, font size , which PyQt passes to us when the user selects a font size from the combo box or, alternatively, enters a custom size. We set the user’s selection as the text’s current font point size.

The last two actions are very similar. In both cases, we create two actions that open a QColorDialog when activated. In case of fontColor , we set the color selection as the font color. For backColor , we set the color as the current text’s background color.

Bold moves

Next, we’ll add actions to make text:

bold

italic

underlined

strikeout

superscript

subscript

The code for this is relatively simple:

initFormatbar() :

boldAction = QtGui . QAction ( QtGui . QIcon ( "icons/bold.png" ), "Bold" , self )

boldAction . triggered . connect ( self . bold )

italicAction = QtGui . QAction ( QtGui . QIcon ( "icons/italic.png" ), "Italic" , self )

italicAction . triggered . connect ( self . italic )

underlAction = QtGui . QAction ( QtGui . QIcon ( "icons/underline.png" ), "Underline" , self )

underlAction . triggered . connect ( self . underline )

strikeAction = QtGui . QAction ( QtGui . QIcon ( "icons/strike.png" ), "Strike-out" , self )

strikeAction . triggered . connect ( self . strike )

superAction = QtGui . QAction ( QtGui . QIcon ( "icons/superscript.png" ), "Superscript" , self )

superAction . triggered . connect ( self . superScript )

subAction = QtGui . QAction ( QtGui . QIcon ( "icons/subscript.png" ), "Subscript" , self )

subAction . triggered . connect ( self . subScript )

Further below:

self . formatbar . addAction ( boldAction )

self . formatbar . addAction ( italicAction )

self . formatbar . addAction ( underlAction )

self . formatbar . addAction ( strikeAction )

self . formatbar . addAction ( superAction )

self . formatbar . addAction ( subAction )

self . formatbar . addSeparator ()

Below initUI() :

def bold ( self ):

if self . text . fontWeight () == QtGui . QFont . Bold :

self . text . setFontWeight ( QtGui . QFont . Normal )

else :

self . text . setFontWeight ( QtGui . QFont . Bold )

def italic ( self ):

state = self . text . fontItalic ()

self . text . setFontItalic ( not state )

def underline ( self ):

state = self . text . fontUnderline ()

self . text . setFontUnderline ( not state )

def strike ( self ):

# Grab the text's format

fmt = self . text . currentCharFormat ()

# Set the fontStrikeOut property to its opposite

fmt . setFontStrikeOut ( not fmt . fontStrikeOut ())

# And set the next char format

self . text . setCurrentCharFormat ( fmt )

def superScript ( self ):

# Grab the current format

fmt = self . text . currentCharFormat ()

# And get the vertical alignment property

align = fmt . verticalAlignment ()

# Toggle the state

if align == QtGui . QTextCharFormat . AlignNormal :

fmt . setVerticalAlignment ( QtGui . QTextCharFormat . AlignSuperScript )

else :

fmt . setVerticalAlignment ( QtGui . QTextCharFormat . AlignNormal )

# Set the new format

self . text . setCurrentCharFormat ( fmt )

def subScript ( self ):

# Grab the current format

fmt = self . text . currentCharFormat ()

# And get the vertical alignment property

align = fmt . verticalAlignment ()

# Toggle the state

if align == QtGui . QTextCharFormat . AlignNormal :

fmt . setVerticalAlignment ( QtGui . QTextCharFormat . AlignSubScript )

else :

fmt . setVerticalAlignment ( QtGui . QTextCharFormat . AlignNormal )

# Set the new format

self . text . setCurrentCharFormat ( fmt )

The changes in initFormatbar() should be relatively understandable by now. We create actions and connect the triggered signals to slot functions, after which we add the actions to the format bar.

In bold() , we invert the font weight of the current text. If the text is bold, we set the font weight to “normal”. If the font weight is normal, we set it to bold.

For italic() and underline() , our QTextEdit object has functions for setting and getting the state of the text. Therefore, we just grab the current state of the text and invert it.

The strike() function is a bit different. We retrieve our text’s currentCharFormat , invert the state of the fontStrikeOut property and finally set our new char format to the text’s “current” char format.

Lastly, in superScript() and subScript() , we again fetch the current char format, toggle the verticalAlignment property like we did in bold() and reset the new char format to make our changes visible.

Alignment

Alignment is very simple, as PyQt provides us with the necessary methods:

initFormatbar() :

alignLeft = QtGui . QAction ( QtGui . QIcon ( "icons/align-left.png" ), "Align left" , self )

alignLeft . triggered . connect ( self . alignLeft )

alignCenter = QtGui . QAction ( QtGui . QIcon ( "icons/align-center.png" ), "Align center" , self )

alignCenter . triggered . connect ( self . alignCenter )

alignRight = QtGui . QAction ( QtGui . QIcon ( "icons/align-right.png" ), "Align right" , self )

alignRight . triggered . connect ( self . alignRight )

alignJustify = QtGui . QAction ( QtGui . QIcon ( "icons/align-justify.png" ), "Align justify" , self )

alignJustify . triggered . connect ( self . alignJustify )

Further below:

self . formatbar . addAction ( alignLeft )

self . formatbar . addAction ( alignCenter )

self . formatbar . addAction ( alignRight )

self . formatbar . addAction ( alignJustify )

self . formatbar . addSeparator ()

Below the initUI() method:

def alignLeft ( self ):

self . text . setAlignment ( Qt . AlignLeft )

def alignRight ( self ):

self . text . setAlignment ( Qt . AlignRight )

def alignCenter ( self ):

self . text . setAlignment ( Qt . AlignCenter )

def alignJustify ( self ):

self . text . setAlignment ( Qt . AlignJustify )

Changes in the initFormatbar() method follow the previous pattern and the slot functions are also very simple. We change the text’s alignment using our QTextEdit ‘s setAlignment method, passing it the respective member of the Qt namespace, e.g. Qt.AlignCenter .

Indent – dedent

Indenting and dedenting is a little more complex, as PyQt provides us with no methods to efficiently adjust the tabbing of a selected area, meaning we need to come up with our own method of doing so:

initFormatbar() :

indentAction = QtGui . QAction ( QtGui . QIcon ( "icons/indent.png" ), "Indent Area" , self )

indentAction . setShortcut ( "Ctrl+Tab" )

indentAction . triggered . connect ( self . indent )

dedentAction = QtGui . QAction ( QtGui . QIcon ( "icons/dedent.png" ), "Dedent Area" , self )

dedentAction . setShortcut ( "Shift+Tab" )

dedentAction . triggered . connect ( self . dedent )

Further below:

self . formatbar . addAction ( indentAction )

self . formatbar . addAction ( dedentAction )

Below initUI() :

def indent ( self ):

# Grab the cursor

cursor = self . text . textCursor ()

if cursor . hasSelection ():

# Store the current line/block number

temp = cursor . blockNumber ()

# Move to the selection's last line

cursor . setPosition ( cursor . selectionEnd ())

# Calculate range of selection

diff = cursor . blockNumber () - temp

# Iterate over lines

for n in range ( diff + 1 ):

# Move to start of each line

cursor . movePosition ( QtGui . QTextCursor . StartOfLine )

# Insert tabbing

cursor . insertText ( "\t" )

# And move back up

cursor . movePosition ( QtGui . QTextCursor . Up )

# If there is no selection, just insert a tab

else :

cursor . insertText ( "\t" )

def dedent ( self ):

cursor = self . text . textCursor ()

if cursor . hasSelection ():

# Store the current line/block number

temp = cursor . blockNumber ()

# Move to the selection's last line

cursor . setPosition ( cursor . selectionEnd ())

# Calculate range of selection

diff = cursor . blockNumber () - temp

# Iterate over lines

for n in range ( diff + 1 ):

self . handleDedent ( cursor )

# Move up

cursor . movePosition ( QtGui . QTextCursor . Up )

else :

self . handleDedent ( cursor )

def handleDedent ( self , cursor ):

cursor . movePosition ( QtGui . QTextCursor . StartOfLine )

# Grab the current line

line = cursor . block (). text ()

# If the line starts with a tab character, delete it

if line . startswith ( "\t" ):

# Delete next character

cursor . deleteChar ()

# Otherwise, delete all spaces until a non-space character is met

else :

for char in line [: 8 ]:

if char != " " :

break

cursor . deleteChar ()

Changes to initFormatbar() as previously discussed.

Let’s go through the indent() function step by step. The first thing we need to do is grab our text’s current QTextCursor object. We check if the user currently has any text under selection. If not, we just insert a tab. If he or she does have something under selection, however, we need to get a bit more funky. More specifically, we have to find out how many lines the user has under selection and insert a tab before each line.

We do so by first getting the current line/block number at the start of the selection, then moving the cursor to the end and subtracting the previously stored block/line number from the new one. This provides us with the range of lines over which we subsequently iterate. For each iteration, we move the cursor to the start of the current line, insert a tab and finally move up one line until we reach the top. (Remember that before we start iterating, we have the cursor at the end of the selection, where we moved it to find out the selection’s last line number)

The dedent() method is quite similar, it differs, however, in our need to also handle excess space and not only tabs. That’s what handleDedent() is for. It’s called at each iteration of the loop that moves up the lines of the selection. In it, we again set the cursor to the beginning of each line, after which we grab the current line’s text. If the line starts with a tab, we can just delete it and our job is done. If it doesn’t, we also check wether there is any excess space (up to 8 spaces, which equals a tab) and delete it if so. This ensures two things:

People who prefer 8 spaces over a tab character (‘/t’) also get their money’s worth

Excess space that could block from you from completely dedenting a block of text is deleted

Final customization options

Now that our tool bar, our format bar and our status bar are populated, we can add some final customization options to toggle the visibility of these three bars:

initMenubar() :

# Toggling actions for the various bars

toolbarAction = QtGui . QAction ( "Toggle Toolbar" , self )

toolbarAction . triggered . connect ( self . toggleToolbar )

formatbarAction = QtGui . QAction ( "Toggle Formatbar" , self )

formatbarAction . triggered . connect ( self . toggleFormatbar )

statusbarAction = QtGui . QAction ( "Toggle Statusbar" , self )

statusbarAction . triggered . connect ( self . toggleStatusbar )

view . addAction ( toolbarAction )

view . addAction ( formatbarAction )

view . addAction ( statusbarAction )

Below initUI() :

def toggleToolbar ( self ):

state = self . toolbar . isVisible ()

# Set the visibility to its inverse

self . toolbar . setVisible ( not state )

def toggleFormatbar ( self ):

state = self . formatbar . isVisible ()

# Set the visibility to its inverse

self . formatbar . setVisible ( not state )

def toggleStatusbar ( self ):

state = self . statusbar . isVisible ()

# Set the visibility to its inverse

self . statusbar . setVisible ( not state )

We create three actions in our initMenubar() method…

toolbarAction

formatbarAction

statusbarAction

…and connect them to slot functions. Note that we don’t add these actions to any of the toolbars, but only to the drop-down menus at the top of our screen.

In the slot functions, we do what we did for some of the formatting functions: we retrieve the visibility states of the various bars and set the them to their opposite.

That’ll be it for this post, be sure to check back for the upcoming part of this series on Building a text editor with PyQt, in which we’ll add some interesting actions for find-and-replace, inserting images and more.

Subscribe below to receive updates on new tutorials!

Read part three here!

Author: Peter Goldsborough