CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations in Tkinter
Part 1: Getting Started with MVC


  1. Our First Example: A KeyPress Counter
  2. Model-View-Controller (MVC)
  3. Legal key values
  4. Moving a Dot with Key Presses
    1. Moving a Dot with Arrows
    2. Moving a Dot with Arrows and Bounds
    3. Moving a Dot with Arrows and Wraparound
    4. Moving a Dot in Two Dimensions
  5. Moving a Dot with Mouse Presses
  6. Moving a Dot with a Timer
  7. Pausing with a Timer
  8. Changing the frequency of onStep
  9. MVC Violations

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.

  1. 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)

  2. Model-View-Controller (MVC)
    Note:
    1. We will write animations using the Model-View-Controller (MVC) paradigm.
    2. 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.
    3. The view draws the app using the values in the model.
      • In the example above, redrawAll is our view.
    4. The controller responds to keyboard, mouse, timer and other events and updates the model.
      • In the example above, onKeyPress is our controller.
    And...
    1. You never call the view or the controllers. The animation framework calls these for you.
      • In the example above, we never call redrawAll or onKeyPress. They are called for us.
    2. Controllers can only update the model, they cannot update the view.
      • In the example above, onKeyPress cannot call redrawAll.
    3. The view can never update the model.
      • In the example above, redrawAll cannot change app.counter or any other values in the model.
    4. 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.

  3. 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)

  4. Moving a Dot with Key Presses

    1. 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)

    2. 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)

    3. 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)

    4. 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)

  5. 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)

  6. 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)

  7. 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)

  8. 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)

  9. MVC Violations

    1. 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)

    2. 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)