Models, Views, and Controllers, Oh My!

Using a StringVar to connect a text-entry box and a label is the first step toward separating models (How do we represent the data?), views (How do we display the data?), and controllers (How do we modify the data?), which is the key to building larger GUIs (as well as many other kinds of applications). This MVC design helps separate the parts of an application, which will make the application easier to understand and modify. The main goal of this design is to keep the representation of the data separate from the parts of the program that the user interacts with; that way, it is easier to make changes to the GUI code without affecting the code that manipulates the data.

As its name suggests, a view is something that displays information to the user, like Label. Many views, like Entry, also accept input, which they display immediately. The key is that they don’t do anything else: they don’t calculate average temperatures, move robot arms, or do any other calculations.

Models, on the other hand, store data, like a piece of text or the current inclination of a telescope. They also don’t do calculations; their job is simply to keep track of the application’s current state (and, in some cases, to save that state to a file or database and reload it later).

Controllers are the pieces that convert user input into calls on functions in the model that manipulate the data. The controller is what decides whether two gene sequences match well enough to be colored green or whether someone is allowed to overwrite an old results file. Controllers may update an application’s models, which in turn can trigger changes to its views.

The following code shows what all of this looks like in practice. Here the model is kept track of by variable counter, which refers to an IntVar so that the view will update itself automatically. The controller is function click, which updates the model whenever a button is clicked. Four objects make up the view: the root window, a Frame, a Label that shows the current value of counter, and a button that the user can click to increment the counter’s value:

 import​ tkinter
 
 # The controller.
 def​ click():
  counter.set(counter.get() + 1)
 
 if​ __name__ == ​'__main__'​:
  window = tkinter.Tk()
 # The model.
  counter = tkinter.IntVar()
  counter.set(0)
 # The views.
  frame = tkinter.Frame(window)
  frame.pack()
 
  button = tkinter.Button(frame, text=​'Click'​, command=click)
  button.pack()
 
  label = tkinter.Label(frame, textvariable=counter)
  label.pack()
 
 # Start the machinery!
  window.mainloop()

The first two arguments used to construct the Button should be familiar by now. The third, command=click, tells it to call function click each time the user presses the button. This makes use of the fact that in Python a function is just another kind of object and can be passed as an argument like anything else.

Function click in the previous code does not have any parameters but uses variable counter, which is defined outside the function. Variables like this are called global variables, and their use should be avoided, since they make programs hard to understand. It would be better to pass any variables the function needs into it as parameters. We can’t do this using the tools we have seen so far, because the functions that our buttons can call must not have any parameters. We will show you one way to avoid using global variables in the next section.

Using Lambda

The simple counter GUI shown earlier does what it’s supposed to, but there is room for improvement. For example, suppose we want to be able to lower the counter’s value as well as raise it.

Using only the tools we have seen so far, we could add another button and another controller function like this:

 import​ tkinter
 
 window = tkinter.Tk()
 
 # The model.
 counter = tkinter.IntVar()
 counter.set(0)
 
 # Two controllers.
 def​ click_up():
  counter.set(counter.get() + 1)
 def​ click_down():
  counter.set(counter.get() - 1)
 
 # The views.
 frame = tkinter.Frame(window)
 frame.pack()
 button = tkinter.Button(frame, text=​'Up'​, command=click_up)
 button.pack()
 button = tkinter.Button(frame, text=​'Down'​, command=click_down)
 button.pack()
 label = tkinter.Label(frame, textvariable=counter)
 label.pack()
 
 window.mainloop()

This seems a little clumsy, though. Functions click_up and click_down are doing almost the same thing; surely we ought to be able to combine them into one. While we’re at it, we’ll pass counter into the function explicitly rather than using it as a global variable:

 # The model.
 counter = tkinter.IntVar()
 counter.set(0)
 
 # One controller with parameters.
 def​ click(variable, value):
  variable.set(variable.get() + value)

The problem with this is figuring out what to pass into the buttons, since we can’t provide any arguments for the functions assigned to the buttons’ command keyword arguments when creating those buttons. tkinter cannot read our minds—it can’t magically know how many arguments our functions require or what values to pass in for them. For that reason, it requires that the controller functions triggered by buttons and other widgets take zero arguments so they can all be called the same way. It is our job to figure out how to take the two-argument function we want to use and turn it into one that needs no arguments at all.

We could do this by writing a couple of wrapper functions:

 def​ click_up():
  click(counter, 1)
 
 def​ click_down():
  click(counter, -1)

But this gets us back to two nearly identical functions that rely on global variables. A better way is to use a lambda function, which allows us to create a one-line function anywhere we want without giving it a name. Here’s a very simple example:

 >>>​​ ​​lambda:​​ ​​3
 <function <lambda> at 0x00A89B30>
 >>>​​ ​​(lambda:​​ ​​3)()
 3

The expression lambda: 3 on the first line creates a nameless function that always returns the number 3. The second expression creates this function and immediately calls it, which has the same effect as this:

 >>>​​ ​​def​​ ​​f():
 ...​​ ​​return​​ ​​3
 ...
 >>>​​ ​​f()
 3

However, the lambda form does not create a new variable or change an existing one. Finally, lambda functions can take arguments, just like other functions:

 >>>​​ ​​(lambda​​ ​​x:​​ ​​2​​ ​​*​​ ​​x)(3)
 6

So how does this help us with GUIs? It lets us write one controller function to handle different buttons in a general way and then wrap up calls to that function when and as needed. Here’s the two-button GUI once again using lambda functions:

 import​ tkinter
 
 window = tkinter.Tk()
 
 # The model.
 counter = tkinter.IntVar()
 counter.set(0)
 
 # General controller.
 def​ click(var, value):
  var.set(var.get() + value)
 
 # The views.
 frame = tkinter.Frame(window)
 frame.pack()
 button = tkinter.Button(frame, text=​'Up'​, command=​lambda​: click(counter, 1))
 button.pack()
 
 button = tkinter.Button(frame, text=​'Down'​, command=​lambda​: click(counter, -1))
 button.pack()
 
 label = tkinter.Label(frame, textvariable=counter)
 label.pack()
 
 window.mainloop()

This code creates a zero-argument lambda function to pass into each button just where it’s needed. Those lambda functions then pass the right values into click. This is cleaner than the preceding code because the function definitions are enclosed in the call that uses them—there is no need to clutter the GUI with little functions that are used only in one place.

Note, however, that it is a very bad idea to repeat the same function several times in different places—if you do that, the odds are very high that you will one day want to change them all but will miss one or two. If you find yourself wanting to do this, reorganize the code so that the function is defined only once.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.76.204