""" 客户端主程序 —— ChatApplication 负责协调 UI 与网络层 """ import sys import os import traceback from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject from PyQt5.QtGui import QFont from config import config from client_core import ChatClient from ui.login_window import LoginWindow from ui.register_window import RegisterWindow from ui.main_window import MainWindow from utils import validate_username, validate_password, create_default_config class ChatApplication(QObject): # 跨线程消息信号:接收线程 → 主线程 _server_message_signal = pyqtSignal(dict) def __init__(self): super().__init__() self.app = QApplication(sys.argv) self.app.setApplicationName("SimpleChat") font = QFont("Microsoft YaHei" if sys.platform == "win32" else "PingFang SC", 10) self.app.setFont(font) for d in ('messages', 'messages/images', 'avatars', 'logs'): os.makedirs(d, exist_ok=True) self.client: ChatClient = None self.window = None self.user_info = None # 跨线程信号 → 主线程安全 dispatch self._server_message_signal.connect(self._safe_dispatch) # ── 窗口切换 ────────────────────────────────────────── def _switch(self, new_window): if self.window: try: self.window.close() except Exception: pass self.window = new_window self.window.show() def show_login(self): try: w = LoginWindow(config) w.login_requested.connect(self._do_login) w.show_register.connect(self.show_register) self._switch(w) except Exception as e: traceback.print_exc() QMessageBox.critical(None, "错误", f"无法打开登录窗口: {e}") def show_register(self): try: w = RegisterWindow() w.register_requested.connect(self._do_register) w.show_login.connect(self.show_login) self._switch(w) except Exception as e: traceback.print_exc() QMessageBox.critical(None, "错误", f"无法打开注册窗口: {e}") def show_main(self, user_info): try: self.user_info = user_info w = MainWindow(user_info['username'], user_info.get('nickname', user_info['username']), user_info) # 连接核心信号 w.chat_requested.connect(self._send_chat) w.group_chat_requested.connect(self._send_group_chat) w.image_chat_requested.connect(self._send_image_chat) w.image_group_chat_requested.connect(self._send_group_image_chat) w.add_friend_requested.connect(lambda u: self.client.add_friend(u)) w.remove_friend_requested.connect(lambda fid: self.client.remove_friend(fid)) w.create_group_requested.connect(lambda n: self.client.create_group(n)) w.join_group_requested.connect(lambda gid: self.client.join_group(gid)) w.get_users_requested.connect(lambda: self.client.get_users()) w.get_friends_requested.connect(lambda: self.client.get_friends()) w.get_groups_requested.connect(lambda: self.client.get_groups()) w.get_all_groups_requested.connect(lambda: self.client.get_all_groups()) w.get_history_requested.connect(lambda u: self.client.get_history(u)) w.get_group_history_requested.connect(lambda gid: self.client.get_group_history(gid)) w.load_more_history_requested.connect( lambda target_name, target_id, offset: ( self.client.get_group_history(target_id, 50, offset) if isinstance(target_id, int) else self.client.get_history(target_name, 50, offset) )) w.logout_requested.connect(self._logout) w.leave_group_requested.connect(lambda gid: self.client.leave_group(gid)) w.invite_to_group_requested.connect( lambda gid, uname: self.client.invite_to_group(gid, uname)) w.all_history_requested.connect(lambda: self.client.get_all_history()) w.search_messages_requested.connect( lambda ct, tid, kw: self.client.search_messages(ct, tid, kw)) w.profile_update_requested.connect(lambda nick: self._update_profile(nick)) w.change_username_requested.connect(lambda uname: self.client.change_username(uname)) w.change_nickname_requested.connect(lambda nick: self.client.change_nickname(nick)) w.change_password_requested.connect(lambda old_pw, new_pw: self.client.change_password(old_pw, new_pw)) w.get_group_members_requested.connect(lambda gid: self.client.get_group_members(gid)) self.client.on_message = self._on_server_message self.client.on_connected = self._on_connection_changed self._switch(w) w.update_status("已连接到服务器") QTimer.singleShot(600, lambda: self.client.get_friends()) QTimer.singleShot(900, lambda: self.client.get_groups()) QTimer.singleShot(1200, lambda: self.client.get_users()) QTimer.singleShot(2000, lambda: self._auto_open_first_chat()) except Exception as e: traceback.print_exc() QMessageBox.critical(None, "错误", f"无法打开主窗口: {e}") self.show_login() # ── 登录 / 注册 ─────────────────────────────────────── def _do_login(self, username, password, host, port): try: ok, msg = validate_username(username) if not ok: self.window.show_error(f"用户名无效: {msg}") return ok, msg = validate_password(password) if not ok: self.window.show_error(f"密码无效: {msg}") return except Exception as e: self.window.show_error(f"验证失败: {e}") return if self.client: try: self.client.disconnect() except Exception: pass self.client = None self.client = ChatClient(host, port) self.client.on_message = self._on_server_message self.client.on_connected = self._on_connection_changed import threading as _threading def _connect_and_login(): try: ok, err = self.client.connect() if not ok: self._emit_error(f"连接失败: {err}") return self.client.login(username, password) # Schedule timeout check on main thread via signal self._server_message_signal.emit({ 'type': '_schedule_login_timeout' }) except Exception as e: self._emit_error(f"登录异常: {e}") _threading.Thread(target=_connect_and_login, daemon=True).start() def _check_login_timeout(self): try: from ui.login_window import LoginWindow as _LW if isinstance(self.window, _LW): if not self.window.login_btn.isEnabled(): self.window.show_error("登录超时,请检查服务器地址或网络连接") except Exception: pass def _do_register(self, username, password, nickname): try: if hasattr(self.window, 'show_loading'): self.window.show_loading() except Exception: pass host = config.get('server_host', '127.0.0.1') port = config.get('server_port', 8888) if self.client: try: self.client.disconnect() except Exception: pass self.client = None import threading as _threading def _connect_and_register(): try: self.client = ChatClient(host, port) self.client.on_message = self._on_server_message self.client.on_connected = self._on_connection_changed ok, err = self.client.connect() if not ok: self._emit_register_error(f"连接失败: {err}") return self.client.register(username, password, nickname) # Schedule timeout check on main thread via signal self._server_message_signal.emit({ 'type': '_schedule_register_timeout' }) except Exception as e: self._emit_register_error(f"注册异常: {e}") _threading.Thread(target=_connect_and_register, daemon=True).start() def _emit_error(self, message): """从子线程安全地发送错误到主线程""" self._server_message_signal.emit({ 'type': '_internal_error', 'message': message }) def _emit_register_error(self, message): self._server_message_signal.emit({ 'type': '_internal_register_error', 'message': message }) def _on_register_error(self, message): try: from ui.register_window import RegisterWindow as _RW if isinstance(self.window, _RW): self.window.show_error(message) except Exception: pass if self.client: try: self.client.disconnect() except Exception: pass self.client = None def _check_register_timeout(self): try: from ui.register_window import RegisterWindow as _RW if isinstance(self.window, _RW): if not self.window.register_btn.isEnabled(): self.window.show_error("注册超时,请检查服务器地址或网络连接") except Exception: pass def _goto_login_after_register(self): self.show_login() def _auto_open_first_chat(self): """After login, auto-open chat with first online friend if any exist.""" try: if isinstance(self.window, MainWindow) and self.window.friends: online = [f for f in self.window.friends if f.get('is_online')] first = online[0] if online else self.window.friends[0] self.window.open_chat('private', first.get('username'), first.get('username')) except Exception: pass def _logout(self): if self.client: try: self.client.disconnect() except Exception: pass self.client = None self.show_login() def _update_profile(self, nickname): try: self.window.nickname = nickname self.window.setWindowTitle( f"SimpleChat — {nickname} (@{self.window.username})") except Exception: pass # ── 发消息 ──────────────────────────────────────────── def _send_chat(self, content, receiver): if self.client and self.client.connected: self.client.send_chat(receiver, content) def _send_group_chat(self, group_id, content): if self.client and self.client.connected: self.client.send_group_chat(group_id, content) def _send_image_chat(self, receiver, filename, file_data_b64): if self.client and self.client.connected: self.client.send_chat_image(receiver, filename, file_data_b64) def _send_group_image_chat(self, group_id, filename, file_data_b64): if self.client and self.client.connected: self.client.send_group_chat_image(group_id, filename, file_data_b64) # ── 服务器消息接收(接收线程调用,通过信号转到主线程)─── def _on_server_message(self, msg): """在接收线程中调用,通过 pyqtSignal 安全转到主线程""" self._server_message_signal.emit(msg) def _safe_dispatch(self, msg): """在主线程中执行,安全更新 UI""" try: self._dispatch(msg) except Exception as e: traceback.print_exc() if self.window and hasattr(self.window, 'show_error'): try: self.window.show_error(f"处理消息时出错: {e}") except Exception: pass def _dispatch(self, msg): t = msg.get('type') # ── 内部错误消息 ── if t == '_internal_error': if self.window and hasattr(self.window, 'show_error'): self.window.show_error(msg.get('message', '未知错误')) return elif t == '_internal_register_error': self._on_register_error(msg.get('message', '未知错误')) return elif t == '_connection_changed': self._handle_connection(msg.get('connected', False)) return elif t == '_schedule_login_timeout': QTimer.singleShot(10000, self._check_login_timeout) return elif t == '_schedule_register_timeout': QTimer.singleShot(10000, self._check_register_timeout) return # ── 登录响应 ── if t == 'login_response': if msg.get('success'): info = msg.get('user_info', {}) config.set('username', info.get('username', '')) config.save() self.show_main(info) else: error_msg = msg.get('message', '未知错误') if self.window and hasattr(self.window, 'show_error'): self.window.show_error(f"登录失败: {error_msg}") # ── 注册响应 ── elif t == 'register_response': if msg.get('success'): if self.client: try: self.client.disconnect() except Exception: pass self.client = None if self.window and hasattr(self.window, 'show_success'): self.window.show_success("注册成功!即将跳转到登录界面...") QTimer.singleShot(2000, self._goto_login_after_register) else: error_msg = msg.get('message', '未知错误') if self.window and hasattr(self.window, 'show_error'): self.window.show_error(f"注册失败: {error_msg}") # ── 私聊消息 ── elif t == 'chat': if isinstance(self.window, MainWindow): sender = msg.get('sender') self.window.receive_message( sender, msg.get('content', ''), msg.get('timestamp', ''), 'private', None, sender) # ── 群聊消息 ── elif t == 'group_chat': if isinstance(self.window, MainWindow): sender = msg.get('sender') group_id = msg.get('group_id') group_name = f"群组{group_id}" for g in self.window.groups: if g.get('id') == group_id: group_name = g.get('name', group_name) break self.window.receive_message( sender, msg.get('content', ''), msg.get('timestamp', ''), 'group', group_id, group_name) # ── 图片消息 ── elif t == 'chat_image': if isinstance(self.window, MainWindow): sender = msg.get('sender') self.window.receive_image_message( sender, msg.get('filename', 'image.png'), msg.get('data', ''), msg.get('path', ''), msg.get('timestamp', ''), 'private', msg.get('receiver', ''), sender) elif t == 'group_chat_image': if isinstance(self.window, MainWindow): sender = msg.get('sender') group_id = msg.get('group_id') group_name = f"群组{group_id}" for g in self.window.groups: if g.get('id') == group_id: group_name = g.get('name', group_name) break self.window.receive_image_message( sender, msg.get('filename', 'image.png'), msg.get('data', ''), msg.get('path', ''), msg.get('timestamp', ''), 'group', group_id, group_name) # ── 列表更新 ── elif t == 'friends_list': if isinstance(self.window, MainWindow): self.window.update_friends_list(msg.get('friends', [])) elif t == 'groups_list': if isinstance(self.window, MainWindow): self.window.update_groups_list(msg.get('groups', [])) elif t == 'all_groups_list': if isinstance(self.window, MainWindow): self.window.update_all_groups_list(msg.get('groups', [])) elif t == 'users_list': if isinstance(self.window, MainWindow): self.window.update_users_list(msg.get('users', [])) elif t == 'chat_history': if isinstance(self.window, MainWindow): prepend = msg.get('offset', 0) > 0 self.window.show_history(msg.get('friend'), msg.get('history', []), prepend=prepend) elif t == 'group_history': if isinstance(self.window, MainWindow): prepend = msg.get('offset', 0) > 0 self.window.show_history(msg.get('group_id'), msg.get('history', []), is_group=True, prepend=prepend) elif t == 'recent_conversations': if isinstance(self.window, MainWindow): self.window.restore_conversations(msg.get('conversations', [])) elif t == 'all_history': if isinstance(self.window, MainWindow) and self.window._history_window: self.window._history_window.load_messages(msg.get('history', [])) elif t == 'user_status': if isinstance(self.window, MainWindow): self.window.update_online_status( msg.get('user_id'), msg.get('username'), msg.get('is_online'), msg.get('online_count', 1)) # ── 好友操作响应 ── elif t == 'add_friend_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '添加好友', msg.get('message', '添加成功')) self.client.get_friends() else: self.window.show_message('error', '添加好友', msg.get('message', '失败')) elif t == 'remove_friend_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '删除好友', msg.get('message', '已删除')) self.client.get_friends() else: self.window.show_message('error', '删除好友', msg.get('message', '失败')) # ── 群组操作响应 ── elif t == 'create_group_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '创建群组', msg.get('message', '创建成功')) self.client.get_groups() else: self.window.show_message('error', '创建群组', msg.get('message', '失败')) elif t == 'join_group_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '加入群组', msg.get('message', '加入成功')) self.client.get_groups() else: self.window.show_message('error', '加入群组', msg.get('message', '失败')) elif t == 'group_members': if isinstance(self.window, MainWindow): self.window.update_group_members_in_detail( msg.get('group_id'), msg.get('members', [])) elif t == 'leave_group_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '退出群组', msg.get('message', '已退出')) self.client.get_groups() else: self.window.show_message('error', '退出群组', msg.get('message', '失败')) elif t == 'invite_to_group_response': if isinstance(self.window, MainWindow): if msg.get('success'): self.window.show_message('success', '邀请好友', msg.get('message', '邀请成功')) group_id = msg.get('group_id') if group_id: self.client.get_group_members(group_id) else: self.window.show_message('error', '邀请好友', msg.get('message', '失败')) elif t == 'search_results': if isinstance(self.window, MainWindow): self.window.show_search_results( msg.get('results', []), msg.get('keyword', ''), msg.get('chat_type', 'private'), msg.get('target_id')) # ── 个人资料修改响应 ── elif t == 'change_username_response': if msg.get('success'): new_username = msg.get('new_username') self.user_info['username'] = new_username if self.window and hasattr(self.window, 'username'): self.window.username = new_username self.window.setWindowTitle( f'SimpleChat — {self.window.nickname} (@{new_username})') if self.window and hasattr(self.window, 'show_message'): self.window.show_message('success', '修改用户名', msg.get('message', '')) else: if self.window and hasattr(self.window, 'show_message'): self.window.show_message('error', '修改用户名', msg.get('message', '')) elif t == 'change_nickname_response': if msg.get('success'): new_nickname = msg.get('new_nickname') if self.window and hasattr(self.window, 'nickname'): self.window.nickname = new_nickname self.window.setWindowTitle( f'SimpleChat — {new_nickname} (@{self.window.username})') if self.window and hasattr(self.window, 'show_message'): self.window.show_message('success', '修改昵称', msg.get('message', '')) else: if self.window and hasattr(self.window, 'show_message'): self.window.show_message('error', '修改昵称', msg.get('message', '')) elif t == 'change_password_response': if self.window and hasattr(self.window, 'show_message'): if msg.get('success'): self.window.show_message('success', '修改密码', msg.get('message', '')) else: self.window.show_message('error', '修改密码', msg.get('message', '')) # ── 错误 ── elif t == 'error': if self.window and hasattr(self.window, 'show_error'): self.window.show_error(msg.get('message', '未知错误')) def _on_connection_changed(self, connected): self._server_message_signal.emit({ 'type': '_connection_changed', 'connected': connected }) def _handle_connection(self, connected): try: if isinstance(self.window, MainWindow): if connected: self.window.update_status("已连接到服务器") else: self.window.update_status("连接已断开") QTimer.singleShot(500, self.show_login) except Exception: pass def _trigger_auto_login(self): """Delay-trigger auto-login after UI is visible.""" try: from ui.login_window import LoginWindow as _LW if isinstance(self.window, _LW): self.window.login() except Exception: pass # ── 启动 ────────────────────────────────────────────── def run(self): create_default_config() self.show_login() if config.get("auto_login") and config.get("username") and config.get("save_password"): QTimer.singleShot(300, self._trigger_auto_login) sys.exit(self.app.exec_()) def main(): print("=" * 48) print(" SimpleChat 客户端 v2.3") print("=" * 48) try: ChatApplication().run() except Exception as e: traceback.print_exc() QMessageBox.critical(None, "启动失败", str(e)) sys.exit(1) if __name__ == '__main__': main()