from PyQt5 import QtCore from PyQt5.QtWidgets import QHeaderView, QStyle, QStyleOptionButton, QTableView, QAbstractItemView, QDesktopWidget from PyQt5.QtCore import (pyqtSignal, Qt, QRect) from PyQt5.QtCore import Qt, QTimer, QAbstractTableModel, QModelIndex, QPoint, QSize from PyQt5.QtGui import QFont, QBrush, QColor from PyQt5.QtWidgets import QTableView, QStyledItemDelegate from utils import Globals class BackgroundDelegate(QStyledItemDelegate): def __init__(self, *args): super(BackgroundDelegate, self).__init__(*args) self.setObjectName('item') def sizeHint(self, option, index): """设置项目的尺寸提示,确保足够的高度""" size = super().sizeHint(option, index) size.setHeight(max(46, size.height())) # 最小高度60px return size def paint(self, painter, option, index): # 确保绘制区域有足够的高度 if index.data(QtCore.Qt.BackgroundRole): height = option.rect.height() top = option.rect.top() # 减少高度调整,保持更多的绘制区域 option.rect.setHeight(max(46, height - 4)) # 最小保持56px高度 option.rect.moveTop(top + 2) painter.fillRect(option.rect, index.data(QtCore.Qt.BackgroundRole)) super().paint(painter, option, index) class ProcedureTabel(QTableView): def __init__(self, testSteps, parent=None): super(ProcedureTabel, self).__init__(parent) self.parent = parent self.createTableSection(testSteps) self.setupTableHeaders() self.delegate = BackgroundDelegate(self) self.setItemDelegate(self.delegate) self.loadStylesheet() def loadStylesheet(self): """加载表格样式表""" try: from PyQt5.QtCore import QFile, QTextStream qssPath = "Static/ProcedureTable.qss" qssFile = QFile(qssPath) if qssFile.exists(): qssFile.open(QFile.ReadOnly | QFile.Text) stream = QTextStream(qssFile) stream.setCodec("UTF-8") self.setStyleSheet(stream.readAll()) qssFile.close() print(f"✅ ProcedureTable成功加载样式表: {qssPath}") else: print(f"⚠️ ProcedureTable样式表文件不存在: {qssPath}") except Exception as e: print(f"❌ ProcedureTable加载样式表失败: {str(e)}") def createTableSection(self, testSteps): """创建表格区域""" self.tableModel = StepTableModel(testSteps) self.setModel(self.tableModel) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.parent.showContextMenu) # 设置表格编辑行为 self.setEditTriggers(QTableView.DoubleClicked) # 设置选择行为为整行选中 self.setSelectionBehavior(QTableView.SelectRows) self.setSelectionMode(QAbstractItemView.SingleSelection) # 单行选择 # 设置表格显示优化 self.setAlternatingRowColors(False) # 交替行颜色 self.setShowGrid(True) # 显示网格线 self.setGridStyle(Qt.SolidLine) # 实线网格 self.setSortingEnabled(False) # 禁用排序避免干扰执行顺序 # 启用鼠标跟踪以支持hover效果 self.setMouseTracking(True) # 设置最小行高 self.verticalHeader().setMinimumSectionSize(30) self.setupTableHeaders() # 初始调整所有行高 self.adjustAllRowHeights() def setupTableHeaders(self): """设置表格头部""" try: header = self.horizontalHeader() if header: # 设置表格自动调整行高 self.verticalHeader().setDefaultSectionSize(35) self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.verticalHeader().hide() # 设置列宽模式 - 让表格填充满整个宽度 header.setStretchLastSection(True) # 让最后一列自动拉伸填充剩余空间 # 设置各列的调整模式 header.setSectionResizeMode(0, QHeaderView.Fixed) # 步骤ID - 固定宽度 header.setSectionResizeMode(1, QHeaderView.Stretch) # 步骤描述 - 拉伸 header.setSectionResizeMode(2, QHeaderView.Fixed) # 操作类型 - 固定宽度 header.setSectionResizeMode(3, QHeaderView.Fixed) # 执行时间 - 固定宽度 header.setSectionResizeMode(4, QHeaderView.Fixed) # 执行结果 - 固定宽度 header.setSectionResizeMode(5, QHeaderView.Stretch) # 详细结果 - 拉伸 header.setSectionResizeMode(6, QHeaderView.Stretch) # 备注 - 拉伸 # 设置固定列的宽度 self.setColumnWidth(0, 80) # 步骤ID self.setColumnWidth(2, 120) # 操作类型 self.setColumnWidth(3, 160) # 执行时间 self.setColumnWidth(4, 80) # 执行结果 # 设置拉伸列的最小宽度 header.setMinimumSectionSize(100) # 设置所有列的最小宽度 # 启用文本换行 self.setWordWrap(True) except Exception as e: print(f"设置表格头部时出错: {e}") def adjustRowHeight(self, row): """调整指定行的高度以适应内容""" try: # 获取该行的内容 stepInfo = self.tableModel.getStepInfo(row) if not stepInfo: return # 计算需要的行高,确保所有值都不为None description = stepInfo.get('description') or '' result = stepInfo.get('result') or '' note = stepInfo.get('note') or '' # 确保所有值都是字符串类型 if not isinstance(description, str): description = str(description) if description is not None else '' if not isinstance(result, str): result = str(result) if result is not None else '' if not isinstance(note, str): note = str(note) if note is not None else '' # 基于最长文本计算行高 max_text_length = max(len(description), len(result), len(note)) # 基础行高 base_height = 35 # 根据文本长度动态调整 if max_text_length > 100: height = base_height + (max_text_length // 50) * 15 elif max_text_length > 50: height = base_height + 15 else: height = base_height # 设置行高,最大不超过150像素 height = min(height, 150) self.setRowHeight(row, height) except Exception as e: print(f"调整行高时出错: {e}") def adjustAllRowHeights(self): """调整所有行的高度""" try: if not hasattr(self, 'tableModel') or self.tableModel is None: return row_count = self.tableModel.rowCount() if row_count <= 0: return for row in range(row_count): self.adjustRowHeight(row) except Exception as e: print(f"调整所有行高时出错: {e}") def mouseMoveEvent(self, event): """鼠标移动事件,实现hover效果""" super().mouseMoveEvent(event) # 获取鼠标位置对应的行 index = self.indexAt(event.pos()) if index.isValid(): row = index.row() if row != self.tableModel.hoverRow: # 清除之前的hover行 oldHoverRow = self.tableModel.hoverRow self.tableModel.hoverRow = row # 更新显示 if oldHoverRow >= 0: self.update(self.tableModel.index(oldHoverRow, 0)) for col in range(self.tableModel.columnCount()): self.update(self.tableModel.index(oldHoverRow, col)) # 更新新的hover行 self.update(self.tableModel.index(row, 0)) for col in range(self.tableModel.columnCount()): self.update(self.tableModel.index(row, col)) else: # 鼠标不在任何行上,清除hover效果 if self.tableModel.hoverRow >= 0: oldHoverRow = self.tableModel.hoverRow self.tableModel.hoverRow = -1 self.update(self.tableModel.index(oldHoverRow, 0)) for col in range(self.tableModel.columnCount()): self.update(self.tableModel.index(oldHoverRow, col)) def leaveEvent(self, event): """鼠标离开事件,清除hover效果""" super().leaveEvent(event) if self.tableModel.hoverRow >= 0: oldHoverRow = self.tableModel.hoverRow self.tableModel.hoverRow = -1 self.update(self.tableModel.index(oldHoverRow, 0)) for col in range(self.tableModel.columnCount()): self.update(self.tableModel.index(oldHoverRow, col)) class StepTableModel(QAbstractTableModel): columns = ['序号', '实验步骤','操作类型', '执行时间', '是否与预期一致', '实际结果', '备注'] def __init__(self, testSteps): super().__init__() self.stepData = [] self.stepIndex = 0 self.hoverRow = -1 # 添加hover行跟踪 for mainStep in testSteps: self.stepData.append({ 'id': self.stepIndex, 'isMain': True, 'stepId': mainStep['步骤ID'], 'description': mainStep['步骤描述'], 'executed': False, 'stepType': mainStep['操作类型'], 'time': None, 'result': None, 'note': mainStep.get('备注', '') # 修复:从主步骤的备注字段读取 }) self.stepIndex += 1 for subStep in mainStep['子步骤']: self.stepData.append({ 'id': self.stepIndex, 'isMain': False, 'stepId': f"{mainStep['步骤ID']}{subStep['序号']}", 'description': subStep['操作'], 'stepType': subStep['操作类型'], 'executed': False, 'time': None, 'result': None, 'note': subStep['备注'] # 新增:从Excel解析的备注字段 }) self.stepIndex += 1 def rowCount(self, parent=QModelIndex()): return len(self.stepData) def columnCount(self, parent=QModelIndex()): return len(self.columns) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None row = index.row() col = index.column() step = self.stepData[row] if role == Qt.DisplayRole: if col == 0: return step['stepId'] or '' elif col == 1: return step['description'] or '' elif col == 2: return step['stepType'] or '' elif col == 3: return step['time'].strftime("%Y-%m-%d %H:%M:%S") if step['time'] else '' elif col == 4: # print(step['result']) if step['executed']: if '失败' in step['result']: return '✘' else: return '✓' elif col == 5: # print(step['result']) return step['result'] or '' elif col == 6: return step['note'] if step['note'] else '' elif role == Qt.BackgroundRole: # 优先级:执行结果 > hover效果 > 主步骤背景 if step['executed']: if '失败' in step['result']: # 失败状态:红色背景 if row == self.hoverRow: return QBrush(QColor(255, 160, 170)) # hover时稍微深一点的红色 else: return QBrush(QColor(255, 182, 193)) # 浅红色 else: # 成功状态:绿色背景 if row == self.hoverRow: return QBrush(QColor(120, 220, 120)) # hover时稍微深一点的绿色 else: return QBrush(QColor(144, 238, 144)) # 浅绿色 elif row == self.hoverRow: # hover效果:浅蓝色背景 if step['isMain']: return QBrush(QColor(200, 200, 230)) # 主步骤hover时的颜色 else: return QBrush(QColor(230, 240, 255)) # 子步骤hover时的颜色 elif step['isMain']: # 主步骤默认背景:浅灰色 return QBrush(QColor(220, 220, 220)) elif role == Qt.TextAlignmentRole: if col in [0, 2, 3, 4]: # 步骤ID、操作类型、执行时间、执行结果居中对齐 return Qt.AlignCenter else: # 其他列左对齐并顶部对齐 return Qt.AlignLeft | Qt.AlignTop elif role == Qt.FontRole and step['isMain']: font = QFont() font.setBold(True) return font # 新增:支持自动换行 elif role == Qt.TextAlignmentRole: if col in [1, 5]: # 描述和备注列 return Qt.AlignLeft | Qt.AlignVCenter elif role == Qt.TextWordWrap: if col in [1, 5]: return True return None def flags(self, index): """返回单元格标志""" flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable return flags def headerData(self, section, orientation, role): if role == Qt.DisplayRole and orientation == Qt.Horizontal: return self.columns[section] return None def getStepInfo(self, row): if 0 <= row < len(self.stepData): return self.stepData[row] return None def getFullStepInfo(self, row): """获取完整的步骤信息""" if 0 <= row < len(self.stepData): return self.stepData[row] return None def updateStepResult(self, row, result, time): if 0 <= row < len(self.stepData): # print(result) self.stepData[row]['executed'] = True self.stepData[row]['time'] = time self.stepData[row]['result'] = result self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount()-1), [Qt.DisplayRole, Qt.BackgroundRole]) return True return False def resetExecutionState(self): """只重置执行状态(颜色),不清除时间和结果""" for step in self.stepData: step['executed'] = False step['result'] = None self.dataChanged.emit(self.index(0, 0), self.index(self.rowCount()-1, self.columnCount()-1), [Qt.BackgroundRole]) def resetAll(self): """完全重置所有状态(包括时间和结果)""" for step in self.stepData: step.update({ 'executed': False, 'time': None, 'result': None }) self.dataChanged.emit(self.index(0, 0), self.index(self.rowCount()-1, self.columnCount()-1), [Qt.DisplayRole, Qt.BackgroundRole]) def getTestSteps(self): """获取测试步骤数据,用于保存""" testSteps = [] currentMainStep = None for step in self.stepData: if step['isMain']: # 如果有前一个主步骤,先添加到结果中 if currentMainStep: testSteps.append(currentMainStep) # 创建新的主步骤 currentMainStep = { '步骤ID': step['stepId'], '步骤描述': step['description'], '操作类型': step['stepType'], '预期结果': '', '子步骤': [] } else: # 子步骤 if currentMainStep: # 从步骤ID中提取序号 stepId = step['stepId'] if currentMainStep['步骤ID'] in stepId: subStepId = stepId.replace(currentMainStep['步骤ID'], '') else: subStepId = stepId subStep = { '序号': subStepId, '操作': step['description'], '操作类型': step['stepType'], '预期结果': '', '实际结果': step.get('result', ''), '一致性': '是' if step.get('result') and '失败' not in step.get('result', '') else '否', '测试时间': step.get('time', ''), '备注': step.get('note', '') } currentMainStep['子步骤'].append(subStep) # 添加最后一个主步骤 if currentMainStep: testSteps.append(currentMainStep) return testSteps