CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations in Tkinter
Part 1: Getting Started with MVC
- Our First Example: A KeyPress Counter
- Model-View-Controller (MVC)
- Legal key values
- Moving a Dot with Key Presses
- Moving a Dot with Arrows
- Moving a Dot with Arrows and Bounds
- Moving a Dot with Arrows and Wraparound
- Moving a Dot in Two Dimensions
Note: The videos on this page use the lecture1/2 animation framework. Though the syntax of their animation framework is different, the explanations in the videos will still be helpful to you.
- Our First Example: A KeyPress Counter
from cmu_graphics import * def onAppStart(app): app.counter = 0 def onKeyPress(app, key): app.counter += 1 def redrawAll(app): drawLabel(f'{app.counter} keypresses', app.width / 2, app.height / 2, font='arial', size=30, bold=True) 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.
- In the example above,
- The view draws the app using the values in the model.
- In the example above,
redrawAll
is our view.
- In the example above,
- The controller responds to keyboard, mouse, timer and other
events and updates the model.
- In the example above,
onKeyPress
is our controller.
- In the example above,
- You never call the view or the controllers. The animation framework
calls these for you.
- In the example above,
we never call
redrawAll
oronKeyPress
. They are called for us.
- In the example above,
we never call
- Controllers can only update the model, they cannot update the view.
- In the example above,
onKeyPress
cannot callredrawAll
.
- In the example above,
- The view can never update the model.
- In the example above,
redrawAll
cannot changeapp.counter
or any other values in the model.
- In the example above,
- 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 key values
from cmu_graphics import * def onAppStart(app): app.message = 'Press any key' def onKeyPress(app, key): app.message = f"key == '{key}'" def redrawAll(app): drawLabel(app.message, app.width/2, 40, font='arial', size=30, bold=True) keyNamesText = '''Here are the legal 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(): drawLabel(line.strip(), app.width/2, y, font='arial', size=20) y += 30 runApp(width=600, height=400)
- Moving a Dot with Key Presses
- Moving a Dot with Arrows
from cmu_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onKeyPress(app, key): if (key == 'left'): app.cx -= 10 elif (key == 'right'): app.cx += 10 def redrawAll(app): drawLabel('Move dot with left and right arrows', app.width / 2, 20) drawCircle(app.cx, 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_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onKeyPress(app, key): if (key == 'left'): app.cx -= 10 if (app.cx - app.r < 0): app.cx = app.r elif (key == 'right'): app.cx += 10 if (app.cx + app.r > app.width): app.cx = app.width - app.r def redrawAll(app): drawLabel('Move dot with left and right arrows', app.width / 2, 20) drawLabel('See how it is bounded by the canvas edges', app.width / 2, 40) drawCircle(app.cx, 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_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onKeyPress(app, key): if (key == 'left'): app.cx -= 10 if (app.cx + app.r <= 0): app.cx = app.width + app.r elif (key == 'right'): app.cx += 10 if (app.cx - app.r >= app.width): app.cx = 0 - app.r def redrawAll(app): drawLabel('Move dot with left and right arrows', app.width / 2, 20) drawLabel('See how it uses wraparound on the edges', app.width / 2, 40) drawCircle(app.cx, 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_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onKeyPress(app, key): if (key == 'left'): app.cx -= 10 elif (key == 'right'): app.cx += 10 elif (key == 'up'): app.cy -= 10 elif (key == 'down'): app.cy += 10 def redrawAll(app): drawLabel('Move dot with up, down, left, and right arrows', app.width / 2, 20) drawCircle(app.cx, app.cy, app.r, fill='darkGreen') runApp(width=400, height=400)
- Moving a Dot with Arrows
- Moving a Dot with Mouse Presses
from cmu_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onMousePress(app, mouseX, mouseY): app.cx = mouseX app.cy = mouseY def redrawAll(app): drawLabel('Move dot with mouse presses', app.width / 2, 20) drawCircle(app.cx, app.cy, app.r, fill='darkGreen') runApp(width=400, height=400)
- Moving a Dot with a Timer
from cmu_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def onStep(app): app.cx -= 10 if (app.cx + app.r <= 0): app.cx = app.width + app.r def redrawAll(app): drawLabel('Watch the dot move!', app.width / 2, 20) drawCircle(app.cx, 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_graphics import * def onAppStart(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 app.paused = False def onStep(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 onKeyPress(app, key): if (key == 'p'): app.paused = not app.paused elif (key == 's') and app.paused: doStep(app) def redrawAll(app): drawLabel('Watch the dot move!', app.width / 2, 20) drawLabel('Press p to pause or unpause', app.width / 2, 40) drawLabel('Press s to step while paused', app.width / 2, 60) drawCircle(app.cx, app.cy, app.r, fill='darkGreen') runApp(width=400, height=400) - Changing the frequency of onStep
from cmu_graphics import * def onAppStart(app): app.counter = 0 app.stepsPerSecond = 20 def onStep(app): app.counter += 1 def onKeyPress(app, key): if (key == 'left'): app.stepsPerSecond = max(0, app.stepsPerSecond - 1) elif (key == 'right'): app.stepsPerSecond += 1 def redrawAll(app): drawLabel('Press the left and right arrow keys.', app.width / 2, 20) drawLabel('See how the counter grows more slowly or more quickly', app.width / 2, 40) drawLabel('when app.stepsPerSecond changes.', app.width / 2, 60) drawLabel(f'app.stepsPerSecond: {app.stepsPerSecond}', app.width / 2, app.height / 2 - 30, size=30) drawLabel(f'Counter: {app.counter}', app.width / 2, app.height / 2 + 30, size=30) runApp(width=400, height=400) - MVC Violations
- Cannot change the model while drawing the view
from cmu_graphics import * def onAppStart(app): app.x = 0 def redrawAll(app): drawLabel('This has an MVC violation!', app.width / 2, 20) 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_graphics import * def onAppStart(app): app.L = [ ] def redrawAll(app): drawLabel('This also has an MVC violation!', app.width / 2, 20) app.L.append(42) # This is an MVC Violation! # We cannot change the model from the view (redrawAll) runApp(width=400, height=400)
- Cannot change the model while drawing the view