John Rey Vilbar
John Rey Vilbar

Reputation: 140

Tkinter make overrideredirect window appear on top of other windows when clicked from taskbar

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:

Custom Tkinter window

Upvotes: 1

Views: 2709

Answers (2)

Tom Smith
Tom Smith

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

John Rey Vilbar
John Rey Vilbar

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:

  • No random taskbar icon glitches when minimizing/maximizing (results from wrapping invisible window)
  • No issues when bringing up the custom Tkinter window when behind other windows (like that of my problem of double-clicking the taskbar icon)
#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:

Window demonstration

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

Related Questions