I'm writing a Tkinter application in Python 3 and I've created a custom Title Bar (the bar at the top of every Windows application with the application name, close button etc.). The way I achieved this was to create an invisible window (we'll call this window test) that controls the main application window's behavior (which we'll call Main App). Below is a piece of test code that illustrates this:
from tkinter import Tk, Toplevel
from tkinter.ttk import Button, Label, Frame, Style
class NewRoot(Tk):
def __init__(self):
Tk.__init__(self)
self.attributes('-alpha', 1.0) # This is normally set to 0.0
class MyMain(Toplevel):
def __init__(self, master):
Toplevel.__init__(self, master)
self.overrideredirect(1)
self.geometry('750x650+400+600')
self.style = Style()
self.style.configure('TTitleBar.Label',
width=8,
relief='flat',
foreground='red',
background='red',
anchor='center',
borderwidth=-1)
self.style.configure('TMainWindow.Label',
width=8,
relief='flat',
background='blue',
anchor='center',
borderwidth=-1)
self.tk_setPalette(background='green')
self.x_win = None
self.y_win = None
self.start_x = None
self.start_y = None
# make a frame for the title bar
title_bar = Frame(self, style='TTitleBar.Label')
title_bar.grid(row=0, column=0, sticky='wn')
label = Label(title_bar, text='Main App', style='TMainWindow.Label')
label.grid(row=0, column=0, padx=(4, 2), pady=(4, 0), sticky='nw')
minimize_button = Button(title_bar, text='MIN', command=self.minimize_window,
style='TMainWindow.Label', takefocus=False)
minimize_button.grid(row=0, column=2, padx=(563.5, 0), sticky='nw')
maximise_button = Button(title_bar, text='MAX', command=self.maximize_window,
style='TMainWindow.Label', takefocus=False)
maximise_button.grid(row=0, column=3, pady=(1.4, 0), sticky='nw')
close_button = Button(title_bar, text='CLOSE', command=self.close_window,
style='TMainWindow.Label', takefocus=False)
close_button.grid(row=0, column=4, sticky='nw')
window = Frame(self)
window.grid(row=1, column=0, sticky='ne')
# bind title bar motion to the move window function
title_bar.bind('<B1-Motion>', self.move_window)
title_bar.bind('<Button-1>', self.get_pos)
self.master.bind("<Map>", self.on_root_deiconify)
self.master.bind("<Unmap>", self.on_root_iconify)
self.mainloop()
def minimize_window(self):
self.master.iconify()
def maximize_window(self):
pass
def close_window(self):
self.master.destroy()
def on_root_iconify(self, event):
# print('unmap')
self.withdraw()
def on_root_deiconify(self, event):
# print('map')
self.deiconify()
def get_pos(self, event):
self.x_win = self.winfo_x()
self.y_win = self.winfo_y()
self.start_x = event.x_root
self.start_y = event.y_root
self.y_win = self.y_win - self.start_y
self.x_win = self.x_win - self.start_x
def move_window(self, event):
# print('+{0}+{1}'.format(event.x_root, event.y_root))
self.geometry('+{0}+{1}'.format(event.x_root + self.x_win, event.y_root + self.y_win))
self.start_x = event.x_root
self.start_y = event.y_root
if __name__ == '__main__':
root = NewRoot()
root.title('test')
app = MyMain(root)
In the code above, whenever the test window is minimized, the Main App window is also minimized, which works as intended.
The problem is that whenever the test window is made active, the Main App window doesn't become active also. For example, if another app covers Main App but test is not minimized, I need to click on the test icon in the Windows Task Bar three times for it to appear.
I was wondering if there is a way to fix this using something like:
self.master.bind(<some_command>, self.some_other_command)
However, I can't find a comprehensive list of bind commands anywhere.
Is this a good way of going about this, or is there something else I should be doing?
Also, I noticed that using self.overrideredirect(1) causes the shadows made by the windows to disappear, which causes overlapping windows in my application to 'merge together', since the background colors are the same. Is there a way to add the shadows back?
Thank you in advance.
I found a solution to this for anyone else with a similar problem. You can create a 'dummy' button in the invisible window that will become active when the window is in focus. You then have that call a function that places the main application window in focus.
class NewRoot(Tk):
def __init__(self):
Tk.__init__(self)
self.attributes('-alpha', 0.0)
entry = Button()
entry.pack()
entry.focus_set()
entry.pack_forget()
Then add this to __init__ in the MyMain class:
self.master.bind('<FocusIn>', self.on_root_deiconify)
Related
I am using Tkinter for building an app and i need scrollable windows. I created a scrollable container using Canvas: ScrollContainer . After this I incorporated the main logic of my program into this container where I put a button that opens another separate TopLevel window. This separate window also has to be scrollable . Therefore i also include it in the same container class
Now, the problem: When I run the program my main window scrolls fine. I open the TopLevel window after clicking the button. Secondary window scrolls fine. After I close the secondary window and hover the mouse again over the main window, now it doesn't scroll and i get an error in the console:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1883, in __call__
return self.func(*args)
File "C:/Users/mirel.voicu/Desktop/python_projects/TKINTER/standalone_program/test.py", line 45, in _on_mousewheel
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
File "C:\Users\mirel.voicu\Anaconda3\envs\gis385\lib\tkinter\__init__.py", line 1929, in yview_scroll
self.tk.call(self._w, 'yview', 'scroll', number, what)
_tkinter.TclError: invalid command name ".!toplevel.!frame.!canvas"
Note:
Instead of self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel) i also tried with self.my_canvas.bind("<MouseWheel>", self._on_mousewheel) and now there is no error .However, the scrolling changes. You cannot scroll anymore if you hover over the labels. You only can scroll if you enlarge the window and hover mouse a bit on the right . I guess it's because you have to bring the mouse over the canvas as it is the only entity scrollable
ScrollContainer class:
from tkinter import *
from tkinter import ttk
class ScrollContainer (ttk.Frame):
def __init__(self, container,w,h,*args, **kwargs):
super().__init__(container, *args, **kwargs)
# Create a main frame
self.main_frame = Frame(container, width=w, height=h)
self.main_frame.pack(side=TOP,fill=BOTH, expand=1) # expand frame to the size of the container
# create a canvas
self.my_canvas = Canvas(self.main_frame)
self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# add h and v scrollbar to canvas
self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
self.my_vscrollbar.pack(side=RIGHT, fill=Y)
self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
self.my_hscrollbar.pack(side=BOTTOM, fill=X)
# configure canvas
self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))
# create another frame inside the canvas
self.second_frame = Frame(self.my_canvas)
# add that new frame to a window in the canvas
self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')
def _on_mousewheel(self, event):
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
Main program logic:
def open():
w=Toplevel()
SecondContainer=ScrollContainer(w,1000,768)
for thing in range(40):
Label(SecondContainer.second_frame, text=f"It's Friday {thing} ").grid(row=thing, column=0)
root=Tk()
MainContainer=ScrollContainer(root,1000,768)
btn=Button(MainContainer.second_frame, text="New Window",bg='yellow',command=open)
btn.grid(row=0,column=0)
for thing in range(1,30):
Label(MainContainer.second_frame,text=f"It's Friday {thing} ").grid(row=thing,column=0)
# frame design
root.mainloop()
It is because you used bind_all() which is application-wise binding. So the binding in the Toplevel() will override the one in root. When the toplevel is destroyed, the binding function is still referring the canvas in the toplevel and so the exception.
You should use window-wise binding:
class ScrollContainer(ttk.Frame):
def __init__(self, container, w, h, *args, **kwargs):
super().__init__(container, *args, **kwargs)
container.bind("<MouseWheel>", self._on_mousewheel) # bind on the parent window
# Create a main frame
self.main_frame = Frame(container, width=w, height=h)
self.main_frame.pack(side=TOP, fill=BOTH, expand=1) # expand frame to the size of the container
# create a canvas
self.my_canvas = Canvas(self.main_frame)
self.my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
#self.my_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# add h and v scrollbar to canvas
self.my_vscrollbar = ttk.Scrollbar(self.main_frame, orient=VERTICAL, command=self.my_canvas.yview)
self.my_vscrollbar.pack(side=RIGHT, fill=Y)
self.my_hscrollbar = ttk.Scrollbar(container, orient=HORIZONTAL, command=self.my_canvas.xview)
self.my_hscrollbar.pack(side=BOTTOM, fill=X)
# configure canvas
self.my_canvas.configure(yscrollcommand=self.my_vscrollbar.set, xscrollcommand=self.my_hscrollbar.set)
self.my_canvas.bind('<Configure>', lambda e: self.my_canvas.configure(scrollregion=self.my_canvas.bbox('all')))
# create another frame inside the canvas
self.second_frame = Frame(self.my_canvas)
# add that new frame to a window in the canvas
self.my_canvas.create_window((0, 0), window=self.second_frame, anchor='nw')
def _on_mousewheel(self, event):
self.my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
I noticed you already solved the problem but the solution #acw1668 gave to you, in my case, worked partially. So, I decided to post here my code in case someone was unlucky just like me.
I used #acw1668 class but modified it a bit.
The replacement of 'bind_all' with 'bind' had the mere effect to deactivate the scroll with the mouse everywhere besides of the scrolling bar zone so I had to define the behaviour of the widget (self.canvas) when the event ('MouseWheel') happens. So I made some searches and I ended up on another question HERE answered by #Saad who explained how to change a widget behaviour when the cursor is over it by means of a new method (set_mousewheel). Then, I putted all together and that's what I got:
class ScrollableFrame(Frame):
"""
Defines a ScrollbarFrame class
"""
def __init__(self, container, width=800, height=700, bg = "white", *args, **kwargs):
super().__init__(container, *args, **kwargs)
# to hide the border
highlightthickness = 0
# Create a main frame
self.main_frame = Frame(container, width=width, height=height, bg=bg)
self.main_frame.pack(side=TOP, fill=BOTH, expand=1) # expand frame to the size of the container
#create a canvas
#highlightthickness = 0 to hide the border
self.canvas = Canvas(self.main_frame, width=width, height=height, bg=bg, highlightthickness=highlightthickness)
self.canvas.pack(side=LEFT, fill=BOTH, expand=1)
self.scrollbar = Scrollbar(self.main_frame, orient=VERTICAL, command=self.canvas.yview)
self.scrollbar.pack(side=RIGHT, fill=Y)
self.scrollbar_x = Scrollbar(container, orient=HORIZONTAL, command=self.canvas.xview)
self.scrollbar_x.pack(side=BOTTOM, fill=X)
self.canvas.configure(yscrollcommand=self.scrollbar.set, xscrollcommand=self.scrollbar_x.set)
self.canvas.bind(
"<Configure>",
lambda e: self.canvas.configure (
scrollregion=self.canvas.bbox("all")
)
)
self.scrollable_frame = Frame(self.canvas, bg=bg)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
#DO NOT PUT bind_all!!!
self.canvas.bind("<MouseWheel>", self._on_mousewheel)
self.canvas.bind("<MouseWheel>", self.set_mousewheel(self.canvas, self._on_mousewheel))
def _on_mousewheel(self, event):
"""
Allowes to scroll the mouse
"""
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def set_mousewheel(self, widget, command):
"""Activate / deactivate mousewheel scrolling when
cursor is over / not over the widget respectively."""
widget.bind("<Enter>", lambda _: widget.bind_all('<MouseWheel>', command))
widget.bind("<Leave>", lambda _: widget.unbind_all('<MouseWheel>'))
I cannot figure out how to use the tag_bind method associated with a canvas. I created an oval and expected to be able to generate an event by clicking on it. When I do this, nothing happens. What is the correct use of tag_bind?
import tkinter as tk
class Window():
def __init__(self, master):
self.master = master
self.create_widgets()
def create_widgets(self):
self.c = tk.Canvas(self.master, bg='white', height=200, width=300)
self.c.pack()
b = tk.Button(self.master, text='Draw', command=self.draw)
b.pack()
def draw(self):
self.c.delete('all')
self.oval = self.c.create_oval([30,50], [130,80])
self.rect = self.c.create_rectangle([180,10], [280,80])
self.c.tag_bind(self.oval, '<Button-1>', self.oval_func)
def oval_func(self, event):
self.c.delete(self.rect)
self.c.create_text(150, 150, text='Hello, world!', anchor='w')
if __name__ == '__main__':
root = tk.Tk()
app = Window(root)
root.mainloop()
The code is working. However, when you bind to a canvas object, you have to click on part of the drawn object. Since you didn't fill the oval, that means you must click on its outline.
If you fill it with the same color as the background (or any other color) you can click anywhere in the oval.
self.oval = self.c.create_oval([30,50], [130,80], fill="white")
I'm trying to create a custom looking dropdown in Tkinter and I decided to go with Toplevel. Everything works as expected except for the focus isn't taking place. When the window gets created, it stays on top of my other windows, but the <Enter>/<Leave> bindings aren't working until I click on the window. Here's a rebuilt example of the exact problem.
import tkinter as tk
from tkinter import ttk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.mainframe = ttk.Frame(self)
self.init_ui()
self.mainloop()
def init_ui(self):
self.mainframe.grid(row=0, column=0, sticky='nsew')
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.mainframe.rowconfigure(0, weight=1)
self.mainframe.columnconfigure(0, weight=1)
l = ttk.Label(self.mainframe, text='Test ▾')
l.config(cursor='hand', font=('Helvetica', 12, 'underline'))
l.bind('<Button-1>', self.dropdown)
l.grid(row=0, column=0, padx=50, pady=(5, 300), sticky='new')
def dropdown(self, *args):
top = tk.Toplevel(self)
top.overrideredirect(1)
top.transient(self)
def create_label(n):
nonlocal top
l = tk.Label(top, text='Test{}'.format(n))
l.config(cursor='hand', relief='ridge')
l.bind('<Enter>', enter_leave)
l.bind('<Leave>', enter_leave)
l.bind('<Button-1>', lambda _: top.destroy())
l.grid(row=n, column=0)
def enter_leave(e):
# 7 = enter
# 8 = leave
if e.type == '7':
e.widget.config(bg='grey')
else:
e.widget.config(bg='white')
# populate some labels
for x in range(9):
create_label(x)
self.update_idletasks()
top_width = top.winfo_width()
top_height = top.winfo_height()
root_x = self.winfo_x()
root_y = self.winfo_y()
top.geometry('{}x{}+{}+{}'.format(
top_width, top_height,
int(root_x + (self.winfo_width() / 2)),
root_y + 50
))
if __name__ == '__main__':
App()
This is what is looks like:
If I take out top.overrideredirect(1) then it works as expected but I get it with the title bar which I don't want:
One other interesting occurrence is when I run self.update_idletasks() right before calling top.overrideredirect(1) then it again works but with a frozen title bar. I'm not sure why that doesn't happen without self.update_idletasks() (this may be a different question):
I have tried a number of combinations of the following with no wanted results:
top.attributes('-topmost', True)
top.lift(aboveThis=self)
top.focus_force()
top.grab_set()
All in all, I would just like to get the "highlighting" effect to work but with the look of image 1 with no title bar. I'm open to all other suggestions or approaches.
I have implemented a splash screen that is shown while my application loads the database from remote cloud storage on startup. The splash screen is kept alive (there's a progressbar on it) with calls to .update() and is destroyed once the separate loading process ends. After this, the mainloop is started and the app runs normally.
The code below used to work fine on my Mac with python 3.6 and tcl/tk 8.5.9. However, after the update to Sierra I was forced to update tk to ActiveTcl 8.5.18. Now, the splash screen is not displayed until the separate process finishes, but then appears and stays on screen together with the root window (even though its .destroy() method is called).
import tkinter as tk
import tkinter.ttk as ttk
import multiprocessing
import time
class SplashScreen(tk.Toplevel):
def __init__(self, root):
tk.Toplevel.__init__(self, root)
self.geometry('375x375')
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.label = ttk.Label(self, text='My Splashscreen', anchor='center')
self.label.grid(column=0, row=0, sticky='nswe')
self.center_splash_screen()
print('initialized splash')
def center_splash_screen(self):
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
x = w / 2 - 375 / 2
y = h / 2 - 375 / 2
self.geometry("%dx%d+%d+%d" % ((375, 375) + (x, y)))
def destroy_splash_screen(self):
self.destroy()
print('destroyed splash')
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.start_up_app()
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = ttk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
def start_up_app(self):
self.show_splash_screen()
# load db in separate process
process_startup = multiprocessing.Process(target=App.startup_process)
process_startup.start()
while process_startup.is_alive():
# print('updating')
self.splash.update()
self.remove_splash_screen()
def show_splash_screen(self):
self.withdraw()
self.splash = SplashScreen(self)
#staticmethod
def startup_process():
# simulate delay while implementation is loading db
time.sleep(5)
def remove_splash_screen(self):
self.splash.destroy_splash_screen()
del self.splash
self.deiconify()
if __name__ == '__main__':
App()
I do not understand why this is happening and how to solve it. Can anybody help? Thanks!
Update:
The splash screen is displayed correctly if you outcomment the line self.overrideredirect(True). However, I don't want window decorations and it still stays on screen at the end of the script. It is being destroyed internally though, any further method calls on self.splash (e.g. .winfo_...-methods) result in _tkinter.TclError: bad window path name ".!splashscreen".
Also, this code works fine under windows and tcl/tk 8.6. Is this a bug/problem with window management of tcl/tk 8.5.18 on Mac?
I came across this while looking for an example on how to make a tkinter splash screen that wasn't time dependent (as most other examples are). Sam's version worked for me as is. I decided to make it an extensible stand-alone class that handles all the logic so it can just be dropped into an existing program:
# Original Stackoverflow thread:
# https://stackoverflow.com/questions/44802456/tkinter-splash-screen-multiprocessing-outside-of-mainloop
import multiprocessing
import tkinter as tk
import functools
class SplashScreen(tk.Toplevel):
def __init__(self, root, **kwargs):
tk.Toplevel.__init__(self, root, **kwargs)
self.root = root
self.elements = {}
root.withdraw()
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Placeholder Vars that can be updated externally to change the status message
self.init_str = tk.StringVar()
self.init_str.set('Loading...')
self.init_int = tk.IntVar()
self.init_float = tk.DoubleVar()
self.init_bool = tk.BooleanVar()
def _position(self, x=.5,y=.5):
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
splash_w = self.winfo_reqwidth()
splash_h = self.winfo_reqheight()
x_loc = (screen_w*x) - (splash_w/2)
y_loc = (screen_h*y) - (splash_h/2)
self.geometry("%dx%d+%d+%d" % ((splash_w, splash_h) + (x_loc, y_loc)))
def update(self, thread_queue=None):
super().update()
if thread_queue and not thread_queue.empty():
new_item = thread_queue.get_nowait()
if new_item and new_item != self.init_str.get():
self.init_str.set(new_item)
def _set_frame(self, frame_funct, slocx=.5, sloxy=.5, ):
"""
Args:
frame_funct: The function that generates the frame
slocx: loction on the screen of the Splash popup
sloxy:
init_status_var: The variable that is connected to the initialization function that can be updated with statuses etc
Returns:
"""
self._position(x=slocx,y=sloxy)
self.frame = frame_funct(self)
self.frame.grid(column=0, row=0, sticky='nswe')
def _start(self):
for e in self.elements:
if hasattr(self.elements[e],'start'):
self.elements[e].start()
#staticmethod
def show(root, frame_funct, function, callback=None, position=None, **kwargs):
"""
Args:
root: The main class that created this SplashScreen
frame_funct: The function used to define the elements in the SplashScreen
function: The function when returns, causes the SplashScreen to self-destruct
callback: (optional) A function that can be called after the SplashScreen self-destructs
position: (optional) The position on the screen as defined by percent of screen coordinates
(.5,.5) = Center of the screen (50%,50%) This is the default if not provided
**kwargs: (optional) options as defined here: https://www.tutorialspoint.com/python/tk_toplevel.htm
Returns:
If there is a callback function, it returns the result of that. Otherwise None
"""
manager = multiprocessing.Manager()
thread_queue = manager.Queue()
process_startup = multiprocessing.Process(target=functools.partial(function,thread_queue=thread_queue))
process_startup.start()
splash = SplashScreen(root=root, **kwargs)
splash._set_frame(frame_funct=frame_funct)
splash._start()
while process_startup.is_alive():
splash.update(thread_queue)
process_startup.terminate()
SplashScreen.remove_splash_screen(splash, root)
if callback: return callback()
return None
#staticmethod
def remove_splash_screen(splash, root):
splash.destroy()
del splash
root.deiconify()
class Screen(tk.Frame):
# Options screen constructor class
def __init__(self, parent):
tk.Frame.__init__(self, master=parent)
self.grid(column=0, row=0, sticky='nsew')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
### Demo ###
import time
def splash_window_constructor(parent):
"""
Function that takes a parent and returns a frame
"""
screen = SplashScreen.Screen(parent)
label = tk.Label(screen, text='My Splashscreen', anchor='center')
label.grid(column=0, row=0, sticky='nswe')
# Connects to the tk.StringVar so we can updated while the startup process is running
label = tk.Label(screen, textvariable=parent.init_str, anchor='center')
label.grid(column=0, row=1, sticky='nswe')
return screen
def startup_process(thread_queue):
# Just a fun method to simulate loading processes
startup_messages = ["Reticulating Splines","Calculating Llama Trajectory","Setting Universal Physical Constants","Updating [Redacted]","Perturbing Matrices","Gathering Particle Sources"]
r = 10
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Loading database.{'.'*n}".ljust(27))
time.sleep(1)
for n in startup_messages:
thread_queue.put_nowait(n)
time.sleep(.2)
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Almost Done.{'.'*n}".ljust(27))
for n in range(r):
time.sleep(.5)
thread_queue.put_nowait("Almost Done..........".ljust(27))
time.sleep(.5)
thread_queue.put_nowait("Almost Done......... ".ljust(27))
def callback(text):
# To be run after the splash screen completes
print(text)
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.callback_return = SplashScreen.show(root=self,
frame_funct=splash_window_constructor,
function=startup_process,
callback=functools.partial(callback,"Callback Done"))
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = tk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
if __name__ == "__main__":
App()
Apparently this is due to a problem with the window stacking order when windows are not decorated by the window manager after calling overrideredirect(True). It seems to have occurred on other platforms as well.
Running the following code on macOS 10.12.5 with Python 3.6.1 and tcl/tk 8.5.18, toplevel windows do not appear after the button 'open' is clicked:
import tkinter as tk
class TL(tk.Toplevel):
def __init__(self):
tk.Toplevel.__init__(self)
self.overrideredirect(True)
# self.after_idle(self.lift)
tl_label = tk.Label(self, text='this is a undecorated\ntoplevel window')
tl_label.grid(row=0)
b_close = tk.Button(self, text='close', command=self.close)
b_close.grid(row=1)
def close(self):
self.destroy()
def open():
TL()
root = tk.Tk()
label = tk.Label(root, text='This is the root')
label.grid(row=0)
b_open = tk.Button(root, text='open', command=open)
b_open.grid(row=1)
root.mainloop()
Uncommenting the line self.after_idle(self.lift) fixes the problem (simply calling self.lift() does too. But using after_idle()prevents the window from flashing up for a fraction of a second before it is moved to its position and resized, which is another problem I have experienced repeatedly with tkinter and keeps me wondering whether I should move on to learn PyQT or PySide2...).
As to the problem with closing an undecorated window in my original question: calling after_idle(window.destroy()) instead of window.destroy() seems to fix that too. I do not understand why.
In case other people reproduce this and somebody hints me towards where to report this as a bug, I am happy to do so.
Is there a way to tell Tkinter that I want some widget to always remain focused? I've created a minimal example that can be run to show my issue , here's an example window with small toplevel windows also overlayed:
Now if I click the upper title tk, the main window comes into focus and suddenly the small windows are behind the main window
I want to treat these smaller windows as if they are always in focus until the user specifically closes them. Of course this is a minimal example that is an idea behind a small subsection of my large application , is there any easy setting I can use for the toplevel that guarantees it will always remain in focus regardless of other windows? Here's the actual code that can be run to replicate this:
from Tkinter import *
class PropertyDialog(Toplevel):
def __init__(self, root, string):
Toplevel.__init__(self)
self.wm_overrideredirect(1)
self.root = root
self.\
geometry('+%d+%d' %
(root.winfo_pointerx(),
root.winfo_pointery()))
try:
self.tk.call('::Tk::unsupported::MacWindowStyle',
'style', self._w,
'help', 'noActivates')
except TclError:
pass
window_frame = Frame(self)
window_frame.pack(side=TOP, fill=BOTH, expand=True)
exit_frame = Frame(window_frame, background='#ffffe0')
exit_frame.pack(side=TOP, fill=X, expand=True)
button = Button(exit_frame, text='x', width=3, command=self.free,
background='#ffffe0', highlightthickness=0, relief=FLAT)
button.pack(side=RIGHT)
text_frame = Frame(window_frame)
text_frame.pack(side=TOP, fill=BOTH, expand=True)
label = Label(text_frame, text=string, justify=LEFT,
background='#ffffe0',
font=('tahoma', '8', 'normal'))
label.pack(ipadx=1)
def free(self):
self.destroy() # first we destroy this one
for val,widget in enumerate(dialogs): # go through the dialogs list
if widget is self: # when we find this widget
dialogs.pop(val) # pop it out
break # and stop searching
if dialogs: # if there are any dialogs left:
for widget in dialogs: # go through each widget
widget.lift(aboveThis=self.root) # and lift it above the root
def bind():
"""
toggle property window creation mode
"""
root.bind('<ButtonPress-1>', create)
def create(event):
"""
Create actual window upon mouse click
"""
dialogs.append(PropertyDialog(root, 'help me'))
root = Tk()
dialogs = []
root.geometry('%dx%d' % (300,400))
Button(root, text='create', command=bind).pack()
root.mainloop()
change this:
if dialogs: # if there are any dialogs left:
for widget in dialogs: # go through each widget
widget.lift(aboveThis=self.root) # and lift it above the root
to this:
if dialogs: # if there are any dialogs left:
for widget in dialogs: # go through each widget
widget.lift() # and lift it above the root
the widgets will stay above the main window.
EDIT:
Sorry that only half worked... the widows will stay above sometimes with that code
:-X
It was keeping the widgets on top until you closed one of them.... this code does keep the widgets on top
it uses the self.attributes("-topmost", True) when you spawn the windows.
Sorry again.
from Tkinter import *
class PropertyDialog(Toplevel):
def __init__(self, root, string):
Toplevel.__init__(self)
self.wm_overrideredirect(1)
self.root = root
self.\
geometry('+%d+%d' %
(root.winfo_pointerx(),
root.winfo_pointery()))
try:
self.tk.call('::Tk::unsupported::MacWindowStyle',
'style', self._w,
'help', 'noActivates')
except TclError:
pass
window_frame = Frame(self)
window_frame.pack(side=TOP, fill=BOTH, expand=True)
exit_frame = Frame(window_frame, background='#ffffe0')
exit_frame.pack(side=TOP, fill=X, expand=True)
button = Button(exit_frame, text='x', width=3, command=self.free,
background='#ffffe0', highlightthickness=0, relief=FLAT)
button.pack(side=RIGHT)
text_frame = Frame(window_frame)
text_frame.pack(side=TOP, fill=BOTH, expand=True)
label = Label(text_frame, text=string, justify=LEFT,
background='#ffffe0',
font=('tahoma', '8', 'normal'))
label.pack(ipadx=1)
self.attributes("-topmost", True)
def free(self):
self.destroy() # first we destroy this one
def bind():
"""
toggle property window creation mode
"""
root.bind('<ButtonPress-1>', create)
def create(event):
"""
Create actual window upon mouse click
"""
dialogs.append(PropertyDialog(root, 'help me'))
root = Tk()
dialogs = []
root.geometry('%dx%d' % (300,400))
Button(root, text='create', command=bind).pack()
root.mainloop()
I recommend moving away from Toplevel widgets, since those are separate windows and you're suppressing their window-like behavior. This version makes PropertyDialog inherit from Frame instead of Toplevel, using the place() geometry manager. When you click the main window, it first checks whether the widget clicked was the main window or a popup window to prevent a new popup from appearing when you close an existing one. Changed areas are marked with #CHANGED#.
from Tkinter import *
class PropertyDialog(Frame): #CHANGED#
def __init__(self, root, string, event): #CHANGED#
Frame.__init__(self) #CHANGED#
self.root = root
try:
self.tk.call('::Tk::unsupported::MacWindowStyle',
'style', self._w,
'help', 'noActivates')
except TclError:
pass
exit_frame = Frame(self, background='#ffffe0') #CHANGED#
exit_frame.pack(side=TOP, fill=X, expand=True)
button = Button(exit_frame, text='x', width=3, command=self.free,
background='#ffffe0', highlightthickness=0, relief=FLAT)
button.pack(side=RIGHT)
text_frame = Frame(self) #CHANGED#
text_frame.pack(side=TOP, fill=BOTH, expand=True)
label = Label(text_frame, text=string, justify=LEFT,
background='#ffffe0',
font=('tahoma', '8', 'normal'))
label.pack(ipadx=1)
self.place(x=event.x, y=event.y, anchor=NW) #CHANGED#
def free(self):
self.destroy()
# other things you want to do - if there's nothing else,
# just bind the close button to self.destroy
def bind():
"""
toggle property window creation mode
"""
root.bind('<ButtonPress-1>', create)
def create(event):
"""
Create actual window upon mouse click
"""
if event.widget is root: #CHANGED#
dialogs.append(PropertyDialog(root, 'help me', event))
root = Tk()
dialogs = []
root.geometry('%dx%d' % (300,400))
Button(root, text='create', command=bind).pack()
root.mainloop()