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
._tab
2row
[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
._tab
2row
[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)