Python Forum
[Tkinter] Background inactivity timer when tkinter window is not active - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: Python Coding (https://python-forum.io/forum-7.html)
+--- Forum: GUI (https://python-forum.io/forum-10.html)
+--- Thread: [Tkinter] Background inactivity timer when tkinter window is not active (/thread-36941.html)



Background inactivity timer when tkinter window is not active - DBox - Apr-14-2022

I'm having trouble getting a timer (loop or anything) running when the tkinter window is withdrawn or not on top of other windows. The functionality of this is like a small screen saver, start the program, have icon displayed, click show the full screen image appears, if triple click it disappears and goes back to system tray. All of that works, however the timer in the after method only works if the window is displayed, and I want it to work when it's not displayed so it can display it after a period of inactivity (10 secs for testing).

How can I get the inactivity timer working when the tkinter window isn't displayed?

from tkinter import *
from ctypes import Structure, windll, c_uint, sizeof, byref
import time
import pystray
from PIL import Image, ImageTk

class LASTINPUTINFO(Structure):
    _fields_ = [
        ('cbSize', c_uint),
        ('dwTime', c_uint),
    ]

def get_idle_duration():
    lastInputInfo = LASTINPUTINFO()
    lastInputInfo.cbSize = sizeof(lastInputInfo)
    windll.user32.GetLastInputInfo(byref(lastInputInfo))
    millis = windll.kernel32.GetTickCount() - lastInputInfo.dwTime
    return millis / 1000.0

class FS_Display:
    def __init__(self):
        self.root = Tk()
        self.root.title("FS Display")
        self.root.bind("<Triple-1>", self.end_fullscreen)
        self.root.bind("<Escape>", self.end_fullscreen)
        self.root.protocol('WM_DELETE_WINDOW', self.end_fullscreen)
        self.end_fullscreen()

    def end_fullscreen(self, event=None):
        self.root.withdraw()
        self.image = Image.open("favicon.png")
        self.menu = (pystray.MenuItem('Quit', self.quit), pystray.MenuItem('Show', self.show_fullscreen))
        self.icon = pystray.Icon("name", self.image, "FS Display", self.menu)
        self.icon.run()

    def show_fullscreen(self, event=None):
        self.root.attributes("-fullscreen", True)
        self.icon.stop()
        self.root.deiconify()

    def quit(self, event=None):
        self.icon.stop()
        self.root.destroy()

    def idle_check(self, event=None):
        GetLastInputInfo = int(get_idle_duration())
        print(GetLastInputInfo)
        if GetLastInputInfo == 10:
            self.show_fullscreen()
        self.root.after(1000, self.idle_check)

if __name__ == '__main__':
    fs = FS_Display()
    main_image = Image.open("D:black_background.png")
    photo = ImageTk.PhotoImage(main_image)
    Label(fs.root, image=photo).pack()
    fs.root.after(1000, fs.idle_check)
    fs.root.mainloop()



RE: Background inactivity timer when tkinter window is not active - deanhystad - Apr-14-2022

This works fine for me.
import tkinter as tk
from ctypes import Structure, windll, c_uint, sizeof, byref

class LASTINPUTINFO(Structure):
    _fields_ = [
        ('cbSize', c_uint),
        ('dwTime', c_uint),
    ]

def get_idle_duration():
    lastInputInfo = LASTINPUTINFO()
    lastInputInfo.cbSize = sizeof(lastInputInfo)
    windll.user32.GetLastInputInfo(byref(lastInputInfo))
    count = windll.kernel32.GetTickCount()
    return (count - lastInputInfo.dwTime) / 1000.0

class Window(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.counter = tk.IntVar(self, 0)
        self.timer = tk.DoubleVar(self, 0.0)
        tk.Label(self, textvariable=self.counter, width=20).pack(padx=10, pady=10)
        tk.Button(self, text="Iconify", command=self.iconify).pack(padx=10, pady=10)
        self.update()

    def update(self):
        self.counter.set(self.counter.get() + 1)
        if get_idle_duration() > 10.0:
            print("Time to wake up!")
            self.deiconify()
        self.after(1000, self.update)

Window().mainloop()
It also works if the Iconify button calls withdraw() instead of iconify().


RE: Background inactivity timer when tkinter window is not active - DBox - Apr-14-2022

(Apr-14-2022, 08:00 PM)deanhystad Wrote: This works fine for me.

Thank you for the sample. I guess it does work, however it appears that the pystray code is not allowing it to function properly. It blocks on icon.run() until icon.stop(). Thank you for the nudge in the right direction.


RE: Background inactivity timer when tkinter window is not active - deanhystad - Apr-15-2022

From the pstray documentation. https://buildmedia.readthedocs.org/media/pdf/pystray/stable/pystray.pdf

Quote:he call to pystray.Icon.run() is blocking, and it must be performed from the main thread of the application.

You will not be able to do what you want with ".after()" which requires mainloop() to process the event. I think you need a to run the idle check in a separate thread.
import tkinter as tk
import time
import threading
from ctypes import Structure, windll, c_uint, sizeof, byref

class LASTINPUTINFO(Structure):
    _fields_ = [
        ('cbSize', c_uint),
        ('dwTime', c_uint),
    ]


def get_idle_duration():
    lastInputInfo = LASTINPUTINFO()
    lastInputInfo.cbSize = sizeof(lastInputInfo)
    windll.user32.GetLastInputInfo(byref(lastInputInfo))
    count = windll.kernel32.GetTickCount()
    return (count - lastInputInfo.dwTime) / 1000.0

def run_blocking_task():
    """Simulate icon.run()"""
    global running_blocking_task
    running_blocking_task = True
    while running_blocking_task:
        time.sleep(0.1)

def stop_blocking_task():
    """Simulate icon.stop()"""
    global running_blocking_task
    running_blocking_task = False

class Window(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.counter = tk.IntVar(self, 0)
        self.timer = tk.DoubleVar(self, 0.0)
        tk.Label(self, textvariable=self.counter, width=20).pack(padx=10, pady=10)
        tk.Button(self, text="Iconify", command=self.dock).pack(padx=10, pady=10)
        self.update()

    def dock(self):
        """This will block tkinter from running.  Start thread to run idle check"""
        self.iconify()
        self.idle_thread = threading.Thread(target=self.idle_check)
        self.idle_thread.start()
        print("Time to go to bed!")
        run_blocking_task()
        print("Time to wake up!")
        self.deiconify()

    def idle_check(self):
        while True:
            if get_idle_duration() > 10.0:
                stop_blocking_task()
                break
            else:
                time.sleep(0.1)

    def update(self):
        self.counter.set(self.counter.get() + 1)
        self.after(1000, self.update)

Window().mainloop()
Running the idle checker in a different thread let's that code run in parallel with the dock, but it does stop tkinter.mainloop(). Notice that the counter in the window does not increment while the window is hidden. Is that a problem?


RE: Background inactivity timer when tkinter window is not active - DBox - Apr-16-2022

(Apr-15-2022, 06:38 PM)deanhystad Wrote: Running the idle checker in a different thread let's that code run in parallel with the dock, but it does stop tkinter.mainloop(). Notice that the counter in the window does not increment while the window is hidden. Is that a problem?

No, that's not a problem at all for what I'm trying to do. Thanks so much for this, I believe I can do what I need to now.