• 0c51ca2e09bf34278aa7eb8b39339789?s=80&d=mm

    Changes from antonc

    antonc - over 1 year ago (Nov 12, 2015, 1:01 AM)
    Correction to variable name to fix undefined error
  • Changes have been accepted Merged
      Looks like something's not quite right here.. We've been notified of the issue, and will get back to you soon.
      year-13/graphical-user-interfaces
      # Graphical User Interfaces

      We are finally up to the part where we will create an interface for our program. Designing program interfaces is a massive subject, and designing useful and great looking interfaces is another subject in itself. We are only going to look at the basic tools of graphical user interfaces (which I will now refer to as GUIs, pronounced gooey) and we won't be worrying about making good looking interfaces.

      There are many different tools you can use to create GUIs, but we will be using the one included in Python 3 called 'tkinter'. It's definitely not the greatest one out there, but it does the job nicely for our programs. The language style doesn't match perfectly to Python so you may notice some small differences.

      ##Event Driven Programming

      When writing a program to be used through a GUI we need to change from sequential programming to event driven programming. A program using sequential programming will always do things in the same order each time it is run. These are the programs we started off writing when learning Python, like a shopping calculator for example. But event driven programs, the user controls the behaviour of the program. When we create a program with a GUI, our code will:

      1. Setup the program up and display the GUI.
      2. Wait for an action from the user.
      3. Do something for that action.
      4. Go back to step 2.

      When using a GUI system, there are three major tools they give us:

      - Interface components/Widget set: These are the things you see on an interfaces like windows, buttons, text boxes, scroll bars, radio buttons. There are loads of different components, but we will just be looking at a small selection of these.
      - Geometry management: This is the system for putting the components above together. There are two geometry managers we can use: pack or grid. We will be using one that use a grid layout as it's the easiest to understand.
      - Event binding: This gives us the ability to trigger some code from an action on a component.

      Let's look at creating a simple interface to start off, and see how these tools tie together:

      ##Our First Interface: Hello World Visualised

      Let's create an interface which has the words "Hello World!" displayed. Have a look at the code below, type it out and run it for yourself, and see if you can decipher it. We are going to write small snippets of test code, but later will wrap all this code in a GUI class (but more on that later!).

      ```python
      from tkinter import *
      from tkinter.ttk import *

      window = Tk()
      hello_label = Label(window, text='Hello World!')
      hello_label.grid(row=0, column=0)
      window.mainloop()
      ```

      The first two lines of our program import the GUI toolkit from the Python libraries. The terminology stands for 'from the tkinter python library, import everything in the library' as the star symbol stands for everything/wildcard. The second line imports some extra features for tkinter that we will use.

      The line `window = Tk()` creates a window by calling the main tkinter function `Tk()`. We assign this to a variable, so when we create other components we can tell them to be displayed in this window. In the interfaces we will be creating, we will only be using single window interfaces, so we will be placing all components into this window.

      The sixth line of the program is `hello_label = Label(window, text='Hello World!')` and there is quite a bit happening here, so let's break it down. We are creating an Label component which is used to display text and it is taking two parameters. The first is the window the component should be placed into (but it isn't placed into it just yet), in this case we use the window variable we created above. The next parameter is setting the text to "Hello World!", without this parameter the label would be blank! We finally save this Label in the variable `hello_label`.

      The seventh line of the program places the component we created with the grid geometry manager. We run the grid function on the variable containing the label, which takes two number parameters, the row and column position. It works like map co-ordinates, but it starts counting from 0. Check the table below for an explaination.

      ||Grid Layout||
      |:-----------:|:-----------:|:-----------:|
      | row 0 col 0 | row 0 col 1 | row 0 col 2 |
      | row 1 col 0 | row 1 col 1 | row 1 col 2 |
      | row 2 col 0 | row 2 col 1 | row 2 col 2 |

      The last line in the main function `window.mainloop()` triggers the function on the window to start waiting for actions from the user. The program will stop at this line of code until the window is closed, so this will be the last line our the programs.

      ##Tkinter Components

      Let's take a look at the several components tkinter offers that we will be using in our programs.

      ###Variables

      When writing a program using tkinter, you can ask the interface to update a component when a certain variable is changed. There’s no way to track changes to Python variables, but tkinter allows you to create special variable types that can be used wherever tkinter requires a variable.

      We will need to use these variables as there will be components of the interface the will need to change. If you remember our Label above, it was set to one value on creation. Instead of connecting a component to a value, we will start connecting these to variables which we can update. If a Label doesn't need to change (like a heading) then we could just use the text property.

      Tkinter has `StringVar` for strings and `IntVar` for integers (there is also `BooleanVar` and `DoubleVar` if you need them). We can set the value of these variables by running a `.set(value)` function on them with a value. We can also run `.get()` to get the current value. Let's see this in action by setting a label to a variable:

      ```python
      from tkinter import *
      from tkinter.ttk import *

      window = Tk()
      data = StringVar()
      data.set('Hello World!')
      hello_label = Label(window, textvariable=data)
      hello_label.grid(row=0, column=0)
      window.mainloop()
      ```

      At the moment this seems fairly pointless, but as we introduce input methods like buttons and text entries then these variables are extremely useful.

      ###Label

      We have already been using the label component and have seen the two parameters we will be using:

      - **text** - Displays one or more lines of text. To use a line break, use a newline character (`¶
      `)
      - **textvariable** - Sets a variable to display text on this label.

      There are loads of extra parameters that allow you to customise height, width, fonts, and colours but these aren't necessary for the assessment. You can [read more about these parameters here](http://effbot.org/tkinterbook/label.htm).

      ###Button

      Buttons are extremely useful and we will using plenty of these in our interfaces. We can create a button with the code `hello_button = Button(window, text="Say hello", command=sayHello)`. It's very similar to creating a Label with a new parameter. The new parameter is called command, and determines what function to run when the button is clicked, in this case it's `sayHello()`.

      The last parameter in creating a button is `command=sayHello`. This the name of the function to run when the button is pressed. Note it's not a function call (there are no brackets after it).

      With all that knowledge, let's try out a small example:

      ```python
      from tkinter import *
      from tkinter.ttk import *

      def sayHello():
      data.set("Hello World!")

      window = Tk()
      data = StringVar()
      hello_label = Label(window, textvariable=data)
      hello_label.grid(row=0, column=0)
      hello_button = Button(window, text="Say hello", command=sayHello)
      hello_button.grid(row=1, column=0)
      window.mainloop()
      ```

      In this case the program starts with a blank label, but if the user clicks the button it will set the label text.

      ---

      ####Exercise: Graphical Greetings

      Using the program above as a starter, create a program that has two buttons and a label. One button should change the text label to "Hello!", while the other button changes the text to "Bye!".

      ---

      ####Exercise: Two Function Increments

      Create a program that uses a label and two buttons, that keeps track of a number counter, using an IntVar variable type. One button should add one onto the value, while the other button should subtract one from the value. When designing the functions for each button, we need to _get_ the current value, _change_ the value depending on the button press, then _set_ the new value.

      >```python
      >from tkinter import *
      >from tkinter.ttk import *
      >
      >def incrementValue():
      > # Your code here
      >
      >def decrementValue():
      > # Your code here
      >
      >window = Tk()
      >value = IntVar()
      >value.set(0) # Setting the starting value is good practice
      >value_label = Label(window, textvariable=value)
      >value_label.grid(row=0, column=0, columnspan=2)
      >value_up_button = Button(window, text="+1", command=incrementValue)
      >value_up_button.grid(row=1, column=0)
      >value_down_button = Button(window, text="-1", command=decrementValue)
      >value_down_button.grid(row=1, column=1)
      >
      >window.mainloop()
      >```

      ---

      **Using buttons that trigger functions with parameters**

      The command property of a button can only take a name of a function. A common mistake is entering the function by `command=sayHello()` which calls the function before the component is created, gets the return value and converts this to a string. Then on a button click, tkinter searches for a function with the name of the string, definitely not what we want.

      What we can use are helper functions that call our function with parameters. For example if we have a greeting program that had multiple buttons that greeted differently, we could have a function for each that changes the label. But to reduce repetition in our code, we can have a function called `change_message(greeting)` that takes a greeting to display. Check out the code below for an example:

      ```python
      from tkinter import *
      from tkinter.ttk import *

      def change_mMessage(greeting):
      message_var.set(greeting)

      def ahoy():
      changeMessage("Ahoy!")

      def avast():
      changeMessage("Avast ye!")

      def aye():
      changeMessage("Aye aye!")

      window = Tk()
      message_var = StringVar()
      message_label = Label(window, textvariable=message_var)
      message_label.grid(row=0, column=0, columnspan=3)
      ahoy_button = Button(window, text="Greeting 1", command=ahoy)
      ahoy_button.grid(row=1, column=0)
      avast_button = Button(window, text="Greeting 2", command=avast)
      avast_button.grid(row=1, column=1)
      aye_button = Button(window, text="Greeting 3", command=aye)
      aye_button.grid(row=1, column=2)
      window.mainloop()
      ```

      > For those who are interested, you can simplify the call to a helper function by creating it inline with a lambda expression like `command=lambda: changeValue(-1)`

      ---

      ####Exercise: Helper Functions

      Complete the program below by entering functions so the program can modify the value either by adding 1, adding 5, subtracting 1, or subtracting 5. You are only allowed to use one `.set()` and one `.get()` functions.

      ```python
      from tkinter import *
      from tkinter.ttk import *

      # Write your functions here

      window = Tk()
      value_var = IntVar()
      value_label = Label(window, textvariable=value_var)
      value_label.grid(row=0, column=0, columnspan=4)
      subtract_five_button = Button(window, text="- 5", command=subtractFive)
      subtract_five_button.grid(row=1, column=0)
      subtract_one_button = Button(window, text="- 1", command=subtractOne)
      subtract_one_button.grid(row=1, column=1)
      add_one_button = Button(window, text="+ 1", command=addOne)
      add_one_button.grid(row=1, column=2)
      add_five_button = Button(window, text="+ 5", command=addFive)
      add_five_button.grid(row=1, column=3)
      window.mainloop()
      ```

      ---

      ###Entry

      Getting input from the user is crucial to many programs, and a text entry box is a great way to get a wide range of inputs through an interface. It connects to a tkinter variable like a Label.

      ```python
      age_var = IntVar()
      age_entry = Entry(window, textvariable=age_var)
      age_entry.grid(row=2, column=0)
      ```

      Users can enter any symbols (text, numbers, whitespace, etc) into this box. When running a function from a button press, we can get the text in the entry by calling `.get()` on the variable. If the values in the entry are not valid with the variable type (for example, letters in a IntVar) then the program will through an exception when calling `.get()`. This is important to remember when choosing variable types, and we will look at how to hand these exceptions later.

      ---

      ####Exercise: Saying Hello

      Create a program that resembles the image below. The interface uses one text entry for a name, one button, and two labels. The button should have the text 'Say hello' and when the user clicks the button, the bottom label should display the name with 'Hi' in front of it.

      ![gui-saying-hello-interface.png](images/gui-saying-hello-interface.png)

      ---

      ####Exercise: Doubler

      Create a program that uses a text entry for the user to enter a number and has a button called "Double it!" that, when clicked, takes the number currently in the text field, doubles it, and displays the result using a label. It should look like the image below:

      ![gui-doublerpng.txt](images/gui-doublerpng.txt)

      >from tkinter import *
      >from tkinter.ttk import *
      >
      >def doubleValue():
      > value_str = str(value.get())
      > double_str = str(value.get() * 2)
      > result.set(value_str + " times 2 is " + double_str)
      >
      >window = Tk()
      >value = IntVar()
      >value.set(0) # Setting the starting value is good practice
      >
      >value_entry = Entry(window, textvariable=value)
      >value_entry.grid(row=0, column=0)
      >
      >double_button = Button(window, text="x 2", command=doubleValue)
      >double_button.grid(row=1, column=0)
      >
      >result = StringVar()
      >result_label = Label(window, textvariable=result)
      >result_label.grid(row=2, column=0)
      >
      >window.mainloop()

      ---

      ###Combo Box

      We can use an entry to get input from the user, but sometimes it's nicer to restrict the input choices. A combo box (commonly called a dropdown) can give the user the choice of picking an item from a list of provided choices. Let's look at an example combo box and break it down:

      ```python
      fruit_choices = ["Apple", "Banana", "Cherry", "Dragon Fruit"]
      selected_fruit = StringVar()
      selected_fruit.set(fruit_choices[0])
      fruit_combo = Combobox(window, textvariable=selected_fruit, values=fruit_choices)
      fruit_combo.grid(row=0, column=0)
      ```

      When creating a combo box there are two properties we need to use. The first is a textvariable as we have used before, and it is set to the currently selected item. The other property is `values`, which is the options the combo box should display. This needs to be given as a list of strings, so the first line of the code above creates a list of options. When a combo box is created it doesn't start with anything selected, so the third line above sets the variable to the first option of our choices.

      ---

      ####Exercise: Calculator

      Create a basic calculator program that uses two entries, a combo box, a label, and a button to do basic calculations. The combo box should have the options for addition, subtraction, and multiplication. See the image below for how the calculator could look.

      ![gui-calculator.png](images/gui-calculator.png)

      >from tkinter import *
      >from tkinter.ttk import *
      >
      >def calculate():
      > selected = operation_var.get()
      > if selected == "+":
      > result = value_1_var.get() + value_2_var.get()
      > elif selected == "-":
      > result = value_1_var.get() - value_2_var.get()
      > else:
      > result = value_1_var.get() * value_2_var.get()
      > result_var.set(result)
      >
      >window = Tk()
      >
      >value_1_var = IntVar()
      >left_entry = Entry(window, textvariable=value_1_var)
      >left_entry.grid(row=0, column=0)
      >
      >operations = ["+","-","x"]
      >operation_var = StringVar()
      >operation_var.set(operations[0])
      >operations_combobox = Combobox(window, textvariable=operation_var, values=operations)
      >operations_combobox.grid(row=0, column=1)
      >
      >value_2_var = IntVar()
      >right_entry = Entry(window, textvariable=value_2_var)
      >right_entry.grid(row=0, column=2)
      >
      >result_var = StringVar()
      >result_label = Label(window, textvariable=result_var)
      >result_label.grid(row=1, column=0, columnspan=3)
      >
      >calculate_button = Button(window, text="Calculate", command=calculate)
      >calculate_button.grid(row=2, column=0, columnspan=3)
      >
      >window.mainloop()

      ---

      There are many more components including check buttons, scrollbars, menus, frames, and canvases however the components we looked at above will be fine for what we wish to achieve.

      > Possibly could talk about rowspan, columnspan, and formatting of labels (with font etc).


      ##Exceptions on input?

      ##Objects and GUIs

      Nearly all the example programs so far have used code in the global scope. But we want to avoid globals for a well designed program!

      The solution is to wrap the GUI code in a class in which the event handlers are methods of the class and the widgets are the instance variables. Since methods have access to all an object's instance variables, we have neatly solved the problem.

      Inspect and run the following code, which is a previous example re-written in an OO style:

      ```python
      from tkinter import *
      from tkinter.ttk import *

      class GreetingGui(object):

      def __init__(self, window):
      """Setup the label and button on given window"""
      self.window = window
      self.data = StringVar()
      self.hello_label = Label(self.window, textvariable=self.data)
      self.hello_label.grid(row=0, column=0)
      self.hello_button = Button(self.window,
      text="Say hello",
      command=self.say_hello)
      self.hello_button.grid(row=1, column=0)

      def say_hello(self):
      """The event handler for clicks on the button"""
      self.data.set("Hello World!")

      def main():
      """Set up the GUI and run it."""
      window = Tk()
      greeting_gui = GreetingGui(window)
      window.mainloop()

      main()
      ```

      **Note the following:**

      1. The instance variables of the class, i.e. the variables like `self.data`, define the GUI state.
      2. The methods of the class are the various event handlers (and support methods).
      3. All event handlers have access to the GUI state via the self parameter.
      4. The `main` function creates the window and triggers the main loop.

      ---

      ####Exercise: Counting in OO

      Recreate the 'Two Function Increments' program from earlier that counts but encapsulate the GUI code in a class CounterGui. This program should have all the same functionality as the previous program: The program has two buttons and a label that displays the current value of a counter. One button, bearing the text +1, should add one to the counter, while the other button, labelled -1, should subtract one from it. The counter should start at zero.

      Here is a template to get you started:

      ```python
      from tkinter import *
      from tkinter.ttk import *

      # Write your code here

      def main():
      """Set up the GUI and run it"""
      window = Tk()
      counter_gui = CounterGui(window)
      window.mainloop()

      main()
      ```

      ---

      ##Projects