- 112 Graphics Event Loop
- Our First Example: A KeyPress Counter
from cmu_112_graphics import *
def appStarted(app):
app.counter = 0
def keyPressed(app, event):
app.counter += 1
def redrawAll(app, canvas):
canvas.create_text(app.width/2, app.height/2,
text=f'{app.counter} keypresses',
font='Arial 30 bold',
fill='black')
runApp(width=400, height=400)
- Model-View-Controller (MVC)
Note:
- We will write animations using the Model-View-Controller (MVC) paradigm.
- The model contains all the data we need for the animation.
We can store the model in the
app
object's attributes.
- In the example above,
app.counter
is our model.
- The view draws the app using the values in the model.
- In the example above,
redrawAll
is our view.
- The controller responds to keyboard, mouse, timer and other
events and updates the model.
- In the example above,
keyPressed
is our controller.
And...
- You never call the view or the controllers. The animation framework
calls these for you.
- In the example above,
we never call
redrawAll
or
keyPressed
. They are called for us.
- Controllers can only update the model, they cannot update the view.
- In the example above,
keyPressed
cannot call redrawAll
.
- The view can never update the model.
- In the example above,
redrawAll
cannot
change app.counter
or any
other values in the model.
- If you violate these rules, it is called an MVC Violation.
If that happens, your code will stop running and will display
the runtime error for you.
- Legal event.key values
# Note: Tkinter uses event.keysym for some keys, and event.char
# for others, and it can be confusing how to use these properly.
# Instead, cmu_112_graphics replaces both of these with event.key,
# which simply works as expected in all cases.
from cmu_112_graphics import *
def appStarted(app):
app.message = 'Press any key'
def keyPressed(app, event):
app.message = f"event.key == '{event.key}'"
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 40, text=app.message,
font='Arial 30 bold', fill='black')
keyNamesText = '''Here are the legal event.key names:
* Keyboard key labels (letters, digits, punctuation)
* Arrow directions ('Up', 'Down', 'Left', 'Right')
* Whitespace ('Space', 'Enter', 'Tab', 'BackSpace')
* Other commands ('Delete', 'Escape')'''
y = 80
for line in keyNamesText.splitlines():
canvas.create_text(app.width/2, y, text=line.strip(),
font='Arial 20', fill='black')
y += 30
runApp(width=600, height=400)
- Moving a Dot with Key Presses
- Moving a Dot with Arrows
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def keyPressed(app, event):
if (event.key == 'Left'):
app.cx -= 10
elif (event.key == 'Right'):
app.cx += 10
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Move dot with left and right arrows',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Moving a Dot with Arrows and Bounds
# This version bounds the dot to remain entirely on the canvas
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def keyPressed(app, event):
if (event.key == 'Left'):
app.cx -= 10
if (app.cx - app.r < 0):
app.cx = app.r
elif (event.key == 'Right'):
app.cx += 10
if (app.cx + app.r > app.width):
app.cx = app.width - app.r
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Move dot with left and right arrows',
fill='black')
canvas.create_text(app.width/2, 40,
text='See how it is bounded by the canvas edges',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Moving a Dot with Arrows and Wraparound
# This version wraps around, so leaving one side enters the opposite side
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def keyPressed(app, event):
if (event.key == 'Left'):
app.cx -= 10
if (app.cx + app.r <= 0):
app.cx = app.width + app.r
elif (event.key == 'Right'):
app.cx += 10
if (app.cx - app.r >= app.width):
app.cx = 0 - app.r
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Move dot with left and right arrows',
fill='black')
canvas.create_text(app.width/2, 40,
text='See how it uses wraparound on the edges',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Moving a Dot in Two Dimensions
# This version moves in both x and y dimensions.
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def keyPressed(app, event):
if (event.key == 'Left'): app.cx -= 10
elif (event.key == 'Right'): app.cx += 10
elif (event.key == 'Up'): app.cy -= 10
elif (event.key == 'Down'): app.cy += 10
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Move dot with up, down, left, and right arrows',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Moving a Dot with Mouse Presses
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def mousePressed(app, event):
app.cx = event.x
app.cy = event.y
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Move dot with mouse presses',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Moving a Dot with a Timer
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
def timerFired(app):
app.cx -= 10
if (app.cx + app.r <= 0):
app.cx = app.width + app.r
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Watch the dot move!',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- Pausing with a Timer
Pausing and stepping are super helpful when
debugging animations!
from cmu_112_graphics import *
def appStarted(app):
app.cx = app.width/2
app.cy = app.height/2
app.r = 40
app.paused = False
def timerFired(app):
if (not app.paused):
doStep(app)
def doStep(app):
app.cx -= 10
if (app.cx + app.r <= 0):
app.cx = app.width + app.r
def keyPressed(app, event):
if (event.key == 'p'):
app.paused = not app.paused
elif (event.key == 's') and app.paused:
doStep(app)
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='Watch the dot move!',
fill='black')
canvas.create_text(app.width/2, 40,
text='Press p to pause or unpause',
fill='black')
canvas.create_text(app.width/2, 60,
text='Press s to step while paused',
fill='black')
canvas.create_oval(app.cx-app.r, app.cy-app.r,
app.cx+app.r, app.cy+app.r,
fill='darkGreen')
runApp(width=400, height=400)
- MVC Violations
- Cannot change the model while drawing the view
from cmu_112_graphics import *
def appStarted(app):
app.x = 0
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='This has an MVC Violation!',
fill='black')
app.x = 10 # This is an MVC Violation!
# We cannot change the model from the view (redrawAll)
runApp(width=400, height=400)
- Once again, but with a mutable value (such as a list)
# Since this version modifies a mutable value in the model,
# the exception does not occur immediately on the line of the change,
# but only after redrawAll has entirely finished.
from cmu_112_graphics import *
def appStarted(app):
app.L = [ ]
def redrawAll(app, canvas):
canvas.create_text(app.width/2, 20,
text='This also has an MVC Violation!',
fill='black')
app.L.append(42) # This is an MVC Violation!
# We cannot change the model from the view (redrawAll)
runApp(width=400, height=400)