wxPython: Working with Menus, Toolbars and Accelerators

Menus and toolbars are used in just about every modern program in existence, barring the ones that only run from the command line. In this post, we'll learn how to create them using the wxPython toolkit. Here's what you'll need to follow along:

Creating Menus

First, we'll take a look at the parts that make up a menu. The base of the menu system in wxPython is the wx.MenuBar object. As its name implies, it creates a bar to put your menus on. The next piece of the puzzle is the wx.Menu object. These are basically the words you see in your menu bar, such as "File" and "Edit". The items under the wx.Menu object are created by using the Append method of the menu object that you create. See the following example:

import wx
 
class MyForm(wx.Frame):
 
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "wx.Menu Tutorial")
 
        # Add a panel so it looks the correct on all platforms
        self.panel = wx.Panel(self, wx.ID_ANY)
 
        menuBar = wx.MenuBar()
        fileMenu = wx.Menu()
        exitMenuItem = fileMenu.Append(wx.NewId(), "Exit",
                                       "Exit the application")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)
 
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyForm().Show()
    app.MainLoop()

Your application should now look something like this:

Figure 1

Note the three arguments to create a menu item:

fileMenu.Append(wx.NewId(), "Exit",  "Exit the application")

You need an id, a label, and a description. The description will appear in the Statusbar, if you have created one.

Lately there's been a lot of people on the wxPython mailing list that want to disable items in the menu or entire menus. To do this, we have to use the counter-intuitively named methods, Enable() and EnableTop(), respectively. The Enable() method is part of the menu item's methods whereas EnableTop() must be called by the MenuBar object. Now be careful here as you need to attach the menubar to the frame BEFORE you can disable an entire menu.

Let's take a look at some code. Based on my source above, you could do something like this to disable the "exit" item in the "File" menu:

exitMenuItem.Enable(False)

If you wanted to disable the entire "File" menu, you would do this:

self.menuBar.EnableTop(0, False)

As you can see, creating menus with wxPython is quite easy and straight-forward. Now, let's bind an event handler to the "Exit" menu item or it won't do anything. Binding an event to a menu item is much the same as binding an event to anything else in wxPython. I will show you the easiest way and also the way that the demo does it.

The quickest way is to just bind to the EVT_MENU event, like this:

self.Bind(wx.EVT_MENU, self.onExit, exitMenuItem)

However, if you have lots of menu items, this gets tedious very quickly. Thus, I like the method I found in the wxPython demo of creating a nested method to do it for me. The general methodology is to put the following code inside one of your methods:

def doBind(item, handler):
        ''' Create menu events. '''
        self.Bind(wx.EVT_MENU, handler, item)

I usually create my menus in their own function that I call from the __init__ constructor. If you do it this way, then your code should end up looking something like the snippet below:

def createMenu(self):
    """ Create the menu bar. """
    def doBind(item, handler):
        """ Create menu events. """
        self.Bind(wx.EVT_MENU, handler, item)
    
    doBind( fileMenu.Append(wx.ID_ANY, "&Exit\tAlt+F4", "ExitProgram"),
            self.onExit)

Finally, you might want a horizontal line to separate some of the items in your menu. To add one, call your menu's AppendSeparator() method:

fileMenu.AppendSeparator()

Creating Toolbars

Using wxPython's toolbar functionality is really easy too. To initialize the toolbar, all you need to do is call wx.CreateToolBar(). I also set the toolbar's icon sizes using the SetToolBitmapSize() method. Toolbars can have separators too, but you use the toolbar's AddSeparator() rather than AppendSeparator().

Let's look at some code so you can see this better. Here is my createToolbar method:

def createToolbar(self):
    """
    Create a toolbar.
    """
 
    self.toolbar = self.CreateToolBar()
    self.toolbar.SetToolBitmapSize((16,16))  # sets icon size
 
    # Use wx.ArtProvider for default icons
    save_ico = wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, (16,16))
    saveTool = self.toolbar.AddSimpleTool(wx.ID_ANY, save_ico, "Save", "Saves the Current Worksheet")
    self.Bind(wx.EVT_MENU, self.onSave, saveTool)
 
    self.toolbar.AddSeparator()
 
    print_ico = wx.ArtProvider.GetBitmap(wx.ART_PRINT, wx.ART_TOOLBAR, (16,16))
    printTool = self.toolbar.AddSimpleTool(wx.ID_ANY, print_ico, "Print", "Sends Timesheet to Default Printer")
    self.Bind(wx.EVT_MENU, self.onPrint, printTool)
 
    delete_ico = wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_TOOLBAR, (16,16))
    deleteTool = self.toolbar.AddSimpleTool(wx.ID_ANY, delete_ico, "Delete", "Delete contents of cell")
    self.Bind(wx.EVT_MENU, self.onDelete, deleteTool)
 
    undo_ico = wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_TOOLBAR, (16,16))
    self.undoTool = self.toolbar.AddSimpleTool(wx.ID_UNDO, undo_ico, "Undo", "")
    self.toolbar.EnableTool(wx.ID_UNDO, False)
    self.Bind(wx.EVT_TOOL, self.onUndo, self.undoTool)
 
    redo_ico = wx.ArtProvider.GetBitmap(wx.ART_REDO, wx.ART_TOOLBAR, (16,16))
    self.redoTool = self.toolbar.AddSimpleTool(wx.ID_REDO, redo_ico, "Redo", "")
    self.toolbar.EnableTool(wx.ID_REDO, False)
    self.Bind(wx.EVT_TOOL, self.onRedo, self.redoTool)
 
    # This basically shows the toolbar 
    self.toolbar.Realize()

Note that I have disabled a couple of toolbar buttons by calling EnableTool(wx.ID_UNDO, False). As you can see, this method takes two arguments: the id of the toolbar button and a bool. To get an idea of how this looks, I have included the following screenshot:

Figure 2

Creating Accelerators

Most power users prefer to use keyboard shortcuts rather than digging through convoluted menus. Fortunately, wxPython provides a way to do just that; it's known as the Accelerator table. These tables are usually associated with the menu system, however you don't need menus to use the table. I will go over both approaches though.

First we'll talk about using the table with the menus. To begin, we need to create an instance of a wx.AcceleratorTable. The AcceleratorTable takes a lists of 3-item tuples consisting of a wx.ACCEL_CTRL, the key-combo, and an id.

For this example, I want to map CTRL+Q to make the application exit. The way to do that with wxPython is like this:

accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('Q'), exitMenuItem.GetId()) ])
self.frame.SetAcceleratorTable(accel_tbl)

Now let's do it without using a menu item. You'll notice that I create a new id and bind to an event handler before creating the table. Other than that, it's really not that much different.

exitId = wx.NewId()
self.Bind(wx.EVT_MENU, self.onExit, id=exitId )
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL,  ord('Q'), exitId )])
self.SetAcceleratorTable(accel_tbl) 

Now we've covered the basics of menu, toolbar and accelerator creation. I hope you've found this helpful. Send comments/questions to mike at pythonlibrary dot org.

** Update ** I had forgotten about this, but Robin Dunn reminded me that you can also use the wxUpdateUIEvent/EVT_UPDATE_UI to update the menus and toolbars and whether or not they are enabled or disabled.

Additional Resources

  1. ZetCode
  2. ShowMeDo Video
  3. Dream In Code
  4. Using Multi-key shortcuts
  5. KeyEvents

Note:All photos in this post are from a Windows XP box

Copyright © 2024 Mouse Vs Python | Powered by Pythonlibrary