TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
11.58k stars 1.08k forks source link

CTkToplevel created behind previous windows #1219

Open JeanSolagnier opened 1 year ago

JeanSolagnier commented 1 year ago

Trying to create a fullscreen application however, any new top levels are created behind the current window. so they cant be accessed.

Even in the original CTkToplevel example code, the new window pops up behind the main window.

Is there a way to have the new windows created be the visible one?

methods tried: .attributes('-topmost', True) .lift() .focus() .focus_force()

customtkinter: 5.1.2 OS: Windows 11

import customtkinter

class ToplevelWindow1(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow1")
        self.label.pack(padx=20, pady=20)

        self.button_3 = customtkinter.CTkButton(self, text="open toplevel3", command=self.open_toplevel3)
        self.button_3.pack(side="top", padx=20, pady=20)

        self.toplevel_window3 = None

    def open_toplevel3(self):
        if self.toplevel_window3 is None or not self.toplevel_window3.winfo_exists():
            self.toplevel_window3 = ToplevelWindow3(self)  # create window if its None or destroyed
        else:
            self.toplevel_window3.focus()  # if window exists focus it

class ToplevelWindow2(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow2")
        self.label.pack(padx=20, pady=20)

class ToplevelWindow3(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)
        #self.attributes("-topmost", True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow3")
        self.label.pack(padx=20, pady=20)

        self.button_4 = customtkinter.CTkButton(self, text="back", command=self.destroy)
        self.button_4.pack(side="top", padx=20, pady=20)

class App(customtkinter.CTk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.button_1 = customtkinter.CTkButton(self, text="open toplevel1", command=self.open_toplevel1)
        self.button_1.pack(side="top", padx=20, pady=20)

        self.toplevel_window1 = None

        self.button_2 = customtkinter.CTkButton(self, text="open toplevel2", command=self.open_toplevel2)
        self.button_2.pack(side="top", padx=20, pady=20)

        self.toplevel_window2 = None

    def open_toplevel1(self):
        if self.toplevel_window1 is None or not self.toplevel_window1.winfo_exists():
            self.toplevel_window1 = ToplevelWindow2(self)  # create window if its None or destroyed
        else:
            self.toplevel_window1.focus()  # if window exists focus it

    def open_toplevel2(self):
        if self.toplevel_window2 is None or not self.toplevel_window2.winfo_exists():
            self.toplevel_window2 = ToplevelWindow2(self)  # create window if its None or destroyed
        else:
            self.toplevel_window2.focus()  # if window exists focus it

if __name__ == "__main__":
    app = App()
    app.mainloop()
Wolf-SO commented 1 year ago

I can confirm the issue (Windows 10, Python 3.11, CustomTkinter 5.1.2).

(In my opinion, it would definitively help to reduce the code to just one secondary top-level window that opens behind the primary window.)

Wolf-SO commented 1 year ago

@TomSchimansky Duplicate of (part of) #1198.

ElectricCandlelight commented 1 year ago

Janky way of forcing it to the top


import customtkinter

app = customtkinter.CTk()
app.attributes('-fullscreen', True)

def open_window():
    new_window=customtkinter.CTkToplevel()
    new_window.attributes('-fullscreen', True)
    label=customtkinter.CTkLabel(new_window, text="New Window")
    label.pack()
    new_window.after(20, new_window.lift)

button = customtkinter.CTkButton(app, text="Open New Window", command=open_window)
button.pack()

app.mainloop()
ElectricCandlelight commented 1 year ago

@JeanSolagnier Using the example you posted


import customtkinter

class ToplevelWindow1(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow1")
        self.label.pack(padx=20, pady=20)

        self.button_3 = customtkinter.CTkButton(self, text="open toplevel3", command=self.open_toplevel3)
        self.button_3.pack(side="top", padx=20, pady=20)

        self.toplevel_window3 = None

    def open_toplevel3(self):
        if self.toplevel_window3 is None or not self.toplevel_window3.winfo_exists():
            self.toplevel_window3 = ToplevelWindow3(self)  # create window if its None or destroyed
            self.toplevel_window3.after(20, self.toplevel_window3.lift)
        else:
            self.toplevel_window3.focus()  # if window exists focus it

class ToplevelWindow2(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow2")
        self.label.pack(padx=20, pady=20)

class ToplevelWindow3(customtkinter.CTkToplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)
        #self.attributes("-topmost", True)

        self.label = customtkinter.CTkLabel(self, text="ToplevelWindow3")
        self.label.pack(padx=20, pady=20)

        self.button_4 = customtkinter.CTkButton(self, text="back", command=self.destroy)
        self.button_4.pack(side="top", padx=20, pady=20)

class App(customtkinter.CTk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.attributes('-fullscreen', True)

        self.button_1 = customtkinter.CTkButton(self, text="open toplevel1", command=self.open_toplevel1)
        self.button_1.pack(side="top", padx=20, pady=20)

        self.toplevel_window1 = None

        self.button_2 = customtkinter.CTkButton(self, text="open toplevel2", command=self.open_toplevel2)
        self.button_2.pack(side="top", padx=20, pady=20)

        self.toplevel_window2 = None

    def open_toplevel1(self):
        if self.toplevel_window1 is None or not self.toplevel_window1.winfo_exists():
            self.toplevel_window1 = ToplevelWindow1(self)  # create window if its None or destroyed
            self.toplevel_window1.after(20, self.toplevel_window1.lift)
        else:
            self.toplevel_window1.focus()  # if window exists focus it

    def open_toplevel2(self):
        if self.toplevel_window2 is None or not self.toplevel_window2.winfo_exists():
            self.toplevel_window2 = ToplevelWindow2(self)  # create window if its None or destroyed
            self.toplevel_window2.after(20, self.toplevel_window2.lift)
        else:
            self.toplevel_window2.focus()  # if window exists focus it

if __name__ == "__main__":
    app = App()
    app.mainloop()
JeanSolagnier commented 1 year ago

Hi, @ElectricCandlelight even though it could be janky but it works, thank you! I've already tried .lift() however it does not work if it's not delayed.

Wolf-SO commented 1 year ago

I can confirm the problem and the workaround for Windows 10 as well. The same can be observed for windows without fullscreen (i.e. replacing self.attributes('-fullscreen', True) with self.geometry("300x200")).

I also tried the same with Tkinter. Here the toplevel windows are topmost, but - interestingly! - don't have the focus.

The following script includes two "hardcoded" switches customTk and fullscreen. I changed the open_toplevel1 method to what I think works as expected in both, Tkinter and CustomTkinter and this changed things.

import tkinter
import customtkinter

customTk = False
fullscreen = True

if customTk:
    Toplevel = customtkinter.CTkToplevel
    Label = customtkinter.CTkLabel
    Button = customtkinter.CTkButton
    Tk = customtkinter.CTk
else:    
    Toplevel = tkinter.Toplevel
    Label = tkinter.Label
    Button = tkinter.Button
    Tk = tkinter.Tk

def fullscreen_option(widget):
    if fullscreen:
        widget.attributes('-fullscreen', True)
    else:
        widget.geometry("300x200")

class ToplevelWindow1(Toplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fullscreen_option(self)

        self.label = Label(self, text="ToplevelWindow1")
        self.label.pack(padx=20, pady=20)

        self.button_3 = Button(self, text="open toplevel3", command=self.open_toplevel3)
        self.button_3.pack(side="top", padx=20, pady=20)

        self.toplevel_window3 = None

    def open_toplevel3(self):
        if self.toplevel_window3 is None or not self.toplevel_window3.winfo_exists():
            self.toplevel_window3 = ToplevelWindow3(self)  # create window if its None or destroyed
        else:
            self.toplevel_window3.focus()  # if window exists focus it

class ToplevelWindow2(Toplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fullscreen_option(self)

        self.label = Label(self, text="ToplevelWindow2")
        self.label.pack(padx=20, pady=20)

class ToplevelWindow3(Toplevel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fullscreen_option(self)

        self.label = Label(self, text="ToplevelWindow3")
        self.label.pack(padx=20, pady=20)

        self.button_4 = Button(self, text="back", command=self.destroy)
        self.button_4.pack(side="top", padx=20, pady=20)

class App(Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        fullscreen_option(self)

        self.button_1 = Button(self, text="open toplevel1", command=self.open_toplevel1)
        self.button_1.pack(side="top", padx=20, pady=20)

        self.toplevel_window1 = None

        self.button_2 = Button(self, text="open toplevel2", command=self.open_toplevel2)
        self.button_2.pack(side="top", padx=20, pady=20)

        self.toplevel_window2 = None

    def open_toplevel1(self):
        if self.toplevel_window1 is None or not self.toplevel_window1.winfo_exists():
            self.toplevel_window1 = ToplevelWindow1(self)  # create window if its None or destroyed
        self.toplevel_window1.focus()  # if window exists focus it

    def open_toplevel2(self):
        if self.toplevel_window2 is None or not self.toplevel_window2.winfo_exists():
            self.toplevel_window2 = ToplevelWindow2(self)  # create window if its None or destroyed
        else:
            self.toplevel_window2.focus()  # if window exists focus it

if __name__ == "__main__":
    app = App()
    app.mainloop()

My conclusion: always focus() a newly created top-level window.

Check out the difference between open_toplevel1 and button open_toplevel2 (as can be observed running above script):

After all, I'm not sure if this issue should be considered valid or should be reported to CPython or Tk.

RunsOnRum commented 1 year ago

EDIT: My apologies, this only seems to work in the IDE, once compiled using pyinstaller, it no longer helps.

This has started happening between version 5.0.5 and 5.1.0 of CustomTkinter as I can reproduce the situation by going between those two versions.

Lines 275 to 277 in ctk_toplevel.py version 5.1.0. Second line is the cause.

if self.focused_widget_before_widthdraw is not None:
    self.after(10, self.focused_widget_before_widthdraw.focus)
    self.focused_widget_before_widthdraw = None
onlyforpeace commented 1 year ago

hello, I have a problem with my script, i've got a main window with a buton that call a progress ba on a separate threading, but i have the same probleme, no focus on the progress bar window with tkinter, and progress bar window behind with customtkinter... i tried lift() and focus method, but no result...

import time
import tkinter as tk
import customtkinter as ctk
import datetime
import threading
from random import randint

today=datetime.datetime.now().strftime("%d/%m/%Y")
last_year=(datetime.datetime.now()-datetime.timedelta(days=365)).strftime("%d/%m/%Y")

color='#c2b0ff'

# class ProgressWindow(ctk.CTkToplevel):
class ProgressWindow(tk.Toplevel):
    def __init__(self, master):
        super().__init__(master)
        self.title("Traitement en cours.")
        self.geometry("300x100")

        # self.progressbar = ttk.Progressbar(self, orient="horizontal", length=200, mode="determinate")
        self.progressbar=ctk.CTkProgressBar(self, orientation="horizontal", height=30,width=200, mode="determinate")
        self.progressbar.set(value=0)
        self.progressbar.pack(pady=20)

        self.label = ctk.CTkLabel(self, text="En attente du traitement...")
        self.label.pack()

    def set_progress(self, value):
        self.progressbar.set(value = value)

    def set_label(self, text):
        self.label.configure(text=text)

class MainApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Analyse Gestion Projet")
        self.create_widgets()

    def create_widgets(self):
        self.date_frame = ctk.CTkFrame(self,fg_color=color)
        self.date_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        self.validate_btn = ctk.CTkButton(self.date_frame, text="Valider", command=self.start_thread)
        self.validate_btn.grid(row=4,column=0,columnspan=2,padx=5,pady=5,stick='we')

        self.close_btn= ctk.CTkButton(self.date_frame, text="Fermer", command=self.close)
        self.close_btn.grid(row=4,column=2,columnspan=2,padx=5,pady=5,stick='we')

        self.info_label = ctk.CTkLabel(self.date_frame, text="Attente du lancement de l'analyse")
        self.info_label.grid(row=5,column=0,columnspan=4,padx=5,pady=5,stick='we')

        self.list_log=[]

    def close(self):
        self.destroy()

    def valider(self):
        self.date_frame.configure(fg_color='red')
        self.update()
        print('lancement de l\'analyse')
        time.sleep(5)
        self.date_frame.configure(fg_color=color)
        self.update()

    def start_thread(self):
        self.validate_btn.configure(state='disabled')
        self.date_frame.configure(fg_color='red')
        self.update()
        self.progress_window = ProgressWindow(self)
        self.thread = threading.Thread(target=self.long_running_process, args=(self.progress_window,))
        self.thread.start()
        self.after(50, self.check_thread)

    def long_running_process(self, progress_window):
        a=randint(1, 5)
        len=3
        text=''
        for i in range(1, len+1):
            b=randint(1, 5)
            tps=a+b
            progress_window.set_label(f"Traitement du fichier {i}/{len}, durée = {tps}s...")
            self.analyse(tps,i,len)
            progress_window.set_progress(i/len)

    def analyse(self, tps,i,len):
        time.sleep(tps)
        self.list_log.append(f"Traitement du fichier {i}/{len} terminé avec succès")
        print(self.list_log)

    def check_thread(self):
        text=""
        if self.thread.is_alive():
            self.after(100, self.check_thread)
        else:
            self.progress_window.destroy()
            self.validate_btn.configure(state='normal')
            self.date_frame.configure(fg_color=color)
            self.info_label.configure(text="Traitement terminé!")
            self.update()
            for log in self.list_log:
                text+=log+'\n'
            self.info_label.configure(text=text)

if __name__ == "__main__":
    app = MainApp()
    app.mainloop()

thank you for your help...

oldflexer commented 1 year ago

The reason is a bug in the "TopLevel" class that sets the default icon after 200ms. A simple workaround is to use the "after" method with a delay of 200ms or more: self.after(200, lambda: self.iconbitmap("path_to_icon"))

onlyforpeace commented 1 year ago

thank you for the answer, but where can i write this line? in the init function of my ProgressWindow class?