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

PySide6程序多实例启动问题及窗口无响应修复咨询

单实例PySide6程序实现:避免多实例启动并唤醒后台窗口

问题背景

开发的PySide6程序已实现Windows托盘图标功能:双击托盘可恢复主窗口,关闭窗口后保留托盘且能回到之前操作状态。但双击程序执行文件多次会启动多实例(托盘出现多个图标),需求是:再次双击执行文件时不启动新实例,直接将后台运行的程序窗口前置显示。

尝试通过本地端口检测实现该功能:启动时检查指定端口是否被占用,若占用则发送信号让后台程序恢复窗口,但触发后窗口出现无响应(双击托盘恢复则正常),推测是线程处理问题导致。


核心代码片段

主程序代码

if __name__ == "__main__":
    root = QApplication(sys.argv)
    app = HomeView()
    app.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowTitleHint | Qt.CustomizeWindowHint)

    app = HomeController(app, HomeModel())
    root.setQuitOnLastWindowClosed(False)
    icon = QIcon("interface/images/icon.ico")
    tray = QSystemTrayIcon()
    tray.setIcon(icon)
    tray.setVisible(True)
    tray.activated.connect(app.show)
    
    root.exec_()

HomeController代码

class HomeController(Controller):
    def __init__(self, view, model, msg=None) -> None:
        super().__init__(view, model, msg=None)
        self.chamados_controller = None
        self.abrir_chamado_controller = None

        if not self.verificar_tokens():
            self._controllerToken.show()
        else:
            self.start()
        self._view = view
        self._model = model
        self.tray_icon = QSystemTrayIcon(QIcon("icon.ico"), self._view)
        self.tray_icon.setToolTip("HUB")
        self.tray_icon.activated.connect(self.tray_icon_activated)
        self.tray_icon.show()
    
    def tray_icon_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            if self._view.isHidden() or self._view.isMinimized():
                self._view.show()
        self._view.raise_()
        self._view.activateWindow()
    
    def closeEvent(self, event):
        event.ignore()
        self.hide()
        self.tray_icon.showMessage("Hub", "Rodando em segundo plano", QIcon.Information, 2000)
    
    def start(self):
        self.showMaximized()
        self.iniciar_sessao()
        self.definir_user()
        self.definir_maquina()
    
        self.chamados_controller = ChamadosController(
            ChamadosView(),
            ChamadoModel(self._model.api),
            self._view.lb_msg,
        )
    
        self.abrir_chamado_controller = AbrirChamadoController(
            AbrirChamadoView(),
            AbrirChamadoModel(self._model.api),
            self._view.lb_msg,
            self,
        )
    
        self._telas = {
            1: self.chamados_controller,
            2: self.abrir_chamado_controller,
        }
    
        self.definir_telas()
    
        self._view.btn_chamados.clicked.connect(
            lambda: self.navegar(1, "Chamados")
        )
        self._view.btn_abrirChamado.clicked.connect(
            lambda: self.navegar(2, "Abrir Chamado")
        )
        
        self.go_to_default_screen()
        # Conectando os sinais dos botões às funções correspondentes
        self._view.btn_minimize.clicked.connect(self.minimize_window)
        self._view.btn_close.clicked.connect(self.close_window)
        self._view.exit.clicked.connect(self.close_window)
        self._view.btn_maximize_restore.clicked.connect(self.maximize_restore_window)
        self._view.btn_toggle_menu.clicked.connect(self.menu)

尝试的本地服务器代码

def __init__(self, view, model, msg=None) -> None:
    super().__init__(view, model, msg=None)
    self.chamados_controller = None
    self.abrir_chamado_controller = None

    if not self.verificar_tokens():
        self._controllerToken.show()
    else:
        self.start()
    self._view = view
    self._model = model
    self.tray_icon = QSystemTrayIcon(QIcon("icon.ico"), self._view)
    self.tray_icon.setToolTip("Client Help Desk")
    self.tray_icon.activated.connect(self.tray_icon_activated)
    self.tray_icon.show()
    server_thread = threading.Thread(target=self.start_server)
    server_thread.start()

def start_server(self):
    host = 'localhost'
    port = 37548

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    server_socket.bind((host, port))

    server_socket.listen(5)

    print(f"Servidor escutando em {host}:{port}")

    while True:
        client_socket, client_address = server_socket.accept()

        print(f"Conexão aceita de {client_address[0]}:{client_address[1]}")
       
        if self._view.isHidden() or self._view.isMinimized():
            self._view.show()
        self._view.raise_()
        self._view.activateWindow()

        client_socket.close()

解决方案:线程安全的单实例实现

问题根源

Qt要求所有UI操作必须在主线程执行,你之前的代码直接在服务器子线程中调用self._view.show()等UI方法,导致线程冲突,引发窗口无响应。

修改后的实现代码

import socket
import threading
from PySide6.QtCore import Signal, QObject
from PySide6.QtWidgets import QSystemTrayIcon

class HomeController(Controller):
    # 定义自定义信号,用于在子线程中触发主线程的UI操作
    wake_up_window = Signal()

    def __init__(self, view, model, msg=None) -> None:
        super().__init__(view, model, msg=None)
        self.chamados_controller = None
        self.abrir_chamado_controller = None

        # 第一步:检测是否已有实例在运行
        if self._check_running_instance():
            # 已有实例,发送唤醒信号后退出当前进程
            self._send_wake_signal()
            import sys
            sys.exit(0)
        
        if not self.verificar_tokens():
            self._controllerToken.show()
        else:
            self.start()
        
        self._view = view
        self._model = model
        self.tray_icon = QSystemTrayIcon(QIcon("icon.ico"), self._view)
        self.tray_icon.setToolTip("HUB")
        self.tray_icon.activated.connect(self.tray_icon_activated)
        self.tray_icon.show()

        # 连接信号到主线程的UI处理函数
        self.wake_up_window.connect(self._bring_window_to_front)

        # 启动服务器守护线程
        server_thread = threading.Thread(target=self._start_server)
        server_thread.daemon = True  # 设置为守护线程,主程序退出时自动终止
        server_thread.start()

    def _check_running_instance(self) -> bool:
        """检查指定端口是否被占用,判断是否已有实例运行"""
        host = 'localhost'
        port = 37548
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((host, port))
            s.close()
            return True
        except ConnectionRefusedError:
            return False

    def _send_wake_signal(self):
        """向已有实例发送唤醒信号"""
        host = 'localhost'
        port = 37548
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((host, port))
            s.send(b'wake')  # 发送任意数据触发唤醒
            s.close()
        except Exception as e:
            print(f"发送唤醒信号失败: {e}")

    def _start_server(self):
        """启动本地服务器监听唤醒信号"""
        host = 'localhost'
        port = 37548

        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind((host, port))
        server_socket.listen(5)

        print(f"服务器监听 {host}:{port}")

        while True:
            client_socket, client_address = server_socket.accept()
            print(f"接受连接来自 {client_address[0]}:{client_address[1]}")
            # 读取客户端发送的数据(仅触发作用)
            client_socket.recv(1024)
            # 发射信号,让主线程处理UI操作
            self.wake_up_window.emit()
            client_socket.close()

    def _bring_window_to_front(self):
        """主线程中执行的窗口前置逻辑,线程安全"""
        if self._view.isHidden() or self._view.isMinimized():
            self._view.show()
        self._view.raise_()
        self._view.activateWindow()

    # 原有托盘双击逻辑复用同一个UI处理函数
    def tray_icon_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            self._bring_window_to_front()

    # 其余原有方法保持不变...

关键改进点

  1. 线程安全的UI操作:通过Qt的Signal机制,将子线程中的唤醒请求传递到主线程执行,避免直接在子线程操作UI控件
  2. 完整的单实例检测:启动时先尝试连接指定端口,成功则说明已有实例,发送信号后退出当前进程
  3. 守护线程设置:服务器线程设为守护线程,确保主程序退出时线程自动终止,避免残留进程

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 04:54:54