Open the running process in the background (systray) with the pinned Taskbar app using Python & Windows

I’ve been trying to implement some Windows native functionality using tkinter, pystray and threading and some extra needed libraries unsuccessfully.

I’m trying to achieve this functionality:

  1. The systray icon, this is already done, mostly managed in run_tray_icon
  2. The process should not open again once it’s running (there should never be two processes of this program at the same time because they can conflict). For this, I’ve implemented an “app lock” functionality, handling all possible exits to remove the app lock with handle_exit, still sometimes I find that some processes are never closed and end running
  3. I’m using some Queues in process_gui_queuebecause I feel that without them, some processes end in the background.
  4. And finally, and the most difficult part, I have found no documentation so far on how to accomplish: Windows has the possibility of pinning apps to the task bar. If the process is running in the background, thanks to threading and hidden in try with pystray, if I click the pinned taskbar icon, it tries to open a new process, instead of doing a deiconify.

The standard behavior of a task bar pinned icon, is a process run. Basically, I should be able to access the running process, to run the root.deiconify but I don’t have a clue on how I can achieve this feature.

This is the whole code I’ve been using for testing purposes, I have decided not to omit anything because I feel that everything is intertwined.

import os
import sys
import atexit
import signal
from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw
import threading
import tkinter as tk
from queue import Queue
from ctypes import windll

LOCK_FILE = 'app.lock'
exit_flag = False
gui_queue = Queue()

def remove_lock_file():
    """Remove the lock file on exit."""
    if os.path.exists(LOCK_FILE):
        os.remove(LOCK_FILE)

def is_already_running():
    """Check if the application is already running."""
    if os.path.exists(LOCK_FILE):
        return True
    else:
        with open(LOCK_FILE, 'w') as f:
            f.write(str(os.getpid()))
        return False

def handle_exit(signum=None, frame=None):
    """Handle exit signals."""
    global exit_flag
    exit_flag = True
    remove_lock_file()
    
# To ensure that Lock File is always removed we register special exit signal handlers
# SIGINT is sent when the user presses Ctrl+C
signal.signal(signal.SIGINT, handle_exit)
# SIGTERM is sent when the system is shutting down
signal.signal(signal.SIGTERM, handle_exit)
# SIGTERM is sent when the system is shutting down
signal.signal(signal.SIGBREAK, handle_exit)

def run_tray_icon(root):
    def show_window():
        gui_queue.put(lambda: root.deiconify())

    def hide_window():
        gui_queue.put(lambda: root.withdraw())

    def exit_application(icon, item):
        handle_exit()
        icon.stop()  # Stop the icon loop

    menu = Menu(
        MenuItem('Show', show_window),
        MenuItem('Hide', hide_window),
        MenuItem('Exit', exit_application)
    )
    
    def create_image(width, height, color1, color2):
        # Generate an image and draw a pattern
        image = Image.new('RGB', (width, height), color1)
        dc = ImageDraw.Draw(image)
        dc.rectangle(
            (width // 2, 0, width, height // 2),
            fill=color2)
        dc.rectangle(
            (0, height // 2, width // 2, height),
            fill=color2)
        return image

    def run_icon():
        icon = Icon("Windows Tester", icon=create_image(64, 64, 'black', 'white'), menu=menu)
        icon.run()

    thread = threading.Thread(target=run_icon)
    thread.daemon = True
    thread.start()

def create_main_window():
    root = tk.Tk()
    root.title("Windows Tester")
    root.geometry("300x200")
    
    # Modify close event to hide the window instead of closing the app
    root.protocol("WM_DELETE_WINDOW", lambda: root.withdraw())

    return root

def process_gui_queue(root):
    """Quit the GUI event loop if the exit flag is set."""
    while not gui_queue.empty():
        action = gui_queue.get()
        action()

    if not exit_flag:
        root.after(100, process_gui_queue, root)
    else:
        root.quit()  # Stop the main loop

def start_gui_event_loop(root):
    process_gui_queue(root)
    root.mainloop()

def main():
    # Check if another instance is running
    if is_already_running():
        print("Another instance is already running.")
        sys.exit(0)

    # Register cleanup function
    atexit.register(remove_lock_file)

    # Set the App User Model ID
    try:
        myappid = 'sirlouen.windows-testing.0.0.1'
        windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
    except Exception as e:
        print(f"Failed to set App User Model ID: {e}")
    
    root = create_main_window()

    # Start the system tray icon in a separate thread
    run_tray_icon(root)
    
    # Start the GUI event loop
    start_gui_event_loop(root)

    # Perform cleanup
    remove_lock_file()

if __name__ == "__main__":
    main()

One extra thing. To check the behavior, I’m using pyinstaller to:

  1. Generate the exe
  2. Pin it in the taskbar
  3. Execute and hide it
  4. Try to re-execute it from pinned icon in the task bar

pyinstaller --onefile main.py

PS: It’s also killing me that to test each change, it takes like 2-3 minutes to generate another new EXE with this command.

3

After a ton of researching, I tried each single idea that I found out there, from the winfo_interps to trying to manage with some Windows libraries with the win32 package and a lock file state like I showed in my first code.

The most important thing, I think I missed in my first part, is the fact that clicking on a Taskbar pinned icon actually opens an entirely new process. So this new process has to find the way to communicate with the previously initialized process living in the system tray.

Tkinter happens to have built-in functionality, where you can call an initialized object method in another process if you happen to define the name of the Tkinter object. But for some reason, Windows doesn’t support this mechanism: /a/73986115/4442122

So the solution here was implementing sockets with the python socket library, which happens to have, similarly to Tkinter call, a mechanism to send a method to a previously initialized object in such socket implementation.

Modifying the previous code, to remove all the LOCK_FILE references I made this new code:

from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw
import threading
import tkinter as tk
import socket
from ctypes import windll

LISTEN_PORT = 21345

class WindowsTester(tk.Tk):
    def __init__(self):
        super().__init__(className='WindowsTester')
        self.title("Windows Tester")
        
        self.label = tk.Label(self, text="This is a Tkinter app!")
        self.label.pack()
        
        self.protocol("WM_DELETE_WINDOW", self.on_close)
        self.start_server()
        
    def start_server(self):
        self.server_thread = threading.Thread(target=self.run_server)
        self.server_thread.daemon = True
        self.server_thread.start()

    def run_server(self):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind(('localhost', LISTEN_PORT))
        server_socket.listen(1)  # Listen for incoming connections
        while True:
            conn, addr = server_socket.accept()  # Accept a connection
            print('Connected by', addr)
            message = conn.recv(1024).decode()  # Receive the message
            print('Received:', message)
            if message == 'activate':
                self.activate()  # Call activate method
            conn.close()  # Close the connection
        
    def on_close(self):
        self.withdraw()  # Hide the window instead of destroying it
        
    def activate(self):
        self.deiconify()  # Show the window
        self.lift()  # Bring the window to the foreground

def run_tray_icon(root):
    def show_window():
        root.activate()

    def hide_window():
        root.on_close()

    def exit_application(icon):
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            client_socket.connect(('localhost', LISTEN_PORT))  # Connect to the server
            client_socket.sendall(b'exit')  # Send the exit message
            print("Exiting application")
        except ConnectionRefusedError:
            print("No running instance found")
        finally:
            client_socket.close()  # Close the client socket
        icon.stop()  # Stop the icon loop

    menu = Menu(
        MenuItem('Show', show_window),
        MenuItem('Hide', hide_window),
        MenuItem('Exit', exit_application)
    )
    
    def create_image(width, height, color1, color2):
        image = Image.new('RGB', (width, height), color1)
        dc = ImageDraw.Draw(image)
        dc.rectangle((width // 2, 0, width, height // 2), fill=color2)
        dc.rectangle((0, height // 2, width // 2, height), fill=color2)
        return image

    def run_icon():
        icon = Icon("Windows Tester", icon=create_image(64, 64, 'black', 'white'), menu=menu)
        icon.run()

    thread = threading.Thread(target=run_icon)
    thread.daemon = True
    thread.start()

def main():  
    # Try to connect to an existing instance
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        client_socket.connect(('localhost', LISTEN_PORT))  # Connect to the server
        client_socket.sendall(b'activate')  # Send the activate message
        print("Activated existing instance")
    except ConnectionRefusedError:
        print("No existing instance found, starting a new one")
        
        try:
            myappid = 'sirlouen.windows-testing.0.0.1'
            windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
        except Exception as e:
            print(f"Failed to set App User Model ID: {e}")
        
        # Start a new instance
        root = WindowsTester()

        # Start the system tray icon in a separate thread
        run_tray_icon(root)
        
        # Start the GUI event loop
        root.mainloop()
    finally:
        print("Closing client socket")
        client_socket.close()  # Close the client socket

if __name__ == "__main__":
    main()

Maybe it could be greatly improved because I have not read the full docs for sockets and there are things I’m missing, but I feel it’s a nice starting boilerplate to initialize a Windows app with systray and taskbar functionality.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật