- wxPython and Autogenerated Menubars
- 05 Jan 2008 10:58:26 pm
- Last edited by Kllrnohj on 06 Jan 2008 01:50:12 am; edited 1 time in total
I figured I would finally write up some of the python tips and tricks I've learned. Python is what I like to call an extremely high level language. The things that are possible in it are purely pipe dreams and wishful thinking for other languages, as I hope to demonstrate in a series of these Python "tips". Here is a pretty cool one I've been working on that will "auto-generate" the menu bar for a GUI. The following assumes at least basic knowledge of Python + GUI programming (any language)
If you have ever done GUI programming you no doubt know that coding a GUI can be extremely repetitive and boring. Sure, GUI designer tools can greatly cut back on that, but it is still repetitive. In the case of a MenuBar, it requires adding each item and then setting up a call-back for it - one by one.
However, I recently came across someone suggesting to use Python's data-driven nature to try and auto-create some GUI elements, and I thought that a MenuBar would be the perfect application for this practice. I've since banged out a simple, working method, and I am quite pleased with the results.
To start out, I will post the source for a simple demonstration of this, followed by a breakdown:
Code:
For the purpose of this write-up, most of the above can be ignored
The first part to notice is the following
Code:
This is the method I chose in which to represent my menus in data form (I don't need sub-menus, or I might have chosen a different representation - at this point it doesn't matter how this list data is gotten or in what form it is). The _menuKeys defines the ordering for the menu's to be created, as dicts (hash-maps) aren't sorted nor does it preserve the ordering.
Now that the menu storage is in place, on to the fun stuff The following is how the menus are actually created.
Code:
First, a MenuBar is created - standard affair as far as that is concerned. The next step is to loop through the _menuKeys (so that the order can be specified). For each 'root' item (each value in _menuKeys), a Menu is created. Then, for each value in the list that is stored in _menu with the key value 'm' (which is one of the values of _menuKeys), loop through and add it to the 'root' Menu.
So far that should be pretty simple if you are familiar with Python, and really it would work in pretty much any language that I know of. The fun part is really in these 4 lines:
Code:
What is happening here is simple, yet not something that is possible in most languages. What is basically happening, is getattr() tries to access an attribute (in this case a function - but this isn't being enforced in any way) and retrieve it. Essentially getattr(self, X) == self.X. However, since 'X' in this case is a string, the latter isn't possible. If the attribute isn't found it throws an AttributeError exception, which is caught and then a default event handler function is used. The function used (either from getattr or the default) is then bound to the menu item being created to use as its callback function.
To see this in action, you need Python (tested with 2.5 - but any version should work as this isn't anything newly added) and wxPython (best stick with the latest version, however, as this demo is using the new-ish AUI frame manager).
The cool thing with this is that callback functions are auto-bound to the menu item, which makes editing a menu extremely simple. Try it out. At the end of the TMGui class, add the following and run the program:
Code:
Now instead of printing out the default "Couldn't find OnCopy()" it will print out "Copied some crap ".
For a 'twist' on this, one could bind all menu items to the default handler, and then in the default handler call getattr to try and find the callback function. By doing it that way, the callback function can be created at runtime and it would work perfectly. Basically you would just have to change the DefaultMenuHandler to the following:
Code:
Now if you run it and choose Edit->Paste, it will print out "Couldn't find OnPaste()". However, if you selected Edit->Copy, and then choose Edit->Paste, the program will close, as there now IS a self.OnPaste() function - which was "created" in the OnCopy() function.
Neat, huh?
suggestions/other ideas you would like to see worked out are welcome - always looking for a challenge
If you have ever done GUI programming you no doubt know that coding a GUI can be extremely repetitive and boring. Sure, GUI designer tools can greatly cut back on that, but it is still repetitive. In the case of a MenuBar, it requires adding each item and then setting up a call-back for it - one by one.
However, I recently came across someone suggesting to use Python's data-driven nature to try and auto-create some GUI elements, and I thought that a MenuBar would be the perfect application for this practice. I've since banged out a simple, working method, and I am quite pleased with the results.
To start out, I will post the source for a simple demonstration of this, followed by a breakdown:
Code:
import sys, os
import wx, wx.aui
class TMGui (wx.Frame):
"""GUI class for the main view of TM"""
# Menu dict
_menu = {'File': ['Open', 'Close'],
'Edit': ['Copy', 'Paste']}
_menuKeys = ['File', 'Edit']
def __init__(self, parent, id=-1, title="", pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE |
wx.SUNKEN_BORDER |
wx.CLIP_CHILDREN):
wx.Frame.__init__(self, parent, id, title, pos, size, style)
# tell FrameManager to manage this frame
self._mgr = wx.aui.AuiManager(self)
# Autogen menus
mb = wx.MenuBar()
for m in self._menuKeys:
root = wx.Menu()
for i in self._menu[m]:
item = root.Append(wx.ID_ANY, i)
try:
self.Bind(wx.EVT_MENU, getattr(self, 'On' + i), item)
except AttributeError:
self.Bind(wx.EVT_MENU, self.DefaultMenuHandler, item)
mb.Append(root, m)
self.SetMenuBar(mb)
# create several text controls
text1 = wx.TextCtrl(self, -1, 'Pane 1 - sample text',
wx.DefaultPosition, wx.Size(200,150),
wx.NO_BORDER | wx.TE_MULTILINE)
text2 = wx.TextCtrl(self, -1, 'Pane 2 - sample text',
wx.DefaultPosition, wx.Size(200,150),
wx.NO_BORDER | wx.TE_MULTILINE)
text3 = wx.TextCtrl(self, -1, 'Main content window',
wx.DefaultPosition, wx.Size(200,150),
wx.NO_BORDER | wx.TE_MULTILINE)
# add the panes to the manager
self._mgr.AddPane(text1, wx.LEFT, 'Pane Number One')
self._mgr.AddPane(text2, wx.BOTTOM, 'Pane Number Two')
self._mgr.AddPane(text3, wx.CENTER)
# tell the manager to 'commit' all the changes just made
self._mgr.Update()
self.Bind(wx.EVT_CLOSE, self.OnClose)
def OnClose(self, event):
# deinitialize the frame manager
self._mgr.UnInit()
# delete the frame
self.Destroy()
def DefaultMenuHandler(self, event):
print 'Couldn\'t find On%s()' % (self.GetMenuBar().FindItemById(event.GetId()).GetItemLabel())
if (__name__ == "__main__"):
app = wx.App()
frame = TMGui(None, size=(750, 600))
frame.Show()
app.MainLoop()
For the purpose of this write-up, most of the above can be ignored
The first part to notice is the following
Code:
# Menu dict
_menu = {'File': ['Open', 'Close'],
'Edit': ['Copy', 'Paste']}
_menuKeys = ['File', 'Edit']
Now that the menu storage is in place, on to the fun stuff The following is how the menus are actually created.
Code:
# Autogen menus
mb = wx.MenuBar()
for m in self._menuKeys:
root = wx.Menu()
for i in self._menu[m]:
item = root.Append(wx.ID_ANY, i)
try:
self.Bind(wx.EVT_MENU, getattr(self, 'On' + i), item)
except AttributeError:
self.Bind(wx.EVT_MENU, self.DefaultMenuHandler, item)
mb.Append(root, m)
self.SetMenuBar(mb)
So far that should be pretty simple if you are familiar with Python, and really it would work in pretty much any language that I know of. The fun part is really in these 4 lines:
Code:
try:
self.Bind(wx.EVT_MENU, getattr(self, 'On' + i), item)
except AttributeError:
self.Bind(wx.EVT_MENU, self.DefaultMenuHandler, item)
What is happening here is simple, yet not something that is possible in most languages. What is basically happening, is getattr() tries to access an attribute (in this case a function - but this isn't being enforced in any way) and retrieve it. Essentially getattr(self, X) == self.X. However, since 'X' in this case is a string, the latter isn't possible. If the attribute isn't found it throws an AttributeError exception, which is caught and then a default event handler function is used. The function used (either from getattr or the default) is then bound to the menu item being created to use as its callback function.
To see this in action, you need Python (tested with 2.5 - but any version should work as this isn't anything newly added) and wxPython (best stick with the latest version, however, as this demo is using the new-ish AUI frame manager).
The cool thing with this is that callback functions are auto-bound to the menu item, which makes editing a menu extremely simple. Try it out. At the end of the TMGui class, add the following and run the program:
Code:
def OnCopy(self, event):
print 'Copied some crap :)'
For a 'twist' on this, one could bind all menu items to the default handler, and then in the default handler call getattr to try and find the callback function. By doing it that way, the callback function can be created at runtime and it would work perfectly. Basically you would just have to change the DefaultMenuHandler to the following:
Code:
def DefaultMenuHandler(self, event):
mName = self.GetMenuBar().FindItemById(event.GetId()).GetItemLabel()
try:
getattr(self, 'On' + mName)(event)
except AttributeError:
print 'Couldn\'t find On%s()' % mName
def OnCopy(self, event):
self.OnPaste = self.OnClose
Now if you run it and choose Edit->Paste, it will print out "Couldn't find OnPaste()". However, if you selected Edit->Copy, and then choose Edit->Paste, the program will close, as there now IS a self.OnPaste() function - which was "created" in the OnCopy() function.
Neat, huh?
suggestions/other ideas you would like to see worked out are welcome - always looking for a challenge