1083 lines
32 KiB
Markdown
1083 lines
32 KiB
Markdown
`./main.py`
|
||
```python
|
||
import sys
|
||
import os
|
||
from pathlib import Path
|
||
from PySide6.QtWidgets import QApplication
|
||
from app.view import Window
|
||
from app.scheduler_manager import scheduler
|
||
|
||
def main():
|
||
# # 设置插件路径 - 关键修复
|
||
# if getattr(sys, 'frozen', False):
|
||
# # 打包后的路径
|
||
# base_dir = Path(sys._MEIPASS)
|
||
# else:
|
||
# # 开发环境路径
|
||
# base_dir = Path(__file__).parent
|
||
|
||
# # 设置Qt插件路径
|
||
# os.environ['QT_PLUGIN_PATH'] = str(base_dir / 'plugins')
|
||
|
||
# 启动队列调度器
|
||
scheduler.start_queue_scheduler()
|
||
|
||
# 创建Qt应用
|
||
app = QApplication(sys.argv)
|
||
window = Window()
|
||
window.show()
|
||
window.setMicaEffectEnabled(True)
|
||
|
||
# 启动事件循环
|
||
exit_code = app.exec()
|
||
|
||
# 停止队列调度器
|
||
scheduler.stop_queue_scheduler()
|
||
|
||
sys.exit(exit_code)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
`./app\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./app\scheduler_manager\__init__.py`
|
||
```python
|
||
from queue_sqlite.scheduler import QueueScheduler
|
||
from .students.add_student import add_student
|
||
from .students.students_listen import students
|
||
|
||
|
||
scheduler = QueueScheduler()
|
||
|
||
__all__ = [
|
||
"scheduler",
|
||
"add_student",
|
||
"students"
|
||
]
|
||
```
|
||
|
||
`./app\scheduler_manager\students\add_student.py`
|
||
```python
|
||
from queue_sqlite.mounter.task_mounter import TaskMounter
|
||
from queue_sqlite.model import MessageItem
|
||
import time
|
||
|
||
|
||
@TaskMounter.task(meta={"task_name": "add_student"})
|
||
def add_student(message_item: MessageItem):
|
||
# create a new instance of the AddStudentDilalog dialog
|
||
# time.sleep(3)
|
||
print("点击了添加按钮")
|
||
return {"message": "学生添加成功"}
|
||
|
||
```
|
||
|
||
`./app\scheduler_manager\students\students_listen.py`
|
||
```python
|
||
from queue_sqlite.mounter.listen_mounter import ListenMounter
|
||
from ...signal import listen_signals
|
||
import json
|
||
|
||
|
||
@ListenMounter.listener()
|
||
def students(str_students_list: str):
|
||
print("students signal received")
|
||
listen_signals.listening_students_signal.emit(json.loads(str_students_list))
|
||
```
|
||
|
||
`./app\signal\global_signals.py`
|
||
```python
|
||
from PySide6.QtCore import Signal, QObject
|
||
|
||
|
||
class GlobalSignals(QObject):
|
||
|
||
show_add_dialog_signal = Signal(dict)
|
||
|
||
__all__ = ["GlobalSignals"]
|
||
```
|
||
|
||
`./app\signal\listen_signals.py`
|
||
```python
|
||
from PySide6.QtCore import Signal, QObject
|
||
|
||
|
||
class ListenSignals(QObject):
|
||
|
||
listening_students_signal = Signal(list)
|
||
|
||
__all__ = ["ListenSignals"]
|
||
```
|
||
|
||
`./app\signal\__init__.py`
|
||
```python
|
||
from .global_signals import GlobalSignals
|
||
from .listen_signals import ListenSignals
|
||
|
||
global_signals = GlobalSignals()
|
||
listen_signals = ListenSignals()
|
||
|
||
__all__ = ["global_signals"]
|
||
```
|
||
|
||
`./app\style\button_style.py`
|
||
```python
|
||
BUTTON_STYLE = """
|
||
QPushButton {
|
||
border: none;
|
||
padding: 5px 10px;
|
||
font-family: 'Segoe UI', 'Microsoft YaHei';
|
||
font-size: 14px;
|
||
color: white;
|
||
border-radius: 5px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
"""
|
||
|
||
ADD_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #0d6efd;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #0b5ed7;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #0a58ca;
|
||
}
|
||
"""
|
||
|
||
DELETE_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #dc3545;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #bb2d3b;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #b02a37;
|
||
"""
|
||
|
||
BATCH_DELETE_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #fd7e14;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #e96b10;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #dc680f;
|
||
}
|
||
"""
|
||
|
||
UPDATE_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #198754;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #157347;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #146c43;
|
||
}
|
||
"""
|
||
|
||
IMPORT_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #6f42c1;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #5936a2;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #4a2d8e;
|
||
}
|
||
"""
|
||
|
||
EXPORT_BUTTON_STYLE = BUTTON_STYLE + """
|
||
QPushButton {
|
||
background-color: #20c997;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #1aa179;
|
||
}
|
||
QPushButton:pressed{
|
||
background-color: #198b6d;
|
||
}
|
||
"""
|
||
|
||
```
|
||
|
||
`./app\style\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./app\view\__init__.py`
|
||
```python
|
||
# coding:utf-8
|
||
from PySide6.QtCore import QUrl, QSize, QEventLoop, QTimer
|
||
from PySide6.QtGui import QIcon, QDesktopServices
|
||
from qfluentwidgets import (NavigationItemPosition, MessageBox, NavigationAvatarWidget, SplitFluentWindow, SplashScreen)
|
||
from qfluentwidgets import FluentIcon as FIF
|
||
|
||
from .students.student_interface import StudentInterface
|
||
from .video.video_interface import VideoInterface
|
||
from .camera.camera_interface import CameraInterface
|
||
from .web_view.web_view_interface import WebViewInterface
|
||
from PySide6.QtWidgets import QApplication
|
||
|
||
|
||
class Window(SplitFluentWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
self.studemt_interface = StudentInterface()
|
||
self.video_interface = VideoInterface()
|
||
self.camera_interface = CameraInterface()
|
||
self.web_view_interface = WebViewInterface(self)
|
||
self.initNavigation()
|
||
self.resize(900, 700)
|
||
self.setWindowIcon(QIcon('./resources/images/logo.png'))
|
||
|
||
desktop = QApplication.screens()[0].availableGeometry()
|
||
w, h = desktop.width(), desktop.height()
|
||
self.move(w//2 - self.width()//2, h//2 - self.height()//2)
|
||
|
||
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
||
self.splashScreen.setIconSize(QSize(102, 102))
|
||
self.show()
|
||
self.createSubInterface()
|
||
self.splashScreen.finish()
|
||
|
||
def createSubInterface(self):
|
||
loop = QEventLoop(self)
|
||
QTimer.singleShot(2000, loop.quit)
|
||
loop.exec()
|
||
|
||
def initNavigation(self):
|
||
self.addSubInterface(self.studemt_interface, FIF.RINGER, "Students")
|
||
self.addSubInterface(self.video_interface, FIF.VIDEO, "Video")
|
||
self.addSubInterface(self.camera_interface, FIF.CAMERA, "Camera")
|
||
self.addSubInterface(self.web_view_interface, FIF.FIT_PAGE, "Web View")
|
||
|
||
self.navigationInterface.addWidget(
|
||
routeKey="Info",
|
||
widget=NavigationAvatarWidget('Info', 'resource/images/logo.png'),
|
||
onClick=self.showMessageBox,
|
||
position=NavigationItemPosition.BOTTOM,
|
||
)
|
||
|
||
self.navigationInterface.addItem(
|
||
routeKey='settingInterface',
|
||
icon=FIF.SETTING,
|
||
text='设置',
|
||
position=NavigationItemPosition.BOTTOM,
|
||
)
|
||
|
||
self.navigationInterface.setExpandWidth(280)
|
||
|
||
def showMessageBox(self):
|
||
w = MessageBox(
|
||
'支持作者🥰',
|
||
'个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀',
|
||
self
|
||
)
|
||
w.yesButton.setText('来啦老弟')
|
||
w.cancelButton.setText('下次一定')
|
||
|
||
if w.exec():
|
||
QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo"))
|
||
|
||
```
|
||
|
||
`./app\view\camera\camera_interface.py`
|
||
```python
|
||
import sys
|
||
import cv2
|
||
from PySide6.QtCore import Qt, QTimer, Signal, QObject
|
||
from PySide6.QtGui import QImage, QPixmap
|
||
from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget, QMessageBox
|
||
from qfluentwidgets import PushButton
|
||
|
||
|
||
class VideoStream(QObject):
|
||
new_frame = Signal(QImage)
|
||
error_occurred = Signal(str)
|
||
|
||
def __init__(self, rtsp_url):
|
||
super().__init__()
|
||
self.stream_url = rtsp_url # 直接使用RTSP URL
|
||
self.cap = None
|
||
self.timer = QTimer()
|
||
self.timer.timeout.connect(self.update_frame)
|
||
self.retry_count = 0
|
||
self.max_retry = 5
|
||
|
||
self.open_stream()
|
||
|
||
def open_stream(self):
|
||
"""尝试打开RTSP视频流"""
|
||
if self.cap:
|
||
self.cap.release()
|
||
|
||
# 创建VideoCapture对象并设置RTSP参数
|
||
self.cap = cv2.VideoCapture(self.stream_url)
|
||
|
||
# 关键设置:使用TCP传输(避免UDP丢包问题),设置超时和缓冲区
|
||
# self.cap.set(cv2.CAP_PROP_RTSP_TRANSPORT, 1) # 1 = TCP传输模式
|
||
self.cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000) # 5秒超时
|
||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小化缓冲区
|
||
|
||
# 打开RTSP流
|
||
self.cap.open(self.stream_url)
|
||
|
||
if not self.cap.isOpened():
|
||
self.retry_count += 1
|
||
if self.retry_count <= self.max_retry:
|
||
QTimer.singleShot(2000, self.open_stream) # 2秒后重试
|
||
else:
|
||
self.error_occurred.emit(f"无法打开RTSP视频流: {self.stream_url}")
|
||
else:
|
||
self.retry_count = 0
|
||
self.timer.start(30) # 约30fps
|
||
|
||
def update_frame(self):
|
||
if not self.cap or not self.cap.isOpened():
|
||
return
|
||
|
||
ret, frame = self.cap.read()
|
||
if ret:
|
||
# 转换为黑白图像
|
||
# gray_image = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||
# h, w = gray_image.shape
|
||
# qt_image = QImage(gray_image.data, w, h, w, QImage.Format.Format_Grayscale8)
|
||
|
||
# 转换为RGB图像
|
||
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||
h, w, ch = rgb_image.shape
|
||
qt_image = QImage(rgb_image.data, w, h, w * ch, QImage.Format.Format_RGB888)
|
||
|
||
self.new_frame.emit(qt_image)
|
||
else:
|
||
# 读取失败时重新连接
|
||
self.open_stream()
|
||
|
||
def stop(self):
|
||
self.timer.stop()
|
||
if self.cap:
|
||
self.cap.release()
|
||
|
||
class CameraInterface(QWidget):
|
||
def __init__(self, rtsp_url="rtsp://127.0.0.1:8554/stream"):
|
||
super().__init__()
|
||
self.setObjectName("camera_interface")
|
||
self.setWindowTitle("RTSP视频流接收器")
|
||
self.resize(800, 600)
|
||
|
||
# 创建UI
|
||
self.label = QLabel("等待RTSP视频流...")
|
||
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.label.setStyleSheet("font-size: 20px; color: #888;")
|
||
|
||
# 创建按钮
|
||
self.listen_button = PushButton("开始监听RTSP")
|
||
self.listen_button.clicked.connect(lambda: self.start_listen(rtsp_url))
|
||
|
||
layout = QVBoxLayout()
|
||
layout.setContentsMargins(20, 50, 20, 20)
|
||
layout.addWidget(self.label)
|
||
layout.addWidget(self.listen_button)
|
||
self.setLayout(layout)
|
||
|
||
def start_listen(self, rtsp_url):
|
||
self.listen_button.setEnabled(False)
|
||
self.label.setText(f"正在连接: {rtsp_url}...")
|
||
# 创建视频流
|
||
self.stream = VideoStream(rtsp_url)
|
||
self.stream.new_frame.connect(self.display_frame)
|
||
self.stream.error_occurred.connect(self.display_error)
|
||
|
||
def display_frame(self, image):
|
||
pixmap = QPixmap.fromImage(image)
|
||
self.label.setPixmap(pixmap.scaled(
|
||
self.label.width(),
|
||
self.label.height(),
|
||
Qt.AspectRatioMode.KeepAspectRatio,
|
||
Qt.TransformationMode.SmoothTransformation
|
||
))
|
||
|
||
def display_error(self, message):
|
||
QMessageBox.critical(self, "RTSP视频流错误", message)
|
||
self.label.setText(f"错误: {message}")
|
||
self.listen_button.setEnabled(True) # 出错时重新启用按钮
|
||
|
||
def closeEvent(self, event):
|
||
if hasattr(self, 'stream') and self.stream:
|
||
self.stream.stop()
|
||
super().closeEvent(event)
|
||
|
||
if __name__ == "__main__":
|
||
app = QApplication(sys.argv)
|
||
|
||
# 使用命令行参数指定RTSP URL
|
||
rtsp_url = "rtsp://127.0.0.1:8554/stream" # 默认URL
|
||
if len(sys.argv) > 1:
|
||
rtsp_url = sys.argv[1]
|
||
|
||
player = CameraInterface(rtsp_url)
|
||
player.show()
|
||
sys.exit(app.exec())
|
||
|
||
```
|
||
|
||
`./app\view\camera\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./app\view\students\student_dilalog.py`
|
||
```python
|
||
from qfluentwidgets import MessageBoxBase, LineEdit, ComboBox, SubtitleLabel
|
||
from PySide6.QtWidgets import QGridLayout, QLabel
|
||
from PySide6.QtCore import Qt
|
||
from PySide6.QtGui import QFont
|
||
|
||
|
||
class BaseStudentDilalog(MessageBoxBase):
|
||
def __init__(self, title, parent=None):
|
||
super().__init__(parent)
|
||
self.title = title
|
||
self.setup_ui()
|
||
|
||
def setup_ui(self):
|
||
self.titleLabel = SubtitleLabel(self.title, self)
|
||
self.viewLayout.addWidget(self.titleLabel)
|
||
self.viewLayout.setAlignment(self.titleLabel, Qt.AlignmentFlag.AlignCenter)
|
||
|
||
grid_layout = QGridLayout()
|
||
self.viewLayout.addLayout(grid_layout)
|
||
|
||
self.nameInput = LineEdit(self)
|
||
self.numberInput = LineEdit(self)
|
||
self.genderCombo = ComboBox(self)
|
||
self.genderCombo.addItems(["男", "女"])
|
||
self.classCombo = ComboBox(self)
|
||
self.chineseInput = LineEdit(self)
|
||
self.mathInput = LineEdit(self)
|
||
self.englishInput = LineEdit(self)
|
||
|
||
fields = [
|
||
("姓名: ", self.nameInput),
|
||
("学号: ", self.numberInput),
|
||
("性别: ", self.genderCombo),
|
||
("班级: ", self.classCombo),
|
||
("语文: ", self.chineseInput),
|
||
("数学: ", self.mathInput),
|
||
("英语: ", self.englishInput),
|
||
]
|
||
|
||
for row, (label_text, widget) in enumerate(fields):
|
||
label_widget = QLabel(label_text)
|
||
font = QFont("微软雅黑")
|
||
font.setBold(True)
|
||
label_widget.setFont(font)
|
||
grid_layout.addWidget(label_widget, row, 0)
|
||
grid_layout.addWidget(widget, row, 1)
|
||
|
||
grid_layout.setColumnStretch(1, 1)
|
||
grid_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||
|
||
self.yesButton.setText('确定')
|
||
self.cancelButton.setText('取消')
|
||
|
||
for widget in [field[1] for field in fields]:
|
||
widget.setMinimumWidth(200)
|
||
|
||
self.nameInput.setFocus()
|
||
|
||
self.yesButton.setDefault(False)
|
||
self.yesButton.setAutoDefault(False)
|
||
|
||
|
||
class AddStudentDilalog(BaseStudentDilalog):
|
||
def __init__(self, parent=None):
|
||
super().__init__("添加学生", parent)
|
||
self.student_id = None
|
||
self.yesButton.setText('添加')
|
||
```
|
||
|
||
`./app\view\students\student_interface.py`
|
||
```python
|
||
import json
|
||
|
||
from PySide6.QtGui import QIcon
|
||
from PySide6.QtCore import Slot
|
||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QHeaderView, QCheckBox, QTableWidgetItem
|
||
|
||
from queue_sqlite.model import MessageItem
|
||
|
||
from qfluentwidgets import CardWidget, PushButton, SearchLineEdit, TableWidget, setCustomStyleSheet
|
||
|
||
from .student_dilalog import AddStudentDilalog
|
||
from ...style.button_style import ADD_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE
|
||
from ...signal import global_signals
|
||
from ...signal import listen_signals
|
||
from ...scheduler_manager import scheduler
|
||
|
||
|
||
class StudentInterface(QWidget):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setObjectName("student_interface")
|
||
self.students = []
|
||
self.setup_ui()
|
||
|
||
self.create_signal_slot()
|
||
self.load_data()
|
||
self.populate_table(self.students)
|
||
|
||
def create_signal_slot(self):
|
||
global_signals.show_add_dialog_signal.connect(self.show_add_student_dialog)
|
||
listen_signals.listening_students_signal.connect(self.populate_table)
|
||
|
||
def setup_ui(self):
|
||
# 设置 title
|
||
self.setWindowTitle("学生管理")
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(20, 50, 20, 20) # (左, 上, 右, 下)
|
||
card_widget = CardWidget(self)
|
||
buttons_layout = QHBoxLayout(card_widget)
|
||
|
||
self.addButton = PushButton("新增", self)
|
||
setCustomStyleSheet(self.addButton, ADD_BUTTON_STYLE, ADD_BUTTON_STYLE)
|
||
self.addButton.clicked.connect(self.add_student)
|
||
|
||
self.searchInput = SearchLineEdit(self)
|
||
self.searchInput.setPlaceholderText("搜索学生姓名或学号...")
|
||
self.searchInput.setFixedWidth(500)
|
||
self.batchDeleteButton = PushButton("批量删除", self)
|
||
setCustomStyleSheet(self.batchDeleteButton, BATCH_DELETE_BUTTON_STYLE, BATCH_DELETE_BUTTON_STYLE)
|
||
|
||
buttons_layout.addWidget(self.addButton)
|
||
buttons_layout.addWidget(self.searchInput)
|
||
buttons_layout.addStretch(2)
|
||
buttons_layout.addWidget(self.batchDeleteButton)
|
||
|
||
layout.addWidget(card_widget)
|
||
|
||
self.table_widget = TableWidget(self)
|
||
self.table_widget.setBorderRadius(8) # 设置边框圆角
|
||
self.table_widget.setBorderVisible(True) # 设置表格边框可见
|
||
table_labels = ["", "学生ID", "姓名", "学号", "性别", "班级", "语文", "数学", "英语", "总分", "操作"]
|
||
self.table_widget.setColumnCount(len(table_labels))
|
||
self.table_widget.setHorizontalHeaderLabels(table_labels)
|
||
self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||
|
||
layout.addWidget(self.table_widget)
|
||
|
||
self.setStyleSheet("StudentInterface {background: white}")
|
||
self.setWindowIcon(QIcon('./resources/images/logo.png'))
|
||
|
||
def load_data(self):
|
||
self.students = [
|
||
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265},
|
||
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265},
|
||
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265},
|
||
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265},
|
||
{"student_id": 1, "student_name": "张三", "student_number": "2024010101", "gender": 1, "class_name": "1班", "chinese_score": 80, "math_score": 85, "english_score": 90, "total_score": 265},
|
||
]
|
||
scheduler.update_listen_data("students", json.dumps(self.students))
|
||
|
||
@Slot(list)
|
||
def populate_table(self, students: list):
|
||
print(f"Received students: {students}")
|
||
self.table_widget.setRowCount(len(students))
|
||
for row, student_info in enumerate(students):
|
||
self.setup_table_row(row, student_info)
|
||
|
||
def setup_table_row(self, row, student_info):
|
||
checkbox = QCheckBox()
|
||
checkbox.setStyleSheet("margin: 10px")
|
||
self.table_widget.setCellWidget(row, 0, checkbox)
|
||
for col, key in enumerate(self.students[0].keys()):
|
||
value = student_info.get(key, "")
|
||
if key == "gender":
|
||
value = "男" if value == 1 else "女" if value == 2 else "未知"
|
||
item = QTableWidgetItem(str(value))
|
||
self.table_widget.setItem(row, col+1, item)
|
||
|
||
@Slot(dict)
|
||
def show_add_student_dialog(self, message_dict: dict):
|
||
message = MessageItem.from_dict(message_dict)
|
||
print(f"Received message: {message}")
|
||
print("Creating and showing AddStudentDialog")
|
||
self.addButton.setEnabled(True)
|
||
add_student_dialog = AddStudentDilalog(self)
|
||
if add_student_dialog.exec():
|
||
print("用户点击了添加按钮")
|
||
else:
|
||
print("用户点击了取消按钮")
|
||
|
||
def add_student_callback(self, message: MessageItem):
|
||
global_signals.show_add_dialog_signal.emit(message.to_dict())
|
||
|
||
def add_student(self):
|
||
self.addButton.setEnabled(False)
|
||
scheduler.send_message(MessageItem(content={}, destination="add_student"), self.add_student_callback)
|
||
|
||
```
|
||
|
||
`./app\view\students\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./app\view\video\video_interface.py`
|
||
```python
|
||
from PySide6.QtCore import QUrl, Qt
|
||
from PySide6.QtGui import QFont # 导入QFont
|
||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog, QHBoxLayout
|
||
from qfluentwidgets import setTheme, Theme, FluentIcon, PushButton, PrimaryPushButton
|
||
from qfluentwidgets.multimedia import VideoWidget
|
||
|
||
|
||
class VideoInterface(QWidget):
|
||
def __init__(self):
|
||
super().__init__()
|
||
from PySide6.QtMultimedia import QMediaPlayer
|
||
self.player = QMediaPlayer()
|
||
self.setObjectName("video_interface")
|
||
self.setWindowTitle("Fluent Video Player")
|
||
self.resize(900, 600)
|
||
|
||
# 设置主题
|
||
# setTheme(Theme.DARK)
|
||
|
||
# 创建主布局
|
||
self.main_layout = QVBoxLayout(self)
|
||
self.main_layout.setContentsMargins(20, 50, 20, 20)
|
||
self.main_layout.setSpacing(15)
|
||
|
||
# 创建标题标签 - 修复字体设置
|
||
self.title_label = QLabel("Fluent Video Player")
|
||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
# 正确设置字体:使用QFont.Bold表示粗体
|
||
title_font = QFont()
|
||
title_font.setPointSize(24)
|
||
title_font.setBold(True) # 使用setBold方法设置粗体
|
||
self.title_label.setFont(title_font)
|
||
|
||
self.main_layout.addWidget(self.title_label)
|
||
|
||
# 创建视频播放器
|
||
self.video_widget = VideoWidget()
|
||
self.video_widget.setMinimumHeight(400)
|
||
self.main_layout.addWidget(self.video_widget, 1)
|
||
|
||
# 创建控制面板
|
||
self.control_layout = QHBoxLayout()
|
||
self.control_layout.setContentsMargins(10, 10, 10, 10)
|
||
self.control_layout.setSpacing(15)
|
||
self.main_layout.addLayout(self.control_layout)
|
||
|
||
# 添加控制按钮
|
||
self.open_button = PushButton(FluentIcon.FOLDER_ADD, "打开视频")
|
||
self.open_button.setFixedWidth(120)
|
||
self.open_button.clicked.connect(self.open_video)
|
||
self.control_layout.addWidget(self.open_button)
|
||
|
||
|
||
# 添加状态标签 - 修复字体设置
|
||
self.status_label = QLabel("准备就绪")
|
||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
status_font = QFont()
|
||
status_font.setPointSize(12)
|
||
self.status_label.setFont(status_font)
|
||
self.control_layout.addWidget(self.status_label)
|
||
|
||
# 设置初始状态
|
||
self.video_loaded = False
|
||
self.is_fullscreen = False
|
||
|
||
# 设置样式
|
||
# self.setStyleSheet("""
|
||
# QWidget {
|
||
# background-color: #2b2b2b;
|
||
# color: #f0f0f0;
|
||
# }
|
||
# QLabel {
|
||
# padding: 5px;
|
||
# }
|
||
# """)
|
||
|
||
def open_video(self):
|
||
"""打开视频文件"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "选择视频文件", "",
|
||
"视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)"
|
||
)
|
||
|
||
if file_path:
|
||
# 设置视频URL
|
||
self.video_widget.setVideo(QUrl.fromLocalFile(file_path))
|
||
self.status_label.setText(f"已加载视频: {file_path.split('/')[-1]}")
|
||
self.video_loaded = True
|
||
|
||
def play_video(self):
|
||
"""播放视频"""
|
||
if self.video_loaded:
|
||
self.video_widget.play()
|
||
self.status_label.setText("正在播放...")
|
||
|
||
def pause_video(self):
|
||
"""暂停视频"""
|
||
if self.video_loaded:
|
||
self.video_widget.pause()
|
||
self.status_label.setText("已暂停")
|
||
|
||
def stop_video(self):
|
||
"""停止视频"""
|
||
if self.video_loaded:
|
||
self.video_widget.stop()
|
||
self.status_label.setText("已停止")
|
||
|
||
def toggle_fullscreen(self):
|
||
"""切换全屏模式"""
|
||
if self.is_fullscreen:
|
||
self.showNormal()
|
||
else:
|
||
self.showFullScreen()
|
||
self.is_fullscreen = True
|
||
|
||
|
||
# if __name__ == "__main__":
|
||
# app = QApplication(sys.argv)
|
||
|
||
# # 设置应用程序样式
|
||
# from qfluentwidgets import setThemeColor
|
||
# setThemeColor('#28a9e0')
|
||
|
||
# player = VideoPlayer()
|
||
# player.show()
|
||
# sys.exit(app.exec())
|
||
```
|
||
|
||
`./app\view\video\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./app\view\web_view\web_view_interface.py`
|
||
```python
|
||
# coding:utf-8
|
||
import sys
|
||
from PySide6.QtWidgets import QFrame, QVBoxLayout, QMenu, QApplication, QWidget
|
||
from qframelesswindow.webengine import FramelessWebEngineView
|
||
from PySide6.QtWebChannel import QWebChannel
|
||
from PySide6.QtCore import QTimer, Slot, QPoint, Qt, QUrl
|
||
from PySide6.QtWebEngineCore import QWebEnginePage
|
||
|
||
import time
|
||
import os
|
||
|
||
class DevToolsWindow(FramelessWebEngineView):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent=parent)
|
||
self.setWindowTitle("开发者工具")
|
||
self.resize(600, 600)
|
||
|
||
class DevToolsView(QWidget):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent=parent)
|
||
self.setWindowTitle("开发者工具")
|
||
self.dev_tools_window = DevToolsWindow(self)
|
||
self.resize(600, 600)
|
||
# 设置窗口大小不可变
|
||
self.setFixedSize(self.size())
|
||
|
||
class WebViewInterface(QFrame):
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent=parent)
|
||
self.setObjectName("webViewInterface")
|
||
self.dev_tools_view = None
|
||
|
||
self.webView = FramelessWebEngineView(self)
|
||
self.channel = QWebChannel()
|
||
self.channel.registerObject('py', self)
|
||
html_path = os.getcwd().replace("\\", '/') + '/resources/webview/test_webview/dist/index.html'
|
||
# html_path = "http://localhost:8080/"
|
||
self.webView.load(QUrl(html_path))
|
||
self.page = self.webView.page()
|
||
self.page.setWebChannel(self.channel)
|
||
self.timer = QTimer(self)
|
||
# self.timer.timeout.connect(self.send_time)
|
||
self.timer.start(100)
|
||
|
||
self.vBoxLayout = QVBoxLayout(self)
|
||
self.vBoxLayout.setContentsMargins(0, 48, 0, 0)
|
||
self.vBoxLayout.addWidget(self.webView)
|
||
self.webView.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||
self.webView.loadFinished.connect(self.on_load_finished)
|
||
self.webView.customContextMenuRequested.connect(self.show_context_menu)
|
||
|
||
|
||
def on_load_finished(self, success):
|
||
"""页面加载完成后初始化开发者工具"""
|
||
if success:
|
||
# 预初始化开发者工具
|
||
self.init_dev_tools()
|
||
|
||
def init_dev_tools(self):
|
||
"""初始化开发者工具"""
|
||
if not self.dev_tools_view:
|
||
# self.dev_tools_window = DevToolsWindow()
|
||
self.dev_tools_view = DevToolsView()
|
||
self.page.setDevToolsPage(self.dev_tools_view.dev_tools_window.page())
|
||
|
||
@Slot(str, result=str)
|
||
def hello(self, message):
|
||
"""js调用python测试"""
|
||
print('call received')
|
||
return f'hello from python: {message}'
|
||
|
||
def send_time(self):
|
||
"""python调用js测试"""
|
||
self.page.runJavaScript(f'console.log(app_sendMessage)')
|
||
|
||
def show_context_menu(self, pos: QPoint):
|
||
"""显示自定义右键菜单"""
|
||
# 创建上下文菜单
|
||
menu = QMenu(self)
|
||
|
||
# 添加标准浏览器动作
|
||
back_action = menu.addAction("后退")
|
||
back_action.triggered.connect(self.webView.back)
|
||
|
||
forward_action = menu.addAction("前进")
|
||
forward_action.triggered.connect(self.webView.forward)
|
||
|
||
reload_action = menu.addAction("重新加载")
|
||
reload_action.triggered.connect(self.webView.reload)
|
||
|
||
menu.addSeparator()
|
||
|
||
# 添加开发者工具相关动作
|
||
dev_tools_action = menu.addAction("打开开发者工具")
|
||
dev_tools_action.triggered.connect(self.toggle_dev_tools)
|
||
|
||
menu.addSeparator()
|
||
|
||
# 在鼠标位置显示菜单
|
||
menu.exec(self.webView.mapToGlobal(pos))
|
||
|
||
def toggle_dev_tools(self):
|
||
"""切换开发者工具窗口显示状态"""
|
||
if not self.dev_tools_view:
|
||
self.init_dev_tools()
|
||
|
||
if self.dev_tools_view is None:
|
||
return
|
||
|
||
if self.dev_tools_view.dev_tools_window.isVisible():
|
||
self.dev_tools_view.hide()
|
||
self.dev_tools_view.dev_tools_window.hide()
|
||
else:
|
||
self.dev_tools_view.show()
|
||
self.dev_tools_view.dev_tools_window.show()
|
||
self.dev_tools_view.dev_tools_window.raise_() # 将窗口置于最前
|
||
|
||
def closeEvent(self, event):
|
||
"""关闭时清理资源"""
|
||
if self.dev_tools_view:
|
||
self.dev_tools_view.close()
|
||
super().closeEvent(event)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = QApplication(sys.argv)
|
||
window = WebViewInterface()
|
||
window.resize(1000, 700)
|
||
window.show()
|
||
sys.exit(app.exec())
|
||
|
||
```
|
||
|
||
`./app\view\web_view\__init__.py`
|
||
```python
|
||
|
||
```
|
||
|
||
`./resources\webview\test_webview\node_modules\flatted\python\flatted.py`
|
||
```python
|
||
# ISC License
|
||
#
|
||
# Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection
|
||
#
|
||
# Permission to use, copy, modify, and/or distribute this software for any
|
||
# purpose with or without fee is hereby granted, provided that the above
|
||
# copyright notice and this permission notice appear in all copies.
|
||
#
|
||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||
# PERFORMANCE OF THIS SOFTWARE.
|
||
|
||
import json as _json
|
||
|
||
class _Known:
|
||
def __init__(self):
|
||
self.key = []
|
||
self.value = []
|
||
|
||
class _String:
|
||
def __init__(self, value):
|
||
self.value = value
|
||
|
||
|
||
def _array_keys(value):
|
||
keys = []
|
||
i = 0
|
||
for _ in value:
|
||
keys.append(i)
|
||
i += 1
|
||
return keys
|
||
|
||
def _object_keys(value):
|
||
keys = []
|
||
for key in value:
|
||
keys.append(key)
|
||
return keys
|
||
|
||
def _is_array(value):
|
||
return isinstance(value, (list, tuple))
|
||
|
||
def _is_object(value):
|
||
return isinstance(value, dict)
|
||
|
||
def _is_string(value):
|
||
return isinstance(value, str)
|
||
|
||
def _index(known, input, value):
|
||
input.append(value)
|
||
index = str(len(input) - 1)
|
||
known.key.append(value)
|
||
known.value.append(index)
|
||
return index
|
||
|
||
def _loop(keys, input, known, output):
|
||
for key in keys:
|
||
value = output[key]
|
||
if isinstance(value, _String):
|
||
_ref(key, input[int(value.value)], input, known, output)
|
||
|
||
return output
|
||
|
||
def _ref(key, value, input, known, output):
|
||
if _is_array(value) and value not in known:
|
||
known.append(value)
|
||
value = _loop(_array_keys(value), input, known, value)
|
||
elif _is_object(value) and value not in known:
|
||
known.append(value)
|
||
value = _loop(_object_keys(value), input, known, value)
|
||
|
||
output[key] = value
|
||
|
||
def _relate(known, input, value):
|
||
if _is_string(value) or _is_array(value) or _is_object(value):
|
||
try:
|
||
return known.value[known.key.index(value)]
|
||
except:
|
||
return _index(known, input, value)
|
||
|
||
return value
|
||
|
||
def _transform(known, input, value):
|
||
if _is_array(value):
|
||
output = []
|
||
for val in value:
|
||
output.append(_relate(known, input, val))
|
||
return output
|
||
|
||
if _is_object(value):
|
||
obj = {}
|
||
for key in value:
|
||
obj[key] = _relate(known, input, value[key])
|
||
return obj
|
||
|
||
return value
|
||
|
||
def _wrap(value):
|
||
if _is_string(value):
|
||
return _String(value)
|
||
|
||
if _is_array(value):
|
||
i = 0
|
||
for val in value:
|
||
value[i] = _wrap(val)
|
||
i += 1
|
||
|
||
elif _is_object(value):
|
||
for key in value:
|
||
value[key] = _wrap(value[key])
|
||
|
||
return value
|
||
|
||
def parse(value, *args, **kwargs):
|
||
json = _json.loads(value, *args, **kwargs)
|
||
wrapped = []
|
||
for value in json:
|
||
wrapped.append(_wrap(value))
|
||
|
||
input = []
|
||
for value in wrapped:
|
||
if isinstance(value, _String):
|
||
input.append(value.value)
|
||
else:
|
||
input.append(value)
|
||
|
||
value = input[0]
|
||
|
||
if _is_array(value):
|
||
return _loop(_array_keys(value), input, [value], value)
|
||
|
||
if _is_object(value):
|
||
return _loop(_object_keys(value), input, [value], value)
|
||
|
||
return value
|
||
|
||
|
||
def stringify(value, *args, **kwargs):
|
||
known = _Known()
|
||
input = []
|
||
output = []
|
||
i = int(_index(known, input, value))
|
||
while i < len(input):
|
||
output.append(_transform(known, input, input[i]))
|
||
i += 1
|
||
return _json.dumps(output, *args, **kwargs)
|
||
|
||
```
|
||
|
||
`./resources\webview\test_webview\node_modules\shell-quote\print.py`
|
||
```python
|
||
#!/usr/bin/env python3
|
||
import sys
|
||
print(sys.argv[1])
|
||
|
||
```
|
||
|