• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""An implementation of tabbed pages using only standard Tkinter.
2
3Originally developed for use in IDLE. Based on tabpage.py.
4
5Classes exported:
6TabbedPageSet -- A Tkinter implementation of a tabbed-page widget.
7TabSet -- A widget containing tabs (buttons) in one or more rows.
8
9"""
10from Tkinter import *
11
12class InvalidNameError(Exception): pass
13class AlreadyExistsError(Exception): pass
14
15
16class TabSet(Frame):
17    """A widget containing tabs (buttons) in one or more rows.
18
19    Only one tab may be selected at a time.
20
21    """
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:
26
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
29        argument.
30
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
34        empty.
35
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.
39
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
42        details.
43
44        """
45        Frame.__init__(self, page_set, **kw)
46        self.select_command = select_command
47        self.n_rows = n_rows
48        self.max_tabs_per_row = max_tabs_per_row
49        self.expand_tabs = expand_tabs
50        self.page_set = page_set
51
52        self._tabs = {}
53        self._tab2row = {}
54        if tabs:
55            self._tab_names = list(tabs)
56        else:
57            self._tab_names = []
58        self._selected_tab = None
59        self._tab_rows = []
60
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)
65
66        self._arrange_tabs()
67
68    def add_tab(self, tab_name):
69        """Add a new tab with the name given in tab_name."""
70        if not 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)
74
75        self._tab_names.append(tab_name)
76        self._arrange_tabs()
77
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)
82
83        self._tab_names.remove(tab_name)
84        self._arrange_tabs()
85
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:
89            return
90        if tab_name is not None and tab_name not in self._tabs:
91            raise KeyError("No such Tab: '%s" % page_name)
92
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
97
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]
102            tab.set_selected()
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)
107
108    def _add_tab_row(self, tab_names, expand_tabs):
109        if not tab_names:
110            return
111
112        tab_row = Frame(self)
113        tab_row.pack(side=TOP, fill=X, expand=0)
114        self._tab_rows.append(tab_row)
115
116        for tab_name in tab_names:
117            tab = TabSet.TabButton(tab_name, self.select_command,
118                                   tab_row, self)
119            if expand_tabs:
120                tab.pack(side=LEFT, fill=X, expand=True)
121            else:
122                tab.pack(side=LEFT)
123            self._tabs[tab_name] = tab
124            self._tab2row[tab] = tab_row
125
126        # tab is the last one created in the above loop
127        tab.is_last_in_row = True
128
129    def _reset_tab_rows(self):
130        while self._tab_rows:
131            tab_row = self._tab_rows.pop()
132            tab_row.destroy()
133        self._tab2row = {}
134
135    def _arrange_tabs(self):
136        """
137        Arrange the tabs in rows, in the order in which they were added.
138
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.
143
144        """
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()
149
150        if not self._tab_names:
151            return
152
153        if self.n_rows is not None and self.n_rows > 0:
154            n_rows = self.n_rows
155        else:
156            # calculate the required number of rows
157            n_rows = (len(self._tab_names) - 1) // self.max_tabs_per_row + 1
158
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]
166            i += n_tabs
167            self._add_tab_row(tab_names, expand_tabs)
168
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)
174
175    class TabButton(Frame):
176        """A simple tab-like widget."""
177
178        bw = 2 # borderwidth
179
180        def __init__(self, name, select_command, tab_row, tab_set):
181            """Constructor arguments:
182
183            name -- The tab's name, which will appear in its button.
184
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.
187
188            """
189            Frame.__init__(self, tab_row, borderwidth=self.bw, relief=RAISED)
190
191            self.name = name
192            self.select_command = select_command
193            self.tab_set = tab_set
194            self.is_last_in_row = False
195
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)
201
202            self._init_masks()
203            self.set_normal()
204
205        def _select_event(self, *args):
206            """Event handler for tab selection.
207
208            With TabbedPageSet, this calls TabbedPageSet.change_page, so that
209            selecting a tab changes the page.
210
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.
214
215            """
216            self.select_command(self.name)
217            return
218
219        def set_selected(self):
220            """Assume selected look"""
221            self._place_masks(selected=True)
222
223        def set_normal(self):
224            """Assume normal look"""
225            self._place_masks(selected=False)
226
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
234            # left border
235            self.mskl = Frame(page_set, borderwidth=0, relief=FLAT,
236                              background=background)
237            self.mskl.ml = Frame(self.mskl, borderwidth=self.bw,
238                                 relief=RAISED)
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
242            # right border
243            self.mskr = Frame(page_set, borderwidth=0, relief=FLAT,
244                              background=background)
245            self.mskr.mr = Frame(self.mskr, borderwidth=self.bw,
246                                 relief=RAISED)
247
248        def _place_masks(self, selected=False):
249            height = self.bw
250            if selected:
251                height += self.bw
252
253            self.mask.place(in_=self,
254                            relx=0.0, x=0,
255                            rely=1.0, y=0,
256                            relwidth=1.0, width=0,
257                            relheight=0.0, height=height)
258
259            self.mskl.place(in_=self,
260                            relx=0.0, x=-self.bw,
261                            rely=1.0, y=0,
262                            relwidth=0.0, width=self.bw,
263                            relheight=0.0, height=height)
264
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())
269                             ):
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)
273                height -= self.bw
274
275            self.mskr.place(in_=self,
276                            relx=1.0, x=0,
277                            rely=1.0, y=0,
278                            relwidth=0.0, width=self.bw,
279                            relheight=0.0, height=height)
280
281            self.mskr.mr.place(x=-self.bw, y=-self.bw,
282                               width=2*self.bw, height=height + self.bw*2)
283
284            # finally, lower the tab set so that all of the frames we just
285            # placed hide it
286            self.tab_set.lower()
287
288class TabbedPageSet(Frame):
289    """A Tkinter tabbed-pane widget.
290
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.
293
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.
297
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.
300
301    Pages may be added or removed at any time using the add_page() and
302    remove_page() methods.
303
304    """
305    class Page(object):
306        """Abstract base class for TabbedPageSet's pages.
307
308        Subclasses must override the _show() and _hide() methods.
309
310        """
311        uses_grid = False
312
313        def __init__(self, page_set):
314            self.frame = Frame(page_set, borderwidth=2, relief=RAISED)
315
316        def _show(self):
317            raise NotImplementedError
318
319        def _hide(self):
320            raise NotImplementedError
321
322    class PageRemove(Page):
323        """Page class using the grid placement manager's "remove" mechanism."""
324        uses_grid = True
325
326        def _show(self):
327            self.frame.grid(row=0, column=0, sticky=NSEW)
328
329        def _hide(self):
330            self.frame.grid_remove()
331
332    class PageLift(Page):
333        """Page class using the grid placement manager's "lift" mechanism."""
334        uses_grid = True
335
336        def __init__(self, page_set):
337            super(TabbedPageSet.PageLift, self).__init__(page_set)
338            self.frame.grid(row=0, column=0, sticky=NSEW)
339            self.frame.lower()
340
341        def _show(self):
342            self.frame.lift()
343
344        def _hide(self):
345            self.frame.lower()
346
347    class PagePackForget(Page):
348        """Page class using the pack placement manager's "forget" mechanism."""
349        def _show(self):
350            self.frame.pack(fill=BOTH, expand=True)
351
352        def _hide(self):
353            self.frame.pack_forget()
354
355    def __init__(self, parent, page_names=None, page_class=PageLift,
356                 n_rows=1, max_tabs_per_row=5, expand_tabs=False,
357                 **kw):
358        """Constructor arguments:
359
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.
365
366        n_rows, max_tabs_per_row -- Parameters for the TabSet which will
367        manage the tabs. See TabSet's docs for details.
368
369        page_class -- Pages can be shown/hidden using three mechanisms:
370
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
375          added/removed.)
376
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.
380
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.
385
386        """
387        Frame.__init__(self, parent, **kw)
388
389        self.page_class = page_class
390        self.pages = {}
391        self._pages_order = []
392        self._current_page = None
393        self._default_page = None
394
395        self.columnconfigure(0, weight=1)
396        self.rowconfigure(1, weight=1)
397
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)
403
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)
408        if page_names:
409            for name in page_names:
410                self.add_page(name)
411        self._tab_set.grid(row=0, column=0, sticky=NSEW)
412
413        self.change_page(self._default_page)
414
415    def add_page(self, page_name):
416        """Add a new page with the name given in page_name."""
417        if not 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)
422
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)
426
427        if len(self.pages) == 1: # adding first page
428            self._default_page = page_name
429            self.change_page(page_name)
430
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)
435
436        self._pages_order.remove(page_name)
437
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]
443        else:
444            self._default_page = None
445
446        if page_name == self._current_page:
447            self.change_page(self._default_page)
448
449        self._tab_set.remove_tab(page_name)
450        page = self.pages.pop(page_name)
451        page.frame.destroy()
452
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:
456            return
457        if page_name is not None and page_name not in self.pages:
458            raise KeyError("No such TabPage: '%s'" % page_name)
459
460        if self._current_page is not None:
461            self.pages[self._current_page]._hide()
462        self._current_page = None
463
464        if page_name is not None:
465            self._current_page = page_name
466            self.pages[page_name]._show()
467
468        self._tab_set.set_selected_tab(page_name)
469
470def _tabbed_pages(parent):
471    # test dialog
472    root=Tk()
473    width, height, x, y = list(map(int, re.split('[x+]', parent.geometry())))
474    root.geometry("+%d+%d"%(x, y + 175))
475    root.title("Test tabbed pages")
476    tabPage=TabbedPageSet(root, page_names=['Foobar','Baz'], n_rows=0,
477                          expand_tabs=False,
478                          )
479    tabPage.pack(side=TOP, expand=TRUE, fill=BOTH)
480    Label(tabPage.pages['Foobar'].frame, text='Foo', pady=20).pack()
481    Label(tabPage.pages['Foobar'].frame, text='Bar', pady=20).pack()
482    Label(tabPage.pages['Baz'].frame, text='Baz').pack()
483    entryPgName=Entry(root)
484    buttonAdd=Button(root, text='Add Page',
485            command=lambda:tabPage.add_page(entryPgName.get()))
486    buttonRemove=Button(root, text='Remove Page',
487            command=lambda:tabPage.remove_page(entryPgName.get()))
488    labelPgName=Label(root, text='name of page to add/remove:')
489    buttonAdd.pack(padx=5, pady=5)
490    buttonRemove.pack(padx=5, pady=5)
491    labelPgName.pack(padx=5)
492    entryPgName.pack(padx=5)
493    root.mainloop()
494
495
496if __name__ == '__main__':
497    from idlelib.idle_test.htest import run
498    run(_tabbed_pages)
499