6

I want to add close buttons to each tab in tkinter.ttk.Notebook. I already tried adding image and react to click event but unfortunately BitmapImage does not have bind() method.

How can I fix this code?

#!/usr/binenv python3

from tkinter import *
from tkinter.ttk import *


class Application(Tk):
    def __init__(self):
        super().__init__()
        notebook = Notebook(self)
        notebook.pack(fill=BOTH, expand=True)
        self.img = BitmapImage(master=self, file='./image.xbm')
        self.img.bind('<Button-1>', self._on_click)
        notebook.add(Label(notebook, text='tab content'), text='tab caption', image=self.img)

    def _on_click(self, event):
        print('it works')

app = Application()
app.mainloop()

image.xbm

#define bullet_width 11
#define bullet_height 9
static char bullet_bits = {
    0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00
}
BPS
  • 1,133
  • 1
  • 17
  • 38

2 Answers2

27

One advantage of the themed (ttk) widgets is that you can create new widgets out of individual widget "elements". While not exactly simple (nor well documented), you can create a new "close tab" element add add that to the "tab" element.

I will present one possible solution. I'll admit it's not particularly easy to understand. Perhaps one of the best sources for how to create custom widget styles can be found at tkdocs.com, starting with the Styles and Themes section.

try:
    import Tkinter as tk
    import ttk
except ImportError:  # Python 3
    import tkinter as tk
    from tkinter import ttk

class CustomNotebook(ttk.Notebook):
    """A ttk Notebook with close buttons on each tab"""

    __initialized = False

    def __init__(self, *args, **kwargs):
        if not self.__initialized:
            self.__initialize_custom_style()
            self.__inititialized = True

        kwargs["style"] = "CustomNotebook"
        ttk.Notebook.__init__(self, *args, **kwargs)

        self._active = None

        self.bind("<ButtonPress-1>", self.on_close_press, True)
        self.bind("<ButtonRelease-1>", self.on_close_release)

    def on_close_press(self, event):
        """Called when the button is pressed over the close button"""

        element = self.identify(event.x, event.y)

        if "close" in element:
            index = self.index("@%d,%d" % (event.x, event.y))
            self.state(['pressed'])
            self._active = index
            return "break"

    def on_close_release(self, event):
        """Called when the button is released"""
        if not self.instate(['pressed']):
            return

        element =  self.identify(event.x, event.y)
        if "close" not in element:
            # user moved the mouse off of the close button
            return

        index = self.index("@%d,%d" % (event.x, event.y))

        if self._active == index:
            self.forget(index)
            self.event_generate("<<NotebookTabClosed>>")

        self.state(["!pressed"])
        self._active = None

    def __initialize_custom_style(self):
        style = ttk.Style()
        self.images = (
            tk.PhotoImage("img_close", data='''
                R0lGODlhCAAIAMIBAAAAADs7O4+Pj9nZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg
                d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU
                5kEJADs=
                '''),
            tk.PhotoImage("img_closeactive", data='''
                R0lGODlhCAAIAMIEAAAAAP/SAP/bNNnZ2cbGxsbGxsbGxsbGxiH5BAEKAAQALAAA
                AAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU5kEJADs=
                '''),
            tk.PhotoImage("img_closepressed", data='''
                R0lGODlhCAAIAMIEAAAAAOUqKv9mZtnZ2Ts7Ozs7Ozs7Ozs7OyH+EUNyZWF0ZWQg
                d2l0aCBHSU1QACH5BAEKAAQALAAAAAAIAAgAAAMVGDBEA0qNJyGw7AmxmuaZhWEU
                5kEJADs=
            ''')
        )

        style.element_create("close", "image", "img_close",
                            ("active", "pressed", "!disabled", "img_closepressed"),
                            ("active", "!disabled", "img_closeactive"), border=8, sticky='')
        style.layout("CustomNotebook", [("CustomNotebook.client", {"sticky": "nswe"})])
        style.layout("CustomNotebook.Tab", [
            ("CustomNotebook.tab", {
                "sticky": "nswe",
                "children": [
                    ("CustomNotebook.padding", {
                        "side": "top",
                        "sticky": "nswe",
                        "children": [
                            ("CustomNotebook.focus", {
                                "side": "top",
                                "sticky": "nswe",
                                "children": [
                                    ("CustomNotebook.label", {"side": "left", "sticky": ''}),
                                    ("CustomNotebook.close", {"side": "left", "sticky": ''}),
                                ]
                        })
                    ]
                })
            ]
        })
    ])

if __name__ == "__main__":
    root = tk.Tk()

    notebook = CustomNotebook(width=200, height=200)
    notebook.pack(side="top", fill="both", expand=True)

    for color in ("red", "orange", "green", "blue", "violet"):
        frame = tk.Frame(notebook, background=color)
        notebook.add(frame, text=color)

    root.mainloop()

Here's what it looks like on a linux system:

enter image description here

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • How hard would it be to make the new elements unique for each tab? In my app I want a "close" element and a "save/modified" element, which goes red or gray when the content is currently unsaved or saved (just like in Notepad++). The issue I'm facing is how to control the "save" element color in each tab. So, for your answer here, when you click on a "close" element, how might you make the one "close" element change color? (instead of closing the tab/child, of course) – GaryMBloom Jan 11 '18 at 06:26
  • Okay, I just found https://stackoverflow.com/questions/23038356/change-color-of-tab-header-in-ttk-notebook, which talks about Themes in lieu of Styles. Still trying to digest it... And of course that example doesn't address the extra complexity of your additional element(s)... – GaryMBloom Jan 11 '18 at 08:58
  • Amazing example! Can you please explain where the 'data' in tk.PhotoImage comes from? I tried searched it myself, but all the other codes samples seem to be using their local image(which have extensions like .png, .gif) – cindy50633 Apr 04 '20 at 16:09
  • Is it possible to modify this code so that the 'x' button inside the tab changes the color to red or yellow only when the mouse cursor hovering over 'x' element in the tab? This way a user won't confuse between selecting and closing the tab by accident . – D V Oct 03 '20 at 23:39
  • @amrsa: I've added a workaround for the bug. – Bryan Oakley Feb 23 '21 at 15:42
  • @BryanOakley It also gives a white border around EVERYTHING see: [here](https://postimg.cc/xkpK23b0) – Whirlpool-Programmer Sep 17 '21 at 12:59
  • I'm getting TclError: Duplicate element close – Rugved Modak Jun 08 '22 at 01:28
  • @RugvedModak I had this problem when testing this code in an IPython Console through Spyder. I no longer had this problem after telling Spyder to run in a system console. – ddm-j Oct 24 '22 at 22:52
1

I enjoyed using this code very much, thank you!!! Fixed a bug in creating multiple Notebooks by modifying the constructor to:

    def __init__(self, *args, **kwargs):
        if not self.__initialized:
            self.__initialize_custom_style()
            CustomNotebook.__initialized = True

Hope others can leverage too:-)