Permet la mise en cascade de calculs
Avant-propos
Bon coding et bon courage !
Le dessin d'un node, confié à la méthode UiNode.paint()
, est mal fait :
Le problème est corrigé, en mettant 3 calques au lieu de 2.
UiNode.paint()
:
def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
""" Le dessin du node est une superposition de plusieurs calques. """
""" Calque 0 (au fond) : Contenu sans bordure. """
painter.setPen(Qt.NoPen)
painter.setBrush(self.state_brush)
painter.drawPath(self.path_outline.simplified())
""" Calque 1 : Titre sans bordure. """
painter.setBrush(pg.mkBrush(color='#bbbbbbaa'))
painter.drawPath(self.path_title.simplified())
""" Calque 2 (au dessus) : Bordure totale seule, sans couleur de fond. """
painter.setPen(self.state_pen)
painter.setBrush(Qt.NoBrush)
painter.drawPath(self.path_outline.simplified())
type
à CtrlNode
.CtrlNode.__init__()
, ajouter cette ligne :
self.type = ''
Cet attribut doit être surchargé par toutes les classes dérivées.
UiNode
:
type_item
.UiNode.__init__()
, ajouter cette ligne :
self.type_item = QGraphicsTextItem(self)
UiNode.init_ui()
:
def init_ui(self):
""" Paramètres. """
h_title, n_round = self.o_node.d_display['geometry']['h_title'], self.o_node.d_display['geometry']['round']
""" Position. """
self.setPos(*self.o_node.pos)
""" Fond du titre. """
geometry = (0, 0, self.o_node.width, h_title, n_round, n_round)
self.path_title.setFillRule(Qt.WindingFill)
self.path_title.addRoundedRect(*geometry)
self.path_title.addRect(0, h_title - n_round, self.o_node.width, n_round)
# |_ Enlève les arrondis du bas du titre, pour éviter un effet disgracieux (Commenter cette ligne pour le voir)
""" Texte du titre. """
self.title_item.setFont(QFont('Arial', 9)) # Police et taille
y = 0 if self.o_node.type == '' else -3
self.title_item.setPos(0, y)
self.set_title()
""" Texte du type. """
self.type_item.setFont(QFont('Arial', 6, italic=True)) # Police et taille
self.type_item.setDefaultTextColor(Qt.white)
self.type_item.setPlainText(self.o_node.type)
x = self.o_node.width - self.type_item.boundingRect().width()
self.type_item.setPos(x, 10)
""" Dessin du node : rectangle aux coins arrondis. """
self.path_outline.addRoundedRect(0, 0, self.o_node.width, self.o_node.height, n_round, n_round)
""" On / Off """
QGraphicsProxyWidget(self).setWidget(self.on_off)
self.on_off.move(self.o_node.width-10, -4)
self.on_off.setChecked(self.o_node.b_chk)
""" Flags. """
self.setFlag(QGraphicsItem.ItemIsMovable) # Peut être déplacé.
self.setFlag(QGraphicsItem.ItemIsSelectable) # Peut être sélectionné.
self.setAcceptHoverEvents(True) # Accepte le survol.
self.setToolTip(f"{self.o_node.get_param('Titre du node', 'Le titre')}\n{self.o_node.pos}")
""" Événements. """
self.on_off.stateChanged.connect(self.o_node.set_checked)
CtrlNode
, déclarer et affecter self.type
.
signaux
: dans Node.__init__()
→ self.type = 'Signaux'
Description
CtrlScene.no_compile()
CtrlScene.debug()
:
def debug(self):
self.o_pkl.od_pkl.clear()
self.o_pkl.od_pkl.write(['Node90', 'path'], 'nodes.indicateurs.macd')
self.o_pkl.od_pkl.write(['Node90', 'position'], (-144, -32))
self.o_pkl.od_pkl.write(['Node91', 'path'], 'nodes.operateurs.union')
self.o_pkl.od_pkl.write(['Node91', 'position'], (48, -96))
self.o_pkl.od_pkl.write('edges', {((90, 0), (91, 4)), ((90, 2), (91, 2))})
setup()
.CtrlScene.setup()
devient :
def setup(self):
""" Paramètres. """
self.o_pkl = Pkl(self)
self.o_params = Parameters(self)
self.friendly_name = self.o_params.od_params.read([self.main_key, "Nom de l'onglet"], 'Choisir un nom')
# self.o_params = Pkl(graph_name=self.graph_name)
""" Création de la scène. """
self.o_grscene = UiScene(self) # pyqtgraph.GraphicsScene.GraphicsScene dérivée.
""" Création de l'unique vue. """
self.o_grview = UiView(self) # pyqtgraph.GraphicsView dérivée.
""" Affectaion de la scène à visionner. """
self.o_grview.setScene(self.o_grscene) # Unique vue de la scène (il pourrait y en avoir plusieurs).
""" 'Boîte' container verticale - elle pourrait aussi être horizontale car elle ne contient qu'un élément. """
box = QVBoxLayout() # Son but est d'étirer la vue sur toute sa surface.
""" On place la vue dans cette boîte. """
box.addWidget(self.o_grview)
""" Suppression des marges (10px par défaut). """
box.setContentsMargins(0, 0, 0, 0)
""" Enfin, on place cette boîte dans l'onglet. """
self.setLayout(box)
""" DEBUG. """
if DEBUG:
self.debug()
""" Affichage des nodes mémorisés dans les paramètres. """
self.show_nodes()
""" Affichage des edges mémorisés dans les paramètres. """
# self.show_edges()
""" MAP ***** MAP ***** MAP ***** MAP ***** MAP ***** MAP ***** """
self.o_pkl.od_pkl.print()
""" MAP ***** MAP ***** MAP ***** MAP ***** MAP ***** MAP ***** """
Remarquez l'appel self.show_eges()
, provisoirement commenté.
DEBUG = True
od_pkl
s'affiche.
edges
qui décrit deux edges dans un set :Node90: ---------------------------------------------------- <class 'dict'> path: nodes.indicateurs.macd --------------------------- <class 'str'> position: (-144, -32) ---------------------------------- <class 'tuple'> Node91: ---------------------------------------------------- <class 'dict'> path: nodes.operateurs.union --------------------------- <class 'str'> position: (48, -96) ------------------------------------ <class 'tuple'> edges: {((90, 0), (91, 4)), ((90, 2), (91, 2))} ------------ <class 'set'>
main
distribue les scènes aux onglets mémorisés.pkl
pour :
Comme pour les nodes et les sockets, les edges nécessitent 2 classes : CtrlEdge
et UiEdge
, respectivement dans les fichiers ctrl_edge.py
et ui_edge.py
.
o_edge
que d'éléments dans la clé edges
du pkl
(ici, 2).o_edge
) est associé à son ui (objet o_gredge
), chargé de l'afficher à l'écran.CtrlScene.setup()
vers la fin :
pkl
(ne pas le supprimer).self.show_edges()
./pc/ui_edge.py
:
from PyQt5.QtWidgets import QGraphicsPathItem
from PyQt5.QtGui import QPainterPath, QPen, QColor
class UiEdge(QGraphicsPathItem):
def __init__(self, o_edge):
super().__init__()
self.o_edge = o_edge
def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
t_ends = self.o_edge.end_points
point_from, point_to = t_ends[0], t_ends[1]
path = QPainterPath(point_from) # Point de départ.
path.lineTo(point_to) # Point d'arrivée.
self.setPath(path)
painter.setPen(QPen(QColor('#aaf'), 3))
painter.drawPath(self.path())
/pc/ctrl_edge.py
:
from pc.ui_edge import UiEdge
class CtrlEdge:
def __init__(self, o_scene):
"""
:param o_scene: objet CtrlScene
"""
self.o_scene = o_scene
self.o_gredge = None
self.end_points = self.two_points() # A supprimer
self.setup()
def setup(self):
self.o_gredge = UiEdge(self) # Dessin de l'edge dans l'UI.
self.o_scene.o_grscene.addItem(self.o_gredge) # Ajout de l'edge dans la scène.
@staticmethod
def two_points():
""" Code provisoire. Retourne des valeurs aléatoires. """
from PyQt5.QtCore import QPointF
import random
p_from = random.randint(-100, 100), random.randint(-100, 100)
p_to = random.randint(-100, 100), random.randint(-100, 100)
return QPointF(*p_from), QPointF(*p_to)
Créer la méthode sommaire show_edges()
.
CtrlScene.show_edges()
:
def show_edges(self):
""" Code provisoire permettant la mise au point. """
edges = self.o_pkl.od_pkl.read('edges', {}) # Lecture de la clé 'edges' du pkl.
for _ in edges:
CtrlEdge(self)
(Décommenter son appel dans le setup et importer CtrlEdge
.)
A chaque lancement les segments sont différents.
UiEdge.paint()
, les coordonnées des extrémités sont récupérées dès la première ligne :
t_ends = self.o_edge.end_points
CtrlNode
:""" Sockets. """
de CtrlNode.setup()
par celle-ci :
""" Sockets. """
for i, d_input in enumerate(self.ld_inputs):
""" On complète les dictionnaires de base. """
if isinstance(d_input, dict): # Si ce n'est pas un dictionnaire, c'est un séparateur.
d_input['id'] = True, self.id, i # On ajoute l'identifiant. Tuple (in ?, id_node, num_socket)
self.lo_sockets_in.append(CtrlSocket(self, d_input)) # Socket instancié et ajouté à la liste.
else:
""" Permet d'éviter les erreurs d'indice. """
self.lo_sockets_in.append('sep') # Séparateur.
for i, d_output in enumerate(self.ld_outputs):
""" On complète les dictionnaires de base. """
if isinstance(d_output, dict): # Si ce n'est pas un dictionnaire, c'est un séparateur.
d_output['id'] = False, self.id, i # On ajoute l'identifiant. Tuple (in ?, id_node, num_socket)
self.lo_sockets_out.append(CtrlSocket(self, d_output)) # Socket instancié et ajouté à la liste.
else:
self.lo_sockets_out.append('sep') # Séparateur.
CtrlScene
:show_edges()
pkl
...
o_node_from
).o_socket_out
)o_node_to
).o_socket_in
)o_socket_in
, o_socket_out
).CtrlScene.show_edges()
:
def show_edges(self):
""" - La clé 'edges' contient un set de descriptions d'edges (un set évite les doublons).
- Pour chaque description, on vérifie sa validité.
- Si elle est valide, on instancie l'edge, sinon on l'ignore.
- L'instanciation prend en charge l'affichage dans la scène.
(node_from, socket_out) from |------------ edge ---------------| to (node_to, socket_in)
"""
def s_id2o_node(s_id):
""" Recheche le node ayant pour clé-pkl la valeur s_id.
:param s_id: Nom de la grappe dans le pkl.
:return: objet CtrlNode ou None si non trouvé.
"""
for o_grnode in l_grnodes:
if o_grnode.s_id == s_id:
return o_grnode.o_node
return None # Non trouvé.
edges = self.o_pkl.od_pkl.read('edges', {}) # Lecture dans le pkl.
l_grnodes = self.get_grnodes() # Liste de tous les nodes présents dans la scène.
for edge in edges:
""" La description de l'edge est-elle valide ? 6 vérifications. """
""" 1) Node from. """
s_id_node = f'Node{edge[0][0]}'
o_node_from = s_id2o_node(s_id_node)
if o_node_from is None:
continue
""" 2) Socket_out du node_from. """
id_socket = edge[0][1]
if not (0 <= id_socket < len(o_node_from.ld_outputs)):
continue
""" 3) Élément de la liste : socket ou séparateur ? """
if not isinstance(o_node_from.ld_outputs[id_socket], dict):
continue # Séparateur.
o_socket_out = o_node_from.lo_sockets_out[id_socket]
""" 4) Node to. """
s_id_node = f'Node{edge[1][0]}'
o_node_to = s_id2o_node(s_id_node)
if o_node_to is None:
continue
""" 5) Socket_in du node_to. """
id_socket = edge[1][1]
if not (0 <= id_socket < len(o_node_to.ld_inputs)):
continue
""" 6) Élément de la liste : socket ou séparateur ? """
if not isinstance(o_node_to.ld_inputs[id_socket], dict):
continue # Séparateur.
o_socket_in = o_node_to.lo_sockets_in[id_socket]
""" 7) Ici tout est ok : les 2 nodes existent, les sockets aussi et ce ne sont pas des séparateurs. """
CtrlEdge(self, edge, o_socket_out, o_socket_in)
CtrlEdge
:CtrlScene.show_edges()
nous montre que la méthode CtrlEdge.__init__()
attend 4 paramètres.CtrlEdge.__init__()
devient :
def __init__(self, o_scene, t_id, o_socket_out, o_socket_in): # , t_from, t_to, o_node_from, o_node_to):
"""
:param o_scene: objet CtrlScene
:param t_id: Tuple de 2 tuples. Exemple : (90, 0), (91, 4)
:param o_socket_out: objet CtrlSocket
:param o_socket_in: objet CtrlSocket
"""
self.o_scene = o_scene
self.o_gredge = None
self.t_id = t_id
self.o_socket_out = o_socket_out
self.o_socket_in = o_socket_in
self.o_node_from = self.o_socket_out.o_node
self.o_node_to = self.o_socket_in.o_node
self.setup()
two_points()
.end_points
.
CtrlEdge.end_points()
:
@property
def end_points(self):
""" - Renvoie un tuple : les points de départ et d'arrivée.
- Les coordonnées d'un socket sont relatives à celles de son node.
- Il faut donc les additionner pour obtenir les coordonnées absolues."""
# à coder ...
En déplaçant les nodes, les edges restent accrochés aux sockets.
z-index
négatif.UiEdge
, modifier __init__()
pour appeler init_ui()
.UiEdge.__init__()
:
def __init__(self, o_edge):
super().__init__()
self.o_edge = o_edge
self.b_hover = False # Flag de survol.
self.init_ui()
init_ui()
.UiEdge.init_ui()
:
def init_ui(self):
self.setZValue(-1) # Sous le socket.
init_ui()
devient.UiEdge.init_ui()
:
def init_ui(self):
self.setZValue(-1) # Sous le socket.
self.setToolTip(f'{self.o_edge.t_id[0]} → {self.o_edge.t_id[1]}')
""" Flags. """
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsPathItem.ItemIsSelectable)
state_pen
et les méthodes appelées par les événements de survol.UiEdge
:
@property
def state_pen(self):
""" Choix de la couleur en fonction d'événements. """
color = '#faa' if self.isSelected() else '#aaf'
color = '#ffa' if self.b_hover else color
return QPen(QColor(color), 3)
def hoverEnterEvent(self, event):
""" Entrée en survol du edge. """
self.b_hover = True
self.update() # Rafraîchit l'affichage
def hoverLeaveEvent(self, event):
""" Sortie du survol. """
self.b_hover = False
self.update()
paint()
le crayon codé 'en dur' par le state_pen
.UiEdge.paint()
:
painter.setPen(self.state_pen)
Adaptez les couleurs et l'épaisseur à votre convenance.
Le dessin de la courbe de Bézier nécessite les coordonnées x et y de 4 points (P0 à P3).
lineTo()
) doit être remplacé par celui d'une courbe de Bézier (cubicTo()
).paint()
devient.UiEdge.paint()
:
def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
""" Extrémités : P0 et P3. """
t_ends = self.o_edge.end_points # QPointF(), QPointF()
point_from, point_to = t_ends[0], t_ends[1] # Point de départ (P0), Point d'arrivée (P3)
path = QPainterPath(point_from)
# path.lineTo(point_to)
# La ligne précédente est remplacée par les suivantes :
# à coder ...
self.setPath(path)
painter.setPen(self.state_pen)
painter.drawPath(self.path())
Bon coding et bon courage !
Snippets
Bonjour les codeurs !