3
\$\begingroup\$

This is a simple console menu that just holds a main menu and a few menu "buttons" (numbers and titles which the user has to enter for the button to do its action). It does not need submenus or more complicated input handling.

I went with an object-oriented approach and defined three classes: Menu, Button, and Controller. The Menu initialises with a name and a list of menu buttons.

class Menu(object):

    """Base class for the menu"""

    def __init__(self, name, buttons):
        # Initialize values
        self.name = name
        self.buttons = buttons

    def display(self):

        """Displaying the menu alongside the navigation elements"""

        # Display menu name
        print self.name

        # Display menu buttons
        for button in self.buttons:
            print "   ", button.nav, button.name

        # Wait for user input
        self.userInput()

    def userInput(self):

        """Method to check and act upon user's input"""

        # This holds the amount of errors for the
        # navigation element to input comparison.
        errSel = 0
        inputSel = raw_input("Enter selection> ")

        for button in self.buttons:
            # If input equals to button's navigation element
            if inputSel == str(button.nav):
                # Do the button's function
                button.do()
            # If input != navigation element
            else:
                # Increase "error on selection" by one, for
                # counting the errors and checking their
                # amount against the total number of
                # buttons. If greater to or equal that means 
                # no elements were selected.
                # In that case show error and try again
                errSel += 1

        # No usable input, try again
        if errSel >= len(self.buttons):
            print "Error on selection; try again."

The Button class is accepting a name, a function and a navigation element (the number you have to press for it to do the button's action):

class Button(object):

    """Base class for menu buttons"""

    def __init__(self, name, func, nav):
        # Initialize values
        self.name = name
        # Function associated with button
        self.func = func
        # Navigation element; number which user has to enter to do button action
        self.nav = nav

    def do(self):
        # Do the button's function
        self.func()

Finally, the Controller. Did I do it the right way? I mean it works but having that while loop cycling with only the raw_input() function stopping it from spamming infinite messages - is that correct or should I write something that prevents the while loop while a menu was already displayed and is awaiting user input?

class Controller(object):
    def __init__(self, menu):
        # Initialize values
        self.menu = menu
        # Start menu displaying / cycling
        self.cycle()

    def cycle(self):

        """Method for displaying / cycling through the menus"""

        while True:
            # Display menu and redisplay after button's function completes
            # Is this right? Will this loop correctly and never interfere
            # with raw_input()?
            self.menu.display()

Finally, the code that puts it all together:

import sys

def main():
    mainMenuButtonName = Button("Show name", showName, 1)
    mainMenuButtonVersion = Button("Show version", showVersion, 2)
    mainMenuButtonAbout = Button("Show about", showAbout, 3)
    mainMenuButtonQuit = Button("Quit", quit, 0)

    mainMenuButtons = [mainMenuButtonName, mainMenuButtonVersion, mainMenuButtonAbout, mainMenuButtonQuit]

    mainMenu = Menu("Main menu", mainMenuButtons)
    controller = Controller(mainMenu)

    controller.cycle()

def showName():
    print "My name is..."

def showVersion():
    print "My version is 0.1"

def showAbout():
    print "I am a demo app for testing menus"

def quit():
    sys.exit(0)

if __name__ == "__main__":
    main()
\$\endgroup\$
2
\$\begingroup\$

I think display() and userInput() would be clearer if you did it like this:

def display(self):
    """
    Display the menu alongside the navigation elements
    """
    response = None
    while response is None:
        # Display menu buttons
        for button in self.menu:
            print "   ", button.nav, button.name

        # Wait for user input
        response = self.userInput()

def userInput(self):
    """
    Method to check and act upon user's input
    """
    inputSel = raw_input("Enter selection> ")
    for button in self.menu:
        # If input equals to button's navigation element
        if inputSel == str(button.nav):
            button.do()
            return inputSel

    return None

So display() keeps showing the menu as long as userInput() returns None. userInput() returns None if the user's input doesn't match a button, or the inputSel value if it does match.

I think the way the Controller loops is fine. The program will always stop when it gets to raw_input() and read stdin until the user hits ENTER or RETURN. That's what raw_input() does.

You're managing your buttons as a list, which means you have to scan the whole list until you find the navigation element the user selected. If you put them in a dict, you can simply use the user's input to index directly into the structure and select the correct button (or determine that the user's selection is invalid). However, you still want them as a list for display because you can't control the order in which entries are pulled from a dictionary and I think you want your menu to display in the same order every time. That needs a list.

I would suggest the following for the constructor for the Display class. We're going to pass in a list so it's easy to define, and we'll keep the list, but we'll also have the class internally turn that list into a dict so it can more easily find the user's input on each selection.

class Display
    def __init__(self, menu):
        self.menu_list = menu
        self.menu_dict = {}
        # Initialise values
        for button in menu:
            self.menu_dict[button.nav] = button

With the simplification in userInput() and display() above, the shouldCycle member goes away. Now, with the menu defined internally as a dictionary (self.menu_dict) and as a list (self.menu_list), display() and userInput() will look like this:

def display(self):
    """
    Display the menu alongside the navigation elements
    """
    response = None
    while response is None:
        # Display menu buttons -- use the list so we get the same
        # order every time.
        for button in self.menu_list:
            print "   ", button.nav, button.name

        # Wait for user input
        response = self.userInput()

def userInput(self):
    """
    Method to check and act upon user's input
    """
    inputSel = raw_input("Enter selection> ")
    try:
        # Here we use the dictionary for ease of lookup
        button = self.menu_dict[int(inputSel)]
    except KeyError:
        # The user's selection didn't match any of the button.nav
        # values, so we got a KeyError exception on the dictionary
        return None

    button.do()
    return inputSel
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.