Make App Pie

Training for Developers and Artists

From Apple to Raspberry Pi: Adding a Tkinter Text Box in Python

Tkinter lacks a good list box. There is a widget, but as shown in earlier posts in this series, it doesn’t do the best job. We had to use underscores to get the list box to work.  There is another widget that might do what we really want: the Text widget.

The Text widget is a full text editor widget. In this post, we will add one to the Popper’s Penguins application to create a report of found penguins.  In future posts, We’ll build some methods to select a line of text  and return the index of the line.

If you would like to follow along and do not have the Popper’s penguins code so far, you can get it from Github here

Adding the Text Box to the View

Add a text box  to the Popper’s Penguins code loadView() the same way we added all our other widgets:

#set up the text box
        self.dataList_textbox = scrolledtext.ScrolledText(listFrame, tabs= ("10c"))
        self.dataList_textbox.grid(row=1, column = 0, sticky = NSEW);
        self.dataList_textbox.insert("0.0","Penguin\tPenguin Action") #test data

Python Tkinter gives us two versions of  the Text widget: one with(ScrolledText) and one without(Text) scrollbars. To save us from doing the mucking around with scrollbars we did last time, we’ll use the ScrolledText version.

While most of our widgets have some control variable, the text box uses methods to do most of the setting and getting of data.  We set up a 10cm tab stop in line 2 with tabs= ("10c") which we will use later.  Using .grid in line 3, we added the text box below our current list box in the listFrame frame. Until we have full functionality, we might want to compare our result to the list box. Line 4 is not necessary, but will help us test our application.

Save and run.

A empty Text Widget with test data
A empty Text Widget with test data

Adding Setters and Getters

Like the rest of our view, we need a setter and getter to use the text in the Text widget. Text widgets do not use control variables for the data.  Text widgets use a range of data indexes. The Text widget’s methods work on the ranges we specify.

Selecting Indexes in a Text Box

Two indexes set a Text box range. An index is a string of the form “line.column” specifying a coordinate in characters in the text box.  The coordinate "0.0" would be the upper left corner. The coordinate "3.2" would be the 3rd line, second character. There are some values calculated for us.  The value of END gives us the coordinate of the last character in Text widget.

We will keep our setters and getters  with the same behavior as  other  text-based widgets like Label for now. It will get all the text and change all the text.  The range is a beginning index and an ending index. The range  ("0.0",END) will select everything in the text box.

Adding the Methods

Add the getter first:

    def getTextBoxtext(self):
    #return a string of all the boxes contents
        return self.dataList_textbox.get('0.0',END)

Using a range of everything, we get all the text and return it as a string.  Our setter is a bit more complicated. Add under the getter the following:

    def setTextboxText(self,listString):
        #delete everything in a text box and replace with the text
        self.dataList_textbox.delete("0.0",END)
        self.dataList_textbox.insert('0.0',listString)

The Text widget does not have a set method. Instead it has an insert method, which inserts at a coordinate a specified string. For us to have the setter we want, we delete everything first in line 3, then insert at the origin in line 4.

Connect the Text Box in the View Controller

Our code for the Text Box is very similar to the code for the List Box. Copy and paste the code for elementListToString() from its current location to under the method listToListString(), then change the method as follows:

#Add list to string processing for the Text widget
    def elementListToString(self,aList): #moved from above code for clarity
        listString = str()
        for  element in aList:
            if len(listString)>0:
                listString = listString + '\t' + element
            else:
                listString = element
        return listString

We added an if clause  to prevent the string from getting a tab before an entry. when empty, listString gets assigned the value for element, and not a tab followed by element. The string starts with the name of a penguin.

Now add the following method below elementListToString():

    def listToTextString(self,aList):
        #method for making lists compatible
        #with the text box
        #returns a string we can insert in the text box
        listString = self.reportHeader 
        for record in aList:
            elementString = self.elementListToString(record)
            listString = listString + '\n' + elementString
        return listString

This one works almost identically to its list box counterpart elementListToString(). Instead of initializing  the listString to blank string, we added a header to it.  We set the header in the __init__() method for the class. Add this to __init__() in the view controller:

        self.reportHeader = "Penguin Type\tPenguin Action\n"+ "="*35 +"\t"+"="*35+"\n"

Python has a few really convenient string and list processing operators.  Concatenation, taking two strings and joining them together uses the + operator used for addition with numbers. Repetition of a string uses the * operator used for multiplication with numbers.  The string  “="*35  is 35 equals signs.  With the \t escape sequence taking us to the tab stop we specified in the view, the header sets up two underlined headings for a table listing our penguins.

Save and run. Add a few penguins.

The text widget with a few penguins added
The text widget with a few penguins added

We now have a working  list of penguins.  You can select in the list, type in the box.  Next time, we will select a line of text.

The Whole Code

Here is a listing of our application so far. you can also download it from Github here

# poppers_penguins_MVC_05
#MVC Version 2014 May 28
#Change: Adds the text box

from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import scrolledtext
 
 
class MyViewController():

    
    def __init__(self,parent):

        self.reportHeader = "Penguin Type\tPenguin Action\n"+ "="*35 +"\t"+"="*35+"\n"
        self.parent = parent;       
        self.view = MyView(self)
        self.model = MyModel(self)
        
    #Handlers -- target action
    def addPressed(self):
        self.view.setLabelText(self.view.getPenguinType()+ ' Penguin '+ self.view.getPenguinAction() + ' Added')
        self.model.addRecordToList(self.view.getPenguinType(),self.view.getPenguinAction())
    def quitPressed(self):
        self.view.setLabelText('Quitting')
        answer = messagebox.askokcancel('Ok to Quit','This will quit the program. \n Ok to quit?')
        if answer==True:            self.parent.destroy()

#Add list to string processing for the scrollbox.
    def spaceToUnderscore(self,aList, width):
        # convert spaces to underscores in list items
        # with a triple underscore between list items
        listString = str()
        for  element in aList:
            elementString = '{:{width}}'.format(element, width=width) 
            elementString = elementString.replace(' ','_')
            listString = listString + '___' + elementString
        return listString
    
    def listToListString(self,aList):
        #a temporary method for making lists compatible
        #with listbox till I find something better
        #returns a string we can place in a StringVar
        listString = str() 
        for record in aList:
            elementString = self.spaceToUnderscore(record,20)
            listString = listString + '\n' + elementString
        return listString
#Add list to string processing for the Text widget
    def elementListToString(self,aList): #moved from above code for clarity
        listString = str()
        for  element in aList:
            if len(listString)>0:
                listString = listString + '\t' + element
            else:
                listString = element
        return listString

    
    def listToTextString(self,aList):
        #method for making lists compatible
        #with the text box
        #returns a string we can insert in the text box
        listString = self.reportHeader 
        for record in aList:
            elementString = self.elementListToString(record)
            listString = listString + '\n' + elementString
        return listString
            
#delegate for the model            
    def modelDidChangeDelegate(self):
        aList = self.model.getList()
        aListString = self.listToListString(aList)
        self.view.setDataList(aListString)
        self.view.setTextboxText(self.listToTextString(aList))
    
class MyView(Frame):
#Change parent to vc and add a frame 2014 May 26 a  
    def __init__(self,vc):
        self.frame=Frame()
        self.frame.grid(row=0,column=0)
        self.vc = vc
        
        #properties of the view 
        self.labelText = StringVar()
        self.labelText.set("Popper's Penguins Ready")
        self.penguinType = StringVar()
        self.penguinType.set('Penguin Type')
        self.penguinAction = StringVar()
        self.penguinAction.set('Penguin Action')

#Add the control variable
        self.dataList = StringVar()
        self.dataList.set ('')
# instantiate the text box
        self.dataList_textbox = Text(self.frame)       
        self.loadView()

 #       self.makeStyle()
         

#Setters and getters for the properties 2014-May 26
    def setLabelText(self,newText):
        self.labelText.set(newText)
    def getLabelText(self):
        return self.labelText.get()
    def setPenguinType(self,newType):
        self.penguinType.set(newType)
    def getPenguinType(self):
        return self.penguinType.get()
    def setPenguinAction(self,newAction):
        self.penguinAction.set(newAction)
    def getPenguinAction(self):
        return self.penguinAction.get()
#Add setters and getter for data list
    def getDataList(self):
        #returns a string
        return self.dataList.get()
    def setDataList(self,listString):
        #needs a list string delimtied by a space
        self.dataList.set(listString)
#Setters and getters for the text widget 2014-Jun-13
    def getTextBoxtext(self):
    #return a string of all the boxes contents
        return self.dataList_textbox.get('0.0',END)
            
    def setTextboxText(self,listString):
        #delete everything in a text box and replace with the text
        self.dataList_textbox.delete("0.0",END)
        self.dataList_textbox.insert('0.0',listString)
    
#Style Sheet
    def makeStyle(self):
        self.s = ttk.Style()
        self.s.configure('TFrame',background = '#5555ff')
        self.s.configure('TButton',background = 'blue', foreground = '#eeeeff', font = ('Sans','14','bold'), sticky = EW)
        self.s.configure('TLabel',font=('Sans','16','bold'),background = '#5555ff', foreground = '#eeeeff')
        self.s.map('TButton', foreground = [('hover','#5555ff'), ('focus', 'yellow')])
        self.s.map('TButton', background = [('hover', '#eeeeff'),('focus','orange')])
        self.s.configure('TCombobox',background = '#5555ff',foreground ='#3333ff',font = ('Sans',18))
   
#loading the view
#changed the command= to refer to the view controller 2014-May-26
    # self.addPressed now is self.vc.addPressed
    # self.quitPressed now is self.vc.quitPressed
    def loadView(self):
        #label
        buttonFrame = ttk.Frame(self.frame)
        buttonFrame.grid(row = 1, column = 0)
        
        status_label = ttk.Label(self.frame, textvariable = self.labelText)
        #status_label.configure(font=('Sans','16','bold'),background = 'blue', foreground = '#eeeeff')
        status_label.grid(row=0,column=0,sticky=NSEW)
 
        add_button = ttk.Button(buttonFrame,command= self.vc.addPressed,text = 'Add')
        add_button.grid(row = 0, column = 0,sticky = NSEW)
        quit_button = ttk.Button(buttonFrame, command = self.vc.quitPressed, text = 'Quit')
        quit_button.grid(row = 0, column = 1,sticky = NSEW)
 
        penguinType_values = ['Adele','Emperor','King','Blackfoot','Humboldt','Galapagos','Macaroni','Tux','Oswald Cobblepot','Flippy Slippy']
        penguinType_combobox = ttk.Combobox(buttonFrame,values = penguinType_values, textvariable = self.penguinType)
        penguinType_combobox.grid(row =1, column = 0,sticky = EW)
 
        penguinAction_values = ['Happy','Sad','Angry','Standing','Swimming','Eating','Sleeping','On Belly','Plotting Evil','Singing','Dancing','Being Cute']
        penguinAction_combobox = ttk.Combobox(buttonFrame, values = penguinAction_values, textvariable = self.penguinAction)
        penguinAction_combobox.grid(row=1, column = 1,sticky = EW)

        listFrame = ttk.Frame(self.frame)
        listFrame.grid(row =2,column = 0)
        
        dataList_yScroll = Scrollbar(listFrame, orient=VERTICAL)
        dataList_yScroll.grid(row=0,column=1,sticky = NS)
    
        dataList_listbox = Listbox(listFrame, listvariable= self.dataList, width = 40)
        dataList_listbox.grid(row=0, column = 0, sticky=NSEW)
#bind the listbox and scrollbar
        dataList_listbox.configure(yscrollcommand = dataList_yScroll.set)
        dataList_yScroll.configure(command=  dataList_listbox.yview)
#set up the text box
        self.dataList_textbox = scrolledtext.ScrolledText(listFrame, tabs= ("10c"))
        self.dataList_textbox.grid(row=1, column = 0, sticky = NSEW);
        self.dataList_textbox.insert("0.0","Penguin\tPenguin Action") #test data


#(1)adding the model 2014-May-28 with convenience method
class MyModel():
    def __init__(self,vc):
        self.vc = vc
        self.myList = []
        self.count = 0
#(2)Delegate goes here. Model would call this on internal change
    def modelDidChange(self):
        self.vc.modelDidChangeDelegate()
#Setters and getters for the model   
    def getList(self):
        return self.myList
    def addToList(self,item):
        myItem = item
        myList = self.myList
        myList.append(myItem)
        self.myList=myList
        self.modelDidChange()
    def getItemAtIndex(self,index):
        return myList[index]
#other methods
    def addRecordToList(self,penguin,action):
        self.addToList([penguin,action])
    

         
def main():
    root = Tk()
#Set up the main loop 2014 May 26
    frame = Frame(root, background="#5555ff", takefocus = 0)
    root.title("Popper's Penguins")         
    app = MyViewController(root)
    root.mainloop() 
 
 
if __name__ == '__main__':
    main()  

 

One response to “From Apple to Raspberry Pi: Adding a Tkinter Text Box in Python”

  1. […] of the mouse buttons. In line 3, we use event.x and event.y to get the mouse position. We discussed in the last post, the index format of Text boxes. The rest of line 3 transforms the event data into a text box index […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: