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,754 @@
|
|
|
1
|
+
"""
|
|
2
|
+
节点项模块 - 表示节点图中的一个节点
|
|
3
|
+
支持圆形、矩形、菱形三种形状
|
|
4
|
+
|
|
5
|
+
重构说明:
|
|
6
|
+
本模块原为一个 1945 行的单体类, 现已按关注点拆分为多层继承结构,
|
|
7
|
+
分散于以下模块:
|
|
8
|
+
- _node_interface.py : INodeItem(Protocol) —— 纯类型契约(不参与运行时继承)
|
|
9
|
+
- _node_base.py : _NodeCore(QGraphicsEllipseItem) —— 常量/信号/几何核心/样式核心/属性
|
|
10
|
+
- _node_ports.py : _NodePortMixin —— 端口属性/布局/hover/字体刷新
|
|
11
|
+
- _node_interaction.py : _NodeInteractionMixin —— hover/鼠标/名称编辑/选中/标记
|
|
12
|
+
- _node_markers.py : HoverButton/StartMarker/SingleMarker/CustomMarker —— 标记辅助类
|
|
13
|
+
|
|
14
|
+
NodeItem 继承上述 Mixin/Core, 并承载依赖跨层方法的最终实现:
|
|
15
|
+
paint / getEdgePoint / toDict / fromDict。
|
|
16
|
+
|
|
17
|
+
对外 API(公开成员、外部依赖的私有成员、模块常量、辅助类)完全保持不变:
|
|
18
|
+
- ``from chartflow.node.node_item import NodeItem`` 不变
|
|
19
|
+
- ``from chartflow.node.node_item import INNER_RATIO, CIRCLE_PORT_WINDOW_DEG`` 不变
|
|
20
|
+
- ``from chartflow.node.node_item import HoverButton, StartMarker, ...`` 不变
|
|
21
|
+
- ``node._port_items`` / ``node._style`` / ``node._custom_fill_color`` 等私有成员不变
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
import math
|
|
26
|
+
from typing import Optional, Dict, Any
|
|
27
|
+
|
|
28
|
+
from PyQt6.QtCore import Qt, QPointF, QRectF, QPropertyAnimation, QEasingCurve, QTimer
|
|
29
|
+
from PyQt6.QtGui import QBrush, QPen, QColor, QPainterPath
|
|
30
|
+
from PyQt6.QtWidgets import QGraphicsItem
|
|
31
|
+
|
|
32
|
+
from chartflow.node.enums import NodeShape, NodeState
|
|
33
|
+
from chartflow.node.port_item import port_to_dict
|
|
34
|
+
|
|
35
|
+
# 分层基类
|
|
36
|
+
from chartflow.node._node_base import _NodeCore
|
|
37
|
+
from chartflow.node._node_ports import _NodePortMixin
|
|
38
|
+
from chartflow.node._node_interaction import _NodeInteractionMixin
|
|
39
|
+
|
|
40
|
+
# 辅助类 + 信号辅助类 re-export(保持历史导入路径)
|
|
41
|
+
from chartflow.node._node_markers import (
|
|
42
|
+
HoverButton, StartMarker, SingleMarker, CustomMarker,
|
|
43
|
+
)
|
|
44
|
+
from chartflow.node._node_base import NodeSignalHelper
|
|
45
|
+
|
|
46
|
+
# 模块常量 re-export(保持 ``from chartflow.node.node_item import XXX`` 可用)
|
|
47
|
+
from chartflow.node._node_base import ( # noqa: F401
|
|
48
|
+
MIN_RADIUS,
|
|
49
|
+
PADDING,
|
|
50
|
+
TEXT_EDIT_PADDING,
|
|
51
|
+
BUTTON_RADIUS_RATIO,
|
|
52
|
+
ANIMATION_DURATION_SHOW,
|
|
53
|
+
ANIMATION_DURATION_HIDE,
|
|
54
|
+
Z_VALUE_NODE,
|
|
55
|
+
Z_VALUE_BUTTON,
|
|
56
|
+
Z_VALUE_MARKER,
|
|
57
|
+
INNER_RATIO,
|
|
58
|
+
PORT_MIN_GAP,
|
|
59
|
+
NAME_INNER_PAD,
|
|
60
|
+
HOVER_MOVE_THRESHOLD_SQ,
|
|
61
|
+
CIRCLE_PORT_WINDOW_DEG,
|
|
62
|
+
CIRCLE_PORT_MAX_STEP_DEG,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Protocol 接口 re-export(便于外部做类型检查/类型提示)
|
|
66
|
+
from chartflow.node._node_interface import INodeItem # noqa: F401
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class QNodeItem(_NodePortMixin, _NodeInteractionMixin, _NodeCore):
|
|
70
|
+
"""
|
|
71
|
+
节点项 - 表示节点图中的一个节点
|
|
72
|
+
支持圆形、矩形、菱形三种形状
|
|
73
|
+
|
|
74
|
+
继承自 _NodePortMixin + _NodeInteractionMixin + _NodeCore, 结构上满足
|
|
75
|
+
INodeItem(Protocol) 契约。本类承载依赖跨层方法的最终实现:
|
|
76
|
+
paint(绘制各形状 + 边缘指示器 + 更名虚线框)、getEdgePoint(各形状边缘交点)、
|
|
77
|
+
toDict/fromDict(序列化)。
|
|
78
|
+
|
|
79
|
+
所有对外成员(含外部依赖的私有成员)路径与重构前完全一致。
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------ #
|
|
83
|
+
# 外环(可连线边缘指示器)可见性
|
|
84
|
+
# ------------------------------------------------------------------ #
|
|
85
|
+
def setEdgeIndicatorVisible(self, visible: bool) -> None:
|
|
86
|
+
"""设置节点外环(可连线边缘指示器)是否显示。
|
|
87
|
+
|
|
88
|
+
外环是绘制在节点边缘、提示可从此处拖拽创建连线的浅灰色环。
|
|
89
|
+
关闭后不再绘制, 但边缘的连线命中判定不变(仍由
|
|
90
|
+
``node_canvas._is_on_node_edge`` 决定)。
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
visible: ``True`` 显示外环, ``False`` 隐藏。
|
|
94
|
+
"""
|
|
95
|
+
self._edge_indicator_visible = visible
|
|
96
|
+
self.update()
|
|
97
|
+
|
|
98
|
+
def isEdgeIndicatorVisible(self) -> bool:
|
|
99
|
+
"""查询节点外环(可连线边缘指示器)当前是否显示。
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
``True`` 表示显示中, ``False`` 表示已隐藏。
|
|
103
|
+
"""
|
|
104
|
+
return self._edge_indicator_visible
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------ #
|
|
107
|
+
# 连线落点高亮(画布在 port/edge 连线拖拽中手动驱动)
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
def setDropTarget(self, on: bool) -> None:
|
|
110
|
+
"""标记/取消本节点作为连线落点(目标端)的高亮态。
|
|
111
|
+
|
|
112
|
+
连线拖拽期间画布接管鼠标移动事件, 光标下的目标节点不会收到
|
|
113
|
+
hoverEnter/hoverLeave(场景 hover 派发被压制), 故由画布在
|
|
114
|
+
mouseMoveEvent 中手动命中检测后调用本方法驱动描边。
|
|
115
|
+
使用与 port 描边一致的 DodgerBlue(#1E90FF)描边, 与选中态描边
|
|
116
|
+
同色但优先级最高(覆盖 hover/selected 的描边视觉), 不改变
|
|
117
|
+
``_state`` 本身。
|
|
118
|
+
"""
|
|
119
|
+
if self._drop_target == on:
|
|
120
|
+
return
|
|
121
|
+
self._drop_target = on
|
|
122
|
+
self.update()
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------ #
|
|
125
|
+
# 不合规连接反馈
|
|
126
|
+
# ------------------------------------------------------------------ #
|
|
127
|
+
def flashError(self) -> None:
|
|
128
|
+
"""红色描边闪烁(试图创建不合规连接时的反馈)。
|
|
129
|
+
|
|
130
|
+
QTimer 驱动 3 次闪烁(150ms 间隔), 在红色描边与正常描边间 toggle。
|
|
131
|
+
"""
|
|
132
|
+
# 停掉正在进行的闪烁, 重新开始
|
|
133
|
+
if self._flash_timer is not None:
|
|
134
|
+
self._flash_timer.stop()
|
|
135
|
+
self._flash_timer.deleteLater()
|
|
136
|
+
self._flash_timer = None
|
|
137
|
+
self._flash_error = True
|
|
138
|
+
self.update()
|
|
139
|
+
|
|
140
|
+
flash_count = [0]
|
|
141
|
+
|
|
142
|
+
def toggleFlash():
|
|
143
|
+
flash_count[0] += 1
|
|
144
|
+
# 奇数次: 显示红色描边(_flash_error=True); 偶数次: 关闭
|
|
145
|
+
self._flash_error = (flash_count[0] % 2 == 0)
|
|
146
|
+
self.update()
|
|
147
|
+
if flash_count[0] >= 6: # 3 次闪烁 = 6 次切换
|
|
148
|
+
self._flash_timer.stop()
|
|
149
|
+
self._flash_timer.deleteLater()
|
|
150
|
+
self._flash_timer = None
|
|
151
|
+
self._flash_error = False
|
|
152
|
+
self.update()
|
|
153
|
+
|
|
154
|
+
self._flash_timer = QTimer()
|
|
155
|
+
self._flash_timer.timeout.connect(toggleFlash)
|
|
156
|
+
self._flash_timer.start(150)
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------ #
|
|
159
|
+
# 连线能力(是否可参与连线)
|
|
160
|
+
# ------------------------------------------------------------------ #
|
|
161
|
+
def setConnectable(self, connectable: bool) -> None:
|
|
162
|
+
"""设置节点是否可参与连线。
|
|
163
|
+
|
|
164
|
+
设为 ``False`` 时:
|
|
165
|
+
- 清除该节点已有的全部连线(两端任一为本节点即删除);
|
|
166
|
+
- 软联动隐藏外环(此后用户仍可单独调用
|
|
167
|
+
:meth:`setEdgeIndicatorVisible` 重新开启, 不会被强制覆盖);
|
|
168
|
+
- 后续任何新建连线(边缘拖拽 / 箭头重连 / 导入)只要涉及本节点,
|
|
169
|
+
都会被 :meth:`~NodeCanvas.addConnection` 拒绝。
|
|
170
|
+
|
|
171
|
+
设为 ``True`` 时仅恢复连线能力, 不重建已删连线, 也不自动恢复外环。
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
connectable: ``True`` 允许参与连线, ``False`` 禁止并清除已有连线。
|
|
175
|
+
"""
|
|
176
|
+
if self._connectable == connectable:
|
|
177
|
+
return
|
|
178
|
+
self._connectable = connectable
|
|
179
|
+
# 软联动: 不可连线时自动隐藏外环(不硬性限制, 用户可随后自行再开)
|
|
180
|
+
if not connectable:
|
|
181
|
+
self._edge_indicator_visible = False
|
|
182
|
+
self.update()
|
|
183
|
+
# 清除已有连线
|
|
184
|
+
if not connectable:
|
|
185
|
+
self._removeAllConnections()
|
|
186
|
+
|
|
187
|
+
def isConnectable(self) -> bool:
|
|
188
|
+
"""查询节点当前是否可参与连线。
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
``True`` 可参与连线(默认), ``False`` 已被禁止。
|
|
192
|
+
"""
|
|
193
|
+
return self._connectable
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------ #
|
|
196
|
+
# 自指规则(selfrefs)
|
|
197
|
+
# ------------------------------------------------------------------ #
|
|
198
|
+
@property
|
|
199
|
+
def selfrefs(self) -> Dict[str, bool]:
|
|
200
|
+
"""自指规则: 控制同节点内 6 类自指连接是否允许创建。
|
|
201
|
+
|
|
202
|
+
只读 property, 但返回的是内部 dict(**非副本**), 可直接修改其中的
|
|
203
|
+
bool 值, 修改立即对后续 :meth:`NodeCanvas.addConnection` 生效::
|
|
204
|
+
|
|
205
|
+
node.selfrefs["node->node"] = False # 禁止 node 自环
|
|
206
|
+
node.selfrefs["port->port"] = True # 允许同 port 自指
|
|
207
|
+
|
|
208
|
+
六个键(默认值 = 既有逻辑):
|
|
209
|
+
|
|
210
|
+
- ``"node->node"`` (默认 True): 同节点、两端均无 port 的自环。
|
|
211
|
+
- ``"port->port"`` (默认 False): 同节点、同极或同一 port 的自指
|
|
212
|
+
(不含异极 left↔right)。
|
|
213
|
+
- ``"node->port"`` (默认 False): 同节点、源为节点边缘、目标为本节点 port。
|
|
214
|
+
- ``"port->node"`` (默认 False): 同节点、源为本节点 port、目标为节点边缘。
|
|
215
|
+
- ``"left->right"`` (默认 True): 同节点、源=left port、目标=right port。
|
|
216
|
+
- ``"right->left"`` (默认 True): 同节点、源=right port、目标=left port。
|
|
217
|
+
|
|
218
|
+
``left->right`` 与 ``right->left`` 是两条**独立**方向的自指边,
|
|
219
|
+
各自独立开关, 互不去重(可同时存在两条不同方向的环)。
|
|
220
|
+
|
|
221
|
+
所有被允许的自指都会被 :class:`ConnectionItem` 绘制为环/椭圆环。
|
|
222
|
+
"""
|
|
223
|
+
return self._selfrefs
|
|
224
|
+
|
|
225
|
+
def _removeAllConnections(self) -> None:
|
|
226
|
+
"""删除场景中所有与本节点相连的连线。
|
|
227
|
+
|
|
228
|
+
通过 ``_findCanvas()`` 定位所属 ``NodeCanvas``, 复用其
|
|
229
|
+
``removeConnection``(会清理箭头、更新场景、置脏)。
|
|
230
|
+
连线只存在于画布的 ``_connections``, 节点本身不持有列表。
|
|
231
|
+
"""
|
|
232
|
+
canvas = self._findCanvas()
|
|
233
|
+
if canvas is None:
|
|
234
|
+
return
|
|
235
|
+
# connections 属性返回副本, 可安全边删边遍历
|
|
236
|
+
attached = [c for c in canvas.connections
|
|
237
|
+
if c.sourceNode is self or c.targetNode is self]
|
|
238
|
+
for c in attached:
|
|
239
|
+
canvas.removeConnection(c)
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------ #
|
|
242
|
+
# 几何: 边缘交点
|
|
243
|
+
# ------------------------------------------------------------------ #
|
|
244
|
+
def getEdgePoint(self, target_pos: QPointF) -> QPointF:
|
|
245
|
+
"""
|
|
246
|
+
计算从节点中心到目标方向的边缘点
|
|
247
|
+
根据节点形状不同,计算方式不同
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
target_pos: 目标位置
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
边缘点坐标
|
|
254
|
+
"""
|
|
255
|
+
center = self.sceneBoundingRect().center()
|
|
256
|
+
|
|
257
|
+
if self._shape == NodeShape.ELLIPSE:
|
|
258
|
+
# 圆形:使用角度计算边缘点
|
|
259
|
+
dx = target_pos.x() - center.x()
|
|
260
|
+
dy = target_pos.y() - center.y()
|
|
261
|
+
angle = math.atan2(dy, dx)
|
|
262
|
+
|
|
263
|
+
return QPointF(
|
|
264
|
+
center.x() + self._radius * math.cos(angle),
|
|
265
|
+
center.y() + self._radius * math.sin(angle)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
elif self._shape == NodeShape.RECTANGLE:
|
|
269
|
+
# 圆角矩形处理 - 参考老版本实现
|
|
270
|
+
rect = self.rect()
|
|
271
|
+
local_target = self.mapFromScene(target_pos)
|
|
272
|
+
local_center = rect.center()
|
|
273
|
+
|
|
274
|
+
dx = local_target.x() - local_center.x()
|
|
275
|
+
dy = local_target.y() - local_center.y()
|
|
276
|
+
|
|
277
|
+
if abs(dx) < 1e-6 and abs(dy) < 1e-6:
|
|
278
|
+
return center
|
|
279
|
+
|
|
280
|
+
half_w = rect.width() / 2
|
|
281
|
+
half_h = rect.height() / 2
|
|
282
|
+
corner_radius = min(half_w, half_h) * 0.2
|
|
283
|
+
|
|
284
|
+
# 计算与外层矩形的交点
|
|
285
|
+
t_x = half_w / abs(dx) if abs(dx) > 1e-6 else float('inf')
|
|
286
|
+
t_y = half_h / abs(dy) if abs(dy) > 1e-6 else float('inf')
|
|
287
|
+
t_rect = min(t_x, t_y)
|
|
288
|
+
|
|
289
|
+
# 矩形交点(本地坐标)
|
|
290
|
+
ix = local_center.x() + dx * t_rect
|
|
291
|
+
iy = local_center.y() + dy * t_rect
|
|
292
|
+
|
|
293
|
+
# 内矩形边界(不含圆角的区域)
|
|
294
|
+
inner_w = half_w - corner_radius
|
|
295
|
+
inner_h = half_h - corner_radius
|
|
296
|
+
|
|
297
|
+
# 检查是否在圆角区域(同时超出内矩形的x和y边界)
|
|
298
|
+
in_corner = abs(ix - local_center.x()) > inner_w and abs(iy - local_center.y()) > inner_h
|
|
299
|
+
|
|
300
|
+
if not in_corner:
|
|
301
|
+
# 在直线边上,直接返回矩形交点
|
|
302
|
+
return self.mapToScene(QPointF(ix, iy))
|
|
303
|
+
|
|
304
|
+
# 在圆角区域,计算与圆的精确交点
|
|
305
|
+
sign_x = 1 if dx > 0 else -1
|
|
306
|
+
sign_y = 1 if dy > 0 else -1
|
|
307
|
+
|
|
308
|
+
# 圆角圆心(本地坐标)
|
|
309
|
+
cx = local_center.x() + sign_x * inner_w
|
|
310
|
+
cy = local_center.y() + sign_y * inner_h
|
|
311
|
+
|
|
312
|
+
# 解二次方程:|t*V - (C - O)|^2 = r^2
|
|
313
|
+
a = dx * dx + dy * dy
|
|
314
|
+
b = -2 * (dx * (cx - local_center.x()) + dy * (cy - local_center.y()))
|
|
315
|
+
c = (cx - local_center.x()) ** 2 + (cy - local_center.y()) ** 2 - corner_radius ** 2
|
|
316
|
+
|
|
317
|
+
discriminant = b * b - 4 * a * c
|
|
318
|
+
|
|
319
|
+
if discriminant < 0:
|
|
320
|
+
return self.mapToScene(QPointF(ix, iy))
|
|
321
|
+
|
|
322
|
+
sqrt_d = math.sqrt(discriminant)
|
|
323
|
+
t1 = (-b - sqrt_d) / (2 * a)
|
|
324
|
+
t2 = (-b + sqrt_d) / (2 * a)
|
|
325
|
+
|
|
326
|
+
# 边界限制
|
|
327
|
+
min_x = local_center.x() - half_w
|
|
328
|
+
max_x = local_center.x() + half_w
|
|
329
|
+
min_y = local_center.y() - half_h
|
|
330
|
+
max_y = local_center.y() + half_h
|
|
331
|
+
|
|
332
|
+
valid_ts = []
|
|
333
|
+
for t in [t1, t2]:
|
|
334
|
+
if t > 1e-6:
|
|
335
|
+
px = local_center.x() + dx * t
|
|
336
|
+
py = local_center.y() + dy * t
|
|
337
|
+
|
|
338
|
+
# 检查是否在正确的象限
|
|
339
|
+
in_quad_x = (sign_x > 0 and px >= cx) or (sign_x < 0 and px <= cx)
|
|
340
|
+
in_quad_y = (sign_y > 0 and py >= cy) or (sign_y < 0 and py <= cy)
|
|
341
|
+
in_bounds = (min_x <= px <= max_x) and (min_y <= py <= max_y)
|
|
342
|
+
|
|
343
|
+
if in_quad_x and in_quad_y and in_bounds:
|
|
344
|
+
valid_ts.append(t)
|
|
345
|
+
|
|
346
|
+
if not valid_ts:
|
|
347
|
+
return self.mapToScene(QPointF(ix, iy))
|
|
348
|
+
|
|
349
|
+
# 取较近的有效交点
|
|
350
|
+
t_arc = min(valid_ts)
|
|
351
|
+
local_edge_x = local_center.x() + dx * t_arc
|
|
352
|
+
local_edge_y = local_center.y() + dy * t_arc
|
|
353
|
+
|
|
354
|
+
return self.mapToScene(QPointF(local_edge_x, local_edge_y))
|
|
355
|
+
|
|
356
|
+
elif self._shape == NodeShape.DIAMOND:
|
|
357
|
+
# 菱形:计算与菱形边界的交点
|
|
358
|
+
dx = target_pos.x() - center.x()
|
|
359
|
+
dy = target_pos.y() - center.y()
|
|
360
|
+
|
|
361
|
+
if dx == 0 and dy == 0:
|
|
362
|
+
return center
|
|
363
|
+
|
|
364
|
+
# 菱形由四个顶点组成,使用参数方程计算交点
|
|
365
|
+
abs_dx = abs(dx)
|
|
366
|
+
abs_dy = abs(dy)
|
|
367
|
+
|
|
368
|
+
if abs_dx + abs_dy == 0:
|
|
369
|
+
return center
|
|
370
|
+
|
|
371
|
+
scale = self._radius / (abs_dx + abs_dy)
|
|
372
|
+
|
|
373
|
+
return QPointF(
|
|
374
|
+
center.x() + dx * scale,
|
|
375
|
+
center.y() + dy * scale
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return center
|
|
379
|
+
|
|
380
|
+
# ------------------------------------------------------------------ #
|
|
381
|
+
# 序列化
|
|
382
|
+
# ------------------------------------------------------------------ #
|
|
383
|
+
def toDict(self) -> dict:
|
|
384
|
+
"""
|
|
385
|
+
将节点转换为字典
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
包含节点所有数据的字典
|
|
389
|
+
"""
|
|
390
|
+
# deepcopy: data 中可能含列表/字典等可变嵌套(如 _importDict 写入的
|
|
391
|
+
# targets/decorates), 浅拷贝会让外部就地修改污染原节点 _data。
|
|
392
|
+
result = {
|
|
393
|
+
"id": self._node_id,
|
|
394
|
+
"name": self._name,
|
|
395
|
+
"shape": self._shape.name,
|
|
396
|
+
"position": {"x": self.pos().x(), "y": self.pos().y()},
|
|
397
|
+
"data": copy.deepcopy(self._data),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# 装饰器类型(semantic 描边色: recursive/behavior/sequence/selector/parallel)。
|
|
401
|
+
# 此前完全未序列化, 导出再导入后彩色边框丢失。
|
|
402
|
+
if self._decorator is not None:
|
|
403
|
+
result["decorator"] = self._decorator
|
|
404
|
+
|
|
405
|
+
# 添加样式信息(如果有自定义样式)
|
|
406
|
+
style_data = {}
|
|
407
|
+
if hasattr(self, '_custom_fill_color') and self._custom_fill_color:
|
|
408
|
+
style_data["fill_color"] = self._custom_fill_color.name()
|
|
409
|
+
if hasattr(self, '_custom_stroke_color') and self._custom_stroke_color:
|
|
410
|
+
style_data["stroke_color"] = self._custom_stroke_color.name()
|
|
411
|
+
if hasattr(self, '_custom_stroke_width') and self._custom_stroke_width is not None:
|
|
412
|
+
style_data["stroke_width"] = self._custom_stroke_width
|
|
413
|
+
if hasattr(self, '_custom_font_family') and self._custom_font_family:
|
|
414
|
+
style_data["font_family"] = self._custom_font_family
|
|
415
|
+
if hasattr(self, '_custom_font_size') and self._custom_font_size:
|
|
416
|
+
style_data["font_size"] = self._custom_font_size
|
|
417
|
+
if hasattr(self, '_custom_radius') and self._custom_radius:
|
|
418
|
+
style_data["radius"] = self._custom_radius
|
|
419
|
+
|
|
420
|
+
if style_data:
|
|
421
|
+
result["style"] = style_data
|
|
422
|
+
|
|
423
|
+
# 添加 custom 标记配置(如果有)
|
|
424
|
+
if self._custom_marker is not None:
|
|
425
|
+
result["custom"] = self._custom_marker.getStyle()
|
|
426
|
+
# 标记可见性: 此前只存样式不存可见性, 导致导入后 marker 对象重建
|
|
427
|
+
# 但 opacity 仍为初始 0(隐藏), 用户可见的标记变看不见。
|
|
428
|
+
if self._is_custom_marker_visible:
|
|
429
|
+
result["custom_marker_visible"] = True
|
|
430
|
+
|
|
431
|
+
# single 标记可见性: 此前完全未序列化, 导入后 single 标记丢失。
|
|
432
|
+
if self._is_single_marker_visible:
|
|
433
|
+
result["single_marker_visible"] = True
|
|
434
|
+
|
|
435
|
+
# 添加端口配置(如果有)
|
|
436
|
+
if self._ports_spec.get("left") or self._ports_spec.get("right"):
|
|
437
|
+
result["ports"] = {
|
|
438
|
+
"left": [port_to_dict(p) for p in self._ports_spec["left"]],
|
|
439
|
+
"right": [port_to_dict(p) for p in self._ports_spec["right"]],
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# 外环可见性(仅在隐藏时序列化,避免污染默认导出)
|
|
443
|
+
if not self._edge_indicator_visible:
|
|
444
|
+
result["edge_indicator_visible"] = False
|
|
445
|
+
|
|
446
|
+
# 连线能力(仅在不可连线时序列化,避免污染默认导出)
|
|
447
|
+
if not self._connectable:
|
|
448
|
+
result["connectable"] = False
|
|
449
|
+
|
|
450
|
+
# 自指规则: 仅序列化与默认值不同的键(避免污染默认导出)。
|
|
451
|
+
# 必须序列化, 否则导入新节点 selfrefs 全为默认值, 用户放宽过的
|
|
452
|
+
# port->port / node->port / port->node 等自指连接会被 addConnection
|
|
453
|
+
# 以规则 False 拒绝, 静默丢失。
|
|
454
|
+
selfref_defaults = {
|
|
455
|
+
"node->node": True, "port->port": False, "node->port": False,
|
|
456
|
+
"port->node": False, "left->right": True, "right->left": True,
|
|
457
|
+
}
|
|
458
|
+
selfref_diff = {
|
|
459
|
+
k: v for k, v in self._selfrefs.items()
|
|
460
|
+
if selfref_defaults.get(k) != v
|
|
461
|
+
}
|
|
462
|
+
if selfref_diff:
|
|
463
|
+
result["selfrefs"] = selfref_diff
|
|
464
|
+
|
|
465
|
+
return result
|
|
466
|
+
|
|
467
|
+
def fromDict(self, data: dict) -> None:
|
|
468
|
+
"""
|
|
469
|
+
从字典更新节点数据
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
data: 节点数据字典
|
|
473
|
+
"""
|
|
474
|
+
# 还原稳定唯一标识(供画布导出/导入时连线按 id 引用本节点)
|
|
475
|
+
if "id" in data and data["id"]:
|
|
476
|
+
self._node_id = data["id"]
|
|
477
|
+
|
|
478
|
+
# 更新名称
|
|
479
|
+
if "name" in data:
|
|
480
|
+
self.name = data["name"]
|
|
481
|
+
|
|
482
|
+
# 更新形状
|
|
483
|
+
if "shape" in data:
|
|
484
|
+
try:
|
|
485
|
+
self._shape = NodeShape[data["shape"]]
|
|
486
|
+
except KeyError:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
# 还原装饰器类型(semantic 描边色)。直接写字段, 末尾由 _layoutPorts
|
|
490
|
+
# 触发的 update 在下一帧重绘; 装饰器仅影响描边色, 不改几何。
|
|
491
|
+
if "decorator" in data and data["decorator"]:
|
|
492
|
+
self._decorator = data["decorator"]
|
|
493
|
+
|
|
494
|
+
# 更新位置
|
|
495
|
+
if "position" in data:
|
|
496
|
+
pos = data["position"]
|
|
497
|
+
self.setPos(QPointF(pos.get("x", 0), pos.get("y", 0)))
|
|
498
|
+
|
|
499
|
+
# 更新数据
|
|
500
|
+
if "data" in data:
|
|
501
|
+
self._data.update(data["data"])
|
|
502
|
+
|
|
503
|
+
# 加载端口配置(可选)
|
|
504
|
+
if "ports" in data:
|
|
505
|
+
self.ports = data["ports"]
|
|
506
|
+
|
|
507
|
+
# 加载样式
|
|
508
|
+
if "style" in data:
|
|
509
|
+
style = data["style"]
|
|
510
|
+
if "fill_color" in style:
|
|
511
|
+
self._custom_fill_color = QColor(style["fill_color"])
|
|
512
|
+
self._style.idle_fill = self._custom_fill_color
|
|
513
|
+
if "stroke_color" in style:
|
|
514
|
+
self._custom_stroke_color = QColor(style["stroke_color"])
|
|
515
|
+
self._style.idle_stroke = self._custom_stroke_color
|
|
516
|
+
self._style.selected_stroke = self._custom_stroke_color
|
|
517
|
+
if "stroke_width" in style:
|
|
518
|
+
self._custom_stroke_width = style["stroke_width"]
|
|
519
|
+
self._style.stroke_width = self._custom_stroke_width
|
|
520
|
+
if "font_family" in style:
|
|
521
|
+
self._custom_font_family = style["font_family"]
|
|
522
|
+
font = self._text_item.font()
|
|
523
|
+
font.setFamily(self._custom_font_family)
|
|
524
|
+
self._text_item.setFont(font)
|
|
525
|
+
if "font_size" in style:
|
|
526
|
+
self._custom_font_size = style["font_size"]
|
|
527
|
+
font = self._text_item.font()
|
|
528
|
+
font.setPointSize(self._custom_font_size)
|
|
529
|
+
self._text_item.setFont(font)
|
|
530
|
+
if "radius" in style:
|
|
531
|
+
self._custom_radius = style["radius"]
|
|
532
|
+
self._applySize(self._custom_radius, self._custom_radius)
|
|
533
|
+
self._applyStyle()
|
|
534
|
+
# stroke_width<=0 表示"无边框"(对应 setBorder 的 NoPen 分支)。
|
|
535
|
+
# 但 _applyStyle 内部用 setPen(QPen(color, 0)) —— Qt 中宽度 0 的
|
|
536
|
+
# cosmetic pen 仍会画 1px 线, 并不等同 NoPen。故显式补一次 NoPen。
|
|
537
|
+
if self._custom_stroke_width is not None and self._custom_stroke_width <= 0:
|
|
538
|
+
self.setPen(QPen(Qt.PenStyle.NoPen))
|
|
539
|
+
|
|
540
|
+
# 加载 custom 标记配置(可选)
|
|
541
|
+
if "custom" in data:
|
|
542
|
+
custom_data = data["custom"]
|
|
543
|
+
if isinstance(custom_data, dict):
|
|
544
|
+
# 创建并配置 custom 标记
|
|
545
|
+
if self._custom_marker is None:
|
|
546
|
+
self._custom_marker = CustomMarker(self)
|
|
547
|
+
self._custom_marker_animation = QPropertyAnimation(self._custom_marker, b"opacity")
|
|
548
|
+
self._custom_marker_animation.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
549
|
+
|
|
550
|
+
# 解析颜色值
|
|
551
|
+
def parseColor(value):
|
|
552
|
+
if isinstance(value, str):
|
|
553
|
+
return QColor(value)
|
|
554
|
+
return None
|
|
555
|
+
|
|
556
|
+
self._custom_marker.setStyle(
|
|
557
|
+
text=custom_data.get("text"),
|
|
558
|
+
bg_color=parseColor(custom_data.get("bg_color")),
|
|
559
|
+
fg_color=parseColor(custom_data.get("fg_color")),
|
|
560
|
+
border_color=parseColor(custom_data.get("border_color")),
|
|
561
|
+
border_width=custom_data.get("border_width"),
|
|
562
|
+
radius=custom_data.get("radius")
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# 如果标记设置为可见,显示它
|
|
566
|
+
# 注意:这里不自动显示,由调用者通过 setCustomMarkerVisible 控制
|
|
567
|
+
|
|
568
|
+
# 加载外环可见性(可选,缺省保持当前默认值 True)
|
|
569
|
+
if "edge_indicator_visible" in data:
|
|
570
|
+
self._edge_indicator_visible = bool(data["edge_indicator_visible"])
|
|
571
|
+
|
|
572
|
+
# 加载连线能力(可选, 缺省保持默认值 True)
|
|
573
|
+
# 仅改字段, 不调 setConnectable, 避免反序列化中途触发清除连线
|
|
574
|
+
if "connectable" in data:
|
|
575
|
+
self._connectable = bool(data["connectable"])
|
|
576
|
+
|
|
577
|
+
# 加载自指规则(增量合并: 仅覆盖字典中出现的键, 缺失键保持默认)
|
|
578
|
+
if "selfrefs" in data and isinstance(data["selfrefs"], dict):
|
|
579
|
+
for k, v in data["selfrefs"].items():
|
|
580
|
+
if k in self._selfrefs:
|
|
581
|
+
self._selfrefs[k] = bool(v)
|
|
582
|
+
|
|
583
|
+
# 样式(可能改了半径)应用完成后,重新排布端口
|
|
584
|
+
self._layoutPorts()
|
|
585
|
+
|
|
586
|
+
# 标记可见性(放在几何/样式就绪之后, 动画与位置依赖节点已布局完成)。
|
|
587
|
+
# 此前 custom 只还原样式不还原可见性、single 完全未持久化, 导致导入
|
|
588
|
+
# 后用户可见的标记消失。
|
|
589
|
+
if data.get("custom_marker_visible"):
|
|
590
|
+
self.setCustomMarkerVisible(True)
|
|
591
|
+
if data.get("single_marker_visible"):
|
|
592
|
+
self.setSingleMarkerVisible(True)
|
|
593
|
+
|
|
594
|
+
# ------------------------------------------------------------------ #
|
|
595
|
+
# 绘制
|
|
596
|
+
# ------------------------------------------------------------------ #
|
|
597
|
+
def paint(self, painter, option, widget=None) -> None:
|
|
598
|
+
"""绘制节点,根据形状绘制圆形、矩形或菱形"""
|
|
599
|
+
# 获取当前样式
|
|
600
|
+
base_color = self._style.idle_fill
|
|
601
|
+
|
|
602
|
+
# 获取装饰器边框颜色(如果有)
|
|
603
|
+
decorator_color = self._getDecoratorStrokeColor()
|
|
604
|
+
|
|
605
|
+
if self._state == NodeState.IDLE:
|
|
606
|
+
fill_color = base_color
|
|
607
|
+
stroke_color = decorator_color if decorator_color else self._style.idle_stroke
|
|
608
|
+
elif self._state == NodeState.HOVER:
|
|
609
|
+
fill_color = self._blendColors(base_color, self._style.hover_overlay)
|
|
610
|
+
stroke_color = decorator_color if decorator_color else self._style.idle_stroke
|
|
611
|
+
elif self._state == NodeState.SELECTED:
|
|
612
|
+
fill_color = self._blendColors(base_color, self._style.selected_overlay)
|
|
613
|
+
# 选中状态下,如果节点有装饰器,稍微加亮装饰器颜色
|
|
614
|
+
if decorator_color:
|
|
615
|
+
# 混合装饰器颜色和选中颜色
|
|
616
|
+
stroke_color = self._blendColors(decorator_color, self._style.selected_stroke, 0.3)
|
|
617
|
+
else:
|
|
618
|
+
stroke_color = self._style.selected_stroke
|
|
619
|
+
else:
|
|
620
|
+
fill_color = base_color
|
|
621
|
+
stroke_color = decorator_color if decorator_color else self._style.idle_stroke
|
|
622
|
+
|
|
623
|
+
# 连线落点高亮: 覆盖描边为 DodgerBlue(与 port 描边一致), 描边略加粗;
|
|
624
|
+
# 不改变填充(保留 hover/selected 的填充反馈), 不改 _state
|
|
625
|
+
if self._drop_target:
|
|
626
|
+
stroke_color = self._style.selected_stroke
|
|
627
|
+
|
|
628
|
+
# 不合规连接闪烁: 红色描边覆盖(优先级最高), 描边加粗
|
|
629
|
+
if self._flash_error:
|
|
630
|
+
stroke_color = QColor("#E53935")
|
|
631
|
+
|
|
632
|
+
painter.setBrush(QBrush(fill_color))
|
|
633
|
+
if self._flash_error:
|
|
634
|
+
drop_width = self._style.stroke_width + 2.0
|
|
635
|
+
elif self._drop_target:
|
|
636
|
+
drop_width = self._style.stroke_width + 1.0
|
|
637
|
+
else:
|
|
638
|
+
drop_width = self._style.stroke_width
|
|
639
|
+
painter.setPen(QPen(stroke_color, drop_width))
|
|
640
|
+
|
|
641
|
+
rect = self.rect()
|
|
642
|
+
|
|
643
|
+
# 根据形状绘制
|
|
644
|
+
if self._shape == NodeShape.ELLIPSE:
|
|
645
|
+
painter.drawEllipse(rect)
|
|
646
|
+
elif self._shape == NodeShape.RECTANGLE:
|
|
647
|
+
corner_radius = min(rect.width(), rect.height()) * 0.2
|
|
648
|
+
painter.drawRoundedRect(rect, corner_radius, corner_radius)
|
|
649
|
+
else: # DIAMOND
|
|
650
|
+
# 绘制菱形
|
|
651
|
+
center = rect.center()
|
|
652
|
+
path = QPainterPath()
|
|
653
|
+
path.moveTo(center.x(), rect.top())
|
|
654
|
+
path.lineTo(rect.right(), center.y())
|
|
655
|
+
path.lineTo(center.x(), rect.bottom())
|
|
656
|
+
path.lineTo(rect.left(), center.y())
|
|
657
|
+
path.closeSubpath()
|
|
658
|
+
painter.drawPath(path)
|
|
659
|
+
|
|
660
|
+
# 绘制可连线边缘区域的视觉指示(外环)
|
|
661
|
+
if self._edge_indicator_visible:
|
|
662
|
+
self._draw_edge_indicator(painter)
|
|
663
|
+
|
|
664
|
+
# 绘制名称编辑区域的虚线框(浅浅浅灰色)
|
|
665
|
+
self._draw_name_edit_area(painter)
|
|
666
|
+
|
|
667
|
+
def _draw_name_edit_area(self, painter) -> None:
|
|
668
|
+
"""
|
|
669
|
+
绘制名称编辑区域的虚线框
|
|
670
|
+
使用浅浅浅灰色虚线框包围文本区域,提示用户可以点击更名
|
|
671
|
+
"""
|
|
672
|
+
if self._is_editing_name:
|
|
673
|
+
return # 编辑时不显示虚线框
|
|
674
|
+
|
|
675
|
+
# 浅浅浅灰色虚线(带透明度)
|
|
676
|
+
dash_pen = QPen(QColor(220, 220, 220, 120), 1) # 浅浅浅灰色,半透明
|
|
677
|
+
dash_pen.setStyle(Qt.PenStyle.DashLine)
|
|
678
|
+
painter.setPen(dash_pen)
|
|
679
|
+
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
680
|
+
|
|
681
|
+
# 绘制虚线矩形(使用本地坐标)
|
|
682
|
+
painter.drawRect(self._name_edit_area)
|
|
683
|
+
|
|
684
|
+
def _draw_edge_indicator(self, painter) -> None:
|
|
685
|
+
"""
|
|
686
|
+
绘制可连线边缘区域的视觉指示
|
|
687
|
+
在节点边缘显示一个浅灰色的环,指示用户可以从此处拖拽创建连线
|
|
688
|
+
"""
|
|
689
|
+
# 边缘区域的起始比例(与 node_canvas.py 中 _is_on_node_edge 一致)
|
|
690
|
+
inner_ratio = 0.75 # 边缘区域起始位置(半径的75%处)
|
|
691
|
+
|
|
692
|
+
# 非常浅的灰色,半透明
|
|
693
|
+
indicator_color = QColor(200, 200, 200, 60) # 浅灰色,半透明
|
|
694
|
+
indicator_pen = QPen(indicator_color, 1)
|
|
695
|
+
indicator_pen.setStyle(Qt.PenStyle.SolidLine)
|
|
696
|
+
painter.setPen(indicator_pen)
|
|
697
|
+
painter.setBrush(QBrush(indicator_color))
|
|
698
|
+
|
|
699
|
+
rect = self.rect()
|
|
700
|
+
|
|
701
|
+
if self._shape == NodeShape.ELLIPSE:
|
|
702
|
+
# 圆环: 外椭圆减去内椭圆, 半轴取自 rect(与节点本体一致, 永不错位)
|
|
703
|
+
center = rect.center()
|
|
704
|
+
rx = rect.width() / 2.0
|
|
705
|
+
ry = rect.height() / 2.0
|
|
706
|
+
path = QPainterPath()
|
|
707
|
+
path.addEllipse(center, rx, ry)
|
|
708
|
+
path.addEllipse(center, rx * inner_ratio, ry * inner_ratio)
|
|
709
|
+
painter.drawPath(path)
|
|
710
|
+
elif self._shape == NodeShape.RECTANGLE:
|
|
711
|
+
# 矩形:绘制圆角矩形环
|
|
712
|
+
corner_radius = min(rect.width(), rect.height()) * 0.2
|
|
713
|
+
|
|
714
|
+
# 内矩形(不包含边缘环的区域)
|
|
715
|
+
margin = (1 - inner_ratio) * min(rect.width(), rect.height()) / 2
|
|
716
|
+
inner_rect = rect.adjusted(margin, margin, -margin, -margin)
|
|
717
|
+
|
|
718
|
+
# 创建矩形环路径(外矩形减去内矩形)
|
|
719
|
+
path = QPainterPath()
|
|
720
|
+
path.addRoundedRect(rect, corner_radius, corner_radius)
|
|
721
|
+
inner_corner = max(0, corner_radius - margin)
|
|
722
|
+
path.addRoundedRect(inner_rect, inner_corner, inner_corner)
|
|
723
|
+
painter.drawPath(path)
|
|
724
|
+
else: # DIAMOND
|
|
725
|
+
# 菱形:绘制菱形环
|
|
726
|
+
center = rect.center()
|
|
727
|
+
margin = (1 - inner_ratio) * min(rect.width(), rect.height()) / 2
|
|
728
|
+
|
|
729
|
+
# 外菱形
|
|
730
|
+
outer_path = QPainterPath()
|
|
731
|
+
outer_path.moveTo(center.x(), rect.top())
|
|
732
|
+
outer_path.lineTo(rect.right(), center.y())
|
|
733
|
+
outer_path.lineTo(center.x(), rect.bottom())
|
|
734
|
+
outer_path.lineTo(rect.left(), center.y())
|
|
735
|
+
outer_path.closeSubpath()
|
|
736
|
+
|
|
737
|
+
# 内菱形(按比例缩小)
|
|
738
|
+
inner_path = QPainterPath()
|
|
739
|
+
inner_top = center.y() - (center.y() - rect.top()) * inner_ratio
|
|
740
|
+
inner_right = center.x() + (rect.right() - center.x()) * inner_ratio
|
|
741
|
+
inner_bottom = center.y() + (rect.bottom() - center.y()) * inner_ratio
|
|
742
|
+
inner_left = center.x() - (center.x() - rect.left()) * inner_ratio
|
|
743
|
+
|
|
744
|
+
inner_path.moveTo(center.x(), inner_top)
|
|
745
|
+
inner_path.lineTo(inner_right, center.y())
|
|
746
|
+
inner_path.lineTo(center.x(), inner_bottom)
|
|
747
|
+
inner_path.lineTo(inner_left, center.y())
|
|
748
|
+
inner_path.closeSubpath()
|
|
749
|
+
|
|
750
|
+
# 创建菱形环路径
|
|
751
|
+
path = QPainterPath()
|
|
752
|
+
path.addPath(outer_path)
|
|
753
|
+
path.addPath(inner_path)
|
|
754
|
+
painter.drawPath(path)
|