You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python开发Windows后台托盘应用:任务栏图标唤醒窗口问题

问题解决指南

核心问题:任务栏固定图标点击唤醒已有窗口

Windows任务栏固定逻辑依赖App User Model ID(AUMID),同时新启动的实例需要主动检测并唤醒已有进程,而非直接退出。修改方案如下:

  1. 完善AUMID设置,确保程序启动时尽早配置且打包前后一致
  2. 新实例检测到已有进程时,通过Windows API定位主窗口并发送显示指令,而非直接退出

进程残留与锁文件问题修复

原锁文件仅存储PID,无法处理进程异常崩溃场景,改用文件独占锁实现单实例:系统会在进程退出(包括异常崩溃)时自动释放锁,避免残留问题。

测试效率提升

无需每次打包--onefile:

  • 使用pyinstaller --onedir main.py生成文件夹格式打包产物,修改代码后仅替换文件夹内的main.exe即可,无需重新打包所有依赖
  • 测试阶段直接运行Python脚本:创建快捷方式指向python.exe并附加脚本路径(如python D:\path\to\main.py),将该快捷方式固定到任务栏,无需打包即可验证任务栏唤醒逻辑

完整修改代码

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
import win32gui
import win32api
import win32con
import msvcrt

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

def remove_lock_file():
    """释放文件锁并删除锁文件"""
    global lock_file_handle
    if lock_file_handle:
        try:
            msvcrt.locking(lock_file_handle.fileno(), msvcrt.LK_UNLCK, 1)
            lock_file_handle.close()
        except:
            pass
    if os.path.exists(LOCK_FILE):
        os.remove(LOCK_FILE)

def find_main_window():
    """枚举窗口查找已有实例的主窗口"""
    def callback(handle, extra):
        if win32gui.GetWindowText(handle) == "Windows Tester":
            extra.append(handle)
        return True
    windows = []
    win32gui.EnumWindows(callback, windows)
    return windows[0] if windows else None

def is_already_running():
    """检测实例是否运行,存在则唤醒窗口"""
    hwnd = find_main_window()
    if hwnd:
        win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
        win32api.SetForegroundWindow(hwnd)
        return True
    try:
        global lock_file_handle
        lock_file_handle = open(LOCK_FILE, 'w')
        msvcrt.locking(lock_file_handle.fileno(), msvcrt.LK_NBLCK, 1)
        return False
    except:
        return True

def handle_exit(signum=None, frame=None):
    """处理退出信号"""
    global exit_flag
    exit_flag = True
    remove_lock_file()

# 注册退出信号处理器
signal.signal(signal.SIGINT, handle_exit)
signal.signal(signal.SIGTERM, handle_exit)
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()

    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 create_main_window():
    root = tk.Tk()
    root.title("Windows Tester")
    root.geometry("300x200")
    root.protocol("WM_DELETE_WINDOW", lambda: root.withdraw())
    return root

def process_gui_queue(root):
    """处理GUI队列任务"""
    while not gui_queue.empty():
        action = gui_queue.get()
        action()

    if not exit_flag:
        root.after(100, process_gui_queue, root)
    else:
        root.quit()

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

def main():
    if is_already_running():
        sys.exit(0)

    atexit.register(remove_lock_file)

    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()
    run_tray_icon(root)
    start_gui_event_loop(root)
    remove_lock_file()

if __name__ == "__main__":
    main()

内容的提问来源于stack exchange,提问作者SirLouen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 16:04:55