brisonus_app_eq/component/widget_card/widget_card.py

677 lines
25 KiB
Python

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, Signal
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):
parameterSelected = Signal(str, str) # Signal that emits (parameter_name, project_name)
itemEdited = Signal(str, str, str, str) # Signal that emits (edit_type, old_value, new_value, project_name)
parameterAdded = Signal(str, str) # Signal that emits (parameter_name, project_name)
def __init__(self):
super().__init__()
self.setup_ui()
self.setup_style()
# Connect the itemDelegate's parameterSelected signal
self.list_widget.itemDelegate().parameterSelected.connect(
lambda param_name, project_name: self.parameterSelected.emit(param_name, project_name)
)
# Connect the itemDelegate's itemEdited signal
self.list_widget.itemDelegate().itemEdited.connect(
lambda edit_type, old_value, new_value, project_name: self.itemEdited.emit(edit_type, old_value, new_value, project_name)
)
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)
# Set the first parameter as selected if parameters exist
if data.params:
self.list_widget.itemDelegate().selected_param_index = 0
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.parameterAdded.emit(new_param.name, data.name)
# 刷新显示
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()
def get_selected_param_name(self) -> tuple[str | None, str | None]:
"""获取当前选中参数的名称和所属项目名称
Returns:
tuple[str | None, str | None]: 返回(参数名称, 项目名称)的元组,如果没有选中参数则返回(None, None)
"""
current_item = self.list_widget.currentItem()
if not current_item:
return None, None
data = current_item.data(Qt.ItemDataRole.UserRole)
delegate = self.list_widget.itemDelegate()
if (delegate.selected_param_index >= 0 and
delegate.selected_param_index < len(data.params)):
return data.params[delegate.selected_param_index].name, data.name
return None, None
def add_parameter(self, param_name: str):
"""添加新参数到当前选中的项目"""
current_item = self.list_widget.currentItem()
if current_item:
card_data = current_item.data(Qt.ItemDataRole.UserRole)
card_data.params.append(ParamData(name=param_name))
# 更新UI
self.list_widget.viewport().update()
# 发射参数添加信号
self.parameterAdded.emit(param_name, card_data.name)
return True
return False
class CardItemDelegate(QStyledItemDelegate):
parameterSelected = Signal(str, str) # Signal that emits (parameter_name, project_name)
itemEdited = Signal(str, str, str, str) # Signal that emits (edit_type, old_value, new_value, project_name)
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
if 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:
# 单击选中
prev_index = self.selected_param_index
self.selected_param_index = self.getParamIndex(pos, params_rect)
# Emit signal if a different parameter is selected
if prev_index != self.selected_param_index and self.selected_param_index >= 0:
data = index.data(Qt.ItemDataRole.UserRole)
param_name = data.params[self.selected_param_index].name
self.parameterSelected.emit(param_name, data.name)
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)
old_value = ""
edit_type = ""
if self.editing_field == 'name':
old_value = data.name
data.name = editor.text()
edit_type = "project_name"
elif self.editing_field == 'description':
old_value = data.description
data.description = editor.text()
edit_type = "project_description"
elif self.editing_param_index >= 0:
old_value = data.params[self.editing_param_index].name
data.params[self.editing_param_index].name = editor.text()
edit_type = "parameter_name"
model.setData(index, data, Qt.ItemDataRole.UserRole)
# 发射编辑完成信号
if old_value != editor.text():
print(f"发射编辑完成信号: {edit_type}, {old_value}, {editor.text()}, {data.name}")
self.itemEdited.emit(edit_type, old_value, editor.text(), data.name)
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())