The purpose of today's lab is to explore how to write Graphical User Interfaces (GUIs) using Python and TKinter. We will also explore test-driven development.

The reading guide provided basic documentation on Tkinter and the lab will provide code snippets, but if you are looking for additional documentation a good starting point is available on the Python site: https://docs.python.org/3.4/library/tkinter.html

Begin by making a new package for today's lab named lab11.

Test driven development

In some cases, the best way to specify the actions of an object is to give a set of test cases that a completed implementation should pass. A software development cycle that begins with a test, and then writes an implementation to pass the tests is considered test driven development. As additional functionality is determined necessary, first, a new test is developed, and then the development work commences.

We will explore this software cycle (very briefly) by developing a model of a simple calculator.

Exercise 11.1

Begin by copying (or downloading) the following modules:

Take a minute to review the contents of these files and note the following:

Your job is to reimplement the calculate() method so that all of the tests pass. Note that you should not change the unit tests, only the calculator class.

When all of the tests pass, we have a functional method in our Calculator class that we can use to do some calculations. Let’s add a graphical user interface to let a user do calculations using this calculator.

A Graphical User Interface

When beginning a graphical application, it is usually a good idea to sketch out what the interface will look like, so you have an idea of where you are heading. Today, let's head for something like this:

Notice that this mockup (a photograph of a hand-sketch) draws the elements of the GUI informally and doesn’t spend time making it look perfect. The labels are simple text, the buttons are boxes with text and the entry fields are empty boxes. We just want a general idea of where things will end up so we have a reference.

Exercise 11.2

Start building the GUI structure from top to bottom as follows:

  1. Create a new module in your lab11 package named gui
  2. We will use the tools in TKinter and the Calculator, so bring all of those tools into the current namespace using
    from tkinter import *
    from calculator import *
  3. Add a driver script at the bottom of the module similar to the ones used in the text and class examples.
    if __name__ == '__main__':
        root = Tk()
        root.title('Calculator')
        app = Gui(root)
        root.mainloop()

    This creates the GUI application class, names it “Calculator” and starts the user event loop.

  4. Create a class named Gui and add a constructor that receives a window object from its calling program and then creates and places widgets on that window as specified by this algorithm:
    1. Create an instance variable for the calculator object.
      self._calc = Calculator()

      This object will perform all the calculations for the GUI interface.

    2. Create a label and entry for the first number to be entered. You can use this code:
      input1_label = Label(window, text="Input 1:")
      input1_label.grid(row=0, column=0, sticky=E)
      input1_entry = Entry(window, width=6)
      input1_entry.grid(row=0, column=1, sticky=W)
    3. Create a similar label and entry for the second number to be entered.

This much should build the first two “lines” of the GUI structure. There are no associated commands, but it is a good first iteration. Check that the application works as expected before continuing.

As we consider our next iteration of development (continuing from top to bottom) we realize that radio buttons would be more appropriate for choosing the operation that the buttons we drew in the mockup (because we only ever want to allow one calculation at a time). We will still make it explicit to the user which button has been chosen, but the radio button interface will be a clue to the user that only one will be valid at a time and will prevent them from entering anything other than the legal operators (i.e., +, -, *, /).

Exercise 11.3

Update your constructor to add radio buttons for choosing the operation, and labels to show what the chosen operation is at any time.

We may want to move the buttons around (or reformat them) as a group, so we'll create a substructure and put the operation buttons together there. Do this using the following algorithm:

  1. Create a Frame object to attach to the window:
    operator_frame = Frame(window)
    operator_frame.grid(row=2, column=0, columnspan=2)
  2. Create a new StringVar instance variable called self._operator that will be shared between all of the radio button objects. Give this variable a value of '+' using the set() method.
  3. Create a new Button for the addition operation and attach it to the frame on the left side. You can use this code:
    add_button = Radiobutton(operator_frame, text="+", variable=self._operator, value='+')
    add_button.pack(side=LEFT)
  4. Create buttons for subtraction (-), multiplication (*) and division (/) and put them in the frame.

Run your driver and verify the following:

With our basic layout in place, let's add both the structure and the calculation in our next iteration.

Exercise 11.4

Our goal in this step is to use the calculator that we have carefully tested in Exercise 11.1 to do our calculations. We also want these results to appear on the screen. Our calculator is ready to go (so it will not change!!), but the driver application needs some work. Start with the graphical structure by adding the following steps to the constructor algorithm started above:

  1. Add a button to the main window (not to the radio button frame) to perform the calculation and put it in the fourth row, first column.
  2. Add a label to the window with the text 'Result:' and put it in the fourth row, second column.
  3. Create a StringVar called self._result
  4. Create a Label whose text matches self._result, and put it in the fourth row to the right of its label. You can do this using this code:
    self.result_label = Label(window, textvariable=self._result, width=6)
    self.result_label.grid(row=3, column=2, sticky=W)

We are now (finally) ready to actually do something with our interface. In particular, we want to perform the calculation indicated by the text fields and chosen operation when the user presses the Calculate button.

  1. Find your definition of input1_entry. We want to be able to access the value the user types in this field, so add a new instance variable called self._input1 which is an instance of StringVar. Associate this variable with input1_entry as you did before, by adding the argument textvariable=self._input1 to its constructor.
  2. Repeat the previous step for input 2.
  3. Find the code that creates the calculate button and add the argument command=self.do_calculation
  4. Create a method called do_calculation that implements the following algorithm:
    1. Use the calculate() method of the self.calc object to perform the calculation specified by the interface. You can use this code (if you named your variables as specified earlier):
      result = self._calc.calculate(self._input1.get(), self._operator.get(), self._input2.get())
    2. Change the result label to match the value of result. You can use:
      self._result.set(result)

Check that your calculator GUI works!

At this point, you should have a functioning calculator — as long as the user correctly enters numbers in the text fields, and never attempts to divide by zero. Of course, we shouldn’t be assuming we have perfect users! We have not specified what should happen if something goes wrong. Let’s remedy this shortcoming.

Exercise 11.5

Using the techniques you have practiced in earlier exercises, add a label to the bottom of the GUI that will indicate if any errors occur. This label should initially welcome the user (e.g. “Come on! Let’s calculate!”) and then be updated if an error occurs to give an indication of what went wrong. If no errors occur during a calculation, the label should remain (or return to) the initial greeting.

NOTE: you should handle the error by "wrapping" your code with a try: / except: block. The question is: what action causes the error to be generated? The answer: the call to do_calculation(). So, that is the place where you should handle the error and update the error message label.

The final optional element of today’s lab is to add a memory component to the calculator.

EXTRA CREDIT

Update the calculator class to access and change the memory instance variable. You must define an accessor and a mutator. Then, in the calculate method, if either of the operands is the letter “M”, the value in memory should be used. Add init tests that demonstrate the correctness of your implementation.

Over in the GUI, add a new row with a button that allows the user to store the current result into the calculator memory and a label that indicates the current value stored in memory.

Checking In

Submit all the code and supporting files for the exercises in this lab. We will grade this exercise according to the following criteria:

If you’re working on a lab computer, don’t forget to log off of your machine when you are finished!