=== agw.FlatNotebook === {{http://www.blog.pythonlibrary.org/wp-content/uploads/2009/12/flatnotebookDemo.png}} The Flatbook control is written in Python rather than a wrapped widget from wxWidgets. It was added to wxPython with the release of wxPython 2.8.9.2 on February 16, 2009 as a part of the 3rd party package, agw. Since then Andrea Gavana has been updating the agw library with lots of fixes. My examples will work with the 2.8.9.2+ versions of wxPython, but I recommend getting the [[http://svn.wxwidgets.org/svn/wx/wxPython/3rdParty/AGW/|SVN version]] of agw and replacing your default one with it as it is the most up-to-date version of the code. Here are a few of the !FlatNotebook’s Features: * 5 Different tab styles * It’s a generic control (i.e. pure python) so it’s easy to modify * You can use the mouse’s middle-click to close tabs * A built-in function to add right-click pop-up menus on tabs * A way to hide the “X” that closes the individual tabs * Support for disabled tabs * Plus lots more! See the source and the wxPython Demo for more information! === Creating a Simple Example === Now that we’ve done an unpaid commercial, let’s create a simple example: '''Listing 1''' {{{ #!python import wx import wx.lib.agw.flatnotebook as fnb ######################################################################## class TabPanel(wx.Panel): """ This will be the first notebook tab """ #---------------------------------------------------------------------- def __init__(self, parent): """""" wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) sizer = wx.BoxSizer(wx.VERTICAL) txtOne = wx.TextCtrl(self, wx.ID_ANY, "") txtTwo = wx.TextCtrl(self, wx.ID_ANY, "") sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(txtOne, 0, wx.ALL, 5) sizer.Add(txtTwo, 0, wx.ALL, 5) self.SetSizer(sizer) ######################################################################## class FlatNotebookDemo(fnb.FlatNotebook): """ Flatnotebook class """ #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY) pageOne = TabPanel(self) pageTwo = TabPanel(self) pageThree = TabPanel(self) self.AddPage(pageOne, "PageOne") self.AddPage(pageTwo, "PageTwo") self.AddPage(pageThree, "PageThree") ######################################################################## class DemoFrame(wx.Frame): """ Frame that holds all other widgets """ #---------------------------------------------------------------------- def __init__(self): """Constructor""" wx.Frame.__init__(self, None, wx.ID_ANY, "FlatNotebook Tutorial", size=(600,400) ) panel = wx.Panel(self) notebook = FlatNotebookDemo(panel) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5) panel.SetSizer(sizer) self.Layout() self.Show() #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.PySimpleApp() frame = DemoFrame() app.MainLoop() }}} In Listing 1, I subclass !FlatNotebook and use a generic panel for the pages. You’ll notice that !FlatNotebook has its own !AddPage method that mimics the wx.Notebook. This should come as no surprise as the !FlatNotebook’s API is such that you should be able to use it as a drop-in replacement for wx.Notebook. Of course, right out of the box, !FlatNotebook has the advantage. If you run the demo above, you’ll see that !FlatNotebook allows the user to rearrange the tabs, close the tabs and it includes some previous/next buttons in case you have more tabs than can fit on-screen at once. === Applying Styles to the FlatNotebook === Now let’s take a look at the various styles that we can apply to !FlatNotebook: {{http://www.blog.pythonlibrary.org/wp-content/uploads/2009/12/flatnotebookStyleDemo.png}} '''Listing 2''' {{{ #!python import wx import wx.lib.agw.flatnotebook as fnb ######################################################################## class TabPanel(wx.Panel): """ This will be the first notebook tab """ #---------------------------------------------------------------------- def __init__(self, parent): """""" wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) sizer = wx.BoxSizer(wx.VERTICAL) txtOne = wx.TextCtrl(self, wx.ID_ANY, "") txtTwo = wx.TextCtrl(self, wx.ID_ANY, "") sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(txtOne, 0, wx.ALL, 5) sizer.Add(txtTwo, 0, wx.ALL, 5) self.SetSizer(sizer) ######################################################################## class FlatNotebookDemo(fnb.FlatNotebook): """ Flatnotebook class """ #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY) pageOne = TabPanel(self) pageTwo = TabPanel(self) pageThree = TabPanel(self) self.AddPage(pageOne, "PageOne") self.AddPage(pageTwo, "PageTwo") self.AddPage(pageThree, "PageThree") ######################################################################## class DemoFrame(wx.Frame): """ Frame that holds all other widgets """ #---------------------------------------------------------------------- def __init__(self): """Constructor""" wx.Frame.__init__(self, None, wx.ID_ANY, "FlatNotebook Tutorial with Style", size=(600,400) ) self.styleDict = {"Default":self.OnDefaultStyle, "VC71":self.OnVC71Style, "VC8":self.OnVC8Style, "Fancy":self.OnFancyStyle, "Firefox 2":self.OnFF2Style} choices = self.styleDict.keys() panel = wx.Panel(self) self.notebook = FlatNotebookDemo(panel) self.styleCbo = wx.ComboBox(panel, wx.ID_ANY, "Default", wx.DefaultPosition, wx.DefaultSize, choices=choices, style=wx.CB_DROPDOWN) styleBtn = wx.Button(panel, wx.ID_ANY, "Change Style") styleBtn.Bind(wx.EVT_BUTTON, self.onStyle) # create some sizers sizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) # add the widgets to the sizers sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5) hSizer.Add(self.styleCbo, 0, wx.ALL|wx.CENTER, 5) hSizer.Add(styleBtn, 0, wx.ALL, 5) sizer.Add(wx.StaticLine(panel), 0, wx.ALL|wx.EXPAND, 5) sizer.Add(hSizer, 0, wx.ALL, 5) panel.SetSizer(sizer) self.Layout() self.Show() #---------------------------------------------------------------------- def onStyle(self, event): """ Changes the style of the tabs """ print "in onStyle" style = self.styleCbo.GetValue() print style self.styleDict[style]() # The following methods were taken from the wxPython # demo for the FlatNotebook def OnFF2Style(self): style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror style |= fnb.FNB_FF2 self.notebook.SetWindowStyleFlag(style) def OnVC71Style(self): style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror style |= fnb.FNB_VC71 self.notebook.SetWindowStyleFlag(style) def OnVC8Style(self): style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror # set new style style |= fnb.FNB_VC8 self.notebook.SetWindowStyleFlag(style) def OnDefaultStyle(self): style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror self.notebook.SetWindowStyleFlag(style) def OnFancyStyle(self): style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror style |= fnb.FNB_FANCY_TABS self.notebook.SetWindowStyleFlag(style) #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.PySimpleApp() frame = DemoFrame() app.MainLoop() }}} That’s a lot of code for a “simple” example, but I think it will help us understand how to apply tab styles to our widget. I borrowed most of the methods from the wxPython demo, in case you didn’t notice. The primary talking point in this code is the contents of those methods, which are mostly the same. Here’s the main snippet to take away from this section: {{{ #!python style = self.notebook.GetWindowStyleFlag() # remove old tabs style mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2) style &= mirror style |= fnb.FNB_FF2 self.notebook.SetWindowStyleFlag(style) }}} First, we need to get the current style of the !FlatNotebook. Then we use some fancy magic in the “mirror” line that creates a set of styles that we want to remove. The line, “style &= mirror” actually does the removing and then we add the style we wanted with “style |= fnb.FNB_FF2?. Finally, we use !SetWindowStyleFlag() to actually apply the style to the widget. You may be wondering what’s up with all those goofy symbols (i.e. |, ~, &). Well, those are known as bitwise operators. I don’t use them much myself, so I recommend reading the [[http://docs.python.org/reference/expressions.html|Python documentation]] for full details as I don’t fully understand them myself. === Adding and Removing Pages from FlatNotebook === {{http://www.blog.pythonlibrary.org/wp-content/uploads/2009/12/flatnotebookPageDemo.png}} For my next demo, I created a way to add and delete pages from the !FlatNotebook. Let’s see how: '''Listing 3''' {{{ #!python import random import sys import wx import wx.lib.agw.flatnotebook as fnb import wx.lib.mixins.listctrl as listmix musicdata = { 1 : ("Bad English", "The Price Of Love", "Rock"), 2 : ("DNA featuring Suzanne Vega", "Tom's Diner", "Rock"), 3 : ("George Michael", "Praying For Time", "Rock"), 4 : ("Gloria Estefan", "Here We Are", "Rock"), 5 : ("Linda Ronstadt", "Don't Know Much", "Rock"), 6 : ("Michael Bolton", "How Am I Supposed To Live Without You", "Blues"), 7 : ("Paul Young", "Oh Girl", "Rock"), 8 : ("Paula Abdul", "Opposites Attract", "Rock"), 9 : ("Richard Marx", "Should've Known Better", "Rock"), 10: ("Rod Stewart", "Forever Young", "Rock"), 11: ("Roxette", "Dangerous", "Rock"), 12: ("Sheena Easton", "The Lover In Me", "Rock"), 13: ("Sinead O'Connor", "Nothing Compares 2 U", "Rock"), 14: ("Stevie B.", "Because I Love You", "Rock"), 15: ("Taylor Dayne", "Love Will Lead You Back", "Rock"), 16: ("The Bangles", "Eternal Flame", "Rock"), 17: ("Wilson Phillips", "Release Me", "Rock"), 18: ("Billy Joel", "Blonde Over Blue", "Rock"), 19: ("Billy Joel", "Famous Last Words", "Rock"), 20: ("Billy Joel", "Lullabye (Goodnight, My Angel)", "Rock"), 21: ("Billy Joel", "The River Of Dreams", "Rock"), 22: ("Billy Joel", "Two Thousand Years", "Rock") } ######################################################################## class TabPanelOne(wx.Panel): """ This will be the first notebook tab """ #---------------------------------------------------------------------- def __init__(self, parent): """""" wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) sizer = wx.BoxSizer(wx.VERTICAL) txtOne = wx.TextCtrl(self, wx.ID_ANY, "") txtTwo = wx.TextCtrl(self, wx.ID_ANY, "") sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(txtOne, 0, wx.ALL, 5) sizer.Add(txtTwo, 0, wx.ALL, 5) self.SetSizer(sizer) ######################################################################## class TestListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0): wx.ListCtrl.__init__(self, parent, ID, pos, size, style) listmix.ListCtrlAutoWidthMixin.__init__(self) ######################################################################## class TabPanelTwo(wx.Panel, listmix.ColumnSorterMixin): """ This will be the second notebook tab """ #---------------------------------------------------------------------- def __init__(self, parent): """""" wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) self.createAndLayout() def createAndLayout(self): sizer = wx.BoxSizer(wx.VERTICAL) self.list = TestListCtrl(self, wx.ID_ANY, style=wx.LC_REPORT | wx.BORDER_NONE | wx.LC_EDIT_LABELS | wx.LC_SORT_ASCENDING) sizer.Add(self.list, 1, wx.EXPAND) self.populateList() # Now that the list exists we can init the other base class, # see wx/lib/mixins/listctrl.py self.itemDataMap = musicdata listmix.ColumnSorterMixin.__init__(self, 3) self.SetSizer(sizer) self.SetAutoLayout(True) def populateList(self): self.list.InsertColumn(0, "Artist") self.list.InsertColumn(1, "Title", wx.LIST_FORMAT_RIGHT) self.list.InsertColumn(2, "Genre") items = musicdata.items() for key, data in items: index = self.list.InsertStringItem(sys.maxint, data[0]) self.list.SetStringItem(index, 1, data[1]) self.list.SetStringItem(index, 2, data[2]) self.list.SetItemData(index, key) self.list.SetColumnWidth(0, wx.LIST_AUTOSIZE) self.list.SetColumnWidth(1, wx.LIST_AUTOSIZE) self.list.SetColumnWidth(2, 100) # show how to select an item self.list.SetItemState(5, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED) # show how to change the colour of a couple items item = self.list.GetItem(1) item.SetTextColour(wx.BLUE) self.list.SetItem(item) item = self.list.GetItem(4) item.SetTextColour(wx.RED) self.list.SetItem(item) self.currentItem = 0 # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py def GetListCtrl(self): return self.list ######################################################################## class FlatNotebookDemo(fnb.FlatNotebook): """ Flatnotebook class """ #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY) ######################################################################## class DemoFrame(wx.Frame): """ Frame that holds all other widgets """ #---------------------------------------------------------------------- def __init__(self, title="FlatNotebook Add/Remove Page Tutorial"): """Constructor""" wx.Frame.__init__(self, None, wx.ID_ANY, title=title, size=(600,400) ) self._newPageCounter = 0 panel = wx.Panel(self) self.createRightClickMenu() # create some widgets self.notebook = FlatNotebookDemo(panel) addPageBtn = wx.Button(panel, label="Add Page") addPageBtn.Bind(wx.EVT_BUTTON, self.onAddPage) removePageBtn = wx.Button(panel, label="Remove Page") removePageBtn.Bind(wx.EVT_BUTTON, self.onDeletePage) self.notebook.SetRightClickMenu(self._rmenu) # create some sizers sizer = wx.BoxSizer(wx.VERTICAL) btnSizer = wx.BoxSizer(wx.HORIZONTAL) # layout the widgets sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5) btnSizer.Add(addPageBtn, 0, wx.ALL, 5) btnSizer.Add(removePageBtn, 0, wx.ALL, 5) sizer.Add(btnSizer) panel.SetSizer(sizer) self.Layout() self.Show() #---------------------------------------------------------------------- def createRightClickMenu(self): """ Based on method from flatnotebook demo """ self._rmenu = wx.Menu() item = wx.MenuItem(self._rmenu, wx.ID_ANY, "Close Tab\tCtrl+F4", "Close Tab") self.Bind(wx.EVT_MENU, self.onDeletePage, item) self._rmenu.AppendItem(item) #---------------------------------------------------------------------- def onAddPage(self, event): """ This method is based on the flatnotebook demo It adds a new page to the notebook """ caption = "New Page Added #" + str(self._newPageCounter) self.Freeze() self.notebook.AddPage(self.createPage(caption), caption, True) self.Thaw() self._newPageCounter = self._newPageCounter + 1 #---------------------------------------------------------------------- def createPage(self, caption): """ Creates a notebook page from one of three panels at random and returns the new page """ panel_list = [TabPanelOne, TabPanelTwo] tab = random.choice(panel_list) page = tab(self.notebook) return page #---------------------------------------------------------------------- def onDeletePage(self, event): """ This method is based on the flatnotebook demo It removes a page from the notebook """ self.notebook.DeletePage(self.notebook.GetSelection()) #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.PySimpleApp() frame = DemoFrame() app.MainLoop() }}} The code above allows the user to add as many pages as they want by clicking the Add Page button. The Remove Page button will remove whatever page is currently selected. When adding a page, but button handler freezes the frame and calls the notebook’s !AddPage method. This calls the “createPage” method which randomly grabs one of my pre-defined panels, instantiates it and returns it to the !AddPage method. On returning to the “onAddPage” method, the frame is thawed and the page counter is incremented. The Remove Page button calls the notebook’s !GetSelection() method to get the currently selected tab and then calls the notebook’s !DeletePage() method to remove it from the notebook. Another fun functionality that I enabled was the tab right-click menu, which gives us another way to close a tab, although you could use to do other actions as well. All you need to do to enable it is to call the notebook’s !SetRightClickMenu() method and pass in a wx.Menu object. There are tons of other features for you to explore as well. Be sure to check out the !FlatNotebook demo in the official wxPython demo where you can learn to close tabs with the middle mouse button or via double-clicks, turn on gradient colors for the tab background, disable tabs, enable smart tabbing (which is kind of like the alt+tab menu in Windows), create drag-and-drop tabs between notebooks and much, much more! '''All code was tested on the following:''' * Windows XP, wxPython 2.8.10.1 (unicode), Python 2.5 * Windows Vista, wxPython 2.8.10.1 (unicode), Python 2.5 === For Further Information === * [[http://xoomer.virgilio.it/infinity77/AGW_Docs/index.html|AGW Official Documentation]] * [[http://wxpython.org/download.php|Official wxPython Demo]] ''Note: The tutorial on this page is a modified version of the tutorial on my [[http://www.blog.pythonlibrary.org/2009/12/09/the-%E2%80%9Cbook%E2%80%9D-controls-of-wxpython-part-2-of-2/|blog]]''