1
"""An implementation of tabbed pages using only standard Tkinter.
3
Originally developed for use in IDLE. Based on tabpage.py.
6
TabbedPageSet -- A Tkinter implementation of a tabbed-page widget.
7
TabSet -- A widget containing tabs (buttons) in one or more rows.
12
class InvalidNameError(Exception): pass
13
class AlreadyExistsError(Exception): pass
17
"""A widget containing tabs (buttons) in one or more rows.
19
Only one tab may be selected at a time.
22
def __init__(self, page_set, select_command,
23
tabs=None, n_rows=1, max_tabs_per_row=5,
24
expand_tabs=False, **kw):
25
"""Constructor arguments:
27
select_command -- A callable which will be called when a tab is
28
selected. It is called with the name of the selected tab as an
31
tabs -- A list of strings, the names of the tabs. Should be specified in
32
the desired tab order. The first tab will be the default and first
33
active tab. If tabs is None or empty, the TabSet will be initialized
36
n_rows -- Number of rows of tabs to be shown. If n_rows <= 0 or is
37
None, then the number of rows will be decided by TabSet. See
38
_arrange_tabs() for details.
40
max_tabs_per_row -- Used for deciding how many rows of tabs are needed,
41
when the number of rows is not constant. See _arrange_tabs() for
45
Frame.__init__(self, page_set, **kw)
46
self.select_command = select_command
48
self.max_tabs_per_row = max_tabs_per_row
49
self.expand_tabs = expand_tabs
50
self.page_set = page_set
55
self._tab_names = list(tabs)
58
self._selected_tab = None
61
self.padding_frame = Frame(self, height=2,
62
borderwidth=0, relief=FLAT,
63
background=self.cget('background'))
64
self.padding_frame.pack(side=TOP, fill=X, expand=False)
68
def add_tab(self, tab_name):
69
"""Add a new tab with the name given in tab_name."""
71
raise InvalidNameError("Invalid Tab name: '%s'" % tab_name)
72
if tab_name in self._tab_names:
73
raise AlreadyExistsError("Tab named '%s' already exists" %tab_name)
75
self._tab_names.append(tab_name)
78
def remove_tab(self, tab_name):
79
"""Remove the tab named <tab_name>"""
80
if not tab_name in self._tab_names:
81
raise KeyError("No such Tab: '%s" % page_name)
83
self._tab_names.remove(tab_name)
86
def set_selected_tab(self, tab_name):
87
"""Show the tab named <tab_name> as the selected one"""
88
if tab_name == self._selected_tab:
90
if tab_name is not None and tab_name not in self._tabs:
91
raise KeyError("No such Tab: '%s" % page_name)
93
# deselect the current selected tab
94
if self._selected_tab is not None:
95
self._tabs[self._selected_tab].set_normal()
96
self._selected_tab = None
98
if tab_name is not None:
99
# activate the tab named tab_name
100
self._selected_tab = tab_name
101
tab = self._tabs[tab_name]
103
# move the tab row with the selected tab to the bottom
104
tab_row = self._tab2row[tab]
105
tab_row.pack_forget()
106
tab_row.pack(side=TOP, fill=X, expand=0)
108
def _add_tab_row(self, tab_names, expand_tabs):
112
tab_row = Frame(self)
113
tab_row.pack(side=TOP, fill=X, expand=0)
114
self._tab_rows.append(tab_row)
116
for tab_name in tab_names:
117
tab = TabSet.TabButton(tab_name, self.select_command,
120
tab.pack(side=LEFT, fill=X, expand=True)
123
self._tabs[tab_name] = tab
124
self._tab2row[tab] = tab_row
126
# tab is the last one created in the above loop
127
tab.is_last_in_row = True
129
def _reset_tab_rows(self):
130
while self._tab_rows:
131
tab_row = self._tab_rows.pop()
135
def _arrange_tabs(self):
137
Arrange the tabs in rows, in the order in which they were added.
139
If n_rows >= 1, this will be the number of rows used. Otherwise the
140
number of rows will be calculated according to the number of tabs and
141
max_tabs_per_row. In this case, the number of rows may change when
142
adding/removing tabs.
145
# remove all tabs and rows
146
for tab_name in self._tabs.keys():
147
self._tabs.pop(tab_name).destroy()
148
self._reset_tab_rows()
150
if not self._tab_names:
153
if self.n_rows is not None and self.n_rows > 0:
156
# calculate the required number of rows
157
n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1
159
# not expanding the tabs with more than one row is very ugly
160
expand_tabs = self.expand_tabs or n_rows > 1
161
i = 0 # index in self._tab_names
162
for row_index in xrange(n_rows):
163
# calculate required number of tabs in this row
164
n_tabs = (len(self._tab_names) - i - 1) // (n_rows - row_index) + 1
165
tab_names = self._tab_names[i:i + n_tabs]
167
self._add_tab_row(tab_names, expand_tabs)
169
# re-select selected tab so it is properly displayed
170
selected = self._selected_tab
171
self.set_selected_tab(None)
172
if selected in self._tab_names:
173
self.set_selected_tab(selected)
175
class TabButton(Frame):
176
"""A simple tab-like widget."""
180
def __init__(self, name, select_command, tab_row, tab_set):
181
"""Constructor arguments:
183
name -- The tab's name, which will appear in its button.
185
select_command -- The command to be called upon selection of the
186
tab. It is called with the tab's name as an argument.
189
Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED)
192
self.select_command = select_command
193
self.tab_set = tab_set
194
self.is_last_in_row = False
196
self.button = Radiobutton(
197
self, text=name, command=self._select_event,
198
padx=5, pady=1, takefocus=FALSE, indicatoron=FALSE,
199
highlightthickness=0, selectcolor='', borderwidth=0)
200
self.button.pack(side=LEFT, fill=X, expand=True)
205
def _select_event(self, *args):
206
"""Event handler for tab selection.
208
With TabbedPageSet, this calls TabbedPageSet.change_page, so that
209
selecting a tab changes the page.
211
Note that this does -not- call set_selected -- it will be called by
212
TabSet.set_selected_tab, which should be called when whatever the
213
tabs are related to changes.
216
self.select_command(self.name)
219
def set_selected(self):
220
"""Assume selected look"""
221
self._place_masks(selected=True)
223
def set_normal(self):
224
"""Assume normal look"""
225
self._place_masks(selected=False)
227
def _init_masks(self):
228
page_set = self.tab_set.page_set
229
background = page_set.pages_frame.cget('background')
230
# mask replaces the middle of the border with the background color
231
self.mask = Frame(page_set, borderwidth=0, relief=FLAT,
232
background=background)
233
# mskl replaces the bottom-left corner of the border with a normal
235
self.mskl = Frame(page_set, borderwidth=0, relief=FLAT,
236
background=background)
237
self.mskl.ml = Frame(self.mskl, borderwidth=self.bw,
239
self.mskl.ml.place(x=0, y=-self.bw,
240
width=2*self.bw, height=self.bw*4)
241
# mskr replaces the bottom-right corner of the border with a normal
243
self.mskr = Frame(page_set, borderwidth=0, relief=FLAT,
244
background=background)
245
self.mskr.mr = Frame(self.mskr, borderwidth=self.bw,
248
def _place_masks(self, selected=False):
253
self.mask.place(in_=self,
256
relwidth=1.0, width=0,
257
relheight=0.0, height=height)
259
self.mskl.place(in_=self,
260
relx=0.0, x=-self.bw,
262
relwidth=0.0, width=self.bw,
263
relheight=0.0, height=height)
265
page_set = self.tab_set.page_set
266
if selected and ((not self.is_last_in_row) or
267
(self.winfo_rootx() + self.winfo_width() <
268
page_set.winfo_rootx() + page_set.winfo_width())
270
# for a selected tab, if its rightmost edge isn't on the
271
# rightmost edge of the page set, the right mask should be one
272
# borderwidth shorter (vertically)
275
self.mskr.place(in_=self,
278
relwidth=0.0, width=self.bw,
279
relheight=0.0, height=height)
281
self.mskr.mr.place(x=-self.bw, y=-self.bw,
282
width=2*self.bw, height=height + self.bw*2)
284
# finally, lower the tab set so that all of the frames we just
288
class TabbedPageSet(Frame):
289
"""A Tkinter tabbed-pane widget.
291
Constains set of 'pages' (or 'panes') with tabs above for selecting which
292
page is displayed. Only one page will be displayed at a time.
294
Pages may be accessed through the 'pages' attribute, which is a dictionary
295
of pages, using the name given as the key. A page is an instance of a
296
subclass of Tk's Frame widget.
298
The page widgets will be created (and destroyed when required) by the
299
TabbedPageSet. Do not call the page's pack/place/grid/destroy methods.
301
Pages may be added or removed at any time using the add_page() and
302
remove_page() methods.
306
"""Abstract base class for TabbedPageSet's pages.
308
Subclasses must override the _show() and _hide() methods.
313
def __init__(self, page_set):
314
self.frame = Frame(page_set, borderwidth=2, relief=RAISED)
317
raise NotImplementedError
320
raise NotImplementedError
322
class PageRemove(Page):
323
"""Page class using the grid placement manager's "remove" mechanism."""
327
self.frame.grid(row=0, column=0, sticky=NSEW)
330
self.frame.grid_remove()
332
class PageLift(Page):
333
"""Page class using the grid placement manager's "lift" mechanism."""
336
def __init__(self, page_set):
337
super(TabbedPageSet.PageLift, self).__init__(page_set)
338
self.frame.grid(row=0, column=0, sticky=NSEW)
347
class PagePackForget(Page):
348
"""Page class using the pack placement manager's "forget" mechanism."""
350
self.frame.pack(fill=BOTH, expand=True)
353
self.frame.pack_forget()
355
def __init__(self, parent, page_names=None, page_class=PageLift,
356
n_rows=1, max_tabs_per_row=5, expand_tabs=False,
358
"""Constructor arguments:
360
page_names -- A list of strings, each will be the dictionary key to a
361
page's widget, and the name displayed on the page's tab. Should be
362
specified in the desired page order. The first page will be the default
363
and first active page. If page_names is None or empty, the
364
TabbedPageSet will be initialized empty.
366
n_rows, max_tabs_per_row -- Parameters for the TabSet which will
367
manage the tabs. See TabSet's docs for details.
369
page_class -- Pages can be shown/hidden using three mechanisms:
371
* PageLift - All pages will be rendered one on top of the other. When
372
a page is selected, it will be brought to the top, thus hiding all
373
other pages. Using this method, the TabbedPageSet will not be resized
374
when pages are switched. (It may still be resized when pages are
377
* PageRemove - When a page is selected, the currently showing page is
378
hidden, and the new page shown in its place. Using this method, the
379
TabbedPageSet may resize when pages are changed.
381
* PagePackForget - This mechanism uses the pack placement manager.
382
When a page is shown it is packed, and when it is hidden it is
383
unpacked (i.e. pack_forget). This mechanism may also cause the
384
TabbedPageSet to resize when the page is changed.
387
Frame.__init__(self, parent, **kw)
389
self.page_class = page_class
391
self._pages_order = []
392
self._current_page = None
393
self._default_page = None
395
self.columnconfigure(0, weight=1)
396
self.rowconfigure(1, weight=1)
398
self.pages_frame = Frame(self)
399
self.pages_frame.grid(row=1, column=0, sticky=NSEW)
400
if self.page_class.uses_grid:
401
self.pages_frame.columnconfigure(0, weight=1)
402
self.pages_frame.rowconfigure(0, weight=1)
404
# the order of the following commands is important
405
self._tab_set = TabSet(self, self.change_page, n_rows=n_rows,
406
max_tabs_per_row=max_tabs_per_row,
407
expand_tabs=expand_tabs)
409
for name in page_names:
411
self._tab_set.grid(row=0, column=0, sticky=NSEW)
413
self.change_page(self._default_page)
415
def add_page(self, page_name):
416
"""Add a new page with the name given in page_name."""
418
raise InvalidNameError("Invalid TabPage name: '%s'" % page_name)
419
if page_name in self.pages:
420
raise AlreadyExistsError(
421
"TabPage named '%s' already exists" % page_name)
423
self.pages[page_name] = self.page_class(self.pages_frame)
424
self._pages_order.append(page_name)
425
self._tab_set.add_tab(page_name)
427
if len(self.pages) == 1: # adding first page
428
self._default_page = page_name
429
self.change_page(page_name)
431
def remove_page(self, page_name):
432
"""Destroy the page whose name is given in page_name."""
433
if not page_name in self.pages:
434
raise KeyError("No such TabPage: '%s" % page_name)
436
self._pages_order.remove(page_name)
438
# handle removing last remaining, default, or currently shown page
439
if len(self._pages_order) > 0:
440
if page_name == self._default_page:
441
# set a new default page
442
self._default_page = self._pages_order[0]
444
self._default_page = None
446
if page_name == self._current_page:
447
self.change_page(self._default_page)
449
self._tab_set.remove_tab(page_name)
450
page = self.pages.pop(page_name)
453
def change_page(self, page_name):
454
"""Show the page whose name is given in page_name."""
455
if self._current_page == page_name:
457
if page_name is not None and page_name not in self.pages:
458
raise KeyError("No such TabPage: '%s'" % page_name)
460
if self._current_page is not None:
461
self.pages[self._current_page]._hide()
462
self._current_page = None
464
if page_name is not None:
465
self._current_page = page_name
466
self.pages[page_name]._show()
468
self._tab_set.set_selected_tab(page_name)
470
if __name__ == '__main__':
473
tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0,
476
tabPage.pack(side=TOP, expand=TRUE, fill=BOTH)
477
Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack()
478
Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack()
479
Label(tabPage.pages['Baz'].frame, text='Baz').pack()
480
entryPgName=Entry(root)
481
buttonAdd=Button(root, text='Add Page',
482
command=lambda:tabPage.add_page(entryPgName.get()))
483
buttonRemove=Button(root, text='Remove Page',
484
command=lambda:tabPage.remove_page(entryPgName.get()))
485
labelPgName=Label(root, text='name of page to add/remove:')
486
buttonAdd.pack(padx=5, pady=5)
487
buttonRemove.pack(padx=5, pady=5)
488
labelPgName.pack(padx=5)
489
entryPgName.pack(padx=5)