23. Graphical user interfaces

[status: somewhat fleshed out, but not complete]

Prerequisites

  • The 10-hour “serious programming” course.

  • A GNU/Linux system with python3 and python3-tk installed (on an Ubuntu 16.04 system this can be done with sudo apt install python3-tk)

23.1. A chat about sources of input in a GUI

The programs we first write with text-based user interfaces, like the tic-tac-toe program in the basic course, have a very simple flow:

  1. You get input from the keyboard (and only from the keyboard).

  2. You do some processing based on that input.

  3. You write some output to the terminal.

Later, in Section 2, we learned to write programs which read data from a file. Still, even in this kind of program you are always only reading input from one place at a time.

At this point I talk to the class about how there are two types of programs that do things quite differently: network programs and GUI programs.

In network programs you can have connections open to several different hosts, and you might be reading input from several of them at once.

In A GUI program you can have input from the keyboard, you can also have mouse movement and mouse clicks. The program also has to track other events that can affect its behavior, like the movement of a window so that a different portion of it is exposed.

GUI programs often use what’s called an *event loop*: you set up your programs layout with windows and widgets and then go in to an infinite loop where you call a function that checks if any events (mouse, keyboard, …) have occurred.

In discussing this I would then to go the whiteboard and draw an example of a simple GUI with a couple of buttons. With that up I would discuss what happens when the user clicks on something?

This leads to the discussion of a *callback* or *callbacks function*.

23.2. Widgets and widget sets

Discuss what are widgets? Little self-contained user interface bits (like buttons and text entry fields and canvases) which you can place in your program’s graphical interface.

As is often the case in the free software world, there is a dizzying array of different *widget sets* available for programming in Python. Tkinter, wxPython, PyQt, PyGTK, PySimpleGui, Kivy, …

The most basic widget set, Tkinter, is included as a standard part of Python. It allows you to develop reasonable comprehensive graphical interfaces, so I will use that as our starting point.

Another which has gained adoption since 2018 is PySimpleGUI, and we will give it a quick glance at the end of this chapter.

23.3. The simplest programs

23.3.1. The programs

Simple single OK button program.

Listing 23.3.1 ok-simplest.py - program with an OK button.
#! /usr/bin/env python3

import tkinter as tk

root = tk.Tk()
okButton = tk.Button(root, text='OK')
okButton.pack()

root.mainloop()

Sure, but that does nothing. Let’s make it do something when you press the button:

Listing 23.3.2 ok-callback.py - program with an OK button that does something.
#! /usr/bin/env python3

import tkinter as tk

def printOK():
    print('OK!!')

def main():
    root = tk.Tk()
    okButton = tk.Button(root, text='OK', command=printOK)
    okButton.pack()

    root.mainloop()

main()

Now let’s make it able to quit with a “Quit” button:

Listing 23.3.3 ok-callback-quit.py - program with an OK button that does something simple, and a Quit button that exits.
#! /usr/bin/env python3

import tkinter as tk

def printOK():
    print('OK!!')

def main():
    root = tk.Tk()
    okButton = tk.Button(root, text='OK', command=printOK)
    okButton.pack()
    quitButton = tk.Button(root, text='Quit', command=root.destroy)
    quitButton.pack()

    root.mainloop()

main()

Observing this simple program raises a couple of questions about things we saw in it:

23.3.2. Packers: more than just one button

One concept in GUI programs is *geometry management*. How does the program lay out all the widgets?

A programmer could say: put this widget at coordinages (12, 74), and this other one at coordinates (740, 210), and so forth. This would be terrible style. It would do the wrong thing if the window gets resized, and it would become impossible to maintain when there are more widgets.

Widget systems introduce the idea of “geometry management” to deal with this. The calls to button.pack() that you saw in ok-callback-quit.py are an example. We told the widgets to pack themselves inside their parent window, and much was taken care of automatically by doing that. We did not, for example, have to specify the position of the buttons in the window. If you resize you will notice that the buttons are kept in somewhat reasonable positions.

As we go on our tutorial tour of widgets let us pay attention to what happens with the packing of widgets inside windows.

23.3.3. A tour of widgets

We only saw button widgets, but this is a chance to point out what other widgets there are. It’s hard to get an exhaustive list since one can write custom widgets, but here are some to mention:

  • button

  • canvas

  • checkbutton

  • combobox

  • entry

  • frame

  • label

  • labelframe

  • listbox

  • menu

  • menubutton

  • message

  • notebook

  • tk_optionMenu

  • panedwindow

  • progressbar

  • radiobutton

  • scale

  • scrollbar

  • separator

  • sizegrip

  • spinbox

  • text

  • treeview

23.4. Following a tutorial

We will now follow this tutorial:

https://www.tutorialspoint.com/python3/python_gui_programming.htm

This tutorial has links to most of the basic Tkinter widgets, with examples of how to use each one. In the course I have the students bring them up one by one, pasting them in to the python3 interpreter to see them at work.

The one that might be most interesting is the Scale widget: it shows three interacting widgets where the slider sets a value, the button then reads that value and causes a label to be updated. I would go through that example in greater detail.

23.5. Cellular automata on a canvas

Students might want to read this before going through this chapter:

https://en.wikipedia.org/wiki/Elementary_cellular_automaton

23.5.1. A simply drawing of the CA

We have an example of a cellular automaton elsewhere in the book (Section 27). We can use the routines in that program to compute the cellular automaton, and just add the graphical portion in this program.

Download the simple_ca.py program from Section 27.3 and save it in a file called simple_ca.py.

Then take a look at the draw_ca.py program in Listing 23.5.1 and try running it.

Listing 23.5.1 draw_ca.py - draws a cellular automaton graphically.
#! /usr/bin/env python3

"""draw a cellular automaton"""

import time
import math
import sys

sys.path.append('../emergent-behavior')
from simple_ca import *

## we use the tkinter widget set; this seems to come automatically
## with python3 on ubuntu 16.04, but on some systems one might need to
## install a package with a name like python3-tk
from tkinter import *

def main():
    ## how many steps and cells for our CA
    n_steps = 200
    n_cells = 200
    ## we will make each cell be 4x4 pixels
    canvas_width = 4*n_cells
    canvas_height = 4*n_steps

    ## prepare a basic canvas
    root = Tk()
    ca_canvas = Canvas(root, 
                       width=canvas_width,
                       height=canvas_height)
    ca_canvas.pack() # boiler-plate: we always call pack() on tk windows

    # row = set_first_row_random(n_cells)
    # row = set_first_row_specific_points(n_cells, [40])
    row = set_first_row_specific_points(n_cells, [12, 40, 51, 52, 60, 110, 111,
                                                  160, 161, 162, 163, 164, 165,
                                                  166, 167, 168, 169, 170, 171, 177])
    # row = set_first_row_specific_points(n_cells, list(range(int(n_cells/2), n_cells)))
    # row = set_first_row_specific_points(n_cells, [12, 13, 50, 51, 70, 71, 99, 100])

    ## now set the rule
    rule = '01101000'           # the basic rule
    # rule = '00011110'           # the famous rule 30
    # rule = '01101110'           # the famous rule 110

    draw_row(ca_canvas, 0, row)
    for i in range(1, n_steps):
        row = take_step(rule, row)
        draw_row(ca_canvas, i, row)
    mainloop()

def draw_row(w, pos, row):    
    color_map = ['black', 'white', 'red', 'green', 'blue', 'yellow']
    for i, cell in enumerate(row):
        color = color_map[cell % len(color_map)]
        w.create_rectangle(4*i, 4*pos, 4*i+4, 4*pos+4, fill=color)
        w.update()
        ## update the canvas
        # if i % 10 == 0:
        #     w.update()
    w.update()


if __name__ == '__main__':
    main()

Change the initial conditions, the size of the automaton, experiment with it.

Exercise 23.1

Modify draw_ca.py to have a first row editor: a small canvas which allows you to modify the cells in the first row so that you can run any configuration you choose.

Exercise 23.2

Modify draw_ca.py to have a rule editor: a small canvas which allows you to modify the rule for the cellular automaton evolution. You should also have widgets to allow a different number of cells, a different number of rows, and more or less states or neighbors.

Exercise 23.3

You might have noticed that the drawing of the canvas gets slower and slower as you have more and more squares. You can get around this by commenting out the w.update() call so that the cellular automaton only gets drawn at the end of the run, but then you miss out on seeing the progression of the cellular automaton. Modify draw_ca.py to be more efficient in drawing the canvas. You can use the information at this stack overflow link as a guide:

https://stackoverflow.com/questions/10515720/tkinter-canvas-updating-speed-reduces-during-the-course-of-a-program

23.5.2. Adding controls to the program

Then save draw_ca_with_controls.py and study it and run it. At this time the program is not yet too clean (FIXME: update this text when I update the program), but we can discuss the new mechanisms that appear in this program:

  • The canvas does not just run automatically: we control it with the buttons.

  • We use the canvas’s after() function to make the canvas draw “in the background” while the controls are still active. This means we can pause, for example.

Exercise 23.4

Introduce a “first row editor” widget between the controls and the canvas. This recognizes mouse clicks and lets you set the initial cell values so you don’t have to set them with code in the program.

Exercise 23.5

introduce a “rule editor” widget, possibly on the same row as the control widgets. You could start by just taking a number between 0 and 255. Them move on to 8 squares or checkboxes, where you would click on them to activate the binary cells that end up in the rule string. But the coolest might be to make a widget that shows the 3 cells and their child, with the 8 possible 3-cell configurations, and picking if they turn in to 1 or 0.

23.6. Conway’s game of life

Goal: create a canvas which allows you to click to select initial cell values. Then kick off Conway’s game of life rule.

I don’t have much explanatory text yet, but for now let’s discuss the program we have.

Download the conway_life.py program from Section 27.3 and save it in a file called conway_life.py.

Then save draw_conway_life.py.

We study the programs together and see how the GUI version works, and then run it.

23.7. Tic-tac-toe with buttons

Listing 23.7.1 ttt-gui.py - GUI for the tic-tac-toe program we wrote in the basic Python course.
#! /usr/bin/env python3

import sys
import os
import tkinter as tk

import tic_tac_toe as ttt

class TTTGui(tk.Frame):
    def __init__(self, parent, _xHuman, _yHuman):
        """Draw the initial board"""
        tk.Frame.__init__(self, parent)
        ## set up some constant variables that last across games
        self.imBlank = tk.PhotoImage(file='green_background.png')
        self.imX = tk.PhotoImage(file='green_back_X.png')
        self.imO = tk.PhotoImage(file='green_back_O.png')
        self.marker2image = {' ': self.imBlank,
                             'x': self.imX,
                             'o': self.imO}
        ## zero out 
        self.ResetGame(_xHuman=_xHuman, _yHuman=_yHuman)
        ngB = tk.Button(self, text='New game', command=lambda:
                        self.ResetGame(_xHuman=_xHuman, _yHuman=_yHuman))
        ngB.grid(row=4, column=0)
        qB = tk.Button(self, text='Quit', command=self.Quit)
        qB.grid(row=4, column=1)
        self.grid(sticky=tk.N+tk.S+tk.E+tk.W)

    def ResetGame(self, _xHuman=True, _yHuman=False):
        ## rest the game state variables
        self.bd = ttt.new_board()
        self.gameState = {'xHuman': _xHuman,
                          'yHuman': _yHuman,
                          'toMove': 'x',
                          'winner': ttt.find_winner(self.bd)}
        ## set up the board buttons from scratch
        self.buttons = [[None, None, None],
                        [None, None, None],
                        [None, None, None]]
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(self, image=self.imBlank)
                self.buttons[row][col].grid(row=row, column=col)
                ## we bind the button to the PlaceMarker() method,
                ## making sure to pass it the row and column
                self.buttons[row][col].bind('<Button-1>',
                                            lambda event, row=row, col=col:
                                            self.PlaceMarker(row, col))
        self.UpdateBoard()

    def PlaceMarker(self, row, col):
        if self.gameState['winner'] != ' ':
            print('GAME_IS_WON, not placing marker')
            return
        if ttt.board_is_full(self.bd):
            print('BOARD_IS_FULL, not placing marker')
            return
        if self.bd[row][col] == ' ':
            print('PLACE: %d, %d' % (row, col))
            ttt.set_cell(self.bd, row, col, self.gameState['toMove'])
            self.HandlePossibleEnd()
            self.UpdateBoard()
            self.gameState['toMove'] \
                = ttt.next_marker(self.gameState['toMove'])
            if self.gameState['winner'] == ' ':
                if (not self.gameState['xHuman']
                    or not self.gameState['yHuman']):
                    if not ttt.board_is_full(self.bd):
                        self.TriggerComputerMove()
        else:
            print('ILLEGAL: %d, %d' % (row, col))
            pass                # invalid move
        self.HandlePossibleEnd()

    def UpdateBoard(self):
        for row in range(3):
            for col in range(3):
                image = self.marker2image[self.bd[row][col]]
                self.buttons[row][col].configure(image=image)
        self.HandlePossibleEnd()
    
    def TriggerComputerMove(self):
        ttt.play_computer_opportunistic(self.bd, self.gameState['toMove'])
        self.gameState['toMove'] = ttt.next_marker(self.gameState['toMove'])
        self.UpdateBoard()
        self.HandlePossibleEnd()

    def HandlePossibleEnd(self):
        self.gameState['winner'] = ttt.find_winner(self.bd)
        print('WINNER: <%s>' % self.gameState['winner'])
        if ttt.board_is_full(self.bd):
            print('I *should* put up some info')
            print('WINNER: <%s>' % self.gameState['winner'])

    def Quit(self):
        self.master.destroy()

def main():
    app = TTTGui(tk.Tk(), True, False)
    app.mainloop()

if __name__ == '__main__':
    main()

You can use that GUI program with the underlying text-based program we wrote in the basic Python course: tic_tac_toe.py

23.8. A glance at PySimpleGUI

Start with:

$ pip3 install PySimpleGUI

Following the tutorial at https://realpython.com/pysimplegui-python/ (archived at https://web.archive.org/web/20231225142347/https://realpython.com/pysimplegui-python/ ) let us create ok_psg.py:

Listing 23.8.1 ok_psg.py - simple introductory program for the PySimpleGUI widgets.
#! /usr/bin/env python3

import PySimpleGUI as sg

# layout = [[sg.Text("Hello from PySimpleGUI")], [sg.Button("OK")]]
layout = [[sg.Button("OK")]]

# Create the window
window = sg.Window("OK button", layout)

# Create an event loop
while True:
    event, values = window.read()
    # End program if user closes window
    if event == sg.WIN_CLOSED:
        break

window.close()

23.9. Other resources

https://tkdocs.com/tutorial/ (does many languages side-by-side)

http://zetcode.com/gui/tkinter/ (object oriented; this is too soon to use OOP in this course)

https://likegeeks.com/python-gui-examples-tkinter-tutorial/ (maybe good)

https://dzone.com/articles/python-gui-examples-tkinter-tutorial-like-geeks (maybe good)

https://www.python-course.eu/tkinter_labels.php (maybe good because it has modern preferences like “import tkinter as tk”, but maybe too lengthy in the early examples)

https://www.tutorialspoint.com/python3/python_gui_programming.htm