547 lines
19 KiB
Python
547 lines
19 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
|
|
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 # 添加选中参数的索引跟踪
|
|
|
|
def editorEvent(self, event, model, option, index):
|
|
data = index.data(Qt.ItemDataRole.UserRole)
|
|
if not data or not data.params:
|
|
return False
|
|
|
|
params_rect = self.getParamsRect(option.rect)
|
|
pos = event.pos()
|
|
|
|
# 计算参数项的位置
|
|
param_height = 24
|
|
spacing = 4
|
|
indent = 20
|
|
|
|
# 检查是否点击在参数区域内
|
|
for i, param in enumerate(data.params):
|
|
param_rect = QRect(
|
|
params_rect.left() + indent,
|
|
params_rect.top() + i * (param_height + spacing),
|
|
params_rect.width() - indent,
|
|
param_height
|
|
)
|
|
|
|
# 创建实际的点击检测区域(考虑滚动位置)
|
|
actual_rect = QRect(
|
|
param_rect.left(),
|
|
param_rect.top(),
|
|
param_rect.width(),
|
|
param_rect.height()
|
|
)
|
|
|
|
if actual_rect.contains(pos):
|
|
if event.type() == QEvent.MouseButtonPress:
|
|
# 单击选中
|
|
self.selected_param_index = i
|
|
self.parent().viewport().update()
|
|
return True
|
|
elif event.type() == QEvent.MouseButtonDblClick:
|
|
# 双击编辑
|
|
self.editing_param_index = i
|
|
self.edit_rect = actual_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
|
|
|
|
if self.editing_param_index >= 0 and self.editing_param_index < len(data.params):
|
|
editor = QLineEdit(parent)
|
|
editor.setText(data.params[self.editing_param_index].name)
|
|
editor.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: #2C2C2C;
|
|
color: white;
|
|
border: 1px solid #346792;
|
|
border-radius: 4px;
|
|
padding: 2px;
|
|
}
|
|
""")
|
|
return editor
|
|
return None
|
|
|
|
def updateEditorGeometry(self, editor, option, index):
|
|
if 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 isinstance(editor, QLineEdit):
|
|
data = index.data(Qt.ItemDataRole.UserRole)
|
|
if self.editing_param_index >= 0 and self.editing_param_index < len(data.params):
|
|
editor.setText(data.params[self.editing_param_index].name)
|
|
|
|
def setModelData(self, editor, model, index):
|
|
if isinstance(editor, QLineEdit):
|
|
data = index.data(Qt.ItemDataRole.UserRole)
|
|
if self.editing_param_index >= 0 and self.editing_param_index < len(data.params):
|
|
data.params[self.editing_param_index].name = editor.text()
|
|
model.setData(index, data, Qt.ItemDataRole.UserRole)
|
|
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()
|
|
|
|
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()) |