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

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.

NativeMacOSXToolbarSelection (last edited 2010-01-04 05:48:28 by c-67-188-7-84)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.