「單一實例」是桌面應用常見需求:防止使用者重複啟動導致資源衝突、資料不一致、授權衝突。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()
FindWindow(None, title) 以視窗標題 (Caption) 為鍵。若標題不唯一 (如多文檔介面、通用標題 "記事本") 會誤判。最佳實踐:使用 APP_TITLE = f'MyApp-{VERSION}-{GUID}' 格式,或在視窗建立後用 win32gui.SetWindowText(hwnd, unique_title) 設定唯一標識。
win32ui.FindWindow 找不到視窗時拋出 win32ui.error (非返回 None),必須用 try/except 捕獲。其他例外 (權限、視窗已銷毀) 也應捕獲並視為「無現有實例」。
找到視窗後,需處理多種狀態:
SW_SHOWNORMAL:恢復正常大小 (從最小化/最大化)SetForegroundWindow():置於前景並獲得焦點 (受 Windows 前景鎖限制,可能需 win32process.AttachThreadInput 輔助)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 應用、原型驗證 |
| 命名互斥體 | 高 (核心級) | 否 | 需 IPC | pywin32 | 生產級 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 方案適合快速原型、簡單工具,但生產環境請升級至互斥體方案。理解各方案權衡,才能根據專案約束 (依賴、平台、打包方式、啟動速度) 做出最佳選擇。