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.
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.
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()
Leave a Reply