== 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: 1. Create a frame and a toolbar. 2. Get a reference to the frame using wx.Frame.!MacGetTopLevelWindowRef (new in wxPython 2.8.8.0). 3. Load the Carbon and !CoreFoundation frameworks using ctypes. 4. Use the frameworks to get pointers to each item in the toolbar. 5. 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.