# 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