def __init__(self, data, radius=15): self.data = data # Add circular node self.node = QGraphicsEllipseItem(0, 0, 1, 1) # Set radius self.radius = radius # Add text label self.label = QGraphicsTextItem(data.label) font = self.label.font() font.setPointSize(10) self.label.setFont(font) # Add line between label and node self.line1 = QGraphicsLineItem(0, 0, 1, 1) self.line2 = QGraphicsLineItem(0, 0, 1, 1) self.node.setZValue(20) self.label.setZValue(10) self.line1.setZValue(10) self.line2.setZValue(10) self.line1.setPen(get_pen('0.5')) self.line2.setPen(get_pen('0.5')) self.color = '0.8'
def setAngledTexts(self, angledTexts: List[AngledText]) -> None: self._angledTexts = angledTexts self._lineItems.clear() self._textItems.clear() scene: QGraphicsScene = self.scene() for item in self.childItems(): item: QGraphicsItem = item self.removeFromGroup(item) if scene is not None: scene.removeItem(item) for angledText in self._angledTexts: lineItem = QGraphicsLineItem(self.parentItem()) self.addToGroup(lineItem) self._lineItems.append(lineItem) textItem = QGraphicsTextItem(self.parentItem()) textItem.setPlainText(angledText.text) self.addToGroup(textItem) self._textItems.append(textItem)
def __init__(self, port_id: str, text: str = 'port', content: Any = None, port_type='input'): super(CustomPort, self).__init__() self.setAcceptHoverEvents(True) # 接受鼠标悬停事件 self.relative_pos = (0, 0) self.color = COLOR_NORMAL self.id = port_id self.text = text self.size = (10, 10) self.content = content self.connected_lines = [] self.canvas = None self.node: 'Node' = None self.text_item = QGraphicsTextItem(parent=self) self.port_type = port_type if port_type == 'input': self.text_item.setPos(10, -5) else: self.text_item.setPos(-30, -5) self.text_item.setPlainText(self.text)
def __init__(self, canvas: 'PMGraphicsScene', node_id, text: str = '', input_ports: List[CustomPort] = None, output_ports: List[CustomPort] = None, icon_path=r'', look: Dict[str, str] = None): super(Node, self).__init__() assert isinstance(look, dict) self.look = look direction = self.look.get('direction') self.direction = 'E' if direction is None else direction # 输出端口的方向 assert_in(self.direction, ['E', 'W']) self.look['direction'] = self.direction self.id = node_id self.text = text if text != '' else node_id self.base_rect = CustomRect(self) icon_path = os.path.join(get_parent_path(__file__), 'icons', 'logo.png') if icon_path == '' else icon_path self.icon_path = icon_path pix_map = QPixmap(icon_path) self.pix_map_item = QGraphicsPixmapItem(pix_map, parent=self.base_rect) self.pix_map_item.setPos(20, 20) self.text_item = QGraphicsTextItem(parent=self.base_rect) start_left = 50 self.text_item.setPos(start_left, 10) self.text_item.setPlainText(self.text) self.internal_value_text = QGraphicsTextItem(parent=self.base_rect) self.internal_value_text.setPos(start_left, 30) self.internal_value_text.setPlainText('') self.status_text = QGraphicsTextItem(parent=self.base_rect) self.status_text.setPos(start_left, 100) self.status_text.setPlainText('wait') self.input_ports = input_ports self.output_ports = output_ports self.input_ports_dic = {p.id: p for p in input_ports} self.output_ports_dic = {p.id: p for p in output_ports} self.canvas = canvas # self.set_content(content) self.setup()
class DataNode: def __init__(self, data, radius=15): self.data = data # Add circular node self.node = QGraphicsEllipseItem(0, 0, 1, 1) # Set radius self.radius = radius # Add text label self.label = QGraphicsTextItem(data.label) font = self.label.font() font.setPointSize(10) self.label.setFont(font) # Add line between label and node self.line1 = QGraphicsLineItem(0, 0, 1, 1) self.line2 = QGraphicsLineItem(0, 0, 1, 1) self.node.setZValue(20) self.label.setZValue(10) self.line1.setZValue(10) self.line2.setZValue(10) self.line1.setPen(get_pen('0.5')) self.line2.setPen(get_pen('0.5')) self.color = '0.8' @property def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = value self.node.setRect(-value, -value, 2 * value, 2 * value) def contains(self, point): # Check label if self.label.contains(self.label.mapFromScene(point)): return True # Check node if self.node.contains(self.node.mapFromScene(point)): return True return False def update(self): self.node.update() def add_to_scene(self, scene): scene.addItem(self.node) scene.addItem(self.label) scene.addItem(self.line1) scene.addItem(self.line2) def remove_from_scene(self, scene): scene.removeItem(self.node) scene.removeItem(self.label) scene.removeItem(self.line1) scene.removeItem(self.line2) @property def node_position(self): pos = self.node.pos() return pos.x(), pos.y() @node_position.setter def node_position(self, value): self.node.setPos(value[0], value[1]) self.update_lines() @property def label_position(self): pos = self.label.pos() return pos.x(), pos.y() @label_position.setter def label_position(self, value): self.label.setPos(value[0], value[1]) self.update_lines() def update_lines(self): x0, y0 = self.label_position x2, y2 = self.node_position x1 = 0.5 * (x0 + x2) y1 = y0 self.line1.setLine(x0, y0, x1, y1) self.line2.setLine(x1, y1, x2, y2) @property def color(self): return qt_to_mpl_color(self.node.brush().color()) @color.setter def color(self, value): self.node.setBrush(mpl_to_qt_color(value))
class Node(QObject): """ Node是一个节点的抽象 """ def __init__(self, canvas: 'PMGraphicsScene', node_id, text: str = '', input_ports: List[CustomPort] = None, output_ports: List[CustomPort] = None, icon_path=r'', look: Dict[str, str] = None): super(Node, self).__init__() assert isinstance(look, dict) self.look = look direction = self.look.get('direction') self.direction = 'E' if direction is None else direction # 输出端口的方向 assert_in(self.direction, ['E', 'W']) self.look['direction'] = self.direction self.id = node_id self.text = text if text != '' else node_id self.base_rect = CustomRect(self) icon_path = os.path.join(get_parent_path(__file__), 'icons', 'logo.png') if icon_path == '' else icon_path self.icon_path = icon_path pix_map = QPixmap(icon_path) self.pix_map_item = QGraphicsPixmapItem(pix_map, parent=self.base_rect) self.pix_map_item.setPos(20, 20) self.text_item = QGraphicsTextItem(parent=self.base_rect) start_left = 50 self.text_item.setPos(start_left, 10) self.text_item.setPlainText(self.text) self.internal_value_text = QGraphicsTextItem(parent=self.base_rect) self.internal_value_text.setPos(start_left, 30) self.internal_value_text.setPlainText('') self.status_text = QGraphicsTextItem(parent=self.base_rect) self.status_text.setPos(start_left, 100) self.status_text.setPlainText('wait') self.input_ports = input_ports self.output_ports = output_ports self.input_ports_dic = {p.id: p for p in input_ports} self.output_ports_dic = {p.id: p for p in output_ports} self.canvas = canvas # self.set_content(content) self.setup() def display_internal_values(self, val_str: str = ''): self.internal_value_text.setPlainText(val_str) def set_content(self, content: 'FlowContentEditableFunction' = None): self.content: 'FlowContentEditableFunction' = content if content is not None else FlowContentEditableFunction( self, '') self.content.signal_exec_finished.connect(self.on_exec_finished) self.content.signal_exec_started.connect(self.on_exec_started) self.content.signal_error_occurs.connect(self.on_error_occurs) self.display_internal_values(self.content.format_param()) def on_error_occurs(self, error: FlowContentError): flow_widget: PMFlowWidget = self.canvas.flow_widget flow_widget.on_error_occurs(error) def on_exec_started(self, content): """ 执行前进行的操作 :param content: :return: """ self.status_text.setPlainText(content) def on_exec_finished(self, content: str): """ 执行完成后进行的操作 :param content: :return: """ self.status_text.setPlainText(content) # def reset(self): self.status_text.setPlainText('wait') def get_port_index(self, port: CustomPort): """ 获取端口的索引 :param port: :return: """ return self.input_ports.index( port) if port.port_type == 'input' else self.output_ports.index( port) def set_icon(self, icon_path: str): pass def set_pos(self, x: int, y: int): """ 设置位置,左上角角点 :param x: :param y: :return: """ self.base_rect.setPos(x, y) self.refresh_pos() def get_pos(self) -> Tuple[int, int]: """ 获取位置,左上角角点 :return: """ pos = self.base_rect.pos() return pos.x(), pos.y() def refresh_pos(self): """ 刷新位置。当节点被拖动的时候,此方法会被触发。 """ y = self.base_rect.y() dy_input = self.base_rect.boundingRect().height() / ( 1 + len(self.input_ports)) dy_output = self.base_rect.boundingRect().height() / ( 1 + len(self.output_ports)) if self.direction == 'E': x_input = self.base_rect.x() + 5 x_output = self.base_rect.x() + self.base_rect.boundingRect( ).width() - 15 elif self.direction == 'W': x_input = self.base_rect.x() + self.base_rect.boundingRect().width( ) - 15 x_output = self.base_rect.x() + 5 else: raise NotImplementedError for i, p in enumerate(self.input_ports): p.setPos(QPointF(x_input, y + int(dy_input * (1 + i)))) for i, p in enumerate(self.output_ports): p.setPos(QPointF(x_output, y + int(dy_output * (1 + i)))) self.canvas.signal_item_dragged.emit('') def setup(self): self.base_rect.setPos(80, 80) self.canvas.signal_clear_selection.connect( self.base_rect.on_clear_selection) self.canvas.addItem(self.base_rect) for p in self.input_ports + self.output_ports: self.canvas.addItem(p) p.port_clicked.connect(self.on_port_clicked) p.node = self self.refresh_pos() def on_port_clicked(self, port: 'CustomPort'): if self.canvas.drawing_lines: if self.canvas.line_start_port is not port: if not port.port_type == self.canvas.line_start_port.port_type: if self.canvas.allow_multiple_input or ( (not self.canvas.allow_multiple_input) and len(port.connected_lines) == 0): self.canvas.connect_port(port) else: if port.port_type == 'output': self.canvas.drawing_lines = True # if self.canvas.item self.canvas.line_start_point = port.center_pos self.canvas.line_start_port = port def on_delete(self): for port in self.input_ports + self.output_ports: port.canvas = self.canvas port.on_delete() self.canvas.removeItem(self.base_rect) self.canvas.nodes.remove(self) self.deleteLater() def __repr__(self): s = super(Node, self).__repr__() return s + repr(self.input_ports) + repr(self.output_ports) def get_port(self, port_id: str) -> 'CustomPort': for port in self.input_ports + self.output_ports: if port_id == port.id: return port return None def add_port(self, port: CustomPort): """ 添加一个端口 :param port: :return: """ port_type = port.port_type if port_type == 'input': self.input_ports.append(port) self.input_ports_dic[port.id] = port elif port_type == 'output': self.output_ports.append(port) self.output_ports_dic[port.id] = port else: raise ValueError('port type invalid') if port.node is None: port.node = self self.refresh_pos() self.canvas.addItem(port) port.port_clicked.connect(self.on_port_clicked) return port def remove_port(self, port: CustomPort): """ 删除一个端口 :param port: :return: """ port_type = port.port_type if port_type == 'input': self.input_ports.remove(port) self.input_ports_dic.pop(port.id) elif port_type == 'output': self.output_ports.remove(port) self.output_ports_dic.pop(port.id) else: raise ValueError('port type invalid') port.on_delete() self.refresh_pos() def change_property(self, property: Dict[str, Union[int, str]]): """ 改变各个端口的文字、端口的数目以及文字。 :param property: :return: """ self.text = property['text'] self.text_item.setPlainText(self.text) self.content.update_settings(property) self.valid_port_ids = [] if property.get('inputs') is not None: for input_id, input_text in zip(property['inputs'][0], property['inputs'][1]): self.valid_port_ids.append(input_id) p = self.get_port(input_id) if p is None: p = self.add_port( CustomPort(port_id=input_id, text=input_text, port_type='input')) p.set_text(input_text) for p in self.input_ports: if p.id not in self.valid_port_ids: self.remove_port(p) if property.get('outputs') is not None: for output_id, output_text in zip(property['outputs'][0], property['outputs'][1]): self.valid_port_ids.append(output_id) p = self.get_port(output_id) if p is None: p = self.add_port( CustomPort(port_id=output_id, text=output_text, port_type='output')) p.set_text(output_text) for p in self.output_ports: if p.id not in self.valid_port_ids: self.remove_port(p) def change_ports_property(self, property: Dict[str, Union[int, str]]): """ 改变各个端口的文字、端口的数目以及文字。 :param property: :return: """ self.text = property['text'] self.text_item.setPlainText(self.text) self.valid_port_ids = [] if property.get('inputs') is not None: for input_id, input_text in zip(property['inputs'][0], property['inputs'][1]): self.valid_port_ids.append(input_id) p = self.get_port(input_id) if p is None: p = self.add_port( CustomPort(port_id=input_id, text=input_text, port_type='input')) p.set_text(input_text) for p in self.input_ports: if p.id not in self.valid_port_ids: self.remove_port(p) if property.get('outputs') is not None: for output_id, output_text in zip(property['outputs'][0], property['outputs'][1]): self.valid_port_ids.append(output_id) p = self.get_port(output_id) if p is None: p = self.add_port( CustomPort(port_id=output_id, text=output_text, port_type='output')) p.set_text(output_text) for p in self.output_ports: if p.id not in self.valid_port_ids: self.remove_port(p) def on_edit_ports(self): from pmgwidgets import PMGPanel from qtpy.QtWidgets import QDialog, QVBoxLayout dlg = QDialog(self.base_rect.scene().flow_widget) sp = PMGPanel() p: 'CustomPort' = None input_ids, output_ids = [], [] input_texts, output_texts = [], [] self._last_var = 0 def new_id_input(): node_id = self.id max_val = 0 for p in self.input_ports: n, t, p = p.parse_id() if max_val < int(p): max_val = int(p) self._last_var += 1 return '%s:input:%d' % (node_id, max_val + self._last_var) def new_id_output(): node_id = self.id max_val = 0 for p in self.output_ports: n, t, p = p.parse_id() if max_val < int(p): max_val = int(p) self._last_var += 1 return '%s:output:%d' % (node_id, max_val + self._last_var) for p in self.input_ports: input_ids.append(p.id) input_texts.append(p.text) for p in self.output_ports: output_ids.append(p.id) output_texts.append(p.text) views = [] # views += self.content.get_settings_params() views += [ ('line_ctrl', 'text', 'Node Text', self.text), ] if self.content.ports_changable[0]: views.append(('list_ctrl', 'inputs', 'Set Inputs', [input_ids, input_texts], new_id_input)) if self.content.ports_changable[1]: views.append(('list_ctrl', 'outputs', 'Set Outputs', [output_ids, output_texts], new_id_output)) sp.set_items(views) dlg.setLayout(QVBoxLayout()) dlg.layout().addWidget(sp) dlg.exec_() dic = sp.get_value() self.change_ports_property(dic) def on_edit_properties_requested(self): """ Show Settings Panel. If It was customized node,it will call the function of the content. Or the default settings parameters will be got. Returns: """ from pmgwidgets import PMGPanel from qtpy.QtWidgets import QDialog, QVBoxLayout if isinstance(self.content, FlowContentForFunction): dlg = QDialog(self.base_rect.scene().flow_widget) sp = PMGPanel() p: 'CustomPort' = None input_ids, output_ids = [], [] input_texts, output_texts = [], [] self._last_var = 0 def new_id_input(): node_id = self.id max_val = 0 for p in self.input_ports: n, t, p = p.parse_id() if max_val < int(p): max_val = int(p) self._last_var += 1 return '%s:input:%d' % (node_id, max_val + self._last_var) def new_id_output(): node_id = self.id max_val = 0 for p in self.output_ports: n, t, p = p.parse_id() if max_val < int(p): max_val = int(p) self._last_var += 1 return '%s:output:%d' % (node_id, max_val + self._last_var) for p in self.input_ports: input_ids.append(p.id) input_texts.append(p.text) for p in self.output_ports: output_ids.append(p.id) output_texts.append(p.text) views = [] views += self.content.get_settings_params() views += [ ('line_ctrl', 'text', 'Node Text', self.text), ] if self.content.ports_changable[0]: views.append(('list_ctrl', 'inputs', 'Set Inputs', [input_ids, input_texts], new_id_input)) if self.content.ports_changable[1]: views.append(('list_ctrl', 'outputs', 'Set Outputs', [output_ids, output_texts], new_id_output)) sp.set_items(views) dlg.setLayout(QVBoxLayout()) dlg.layout().addWidget(sp) dlg.exec_() dic = sp.get_value() self.change_property(dic) else: try: self.content.settings_window_requested(self.canvas.flow_widget) except Exception as e: import traceback exc = traceback.format_exc() print(exc) QMessageBox.warning(self.canvas.flow_widget, 'Error', str(e)) def invert(self): self.direction = 'W' if self.direction == 'E' else 'E' self.refresh_pos() self.look['direction'] = self.direction
def select_next(self, source: QtWidgets.QGraphicsTextItem): text_item: pg.TextItem = source.parentItem() index = self.names.index(text_item) new_index = (index + 1) % len(self.names) source.clearFocus() self.focusNew(new_index)
class CustomPort(QGraphicsObject): port_clicked = Signal(QGraphicsObject) def __init__(self, port_id: str, text: str = 'port', content: Any = None, port_type='input'): super(CustomPort, self).__init__() self.setAcceptHoverEvents(True) # 接受鼠标悬停事件 self.relative_pos = (0, 0) self.color = COLOR_NORMAL self.id = port_id self.text = text self.size = (10, 10) self.content = content self.connected_lines = [] self.canvas = None self.node: 'Node' = None self.text_item = QGraphicsTextItem(parent=self) self.port_type = port_type if port_type == 'input': self.text_item.setPos(10, -5) else: self.text_item.setPos(-30, -5) self.text_item.setPlainText(self.text) def set_text(self, text: str): self.text_item.setPlainText(text) self.text = text def boundingRect(self): return QRectF(0, 0, self.size[0], self.size[1]) @property def center_pos(self): return QPointF(self.x() + self.size[0] / 2, self.y() + self.size[1] / 2) def paint(self, painter, styles, widget=None): pen1 = QPen(Qt.SolidLine) pen1.setColor(QColor(128, 128, 128)) painter.setPen(pen1) brush1 = QBrush(Qt.SolidPattern) brush1.setColor(self.color) painter.setBrush(brush1) painter.setRenderHint(QPainter.Antialiasing) # 反锯齿 painter.drawRoundedRect(self.boundingRect(), 10, 10) def hoverEnterEvent(self, event: 'QGraphicsSceneHoverEvent') -> None: self.color = COLOR_HOVER_PORT self.update() def hoverLeaveEvent(self, event: 'QGraphicsSceneHoverEvent') -> None: self.color = COLOR_NORMAL self.update() def mousePressEvent(self, evt: QGraphicsSceneMouseEvent): if evt.button() == Qt.LeftButton: pos = (evt.scenePos().x(), evt.scenePos().y()) self.relative_pos = (pos[0] - self.x(), pos[1] - self.y()) self.port_clicked.emit(self) elif evt.button() == Qt.RightButton: pass elif evt.button() == Qt.MidButton: pass def paintEvent(self, paint_event): pen1 = QPen() pen1.setColor(QColor(166, 66, 250)) painter = QPainter(self) painter.setPen(pen1) painter.begin(self) painter.drawRoundedRect(self.boundingRect(), 10, 10) # 绘制函数 painter.end() def get_pos(self) -> Tuple[int, int]: pos = self.pos() return pos.x(), pos.y() def get_connected_lines(self) -> List['CustomLine']: if len(self.connected_lines) == 0: return [] else: return self.connected_lines def get_connected_port(self) -> List['CustomPort']: """ 获取连接的节点 :return: """ lines = self.get_connected_lines() if len(lines) == 0: return [] port_list = [] for l in lines: port_list.append(l.get_opposite_port(self)) return port_list def on_delete(self): for line in self.connected_lines: line.on_delete() self.scene().removeItem(self) def parse_id(self) -> Tuple[str, str, str]: """ 解析id。id由三个元素构成,由冒号分割。 如3:input:2,意思就是id3为3的节点中,id为2的输入端口。注意 :return: """ node_id, type, port_id = self.id.split(':') return node_id, type, port_id def __repr__(self): return super(CustomPort, self).__repr__() + 'id = ' + str(self.id)
def _to_qgraphicstextitem(self): t = QGraphicsTextItem() t.setDefaultTextColor(QColor(self.color.hexcolor)) if self.html: text = self.text.replace(u'\n', u'<br />') t.setHtml(u'<div align="center">%s</div>' % text if self.center else text) else: t.setPlainText(self.text) mw = self.max_width if mw is None: if self.uniform_coordinates: mw = self._canvas.width // 2 - self.x else: mw = self._canvas.width - self.x if self.center: mw *= 2 t.setTextWidth(mw) f = QFont(self.font_family, weight=QFont.Bold if self.font_bold else QFont.Normal, italic=self.font_italic) for family, substitute in font_substitutions: f.insertSubstitution(substitute, family) f.setPixelSize(self.font_size) t.setFont(f) return t