Inkblot
Inkblot

Reputation: 748

Tkinter - Checking if application has focus

I'm trying to subclass Tk where it pauses audio if and only if the entire application loses focus (i.e. the Tk instance loses focus and focus wasn't passed to a Toplevel or messagebox widget).

I managed to get it sort-of working with a 'hack' - when a messagebox is open, it is the last child of the Tk instance and also has no children. Here is what I've tried:

class TkWin(Tk): 
   def __init__(self, title):
        super().__init__(className=title, baseName=title)
        self.bind('<FocusOut>', lambda event: self.pause_audio())

    def pause_audio(self):
        if self.has_focus():
            return
        else:
            pass
            # pause the audio

    def has_focus(self):
        children = self.winfo_children()
        if any(isinstance(x, Toplevel) for x in children):
            return True
        if len(children[-1].winfo_children()) == 0:
            return True
        return False


win = TkWin('test')
win.mainloop()

The above solution solves the problem of not pausing the audio if a Toplevel or messagebox is opened. However it fails if a Toplevel or messagebox widget is opened and then another window is given focus.

(I am aware this would fail if you opened a messagebox and then created a container with some widgets inside it, but it works for how I build my applications)

Is there a better way to go about this?

Tried @Atlas345 's solution and am met with this error when trying to open a messagebox:

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1705, in __call__
    return self.func(*args)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 749, in callit
    func(*args)
  File "/home/inkblot/Desktop/win test.py", line 12, in check
    if self.focus_get() is None:
  File "/usr/lib/python3.6/tkinter/__init__.py", line 699, in focus_get
    return self._nametowidget(name)
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1353, in nametowidget
    w = w.children[n]
KeyError: '__tk__messagebox'

EDIT:

Trying to override the focus_get method gets me somewhere but I have one last issue:

from tkinter import messagebox, Tk, Button, Label, Toplevel

class TkWin(Tk): 
    def __init__(self, title):
        super().__init__(className=title, baseName=title)
        self.focus_id = self.after(10, self.has_focus)
        
    def focus_get(self):
        try:
            return super().focus_get() is not None
        except KeyError:
            print('messagebox is open')
            return True

    def has_focus(self):
        print('resume audio') if self.focus_get() else print('pause audio')
        self.focus_id = self.after(50, self.has_focus)

def popupmsg(msg):
    popup = Toplevel(win)
    popup.wm_title("Warning!")
    Label(popup, text=msg).pack(side="top", fill='both', expand=True)
    Button(popup, text="okay", command = popup.destroy).pack()
    
win = TkWin('test')
Button(win, text='top', command=lambda: Toplevel(win)).pack()
Button(win, text='msg', command=lambda: messagebox.showinfo('title', 'msg')).pack()
Button(win, text='popup', command= lambda: popupmsg('I dare you!')).pack()
win.mainloop()
        

This lets the above error fail silently but I have now noticed that the focus_get() method returns None if the topmost widget does not have focus. This means if any type of popup is open and I then click on the root window (or any popup that was not the most recently created), it assumes the entire application does not have focus and hence pauses the audio which is undesirable to say the least.

Upvotes: 2

Views: 893

Answers (1)

Thingamabobs
Thingamabobs

Reputation: 8072

I'm trying to subclass Tk where it pauses audio if and only if the entire application loses focus (i.e. the Tk instance loses focus and focus wasn't passed to a Toplevel or messagebox widget).

I think you get away with this:

import tkinter as tk

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()
def check ():
    condition = root.focus_get()
    root.after(500, check)
    if condition == None:
        print('pause music')
        pass
check()
root.mainloop()

Because root.focus_get() just returns None if all windows are minimized or loses focus. Most other methods I've tried was returning None as soon as the root was minimized.

This method also fails by a builtin messagebox, because these aren't related to the root window. Thats why you would need to build your own like:

def popupmsg(msg):
    popup = tk.Toplevel()
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill=BOTH, expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

So the full exampel would be like this:

import tkinter as tk
from tkinter import messagebox

def popupmsg(msg):
    popup = tk.Toplevel(root)
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill='both', expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()

b1 = tk.Button(root, text='msg', command= lambda: messagebox.showinfo('title', 'msg'))
b1.pack()

b2 = tk.Button(root, text='popup', command= lambda: popupmsg('I dare you!'))
b2.pack()
def check ():
    condition = root.focus_get()
    root.after(500, check)
    if condition == None:
        print('pause music')
        pass
check()
root.mainloop()

Another way

I found this code here: Determining what tkinter window is currently on top

and implemented in the code. It has the benefit that it works with messagebox, but only when another tk window that is related to your root is aktive. This code isn't about focus, it just tells you which window is currently on top.

So the conditions is all of the windows needs to be iconify.

import tkinter as tk
from tkinter import messagebox

def popupmsg(msg):
    popup = tk.Toplevel(root)
    popup.wm_title("Warning!")
 
    label = tk.Label(popup, text=msg)
    label.pack(side="top", fill='both', expand=True)
    B1 = tk.Button(popup, text="okay", command = popup.destroy)
    B1.pack()

root = tk.Tk()
b = tk.Button(root, text='top', command= lambda: tk.Toplevel(root))
b.pack()

b1 = tk.Button(root, text='msg', command= lambda: [root.iconify(), messagebox.showinfo('title', 'msg')])
b1.pack()

b2 = tk.Button(root, text='popup', command= lambda: popupmsg('I dare you!'))
b2.pack()
def check ():
    condition1 = root.tk.eval('wm stackorder '+str(root))
    root.after(500, check)
    if condition1 == "":
        print('pause music')
        pass
    else:
        print('return music')
    
check()
root.mainloop()

Upvotes: 2

Related Questions