Reputation: 140
How to display a custom Tkinter window (with overrideredirect
true) on top of other opened windows when clicking from the taskbar icon? My code below works (maximize not implemented yet) except when this Tkinter window is overlapped by other non-Tkinter windows. For instance, when this Tkinter window is situated below two windows (of other programs) and when called (by clicking the taskbar icon), it will take 2 clicks on that icon before this window will appear on top of those 2 windows. I want to bring my window automatically on top when its taskbar icon clicked.
I based my code from this answer and stitched this workaround for taskbar icon. I tried searching for any Tkinter code that deals on taskbar icon click event but found nothing.
import tkinter as tk
from tkinter import ttk
def get_pos(event):
global xwin
global ywin
xwin = event.x
ywin = event.y
def move_window(event):
root.geometry(f'+{event.x_root - xwin}+{event.y_root - ywin}')
def quit():
root.destroy()
#window contents
root = tk.Tk()
container = tk.Toplevel(root)
root.overrideredirect(True)
#default window dimension
root.geometry('350x150+200+200')
root.minsize(350, 150)
container.attributes("-alpha",0.0)
back_ground = "#2c2c2c"
#minimize btn binding
def onRootIconify(event): root.withdraw()
container.bind("<Unmap>", onRootIconify)
root.bind("<Unmap>", onRootIconify)
def onRootDeiconify(event): root.deiconify()
container.bind("<Map>", onRootDeiconify)
root.bind("<Map>", onRootDeiconify)
#title bar
title_bar = tk.Frame(root, bg=back_ground, bd=1,
highlightcolor=back_ground,
highlightthickness=0)
#minimize btn
minimize_btn = tk.Button(title_bar, text='🗕', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=lambda: container.wm_state('iconic'))
#maximize btn
maximize_btn = tk.Button(title_bar, text='🗖', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=None)
#close btn
close_button = tk.Button(title_bar, text='🗙', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command= quit)
#window title
title_window = "Untitled window"
title_name = tk.Label(title_bar, text=title_window, font="Arial 12", bg=back_ground, fg="white")
#main area of the window
window = tk.Frame(root, bg="white", highlightthickness=1, highlightbackground=back_ground)
txt = tk.Label(window, bg='white', text="Prototype window").pack(anchor="center")
# pack the widgets
title_bar.pack(fill='x', side=tk.TOP)
title_name.pack(side='left', padx=5)
close_button.pack(side='right')
maximize_btn.pack(side=tk.RIGHT)
minimize_btn.pack(side=tk.RIGHT)
window.pack(fill='both', expand=True, side=tk.TOP)
# bind title bar motion to the move window function
title_bar.bind("<B1-Motion>", move_window)
title_bar.bind("<Button-1>", get_pos)
#workaround to enable window dragging on window title text
title_name.bind("<B1-Motion>", move_window)
title_name.bind("<Button-1>", get_pos)
minimize_btn.bind('<Enter>', lambda x: minimize_btn.configure(bg='#777777'))
minimize_btn.bind('<Leave>', lambda x: minimize_btn.configure(bg=back_ground))
maximize_btn.bind('<Enter>', lambda x: maximize_btn.configure(bg='#777777'))
maximize_btn.bind('<Leave>', lambda x: maximize_btn.configure(bg=back_ground))
close_button.bind('<Enter>', lambda x: close_button.configure(bg='red'))
close_button.bind('<Leave>',lambda x: close_button.configure(bg=back_ground))
root.mainloop()
The custom window:
Upvotes: 1
Views: 2709
Reputation: 41
Expanding on John Rey Vilbar' request/suggestion, here is his implementation wrapped as an Object-Oriented, Tkinter widget. I tried to keep the class as neutral as possible while reflecting the Tkinter widget intuition, as well as John's understanding of dll calls. I welcome any and all concerns/suggestions as I have a few "next step" ideas myself.
Very little, if any modifications to the class should be needed for everyday use. The object is designed in a fashion as to operate like a wrapper with the added benefit of resolving the WM operations.
Thank you John for the very mature coding style/disciplines and the good solution. Pleasure working with you. This has been a little bit of a frustrating pursuit. Though I am happy having found your code.
UPDATE: 10-12-2023: Added functionality to restore previous window size after maximizing
#https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos
#https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
import tkinter as tk
from ctypes import windll
class RootFrame(tk.Frame):
def __init__(self, parent, **kwargs):
self.parent = parent
super().__init__(self.parent, **kwargs)
self.maximized = False
self.hasstyle = False
self.parent.windowSize = [self.parent.winfo_width(),
self.parent.winfo_height()]
for key, val in kwargs.items():
if key == 'highlightbackground':
self.back_ground = val
else:
self.back_ground = "#2c2c2c"
self.parent.withdraw()
self.parent.update()
dims = [int(x) for x in self.parent.geometry().split('+')[0].split('x')]
dimension = (dims[0], dims[1])
x = (self.parent.winfo_screenwidth()/2)-(dimension[0]/2)
y = (self.parent.winfo_screenheight()/2)-250
self.parent.geometry(f'{dimension[0]}x{dimension[1]}+{int(x)}+{int(y)}')
self.parent.minsize(dimension[0], dimension[1])
self.previousPosition = [int(x), int(y)]
self.__ParentFrame__()
self.__events__()
self.loop_control()
'''GUI Private Methods'''
def __events__(self):
self.title_bar.bind('<Double-1>', self.maximizeToggle)
self.title_name.bind('<Double-1>', self.maximizeToggle)
self.minimize_btn.bind('<Enter>', lambda x: self.minimize_btn.configure(bg='#777777'))
self.minimize_btn.bind('<Leave>', lambda x: self.minimize_btn.configure(bg=self.back_ground))
self.maximize_btn.bind('<Enter>', lambda x: self.maximize_btn.configure(bg='#777777'))
self.maximize_btn.bind('<Leave>', lambda x: self.maximize_btn.configure(bg=self.back_ground))
self.close_button.bind('<Enter>', lambda x: self.close_button.configure(bg='red'))
self.close_button.bind('<Leave>', lambda x: self.close_button.configure(bg=self.back_ground))
def __ParentFrame__(self):
self.parent.overrideredirect(True)
#title bar
self.title_bar = tk.Frame(self.parent, bg=self.back_ground, bd=1,
highlightcolor=self.back_ground,
highlightthickness=0)
#window title
self.title_window = "Untitled window"
self.title_name = tk.Label(self.title_bar, text=self.title_window,
font="Arial 12", bg=self.back_ground, fg="white")
#minimize btn
self.minimize_btn = tk.Button(self.title_bar, text='🗕', bg=self.back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=self.minimize)
#maximize btn
self.maximize_btn = tk.Button(self.title_bar, text='🗖', bg=self.back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=self.maximizeToggle)
#close btn
self.close_button = tk.Button(self.title_bar, text='🗙', bg=self.back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command= quit)
# pack the widgets
self.title_bar.pack(fill='x', side=tk.TOP)
self.title_name.pack(side='left', padx=5)
self.close_button.pack(side='right')
self.maximize_btn.pack(side=tk.RIGHT)
self.minimize_btn.pack(side=tk.RIGHT)
self.move_window_bindings(status=True)
'''Functional Public Methods'''
def get_pos(self, event):
self.xwin = event.x
self.ywin = event.y
def loop_control(self):
self.parent.update_idletasks()
self.parent.withdraw()
self.set_appwindow()
def maximizeToggle(self, event=None):
if self.maximized == False:
self.winfo_update()
#maximize current window
self.maximize_btn.config(text="❐")
hwnd = windll.user32.GetParent(self.parent.winfo_id())
SWP_SHOWWINDOW = 0x40
windll.user32.SetWindowPos(hwnd, 0, 0, 0,
int(self.parent.winfo_screenwidth()),
int(self.parent.winfo_screenheight()-48),
SWP_SHOWWINDOW)
self.maximized = True
self.move_window_bindings(status=False)
else:
#restore down window
self.maximize_btn.config(text="🗖")
hwnd = windll.user32.GetParent(self.parent.winfo_id())
SWP_SHOWWINDOW = 0x40
windll.user32.SetWindowPos(hwnd, 0,
self.previousPosition[0],
self.previousPosition[1],
int(self.parent.windowSize[0]),
int(self.parent.windowSize[1]),
SWP_SHOWWINDOW)
self.maximized = False
self.move_window_bindings(status=True)
def minimize(self, hide=False):
#reference: https://programtalk.com/python-examples/ctypes.windll.user32.ShowWindow/
hwnd = windll.user32.GetParent(self.parent.winfo_id())
windll.user32.ShowWindow(hwnd, 0 if hide else 6)
def move_window(self, event):
self.parent.geometry(f'+{event.x_root - self.xwin}+{event.y_root - self.ywin}')
self.previousPosition = [self.parent.winfo_x(), self.parent.winfo_y()]
def move_window_bindings(self, *args, status=True):
if status == True:
self.title_bar.bind("<B1-Motion>", self.move_window)
self.title_bar.bind("<Button-1>", self.get_pos)
self.title_name.bind("<B1-Motion>", self.move_window)
self.title_name.bind("<Button-1>", self.get_pos)
else:
self.title_bar.unbind("<B1-Motion>")
self.title_bar.unbind("<Button-1>")
self.title_name.unbind("<B1-Motion>")
self.title_name.unbind("<Button-1>")
def quit(self):
self.parent.destroy()
def set_appwindow(self):
GWL_EXSTYLE=-20
WS_EX_APPWINDOW=0x00040000
WS_EX_TOOLWINDOW=0x00000080
if not self.hasstyle:
hwnd = windll.user32.GetParent(self.parent.winfo_id())
style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
style = style & ~WS_EX_TOOLWINDOW
style = style | WS_EX_APPWINDOW
res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
self.parent.withdraw()
self.parent.after(10, lambda:self.parent.wm_deiconify())
self.hasstyle=True
def winfo_update(self):
"""Update geometry() information, return None"""
self.parent.windowSize = [self.parent.winfo_width(),
self.parent.winfo_height()]
if __name__ == '__main__':
root = tk.Tk()
root.geometry("300x300")
root_frame = RootFrame(root, bg="white", highlightthickness=1, highlightbackground="#2c2c2c")
root_frame.pack(fill=tk.BOTH, expand=True)
txt = tk.Label(root_frame, bg='white', text="Prototype window")
txt.pack(anchor="center")
def resize():
root.geometry("500x650")
rsz = tk.Button(root_frame, text="Resize", command=resize)
rsz.pack()
root.mainloop()
Upvotes: 2
Reputation: 140
After looking for related answers and recommendations of Coder, I finally solved my problem. In my solution, I used the technique from this answer. My finished code does not use any invisible Tkinter window and heavily utilizes ctypes.windll
(hence my code is only limited to Windows). The logic for minimize, maximize, and close window are finished as well.
I managed to solve the following:
#https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos
#https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
import tkinter as tk
from tkinter import ttk
from ctypes import windll
def set_appwindow():
global hasstyle
GWL_EXSTYLE=-20
WS_EX_APPWINDOW=0x00040000
WS_EX_TOOLWINDOW=0x00000080
if not hasstyle:
hwnd = windll.user32.GetParent(root.winfo_id())
style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
style = style & ~WS_EX_TOOLWINDOW
style = style | WS_EX_APPWINDOW
res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
root.withdraw()
root.after(10, lambda:root.wm_deiconify())
hasstyle=True
def get_pos(event):
global xwin
global ywin
xwin = event.x
ywin = event.y
def move_window(event):
global previousPosition
root.geometry(f'+{event.x_root - xwin}+{event.y_root - ywin}')
previousPosition = [root.winfo_x(), root.winfo_y()]
def move_window_bindings(*args, status=True):
if status == True:
title_bar.bind("<B1-Motion>", move_window)
title_bar.bind("<Button-1>", get_pos)
title_name.bind("<B1-Motion>", move_window)
title_name.bind("<Button-1>", get_pos)
else:
title_bar.unbind("<B1-Motion>")
title_bar.unbind("<Button-1>")
title_name.unbind("<B1-Motion>")
title_name.unbind("<Button-1>")
def quit():
root.destroy()
#reference: https://programtalk.com/python-examples/ctypes.windll.user32.ShowWindow/
def minimize(hide=False):
hwnd = windll.user32.GetParent(root.winfo_id())
windll.user32.ShowWindow(hwnd, 0 if hide else 6)
def maximizeToggle():
global maximized
global previousPosition
if maximized == False:
#maximize current window
maximize_btn.config(text="❐")
hwnd = windll.user32.GetParent(root.winfo_id())
SWP_SHOWWINDOW = 0x40
windll.user32.SetWindowPos(hwnd, 0, 0, 0, int(root.winfo_screenwidth()), int(root.winfo_screenheight()-48),SWP_SHOWWINDOW)
maximized = True
move_window_bindings(status=False)
else:
#restore down window
maximize_btn.config(text="🗖")
hwnd = windll.user32.GetParent(root.winfo_id())
SWP_SHOWWINDOW = 0x40
windll.user32.SetWindowPos(hwnd, 0, previousPosition[0], previousPosition[1], int(root.minsize()[0]), int(root.minsize()[1]),SWP_SHOWWINDOW)
maximized = False
move_window_bindings(status=True)
#---------------------------------
root = tk.Tk()
root.overrideredirect(True)
#window details
maximized = False
back_ground = "#2c2c2c"
dimension = (300, 300)
#------------------------------
if len(dimension) == 0:
#default window dimension
x = (root.winfo_screenwidth()/2)-(350/2)
y = (root.winfo_screenheight()/2)-(250)
root.geometry(f'350x150+{int(x)}+{int(y)}')
root.minsize(350, 150)
dimension = (350, 150)
previousPosition = [int(x), int(y)]
else:
x = (root.winfo_screenwidth()/2)-(dimension[0]/2)
y = (root.winfo_screenheight()/2)-250
root.geometry(f'{dimension[0]}x{dimension[1]}+{int(x)}+{int(y)}')
root.minsize(dimension[0], dimension[1])
previousPosition = [int(x), int(y)]
#title bar
title_bar = tk.Frame(root, bg=back_ground, bd=1,
highlightcolor=back_ground,
highlightthickness=0)
#window title
title_window = "Untitled window"
title_name = tk.Label(title_bar, text=title_window,
font="Arial 12", bg=back_ground, fg="white")
#minimize btn
minimize_btn = tk.Button(title_bar, text='🗕', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=minimize)
#maximize btn
maximize_btn = tk.Button(title_bar, text='🗖', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command=maximizeToggle)
#close btn
close_button = tk.Button(title_bar, text='🗙', bg=back_ground, padx=5, pady=2,
bd=0, font="bold", fg='white', width=2,
activebackground="red",
activeforeground="white",
highlightthickness=0,
command= quit)
#hover effect
minimize_btn.bind('<Enter>', lambda x: minimize_btn.configure(bg='#777777'))
minimize_btn.bind('<Leave>', lambda x: minimize_btn.configure(bg=back_ground))
maximize_btn.bind('<Enter>', lambda x: maximize_btn.configure(bg='#777777'))
maximize_btn.bind('<Leave>', lambda x: maximize_btn.configure(bg=back_ground))
close_button.bind('<Enter>', lambda x: close_button.configure(bg='red'))
close_button.bind('<Leave>',lambda x: close_button.configure(bg=back_ground))
#main area of the window
window = tk.Frame(root, bg="white", highlightthickness=1, highlightbackground=back_ground)
txt = tk.Label(window, bg='white', text="Prototype window").pack(anchor="center")
# pack the widgets
title_bar.pack(fill='x', side=tk.TOP)
title_name.pack(side='left', padx=5)
close_button.pack(side='right')
maximize_btn.pack(side=tk.RIGHT)
minimize_btn.pack(side=tk.RIGHT)
window.pack(fill='both', expand=True, side=tk.TOP)
move_window_bindings(status=True)
#ctype
hasstyle = False
root.update_idletasks()
root.withdraw()
set_appwindow()
root.mainloop()
Demo:
If someone can transform my code into a classful syntax, please comment since I have a hard time in doing so hehe. Code optimizations are very welcomed.
Upvotes: 7