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:
![tt7fg How to add a question mark [?] button on the top of a tkinter window](https://magenaut.com/wp-content/uploads/2022/08/tt7fg.png)
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