For the last few posts, we have tried to get past the Tkinter limitation with scrolling list boxes using only one word per line. This post we take the Text widget we added last week, and turn it into a true list box. We will be working completely within the view, though creating one new property for use by the view controller – an index.
Code from the last post is on github here, if you wish to use it to follow along.
Our New Property
Before we get started, add the following line to __init__()
in the MyView
class:
self.selectedLine = "1"
We now have initialized our new property, which gives us an index of the selected line in the text box. Text boxes are a bit odd, in that they start with row 1, not row 0. If we want to use this information in our view controller eventually, we will need a getter that subtracts 1 to match the standard indexing system. Add this to our getters and setters in MyView
def getSelectedLine(self): #for use externally as an index. Do not use the property in the view controller return self.selectedLine - 1
Events
Up to this point, we have used the command
attribute to control events. For example:
add_button = ttk.Button(buttonFrame,command= self.vc.addPressed,text = 'Add')
Text buttons do not have a command attribute. Instead we need to bind and handle the event ourselves. We’ll use the Tkinter method .bind()
. Change the code for the text scroll box to this:
#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 commented out v6 #Added event binding to a button release self.dataList_textbox.bind('',self.mouseSelectTextLine)
We commented out our line 4 test data and added line 6. The .bind()
method binds the event loop to the text box, so events that involve the text box will run a handler. There are two required parameters here: the event and the handler if the event happens. Events include both mouse and keyboard events. The format for an event is a string with < and > surrounding it. In our case we are looking for the
ButtonRelease
event. We could have used a Button
event for a mouse button down. But for several reasons it is better practice to start an event on releasing than pressing the mouse button. When we have a ButtonRelease
event in the text box, Python executes the code self.mouseSelectTextLine
. For the MVC pattern, this handler is still in the view not the view controller. These are changes to internal workings of the text box, so it does not belong in the view controller. We address the view controller next week.
Adding the Handler
We set up a listener for an event, next we need the handeler for that event. Add the following code after the text box’s setters and getters:
# Internal behavior for the text box def mouseSelectTextLine(self, event): mousePosition = "@"+str(event.x)+","+ str(event.y) #get the mouse position self.selectedIndex = self.dataList_textbox.index(mousePosition) #convert to textbox index splitIndex = self.selectedIndex.split('.') #get the row we are on selectedLine = splitIndex[0] self.highlightTextLine(selectedLine)
Unlike previous handlers, event handlers have an event
parameter, which is a special class of event information. It has properties about the event, such as key presses, the mouse’s current position, and the action 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 of a mouse position. For our list box, we will need what character line we are on, so in line 4 we use the .index()
method of a Text widget which takes the mouse coordinates and transforms them into character coordinates in the string "line.column"
.
We do not care about columns, we only need the line. Lines 5 and 6 use Python’s split()
method for strings to convert the index to a list. The split()
method uses a string '.'
for a delimiter. Once a string, we get the first element of the string, which is the row represented as a string. In line 7, we send that row to the method we’ll use to highlight the row.
Adding Tags
The Text box is a bit different from our other widgets when it come to formatting. Text
widgets are small word processors, with a lot more formatting capabilities than a Label
widget. To do this, they use a system of tags to set formatting. A tag consists of a name and a range of the tag. Once set, format the tag with the .tag_config()
method.
The goal is to convert a Text widget to a list box. For a List Box, usually the line selected has a reversed foreground and background. We will create a tag from the beginning to the end of a line to reverse the text there. Text indices are strings. We do not have to convert it to an integer, just concatenate a few constants to it.
Add the following just below the code we just added:
def highlightTextLine(self,selectedLine): self.selectedLine=int(selectedLine) selectedLineStart = selectedLine + ".0" #beginning and end of line selectedLineEnd = self.dataList_textbox.index(selectedLine + ".end") #highlight the line on the box self.dataList_textbox.tag_delete("selectedLine") self.dataList_textbox.tag_add("selectedLine",selectedLineStart,selectedLineEnd) self.dataList_textbox.tag_config("selectedLine",background = "blue",foreground = "white")
Although we do not need a string for our internal processing, the purpose of building a List box is to get an integer of a selected list so we access data by an index. Line 2 above save our integer in a property, and allows us to use our mouse selected row in a few other places.
Lines 3 and 4 creates strings representing our beginning and end positions. Line 3 just adds “.0” to the string, signifying the beginning of the line. Line 4 again uses the .index()
method for Text
to find the index at the end of the line. We could write line 4 without the .index()
method, but it helps with debugging to do it this way.
Lines 6 through 8 add the tag. We are setting up a single selection list box, so every time we select a new row, we set the previous row back to its normal condition by deleting the tag in line 6. In line 7, we add the tag with the .tag_add()
naming the tag selectline and specifying our beginning and end of the tag. In line 8, we change the foreground and background attributes to a reversed state.
With that we can save and run. Add a few penguins and select a line, and then another line:

Adding Tkinter Keyboard Events
Most list boxes allow for keyboard controls. Let’s add the up and down keys to move the selection up or down one line. First add the following two bindings below the mouse binding:
self.dataList_textbox.bind('<Down>',self.kbSelectTextLine) self.dataList_textbox.bind('<Up>',self.kbSelectTextLine)
The strings Up
and Down
are the up and down key events. We’ll use the same handler for both. Between the two methods we just wrote add this:
def kbSelectTextLine(self,event): selected=self.selectedLine if event.keysym == "Up": selectedLine = str(selected - 1) else: selectedLine = str(selected + 1) self.highlightTextLine(selectedLine)
In line 3, we use another property of the event
object .keysym
which returns the symbol for the key pressed. If the user presses the up key, we subtract one and if down, we add one. Save and run. Add some penguins and click with the mouse a line, then use the up and down arrow keys.

Those headings are still a bit of a problem. We can select the headings. They mess up the index we are trying to get out of the text box by adding a few lines. Along with what we can do with the text box in the view controller, that will be the lesson next time.
The Whole Code
Below is the code for our application so far. You can also find it here at Github
# poppers_penguins_MVC_06a #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 elementListToString(self,aList): listString = str() for element in aList: if len(listString)>0: listString = listString + '\t' + element else: listString = element 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 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.selectedLine = 1 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 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) def getTextBoxtext(self): #return a string of all the boxes contents return self.dataList_textbox.get('0.0',END) def getSelectedLine(self): #for use externally as an index. Do not use the property straight return self.selectedLine - 1 # Internal behavior for the text box def mouseSelectTextLine(self, event): mousePosition = "@"+str(event.x)+","+ str(event.y) #get the mouse position self.selectedIndex = self.dataList_textbox.index(mousePosition) #convert to textbox index splitIndex = self.selectedIndex.split('.') #get the row we are on selectedLine = splitIndex[0] self.highlightTextLine(selectedLine) def kbSelectTextLine(self,event): selected=self.selectedLine if event.keysym == "Up": selectedLine = str(selected - 1) else: selectedLine = str(selected + 1) self.highlightTextLine(selectedLine) def highlightTextLine(self,selectedLine): self.selectedLine=int(selectedLine) selectedLineStart = selectedLine + ".0" #beginning and end of line selectedLineEnd = self.dataList_textbox.index(selectedLine + ".end") #highlight the line on the box self.dataList_textbox.tag_delete("selectedLine") self.dataList_textbox.tag_add("selectedLine",selectedLineStart,selectedLineEnd) self.dataList_textbox.tag_config("selectedLine",background = "blue",foreground = "white") #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 #Added event binding to a button release self.dataList_textbox.bind('<ButtonRelease>',self.mouseSelectTextLine) self.dataList_textbox.bind('<Down>',self.kbSelectTextLine) self.dataList_textbox.bind('<Up>',self.kbSelectTextLine) #(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() #(8) 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