How to Add a Question Mark [] Button on the Top of a Tkinter Window

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

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. 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.

How to let the frame go top level in tkinter?

Call tkraise() for the frame everywhere (Every Entry widget in your case) you are willing the frame to be rise above:

self.test_entry.bind("<FocusIn>", lambda event:self.numpad_frame.place(x=105,y=25), self.numpad_frame.tkraise())


Could you let me know how to create the float windows in tkinter?

You can use tkinter.messagebox for such purpose. Use messagebox.askokcancel to ask the user to select between confirm or cancel.

PS: Try to place the frame at coordinates such that it does not overlap with the Entry widgets, as is happening in your case, because it builds a pretty poor UI. And please provide a minimal reproducible example when you ask a question.

Create Toplevel tkinter Window after the closure of previous Toplevel window

You need to call top.wait_window() at the end inside the for loop:

for _ in range(len(a)):
top = Toplevel(root)
top.title("selections")
def next_window():
top.destroy()
show() # This function is supposed to show the selections of each popup window on root gui
for i in range(3):
b.append(IntVar())
b[i].set(0) # de selecting all checkboxes intiially
# checkboxes
Checkbutton(top, text=a[i], variable=b[i]).pack()
Button(top, text = "Submit", command=next_window).pack()
Button(top, text = "skip", command=top.destroy).pack() # this button is used to skip the popup if no selection required
top.grab_set() # route all events to this window
top.wait_window() # wait for current window to close


Related Topics



Leave a reply



Submit