---
slideOptions:
theme: white
---
# Chapter 20 - Event-driven programming
###### tags: `GUI Programming`
---
## Event-driven programming
*Events* are the actions performed by the user by using keyboards and mouses. For example, clicking the picture in a window is called "an event trigged by clicking the picture".
## Mouse event
We can use `bind()` to command and receive mouse event functions. We need to firstly declare variables for mouse coordination, and the mouse clicking determination.
Please input and run the following program.
```python=
import tkinter
mouse_x = 0
mouse_y = 0
mouse_c = 0
def mouse_move(e):
global mouse_x, mouse_y
mouse_x = e.x
mouse_y = e.y
def mouse_press(e):
global mouse_c
mouse_c = 1
def mouse_release(e):
global mouse_c
mouse_c = 0
def game_main():
fnt = ("Times New Roman", 30)
txt = "mouse({}, {}){}".format(mouse_x, mouse_y, mouse_c)
cvs.delete("TEST")
cvs.create_text(456, 384, text=txt,
fill="black",
font=fnt,
tag="TEST")
root.after(100, game_main)
root = tkinter.Tk() # Create the window
root.title("Mouse Event") # Set the title
root.resizable(False, False)
root.bind("<Motion>", mouse_move)
root.bind("<ButtonPress>", mouse_press)
root.bind("<ButtonRelease>", mouse_release)
cvs = tkinter.Canvas(root, width=912, height=768)
cvs.pack()
game_main()
root.mainloop() # Display the window
```
After the program is run, the coordination of the mouse will be shown. When the mouse is clicked, the rightmost value will changed from 0 to 1.

In line 7-18, the functions for mouse moving, mouse press and mouse release are defined. The coordinates of the mouse is sent from the event variable (the program is `e`), we can get them on line 9 to 10 by `.x` and `.y`. When the mouse released, 1 and 0 will be assigned to `mouse_c`. We can ensure that the user clicks the mouse if the variable is 1.
## Keyboard events
We can use `bind()` to receive the event commands. Please input and run the following program.
```python=
import tkinter
key = 0 # Declare key as 0
def key_down(e):
global key
key = e.keycode
print("KEY:", key)
root = tkinter.Tk() # Create the window
root.title("Get keycode") # Set the title
root.bind("<KeyPress>", key_down)
root.mainloop() # Display the window
```
After the program is run, nothing will be shown on the window until the user click any key on the keyboard. The the will shown the keycode.

We can use `keysym` to get the key name, that is easier to read than using `keycode`. We can change the `key_down` function to the following and test.
```python!
def key_down(e):
global key
key = e.keysym
print("KEY:", key)
```

### Using `bind()` command to get the event
The syntax of `bind()` is shown below.
```python!
bind("Event", function_name)
```
The `function_name` about do not need to add `()`.
We can get the following events.
|Event|Description|
|:----|:----------|
|`<KeyPress>` or `<Key>`|Key on click|
|`<KeyRelease>`|Key on release|
|`<Motion>`|The position of mouse motion|
|`<ButtonPress>` or `<Button>`|Mouse Button on click|
## Moving Picture by pressing key
We can use the direction keys to move the character in the window.
You need to put [mimi.png](https://hackmd.io/_uploads/r1K1K1Aw2.png) inside the folder with the program.
Please input and run the following program.
```python=
import tkinter
key = ""
cx = 400
cy = 300
def key_down(e):
global key
key = e.keysym
def key_up(e):
global key
key = ""
def main_proc():
global cx, cy
if key == "Up":
cy -= 20
if key == "Down":
cy += 20
if key == "Left":
cx -= 20
if key == "Right":
cx += 20
canvas.coords("MYCHR", cx, cy)
root.after(100, main_proc)
root = tkinter.Tk() # Create the window
root.title("Moving Character") # Set the title
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
canvas = tkinter.Canvas(width=800, height=600,
bg="lightgreen")
canvas.pack()
img = tkinter.PhotoImage(file="mimi.png")
canvas.create_image(cx, cy, image=img, tag="MYCHR")
main_proc()
root.mainloop() # Display the window
```
After the program is run, the character will be shown in the window. At the same time, we can use direction keys to move the character.

The function `main_proc()` in line 15-26 defines the function for the procedure.
The code in line 4-5 contains the global variables `cx` and `cy`, that are used to manage the coordinate of the character. `main_proc()` will change the value of `cx` and `cy` according to the key pressed.
The `coords()` command in line 23 is used to move the picture to the new coordinate. `coords` includes the parameter for tag name, x-axis and y-axis.
### Using `tag`
The `PhotoImage()` command in line 35 will import picture. The `create_image()` command in line 36 will show it on the canvas. At this moment, we will assign tag as a parameter of `create_image()`.
```python!
canvas.create_image(cx, cy, image=img, tag="MYCHR")
```
`tag` can be used to place graphics or pictures. We usually use it when we need to move or hide the graphic or picture. It is suggested to name a easy-to-read name for tag.
### About the coordinate of `create_image()`
The coordinate of `create_image()` command is the centre of the picture. Please refer to the following graph.

## Creating the Maze
In a 2D game, we can use 2D array to manage the background information.
### About 2D Array
We can use 2D list to define the information of a maze. The indexes of the list can be used to control the information of horizontal (row) and vertical (column). Assuming that horizontal as x, and vertical as y.

For example, if we need to put 10 into `m[2][3]` on the right bottom column, we can write `m[2][3] = 10`.
### Define maze

We can define the above maze by write is the floor and grey is the wall.
We can convert the floor as 0 and the wall as 1 such that we can define it in the program.

We define a list called `maze`. The horizontal part is row.
```python!
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
```
Then we need to display the maze just defined on the window. The following program just show the maze, and do not manage the events. In this program, we need to use *nested for loop*. Please input and run the following program.
```python=
import tkinter
root = tkinter.Tk() # Create the window
root.title("Maze") # Set the title
canvas = tkinter.Canvas(width=800, height=560,
bg="white")
canvas.pack()
# Input the information of the maze
# 0 - Floor; 1- Wall
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
# Draw the wall (1) as grey squares
for y in range(7):
for x in range(10):
if maze[y][x] == 1:
canvas.create_rectangle(x*80, y*80,x*80+80, y*80+80, fill="gray")
root.mainloop() # Display the window
```
The maze will be shown after the program is run.

## Creating Maze Game
Now, we will create a game. We need the use the following picture (save it as `mimi_s.png`) as the character.

Please input and run the following program.
```python=
import tkinter
key = "" # The variable that store the key state
mx = 1 # The variable to store the x-axis of character
my = 1 # The variable to store the yg-axis of character
def key_down(e):
global key
key = e.keysym
def key_up(e):
global key
key = ""
def main_proc():
global mx, my
if key == "Up" and maze[my-1][mx] == 0:
my = my - 1
if key == "Down" and maze[my+1][mx] == 0:
my = my + 1
if key == "Left" and maze[my][mx-1] == 0:
mx = mx - 1
if key == "Right" and maze[my][mx+1] == 0:
mx = mx + 1
canvas.coords("MYCHR", mx*80+40, my*80+40)
root.after(300, main_proc)
root = tkinter.Tk() # Create the window
root.title("Move within Maze") # Set the title
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
canvas = tkinter.Canvas(root, width=800, height=560, bg="white")
canvas.pack()
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
for y in range(7):
for x in range(10):
if maze[y][x] == 1:
canvas.create_rectangle(x*80, y*80,
x*80+79, y*80+79,
fill="skyblue",
width=0)
img = tkinter.PhotoImage(file="mimi_s.png")
canvas.create_image(mx*80+40, my*80+40,
image=img, tag="MYCHR")
main_proc()
root.mainloop() # Display the window
```
The movement of the character is controlled by the function `main_proc`, as shown below.
```python=15
def main_proc():
global mx, my
if key == "Up" and maze[my-1][mx] == 0:
my = my - 1
if key == "Down" and maze[my+1][mx] == 0:
my = my + 1
if key == "Left" and maze[my][mx-1] == 0:
mx = mx - 1
if key == "Right" and maze[my][mx+1] == 0:
mx = mx + 1
canvas.coords("MYCHR", mx*80+40, my*80+40)
root.after(300, main_proc)
```
In this program, the width and height of each box is 80 pixel. Hence, the position of the character is `canvas.coords("MYCHR", mx*80+40, my*80+40)`. The x-axis is `mx*80+40` and y-axis is `my*80+40` respectively. As the character is coordinated according to the centre of the picture, `mx` and `my` need to add 40 pixel.
The maze can be called by using the the format of 2D list, i.e. `maze[][]`.

In this program, `0` and `1` are used to represent the floor and wall respectively. We can use other values to represent other things, like `1` as tree and `2` as river.
## Finish the game
Mark the place the walked by the character as pick. At this moment, the maze represented 2D arrays uses 0 as the floor and 1 and the wall. Now, we set 2 as the position that walked by the character. The character cannot walk back. That is the game rule.
Change the function `main_proc` in the above program and run afterwards.
```python=15
def main_proc():
global mx, my
if key == "Up" and maze[my-1][mx] == 0:
my = my - 1
if key == "Down" and maze[my+1][mx] == 0:
my = my + 1
if key == "Left" and maze[my][mx-1] == 0:
mx = mx - 1
if key == "Right" and maze[my][mx+1] == 0:
mx = mx + 1
if maze[my][mx] == 0:
maze[my][mx] = 2
canvas.create_rectangle(mx*80, my*80,
mx*80+79, my*80+79,
fill="pink", width=0)
canvas.delete("MYCAR")
canvas.create_image(mx*80+40, my*80+40,
image=img, tag="MYCAR")
root.after(300, main_proc)
```
When the program is run, the floor becomes pick when the character walk through.
Line 25-28 is used to colour the floor. We use `if` the get the position of the character. If the value is 0, then set it as 2 and colour it in pink.
Line 26 uses ==`delete`== to delete the character, and redraw it by `create_image()` in line 27. The parameter of `delete` command can delete the image by tag.

### Determine win or not
We can determine if all floor are pink. Change the program as following and run it.
1. `import tkinter.messagebox`
2. Add global `yuka = 0`
3. Change function `main_proc`
```python=
import tkinter
import tkinter.messagebox
key = "" # The variable that store the key state
mx = 1 # The variable to store the x-axis of character
my = 1 # The variable to store the yg-axis of character
yuka = 0
def key_down(e):
global key
key = e.keysym
def key_up(e):
global key
key = ""
def main_proc():
global mx, my, yuka
if key == "Up" and maze[my-1][mx] == 0:
my = my - 1
if key == "Down" and maze[my+1][mx] == 0:
my = my + 1
if key == "Left" and maze[my][mx-1] == 0:
mx = mx - 1
if key == "Right" and maze[my][mx+1] == 0:
mx = mx + 1
if maze[my][mx] == 0:
maze[my][mx] = 2
yuka = yuka + 1
canvas.create_rectangle(mx*80, my*80, mx*80+79, my*80+79,
fill="pink", width=0)
canvas.delete("MYCAR")
canvas.create_image(mx*80+40, my*80+40,
image=img, tag="MYCAR")
if yuka == 30:
canvas.update()
tkinter.messagebox.showinfo("congratulations!", "All floor are painted.")
else:
root.after(300, main_proc)
root = tkinter.Tk() # Create the window
root.title("Move within Maze") # Set the title
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
canvas = tkinter.Canvas(root, width=800, height=560, bg="white")
canvas.pack()
maze = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]
for y in range(7):
for x in range(10):
if maze[y][x] == 1:
canvas.create_rectangle(x*80, y*80,
x*80+79, y*80+79,
fill="skyblue",
width=0)
img = tkinter.PhotoImage(file="mimi_s.png")
canvas.create_image(mx*80+40, my*80+40,
image=img, tag="MYCHR")
main_proc()
root.mainloop() # Display the window
```
There are totally 30 boxes in the maze.
Line 29 counts the number of boxes painted.
The `if` in line 35 determines if all the boxes are painted.
The `canvas.update()` in line 32 used to reflash the canvas. If `update` is missed, the message box cannot be shown when all boxes are painted.

## Reference
https://tkdocs.com/pyref/
[mimi.png](https://hackmd.io/_uploads/r1K1K1Aw2.png)
[mimi_s.png](https://hackmd.io/_uploads/BJgj5-iO2.png)