Skip to content

Instantly share code, notes, and snippets.

@israel-dryer
Last active July 23, 2022 14:26
Show Gist options
  • Save israel-dryer/a8796c3cec7da1553ecb5425e0dd202b to your computer and use it in GitHub Desktop.
Save israel-dryer/a8796c3cec7da1553ecb5425e0dd202b to your computer and use it in GitHub Desktop.
How to position a topside relative to another widget with and without titlebar
import tkinter as tk
from tkinter import ttk
"""
The demo shows how to position a topside window relative to a button widget from multiple directions. The purpose
is to show that one could theoretically create a custom menu from a toplevel that could be positioned correctly
aligned to the parent widget with and without a titlebar.
The problem with positioning a topside relative to another widget is due to the fact that, normally, window
decorations are added to the window. This adds additional horizontal a vertical space that is not accounted for in
the screen coordinates calculated by `winfo_rootx` and `winfo_x`. However, using these two measurements together,
you can calculate the amount of space allocated to the borders and titlebar and adjust your screen position
accordingly with these offsets.
The calculate the offset of the x and y position by taking `winfo_x/y` - `winfo_rootx/y`. This difference is then
added to the x or y position to adjust for the border and titlebar depending on how you want the topside aligned to
the parent.
"""
class Application(tk.Tk):
def __init__(self, show_menu_titlebar=False):
super().__init__()
self.title('Custom Menu Demo')
self.geometry('+500+500')
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure('menu.TFrame', relief='raised')
self.frame = ttk.Frame(self, padding=20)
self.frame.pack()
ttk.Label(self.frame, text='Choose a direction and push the button').pack()
values = ['wn','ws','sw','se','es','en','ne','nw']
self.cbo = ttk.Combobox(self.frame, values=values, foreground='black')
self.cbo.current(0)
self.cbo.pack(fill=tk.X, padx=10, pady=10)
self.titlevar = tk.BooleanVar(value=show_menu_titlebar)
self.show_titlebar = ttk.Checkbutton(self.frame, text="Show titlebar", variable=self.titlevar)
self.show_titlebar.pack(padx=10, pady=10)
self.mb = ttk.Button(self.frame, text='Show Menu', command=self.show_menu)
self.mb.pack(fill=tk.X, padx=10, pady=30)
self.top = tk.Toplevel()
self.top.protocol('WM_DELETE_WINDOW', self.hide_menu)
self.top.state('withdrawn')
self.topframe = ttk.Frame(self.top, padding=5, style='menu.TFrame')
self.topframe.pack(fill=tk.BOTH)
self.option_var = tk.Variable(value=1)
for x in range(5):
b = ttk.Radiobutton(self.topframe, text=f'Option {x+1}', value=x, variable=self.option_var)
b.configure(command=self.hide_menu)
b.grid(row=x, sticky=tk.EW)
def hide_menu(self):
self.top.state('withdrawn')
def show_menu(self):
"""Update the toplevel position and display"""
# calculate offset for window decorations if not overrideredirect
show_titlebar = bool(self.titlevar.get())
if show_titlebar:
self.top.overrideredirect(0)
self.offset_x = self.winfo_x() - self.winfo_rootx()
self.offset_y = self.winfo_y() - self.winfo_rooty()
else:
self.top.overrideredirect(1)
self.offset_x = 0
self.offset_y = 0
self.top.state('normal')
direction = self.cbo.get()
parent_x = self.mb.winfo_rootx()
parent_y = self.mb.winfo_rooty()
parent_height = self.mb.winfo_height()
parent_width = self.mb.winfo_width()
self.update_idletasks()
top_width = self.top.winfo_width()
top_height = self.top.winfo_height()
self.top.focus()
self.top.lift()
if direction == 'wn':
# West-North
xpos = parent_x - top_width
ypos = parent_y
elif direction == 'ws':
# West-South
xpos = parent_x - top_width
ypos = parent_y + parent_height - top_height + self.offset_y
elif direction == 'sw': # this is likely what is needed for option menu
# South-West
xpos = parent_x + self.offset_x
ypos = parent_y + parent_height
elif direction == 'se':
# South-East
xpos = parent_x + parent_width - top_width + self.offset_x
ypos = parent_y + parent_height
elif direction == 'es':
# West-South
xpos = parent_x + parent_width + self.offset_x
ypos = parent_y + parent_height - top_height + self.offset_y
elif direction == 'en':
# East-North
xpos = parent_x + parent_width + self.offset_x
ypos = parent_y
elif direction == 'ne':
# North-East
xpos = parent_x + parent_width - top_width + self.offset_x
ypos = parent_y - top_height + self.offset_y
elif direction == 'nw':
# North-West
xpos = parent_x + self.offset_x
ypos = parent_y - top_height + self.offset_y
self.top.geometry(f'+{xpos}+{ypos}')
if __name__ == '__main__':
app = Application(show_menu_titlebar=False)
app.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment