首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 更多程式設計知識

Python 實現單一實例應用程式:wxPython + win32ui 互斥鎖方案詳解

     編輯:更多程式設計知識

「單一實例」是桌面應用常見需求:防止使用者重複啟動導致資源衝突、資料不一致、授權衝突。Python 在 Windows 上實現單一實例,主流方案有:win32event.CreateMutex (核心物件)、win32ui.FindWindow (視窗查找)、檔案鎖、psutil 程序掃描。本文深入解析基於 視窗查找 + wxPython 的經典方案,並提供更健壯的現代替代方案。

經典方案:win32ui.FindWindow + wxPython

原理:嘗試尋找特定標題的視窗,若存在則喚醒並聚焦;否則建立新視窗啟動應用。

import wx
import win32ui
import win32con

def main():
    APP_TITLE = 'My Unique App Title - 2024'  # 必須唯一,建議包含版本/GUID
    
    try:
        # 嘗試尋找現有視窗
        win = win32ui.FindWindow(None, APP_TITLE)
        if win:
            # 視窗存在:恢復顯示、置頂、聚焦
            win.ShowWindow(win32con.SW_SHOWNORMAL)
            win.SetForegroundWindow()
            print('應用程式已在運行,已切換至現有視窗。')
            return  # 直接退出,不建立新實例
    except win32ui.error:
        # 找不到視窗會拋出例外 (error code 0)
        pass
    except Exception as e:
        print(f'視窗查找異常: {e}')
    
    # 無現有視窗:啟動新實例
    app = wx.App(False)  # False: 不重定向 stdout/stderr
    frame = wx.Frame(None, title=APP_TITLE, size=(800, 600))
    frame.Show()
    app.MainLoop()

if __name__ == '__main__':
    main()

關鍵細節與坑點分析

1. 視窗標題唯一性

FindWindow(None, title) 以視窗標題 (Caption) 為鍵。若標題不唯一 (如多文檔介面、通用標題 "記事本") 會誤判。最佳實踐:使用 APP_TITLE = f'MyApp-{VERSION}-{GUID}' 格式,或在視窗建立後用 win32gui.SetWindowText(hwnd, unique_title) 設定唯一標識。

2. 例外處理機制

win32ui.FindWindow 找不到視窗時拋出 win32ui.error (非返回 None),必須用 try/except 捕獲。其他例外 (權限、視窗已銷毀) 也應捕獲並視為「無現有實例」。

3. 視窗狀態恢復

找到視窗後,需處理多種狀態:

  • SW_SHOWNORMAL:恢復正常大小 (從最小化/最大化)
  • SetForegroundWindow():置於前景並獲得焦點 (受 Windows 前景鎖限制,可能需 win32process.AttachThreadInput 輔助)
  • 若視窗被最小化到系統托盤 (非任務列),需額外發送自訂訊息通知恢復

4. wxPython 初始化參數

wx.App(False) 中的 False 關鍵:預設 True 會重定向 stdout/stderr 到視窗,導致主控台輸出丟失、除錯困難。生產環境建議明確指定 False。

進階方案一:命名互斥體 (推薦,更底層可靠)

使用核心命名互斥體,不依賴視窗標題,適用於無 GUI、服務型、啟動極快的場景。

import win32event
import win32api
import sys
import wx

MUTEX_NAME = 'Global\\MyApp-Unique-Mutex-Name-{GUID}'

def acquire_mutex():
    """嘗試建立/開啟命名互斥體,返回 (mutex_handle, is_first_instance)"""
    mutex = win32event.CreateMutex(None, False, MUTEX_NAME)
    last_error = win32api.GetLastError()
    is_first = (last_error != win32event.ERROR_ALREADY_EXISTS)
    return mutex, is_first

def main():
    mutex, is_first = acquire_mutex()
    if not is_first:
        print('另一個實例已在運行。')
        # 可選:透過 IPC 通知現有實例 (如 WM_COPYDATA、命名管道、本地 socket)
        return
    
    try:
        app = wx.App(False)
        frame = wx.Frame(None, title='My App')
        frame.Show()
        app.MainLoop()
    finally:
        # 釋放互斥體 (進程結束自動釋放,但顯式釋放更好)
        win32api.CloseHandle(mutex)

if __name__ == '__main__':
    main()

優點:進程級互斥、不依賴 GUI、支援 Global\\ 前綴跨會話 (服務與使用者桌面隔離)。
缺點:無法直接喚醒現有視窗,需配合 IPC 通知。

進階方案二:檔案鎖 (跨平台、無相依)

使用 fcntl (Linux) / msvcrt (Windows) 檔案鎖,適合跨平台腳本、無 pywin32 環境。


import sys
import os

LOCK_FILE = os.path.join(os.getenv('TEMP') or '/tmp', 'myapp.lock')

def try_lock():
    try:
        fd = os.open(LOCK_FILE, os.O_CREAT | os.O_RDWR)
    except OSError:
        return False
    
    try:
        if sys.platform == 'win32':
            import msvcrt
            msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
        else:
            import fcntl
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        return fd
    except (IOError, OSError):
        os.close(fd)
        return False

def main():
    fd = try_lock()
    if not fd:
        print('另一個實例已在運行。')
        return
    
    try:
        # 你的應用邏輯
        import wx
        app = wx.App(False)
        frame = wx.Frame(None, title='My App')
        frame.Show()
        app.MainLoop()
    finally:
        os.close(fd)
        try:
            os.unlink(LOCK_FILE)
        except:
            pass

if __name__ == '__main__':
    main()

進階方案三:psutil 程序掃描 (簡單、跨平台)


import psutil
import sys

def is_running(script_name='myapp.py'):
    current_pid = os.getpid()
    for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
        try:
            if proc.info['pid'] == current_pid:
                continue
            cmdline = proc.info['cmdline'] or []
            if any(script_name in cmd for cmd in cmdline):
                return proc.info['pid']
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return None

pid = is_running()
if pid:
    print(f'已在運行 (PID: {pid})')
    sys.exit(0)
# 繼續啟動...

注意:僅適用於腳本啟動方式固定 (如 python myapp.py);打包後 (PyInstaller) 進程名變為 exe 名,需調整判斷邏輯。

方案對比總結

方案可靠性跨平台喚醒現有視窗依賴適用場景
win32ui FindWindow中 (標題衝突)否是pywin32簡單 GUI 應用、原型驗證
命名互斥體高 (核心級)否需 IPCpywin32生產級 Windows 應用、服務、無 GUI
檔案鎖高是需 IPC無 (標準庫)跨平台腳本、輕量工具、無 pywin32
psutil 掃描中 (誤判風險)是否psutil快速檢查、開發調試、輔助判斷

生產級建議架構

結合 互斥體 (防重啟) + 本地 Socket/IPC (喚醒傳參):


# 啟動時:
1. 嘗試建立命名互斥體
2. 若失敗 (已存在):
   a. 連線本地 TCP/Unix Socket (固定埠/路徑)
   b. 發送啟動參數 (檔案路徑、URL、命令)
   c. 退出
3. 若成功 (首個實例):
   a. 啟動本地 Socket 伺服器監聽
   b. 初始化 GUI
   c. 進入主迴圈
4. 退出時:關閉 Socket、釋放互斥體

這樣實現「單一實例 + 多次啟動傳參給首個實例」的完整體驗 (如瀏覽器點擊連結在現有視窗開新分頁)。

總結

Python 實現單一實例,核心互斥體是基礎,視窗查找是輔助,IPC 通訊是昇華。對於 wxPython Windows 應用,推薦「命名互斥體 + 本地 Socket 喚醒」組合:可靠性最高、支援傳參、無標題衝突風險。本文提供的 win32ui.FindWindow 方案適合快速原型、簡單工具,但生產環境請升級至互斥體方案。理解各方案權衡,才能根據專案約束 (依賴、平台、打包方式、啟動速度) 做出最佳選擇。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved