from functools import wraps from typing import Literal, Callable from PySide6.QtCore import Qt, Signal, QThread, QEvent from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QFileDialog, QButtonGroup, QWidget, QApplication from qfluentwidgets import GroupHeaderCardWidget, PushButton, IconWidget, BodyLabel, PrimaryPushButton, FluentIcon, \ LineEdit, RadioButton, HyperlinkButton, FlyoutViewBase, TeachingTip, TeachingTipTailPosition from module.worker import DTGWorker from ui import MAIN_THEME_COLOR from ui.components.infobar import ProgressInfoBar from ui.components.widget import Widget, MyGroupHeaderCardWidget from utils.function import open_template class ChooseTemplateView(FlyoutViewBase): closed = Signal() def __init__(self, parent=None): super().__init__(parent) self.vBoxLayout = QVBoxLayout(self) QApplication.instance().installEventFilter(self) def paintEvent(self, e): ... def eventFilter(self, watched, event): if event.type() == QEvent.MouseButtonPress: if not self.rect().contains(self.mapFromGlobal(event.globalPosition().toPoint())): self.closed.emit() return super().eventFilter(watched, event) def addTemplate(self, content: str, cb: Callable[[], None]): label = HyperlinkButton("", content) self.vBoxLayout.addWidget(label) label.clicked.connect(cb) label.clicked.connect(self.closed.emit) class InitSettingCard(MyGroupHeaderCardWidget): chooseSignal = Signal(str, str) def __init__(self, parent=None): super().__init__(parent) self.setTitle("输入选项") self.setBorderRadius(8) self.sBtnHBoxLayout = QHBoxLayout(self) self.qBtnHBoxLayout = QHBoxLayout(self) self.sTemplateButton = HyperlinkButton("", "下载模板") self.chooseStudentButton = PushButton("打开") self.qTemplateButton = HyperlinkButton("", "下载模板") self.chooseQuestionButton = PushButton("打开") self.chooseStudentButton.setFixedWidth(120) self.chooseQuestionButton.setFixedWidth(120) self.sBtnHBoxLayout.addWidget(self.sTemplateButton) self.sBtnHBoxLayout.addWidget(self.chooseStudentButton) self.qBtnHBoxLayout.addWidget(self.qTemplateButton) self.qBtnHBoxLayout.addWidget(self.chooseQuestionButton) self.stuGroup = self.addGroup(FluentIcon.DOCUMENT, "学生名单", "选择学生名单文件", self.sBtnHBoxLayout) self.QueGroup = self.addGroup(FluentIcon.DOCUMENT, "题库", "选择题库文件", self.qBtnHBoxLayout) self.chooseStudentButton.clicked.connect( lambda: self.choose_file(self.stuGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('s', x))) self.chooseQuestionButton.clicked.connect( lambda: self.choose_file(self.QueGroup.setContent, "已选择文件:", lambda x: self.chooseSignal.emit('q', x))) self.qTemplateButton.clicked.connect(lambda: open_template('template-defense-paper-questions.xlsm', self)) self.sTemplateButton.clicked.connect(self.show_template_list_view) def choose_file( self, set_value: Callable[[str], None] = None, prefix: str = "", cb: Callable[[str], None] = None ) -> None: file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Excel 文件 (*.xlsx *.xlsm);") if file_path and set_value: set_value(prefix + file_path) if cb: cb(file_path) def show_template_list_view(self): view = ChooseTemplateView(self) view.addTemplate("普通模板", lambda: open_template("template-defense-paper-student-1.xlsm", self)) view.addTemplate("达成度模板", lambda: open_template("template-defense-paper-student-2.xlsm", self)) w = TeachingTip.make( target=self.sTemplateButton, view=view, tailPosition=TeachingTipTailPosition.TOP, duration=-1, parent=self ) view.closed.connect(w.close) class ExportSettingsCard(GroupHeaderCardWidget): startSignal = Signal() updateSignal = Signal(str, str) def __init__(self, parent=None): super().__init__(parent) self.setTitle("输出选项") self.setBorderRadius(8) self.chooseExportDirectoryButton = PushButton("选择") self.exportFileNameLineEdit = LineEdit() self.startButton = PrimaryPushButton(FluentIcon.PLAY_SOLID, "开始") self.pdfRadio = RadioButton("PDF") self.wordRadio = RadioButton("Word") self.radioWidget = QWidget(self) self.radioHbox = QHBoxLayout(self.radioWidget) self.radioGroup = QButtonGroup(self.radioWidget) self.radioGroup.addButton(self.pdfRadio) self.radioGroup.addButton(self.wordRadio) self.radioHbox.addWidget(self.pdfRadio) self.radioHbox.addWidget(self.wordRadio) self.pdfRadio.setChecked(True) self.hintIcon = IconWidget(FluentIcon.INFO.icon(color=MAIN_THEME_COLOR)) self.hintLabel = BodyLabel("点击开始按钮以开始生成 👉") self.chooseExportDirectoryButton.setFixedWidth(120) self.startButton.setFixedWidth(120) self.exportFileNameLineEdit.setFixedWidth(360) self.exportFileNameLineEdit.setPlaceholderText("输入导出文件名,例如:21工程管理(1)答辩题目") self.bottomLayout = QHBoxLayout() self.startButton.setEnabled(False) # 设置底部工具栏布局 self.hintIcon.setFixedSize(16, 16) self.bottomLayout.setSpacing(10) self.bottomLayout.setContentsMargins(24, 15, 24, 20) self.bottomLayout.addWidget(self.hintIcon, 0, Qt.AlignLeft) self.bottomLayout.addWidget(self.hintLabel, 0, Qt.AlignLeft) self.bottomLayout.addStretch(1) self.bottomLayout.addWidget(self.startButton, 0, Qt.AlignRight) self.bottomLayout.setAlignment(Qt.AlignVCenter) self.dirGroup = self.addGroup(FluentIcon.FOLDER, "导出目录", "选择导出文件的目录", self.chooseExportDirectoryButton) self.fnGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件名", "输入导出文件的名称", self.exportFileNameLineEdit) self.exportFormatGroup = self.addGroup(FluentIcon.DOCUMENT, "导出文件格式", "选择导出文件的格式", self.radioWidget) self.exportFormatGroup.setSeparatorVisible(True) self.vBoxLayout.addLayout(self.bottomLayout) # ============== self.chooseExportDirectoryButton.clicked.connect(self.choose_dir) self.startButton.clicked.connect(self.startSignal.emit) self.exportFileNameLineEdit.textChanged.connect(lambda x: self.updateSignal.emit('n', x)) def choose_dir(self) -> None: dir_path = QFileDialog.getExistingDirectory(self, "选择文件夹", "") if dir_path: self.dirGroup.setContent(f"当前保存的文件目录:{dir_path}") self.updateSignal.emit('d', dir_path) def update_export_setting_by_signal(self, path: str) -> None: f_dir = path[:path.rfind('/')] f_name = path[path.rfind('/') + 1:path.rfind('.')] + '-答辩题目' self.dirGroup.setContent(f"当前保存的文件目录:{f_dir}") self.updateSignal.emit('d', f_dir) self.exportFileNameLineEdit.setText(f_name) self.updateSignal.emit('n', f_name) class DefenseWidget(Widget): errorSignal = Signal(str, str) def __init__(self, key: str, parent=None): super().__init__(key, parent) self.initCard = InitSettingCard(self) self.exportCard = ExportSettingsCard(self) self.vbox = QVBoxLayout(self) self.vbox.addWidget(self.initCard) self.vbox.addWidget(self.exportCard) self.vbox.addStretch(1) self.pib = ProgressInfoBar(parent=self, isClosable=False, duration=-1) self.pib.hide() self.thread = None self.worker = None self.successFlag = True # =================================== self.input_student_filepath = None self.input_question_filepath = None self.output_filepath = None self.output_filename = None # ==================================== self.initCard.chooseSignal.connect(self.input_signal_receive) self.exportCard.updateSignal.connect(self.export_update_signal_receive) self.exportCard.startSignal.connect(self.start_generate) def show_info_bar(self): self.pib.show() self.pib.set_title('请稍后') def set_pb_value(self, value: int) -> None: if value == -1: self.successFlag = False self.pib.set_progress(value) def set_pb_msg(self, value: str) -> None: self.pib.set_title(value) def enable_start_check(func: Callable): @wraps(func) def wrapper(self, *args, **kwargs): result = func(self, *args, **kwargs) fields = [ self.input_student_filepath, self.input_question_filepath, self.output_filepath, self.output_filename ] if all(fields): self.exportCard.startButton.setEnabled(True) else: self.exportCard.startButton.setEnabled(False) return result return wrapper @enable_start_check def set_value(self, key: str, value: str) -> None: if key == "input_student_filepath": self.input_student_filepath = value elif key == "input_question_filepath": self.input_question_filepath = value elif key == "output_filepath": self.output_filepath = value elif key == "output_filename": self.output_filename = value def input_signal_receive(self, s_type: Literal['s', 'q'], value: str): if s_type == "s": self.set_value("input_student_filepath", value) self.exportCard.update_export_setting_by_signal(value) elif s_type == "q": self.set_value("input_question_filepath", value) def export_update_signal_receive(self, s_type: Literal['d', 'n'], value: str): if s_type == "d": self.set_value("output_filepath", value) elif s_type == "n": self.set_value("output_filename", value) def start_generate(self): self.thread = QThread() self.worker = DTGWorker( self.input_student_filepath, self.input_question_filepath, self.output_filepath, self.output_filename, self.exportCard.radioGroup.checkedButton().text().lower() ) self.worker.moveToThread(self.thread) self.show_info_bar() self.successFlag = True self.exportCard.startButton.setEnabled(False) # 线程启动与信号连接 self.thread.started.connect(self.worker.run) self.worker.progress[int].connect(self.set_pb_value) self.worker.progress[str].connect(self.set_pb_msg) self.worker.error.connect(self.show_error) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.finished.connect(self.clear_thread_worker_refs) self.thread.finished.connect(self.after_generate) # 启动线程 self.thread.start() def clear_thread_worker_refs(self): self.thread = None self.worker = None def after_generate(self): self.exportCard.startButton.setEnabled(True) if self.successFlag: self.pib.show_success(content="正在打开文件...") else: self.pib.show_error() def show_error(self, title: str, content: str): self.errorSignal.emit(title, content)