You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

607 lines
25 KiB

"""
客户端主程序 —— 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: (
self.client.get_group_history(target_id, 100) if isinstance(target_id, int)
else self.client.get_history(target_name, 100)
))
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):
self.window.show_history(msg.get('friend'), msg.get('history', []))
elif t == 'group_history':
if isinstance(self.window, MainWindow):
self.window.show_history(msg.get('group_id'), msg.get('history', []),
is_group=True)
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':
pass # 搜索窗口自行轮询
# ── 个人资料修改响应 ──
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()