CS 151: Lab #12

Lab Exercise 12: GUI and Menu Design

In this lab, we'll build a menu class and a skeleton GUI for the current project.


Tasks

Menus are one of the key interface tools of current interfaces. There are two types of menus: fixed and popup. The two types share many characteristics, but have different ways of interacting with the user. Two types of objects with similar characteristics suggests that we could have a parent Menu class for one type (popup) and a variation of it for the other type (fixed).

Selecting the proper action given the user's menu selection is another important piece of GUIs. Rather than write some large if-statements, we can set up the GUI using a dictionary that contains an entry for each menu item. The entry will contain the function that should be executed.

We're also going to make use of the fact that we can dynamically add new fields to an object. Since the window itself is a GraphWin object, and is needed for many drawing tasks, we're going to add fields to the window to hold lots of useful stuff.

  1. Create the parent class Menu. This class will, by itself, act as a popup menu. We'll derive fixed menus from the Menu class.
    class Menu:
    

    The Menu constructor (__init__ method) should take a window (win) and a list of strings (menuText) in addition to the self reference. The constructor should figure out how big the menu should be, create a background rectangle, and a list of Text objects, one for each menu item. The constructor should not draw the objects, but it should store the window reference in a field so they can be drawn later. The basic idea is to create the form of the menu (a box with text arranged vertically within it) but not worry about exactly where it should go in the window. It's not much different from creating a building outline and some windows.

    The structure of the constructor should be as follows.

    Define __init__ method with arguments self, win, and menuText
    
        Store the window reference in a field of self (e.g. self.win)
        Create an empty list and fill it with the strings in menuText (use a loop)
        
        # Figure out the length of the longest string in menuText
        Create a local variable max and initialize it to zero
        For each item in menuText
            If the length of the item is greater than max
                Assign max the length of the item
    
    
        Store the height of a menu item in a field of self (e.g self.itemHeight)
        Calculate the width of the menu box as max * (character width) (e.g. 13)
        Calculate the height of the menu box as the (number of items) * (itemHeight)
    
        Create a rectangle using the points (0, 0) and (width, height)
        Set the fill color to white
        Store the rectangle in a list that is a field of self (e.g. self.shapes)
    
        Assign width/2 to a local variable (e.g. tx)
        Assign itemHeight/2 to a local variable (e.g. ty)
        For each item in menuText
            Create a temporary Point pt at (tx, ty)
    	Create a Text object at location pt and string item
    	Append the Text object to the shapes list (e.g. self.shapes)
    	Increment ty by itemHeight
    
  2. The other function in the Menu class is a select function that takes a Point as its argument, draws the menu at the location, and then waits for the user to make a selection. If the selection is inside the background rectangle, the function should return the closest menu option. The algorithm is as follows.
    def select(self, location):
        Calculate the difference between location and the menu's current position
        Move the menu so the upper left corner is aligned with the given location
        Draw the menu 
    
        Wait for a mouse click and store its the location in a local variable (e.g. click)
    
        Undraw the menu
    
        If the mouse click was inside the background rectangle
            Calculate which menu item was picked
            Return a tuple with the index and item string in it
        else
            Return a tuple like (-1, '')
    
    

    Note, there is a handy trick you can use to figure out which menu item was picked. We know the top of the menu box is the Y value of the first point of the Rectangle. We know the Rectangle is the first item in the self.shapes list. So the top of the menu box, y0, is at:

    y0 = self.shapes[0].getP1().getY()

    We know the location of the mouse click is at click.getY(), so the distance from the top of the menu box to the mouse click is dy = click.getY() - y0.

    We also know the height of each menu item, which is stored in self.itemHeight. Therefore, you can divide dy by the itemHeight and round it to an integer (using the int() function) to discover which menu item was picked. If it was the first item, the result of the division will be 0. If it was the second item, the result will be 1, and so on.

  3. Write a test function that creates a window, creates a Menu item and handles a couple of mouse clicks. Note that the Menu is, for all intents and purposes, a popup menu class.
  4. The standard fixed menu class wants a bit more information. In particular, it needs a title and an anchor. The FixedMenu class needs to do everything the Menu class does, plus some. To avoid recoding the Menu init function, we can call it explicitly as in the following.
    class FixedMenu(Menu):
    
        def __init__(self, win, menuText, title, anchor):
            
            # call the parent initializer
            Menu.__init__(self, win, menuText)
    

    The init function should then do the following:

            Create a rectangle big enough to hold the title and save it in a field
    	Fill the rectangle white
    
    	Create a Text object for the title and save it in a field
    	Draw the rectangle and text object.
    
  5. Finally, the select function for the FixedMenu class should take in a mouse click, test if the click was in the title box, then call the Menu select function, passing in an anchor on the lower left corner of the title box. If the click was not in the title box, the function should return something like (-1, '').
  6. Add a FixedMenu to the test function and try it out.
  7. Now we're going to write a very simple GUI that uses a menu to let the user quit the program. Create a new file simplegui.py. Import the graphics package and the Menu and FixedMenu classes. Then create a main function that does the following:
    Begin main function
        Create a window
        Create a FixedMenu with the options 'Something' and 'Quit' and put it in fmenu
        Set the variable Done to false
    
        while not Done
    
            Get a mouse click and put it into a variable pt
            Call the select function of the FixedMenu fmenu with the location pt
    
            If the return value index is greater than zero
                Print out the return value string
                If the return value string is 'Quit', set Done to True
    
  8. Finally, we can use a dictionary to set up the actions for the menu items. First, define two functions called doSomething(win) and doQuit(win). Have doSomething print out a string and return False. Have doQuit print out something and return True.

    Second, in your main function, create an empty dictionary and set the entry corresponding to 'Something' to doSomething. Set the entry corresponding to 'Quit' to doQuit.

        action = {}
        action['Something'] = doSomething
        action['Quit'] = doQuit
    

    Finally, in the last step of the main function above, use the notation:

                Done = action[ selection[1] ](win)
    
    What does this do? It uses the string returned by the select function to index into the dictionary. That entry is a function that expects a single variable and which returns True or False. Either doSomething or doQuit will get called, depending upon what menu item is selected. It might also be worth checking if selection[1] is a key for action, and doing nothing if it isn't.

Now you have a working skeleton for building a GUI with menus.

Once you've finished the above exercise, keep going on the current project.