|
|
# P2P Network Communication - Main Window
|
|
|
"""
|
|
|
主窗口模块
|
|
|
实现应用程序主界面,包含联系人列表、聊天窗口和功能按钮
|
|
|
|
|
|
需求: 9.1
|
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
|
from PyQt6.QtWidgets import (
|
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
QSplitter, QStatusBar, QToolBar, QMenuBar,
|
|
|
QMenu, QMessageBox, QLabel, QFrame
|
|
|
)
|
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
|
|
from PyQt6.QtGui import QAction, QIcon, QCloseEvent
|
|
|
|
|
|
from config import ClientConfig
|
|
|
from shared.models import UserInfo, UserStatus, Message, MessageType
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
"""
|
|
|
主窗口
|
|
|
|
|
|
实现应用程序主界面 (需求 9.1)
|
|
|
WHEN P2P_Client 启动 THEN P2P_Client SHALL 显示主界面包含联系人列表、聊天窗口和功能按钮
|
|
|
"""
|
|
|
|
|
|
# 信号定义
|
|
|
message_received = pyqtSignal(Message)
|
|
|
user_status_changed = pyqtSignal(str, UserStatus)
|
|
|
connection_state_changed = pyqtSignal(str)
|
|
|
|
|
|
def __init__(self, config: Optional[ClientConfig] = None, parent: Optional[QWidget] = None):
|
|
|
"""
|
|
|
初始化主窗口
|
|
|
|
|
|
Args:
|
|
|
config: 客户端配置
|
|
|
parent: 父窗口
|
|
|
"""
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.config = config or ClientConfig()
|
|
|
self._current_user: Optional[UserInfo] = None
|
|
|
self._current_chat_peer: Optional[str] = None
|
|
|
|
|
|
# 子组件引用(延迟导入避免循环依赖)
|
|
|
self._contact_list: Optional[QWidget] = None
|
|
|
self._chat_widget: Optional[QWidget] = None
|
|
|
self._file_transfer_widget: Optional[QWidget] = None
|
|
|
self._media_player_widget: Optional[QWidget] = None
|
|
|
self._voice_call_widget: Optional[QWidget] = None
|
|
|
self._system_tray: Optional[Any] = None
|
|
|
|
|
|
self._setup_ui()
|
|
|
self._setup_menu_bar()
|
|
|
self._setup_tool_bar()
|
|
|
self._setup_status_bar()
|
|
|
self._connect_signals()
|
|
|
|
|
|
logger.info("MainWindow initialized")
|
|
|
|
|
|
def _setup_ui(self) -> None:
|
|
|
"""设置UI布局"""
|
|
|
self.setWindowTitle("P2P 通信应用")
|
|
|
self.setMinimumSize(800, 600)
|
|
|
self.resize(self.config.window_width, self.config.window_height)
|
|
|
|
|
|
# 创建中央部件
|
|
|
central_widget = QWidget()
|
|
|
self.setCentralWidget(central_widget)
|
|
|
|
|
|
# 主布局
|
|
|
main_layout = QHBoxLayout(central_widget)
|
|
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
main_layout.setSpacing(0)
|
|
|
|
|
|
# 创建分割器
|
|
|
self._splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
main_layout.addWidget(self._splitter)
|
|
|
|
|
|
# 左侧面板:联系人列表
|
|
|
self._left_panel = self._create_left_panel()
|
|
|
self._splitter.addWidget(self._left_panel)
|
|
|
|
|
|
# 右侧面板:聊天窗口和功能区
|
|
|
self._right_panel = self._create_right_panel()
|
|
|
self._splitter.addWidget(self._right_panel)
|
|
|
|
|
|
# 设置分割比例
|
|
|
self._splitter.setSizes([250, 750])
|
|
|
self._splitter.setStretchFactor(0, 0)
|
|
|
self._splitter.setStretchFactor(1, 1)
|
|
|
|
|
|
def _create_left_panel(self) -> QWidget:
|
|
|
"""
|
|
|
创建左侧面板(联系人列表)
|
|
|
|
|
|
实现联系人列表面板 (需求 9.1)
|
|
|
"""
|
|
|
panel = QFrame()
|
|
|
panel.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
panel.setMinimumWidth(200)
|
|
|
panel.setMaximumWidth(400)
|
|
|
|
|
|
layout = QVBoxLayout(panel)
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
layout.setSpacing(0)
|
|
|
|
|
|
# 用户信息区域
|
|
|
self._user_info_widget = self._create_user_info_widget()
|
|
|
layout.addWidget(self._user_info_widget)
|
|
|
|
|
|
# 联系人列表占位符(将由ContactListWidget替换)
|
|
|
self._contact_list_placeholder = QLabel("联系人列表")
|
|
|
self._contact_list_placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
self._contact_list_placeholder.setStyleSheet("""
|
|
|
QLabel {
|
|
|
background-color: #f5f5f5;
|
|
|
color: #666;
|
|
|
padding: 20px;
|
|
|
}
|
|
|
""")
|
|
|
layout.addWidget(self._contact_list_placeholder, 1)
|
|
|
|
|
|
return panel
|
|
|
|
|
|
def _create_user_info_widget(self) -> QWidget:
|
|
|
"""创建用户信息显示区域"""
|
|
|
widget = QFrame()
|
|
|
widget.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
widget.setStyleSheet("""
|
|
|
QFrame {
|
|
|
background-color: #4a90d9;
|
|
|
border: none;
|
|
|
border-bottom: 1px solid #3a80c9;
|
|
|
}
|
|
|
""")
|
|
|
|
|
|
layout = QHBoxLayout(widget)
|
|
|
layout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
|
|
# 用户头像占位
|
|
|
self._avatar_label = QLabel("👤")
|
|
|
self._avatar_label.setFixedSize(40, 40)
|
|
|
self._avatar_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
self._avatar_label.setStyleSheet("""
|
|
|
QLabel {
|
|
|
background-color: white;
|
|
|
border-radius: 20px;
|
|
|
font-size: 20px;
|
|
|
}
|
|
|
""")
|
|
|
layout.addWidget(self._avatar_label)
|
|
|
|
|
|
# 用户名和状态
|
|
|
info_layout = QVBoxLayout()
|
|
|
info_layout.setSpacing(2)
|
|
|
|
|
|
self._username_label = QLabel("未登录")
|
|
|
self._username_label.setStyleSheet("color: white; font-weight: bold; font-size: 14px;")
|
|
|
info_layout.addWidget(self._username_label)
|
|
|
|
|
|
self._status_label = QLabel("离线")
|
|
|
self._status_label.setStyleSheet("color: #ddd; font-size: 12px;")
|
|
|
info_layout.addWidget(self._status_label)
|
|
|
|
|
|
layout.addLayout(info_layout, 1)
|
|
|
|
|
|
return widget
|
|
|
|
|
|
def _create_right_panel(self) -> QWidget:
|
|
|
"""
|
|
|
创建右侧面板(聊天窗口和功能区)
|
|
|
|
|
|
实现聊天窗口面板和功能按钮区域 (需求 9.1)
|
|
|
"""
|
|
|
panel = QFrame()
|
|
|
panel.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
|
|
|
layout = QVBoxLayout(panel)
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
layout.setSpacing(0)
|
|
|
|
|
|
# 聊天窗口占位符(将由ChatWidget替换)
|
|
|
self._chat_placeholder = QLabel("选择联系人开始聊天")
|
|
|
self._chat_placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
self._chat_placeholder.setStyleSheet("""
|
|
|
QLabel {
|
|
|
background-color: #fafafa;
|
|
|
color: #999;
|
|
|
font-size: 16px;
|
|
|
}
|
|
|
""")
|
|
|
layout.addWidget(self._chat_placeholder, 1)
|
|
|
|
|
|
# 功能按钮区域
|
|
|
self._function_bar = self._create_function_bar()
|
|
|
layout.addWidget(self._function_bar)
|
|
|
|
|
|
return panel
|
|
|
|
|
|
def _create_function_bar(self) -> QWidget:
|
|
|
"""
|
|
|
创建功能按钮区域
|
|
|
|
|
|
实现功能按钮区域 (需求 9.1)
|
|
|
"""
|
|
|
bar = QFrame()
|
|
|
bar.setFrameShape(QFrame.Shape.StyledPanel)
|
|
|
bar.setFixedHeight(50)
|
|
|
bar.setStyleSheet("""
|
|
|
QFrame {
|
|
|
background-color: #f0f0f0;
|
|
|
border-top: 1px solid #ddd;
|
|
|
}
|
|
|
""")
|
|
|
|
|
|
layout = QHBoxLayout(bar)
|
|
|
layout.setContentsMargins(10, 5, 10, 5)
|
|
|
layout.setSpacing(10)
|
|
|
|
|
|
# 功能按钮将在后续子任务中添加
|
|
|
layout.addStretch()
|
|
|
|
|
|
return bar
|
|
|
|
|
|
def _setup_menu_bar(self) -> None:
|
|
|
"""设置菜单栏"""
|
|
|
menubar = self.menuBar()
|
|
|
|
|
|
# 文件菜单
|
|
|
file_menu = menubar.addMenu("文件(&F)")
|
|
|
|
|
|
self._login_action = QAction("登录(&L)", self)
|
|
|
self._login_action.setShortcut("Ctrl+L")
|
|
|
self._login_action.triggered.connect(self._on_login)
|
|
|
file_menu.addAction(self._login_action)
|
|
|
|
|
|
self._logout_action = QAction("注销(&O)", self)
|
|
|
self._logout_action.setEnabled(False)
|
|
|
self._logout_action.triggered.connect(self._on_logout)
|
|
|
file_menu.addAction(self._logout_action)
|
|
|
|
|
|
file_menu.addSeparator()
|
|
|
|
|
|
self._exit_action = QAction("退出(&X)", self)
|
|
|
self._exit_action.setShortcut("Ctrl+Q")
|
|
|
self._exit_action.triggered.connect(self.close)
|
|
|
file_menu.addAction(self._exit_action)
|
|
|
|
|
|
# 聊天菜单
|
|
|
chat_menu = menubar.addMenu("聊天(&C)")
|
|
|
|
|
|
self._send_file_action = QAction("发送文件(&F)", self)
|
|
|
self._send_file_action.setShortcut("Ctrl+Shift+F")
|
|
|
self._send_file_action.setEnabled(False)
|
|
|
chat_menu.addAction(self._send_file_action)
|
|
|
|
|
|
self._send_image_action = QAction("发送图片(&I)", self)
|
|
|
self._send_image_action.setShortcut("Ctrl+Shift+I")
|
|
|
self._send_image_action.setEnabled(False)
|
|
|
chat_menu.addAction(self._send_image_action)
|
|
|
|
|
|
chat_menu.addSeparator()
|
|
|
|
|
|
self._voice_call_action = QAction("语音通话(&V)", self)
|
|
|
self._voice_call_action.setShortcut("Ctrl+Shift+V")
|
|
|
self._voice_call_action.setEnabled(False)
|
|
|
chat_menu.addAction(self._voice_call_action)
|
|
|
|
|
|
# 视图菜单
|
|
|
view_menu = menubar.addMenu("视图(&V)")
|
|
|
|
|
|
self._show_contacts_action = QAction("显示联系人(&C)", self)
|
|
|
self._show_contacts_action.setCheckable(True)
|
|
|
self._show_contacts_action.setChecked(True)
|
|
|
self._show_contacts_action.triggered.connect(self._toggle_contacts_panel)
|
|
|
view_menu.addAction(self._show_contacts_action)
|
|
|
|
|
|
# 帮助菜单
|
|
|
help_menu = menubar.addMenu("帮助(&H)")
|
|
|
|
|
|
self._about_action = QAction("关于(&A)", self)
|
|
|
self._about_action.triggered.connect(self._show_about)
|
|
|
help_menu.addAction(self._about_action)
|
|
|
|
|
|
def _setup_tool_bar(self) -> None:
|
|
|
"""设置工具栏"""
|
|
|
toolbar = QToolBar("主工具栏")
|
|
|
toolbar.setMovable(False)
|
|
|
toolbar.setIconSize(QSize(24, 24))
|
|
|
self.addToolBar(toolbar)
|
|
|
|
|
|
# 工具栏按钮将在后续添加图标后完善
|
|
|
toolbar.addAction(self._login_action)
|
|
|
toolbar.addSeparator()
|
|
|
toolbar.addAction(self._send_file_action)
|
|
|
toolbar.addAction(self._send_image_action)
|
|
|
toolbar.addAction(self._voice_call_action)
|
|
|
|
|
|
def _setup_status_bar(self) -> None:
|
|
|
"""设置状态栏"""
|
|
|
self._statusbar = QStatusBar()
|
|
|
self.setStatusBar(self._statusbar)
|
|
|
|
|
|
# 连接状态标签
|
|
|
self._connection_status_label = QLabel("未连接")
|
|
|
self._connection_status_label.setStyleSheet("color: #999;")
|
|
|
self._statusbar.addPermanentWidget(self._connection_status_label)
|
|
|
|
|
|
self._statusbar.showMessage("就绪")
|
|
|
|
|
|
def _connect_signals(self) -> None:
|
|
|
"""连接信号和槽"""
|
|
|
self.message_received.connect(self._on_message_received)
|
|
|
self.user_status_changed.connect(self._on_user_status_changed)
|
|
|
self.connection_state_changed.connect(self._on_connection_state_changed)
|
|
|
|
|
|
# ==================== 事件处理 ====================
|
|
|
|
|
|
def closeEvent(self, event: QCloseEvent) -> None:
|
|
|
"""
|
|
|
窗口关闭事件
|
|
|
"""
|
|
|
# 确认退出
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"确认退出",
|
|
|
"确定要退出应用程序吗?",
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
QMessageBox.StandardButton.No
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
self._cleanup()
|
|
|
event.accept()
|
|
|
else:
|
|
|
event.ignore()
|
|
|
|
|
|
def resizeEvent(self, event) -> None:
|
|
|
"""
|
|
|
窗口大小调整事件
|
|
|
|
|
|
实现自适应调整界面布局 (需求 9.6)
|
|
|
WHEN 用户调整窗口大小 THEN P2P_Client SHALL 自适应调整界面布局
|
|
|
"""
|
|
|
super().resizeEvent(event)
|
|
|
# 布局会自动调整,无需额外处理
|
|
|
|
|
|
# ==================== 槽函数 ====================
|
|
|
|
|
|
def _on_login(self) -> None:
|
|
|
"""处理登录操作"""
|
|
|
from client.ui.login_dialog import LoginDialog
|
|
|
|
|
|
dialog = LoginDialog(self)
|
|
|
if dialog.exec():
|
|
|
user_info = dialog.get_user_info()
|
|
|
if user_info:
|
|
|
self.set_current_user(user_info)
|
|
|
self._statusbar.showMessage(f"已登录: {user_info.username}")
|
|
|
|
|
|
def _on_logout(self) -> None:
|
|
|
"""处理注销操作"""
|
|
|
reply = QMessageBox.question(
|
|
|
self,
|
|
|
"确认注销",
|
|
|
"确定要注销当前账户吗?",
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
QMessageBox.StandardButton.No
|
|
|
)
|
|
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
|
self._current_user = None
|
|
|
self._update_user_display()
|
|
|
self._login_action.setEnabled(True)
|
|
|
self._logout_action.setEnabled(False)
|
|
|
self._statusbar.showMessage("已注销")
|
|
|
|
|
|
def _toggle_contacts_panel(self, checked: bool) -> None:
|
|
|
"""切换联系人面板显示"""
|
|
|
self._left_panel.setVisible(checked)
|
|
|
|
|
|
def _show_about(self) -> None:
|
|
|
"""显示关于对话框"""
|
|
|
QMessageBox.about(
|
|
|
self,
|
|
|
"关于 P2P 通信应用",
|
|
|
"P2P 网络通信应用程序\n\n"
|
|
|
"版本: 0.1.0\n\n"
|
|
|
"支持功能:\n"
|
|
|
"- 文本消息通信\n"
|
|
|
"- 文件传输\n"
|
|
|
"- 图片传输与显示\n"
|
|
|
"- 音视频播放\n"
|
|
|
"- 语音聊天"
|
|
|
)
|
|
|
|
|
|
def _on_message_received(self, message: Message) -> None:
|
|
|
"""处理接收到的消息"""
|
|
|
logger.debug(f"Message received: {message.msg_type.value}")
|
|
|
# 消息处理将在ChatWidget中实现
|
|
|
|
|
|
def _on_user_status_changed(self, user_id: str, status: UserStatus) -> None:
|
|
|
"""处理用户状态变化"""
|
|
|
logger.debug(f"User {user_id} status changed to {status.value}")
|
|
|
# 状态更新将在ContactListWidget中实现
|
|
|
|
|
|
def _on_connection_state_changed(self, state: str) -> None:
|
|
|
"""处理连接状态变化"""
|
|
|
self._connection_status_label.setText(state)
|
|
|
|
|
|
if state == "已连接":
|
|
|
self._connection_status_label.setStyleSheet("color: green;")
|
|
|
elif state == "连接中...":
|
|
|
self._connection_status_label.setStyleSheet("color: orange;")
|
|
|
else:
|
|
|
self._connection_status_label.setStyleSheet("color: red;")
|
|
|
|
|
|
# ==================== 公共方法 ====================
|
|
|
|
|
|
def set_current_user(self, user_info: UserInfo) -> None:
|
|
|
"""
|
|
|
设置当前用户
|
|
|
|
|
|
Args:
|
|
|
user_info: 用户信息
|
|
|
"""
|
|
|
self._current_user = user_info
|
|
|
self._update_user_display()
|
|
|
self._login_action.setEnabled(False)
|
|
|
self._logout_action.setEnabled(True)
|
|
|
|
|
|
logger.info(f"Current user set: {user_info.username}")
|
|
|
|
|
|
def _update_user_display(self) -> None:
|
|
|
"""更新用户显示"""
|
|
|
if self._current_user:
|
|
|
self._username_label.setText(self._current_user.display_name or self._current_user.username)
|
|
|
self._status_label.setText(self._current_user.status.value)
|
|
|
else:
|
|
|
self._username_label.setText("未登录")
|
|
|
self._status_label.setText("离线")
|
|
|
|
|
|
def set_contact_list_widget(self, widget: QWidget) -> None:
|
|
|
"""
|
|
|
设置联系人列表组件
|
|
|
|
|
|
Args:
|
|
|
widget: 联系人列表组件
|
|
|
"""
|
|
|
# 移除占位符
|
|
|
layout = self._left_panel.layout()
|
|
|
layout.removeWidget(self._contact_list_placeholder)
|
|
|
self._contact_list_placeholder.deleteLater()
|
|
|
|
|
|
# 添加实际组件
|
|
|
self._contact_list = widget
|
|
|
layout.addWidget(widget, 1)
|
|
|
|
|
|
def set_chat_widget(self, widget: QWidget) -> None:
|
|
|
"""
|
|
|
设置聊天组件
|
|
|
|
|
|
Args:
|
|
|
widget: 聊天组件
|
|
|
"""
|
|
|
# 移除占位符
|
|
|
layout = self._right_panel.layout()
|
|
|
layout.removeWidget(self._chat_placeholder)
|
|
|
self._chat_placeholder.deleteLater()
|
|
|
|
|
|
# 添加实际组件(在功能栏之前)
|
|
|
self._chat_widget = widget
|
|
|
layout.insertWidget(0, widget, 1)
|
|
|
|
|
|
def set_system_tray(self, tray_manager) -> None:
|
|
|
"""
|
|
|
设置系统托盘管理器
|
|
|
|
|
|
Args:
|
|
|
tray_manager: 系统托盘管理器
|
|
|
"""
|
|
|
self._system_tray = tray_manager
|
|
|
|
|
|
def show_notification(self, title: str, message: str) -> None:
|
|
|
"""
|
|
|
显示通知
|
|
|
|
|
|
实现新消息通知 (需求 9.3)
|
|
|
WHEN 有新消息到达 THEN P2P_Client SHALL 在系统托盘显示通知提醒
|
|
|
|
|
|
Args:
|
|
|
title: 通知标题
|
|
|
message: 通知内容
|
|
|
"""
|
|
|
if self._system_tray:
|
|
|
self._system_tray.show_message(title, message)
|
|
|
|
|
|
def enable_chat_actions(self, enabled: bool = True) -> None:
|
|
|
"""
|
|
|
启用/禁用聊天相关操作
|
|
|
|
|
|
Args:
|
|
|
enabled: 是否启用
|
|
|
"""
|
|
|
self._send_file_action.setEnabled(enabled)
|
|
|
self._send_image_action.setEnabled(enabled)
|
|
|
self._voice_call_action.setEnabled(enabled)
|
|
|
|
|
|
def set_current_chat_peer(self, peer_id: Optional[str]) -> None:
|
|
|
"""
|
|
|
设置当前聊天对象
|
|
|
|
|
|
Args:
|
|
|
peer_id: 对等端ID
|
|
|
"""
|
|
|
self._current_chat_peer = peer_id
|
|
|
self.enable_chat_actions(peer_id is not None)
|
|
|
|
|
|
def _cleanup(self) -> None:
|
|
|
"""清理资源"""
|
|
|
logger.info("MainWindow cleanup")
|
|
|
# 清理将在集成时实现
|
|
|
|
|
|
@property
|
|
|
def current_user(self) -> Optional[UserInfo]:
|
|
|
"""获取当前用户"""
|
|
|
return self._current_user
|
|
|
|
|
|
@property
|
|
|
def current_chat_peer(self) -> Optional[str]:
|
|
|
"""获取当前聊天对象"""
|
|
|
return self._current_chat_peer
|