How to add a question mark [?] button on the top of a tkinter window

I would like to create a window for my python tkinter project which has a question mark button on top of the window like this:

How to add a question mark [?] button on the top of a tkinter window

Is there anyway I can do this?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

I think I got it working:

from PIL import Image, ImageTk
import tkinter as tk

import sys
USING_WINDOWS = ("win" in sys.platform)

THEME_OPTIONS = ("light", "dark")
THEME = "dark"

if THEME == "dark":
    THEME_BG = "black"
    THEME_FG = "white"
    THEME_SEP_COLOUR = "grey"
    THEME_HIGHLIGHT = "grey"
    THEME_ACTIVE_TITLEBAR_BG = "black"
    THEME_INACTIVE_TITLEBAR_BG = "grey17"
elif THEME == "light":
    THEME_BG = "#f0f0ed"
    THEME_FG = "black"
    THEME_SEP_COLOUR = "grey"
    THEME_HIGHLIGHT = "grey"
    THEME_ACTIVE_TITLEBAR_BG = "white"
    THEME_INACTIVE_TITLEBAR_BG = "grey80"

SNAP_THRESHOLD = 200
SEPARATOR_SIZE = 1
NUMBER_OF_CUSTOM_BUTTONS = 5

USE_UNICODE = False


class CustomButton(tk.Button):
    def __init__(self, master, betterroot, name="#", function=None, column=0):
        self.betterroot = betterroot
        if function is None:
            self.callback = lambda: None
        else:
            self.callback = function
        super().__init__(master, text=name, relief="flat", bg=THEME_BG,
                         fg=THEME_FG, command=lambda: self.callback())
        self.column = column

    def show(self, column=None):
        """
        Shows the button on the screen
        """
        if column is None:
            column = self.column
        super().grid(row=1, column=column)

    def hide(self):
        """
        Hides the button from the screen
        """
        super().grid_forget()


class MinimiseButton(tk.Button):
    def __init__(self, master, betterroot):
        self.betterroot = betterroot
        if USE_UNICODE:
            text = "u2014"
        else:
            text = "_"
        super().__init__(master, text=text, relief="flat", bg=THEME_BG,
                         fg=THEME_FG, command=self.minimise_window)

    def minimise_window(self):
        """
        Minimises the window
        """
        self.betterroot.dummy_root.iconify()
        self.betterroot.root.withdraw()

    def show(self, column=NUMBER_OF_CUSTOM_BUTTONS+2):
        """
        Shows the button on the screen
        """
        super().grid(row=1, column=column)

    def hide(self):
        """
        Hides the button from the screen
        """
        super().grid_forget()


class FullScreenButton(tk.Button):
    def __init__(self, master, betterroot):
        self.betterroot = betterroot
        if USE_UNICODE:
            text = "u2610"
        else:
            text = "[]"
        super().__init__(master, text=text, relief="flat", bg=THEME_BG,
                         fg=THEME_FG, command=self.toggle_fullscreen)

    def toggle_fullscreen(self, event=None):
        """
        Toggles fullscreen.
        """
        # If it is called from double clicking:
        if event is not None:
            # Make sure that we didn't double click something else
            if not self.betterroot.check_parent_titlebar(event):
                return None
        # If it is the title bar toggle fullscreen:
        if self.betterroot.is_full_screen:
            self.notfullscreen()
        else:
            self.fullscreen()

    def fullscreen(self):
        """
        Switches to full screen.
        """
        if self.betterroot.is_full_screen:
            return "error"
        super().config(command=self.notfullscreen)
        if USING_WINDOWS:
            self.betterroot.root.overrideredirect(False)
        else:
            self.betterroot.root.attributes("-type", "normal")
        self.betterroot.root.attributes("-fullscreen", True)
        self.betterroot.is_full_screen = True

    def notfullscreen(self):
        """
        Switches to back to normal (not full) screen.
        """
        if not self.betterroot.is_full_screen:
            return "error"
        # This toggles between the `fullscreen` and `notfullscreen` methods
        super().config(command=self.fullscreen)
        self.betterroot.root.attributes("-fullscreen", False)
        if USING_WINDOWS:
            self.betterroot.root.overrideredirect(True)
        else:
            self.betterroot.root.attributes("-type", "splash")
        self.betterroot.is_full_screen = False

    def show(self, column=NUMBER_OF_CUSTOM_BUTTONS+3):
        """
        Shows the button on the screen
        """
        super().grid(row=1, column=column)

    def hide(self):
        """
        Hides the button from the screen
        """
        super().grid_forget()


class CloseButton(tk.Button):
    def __init__(self, master, betterroot):
        self.betterroot = betterroot
        if USE_UNICODE:
            text = "u26cc"
        else:
            text = "X"
        super().__init__(master, text=text, relief="flat", bg=THEME_BG,
                         fg=THEME_FG, command=self.close_window_protocol)

    def close_window_protocol(self):
        """
        Generates a `WM_DELETE_WINDOW` protocol request.
        If unhandled it will automatically go to `root.destroy()`
        """
        self.betterroot.protocol_generate("WM_DELETE_WINDOW")

    def show(self, column=NUMBER_OF_CUSTOM_BUTTONS+4):
        """
        Shows the button on the screen
        """
        super().grid(row=1, column=column)

    def hide(self):
        """
        Hides the button from the screen
        """
        super().grid_forget()


class BetterTk(tk.Frame):
    """
    Attributes:
        disable_north_west_resizing
        *Buttons*
            minimise_button
            fullscreen_button
            close_button
        *List of all buttons*
            buttons: [minimise_button, fullscreen_button, close_button, ...]

    Methods:
        *List of newly defined methods*
            change_titlebar_bg(new_bg_colour) => None
            protocol_generate(protocol) => None
            #custom_buttons#
            topmost() => None

        *List of methods that act the same was as tkinter.Tk's methods*
            title
            config
            protocol
            geometry
            focus_force
            destroy
            iconbitmap
            resizable
            attributes
            withdraw
            iconify
            deiconify
            maxsize
            minsize
            state
            report_callback_exception


    The buttons:
        minimise_button:
            minimise_window() => None
            show(column) => None
            hide() => None

        fullscreen_button:
            toggle_fullscreen() => None
            fullscreen() => None
            notfullscreen() => None
            show(column) => None
            hide() => None

        close_button:
            close_window_protocol() => None
            show(column) => None
            hide() => None
        buttons: # It is a list of all of the buttons

    The custom_buttons:
        The proper way of using it is:
            ```
            root = BetterTk()

            root.custom_buttons = {"name": "?",
                                   "function": questionmark_pressed,
                                   "column": 0}
            questionmark_button = root.buttons[-1]

            root.custom_buttons = {"name": "u2263",
                                   "function": three_lines_pressed,
                                   "column": 2}
            threelines_button = root.buttons[-1]
            ```
        You can call:
            show(column) => None
            hide() => None
    """
    def __init__(self, master=None, Class=tk.Tk):
        if Class == tk.Toplevel:
            self.root = tk.Toplevel(master)
        elif Class == tk.Tk:
            self.root = tk.Tk()
        else:
            raise ValueError("Invalid `Class` argument.")
        self.protocols = {"WM_DELETE_WINDOW": self.destroy}
        self.window_destroyed = False
        self.focused_widget = None
        self.is_full_screen = False

        # Create the dummy window
        self.dummy_root = tk.Toplevel(self.root)
        self.dummy_root.bind("<FocusIn>", self.focus_main)
        self.dummy_root.protocol("WM_DELETE_WINDOW", lambda: self.protocol_generate("WM_DELETE_WINDOW"))
        self.root.update()
        self.dummy_root.after(1, self.dummy_root.geometry, "1x1")
        geometry = "+%i+%i" % (self.root.winfo_x(), self.root.winfo_y())
        if USING_WINDOWS:
            self.root.overrideredirect(True)
        else:
            self.root.attributes("-type", "splash")
        self.geometry(geometry)
        self.root.bind("<FocusIn>", self.window_focused)
        self.root.bind("<FocusOut>", self.window_unfocused)

        # Master frame so that I can add a grey border around the window
        self.master_frame = tk.Frame(self.root, highlightthickness=3, bd=0,
                                     highlightbackground=THEME_HIGHLIGHT)
        self.master_frame.pack(expand=True, fill="both")
        self.resizable_window = ResizableWindow(self.master_frame, self)

        # The actual <tk.Frame> where you can put your widgets
        super().__init__(self.master_frame, bd=0, bg=THEME_BG, cursor="arrow")
        super().pack(expand=True, side="bottom", fill="both")

        # Set up the title bar frame
        self.title_bar = tk.Frame(self.master_frame, bg=THEME_BG, bd=0,
                                  cursor="arrow")
        self.title_bar.pack(side="top", fill="x")
        self.draggable_window = DraggableWindow(self.title_bar, self)

        # Add a separator
        self.separator = tk.Frame(self.master_frame, bg=THEME_SEP_COLOUR,
                                  height=SEPARATOR_SIZE, bd=0, cursor="arrow")
        self.separator.pack(fill="x")

        # For the titlebar frame
        self.title_frame = tk.Frame(self.title_bar, bg=THEME_BG)
        self.title_frame.pack(expand=True, side="left", anchor="w", padx=5)

        self.buttons_frame = tk.Frame(self.title_bar, bg=THEME_BG)
        self.buttons_frame.pack(expand=True, side="right", anchor="e")

        self.title_label = tk.Label(self.title_frame, text="Better Tk",
                                    bg=THEME_BG, fg=THEME_FG)
        self.title_label.grid(row=1, column=2, sticky="news")
        self.icon_label = None

        self.minimise_button = MinimiseButton(self.buttons_frame, self)
        self.minimise_button.show()
        self.fullscreen_button = FullScreenButton(self.buttons_frame, self)
        self.fullscreen_button.show()
        self.close_button = CloseButton(self.buttons_frame, self)
        self.close_button.show()

        # When the user double clicks on the titlebar
        self.title_bar.bind_all("<Double-Button-1>",
                                self.fullscreen_button.toggle_fullscreen)
        # When the user middle clicks on the titlebar
        self.title_bar.bind_all("<Button-2>", self.snap_to_side)

        self.buttons = [self.minimise_button, self.fullscreen_button,
                        self.close_button]

    def snap_to_side(self, event):
        """
        Moves the window to the side that it's close to.
        """
        if (event is not None) and (not self.check_parent_titlebar(event)):
            return None
        rootx, rooty = self.root.winfo_rootx(), self.root.winfo_rooty()
        width = self.master_frame.winfo_width()
        height = self.master_frame.winfo_height()
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()

        geometry = [rootx, rooty]

        if rootx < SNAP_THRESHOLD:
            geometry[0] = 0
        if rooty < SNAP_THRESHOLD:
            geometry[1] = 0
        if screen_width - (rootx + width) < SNAP_THRESHOLD:
            geometry[0] = screen_width - width
        if screen_height - (rooty + height) < SNAP_THRESHOLD:
            geometry[1] = screen_height - height
        self.geometry("+%i+%i" % tuple(geometry))

    def focus_main(self, event=None):
        """
        When the dummy window gets focused it passes the focus to the main
        window. It also focuses the last focused widget.
        """
        self.root.lift()
        self.root.deiconify()
        if self.focused_widget is None:
            self.root.focus_force()
        else:
            self.focused_widget.focus_force()

    def get_focused_widget(self, event=None):
        widget = self.root.focus_get()
        if not ((widget == self.root) or (widget == None)):
            self.focused_widget = widget

    def window_focused(self, event):
        self.get_focused_widget()
        self.change_titlebar_bg(THEME_ACTIVE_TITLEBAR_BG)

    def window_unfocused(self, event):
        self.get_focused_widget()
        self.change_titlebar_bg(THEME_INACTIVE_TITLEBAR_BG)

    def change_titlebar_bg(self, colour):
        """
        Changes the bg of the root.
        """
        items = (self.title_bar, self.buttons_frame, self.title_label)
        items += tuple(self.buttons)
        if self.icon_label is not None:
            items += (self.icon_label, )
        for item in items:
            item.config(background=colour)

    def protocol_generate(self, protocol):
        """
        Generates a protocol.
        """
        try:
            function = self.protocols[protocol]
            function()
        except KeyError:
            raise tk.TclError("Tried generating unknown protocol: "%s"" %
                              protocol)

    def check_parent_titlebar(self, event):
        # Get the widget that was pressed:
        widget = event.widget
        # Check if it is part of the title bar or something else
        # It checks its parent and its parent's parent and
        # its parent's parent's parent and ... until it finds
        # whether or not the widget clicked on is the title bar.

        while widget != self.root:
            if widget == self.buttons_frame:
                # Don't allow moving the window when buttons are clicked
                return False
            if widget == self.title_bar:
                return True

            # In some very rare cases `widget` can be `None`
            # And widget.master will throw an error
            if widget is None:
                return False
            widget = widget.master
        return False

    @property
    def custom_buttons(self):
        return None

    @custom_buttons.setter
    def custom_buttons(self, value):
        self.custom_button = CustomButton(self.buttons_frame, self, **value)
        self.custom_button.show()
        self.buttons.append(self.custom_button)

    @property
    def disable_north_west_resizing(self):
        return self.resizable_window.disable_north_west_resizing

    @disable_north_west_resizing.setter
    def disable_north_west_resizing(self, value):
        self.resizable_window.disable_north_west_resizing = value

    # Normal <tk.Tk> methods:
    def title(self, title):
        # Changing the title of the window
        # Note the name will aways be shows and the window can't be resized
        # to cover it up.
        self.title_label.config(text=title)
        self.root.title(title)
        self.dummy_root.title(title)

    def config(self, bg=None, **kwargs):
        if bg is not None:
            super().config(bg=bg)
        self.root.config(**kwargs)

    def protocol(self, protocol, function):
        """
        Binds a function to a protocol.
        """
        self.protocols.update({protocol: function})

    def topmost(self):
        self.attributes("-topmost", True)

    def geometry(self, geometry):
        if not isinstance(geometry, str):
            raise ValueError("The geometry must be a string")
        if geometry.count("+") not in (0, 2):
            raise ValueError("Invalid geometry: "%s"" % repr(geometry)[1:-1])
        dummy_geometry = ""
        if "+" in geometry:
            _, posx, posy = geometry.split("+")
            dummy_geometry = "+%i+%i" % (int(posx) + 75, int(posy) + 20)
        self.root.geometry(geometry)
        self.dummy_root.geometry(dummy_geometry)

    def focus_force(self):
        self.root.deiconify()
        self.root.focus_force()

    def destroy(self):
        if self.window_destroyed:
            super().destroy()
        else:
            self.window_destroyed = True
            self.root.destroy()

    def iconbitmap(self, filename):
        if self.icon_label is not None:
            self.icon_label.destroy()
        self.dummy_root.iconbitmap(filename)
        self.root.lift()
        self.root.update_idletasks()
        size = self.title_frame.winfo_height()
        img = Image.open(filename).resize((size, size), Image.LANCZOS)
        self._tk_icon = ImageTk.PhotoImage(img, master=self.root)
        bg = self.title_label.cget("background")
        self.icon_label = tk.Label(self.title_frame, image=self._tk_icon, bg=bg)
        self.icon_label.grid(row=1, column=1, sticky="news")

    def resizable(self, width=None, height=None):
        if width is not None:
            self.resizable_window.resizable_horizontal = width
        if height is not None:
            self.resizable_window.resizable_vertical = height
        return None

    def attributes(self, *args, **kwargs):
        self.root.attributes(*args, **kwargs)

    def withdraw(self):
        self.minimise_button.minimise_window()
        self.dummy_root.withdraw()

    def iconify(self):
        self.dummy_root.iconify()
        self.minimise_button.minimise_window()

    def deiconify(self):
        self.dummy_root.deiconify()
        self.dummy_root.focus_force()

    def maxsize(self, *args, **kwargs):
        self.root.maxsize(*args, **kwargs)

    def minsize(self, *args, **kwargs):
        self.root.minsize(*args, **kwargs)

    def state(self, *args, **kwargs):
        self.root.state(*args, **kwargs)

    def report_callback_exception(self, *args, **kwargs):
        self.root.report_callback_exception(*args, **kwargs)


class ResizableWindow:
    def __init__(self, frame, betterroot):
        # Makes the frame resizable like a window
        self.frame = frame
        self.geometry = betterroot.geometry
        self.betterroot = betterroot

        self.sensitivity = 10

        # Variables for resizing:
        self.started_resizing = False
        self.quadrant_resizing = None
        self.disable_north_west_resizing = False
        self.resizable_horizontal = True
        self.resizable_vertical = True

        self.frame.bind("<Enter>", self.change_cursor_resizing)
        self.frame.bind("<Motion>", self.change_cursor_resizing)

        frame.bind("<Button-1>", self.mouse_press)
        frame.bind("<B1-Motion>", self.mouse_motion)
        frame.bind("<ButtonRelease-1>", self.mouse_release)

        self.started_resizing = False

    def mouse_motion(self, event):
        if self.started_resizing:
            new_params = [self.current_width, self.current_height,
                          self.currentx, self.currenty]

            if "e" in self.quadrant_resizing:
                self.update_resizing_params(new_params, self.resize_east())
            if "n" in self.quadrant_resizing:
                self.update_resizing_params(new_params, self.resize_north())
            if "s" in self.quadrant_resizing:
                self.update_resizing_params(new_params, self.resize_south())
            if "w" in self.quadrant_resizing:
                self.update_resizing_params(new_params, self.resize_west())

            self.geometry("%ix%i+%i+%i" % tuple(new_params))

    def mouse_release(self, event):
        self.started_resizing = False

    def mouse_press(self, event):
        if self.betterroot.is_full_screen:
            return None
        # Resizing the window:
        if event.widget == self.frame:
            self.current_width = self.betterroot.root.winfo_width()
            self.current_height = self.betterroot.root.winfo_height()
            self.currentx = self.betterroot.root.winfo_rootx()
            self.currenty = self.betterroot.root.winfo_rooty()

            quadrant_resizing = self.get_quadrant_resizing()

            if len(quadrant_resizing) > 0:
                self.started_resizing = True
                self.quadrant_resizing = quadrant_resizing

    # For resizing:
    def change_cursor_resizing(self, event):
        if self.betterroot.is_full_screen:
            self.frame.config(cursor="arrow")
            return None
        if self.started_resizing:
            return None
        quadrant_resizing = self.get_quadrant_resizing()

        if quadrant_resizing == "":
            # Reset the cursor back to "arrow"
            self.frame.config(cursor="arrow")
        elif (quadrant_resizing == "ne") or (quadrant_resizing == "sw"):
            if USING_WINDOWS:
                # Available on Windows
                self.frame.config(cursor="size_ne_sw")
            else:
                # Available on Linux
                if quadrant_resizing == "nw":
                    self.frame.config(cursor="bottom_left_corner")
                else:
                    self.frame.config(cursor="top_right_corner")
        elif (quadrant_resizing == "nw") or (quadrant_resizing == "se"):
            if USING_WINDOWS:
                # Available on Windows
                self.frame.config(cursor="size_nw_se")
            else:
                # Available on Linux
                if quadrant_resizing == "nw":
                    self.frame.config(cursor="top_left_corner")
                else:
                    self.frame.config(cursor="bottom_right_corner")
        elif (quadrant_resizing == "n") or (quadrant_resizing == "s"):
            # Available on Windows/Linux
            self.frame.config(cursor="sb_v_double_arrow")
        elif (quadrant_resizing == "e") or (quadrant_resizing == "w"):
            # Available on Windows/Linux
            self.frame.config(cursor="sb_h_double_arrow")

    def get_quadrant_resizing(self):
        x, y = self.betterroot.root.winfo_pointerx(), self.betterroot.root.winfo_pointery()
        width, height = self.betterroot.root.winfo_width(), self.betterroot.root.winfo_height()

        x -= self.betterroot.root.winfo_rootx()
        y -= self.betterroot.root.winfo_rooty()
        quadrant_resizing = ""
        if self.resizable_vertical:
            if y + self.sensitivity > height:
                quadrant_resizing += "s"
            if not self.disable_north_west_resizing:
                if y < self.sensitivity:
                    quadrant_resizing += "n"
        if self.resizable_horizontal:
            if x + self.sensitivity > width:
                quadrant_resizing += "e"
            if not self.disable_north_west_resizing:
                if x < self.sensitivity:
                    quadrant_resizing += "w"
        return quadrant_resizing

    def resize_east(self):
        x = self.betterroot.root.winfo_pointerx()
        new_width = x - self.currentx
        if new_width < 240:
            new_width = 240
        return new_width, None, None, None

    def resize_south(self):
        y = self.betterroot.root.winfo_pointery()
        new_height = y - self.currenty
        if new_height < 80:
            new_height = 80
        return None, new_height, None, None

    def resize_north(self):
        y = self.betterroot.root.winfo_pointery()
        dy = self.currenty - y
        if dy < 80 - self.current_height:
            dy = 80 - self.current_height
        new_height = self.current_height + dy
        return None, new_height, None, self.currenty - dy

    def resize_west(self):
        x = self.betterroot.root.winfo_pointerx()
        dx = self.currentx - x
        if dx < 240 - self.current_width:
            dx = 240 - self.current_width
        new_width = self.current_width + dx
        return new_width, None, self.currentx - dx, None

    def update_resizing_params(self, _list, _tuple):
        for i in range(len(_tuple)):
            element = _tuple[i]
            if element is not None:
                _list[i] = element


class DraggableWindow:
    def __init__(self, frame, betterroot):
        # Makes the frame draggable like a window
        self.frame = frame
        self.geometry = betterroot.geometry
        self.betterroot = betterroot

        self.dragging = False
        self._offsetx = 0
        self._offsety = 0
        self.frame.bind_all("<Button-1>", self.clickwin)
        self.frame.bind_all("<B1-Motion>", self.dragwin)
        self.frame.bind_all("<ButtonRelease-1>", self.stopdragwin)

    def stopdragwin(self, event):
        self.dragging = False

    def dragwin(self, event):
        if self.dragging:
            x = self.frame.winfo_pointerx() - self._offsetx
            y = self.frame.winfo_pointery() - self._offsety
            self.geometry("+%i+%i" % (x, y))

    def clickwin(self, event):
        if self.betterroot.is_full_screen:
            return None
        if not self.betterroot.check_parent_titlebar(event):
            return None
        self.dragging = True
        self._offsetx = event.widget.winfo_rootx() -
                        self.betterroot.root.winfo_rootx() + event.x
        self._offsety = event.widget.winfo_rooty() -
                        self.betterroot.root.winfo_rooty() + event.y

and use this to try it out:

def questionmark_pressed():
    print(""?" was pressed")
def three_lines_pressed():
    print(""u2263" was pressed")

root = BetterTk()
# Adding a custom button:
root.custom_buttons = {"name": "?",
                       "function": questionmark_pressed,
                       "column": 0}
# Adding another custom button:
root.custom_buttons = {"name": "u2263",
                       "function": three_lines_pressed,
                       "column": 2}
root.geometry("400x400")
# root.minimise_button.hide()
root.mainloop()

I removed the title bar from the tk.Tk by using .overrideredirect(True). After that I just created my own title bar and placed it at the top. With this method you can add as many buttons as you want. I also made the title bar draggable so that you can move the window arround.

Edit: You can find the latest version here. Also please report all bugs that you find here. This code is part of my bigger project that I will keep updating.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x