Clicky Clicky

Jed Rembold & Fred Agbo

February 21, 2024

Announcements

  • Grading for PS3 is returned! Ask me questions if you’ve got one regarding it
  • We are in Ch6 of the text this week, which is interactive graphics
  • Remember the 1st midterm exam is this Friday Feb 23 in this hall
    • Test objectives and first practice questions were sent on Monday
    • Second practice questions will be opened today: Canvas demo, in-clase today!
    • Those with accommodations with Testing Center should contact them to make arrangement NOW! and cc me in the email
  • Polling continues https://www.polleverywhere.com/agbofred203

Our First Interactive Example

  • Consider the simple program below, where we’ve imported the basics and some of our helper functions

    def draw_dots():
        def click_action(event):
            c = create_filled_rect(
                event.get_x(), event.get_y(), 
                10,10, random_color())
            gw.add(c)
    
        gw = GWindow(500, 500)
        gw.add_event_listener("click", click_action)
  • The click_action function specifies what to do when the mouse is clicked

    • Note that it has access to the gw variable since it is in the enclosing function and thus in the closure.

Registering a Listener

  • The last line of our example function:

    gw.add_event_listener("click", click_action)

    tells the graphics window (gw) to call the click_action function whenever a mouse “click” occurs within the window.

  • When the user clicks the mouse, the graphics window, in essense, calls the client back to let them know that a click has occured. Thus, functions such as click_action are known as callback functions.

  • The parameter event given to the callback function is a special data structure called a mouse event, which contains details about the specifics of the event that triggered the action.

Mouse Events

  • We have a fairly comprehensive list of mouse-events that we can trigger callbacks on:
Name Description
"click" The user clicks the mouse in the window
"dblclk" The user double-clicks the mouse in the window
"mousedown" The user presses the mouse button down
"mouseup" The user releases the mouse button
"mousemove" The user moves the mouse
"drag" The user moves the mouse with the button down

Event Details

  • Certain actions can trigger more than one event
    • Clicking generates a “mousedown”, “mouseup”, and then “click” event, in that order
  • Events trigger no action unless the window is listening for that event
    • If I drag my mouse in the draw_dots() function, you’ll notice that nothing happens
  • You can setup however many listeners you feel you need in order to make your program behave as desired
gw.add_event_listener("click", click_action)
gw.add_event_listener("dblclk", dblclk_action)

Line Drawing

  • Say we wanted to write a simple program that allows the user to draw lines by clicking and dragging the mouse
  • Using two event listeners would be useful:
    • “mousedown” could start drawing a zero-length line at the current mouse position (and add it to the window)
    • “drag” could update the end-point of that line
  • The strategy would allow the user to have visual feedback as they drag around, helping them to position the line
    • Since the line stretches and contracts as you move the cursor around, the technique is commonly called rubber-banding

Attempt #1

from pgl import GWindow, GLine

WIDTH = 500
HEIGHT = 500

def draw_lines():
    def mousedown_event(e):
        x = e.get_x()
        y = e.get_y()
        line = GLine(x,y,x,y)
        gw.add(line)

    def drag_action(e):
        line.set_end_point(e.get_x(), e.get_y())

    gw = GWindow(WIDTH, HEIGHT)
    line = None
    gw.add_event_listener("mousedown", mousedown_event)
    gw.add_event_listener("drag", drag_action)

if __name__ == '__main__':
    draw_lines()

What Happened?

  • Remember that if you define a variable in a function, that variable is assumed to be local!
    • Keeps you from accidentally overwriting variables you may not have meant to
    • It works against us here, since we WANT to override the original value
  • We can’t pass in the info as a parameter, since it is not part of the event information
  • Python does have a nonlocal keyword, which allows you to state that a specific variable is not local, but it tends to just confuse students

In the Window

  • A common tactic is to store all variables that need to be shared between two or more functions in a state object
  • A state object is just a single object which serves as a storage space for a collection of values
  • The object is created in such a location as to ensure it is in the closure of any functions that need to access its contents
  • We will most often encounter this issue with graphics applications, where we actually already have an object that could serve as a state object
    • The GWindow object (mostly commonly named gw)!

Storage and Retrieval

  • Do you want to store a value in your state object?
    • We can store it as an attribute to the gw object
    • Requires specifying the object name, followed by a dot and then your desired attribute name:
    gw.my_attribute_name = some_cool_value
  • Do you want to retrieve a value from your state object?
    • Just refer to the object and attribute name:

      print(gw.my_attribute_name)

Fixed Line-Drawing

from pgl import GWindow, GLine

WIDTH = 500
HEIGHT = 500

def draw_lines():
    def mousedown_event(e):
        x = e.get_x()
        y = e.get_y()
        gw.line = GLine(x,y,x,y)
        gw.add(gw.line)

    def drag_action(e):
        gw.line.set_end_point(e.get_x(), e.get_y())

    gw = GWindow(WIDTH, HEIGHT)
    gw.line = None
    gw.add_event_listener("mousedown", mousedown_event)
    gw.add_event_listener("drag", drag_action)

if __name__ == '__main__':
    draw_lines()

Timer Events

  • Previously we looked at how our programs could react to mouse events
  • Can also listen for timer events, which occur after a specific time interval
  • You specify the listener for a timer event in the form of a callback function that is invoked at the end of the time interval
  • Can add animation to our graphics by creating a timer whose callback makes small updates to the graphical objects in the window
    • If the time interval is short enough (usually sub 30 milliseconds), the animations will appear smooth to the human eye

Timer Types

  • PGL supports two kinds of timers:
    • A one-shot timer invokes its callback only once after a specified delay
      • Created with

        gw.set_timeout(function, delay)

        where function is the callback function and delay is the time interval in milliseconds

    • An interval timer invokes its callback function repeatedly at regular intervals
      • Created with

        gw.set_interval(function, delay)
  • Both methods return a GTimer object that identifies the timer, and can be stopped by invoking the .stop() method on that timer

Moving Square

def moving_square():
    def step():
        square.move(dx, dy)
        if square.get_x() > 500:
            timer.stop()

    gw = GWindow(500, 200)
    dx = 1
    dy = 0
    square = create_filled_rect(12, 100, 24, 24, "red")
    gw.add(square)
    timer = gw.set_interval(step, 20)
// reveal.js plugins