import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) from PySide6.QtWidgets import (QWidget, QListWidget, QStyledItemDelegate, QApplication, QVBoxLayout, QMenu, QListWidgetItem, QStyle, QLineEdit) from PySide6.QtCore import Qt, QSize, QRect, QPoint, QEvent from PySide6.QtGui import (QPainter, QPainterPath, QColor, QLinearGradient, QPen, QFont, QIcon, QCursor) from component.widget_card.ui_widget_card import CardItemDelegate from dataclasses import dataclass import sys from datetime import date import os import subprocess @dataclass class ParamData: name: str # 只保留参数名称 @dataclass class CardData: name: str date: str description: str params: list[ParamData] = None activated: bool = False def __post_init__(self): if self.params is None: self.params = [] class Widget_Card(QWidget): def __init__(self): super().__init__() self.setup_ui() self.setup_style() def setup_ui(self): self.setWindowTitle("卡片列表示例") self.resize(400, 600) layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(0) self.list_widget = QListWidget(self) self.list_widget.setSpacing(10) self.list_widget.setResizeMode(QListWidget.ResizeMode.Adjust) self.list_widget.setUniformItemSizes(False) self.list_widget.setViewMode(QListWidget.ViewMode.ListMode) self.list_widget.setVerticalScrollMode(QListWidget.ScrollMode.ScrollPerPixel) self.list_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) delegate = CardItemDelegate(self.list_widget) self.list_widget.setItemDelegate(delegate) # 连接信号 self.list_widget.itemDoubleClicked.connect(self.on_item_double_clicked) self.list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.list_widget.customContextMenuRequested.connect(self.show_context_menu) layout.addWidget(self.list_widget) def setup_style(self): self.setStyleSheet(""" QWidget { background-color: #1e1e1e; } QListWidget { background-color: #1e1e1e; border: none; outline: none; } QListWidget::item { background-color: transparent; padding: 4px; } QListWidget::item:selected { background-color: transparent; } QScrollBar:vertical { border: none; background: #1e1e1e; width: 8px; margin: 0px; } QScrollBar::handle:vertical { background: #404040; min-height: 20px; border-radius: 4px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } QMenu { background-color: #2d2d2d; border: 1px solid #404040; padding: 5px; } QMenu::item { background-color: transparent; padding: 6px 25px; border-radius: 4px; color: #ffffff; } QMenu::item:selected { background-color: #404040; } QMenu::separator { height: 1px; background: #404040; margin: 5px 0px; } """) def add_card_item(self, data: CardData): """添加卡片项目""" item = QListWidgetItem(self.list_widget) item.setData(Qt.ItemDataRole.UserRole, data) # 计算并设置卡片高度 item_height = self.calculate_item_height(data) item.setSizeHint(QSize(380, item_height)) item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEditable) self.list_widget.addItem(item) self.list_widget.setCurrentItem(item) def calculate_item_height(self, data: CardData): """计算卡片高度""" base_height = 65 # 标题和描述的基础高度 param_height = 24 # 每个参数的高度 param_spacing = 4 # 参数之间的间距 param_padding = 10 # 参数区域的上下padding # 如果有参数,计算参数区域的高度 params_height = 0 if data.params: params_height = (len(data.params) * (param_height + param_spacing)) + param_padding total_height = base_height + params_height return max(120, total_height) # 确保最小高度为120 def on_item_double_clicked(self, item): # 先将所有项目设置为未激活状态 for row in range(self.list_widget.count()): current_item = self.list_widget.item(row) data = current_item.data(Qt.ItemDataRole.UserRole) data.activated = False current_item.setData(Qt.ItemDataRole.UserRole, data) # 设置当前点击项目为激活状态 data = item.data(Qt.ItemDataRole.UserRole) data.activated = True item.setData(Qt.ItemDataRole.UserRole, data) self.list_widget.viewport().update() def show_context_menu(self, pos: QPoint): item = self.list_widget.itemAt(pos) menu = QMenu(self) if item: # 有选中项目时显示的菜单 send_action = menu.addAction("发送到设备") menu.addSeparator() # 添加参数管理菜单 param_menu = QMenu("参数管理", self) add_param_action = param_menu.addAction("添加参数") if item.data(Qt.ItemDataRole.UserRole).params: # 只有存在参数时才显示删除选项 delete_param_action = param_menu.addAction("删除参数") menu.addMenu(param_menu) menu.addSeparator() edit_action = menu.addAction("修改") delete_action = menu.addAction("删除") menu.addSeparator() show_in_explorer_action = menu.addAction("在资源管理器中显示") # 设置图标 send_action.setIcon(QIcon.fromTheme("document-send")) add_param_action.setIcon(QIcon.fromTheme("list-add")) if item.data(Qt.ItemDataRole.UserRole).params: delete_param_action.setIcon(QIcon.fromTheme("list-remove")) edit_action.setIcon(QIcon.fromTheme("document-edit")) delete_action.setIcon(QIcon.fromTheme("document-delete")) show_in_explorer_action.setIcon(QIcon.fromTheme("folder")) else: # 点击空白处时显示的菜单 add_action = menu.addAction("添加项目") add_action.setIcon(QIcon.fromTheme("document-new")) action = menu.exec(self.list_widget.viewport().mapToGlobal(pos)) if not action: return if item: data = item.data(Qt.ItemDataRole.UserRole) if action == send_action: self.send_to_device(item) elif action.text() == "添加参数": self.add_param_to_item(item) elif action.text() == "删除参数": self.delete_param_from_item(item) elif action == edit_action: self.edit_item(item) elif action == delete_action: self.delete_item(item) elif action == show_in_explorer_action: self.show_in_explorer(item) else: if action.text() == "添加项目": self.add_new_item() def add_new_item(self): """添加新项目""" # 创建新的卡片数据,不包含默认参数 new_data = CardData( name=f"新项目 {self.list_widget.count() + 1}", date=date.today().strftime("%Y-%m-%d"), description="点击此处编辑描述", params=[] # 空参数列表 ) self.add_card_item(new_data) def send_to_device(self, item): data = item.data(Qt.ItemDataRole.UserRole) print(f"发送到设备: {data.name}") def edit_item(self, item): data = item.data(Qt.ItemDataRole.UserRole) print(f"编辑项目: {data.name}") def delete_item(self, item): row = self.list_widget.row(item) self.list_widget.takeItem(row) def show_in_explorer(self, item): path = os.getcwd() if sys.platform == 'win32': subprocess.run(['explorer', '/select,', os.path.normpath(path)]) elif sys.platform == 'darwin': subprocess.run(['open', '-R', path]) else: subprocess.run(['xdg-open', os.path.dirname(path)]) def add_param_to_item(self, item): """添加参数到卡片""" data = item.data(Qt.ItemDataRole.UserRole) param_count = len(data.params) # 创建新参数,只包含名称 new_param = ParamData( name=f"参数 {param_count + 1}" ) # 添加到参数列表 data.params.append(new_param) # 更新项目数据 item.setData(Qt.ItemDataRole.UserRole, data) # 更新卡片高度 item_height = self.calculate_item_height(data) item.setSizeHint(QSize(380, item_height)) # 刷新显示 self.list_widget.viewport().update() def delete_param_from_item(self, item): """从卡片中删除选中的参数""" data = item.data(Qt.ItemDataRole.UserRole) if data.params and self.selected_param_index >= 0: # 删除选中的参数 data.params.pop(self.selected_param_index) # 重置选中状态 self.selected_param_index = -1 # 更新项目数据 item.setData(Qt.ItemDataRole.UserRole, data) # 更新卡片高度 item_height = self.calculate_item_height(data) item.setSizeHint(QSize(380, item_height)) # 刷新显示 self.list_widget.viewport().update() class CardItemDelegate(QStyledItemDelegate): def __init__(self, parent=None): super().__init__(parent) self.edit_mode = False self.edit_rect = QRect() self.editing_param_index = -1 self.selected_param_index = -1 self.editing_field = None # 添加字段标识:'name', 'description' 或 None def editorEvent(self, event, model, option, index): data = index.data(Qt.ItemDataRole.UserRole) if not data: return False pos = event.pos() # 检查是否点击在标题或描述区域 title_rect = self.getTitleRect(option.rect) desc_rect = self.getDescriptionRect(option.rect) if event.type() == QEvent.MouseButtonDblClick: if title_rect.contains(pos): self.editing_field = 'name' self.edit_rect = title_rect self.parent().edit(index) return True elif desc_rect.contains(pos): self.editing_field = 'description' self.edit_rect = desc_rect self.parent().edit(index) return True # 检查是否点击在参数区域内 params_rect = self.getParamsRect(option.rect) if params_rect.contains(pos): if event.type() == QEvent.MouseButtonPress: # 单击选中 self.selected_param_index = self.getParamIndex(pos, params_rect) self.parent().viewport().update() return True elif event.type() == QEvent.MouseButtonDblClick: # 双击编辑 self.editing_param_index = self.getParamIndex(pos, params_rect) self.edit_rect = self.getParamRect(self.selected_param_index, params_rect) self.parent().edit(index) return True # 点击空白区域取消选中 if event.type() == QEvent.MouseButtonPress and self.selected_param_index != -1: self.selected_param_index = -1 self.parent().viewport().update() return super().editorEvent(event, model, option, index) def createEditor(self, parent, option, index): data = index.data(Qt.ItemDataRole.UserRole) if not data: return None editor = QLineEdit(parent) editor.setStyleSheet(""" QLineEdit { background-color: #2C2C2C; color: white; border: 1px solid #346792; border-radius: 4px; padding: 2px; } """) if self.editing_field == 'name': editor.setText(data.name) elif self.editing_field == 'description': editor.setText(data.description) elif self.editing_param_index >= 0 and self.editing_param_index < len(data.params): editor.setText(data.params[self.editing_param_index].name) return editor def updateEditorGeometry(self, editor, option, index): if self.editing_field in ['name', 'description']: editor.setGeometry(self.edit_rect) elif self.editing_param_index >= 0: params_rect = self.getParamsRect(option.rect) param_height = 24 spacing = 4 indent = 20 # 计算正确的编辑器位置 editor_rect = QRect( params_rect.left() + indent, params_rect.top() + self.editing_param_index * (param_height + spacing), params_rect.width() - indent, param_height ) editor.setGeometry(editor_rect) def setEditorData(self, editor, index): if not isinstance(editor, QLineEdit): return data = index.data(Qt.ItemDataRole.UserRole) if self.editing_field == 'name': editor.setText(data.name) elif self.editing_field == 'description': editor.setText(data.description) elif self.editing_param_index >= 0: editor.setText(data.params[self.editing_param_index].name) def setModelData(self, editor, model, index): if not isinstance(editor, QLineEdit): return data = index.data(Qt.ItemDataRole.UserRole) if self.editing_field == 'name': data.name = editor.text() elif self.editing_field == 'description': data.description = editor.text() elif self.editing_param_index >= 0: data.params[self.editing_param_index].name = editor.text() model.setData(index, data, Qt.ItemDataRole.UserRole) self.editing_field = None self.editing_param_index = -1 def paint(self, painter, option, index): if not index.isValid(): return data = index.data(Qt.ItemDataRole.UserRole) if not data: return # 绘制背景 painter.save() if option.state & QStyle.State_Selected: painter.fillRect(option.rect, QColor("#346792")) else: painter.fillRect(option.rect, QColor("#1E1E1E")) painter.restore() # 绘制标题 title_rect = self.getTitleRect(option.rect) painter.save() font = painter.font() font.setPointSize(12) font.setBold(True) painter.setFont(font) painter.setPen(Qt.white) painter.drawText(title_rect, Qt.AlignLeft | Qt.AlignVCenter, data.name) painter.restore() # 绘制日期 date_rect = self.getDateRect(option.rect) painter.save() painter.setPen(QColor("#808080")) painter.drawText(date_rect, Qt.AlignRight | Qt.AlignVCenter, data.date) painter.restore() # 绘制描述 desc_rect = self.getDescriptionRect(option.rect) painter.save() painter.setPen(QColor("#CCCCCC")) painter.drawText(desc_rect, Qt.AlignLeft | Qt.AlignVCenter, data.description if data.description else "点击此处添加描述") painter.restore() # 绘制参数 params_rect = self.getParamsRect(option.rect) self.paintParams(painter, params_rect, data.params) def getTitleRect(self, rect): return QRect(rect.left() + 10, rect.top() + 5, rect.width() - 120, 30) def getDateRect(self, rect): return QRect(rect.right() - 110, rect.top() + 5, 100, 30) def getDescriptionRect(self, rect): return QRect(rect.left() + 10, rect.top() + 35, rect.width() - 20, 25) def getParamsRect(self, rect): # 调整参数区域,考虑实际高度 base_height = 65 # 标题和描述的基础高度 return QRect(rect.left() + 10, rect.top() + base_height, rect.width() - 30, rect.height() - base_height) def paintParams(self, painter, rect, params): if not params: return # 计算每个参数的高度和缩进 param_height = 24 # 每个参数项的高度 indent = 20 # 缩进宽度 spacing = 4 # 参数之间的间距 # 绘制根节点连接线 painter.save() painter.setPen(QPen(QColor("#404040"), 1)) root_x = rect.left() + indent - 10 root_y = rect.top() # 垂直线 total_height = (len(params) - 1) * (param_height + spacing) + param_height/2 painter.drawLine(root_x, root_y, root_x, root_y + total_height) painter.restore() for i, param in enumerate(params): # 计算当前参数项的矩形区域 param_rect = QRect( rect.left() + indent, # 缩进 rect.top() + i * (param_height + spacing), # 垂直位置 rect.width() - indent, # 宽度 param_height # 高度 ) # 绘制连接线 painter.save() painter.setPen(QPen(QColor("#404040"), 1)) line_x = rect.left() + indent - 10 line_y = param_rect.center().y() painter.drawLine(line_x, line_y, line_x + 10, line_y) # 水平连接线 painter.restore() # 绘制参数背景 painter.save() if i == self.selected_param_index: painter.fillRect(param_rect, QColor("#346792")) else: painter.fillRect(param_rect, QColor("#2C2C2C")) painter.restore() # 绘制参数边框 painter.save() painter.setPen(QPen(QColor("#404040"), 1)) painter.drawRect(param_rect) painter.restore() # 绘制参数名称 painter.save() text_rect = param_rect.adjusted(8, 0, -8, 0) # 文本区域留出边距 if i == self.selected_param_index: painter.setPen(Qt.white) else: painter.setPen(QColor("#808080")) painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, param.name) painter.restore() def getParamIndex(self, pos, rect): """根据点击位置获取参数索引""" if not rect.contains(pos): return -1 param_height = 24 spacing = 4 indent = 20 # 计算点击位置对应的参数索引 relative_y = pos.y() - rect.top() index = relative_y // (param_height + spacing) # 确保索引在有效范围内 if 0 <= index < len(self.parent().currentItem().data(Qt.ItemDataRole.UserRole).params): return index return -1 def getParamRect(self, index, rect): """获取指定参数索引的矩形区域""" param_height = 24 spacing = 4 indent = 20 return QRect( rect.left() + indent, rect.top() + index * (param_height + spacing), rect.width() - indent, param_height ) if __name__ == '__main__': app = QApplication(sys.argv) # 设置字体 font = QFont("Microsoft YaHei", 9) app.setFont(font) window = Widget_Card() # 添加示例数据 for i in range(1, 6): data = CardData( name=f"测试项目 {i}", date=date.today().strftime("%Y-%m-%d"), description="这是一个测试项目,用于演示参数树形结构显示效果。", params=[ ParamData("温度设定"), ParamData("运行时间"), ParamData("功率"), ParamData("状态") ] ) window.add_card_item(data) window.show() sys.exit(app.exec())