Working With Menus

Creating and using drop down menus is easy. We will start by creating a class consisting of nothing more than a window with two drop down menus, 'File' and 'Help'.

I will add more examples as I get the time (or you can, if you'd like). Two examples that come to mind are "accelerators" or "hot keys" and showing sub-menus. It would also be nice to have an example to show how to use the status bar with the menu items (it's really quite simple, but as with most others on the list, I am a volunteer, pressed for time).

Example 1 - A Simple Menu

from wxPython.wx import *
# we want a unique identifiers to associate with each menu
# option. I use the convention of all (menu) identifiers begin
# with 'ID', followed by the menu the option belongs to, then
# the option name. It's a bit wordy, but I like it.
ID_FILE_OPEN = wxNewId()
ID_FILE_CLOSE = wxNewId()
ID_FILE_EXIT = wxNewId()
ID_HELP_ABOUT = wxNewId()
class myFrame(wxFrame):
    def __init__(self, parent, id, title):
        # create a frame for our demo
        wxFrame.__init__(self, parent, id, title)
        # create the 'File' menu, then add the 'open',
        # 'close', and 'exit' options. Notice that we use
        # the identifiers created above to associate an id
        # to each menu item (we will use the id again when
        # we want to 'tell' the program what to do when the
        # user selects a menu item).  We didn't mention
        # the 'AppendSeparator()', but you should be able
        # to figure out what it does :-)
        #
        file_menu = wxMenu()
        file_menu.Append(ID_FILE_OPEN, 'Open File')
        file_menu.Append(ID_FILE_CLOSE, 'Close File')
        file_menu.AppendSeparator()
        file_menu.Append(ID_FILE_EXIT, 'Exit Program')
        # create the 'Help' menu
        help_menu = wxMenu()
        help_menu.Append(ID_HELP_ABOUT, 'About')
        # we now need a menu bar to hold the 2 menus just created
        menu_bar = wxMenuBar()
        # add our menus to the menu bar. The first argument is the
        # menu (created above), and the second is what we want to
        # appear on the menu bar.
        #
        menu_bar.Append(file_menu, 'File')
        menu_bar.Append(help_menu, 'Help')
        # set the menu bar (tells the system we're done)
        self.SetMenuBar(menu_bar)
        # that's great! Now let's make the menu options do something!
        # Using EVT_MENU, we associate the identifier for each menu
        # item to a method to be called when the menu item is selected.
        # Most of these items will call the 'ToDo' function; essentially
        # a small stub method to tell the user something will happen,
        # but we have not got around to programming it, yet.
        #
        EVT_MENU(self, ID_FILE_OPEN, self.ToDo)
        EVT_MENU(self, ID_FILE_CLOSE, self.ToDo)
        EVT_MENU(self, ID_FILE_EXIT, self.OnFileExit)
        EVT_MENU(self, ID_HELP_ABOUT, self.ToDo)
    def OnFileExit(self, evt):
        """
        This is executed when the user clicks the 'Exit' option
        under the 'File' menu.  We ask the user if they *really*
        want to exit, then close everything down if they do.
        """
        dlg = wxMessageDialog(self, 'Exit Program?', 'I Need To Know!',
                              wxYES_NO | wxICON_QUESTION)
        if dlg.ShowModal() == wxID_YES:
            dlg.Destroy()
            self.Close(true)
        else:
            dlg.Destroy()
    def ToDo(self, evt):
        """
        A general purpose "we'll do it later" dialog box
        """
        dlg = wxMessageDialog(self, 'Not Yet Implimented!', 'ToDo',
                             wxOK | wxICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()
class myMenuApp(wxApp):
    def OnInit(self):
        frame = myFrame(NULL, -1, 'Menu Demo - Powered by wxPython')
        frame.Show(true)
        self.SetTopWindow(frame)
        return true
##########################################################################
### Test Code ############################################################
##########################################################################
app=myMenuApp(0)
app.MainLoop()

Example 2 - A Menu with Status Bar help

Status bars are really easy; all you have to do is create the status bar with a CreateStatusBar() call; then to change the text of the status bar you use SetStatusText().

Status bars can also be tied into your menu system very easily, by simply providing another argument to the wxMenu.Append() call, as you can see below. Here we've taken the code from example 1 and added a status bar that provides context-sensitive help as you browse the menu. And with only six more lines of code! (Well, seven if you count the comment.)

from wxPython.wx import *
# we want a unique identifiers to associate with each menu
# option. I use the convention of all (menu) identifiers begin
# with 'ID', followed by the menu the option belongs to, then
# the option name. It's a bit wordy, but I like it.
ID_FILE_OPEN = wxNewId()
ID_FILE_CLOSE = wxNewId()
ID_FILE_EXIT = wxNewId()
ID_HELP_ABOUT = wxNewId()
class myFrame(wxFrame):
    def __init__(self, parent, id, title):
        # create a frame for our demo
        wxFrame.__init__(self, parent, id, title)
        # create the status bar and put in some initial text
        self.CreateStatusBar()
        self.SetStatusText('Welcome to Menu Demo!')
        # create the 'File' menu, then add the 'open',
        # 'close', and 'exit' options. Notice that we use
        # the identifiers created above to associate an id
        # to each menu item (we will use the id again when
        # we want to 'tell' the program what to do when the
        # user selects a menu item).  We didn't mention
        # the 'AppendSeparator()', but you should be able
        # to figure out what it does :-)
        #
        file_menu = wxMenu()
        file_menu.Append(ID_FILE_OPEN, 'Open File',
                         'Open a new file.')
        file_menu.Append(ID_FILE_CLOSE, 'Close File',
                         'Close the current file.')
        file_menu.AppendSeparator()
        file_menu.Append(ID_FILE_EXIT, 'Exit Program',
                         'Exit (will ask for confirmation).')
        # create the 'Help' menu
        help_menu = wxMenu()
        help_menu.Append(ID_HELP_ABOUT, 'About',
                         'About this program.')
        # we now need a menu bar to hold the 2 menus just created
        menu_bar = wxMenuBar()
        # add our menus to the menu bar. The first argument is the
        # menu (created above), and the second is what we want to
        # appear on the menu bar.
        #
        menu_bar.Append(file_menu, 'File')
        menu_bar.Append(help_menu, 'Help')
        # set the menu bar (tells the system we're done)
        self.SetMenuBar(menu_bar)
        # that's great! Now let's make the menu options do something!
        # Using EVT_MENU, we associate the identifier for each menu
        # item to a method to be called when the menu item is selected.
        # Most of these items will call the 'ToDo' function; essentially
        # a small stub method to tell the user something will happen,
        # but we have not got around to programming it, yet.
        #
        EVT_MENU(self, ID_FILE_OPEN, self.ToDo)
        EVT_MENU(self, ID_FILE_CLOSE, self.ToDo)
        EVT_MENU(self, ID_FILE_EXIT, self.OnFileExit)
        EVT_MENU(self, ID_HELP_ABOUT, self.ToDo)
    def OnFileExit(self, evt):
        """
        This is executed when the user clicks the 'Exit' option
        under the 'File' menu.  We ask the user if they *really*
        want to exit, then close everything down if they do.
        """
        dlg = wxMessageDialog(self, 'Exit Program?', 'I Need To Know!',
                              wxYES_NO | wxICON_QUESTION)
        if dlg.ShowModal() == wxID_YES:
            dlg.Destroy()
            self.Close(true)
        else:
            dlg.Destroy()
    def ToDo(self, evt):
        """
        A general purpose -we'll do it later- dialog box
        """
        dlg = wxMessageDialog(self, 'Not Yet Implimented!', 'ToDo',
                             wxOK | wxICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()
class myMenuApp(wxApp):
    def OnInit(self):
        frame = myFrame(NULL, -1, 'Menu Demo - Powered by wxPython')
        frame.Show(true)
        self.SetTopWindow(frame)
        return true
##########################################################################
### Test Code ############################################################
##########################################################################
app=myMenuApp(0)
app.MainLoop()

A More Versatile Menu System

The default wxPython menu system is fine, but have you ever wished for a better way of creating menus? One where you could just make a Python list containing your menu, and assign functions directly to menu items? I have. That's why I created the following recipe.

import wx
class curry:
    """Taken from the Python Cookbook, this class provides an easy way to
    tie up a function with some default parameters and call it later.
    See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 for more.
    """
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.pending = args[:]
        self.kwargs = kwargs
    def __call__(self, *args, **kwargs):
        if kwargs and self.kwargs:
            kw = self.kwargs.copy()
            kw.update(kwargs)
        else:
            kw = kwargs or self.kwargs
        return self.func(*(self.pending + args), **kw)
class MainFrame(wx.Frame):
    def BuildSubmenu(self, subMenu):
        subMenuObject = wx.Menu()
        for item in subMenu:
            if not item: #allow now to add separators
                subMenuObject.AppendSeparator()
                continue
            statustext = ''; uihandler = None
            if len(item) == 2:
                title, action = item
            elif len(item) == 3:
                if type(item[2]) is str:
                    title, action, statustext = item
                else:
                    title, action, statustext = item
            elif len(item) == 4:
                title, action, statustext, uihandler = item
            else:
                raise AssertionError, \
                    'Item %s should have either 2 to 4 parts' % (item,)
            if type(action) is list:
                _id = wx.NewId()
                subMenuObject.AppendMenu(_id, title, self.BuildSubmenu(action))
            else:
                _id = wx.NewId()
                subMenuObject.Append(_id, title, statustext)
                wx.EVT_MENU(self, _id, action)
            if uihandler:
                wx.EVT_UPDATE_UI(self, _id, uihandler)
        return subMenuObject
    def BuildMenu(self, menu):
        mainMenu = wx.MenuBar()
        for title, subMenu in menu:
            mainMenu.Append(self.BuildSubmenu(subMenu), title)
        return mainMenu
    def CreateMySampleMenu(self):
        menu = [
            ('&File', [
                ('&Open', self.FileOpen),
            ]),
            ('&Edit', [
                ('&Copy', self.EditCopy),
                ('&Paste', self.EditPaste),
            ]),
            ('&View', [
                ('&One item', curry(self.DataBox, 1)),
                ('&Second item', curry(self.DataBox, 2)),
                ('Sub&menu', [
                    ('&Three', curry(self.DataBox, 3)),
                    ('&Four', curry(self.DataBox, 4)),
                ]),
            ]),
        ]
        self.SetMenuBar(self.BuildMenu(menu))
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, 'Main window')
        self.CreateStatusBar()
        self.SetStatusText('Welcome to a better menu system!')
        self.CreateMySampleMenu()
    def FileOpen(self, event):
        self.Info(self, 'You chose File->Open')
    def EditCopy(self, event):
        self.Info(self, 'You chose Edit->Copy')
    def EditPaste(self, event):
        self.Info(self, 'You chose Edit->Paste')
    def DataBox(self, num, event):
        self.Info(self, 'You chose item %d' % (num,))
    def Info(self, parent, message, caption = 'Better Menus'):
        dlg = wx.MessageDialog(parent, message, caption, \
            wx.OK | wx.ICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()
class MyApp(wx.App):
    def OnInit(self):
        frame = MainFrame(None, -1)
        frame.Show(True)
        self.SetTopWindow(frame)
        return True
if __name__ == '__main__':
    app = MyApp(0)
    app.MainLoop()

Using this system is quite simple. A menu entry is a tuple consisting of (title, action). You can also include text to appear in the status bar when the cursor is hovering over that menu item; then the tuple becomes (title, action, statustext). A menu is simply a list of menu entries.

The "action" component of a menu is either a function object (which will be called with a single parameter, event), or else a list. If the "action" component is a list, then this is a submenu.

That's all there is to it! The results then look like this:

Main page:

screenshot_main.png

File menu:

screenshot_filemenu.png

Edit menu:

screenshot_editmenu.png

View menu (showing the submenu)

screenshot_viewmenu.png

Pretty nifty, huh?

Comments...

A quick note about NewId() being deprecated.. it's now better to use wxNewId() I believe?

--Pete Crosier



#python partial code follows
    def BuildSubmenu(self, subMenu):
        subMenuObject = wx.Menu()
        for item in subMenu:
            if not item: #allow now to add separators
                subMenuObject.AppendSeparator()
                continue
            statustext = ''; uihandler = None
            if len(item) == 2:
                title, action = item
            elif len(item) == 3:
                if type(item[2]) is str:
                    title, action, statustext = item
                else:
                    title, action, statustext = item
            elif len(item) == 4:
                title, action, statustext, uihandler = item
            else:
                raise AssertionError, \
                    'Item %s should have either 2 to 4 parts' % (item,)
            if type(action) is list:
                _id = wx.NewId()
                subMenuObject.AppendMenu(_id, title, self.BuildSubmenu(action))
            else:
                _id = wx.NewId()
                subMenuObject.Append(_id, title, statustext)
                wx.EVT_MENU(self, _id, action)
            if uihandler:
                wx.EVT_UPDATE_UI(self, _id, uihandler)
        return subMenuObject

So, the menu could change to something like:

#python partial code follows
        menu = [
            ('&File', [
                ('&Open', self.FileOpen),
            ]),
            ('&Edit', [
                ('&Undo', self.EditUndo, 'Undo last action', self.UICanUndo),
                 None, #append separator
                ('&Copy', self.EditCopy, 'Copy current selection', self.UICanCopy),
                ('&Paste', self.EditPaste, self.UICanPaste), # check the clipboard
            ]),
            ('&View', [
                ('&One item', curry(self.DataBox, 1)),
                ('&Second item', curry(self.DataBox, 2)),
                ('Sub&menu', [
                    ('&Three', curry(self.DataBox, 3)),
                    ('&Four', curry(self.DataBox, 4)),
                ]),
            ]),
        ]

Then you could manage on 'UICanPaste' function to decide if you need to enable or disable your item (like checking if the clipboard has your desired kind of data).

In fact inside your UI handler function you can also check/uncheck your items. Check the wxWindow::UpdateWindowUI and wxUpdateUIEvent documentation.


        MenuBar = MenuBarConfig
        Menu = MenuConfig
        Item = MenuItemConfig
        menuBarConfig = MenuBar([
            Menu('&File', [
                Item('&Open', self.FileOpen),
            ]),
            Menu('&Edit', [
                Item('&Undo', self.EditUndo, 'Undo last action', isUndoable=self.UICanUndo),
                None, #append separator
                Item('&Copy', self.EditCopy, 'Copy current selection', self.UICanCopy),
                Item('&Paste', self.EditPaste, self.UICanPaste), # check the clipboard
            ]),
            Menu('&View', [
                Item('&One item', curry(self.DataBox, 1)),
                Item('&Second item', curry(self.DataBox, 2)),
                Menu('Sub&menu', [
                    Item('&Three', curry(self.DataBox, 3)),
                    Item('&Four', curry(self.DataBox, 4)),
                ]),
            ]),
        ])

The advantages is that keyword arguments can be used, like in the isUndoable case. It can also be more flexible by delegating more responsabilities to the objects and let the user create custom classes. In fact, I guess I would make a command class that is the second parameter for items:

            ...
            Menu('&Edit', [
                Item('&Undo', Command(self.EditUndo, 'Undo last action', isUndoable=self.UICanUndo)),
                ...

This way commands can be used in multiple contexts; the same command can be used for toolbars, contextual menus (which are menus anyway), etc.

WorkingWithMenus (last edited 2008-03-11 10:50:28 by localhost)