brisonus_app_eq/component/widget_card/widget_card.py

677 lines
25 KiB
Python
Raw Permalink Normal View History

import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
2025-02-18 22:05:52 +08:00
from PySide6.QtWidgets import (QWidget, QListWidget, QStyledItemDelegate,
QApplication, QVBoxLayout, QMenu, QListWidgetItem,
QStyle, QLineEdit)
from PySide6.QtCore import Qt, QSize, QRect, QPoint, QEvent, Signal
2025-02-18 22:05:52 +08:00
from PySide6.QtGui import (QPainter, QPainterPath, QColor, QLinearGradient,
QPen, QFont, QIcon, QCursor)
2025-02-18 22:05:52 +08:00
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 # 只保留参数名称
2025-02-18 22:05:52 +08:00
@dataclass
class CardData:
name: str
date: str
description: str
params: list[ParamData] = None
2025-02-18 22:05:52 +08:00
activated: bool = False
def __post_init__(self):
if self.params is None:
self.params = []
2025-02-18 22:05:52 +08:00
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)
2025-02-18 22:05:52 +08:00
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)
)
2025-02-18 22:05:52 +08:00
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):
"""添加卡片项目"""
2025-02-18 22:05:52 +08:00
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
2025-02-18 22:05:52 +08:00
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"))
2025-02-18 22:05:52 +08:00
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)
2025-02-18 22:05:52 +08:00
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
2025-02-23 21:35:34 +08:00
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)
2025-02-23 21:35:34 +08:00
if not data:
return False
pos = event.pos()
2025-02-23 21:35:34 +08:00
# 检查是否点击在标题或描述区域
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):
2025-02-23 21:35:34 +08:00
self.editing_field = 'description'
self.edit_rect = desc_rect
self.parent().edit(index)
return True
# 检查是否点击在参数区域内
2025-02-23 21:35:34 +08:00
params_rect = self.getParamsRect(option.rect)
if params_rect.contains(pos):
if event.type() == QEvent.MouseButtonPress:
# 单击选中
prev_index = self.selected_param_index
2025-02-23 21:35:34 +08:00
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)
2025-02-23 21:35:34 +08:00
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
2025-02-23 21:35:34 +08:00
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)
2025-02-23 21:35:34 +08:00
return editor
def updateEditorGeometry(self, editor, option, index):
2025-02-23 21:35:34 +08:00
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):
2025-02-23 21:35:34 +08:00
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):
2025-02-23 21:35:34 +08:00
if not isinstance(editor, QLineEdit):
return
data = index.data(Qt.ItemDataRole.UserRole)
old_value = ""
edit_type = ""
2025-02-23 21:35:34 +08:00
if self.editing_field == 'name':
old_value = data.name
2025-02-23 21:35:34 +08:00
data.name = editor.text()
edit_type = "project_name"
2025-02-23 21:35:34 +08:00
elif self.editing_field == 'description':
old_value = data.description
2025-02-23 21:35:34 +08:00
data.description = editor.text()
edit_type = "project_description"
2025-02-23 21:35:34 +08:00
elif self.editing_param_index >= 0:
old_value = data.params[self.editing_param_index].name
2025-02-23 21:35:34 +08:00
data.params[self.editing_param_index].name = editor.text()
edit_type = "parameter_name"
2025-02-23 21:35:34 +08:00
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)
2025-02-23 21:35:34 +08:00
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()
2025-02-23 21:35:34 +08:00
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
)
2025-02-18 22:05:52 +08:00
if __name__ == '__main__':
app = QApplication(sys.argv)
# 设置字体
font = QFont("Microsoft YaHei", 9)
app.setFont(font)
window = Widget_Card()
# 添加示例数据
for i in range(1, 6):
2025-02-18 22:05:52 +08:00
data = CardData(
name=f"测试项目 {i}",
2025-02-18 22:05:52 +08:00
date=date.today().strftime("%Y-%m-%d"),
description="这是一个测试项目,用于演示参数树形结构显示效果。",
params=[
ParamData("温度设定"),
ParamData("运行时间"),
ParamData("功率"),
ParamData("状态")
]
2025-02-18 22:05:52 +08:00
)
window.add_card_item(data)
window.show()
sys.exit(app.exec())