Introduction
This page describes how to get the native "selected" look for toolbar buttons on Mac OS X. That's useful when you want to use the toolbar as a tab bar, e.g. in a preferences window. Look at the Finder preferences dialog for an example; notice the sunken look of the selected tab.
wxPython creates the native Mac OS X toolbar correctly. But the look for a "toggled" button looks nothing like the native look. That's not a bug; the native look can't be used in the general case, because it only allows one button to be toggled at a time (unlike wxPython). But for a tabs, that restriction doesn't matter. So this is how to invoke the native style manually, from Python.
Requirements
- wxPython 2.8.8.0 or newer
- ctypes
- OS X
Overview
The process works like this:
- Create a frame and a toolbar.
Get a reference to the frame using wx.Frame.MacGetTopLevelWindowRef (new in wxPython 2.8.8.0).
Load the Carbon and CoreFoundation frameworks using ctypes.
- Use the frameworks to get pointers to each item in the toolbar.
- When the user clicks a toolbar button, use Carbon's HIToolbarItemChangeAttributes and the pointer to change the "selected" property of the button.
The code below implements this behavior as a wx.Frame subclass which you can use or copy. If any of the conditions aren't met (not running on a Mac, older wxPython, ctypes not available), it defaults to the normal behavior. You can run it from the command line to see it working.
Code
import wx class TabbedFrame(wx.Frame): """ A wx.Frame subclass which uses a toolbar to implement tabbed views and invokes the native 'selected' look on OS X when running wxPython version 2.8.8.0 or higher. To use: - Create an instance. - Call CreateTabs with a list of (label, bitmap) pairs for the tabs. - Override OnTabChange(tabIndex) to respond to the user switching tabs. The native selection look on OS X requires that only one toolbar item be active at a time (like radio buttons). There is no such requirement with the toggle tools in wx, which is why the native look is not used (see http://trac.wxwidgets.org/ticket/8789). But this class enforces that exactly one tool is toggled at a time, so the native look can be enabled by loading the Carbon and CoreFoundation frameworks via ctypes and manipulating the toolbar. """ def CreateTabs(self, tabs): """ Create the toolbar and add a tool for each tab. tabs -- List of (label, bitmap) pairs. """ # Create the toolbar self.tabIndex = 0 self.toolbar = self.CreateToolBar(style=wx.TB_HORIZONTAL|wx.TB_TEXT) for i, tab in enumerate(tabs): self.toolbar.AddCheckLabelTool(id=i, label=tab[0], bitmap=tab[1]) self.toolbar.Realize() # Determine whether to invoke the special toolbar handling macNative = False if wx.Platform == '__WXMAC__': if hasattr(self, 'MacGetTopLevelWindowRef'): try: import ctypes macNative = True except ImportError: pass if macNative: self.PrepareMacNativeToolBar() self.Bind(wx.EVT_TOOL, self.OnToolBarMacNative) else: self.toolbar.ToggleTool(0, True) self.Bind(wx.EVT_TOOL, self.OnToolBarDefault) self.Show() def OnTabChange(self, tabIndex): """Respond to the user switching tabs.""" pass def PrepareMacNativeToolBar(self): """Extra toolbar setup for OS X native toolbar management.""" # Load the frameworks import ctypes carbonLoc = '/System/Library/Carbon.framework/Carbon' coreLoc = '/System/Library/CoreFoundation.framework/CoreFoundation' self.carbon = ctypes.CDLL(carbonLoc) # Also used in OnToolBarMacNative core = ctypes.CDLL(coreLoc) # Get a reference to the main window frame = self.MacGetTopLevelWindowRef() # Allocate a pointer to pass around p = ctypes.c_voidp() # Get a reference to the toolbar self.carbon.GetWindowToolbar(frame, ctypes.byref(p)) toolbar = p.value # Get a reference to the array of toolbar items self.carbon.HIToolbarCopyItems(toolbar, ctypes.byref(p)) # Get references to the toolbar items (note: separators count) self.macToolbarItems = [core.CFArrayGetValueAtIndex(p, i) for i in xrange(self.toolbar.GetToolsCount())] # Set the native "selected" state on the first tab # 128 corresponds to kHIToolbarItemSelected (1 << 7) item = self.macToolbarItems[self.tabIndex] self.carbon.HIToolbarItemChangeAttributes(item, 128, 0) def OnToolBarDefault(self, event): """Ensure that there is always one tab selected.""" i = event.GetId() if i in xrange(self.toolbar.GetToolsCount()): self.toolbar.ToggleTool(i, True) if i != self.tabIndex: self.toolbar.ToggleTool(self.tabIndex, False) self.OnTabChange(i) self.tabIndex = i else: event.Skip() def OnToolBarMacNative(self, event): """Manage the toggled state of the tabs manually.""" i = event.GetId() if i in xrange(self.toolbar.GetToolsCount()): self.toolbar.ToggleTool(i, False) # Suppress default selection if i != self.tabIndex: # Set the native selection look via the Carbon APIs # 128 corresponds to kHIToolbarItemSelected (1 << 7) item = self.macToolbarItems[i] self.carbon.HIToolbarItemChangeAttributes(item, 128, 0) self.OnTabChange(i) self.tabIndex = i else: event.Skip() if __name__ == '__main__': app = wx.PySimpleApp() size = (32, 32) tabs = [ ('List View', wx.ArtProvider.GetBitmap(wx.ART_LIST_VIEW, size=size)), ('Report View', wx.ArtProvider.GetBitmap(wx.ART_REPORT_VIEW, size=size)) ] frame = TabbedFrame(None) frame.CreateTabs(tabs) def OnTabChange(tabIndex): print "Switched to tab", tabIndex frame.OnTabChange = OnTabChange frame.Show() app.MainLoop()
Comments
Send questions or feedback to niemasik@gmail.com.