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
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()
|