This page has some hints on how to make your app more "Mac-like" on OS-X. See the bottom for a small sample app that implements all these methods.
Menus
Apple and Microsoft specify slightly different layouts for menubars. (Apple specification. Microsoft specification.) WxWidgets will automatically move certain menus on a Macintosh, to ease the task of writing cross-platform applications with native look and feel on both MS-Windows and Apple Macintosh; the wxWidgets default is Windows-like. The following code makes a "Help" menubar with an "About" item which will appear properly on both platforms:
That results in a "Help" menu on Windows, with "About MyApp" as a menu item (which is the Windows standard), and an "About MyApp" menu item in the Application menu on MacOS. (Note that it also creates an empty "Help" menu, but presumably you can fill that up with, well, Help. --MarcHedlund) There are similar tricks for Preferences and Quit menus, which are moved to the "Application" menu. See the sample below.
In addition, on the Macintosh, wxWidgets automatically creates the "Windows" menu, with a list of windows and "Minimize", "Zoom", and "Bring All to Front" items. This can be disabled by invoking the menubar's SetAutoWindowMenu method.
Creating an Application Bundle (.app)
Just like you can create stand alone executables on Windows, you can also make stand alone executables of your apps on Mac OS X, though it is a bit trickier due to compatibility issues between the various Mac OS X versions out there. Generally, the rule of thumb is that your stand alone executable should work on every version of OS X equal or greater to the version the executable was created on. Thus a .app bundle created on Jaguar would run on Panther, but one created on Panther would not run on Jaguar.
The old "BundleBuilder" module for making .app files is no longer supported or recommended. To make an .app on Mac now, use py2app. py2app 0.3.6 is current as of this writing. Unfortunately, the py2app project is somewhat confusing to learn; if you run into trouble, the pythonmac-sig mailing list is the best place to ask for help. The docs have gotten better, however.
The following setup.py excerpt works to create a wxPython-based .app under Python version 2.7; wxPython version 2.8.12.1; MacOS X 10.8.3 (Intel); py2app 0.7.3:
1 from setuptools import setup
2
3 APP = ['main.py'] #main file of your app
4 DATA_FILES = []
5 OPTIONS = {'argv_emulation': True,
6 'site_packages': True,
7 'arch': 'i386',
8 'iconfile': 'lan.icns', #if you want to add some ico
9 'plist': {
10 'CFBundleName': 'MyAppName',
11 'CFBundleShortVersionString':'1.0.0', # must be in X.X.X format
12 'CFBundleVersion': '1.0.0',
13 'CFBundleIdentifier':'com.company.myapp', #optional
14 'NSHumanReadableCopyright': '@ Me 2013', #optional
15 'CFBundleDevelopmentRegion': 'English', #optional - English is default
16 }
17 }
18 setup(
19 app=APP,
20 data_files=DATA_FILES,
21 options={'py2app': OPTIONS},
22 setup_requires=['py2app'],
23 )
In setup file must be arch option because in new macs based on i5, i7 etc python most of the time is default in 64bit version and wx python don'like this.
Don't be scary by size of final file. Simple app containing about 350 lines get 80mb... If you don't need to run that app on PPC based processors try TrimTheFat to get smaller size.
I've generally found that trying to use one set of options for both py2app and py2exe is a good way to get frustrated. I have two blocks in my setup.py, one for Mac and one for Windows, and this block starts with if sys.platform == 'darwin': . --MarcHedlund
The py2app docs also discuss using py2app and py2exe with the same setup.py
Making your Application a Drop Target for Files
To make your application a drop target for files, you must do two things. The first is to override wx.App.MacOpenFile(string) to contain your code for loading the file. For example:
class MyApp(wx.App):
def OnInit(self):
- #do init stuff here
def MacOpenFile(self, filename):
- print filename #code to load filename goes here.
Once you do this, if you hold down the Option key and drag the file over your script, it will run the code in MacOpenFile. Why do you need to hold down option? Because you haven't yet gotten Mac OS X to recognize that you can open the files in your app. =) To do this, first create an application bundle as described in "Creating an Application Bundle", then add code like the following into the bundle's "info.plist" file (the below registers the htm and html file extensions):
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key> <array>
<string>html</string> <string>htm</string>
</array> <key>CFBundleTypeName</key> <string>HTML Document</string> <key>CFBundleTypeRole</key> <string>Viewer</string>
</dict>
</array>
Note that CFBundleTypeName refers to the "human readable" name for the format, and the common CFBundleTypeRole values are "Viewer" and "Editor". I think they're pretty self-explanatory. This the basic info needed to put together a file extension. For more information, see: http://developer.apple.com/mac/library/documentation/MacOSX/Conceptual/BPRuntimeConfig/000-Introduction/introduction.html
py2app allows you to define the info.plist contents in a python dictionary, and it will build the plist for you. This is an example of the dictionary for the same example as above:
# A custom plist for letting it associate with all files. Plist = dict(CFBundleDocumentTypes=[dict(CFBundleTypeExtensions=["html","htm"], CFBundleTypeName="HTML Document", CFBundleTypeRole="Viewer"), ] )
This can then be added to the py2app options like so:
OPTIONS = {'argv_emulation': True, 'iconfile': 'MacAppIcon.icns', 'plist': Plist, }
which can then be added to the setup command:
setup( app=APP, data_files=DATA_FILES, options={'py2app': OPTIONS}, setup_requires=['py2app'], )
Dropped Files on Startup
The above method doesn't work right for files dropped on the app's icon when the application is started. If you want to capture those (and you probably do), you want to use "argv emulation". Py2app sets this to true by default, and what it does is put the files dropped on the app at startup into sys.argv. They can then be handled just like you would file names passed in on a command line with a traditional command line *nix app.
In your app class implement the following message to get the callback for it:
def MacReopenApp(self):
- # do your window raising
Making it work correctly with the Dock
A Mac app should come to the foreground and show a window when it's Dock icon is clicked. This requires that you handle the kAEReopenApplication event, which wx has built in handling for. You need to add a MacReopenApp method to your App:
def MacReopenApp(self): """Called when the doc icon is clicked, and ???""" self.GetTopWindow().Raise()
Which will raise the Top Window of your app. Your app may require something special, and you can put there in this method. Apple's HIG about this is here: APPLE HIG for Dock
Other Apple events
wx has a few more built in handlers for other system events that you may want to handle:
def MacNewFile(self): pass def MacPrintFile(self, file_path): pass
There is also the wx.EVT_ACTIVATE_APP event, which yu may want to catch in your App object. I'm not entirely sure when it gets called, but I needed to do this to make it work right when another application was trying to raise my app with a system call. Without it, the app would not raise if it was minimized.
Putting it all together
Below is a simple app that does all the menus right, and handles drag and drop of files onto the app's icon in the doc or in the finder. (Tested on OS-X 10.4, Python 2.5, wxPython 2.8.1)
""" MacApp.py This is a small, simple app that tried to do all the right things on OS-X -- putting standard menus in the right place, etc. It should work just fine on other platforms as well : that's the beauty of wx! """ import wx class DemoFrame(wx.Frame): """ This window displays a button """ def __init__(self, title = "Micro App"): wx.Frame.__init__(self, None , -1, title) MenuBar = wx.MenuBar() FileMenu = wx.Menu() item = FileMenu.Append(wx.ID_EXIT, text = "&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) item = FileMenu.Append(wx.ID_ANY, text = "&Open") self.Bind(wx.EVT_MENU, self.OnOpen, item) item = FileMenu.Append(wx.ID_PREFERENCES, text = "&Preferences") self.Bind(wx.EVT_MENU, self.OnPrefs, item) MenuBar.Append(FileMenu, "&File") HelpMenu = wx.Menu() item = HelpMenu.Append(wx.ID_HELP, "Test &Help", "Help for this simple test") self.Bind(wx.EVT_MENU, self.OnHelp, item) ## this gets put in the App menu on OS-X item = HelpMenu.Append(wx.ID_ABOUT, "&About", "More information About this program") self.Bind(wx.EVT_MENU, self.OnAbout, item) MenuBar.Append(HelpMenu, "&Help") self.SetMenuBar(MenuBar) btn = wx.Button(self, label = "Quit") btn.Bind(wx.EVT_BUTTON, self.OnQuit ) self.Bind(wx.EVT_CLOSE, self.OnQuit) def OnQuit(self,Event): self.Destroy() def OnAbout(self, event): dlg = wx.MessageDialog(self, "This is a small program to test\n" "the use of menus on Mac, etc.\n", "About Me", wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() def OnHelp(self, event): dlg = wx.MessageDialog(self, "This would be help\n" "If there was any\n", "Test Help", wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() def OnOpen(self, event): dlg = wx.MessageDialog(self, "This would be an open Dialog\n" "If there was anything to open\n", "Open File", wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() def OnPrefs(self, event): dlg = wx.MessageDialog(self, "This would be an preferences Dialog\n" "If there were any preferences to set.\n", "Preferences", wx.OK | wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() class MyApp(wx.App): def __init__(self, *args, **kwargs): wx.App.__init__(self, *args, **kwargs) # This catches events when the app is asked to activate by some other # process self.Bind(wx.EVT_ACTIVATE_APP, self.OnActivate) def OnInit(self): frame = DemoFrame() frame.Show() import sys for f in sys.argv[1:]: self.OpenFileMessage(f) return True def BringWindowToFront(self): try: # it's possible for this event to come when the frame is closed self.GetTopWindow().Raise() except: pass def OnActivate(self, event): # if this is an activate event, rather than something else, like iconize. if event.GetActive(): self.BringWindowToFront() event.Skip() def OpenFileMessage(self, filename): dlg = wx.MessageDialog(None, "This app was just asked to open:\n%s\n"%filename, "File Dropped", wx.OK|wx.ICON_INFORMATION) dlg.ShowModal() dlg.Destroy() def MacOpenFile(self, filename): """Called for files droped on dock icon, or opened via finders context menu""" print filename print "%s dropped on app"%(filename) #code to load filename goes here. self.OpenFileMessage(filename) def MacReopenApp(self): """Called when the doc icon is clicked, and ???""" self.BringWindowToFront() def MacNewFile(self): pass def MacPrintFile(self, file_path): pass app = MyApp(False) app.MainLoop()
The app above can be bundled up with py2app and the following setup.py:
""" This is a setup.py script altered from one generated by py2applet Usage: python setup.py py2app """ from setuptools import setup # A custom plist for letting it associate with all files. Plist = dict(CFBundleDocumentTypes= [dict(CFBundleTypeExtensions=["*"], #CFBundleTypeName="kUTTypeText", # this should be text files, but I'm not sure the details. CFBundleTypeRole="Editor"), ] ) APP = ['MacApp.py'] DATA_FILES = [] OPTIONS = {'argv_emulation': True, # this puts the names of dropped files into sys.argv when starting the app. 'iconfile': 'MacAppIcon.icns', 'plist': Plist, } setup( app=APP, data_files=DATA_FILES, options={'py2app': OPTIONS}, setup_requires=['py2app'], )
You can use any icon bundle named MacAppIcon.icns (I can't figure out how to enclosed a file here)
Accepting AppleEvent handlers that are not taken care
In MacOpenFile the respective AppleEvent handler is taken care of .. though what is with special things like to accept a pseudo protocol (GURL AppleEvent). In this case a custom wxPython Extension would have to be compiled .. the procedure is described on: Catching AppleEvents in wxMAC .. though at the moment the includes are a bit outdated thus its not compilable directly as is. also like before the correct Info.plist entrys would have to be made to register as ex the protocol to the application.
Older Versions of wxPython (< 2.6):
I've been unable to get the About/Preferences/Quit menu items defined correctly without calling:
wx.App_SetMacAboutMenuItemId()
wx.App_SetMacPreferencesMenuItemId()
wx.App_SetMacExitMenuItemId()
Also, for the About menu to get set correctly, it appears that you must also set
wx.App_SetMacHelpMenuTitleName("&Help")
the ampersand is mandatory. This is true even if your About menu item isn't in your Help menu - the help menu still needs to be set in order for About to work.
Scripting Other Applications
This isn't directly related to wxPython applications, but you can use Python to script applications which understand AppleScript using AppScript. This is a very useful tool for integrating your wxPython app with other Mac apps on the user's machine.
- This was also posted as feedback to my original Python entry:
I haven't yet done any work with wXpython on OS X, and I'm not sure that you won't encounter the same problem that you found with DropThing, but you might want to give Platypus a try: http://sveinbjorn.sytes.net/platypus
Good luck!