agw.FlatNotebook
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 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
1 import wx
2 import wx.lib.agw.flatnotebook as fnb
3
4 ########################################################################
5 class TabPanel(wx.Panel):
6 """
7 This will be the first notebook tab
8 """
9 #----------------------------------------------------------------------
10 def __init__(self, parent):
11 """"""
12
13 wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
14
15 sizer = wx.BoxSizer(wx.VERTICAL)
16 txtOne = wx.TextCtrl(self, wx.ID_ANY, "")
17 txtTwo = wx.TextCtrl(self, wx.ID_ANY, "")
18
19 sizer = wx.BoxSizer(wx.VERTICAL)
20 sizer.Add(txtOne, 0, wx.ALL, 5)
21 sizer.Add(txtTwo, 0, wx.ALL, 5)
22
23 self.SetSizer(sizer)
24
25 ########################################################################
26 class FlatNotebookDemo(fnb.FlatNotebook):
27 """
28 Flatnotebook class
29 """
30
31 #----------------------------------------------------------------------
32 def __init__(self, parent):
33 """Constructor"""
34 fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
35
36 pageOne = TabPanel(self)
37 pageTwo = TabPanel(self)
38 pageThree = TabPanel(self)
39
40 self.AddPage(pageOne, "PageOne")
41 self.AddPage(pageTwo, "PageTwo")
42 self.AddPage(pageThree, "PageThree")
43
44 ########################################################################
45 class DemoFrame(wx.Frame):
46 """
47 Frame that holds all other widgets
48 """
49
50 #----------------------------------------------------------------------
51 def __init__(self):
52 """Constructor"""
53 wx.Frame.__init__(self, None, wx.ID_ANY,
54 "FlatNotebook Tutorial",
55 size=(600,400)
56 )
57 panel = wx.Panel(self)
58
59 notebook = FlatNotebookDemo(panel)
60 sizer = wx.BoxSizer(wx.VERTICAL)
61 sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
62 panel.SetSizer(sizer)
63 self.Layout()
64
65 self.Show()
66
67 #----------------------------------------------------------------------
68 if __name__ == "__main__":
69 app = wx.PySimpleApp()
70 frame = DemoFrame()
71 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:
Listing 2
1 import wx
2 import wx.lib.agw.flatnotebook as fnb
3
4 ########################################################################
5 class TabPanel(wx.Panel):
6 """
7 This will be the first notebook tab
8 """
9 #----------------------------------------------------------------------
10 def __init__(self, parent):
11 """"""
12
13 wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
14
15 sizer = wx.BoxSizer(wx.VERTICAL)
16 txtOne = wx.TextCtrl(self, wx.ID_ANY, "")
17 txtTwo = wx.TextCtrl(self, wx.ID_ANY, "")
18
19 sizer = wx.BoxSizer(wx.VERTICAL)
20 sizer.Add(txtOne, 0, wx.ALL, 5)
21 sizer.Add(txtTwo, 0, wx.ALL, 5)
22
23 self.SetSizer(sizer)
24
25 ########################################################################
26 class FlatNotebookDemo(fnb.FlatNotebook):
27 """
28 Flatnotebook class
29 """
30
31 #----------------------------------------------------------------------
32 def __init__(self, parent):
33 """Constructor"""
34 fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
35
36 pageOne = TabPanel(self)
37 pageTwo = TabPanel(self)
38 pageThree = TabPanel(self)
39
40 self.AddPage(pageOne, "PageOne")
41 self.AddPage(pageTwo, "PageTwo")
42 self.AddPage(pageThree, "PageThree")
43
44
45 ########################################################################
46 class DemoFrame(wx.Frame):
47 """
48 Frame that holds all other widgets
49 """
50
51 #----------------------------------------------------------------------
52 def __init__(self):
53 """Constructor"""
54 wx.Frame.__init__(self, None, wx.ID_ANY,
55 "FlatNotebook Tutorial with Style",
56 size=(600,400)
57 )
58 self.styleDict = {"Default":self.OnDefaultStyle,
59 "VC71":self.OnVC71Style,
60 "VC8":self.OnVC8Style,
61 "Fancy":self.OnFancyStyle,
62 "Firefox 2":self.OnFF2Style}
63 choices = self.styleDict.keys()
64
65 panel = wx.Panel(self)
66 self.notebook = FlatNotebookDemo(panel)
67 self.styleCbo = wx.ComboBox(panel, wx.ID_ANY, "Default",
68 wx.DefaultPosition, wx.DefaultSize,
69 choices=choices, style=wx.CB_DROPDOWN)
70 styleBtn = wx.Button(panel, wx.ID_ANY, "Change Style")
71 styleBtn.Bind(wx.EVT_BUTTON, self.onStyle)
72
73 # create some sizers
74 sizer = wx.BoxSizer(wx.VERTICAL)
75 hSizer = wx.BoxSizer(wx.HORIZONTAL)
76
77 # add the widgets to the sizers
78 sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
79 hSizer.Add(self.styleCbo, 0, wx.ALL|wx.CENTER, 5)
80 hSizer.Add(styleBtn, 0, wx.ALL, 5)
81 sizer.Add(wx.StaticLine(panel), 0, wx.ALL|wx.EXPAND, 5)
82 sizer.Add(hSizer, 0, wx.ALL, 5)
83
84 panel.SetSizer(sizer)
85 self.Layout()
86
87 self.Show()
88
89 #----------------------------------------------------------------------
90 def onStyle(self, event):
91 """
92 Changes the style of the tabs
93 """
94 print "in onStyle"
95 style = self.styleCbo.GetValue()
96 print style
97 self.styleDict[style]()
98
99 # The following methods were taken from the wxPython
100 # demo for the FlatNotebook
101 def OnFF2Style(self):
102
103 style = self.notebook.GetWindowStyleFlag()
104
105 # remove old tabs style
106 mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
107 style &= mirror
108
109 style |= fnb.FNB_FF2
110
111 self.notebook.SetWindowStyleFlag(style)
112
113
114 def OnVC71Style(self):
115
116 style = self.notebook.GetWindowStyleFlag()
117
118 # remove old tabs style
119 mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
120 style &= mirror
121
122 style |= fnb.FNB_VC71
123
124 self.notebook.SetWindowStyleFlag(style)
125
126
127 def OnVC8Style(self):
128
129 style = self.notebook.GetWindowStyleFlag()
130
131 # remove old tabs style
132 mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
133 style &= mirror
134
135 # set new style
136 style |= fnb.FNB_VC8
137
138 self.notebook.SetWindowStyleFlag(style)
139
140
141 def OnDefaultStyle(self):
142
143 style = self.notebook.GetWindowStyleFlag()
144
145 # remove old tabs style
146 mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
147 style &= mirror
148
149 self.notebook.SetWindowStyleFlag(style)
150
151
152 def OnFancyStyle(self):
153
154 style = self.notebook.GetWindowStyleFlag()
155
156 # remove old tabs style
157 mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
158 style &= mirror
159
160 style |= fnb.FNB_FANCY_TABS
161 self.notebook.SetWindowStyleFlag(style)
162
163 #----------------------------------------------------------------------
164 if __name__ == "__main__":
165 app = wx.PySimpleApp()
166 frame = DemoFrame()
167 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:
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 Python documentation for full details as I don’t fully understand them myself.
Adding and Removing Pages from FlatNotebook
For my next demo, I created a way to add and delete pages from the FlatNotebook. Let’s see how:
Listing 3
1 import random
2 import sys
3 import wx
4 import wx.lib.agw.flatnotebook as fnb
5 import wx.lib.mixins.listctrl as listmix
6
7 musicdata = {
8 1 : ("Bad English", "The Price Of Love", "Rock"),
9 2 : ("DNA featuring Suzanne Vega", "Tom's Diner", "Rock"),
10 3 : ("George Michael", "Praying For Time", "Rock"),
11 4 : ("Gloria Estefan", "Here We Are", "Rock"),
12 5 : ("Linda Ronstadt", "Don't Know Much", "Rock"),
13 6 : ("Michael Bolton", "How Am I Supposed To Live Without You", "Blues"),
14 7 : ("Paul Young", "Oh Girl", "Rock"),
15 8 : ("Paula Abdul", "Opposites Attract", "Rock"),
16 9 : ("Richard Marx", "Should've Known Better", "Rock"),
17 10: ("Rod Stewart", "Forever Young", "Rock"),
18 11: ("Roxette", "Dangerous", "Rock"),
19 12: ("Sheena Easton", "The Lover In Me", "Rock"),
20 13: ("Sinead O'Connor", "Nothing Compares 2 U", "Rock"),
21 14: ("Stevie B.", "Because I Love You", "Rock"),
22 15: ("Taylor Dayne", "Love Will Lead You Back", "Rock"),
23 16: ("The Bangles", "Eternal Flame", "Rock"),
24 17: ("Wilson Phillips", "Release Me", "Rock"),
25 18: ("Billy Joel", "Blonde Over Blue", "Rock"),
26 19: ("Billy Joel", "Famous Last Words", "Rock"),
27 20: ("Billy Joel", "Lullabye (Goodnight, My Angel)", "Rock"),
28 21: ("Billy Joel", "The River Of Dreams", "Rock"),
29 22: ("Billy Joel", "Two Thousand Years", "Rock")
30 }
31
32 ########################################################################
33 class TabPanelOne(wx.Panel):
34 """
35 This will be the first notebook tab
36 """
37 #----------------------------------------------------------------------
38 def __init__(self, parent):
39 """"""
40
41 wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
42
43 sizer = wx.BoxSizer(wx.VERTICAL)
44 txtOne = wx.TextCtrl(self, wx.ID_ANY, "")
45 txtTwo = wx.TextCtrl(self, wx.ID_ANY, "")
46
47 sizer = wx.BoxSizer(wx.VERTICAL)
48 sizer.Add(txtOne, 0, wx.ALL, 5)
49 sizer.Add(txtTwo, 0, wx.ALL, 5)
50
51 self.SetSizer(sizer)
52
53 ########################################################################
54 class TestListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
55 def __init__(self, parent, ID, pos=wx.DefaultPosition,
56 size=wx.DefaultSize, style=0):
57 wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
58 listmix.ListCtrlAutoWidthMixin.__init__(self)
59
60 ########################################################################
61 class TabPanelTwo(wx.Panel, listmix.ColumnSorterMixin):
62 """
63 This will be the second notebook tab
64 """
65 #----------------------------------------------------------------------
66 def __init__(self, parent):
67 """"""
68 wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
69 self.createAndLayout()
70
71 def createAndLayout(self):
72 sizer = wx.BoxSizer(wx.VERTICAL)
73 self.list = TestListCtrl(self, wx.ID_ANY, style=wx.LC_REPORT
74 | wx.BORDER_NONE
75 | wx.LC_EDIT_LABELS
76 | wx.LC_SORT_ASCENDING)
77 sizer.Add(self.list, 1, wx.EXPAND)
78 self.populateList()
79 # Now that the list exists we can init the other base class,
80 # see wx/lib/mixins/listctrl.py
81 self.itemDataMap = musicdata
82 listmix.ColumnSorterMixin.__init__(self, 3)
83 self.SetSizer(sizer)
84 self.SetAutoLayout(True)
85
86 def populateList(self):
87 self.list.InsertColumn(0, "Artist")
88 self.list.InsertColumn(1, "Title", wx.LIST_FORMAT_RIGHT)
89 self.list.InsertColumn(2, "Genre")
90 items = musicdata.items()
91
92 for key, data in items:
93 index = self.list.InsertStringItem(sys.maxint, data[0])
94 self.list.SetStringItem(index, 1, data[1])
95 self.list.SetStringItem(index, 2, data[2])
96 self.list.SetItemData(index, key)
97
98 self.list.SetColumnWidth(0, wx.LIST_AUTOSIZE)
99 self.list.SetColumnWidth(1, wx.LIST_AUTOSIZE)
100 self.list.SetColumnWidth(2, 100)
101
102 # show how to select an item
103 self.list.SetItemState(5, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)
104
105 # show how to change the colour of a couple items
106 item = self.list.GetItem(1)
107 item.SetTextColour(wx.BLUE)
108 self.list.SetItem(item)
109 item = self.list.GetItem(4)
110 item.SetTextColour(wx.RED)
111 self.list.SetItem(item)
112
113 self.currentItem = 0
114
115 # Used by the ColumnSorterMixin, see wx/lib/mixins/listctrl.py
116 def GetListCtrl(self):
117 return self.list
118
119 ########################################################################
120 class FlatNotebookDemo(fnb.FlatNotebook):
121 """
122 Flatnotebook class
123 """
124 #----------------------------------------------------------------------
125 def __init__(self, parent):
126 """Constructor"""
127 fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
128
129 ########################################################################
130 class DemoFrame(wx.Frame):
131 """
132 Frame that holds all other widgets
133 """
134
135 #----------------------------------------------------------------------
136 def __init__(self, title="FlatNotebook Add/Remove Page Tutorial"):
137 """Constructor"""
138 wx.Frame.__init__(self, None, wx.ID_ANY,
139 title=title,
140 size=(600,400)
141 )
142 self._newPageCounter = 0
143 panel = wx.Panel(self)
144 self.createRightClickMenu()
145
146 # create some widgets
147 self.notebook = FlatNotebookDemo(panel)
148 addPageBtn = wx.Button(panel, label="Add Page")
149 addPageBtn.Bind(wx.EVT_BUTTON, self.onAddPage)
150 removePageBtn = wx.Button(panel, label="Remove Page")
151 removePageBtn.Bind(wx.EVT_BUTTON, self.onDeletePage)
152 self.notebook.SetRightClickMenu(self._rmenu)
153
154 # create some sizers
155 sizer = wx.BoxSizer(wx.VERTICAL)
156 btnSizer = wx.BoxSizer(wx.HORIZONTAL)
157
158 # layout the widgets
159 sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
160 btnSizer.Add(addPageBtn, 0, wx.ALL, 5)
161 btnSizer.Add(removePageBtn, 0, wx.ALL, 5)
162 sizer.Add(btnSizer)
163 panel.SetSizer(sizer)
164 self.Layout()
165
166 self.Show()
167
168 #----------------------------------------------------------------------
169 def createRightClickMenu(self):
170 """
171 Based on method from flatnotebook demo
172 """
173 self._rmenu = wx.Menu()
174 item = wx.MenuItem(self._rmenu, wx.ID_ANY,
175 "Close Tab\tCtrl+F4",
176 "Close Tab")
177 self.Bind(wx.EVT_MENU, self.onDeletePage, item)
178 self._rmenu.AppendItem(item)
179
180 #----------------------------------------------------------------------
181 def onAddPage(self, event):
182 """
183 This method is based on the flatnotebook demo
184
185 It adds a new page to the notebook
186 """
187 caption = "New Page Added #" + str(self._newPageCounter)
188 self.Freeze()
189
190 self.notebook.AddPage(self.createPage(caption), caption, True)
191 self.Thaw()
192 self._newPageCounter = self._newPageCounter + 1
193
194 #----------------------------------------------------------------------
195 def createPage(self, caption):
196 """
197 Creates a notebook page from one of three
198 panels at random and returns the new page
199 """
200 panel_list = [TabPanelOne, TabPanelTwo]
201 tab = random.choice(panel_list)
202 page = tab(self.notebook)
203 return page
204
205 #----------------------------------------------------------------------
206 def onDeletePage(self, event):
207 """
208 This method is based on the flatnotebook demo
209
210 It removes a page from the notebook
211 """
212 self.notebook.DeletePage(self.notebook.GetSelection())
213
214 #----------------------------------------------------------------------
215 if __name__ == "__main__":
216 app = wx.PySimpleApp()
217 frame = DemoFrame()
218 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
Note: The tutorial on this page is a modified version of the tutorial on my blog