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.
