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.

542 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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