from sys import exit from PySide6 import QtCore, QtWidgets, QtGui from PySide6.QtGui import QFont from utils import read_excel, save_to_excel from solve import solve_program from datetime import datetime from traceback import format_exc # 约束条件配置 CONSTRAINTS_CONFIG = [ ("每班次人数:", "5", "8"), ("每班次电脑或电器的老人数:", "", "2"), ("每班次老人数:", "1", ""), ("每班次小朋友数:", "2", "") ] # 权重条件配置 WEIGHTS_CONFIG = [ ("目标 1 权重:每班人数平均程度", "1.0"), ("目标 2 权重:每班技术部老人", "1.0"), ("目标 3 权重:每班技术部小朋友", "1.0"), ("目标 4 权重:每班人资部小朋友", "0.5"), ("目标 5 权重:每班部门平均程度", "0.5") ] # 参数调优顺序和尝试值 PARAM_CONFIGS = [ ('num_min', [5, 4, 3, 2, 1]), ('num_max', [8, 9, 10, 11, 12, 13, 14, 15]), ('num_old_min', [2, 1, 0]), ('num_old_max', [3, 4, 5, 6, 7, 8]), ('num_new_min', [1, 0]), ('num_tech_old_min', [1, 0]), ('num_tech_old_max', [1, 2, 3, 4, 5, 6, 7, 8]), ] # 工作线程类,用于在后台执行求解任务 class SolverThread(QtCore.QThread): # 定义信号用于线程与主界面通信 log_signal = QtCore.Signal(str) # 发送日志消息 finished_signal = QtCore.Signal(object, dict) # 发送求解结果 (vars, params) error_signal = QtCore.Signal(str) # 发送错误消息 def __init__(self, preference_mat, depart_mat, want_num_array, is_new_array, auto_mode, manual_params=None, parent=None): super().__init__(parent) self.preference_mat = preference_mat self.depart_mat = depart_mat self.want_num_array = want_num_array self.is_new_array = is_new_array self.auto_mode = auto_mode self.manual_params = manual_params or {} def run(self): try: vars = None final_params = {} if self.auto_mode: self.log_signal.emit("自动调参开始...") auto_params = { 'num_min': None, 'num_max': None, 'num_tech_old_min': None, 'num_tech_old_max': None, 'num_old_min': None, 'num_old_max': None, 'num_new_min': None, 'num_new_max': None, 'weights': self.manual_params.get('weights', [1.0, 1.0, 1.0, 0.5, 0.5]) } # 逐步优化参数 for param_name, try_values in PARAM_CONFIGS: value = None vars = None for try_value in try_values: self.log_signal.emit(f"尝试 {param_name} = {try_value}...") test_params = auto_params.copy() test_params[param_name] = try_value vars = solve_program( preference_mat=self.preference_mat, depart_mat=self.depart_mat, want_num_array=self.want_num_array, is_new_array=self.is_new_array, **test_params ) if vars: value = try_value self.log_signal.emit(f"✓ {param_name} = {try_value} 成功") break else: self.log_signal.emit(f"✗ {param_name} = {try_value} 失败") if value: auto_params[param_name] = value else: break final_params = auto_params else: self.log_signal.emit("计算最优解中...") vars = solve_program( preference_mat=self.preference_mat, want_num_array=self.want_num_array, depart_mat=self.depart_mat, is_new_array=self.is_new_array, **self.manual_params ) final_params = self.manual_params # 发送完成信号 self.finished_signal.emit(vars, final_params) except Exception as e: self.error_signal.emit(format_exc()) class MyWidget(QtWidgets.QWidget): def __init__(self): super().__init__() self.solver_thread = None # 用于存储求解线程 self.main_layout = QtWidgets.QVBoxLayout(self) bold_font = QFont() bold_font.setBold(True) thin_font = QFont() thin_font.setBold(False) # 选择文件部分 self._excel_dir = None self.group_box_1 = QtWidgets.QGroupBox("Step 1. 选择问卷星导出的表格文件") self.group_box_1.setFont(bold_font) self.openfile_layout = QtWidgets.QVBoxLayout() self.button_openfile = QtWidgets.QPushButton("选择文件") self.button_openfile.setFont(thin_font) self.button_openfile.clicked.connect(self.open_file) self.label_openfile = QtWidgets.QLabel("未选择文件") self.label_openfile.setFont(thin_font) self.openfile_layout.addWidget(self.button_openfile) self.openfile_layout.addWidget(self.label_openfile) self.group_box_1.setLayout(self.openfile_layout) self.main_layout.addWidget(self.group_box_1) # 限制条件部分 self.group_box_2 = QtWidgets.QGroupBox("Step 2. 输入限制条件,权重参数均已归一化") self.group_box_2.setFont(bold_font) self.cond_layout_overall = QtWidgets.QVBoxLayout() self.group_box_2.setLayout(self.cond_layout_overall) # 创建权重输入框 self.weight_widgets = {} for i, (label_text, value) in enumerate(WEIGHTS_CONFIG, 1): layout, edit = self._create_weight_row(label_text, value, thin_font) self.weight_widgets[f'layout_{i}'] = layout self.weight_widgets[f'edit_{i}'] = edit self.cond_layout_overall.addLayout(layout) # 自动模式开关 self._auto_mode = True self.cond_layout_0 = QtWidgets.QVBoxLayout() self.auto_switch = QtWidgets.QCheckBox("自动调参模式") self.auto_switch.setTristate(False) # 禁用三态模式 self.auto_switch.setChecked(True) self.auto_switch.stateChanged.connect(self.on_switch_toggled) self.auto_switch.setFont(thin_font) self.cond_layout_0.addWidget(self.auto_switch) self.cond_layout_overall.addLayout(self.cond_layout_0) # 创建限制条件输入框 self.constraint_widgets = {} for i, (label_text, min_value, max_value) in enumerate(CONSTRAINTS_CONFIG, 1): layout, min_edit, max_edit = self._create_constraint_row(label_text, min_value, max_value, thin_font) self.constraint_widgets[f'layout_{i}'] = layout self.constraint_widgets[f'min_{i}'] = min_edit self.constraint_widgets[f'max_{i}'] = max_edit self.cond_layout_overall.addLayout(layout) self.main_layout.addWidget(self.group_box_2) # 处理文件部分 self.group_box_3 = QtWidgets.QGroupBox("Step 3. 获取最优结果") self.group_box_3.setFont(bold_font) self.button_solve = QtWidgets.QPushButton("开始排班!") self.button_solve.setFont(thin_font) self.text_solve = QtWidgets.QTextEdit(self) self.text_solve.setFont(thin_font) self.text_solve.setReadOnly(True) # 设置为只读模式 # 创建 QScrollArea 并将 QTextEdit 添加进去 self.scroll_area = QtWidgets.QScrollArea(self) self.scroll_area.setWidget(self.text_solve) # 将 QTextEdit 设置为 QScrollArea 的内容 self.scroll_area.setWidgetResizable(True) # 允许 QTextEdit 根据 QScrollArea 的大小自动调整 self.button_solve.clicked.connect(self.magic) self.solve_layout = QtWidgets.QVBoxLayout() self.solve_layout.addWidget(self.scroll_area) self.solve_layout.addWidget(self.button_solve) self.group_box_3.setLayout(self.solve_layout) self.main_layout.addWidget(self.group_box_3) def _create_constraint_row(self, label_text, min_value, max_value, font): """创建一行限制条件输入框""" layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(label_text, self) label.setFont(font) # 创建输入框的辅助函数 def create_input(value): edit = QtWidgets.QLineEdit(self) edit.setFont(font) edit.setValidator(QtGui.QIntValidator()) edit.setPlaceholderText("无限制") if value: edit.setText(value) edit.setEnabled(False) return edit min_edit = create_input(min_value) to_label = QtWidgets.QLabel("到", self) to_label.setFont(font) max_edit = create_input(max_value) layout.addWidget(label) layout.addWidget(min_edit) layout.addWidget(to_label) layout.addWidget(max_edit) return layout, min_edit, max_edit def _create_weight_row(self, label_text, value, font): """创建一行权重输入框""" layout = QtWidgets.QHBoxLayout() label = QtWidgets.QLabel(label_text, self) label.setFont(font) edit = QtWidgets.QLineEdit(self) edit.setFont(font) edit.setValidator(QtGui.QDoubleValidator(0.0, 1.0, 2, self)) edit.setText(str(value)) layout.addWidget(label) layout.addWidget(edit) return layout, edit def open_file(self): file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*.*);;文本文件 (*.txt)") if file_path: self.label_openfile.setText(f"选中文件: {file_path}") self._excel_dir = file_path def on_switch_toggled(self, state): enabled = state == 0 self._auto_mode = not enabled for i in range(1, len(CONSTRAINTS_CONFIG) + 1): self.constraint_widgets[f'min_{i}'].setEnabled(enabled) self.constraint_widgets[f'max_{i}'].setEnabled(enabled) @QtCore.Slot() def magic(self): try: self.text_solve.append(f"******** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ********") # 读取文件 if self._excel_dir is None: self.text_solve.append("请先选择文件!") return # 禁用按钮,防止重复点击 self.button_solve.setEnabled(False) self.button_solve.setText("计算中...") self.text_solve.append("开始排班...") self.text_solve.append("读取文件中...") self.all_data, self.index_to_name_dict, preference_mat, depart_mat, want_num_array, is_new_array = read_excel(self._excel_dir) self.text_solve.append("读取文件成功!") # 计算并打印统计信息 is_tech_array = [depart_mat[i][0] == 1 or depart_mat[i][1] == 1 for i in range(len(depart_mat))] stu_num = len(self.index_to_name_dict) class_num = len(preference_mat[0]) tech_sum = sum(want_num_array[i] for i in range(len(preference_mat)) if not is_new_array[i] and is_tech_array[i]) all_sum = sum(want_num_array) min_prefer = min(sum(preference_mat[i][j] for i in range(len(preference_mat))) for j in range(class_num)) self.text_solve.append("信息统计:") self.text_solve.append(f"\t学生总人数:{stu_num}") self.text_solve.append(f"\t班次总数:{class_num}") self.text_solve.append(f"\t电脑部或电器部的所有老人的意愿班次之和:{tech_sum}") self.text_solve.append(f"\t所有人的意愿班次之和:{all_sum}") self.text_solve.append(f"\t平均每班人数:{all_sum/20}") self.text_solve.append(f"\t所有班次中最少拥有意愿数:{min_prefer}") def get_weight_value(key): text = self.weight_widgets[key].text() return float(text) if text else 0.0 def get_constraint_value(key): text = self.constraint_widgets[key].text() return int(text) if text else None # 准备参数 params = { 'num_min': get_constraint_value('min_1'), 'num_max': get_constraint_value('max_1'), 'num_tech_old_min': get_constraint_value('min_2'), 'num_tech_old_max': get_constraint_value('max_2'), 'num_old_min': get_constraint_value('min_3'), 'num_old_max': get_constraint_value('max_3'), 'num_new_min': get_constraint_value('min_4'), 'num_new_max': get_constraint_value('max_4'), 'weights': [get_weight_value(f'edit_{i}') for i in range(1, len(WEIGHTS_CONFIG) + 1)] } # 创建并启动求解线程 self.solver_thread = SolverThread( preference_mat, depart_mat, want_num_array, is_new_array, self._auto_mode, params, self ) # 连接信号 self.solver_thread.log_signal.connect(self.on_log_message) self.solver_thread.finished_signal.connect(self.on_solve_finished) self.solver_thread.error_signal.connect(self.on_solve_error) # 启动线程 self.solver_thread.start() except Exception: self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!") self.text_solve.append(f"Error Details:\n{format_exc()}") self.button_solve.setEnabled(True) self.button_solve.setText("开始排班!") @QtCore.Slot(str) def on_log_message(self, message): """接收线程发送的日志消息并实时显示""" self.text_solve.append(message) @QtCore.Slot(object, dict) def on_solve_finished(self, vars, params): """求解完成后的回调""" try: if vars is None: if self._auto_mode: self.text_solve.append("自动求解失败!请尝试手动调参!") else: self.text_solve.append("在目前限制条件下无解!请尝试更改限制条件!") else: if self._auto_mode: self.text_solve.append("自动计算成功!") self.text_solve.append("参数:") self.text_solve.append(f"\t每班人数:[{params['num_min']}, {params['num_max']}]") self.text_solve.append(f"\t每班电脑或电器的老人数:[{params['num_tech_old_min']}, {params['num_tech_old_max']}]") self.text_solve.append(f"\t每班老人数:[{params['num_old_min']}, {params['num_old_max']}]") self.text_solve.append(f"\t每班小朋友数:[{params['num_new_min']}, inf]") else: self.text_solve.append("计算最优解成功!") # 保存结果到 excel self.text_solve.append("保存结果中...") save_dir = f"result_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" save_to_excel(vars, self.all_data, self.index_to_name_dict, save_dir) self.text_solve.append(f"保存结果成功!保存路径:{save_dir}") except Exception: self.text_solve.append("保存结果时出现错误!") self.text_solve.append(f"Error Details:\n{format_exc()}") finally: # 恢复按钮状态 self.button_solve.setEnabled(True) self.button_solve.setText("开始排班!") @QtCore.Slot(str) def on_solve_error(self, error_msg): """处理求解过程中的错误""" self.text_solve.append("程序出现严重错误,请联系开发者解决问题!!!") self.text_solve.append(f"Error Details:\n{error_msg}") self.button_solve.setEnabled(True) self.button_solve.setText("开始排班!") if __name__ == "__main__": app = QtWidgets.QApplication([]) widget = MyWidget() widget.setWindowTitle("EVA 值班排班软件") widget.resize(600, 600) widget.show() exit(app.exec())