#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ P2P Chat Client GUI 启动脚本 用法: python run_client_gui.py [--server HOST] [--port PORT] 示例: python run_client_gui.py python run_client_gui.py --server 192.168.1.100 --port 8888 """ import asyncio import argparse import logging import sys import os from typing import Optional, List # 确保能找到模块 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PyQt6.QtWidgets import QApplication, QMessageBox, QSplashScreen from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject from PyQt6.QtGui import QPixmap, QFont from client.app import P2PClientApp from client.connection_manager import ConnectionState from client.ui import ( MainWindow, LoginDialog, ChatWidget, ContactListWidget, FileTransferWidget, MediaPlayerWidget, VoiceCallWidget, SystemTrayManager ) from shared.models import UserInfo, UserStatus, Message, MessageType from config import load_client_config, ClientConfig # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler('client_gui.log', encoding='utf-8') ] ) logger = logging.getLogger(__name__) class AsyncWorker(QThread): """异步工作线程,用于运行 asyncio 事件循环""" connected = pyqtSignal(bool, str) # (success, message) disconnected = pyqtSignal(str) # reason message_received = pyqtSignal(object) # Message user_list_updated = pyqtSignal(list) # List[UserInfo] state_changed = pyqtSignal(str, str) # (state, reason) def __init__(self, client: P2PClientApp, user_info: UserInfo): super().__init__() self.client = client self.user_info = user_info self._running = False self._loop: Optional[asyncio.AbstractEventLoop] = None def run(self): """运行异步事件循环""" self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._running = True try: # 连接到服务器 success = self._loop.run_until_complete( self.client.start(self.user_info) ) if success: self.connected.emit(True, "连接成功") # 设置回调 self.client.add_message_callback(self._on_message) self.client.add_state_callback(self._on_state_change) self.client.add_user_list_callback(self._on_user_list) # 保持运行 while self._running: self._loop.run_until_complete(asyncio.sleep(0.1)) else: self.connected.emit(False, "连接失败") except Exception as e: logger.error(f"Worker error: {e}") self.connected.emit(False, str(e)) finally: self._loop.close() def stop(self): """停止工作线程""" self._running = False if self._loop and self._loop.is_running(): self._loop.call_soon_threadsafe(self._loop.stop) def _on_message(self, message: Message): """消息回调""" self.message_received.emit(message) def _on_state_change(self, state: ConnectionState, reason: Optional[str]): """状态变化回调""" self.state_changed.emit(state.value, reason or "") if state == ConnectionState.DISCONNECTED: self.disconnected.emit(reason or "连接断开") def _on_user_list(self, users: List[UserInfo]): """用户列表更新回调""" self.user_list_updated.emit(users) def send_message(self, peer_id: str, content: str) -> bool: """发送消息(线程安全)""" if self._loop and self._running: future = asyncio.run_coroutine_threadsafe( self.client.send_text_message(peer_id, content), self._loop ) try: return future.result(timeout=5.0) except Exception as e: logger.error(f"Send message error: {e}") return False return False def send_file(self, peer_id: str, file_path: str) -> bool: """发送文件(线程安全)""" if self._loop and self._running: future = asyncio.run_coroutine_threadsafe( self.client.send_file(peer_id, file_path), self._loop ) try: return future.result(timeout=300.0) # 5分钟超时 except Exception as e: logger.error(f"Send file error: {e}") return False return False def refresh_users(self): """刷新用户列表(线程安全)""" if self._loop and self._running: asyncio.run_coroutine_threadsafe( self.client.refresh_online_users(), self._loop ) class P2PChatGUI(QObject): """P2P 聊天 GUI 应用程序""" def __init__(self, config: ClientConfig): super().__init__() self.config = config self.app: Optional[QApplication] = None self.main_window: Optional[MainWindow] = None self.client: Optional[P2PClientApp] = None self.worker: Optional[AsyncWorker] = None self._current_user: Optional[UserInfo] = None self._current_chat_peer: Optional[str] = None def run(self) -> int: """运行 GUI 应用程序""" # 创建 Qt 应用 self.app = QApplication(sys.argv) self.app.setApplicationName("P2P Chat") self.app.setApplicationVersion("0.1.0") # 设置默认字体 font = QFont("Microsoft YaHei", 10) self.app.setFont(font) # 设置样式 self.app.setStyle("Fusion") # 应用退出时清理资源 self.app.aboutToQuit.connect(self.cleanup) # 显示登录对话框 if not self._show_login(): return 0 # 创建主窗口 self._create_main_window() # 连接到服务器 self._connect_to_server() # 运行应用 return self.app.exec() def _show_login(self) -> bool: """显示登录对话框""" dialog = LoginDialog() # 设置服务器地址 dialog._login_server_input.setText( f"{self.config.server_host}:{self.config.server_port}" ) dialog._reg_server_input.setText( f"{self.config.server_host}:{self.config.server_port}" ) if dialog.exec(): self._current_user = dialog.get_user_info() host, port = dialog.get_server_address() self.config.server_host = host self.config.server_port = port return True return False def _create_main_window(self): """创建主窗口""" self.main_window = MainWindow(self.config) if self._current_user: self.main_window.set_current_user(self._current_user) # 创建并设置联系人列表组件 self._contact_list_widget = ContactListWidget() self.main_window.set_contact_list_widget(self._contact_list_widget) # 连接联系人列表信号 self._contact_list_widget.contact_selected.connect(self._on_contact_selected) self._contact_list_widget.contact_double_clicked.connect(self._on_contact_double_clicked) self._contact_list_widget.refresh_requested.connect(self._refresh_users) # 设置系统托盘 try: tray = SystemTrayManager(self.main_window) self.main_window.set_system_tray(tray) except Exception as e: logger.warning(f"Failed to create system tray: {e}") # 连接菜单动作 self._connect_menu_actions() self.main_window.show() def _connect_menu_actions(self): """连接菜单动作""" # 发送文件 self.main_window._send_file_action.triggered.connect(self._on_send_file) # 发送图片 self.main_window._send_image_action.triggered.connect(self._on_send_image) # 语音通话 self.main_window._voice_call_action.triggered.connect(self._on_voice_call) def _connect_to_server(self): """连接到服务器""" if not self._current_user: return # 更新状态 self.main_window.connection_state_changed.emit("连接中...") # 创建客户端 self.client = P2PClientApp(self.config) # 创建工作线程 self.worker = AsyncWorker(self.client, self._current_user) self.worker.connected.connect(self._on_connected) self.worker.disconnected.connect(self._on_disconnected) self.worker.message_received.connect(self._on_message_received) self.worker.user_list_updated.connect(self._on_user_list_updated) self.worker.state_changed.connect(self._on_state_changed) # 启动工作线程 self.worker.start() def _on_connected(self, success: bool, message: str): """连接结果回调""" if success: self.main_window.connection_state_changed.emit("已连接") self.main_window._statusbar.showMessage(f"已连接到服务器") logger.info("Connected to server") # 刷新用户列表 QTimer.singleShot(500, self._refresh_users) else: self.main_window.connection_state_changed.emit("连接失败") QMessageBox.critical( self.main_window, "连接失败", f"无法连接到服务器: {message}" ) def _on_disconnected(self, reason: str): """断开连接回调""" self.main_window.connection_state_changed.emit("已断开") self.main_window._statusbar.showMessage(f"连接断开: {reason}") # 询问是否重连 reply = QMessageBox.question( self.main_window, "连接断开", f"与服务器的连接已断开: {reason}\n\n是否尝试重新连接?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self._connect_to_server() def _on_message_received(self, message: Message): """消息接收回调""" if message.msg_type == MessageType.TEXT: content = message.payload.decode('utf-8') sender = message.sender_id # 显示通知 if self.main_window.isMinimized() or not self.main_window.isActiveWindow(): self.main_window.show_notification( f"来自 {sender} 的消息", content[:50] + ("..." if len(content) > 50 else "") ) # 更新状态栏 self.main_window._statusbar.showMessage( f"收到来自 {sender} 的消息", 3000 ) logger.info(f"Message from {sender}: {content[:50]}...") elif message.msg_type == MessageType.VOICE_CALL_REQUEST: sender = message.sender_id self.main_window.show_notification( "来电", f"{sender} 正在呼叫你" ) def _on_user_list_updated(self, users: List[UserInfo]): """用户列表更新回调""" logger.info(f"Online users: {len(users)}") self.main_window._statusbar.showMessage( f"在线用户: {len(users)}", 3000 ) # 更新联系人列表(排除自己) if hasattr(self, '_contact_list_widget') and self._contact_list_widget: # 过滤掉当前用户自己 other_users = [ user for user in users if self._current_user is None or user.user_id != self._current_user.user_id ] self._contact_list_widget.set_contacts(other_users) logger.info(f"Contact list updated: {len(other_users)} contacts") def _on_state_changed(self, state: str, reason: str): """状态变化回调""" state_text = { "disconnected": "已断开", "connecting": "连接中...", "connected": "已连接", "reconnecting": "重连中..." }.get(state, state) self.main_window.connection_state_changed.emit(state_text) def _refresh_users(self): """刷新用户列表""" if self.worker: self.worker.refresh_users() def _on_send_file(self): """发送文件""" from PyQt6.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( self.main_window, "选择文件", "", "所有文件 (*.*)" ) if file_path and self._current_chat_peer: self.main_window._statusbar.showMessage(f"正在发送文件...") # TODO: 实现文件发送 def _on_send_image(self): """发送图片""" from PyQt6.QtWidgets import QFileDialog file_path, _ = QFileDialog.getOpenFileName( self.main_window, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.gif *.bmp)" ) if file_path and self._current_chat_peer: self.main_window._statusbar.showMessage(f"正在发送图片...") # TODO: 实现图片发送 def _on_voice_call(self): """发起语音通话""" if self._current_chat_peer: self.main_window._statusbar.showMessage(f"正在呼叫...") # TODO: 实现语音通话 def _on_contact_selected(self, user_id: str): """联系人选中回调""" self._current_chat_peer = user_id logger.debug(f"Contact selected: {user_id}") def _on_contact_double_clicked(self, user_id: str): """联系人双击回调 - 打开聊天""" self._current_chat_peer = user_id logger.info(f"Opening chat with: {user_id}") # TODO: 打开聊天窗口 self.main_window._statusbar.showMessage(f"与 {user_id} 聊天", 3000) def cleanup(self): """清理资源""" logger.info("Cleaning up resources...") if self.worker: self.worker.stop() self.worker.wait(3000) self.worker = None if self.client: # 在新的事件循环中停止客户端 loop = asyncio.new_event_loop() try: loop.run_until_complete(self.client.stop()) except Exception as e: logger.error(f"Error stopping client: {e}") finally: loop.close() self.client = None logger.info("Cleanup completed") def parse_args() -> argparse.Namespace: """解析命令行参数""" parser = argparse.ArgumentParser( description='P2P Chat Client (GUI)', formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( '--server', '-s', type=str, default=None, help='服务器地址 (默认: 127.0.0.1)' ) parser.add_argument( '--port', '-p', type=int, default=None, help='服务器端口 (默认: 8888)' ) parser.add_argument( '--debug', action='store_true', help='启用调试模式' ) return parser.parse_args() def main() -> int: """主函数""" args = parse_args() # 设置日志级别 if args.debug: logging.getLogger().setLevel(logging.DEBUG) # 加载配置 config = load_client_config() # 命令行参数覆盖配置 if args.server: config.server_host = args.server if args.port: config.server_port = args.port # 创建并运行 GUI gui = P2PChatGUI(config) try: exit_code = gui.run() return exit_code except KeyboardInterrupt: logger.info("Application interrupted") gui.cleanup() return 0 except Exception as e: logger.error(f"Application error: {e}") gui.cleanup() return 1 if __name__ == "__main__": sys.exit(main())