chartflow 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
节点标记辅助类模块
|
|
3
|
+
|
|
4
|
+
定义节点上附着的浮动标记/按钮辅助类:
|
|
5
|
+
- HoverButton: 左上角编辑按钮(📖)
|
|
6
|
+
- StartMarker: 右上角起点标记(🔷)
|
|
7
|
+
- SingleMarker: 左下角执行状态标记(⚡)
|
|
8
|
+
- CustomMarker: 右下角用户自定义标记(★)
|
|
9
|
+
|
|
10
|
+
这些类仅依赖 Qt 与模块常量, 不依赖 NodeItem, 故独立存放以打破循环导入。
|
|
11
|
+
node_item.py 通过 re-export 保持 ``from chartflow.node.node_item import HoverButton``
|
|
12
|
+
等历史导入路径不变。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from PyQt6.QtCore import Qt, QPointF, QRectF, QObject, pyqtProperty, pyqtSignal
|
|
18
|
+
from PyQt6.QtGui import QBrush, QPen, QColor
|
|
19
|
+
from PyQt6.QtWidgets import (
|
|
20
|
+
QGraphicsItem, QGraphicsEllipseItem, QGraphicsTextItem,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from chartflow.node._node_base import (
|
|
24
|
+
Z_VALUE_BUTTON,
|
|
25
|
+
Z_VALUE_MARKER,
|
|
26
|
+
ANIMATION_DURATION_SHOW,
|
|
27
|
+
ANIMATION_DURATION_HIDE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HoverButton(QObject):
|
|
32
|
+
"""
|
|
33
|
+
悬浮按钮 - 用于节点编辑的浮动按钮
|
|
34
|
+
包含圆形背景和编辑图标
|
|
35
|
+
"""
|
|
36
|
+
clicked = pyqtSignal() # 点击信号
|
|
37
|
+
|
|
38
|
+
def __init__(self, parent: QGraphicsItem = None) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
|
|
41
|
+
self._parent = parent
|
|
42
|
+
self._radius: float = 15.0
|
|
43
|
+
|
|
44
|
+
# 创建圆形背景(浅黄灰色)
|
|
45
|
+
self._ellipse_item = QGraphicsEllipseItem(parent)
|
|
46
|
+
self._ellipse_item.setBrush(QBrush(QColor(240, 230, 200))) # 浅黄灰色
|
|
47
|
+
self._ellipse_item.setPen(QPen(QColor(180, 130, 80), 2)) # 橙灰色描边,2像素宽
|
|
48
|
+
self._ellipse_item.setZValue(Z_VALUE_BUTTON) # 确保在最上层
|
|
49
|
+
self._ellipse_item.setAcceptHoverEvents(True)
|
|
50
|
+
self._ellipse_item.setToolTip("编辑窗口已打开") # 添加 tooltip
|
|
51
|
+
|
|
52
|
+
# 创建编辑图标文本(书本图标)
|
|
53
|
+
self._text_item = QGraphicsTextItem("📖", parent)
|
|
54
|
+
self._text_item.setZValue(Z_VALUE_BUTTON + 1) # 确保在圆形背景之上
|
|
55
|
+
self._text_item.setAcceptHoverEvents(False)
|
|
56
|
+
self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
57
|
+
|
|
58
|
+
# 初始隐藏(只有当编辑窗口打开时才显示)
|
|
59
|
+
self._opacity: float = 0.0
|
|
60
|
+
self._updateOpacity()
|
|
61
|
+
|
|
62
|
+
# 设置按钮位置(左上角)
|
|
63
|
+
self._updatePosition()
|
|
64
|
+
|
|
65
|
+
def _updateOpacity(self) -> None:
|
|
66
|
+
"""更新透明度"""
|
|
67
|
+
if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
|
|
68
|
+
return
|
|
69
|
+
try:
|
|
70
|
+
self._ellipse_item.setOpacity(self._opacity)
|
|
71
|
+
self._text_item.setOpacity(self._opacity)
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def getOpacity(self) -> float:
|
|
76
|
+
"""获取当前透明度"""
|
|
77
|
+
return self._opacity
|
|
78
|
+
|
|
79
|
+
def setOpacity(self, value: float) -> None:
|
|
80
|
+
"""设置透明度"""
|
|
81
|
+
self._opacity = value
|
|
82
|
+
self._updateOpacity()
|
|
83
|
+
|
|
84
|
+
# 创建属性用于动画
|
|
85
|
+
opacity = pyqtProperty(float, getOpacity, setOpacity)
|
|
86
|
+
|
|
87
|
+
def _isValid(self) -> bool:
|
|
88
|
+
"""检查 C++ 对象是否仍然有效"""
|
|
89
|
+
if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
|
|
90
|
+
return False
|
|
91
|
+
try:
|
|
92
|
+
self._ellipse_item.isVisible()
|
|
93
|
+
return True
|
|
94
|
+
except RuntimeError:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def _updatePosition(self) -> None:
|
|
98
|
+
"""更新按钮位置(节点左上角)"""
|
|
99
|
+
if not self._isValid():
|
|
100
|
+
return
|
|
101
|
+
if self._parent is None:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
parent_rect = self._parent.rect()
|
|
106
|
+
center = parent_rect.center()
|
|
107
|
+
|
|
108
|
+
# 计算节点左上角的偏移位置
|
|
109
|
+
offset_x = -parent_rect.width() / 2 + self._radius
|
|
110
|
+
offset_y = -parent_rect.height() / 2 + self._radius
|
|
111
|
+
|
|
112
|
+
# 设置圆形背景位置
|
|
113
|
+
ellipse_rect = QRectF(
|
|
114
|
+
center.x() + offset_x - self._radius,
|
|
115
|
+
center.y() + offset_y - self._radius,
|
|
116
|
+
self._radius * 2,
|
|
117
|
+
self._radius * 2
|
|
118
|
+
)
|
|
119
|
+
self._ellipse_item.setRect(ellipse_rect)
|
|
120
|
+
|
|
121
|
+
# 设置文本居中
|
|
122
|
+
text_rect = self._text_item.boundingRect()
|
|
123
|
+
text_pos = QPointF(
|
|
124
|
+
center.x() + offset_x - text_rect.width() / 2,
|
|
125
|
+
center.y() + offset_y - text_rect.height() / 2
|
|
126
|
+
)
|
|
127
|
+
self._text_item.setPos(text_pos)
|
|
128
|
+
except RuntimeError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def setRadius(self, radius: float) -> None:
|
|
132
|
+
"""设置按钮半径"""
|
|
133
|
+
self._radius = radius
|
|
134
|
+
self._updatePosition()
|
|
135
|
+
|
|
136
|
+
def show(self) -> None:
|
|
137
|
+
"""显示按钮(带动画)"""
|
|
138
|
+
if not self._isValid():
|
|
139
|
+
return
|
|
140
|
+
try:
|
|
141
|
+
self._updatePosition()
|
|
142
|
+
self._ellipse_item.show()
|
|
143
|
+
self._text_item.show()
|
|
144
|
+
except RuntimeError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def hide(self) -> None:
|
|
148
|
+
"""隐藏按钮"""
|
|
149
|
+
if not self._isValid():
|
|
150
|
+
return
|
|
151
|
+
try:
|
|
152
|
+
self._ellipse_item.hide()
|
|
153
|
+
self._text_item.hide()
|
|
154
|
+
except RuntimeError:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
def contains(self, pos: QPointF) -> bool:
|
|
158
|
+
"""检查点是否在按钮内"""
|
|
159
|
+
if not self._isValid():
|
|
160
|
+
return False
|
|
161
|
+
try:
|
|
162
|
+
return self._ellipse_item.contains(pos) or self._text_item.contains(pos)
|
|
163
|
+
except RuntimeError:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
def mousePressEvent(self, event) -> bool:
|
|
167
|
+
"""处理鼠标按下事件"""
|
|
168
|
+
if not self._isValid():
|
|
169
|
+
return False
|
|
170
|
+
try:
|
|
171
|
+
local_pos = self._ellipse_item.mapFromScene(event.scenePos())
|
|
172
|
+
if self._ellipse_item.contains(local_pos):
|
|
173
|
+
self.clicked.emit()
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
text_local_pos = self._text_item.mapFromScene(event.scenePos())
|
|
177
|
+
if self._text_item.contains(text_local_pos):
|
|
178
|
+
self.clicked.emit()
|
|
179
|
+
return True
|
|
180
|
+
except RuntimeError:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class StartMarker(QObject):
|
|
187
|
+
"""
|
|
188
|
+
起点标记 - 用于在节点右上角显示起点状态
|
|
189
|
+
包含圆形背景和🔷图标
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(self, parent: QGraphicsItem = None) -> None:
|
|
193
|
+
super().__init__()
|
|
194
|
+
|
|
195
|
+
self._parent = parent
|
|
196
|
+
self._radius: float = 15.0
|
|
197
|
+
|
|
198
|
+
# 创建圆形背景
|
|
199
|
+
self._ellipse_item = QGraphicsEllipseItem(parent)
|
|
200
|
+
self._ellipse_item.setBrush(QBrush(QColor(200, 240, 220))) # 浅绿色背景
|
|
201
|
+
self._ellipse_item.setPen(QPen(QColor(80, 150, 100), 2)) # 绿灰色描边,2像素宽
|
|
202
|
+
self._ellipse_item.setZValue(Z_VALUE_MARKER) # 确保在最上层
|
|
203
|
+
self._ellipse_item.setAcceptHoverEvents(False)
|
|
204
|
+
self._ellipse_item.setToolTip("起点节点") # 添加 tooltip
|
|
205
|
+
|
|
206
|
+
# 创建图标文本(🔷)
|
|
207
|
+
self._text_item = QGraphicsTextItem("🔷", parent)
|
|
208
|
+
self._text_item.setZValue(Z_VALUE_MARKER + 1) # 确保在圆形背景之上
|
|
209
|
+
self._text_item.setAcceptHoverEvents(False)
|
|
210
|
+
self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
211
|
+
|
|
212
|
+
# 初始隐藏
|
|
213
|
+
self._opacity: float = 0.0
|
|
214
|
+
self._updateOpacity()
|
|
215
|
+
|
|
216
|
+
# 设置按钮位置(右上角)
|
|
217
|
+
self._updatePosition()
|
|
218
|
+
|
|
219
|
+
def _isValid(self) -> bool:
|
|
220
|
+
"""检查 C++ 对象是否仍然有效"""
|
|
221
|
+
if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
|
|
222
|
+
return False
|
|
223
|
+
try:
|
|
224
|
+
self._ellipse_item.isVisible()
|
|
225
|
+
return True
|
|
226
|
+
except RuntimeError:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def _updateOpacity(self) -> None:
|
|
230
|
+
"""更新透明度"""
|
|
231
|
+
if not self._isValid():
|
|
232
|
+
return
|
|
233
|
+
try:
|
|
234
|
+
self._ellipse_item.setOpacity(self._opacity)
|
|
235
|
+
self._text_item.setOpacity(self._opacity)
|
|
236
|
+
except RuntimeError:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
def getOpacity(self) -> float:
|
|
240
|
+
"""获取当前透明度"""
|
|
241
|
+
return self._opacity
|
|
242
|
+
|
|
243
|
+
def setOpacity(self, value: float) -> None:
|
|
244
|
+
"""设置透明度"""
|
|
245
|
+
self._opacity = value
|
|
246
|
+
self._updateOpacity()
|
|
247
|
+
|
|
248
|
+
# 创建属性用于动画
|
|
249
|
+
opacity = pyqtProperty(float, getOpacity, setOpacity)
|
|
250
|
+
|
|
251
|
+
def _updatePosition(self) -> None:
|
|
252
|
+
"""更新标记位置(节点右上角)"""
|
|
253
|
+
if not self._isValid():
|
|
254
|
+
return
|
|
255
|
+
if self._parent is None:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
parent_rect = self._parent.rect()
|
|
260
|
+
center = parent_rect.center()
|
|
261
|
+
|
|
262
|
+
# 计算节点右上角的偏移位置
|
|
263
|
+
offset_x = parent_rect.width() / 2 - self._radius
|
|
264
|
+
offset_y = -parent_rect.height() / 2 + self._radius
|
|
265
|
+
|
|
266
|
+
# 设置圆形背景位置
|
|
267
|
+
ellipse_rect = QRectF(
|
|
268
|
+
center.x() + offset_x - self._radius,
|
|
269
|
+
center.y() + offset_y - self._radius,
|
|
270
|
+
self._radius * 2,
|
|
271
|
+
self._radius * 2
|
|
272
|
+
)
|
|
273
|
+
self._ellipse_item.setRect(ellipse_rect)
|
|
274
|
+
|
|
275
|
+
# 设置文本居中
|
|
276
|
+
text_rect = self._text_item.boundingRect()
|
|
277
|
+
text_pos = QPointF(
|
|
278
|
+
center.x() + offset_x - text_rect.width() / 2,
|
|
279
|
+
center.y() + offset_y - text_rect.height() / 2
|
|
280
|
+
)
|
|
281
|
+
self._text_item.setPos(text_pos)
|
|
282
|
+
except RuntimeError:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
def setRadius(self, radius: float) -> None:
|
|
286
|
+
"""设置标记半径"""
|
|
287
|
+
self._radius = radius
|
|
288
|
+
self._updatePosition()
|
|
289
|
+
|
|
290
|
+
def show(self) -> None:
|
|
291
|
+
"""显示标记(带动画)"""
|
|
292
|
+
if not self._isValid():
|
|
293
|
+
return
|
|
294
|
+
try:
|
|
295
|
+
self._updatePosition()
|
|
296
|
+
self._ellipse_item.show()
|
|
297
|
+
self._text_item.show()
|
|
298
|
+
except RuntimeError:
|
|
299
|
+
pass
|
|
300
|
+
|
|
301
|
+
def hide(self) -> None:
|
|
302
|
+
"""隐藏标记"""
|
|
303
|
+
if not self._isValid():
|
|
304
|
+
return
|
|
305
|
+
try:
|
|
306
|
+
self._ellipse_item.hide()
|
|
307
|
+
self._text_item.hide()
|
|
308
|
+
except RuntimeError:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class SingleMarker(QObject):
|
|
313
|
+
"""
|
|
314
|
+
Single标记 - 用于在节点左下角显示当前执行状态
|
|
315
|
+
包含圆形背景和⚡图标(表示正在执行)
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
def __init__(self, parent: QGraphicsItem = None) -> None:
|
|
319
|
+
super().__init__()
|
|
320
|
+
|
|
321
|
+
self._parent = parent
|
|
322
|
+
self._radius: float = 15.0
|
|
323
|
+
|
|
324
|
+
# 创建圆形背景(橙黄色 - 表示活跃/执行中)
|
|
325
|
+
self._ellipse_item = QGraphicsEllipseItem(parent)
|
|
326
|
+
self._ellipse_item.setBrush(QBrush(QColor(255, 200, 100))) # 橙黄色背景
|
|
327
|
+
self._ellipse_item.setPen(QPen(QColor(200, 150, 50), 2)) # 深橙色描边,2像素宽
|
|
328
|
+
self._ellipse_item.setZValue(Z_VALUE_MARKER) # 确保在最上层
|
|
329
|
+
self._ellipse_item.setAcceptHoverEvents(False)
|
|
330
|
+
self._ellipse_item.setToolTip("正在执行") # 添加 tooltip
|
|
331
|
+
|
|
332
|
+
# 创建图标文本(⚡)
|
|
333
|
+
self._text_item = QGraphicsTextItem("⚡", parent)
|
|
334
|
+
self._text_item.setZValue(Z_VALUE_MARKER + 1) # 确保在圆形背景之上
|
|
335
|
+
self._text_item.setAcceptHoverEvents(False)
|
|
336
|
+
self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
337
|
+
|
|
338
|
+
# 初始隐藏
|
|
339
|
+
self._opacity: float = 0.0
|
|
340
|
+
self._updateOpacity()
|
|
341
|
+
|
|
342
|
+
# 设置标记位置(左下角)
|
|
343
|
+
self._updatePosition()
|
|
344
|
+
|
|
345
|
+
def _isValid(self) -> bool:
|
|
346
|
+
"""检查 C++ 对象是否仍然有效"""
|
|
347
|
+
if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
|
|
348
|
+
return False
|
|
349
|
+
try:
|
|
350
|
+
self._ellipse_item.isVisible()
|
|
351
|
+
return True
|
|
352
|
+
except RuntimeError:
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
def _updateOpacity(self) -> None:
|
|
356
|
+
"""更新透明度"""
|
|
357
|
+
if not self._isValid():
|
|
358
|
+
return
|
|
359
|
+
try:
|
|
360
|
+
self._ellipse_item.setOpacity(self._opacity)
|
|
361
|
+
self._text_item.setOpacity(self._opacity)
|
|
362
|
+
except RuntimeError:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
def getOpacity(self) -> float:
|
|
366
|
+
"""获取当前透明度"""
|
|
367
|
+
return self._opacity
|
|
368
|
+
|
|
369
|
+
def setOpacity(self, value: float) -> None:
|
|
370
|
+
"""设置透明度"""
|
|
371
|
+
self._opacity = value
|
|
372
|
+
self._updateOpacity()
|
|
373
|
+
|
|
374
|
+
# 创建属性用于动画
|
|
375
|
+
opacity = pyqtProperty(float, getOpacity, setOpacity)
|
|
376
|
+
|
|
377
|
+
def _updatePosition(self) -> None:
|
|
378
|
+
"""更新标记位置(节点左下角)"""
|
|
379
|
+
if not self._isValid():
|
|
380
|
+
return
|
|
381
|
+
if self._parent is None:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
parent_rect = self._parent.rect()
|
|
386
|
+
center = parent_rect.center()
|
|
387
|
+
|
|
388
|
+
# 计算节点左下角的偏移位置
|
|
389
|
+
offset_x = -parent_rect.width() / 2 + self._radius
|
|
390
|
+
offset_y = parent_rect.height() / 2 - self._radius
|
|
391
|
+
|
|
392
|
+
# 设置圆形背景位置
|
|
393
|
+
ellipse_rect = QRectF(
|
|
394
|
+
center.x() + offset_x - self._radius,
|
|
395
|
+
center.y() + offset_y - self._radius,
|
|
396
|
+
self._radius * 2,
|
|
397
|
+
self._radius * 2
|
|
398
|
+
)
|
|
399
|
+
self._ellipse_item.setRect(ellipse_rect)
|
|
400
|
+
|
|
401
|
+
# 设置文本居中
|
|
402
|
+
text_rect = self._text_item.boundingRect()
|
|
403
|
+
text_pos = QPointF(
|
|
404
|
+
center.x() + offset_x - text_rect.width() / 2,
|
|
405
|
+
center.y() + offset_y - text_rect.height() / 2
|
|
406
|
+
)
|
|
407
|
+
self._text_item.setPos(text_pos)
|
|
408
|
+
except RuntimeError:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
def setRadius(self, radius: float) -> None:
|
|
412
|
+
"""设置标记半径"""
|
|
413
|
+
self._radius = radius
|
|
414
|
+
self._updatePosition()
|
|
415
|
+
|
|
416
|
+
def show(self) -> None:
|
|
417
|
+
"""显示标记(带动画)"""
|
|
418
|
+
if not self._isValid():
|
|
419
|
+
return
|
|
420
|
+
try:
|
|
421
|
+
self._updatePosition()
|
|
422
|
+
self._ellipse_item.show()
|
|
423
|
+
self._text_item.show()
|
|
424
|
+
except RuntimeError:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
def hide(self) -> None:
|
|
428
|
+
"""隐藏标记"""
|
|
429
|
+
if not self._isValid():
|
|
430
|
+
return
|
|
431
|
+
try:
|
|
432
|
+
self._ellipse_item.hide()
|
|
433
|
+
self._text_item.hide()
|
|
434
|
+
except RuntimeError:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class CustomMarker(QObject):
|
|
439
|
+
"""
|
|
440
|
+
Custom标记 - 用户自定义标识
|
|
441
|
+
支持自定义显示字符、背景色、前景色、边框颜色和厚度、半径
|
|
442
|
+
默认位置在节点右下角
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
# 默认样式值
|
|
446
|
+
DEFAULT_TEXT = "★"
|
|
447
|
+
DEFAULT_BG_COLOR = QColor(227, 242, 253) # 浅蓝色 #E3F2FD
|
|
448
|
+
DEFAULT_FG_COLOR = QColor(21, 101, 192) # 深蓝色 #1565C0
|
|
449
|
+
DEFAULT_BORDER_COLOR = QColor(100, 181, 246) # 中蓝色 #64B5F6
|
|
450
|
+
DEFAULT_BORDER_WIDTH = 2
|
|
451
|
+
DEFAULT_RADIUS = 15.0
|
|
452
|
+
|
|
453
|
+
def __init__(self, parent: QGraphicsItem = None) -> None:
|
|
454
|
+
super().__init__()
|
|
455
|
+
|
|
456
|
+
self._parent = parent
|
|
457
|
+
self._radius: float = self.DEFAULT_RADIUS
|
|
458
|
+
self._text: str = self.DEFAULT_TEXT
|
|
459
|
+
self._bg_color: QColor = self.DEFAULT_BG_COLOR
|
|
460
|
+
self._fg_color: QColor = self.DEFAULT_FG_COLOR
|
|
461
|
+
self._border_color: QColor = self.DEFAULT_BORDER_COLOR
|
|
462
|
+
self._border_width: float = self.DEFAULT_BORDER_WIDTH
|
|
463
|
+
|
|
464
|
+
# 创建圆形背景
|
|
465
|
+
self._ellipse_item = QGraphicsEllipseItem(parent)
|
|
466
|
+
self._ellipse_item.setBrush(QBrush(self._bg_color))
|
|
467
|
+
self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
|
|
468
|
+
self._ellipse_item.setZValue(Z_VALUE_MARKER)
|
|
469
|
+
self._ellipse_item.setAcceptHoverEvents(False)
|
|
470
|
+
self._ellipse_item.setToolTip("自定义标记")
|
|
471
|
+
|
|
472
|
+
# 创建图标文本
|
|
473
|
+
self._text_item = QGraphicsTextItem(self._text, parent)
|
|
474
|
+
self._text_item.setDefaultTextColor(self._fg_color)
|
|
475
|
+
self._text_item.setZValue(Z_VALUE_MARKER + 1)
|
|
476
|
+
self._text_item.setAcceptHoverEvents(False)
|
|
477
|
+
self._text_item.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
478
|
+
|
|
479
|
+
# 初始隐藏
|
|
480
|
+
self._opacity: float = 0.0
|
|
481
|
+
self._updateOpacity()
|
|
482
|
+
|
|
483
|
+
# 设置标记位置(右下角)
|
|
484
|
+
self._updatePosition()
|
|
485
|
+
|
|
486
|
+
def _isValid(self) -> bool:
|
|
487
|
+
"""检查 C++ 对象是否仍然有效"""
|
|
488
|
+
if not hasattr(self, '_ellipse_item') or self._ellipse_item is None:
|
|
489
|
+
return False
|
|
490
|
+
try:
|
|
491
|
+
self._ellipse_item.isVisible()
|
|
492
|
+
return True
|
|
493
|
+
except RuntimeError:
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
def _updateOpacity(self) -> None:
|
|
497
|
+
"""更新透明度"""
|
|
498
|
+
if not self._isValid():
|
|
499
|
+
return
|
|
500
|
+
try:
|
|
501
|
+
self._ellipse_item.setOpacity(self._opacity)
|
|
502
|
+
self._text_item.setOpacity(self._opacity)
|
|
503
|
+
except RuntimeError:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
def getOpacity(self) -> float:
|
|
507
|
+
"""获取当前透明度"""
|
|
508
|
+
return self._opacity
|
|
509
|
+
|
|
510
|
+
def setOpacity(self, value: float) -> None:
|
|
511
|
+
"""设置透明度"""
|
|
512
|
+
self._opacity = value
|
|
513
|
+
self._updateOpacity()
|
|
514
|
+
|
|
515
|
+
# 创建属性用于动画
|
|
516
|
+
opacity = pyqtProperty(float, getOpacity, setOpacity)
|
|
517
|
+
|
|
518
|
+
def setText(self, text: str) -> None:
|
|
519
|
+
"""设置显示字符"""
|
|
520
|
+
self._text = text if text else self.DEFAULT_TEXT
|
|
521
|
+
if self._isValid():
|
|
522
|
+
self._text_item.setPlainText(self._text)
|
|
523
|
+
self._updatePosition()
|
|
524
|
+
|
|
525
|
+
def setBgColor(self, color: QColor) -> None:
|
|
526
|
+
"""设置背景色"""
|
|
527
|
+
self._bg_color = color if color else self.DEFAULT_BG_COLOR
|
|
528
|
+
if self._isValid():
|
|
529
|
+
self._ellipse_item.setBrush(QBrush(self._bg_color))
|
|
530
|
+
|
|
531
|
+
def setFgColor(self, color: QColor) -> None:
|
|
532
|
+
"""设置前景色(文本颜色)"""
|
|
533
|
+
self._fg_color = color if color else self.DEFAULT_FG_COLOR
|
|
534
|
+
if self._isValid():
|
|
535
|
+
self._text_item.setDefaultTextColor(self._fg_color)
|
|
536
|
+
|
|
537
|
+
def setBorderColor(self, color: QColor) -> None:
|
|
538
|
+
"""设置边框颜色"""
|
|
539
|
+
self._border_color = color if color else self.DEFAULT_BORDER_COLOR
|
|
540
|
+
if self._isValid():
|
|
541
|
+
self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
|
|
542
|
+
|
|
543
|
+
def setBorderWidth(self, width: float) -> None:
|
|
544
|
+
"""设置边框厚度"""
|
|
545
|
+
self._border_width = width if width > 0 else self.DEFAULT_BORDER_WIDTH
|
|
546
|
+
if self._isValid():
|
|
547
|
+
self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
|
|
548
|
+
|
|
549
|
+
def setRadius(self, radius: float) -> None:
|
|
550
|
+
"""设置标记半径"""
|
|
551
|
+
self._radius = radius if radius > 0 else self.DEFAULT_RADIUS
|
|
552
|
+
self._updatePosition()
|
|
553
|
+
|
|
554
|
+
def setStyle(
|
|
555
|
+
self,
|
|
556
|
+
text: str = None,
|
|
557
|
+
bg_color: QColor = None,
|
|
558
|
+
fg_color: QColor = None,
|
|
559
|
+
border_color: QColor = None,
|
|
560
|
+
border_width: float = None,
|
|
561
|
+
radius: float = None
|
|
562
|
+
) -> None:
|
|
563
|
+
"""批量设置样式"""
|
|
564
|
+
if text is not None:
|
|
565
|
+
self._text = text
|
|
566
|
+
if bg_color is not None:
|
|
567
|
+
self._bg_color = bg_color
|
|
568
|
+
if fg_color is not None:
|
|
569
|
+
self._fg_color = fg_color
|
|
570
|
+
if border_color is not None:
|
|
571
|
+
self._border_color = border_color
|
|
572
|
+
if border_width is not None:
|
|
573
|
+
self._border_width = border_width
|
|
574
|
+
if radius is not None:
|
|
575
|
+
self._radius = radius
|
|
576
|
+
|
|
577
|
+
if self._isValid():
|
|
578
|
+
self._text_item.setPlainText(self._text)
|
|
579
|
+
self._text_item.setDefaultTextColor(self._fg_color)
|
|
580
|
+
self._ellipse_item.setBrush(QBrush(self._bg_color))
|
|
581
|
+
self._ellipse_item.setPen(QPen(self._border_color, self._border_width))
|
|
582
|
+
self._updatePosition()
|
|
583
|
+
|
|
584
|
+
def getStyle(self) -> dict:
|
|
585
|
+
"""获取当前样式配置"""
|
|
586
|
+
return {
|
|
587
|
+
"text": self._text,
|
|
588
|
+
"bg_color": self._bg_color.name(),
|
|
589
|
+
"fg_color": self._fg_color.name(),
|
|
590
|
+
"border_color": self._border_color.name(),
|
|
591
|
+
"border_width": self._border_width,
|
|
592
|
+
"radius": self._radius
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
def _updatePosition(self) -> None:
|
|
596
|
+
"""更新标记位置(节点右下角)"""
|
|
597
|
+
if not self._isValid():
|
|
598
|
+
return
|
|
599
|
+
if self._parent is None:
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
parent_rect = self._parent.rect()
|
|
604
|
+
center = parent_rect.center()
|
|
605
|
+
|
|
606
|
+
# 计算节点右下角的偏移位置
|
|
607
|
+
offset_x = parent_rect.width() / 2 - self._radius
|
|
608
|
+
offset_y = parent_rect.height() / 2 - self._radius
|
|
609
|
+
|
|
610
|
+
# 设置圆形背景位置
|
|
611
|
+
ellipse_rect = QRectF(
|
|
612
|
+
center.x() + offset_x - self._radius,
|
|
613
|
+
center.y() + offset_y - self._radius,
|
|
614
|
+
self._radius * 2,
|
|
615
|
+
self._radius * 2
|
|
616
|
+
)
|
|
617
|
+
self._ellipse_item.setRect(ellipse_rect)
|
|
618
|
+
|
|
619
|
+
# 设置文本居中
|
|
620
|
+
text_rect = self._text_item.boundingRect()
|
|
621
|
+
text_pos = QPointF(
|
|
622
|
+
center.x() + offset_x - text_rect.width() / 2,
|
|
623
|
+
center.y() + offset_y - text_rect.height() / 2
|
|
624
|
+
)
|
|
625
|
+
self._text_item.setPos(text_pos)
|
|
626
|
+
except RuntimeError:
|
|
627
|
+
pass
|
|
628
|
+
|
|
629
|
+
def show(self) -> None:
|
|
630
|
+
"""显示标记(带动画)"""
|
|
631
|
+
if not self._isValid():
|
|
632
|
+
return
|
|
633
|
+
try:
|
|
634
|
+
self._updatePosition()
|
|
635
|
+
self._ellipse_item.show()
|
|
636
|
+
self._text_item.show()
|
|
637
|
+
except RuntimeError:
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
def hide(self) -> None:
|
|
641
|
+
"""隐藏标记"""
|
|
642
|
+
if not self._isValid():
|
|
643
|
+
return
|
|
644
|
+
try:
|
|
645
|
+
self._ellipse_item.hide()
|
|
646
|
+
self._text_item.hide()
|
|
647
|
+
except RuntimeError:
|
|
648
|
+
pass
|