You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:基于wxPython Phoenix实现类似Unreal的拖拽节点连线应用

实现wxPython节点连线与连接功能的核心方案

1. 基础数据结构定义

先通过简单类存储节点和连接的核心信息,方便后续管理:

class Node:
    def __init__(self, x, y, width=100, height=60):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        # 定义节点的输入/输出引脚坐标
        self.input_pin = (x, y + height//2)
        self.output_pin = (x + width, y + height//2)

class Connection:
    def __init__(self, start_node, end_node):
        self.start_node = start_node
        self.end_node = end_node
        self.start = start_node.output_pin
        self.end = end_node.input_pin

2. 自定义节点编辑器面板

继承wx.Panel,重写事件处理函数实现拖拽、连线绘制等核心交互:

import wx

class NodeEditorPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent)
        self.nodes = []
        self.connections = []
        self.temp_connection = None  # 记录拖拽中的临时连线
        self.selected_node = None  # 记录正在拖拽的节点
        self.drag_offset = (0, 0)

        # 绑定事件
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)

        # 添加测试节点
        self.nodes.append(Node(100, 100))
        self.nodes.append(Node(300, 200))

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        dc.SetBackground(wx.Brush(wx.WHITE))
        dc.Clear()

        # 先绘制连线(避免被节点遮挡)
        dc.SetPen(wx.Pen(wx.BLUE, 2))
        for conn in self.connections:
            dc.DrawLine(conn.start[0], conn.start[1], conn.end[0], conn.end[1])
        
        # 绘制拖拽中的临时虚线连线
        if self.temp_connection:
            dc.SetPen(wx.Pen(wx.RED, 2, wx.PENSTYLE_DASH))
            dc.DrawLine(*self.temp_connection)

        # 绘制节点和引脚
        dc.SetPen(wx.Pen(wx.BLACK, 1))
        dc.SetBrush(wx.Brush(wx.LIGHT_GREY))
        for node in self.nodes:
            dc.DrawRectangle(node.x, node.y, node.width, node.height)
            # 绘制输入引脚(绿色)
            dc.SetBrush(wx.Brush(wx.GREEN))
            dc.DrawCircle(node.input_pin[0], node.input_pin[1], 5)
            # 绘制输出引脚(红色)
            dc.SetBrush(wx.Brush(wx.RED))
            dc.DrawCircle(node.output_pin[0], node.output_pin[1], 5)

    def OnLeftDown(self, event):
        pos = event.GetPosition()
        # 检查是否点击输出引脚,启动连线拖拽
        for node in self.nodes:
            if abs(pos.x - node.output_pin[0]) < 5 and abs(pos.y - node.output_pin[1]) < 5:
                self.temp_connection = (node.output_pin[0], node.output_pin[1], pos.x, pos.y)
                return
        
        # 检查是否点击节点,启动节点拖拽
        for node in reversed(self.nodes):
            if node.x <= pos.x <= node.x + node.width and node.y <= pos.y <= node.y + node.height:
                self.selected_node = node
                self.drag_offset = (pos.x - node.x, pos.y - node.y)
                return

    def OnLeftUp(self, event):
        pos = event.GetPosition()
        # 完成连线创建:检查是否连接到输入引脚
        if self.temp_connection:
            start_pin = (self.temp_connection[0], self.temp_connection[1])
            start_node = next((n for n in self.nodes if n.output_pin == start_pin), None)
            for node in self.nodes:
                if abs(pos.x - node.input_pin[0]) < 5 and abs(pos.y - node.input_pin[1]) < 5 and start_node != node:
                    self.connections.append(Connection(start_node, node))
                    self.Refresh()
            self.temp_connection = None
        
        self.selected_node = None

    def OnMouseMotion(self, event):
        if event.Dragging() and event.LeftIsDown():
            pos = event.GetPosition()
            if self.temp_connection:
                # 更新临时连线的终点坐标
                self.temp_connection = (self.temp_connection[0], self.temp_connection[1], pos.x, pos.y)
                self.Refresh()
            elif self.selected_node:
                # 拖拽节点并更新关联连线的坐标
                self.selected_node.x = pos.x - self.drag_offset[0]
                self.selected_node.y = pos.y - self.drag_offset[1]
                self.selected_node.input_pin = (self.selected_node.x, self.selected_node.y + self.selected_node.height//2)
                self.selected_node.output_pin = (self.selected_node.x + self.selected_node.width, self.selected_node.y + self.selected_node.height//2)
                # 更新所有与该节点相关的连线
                for conn in self.connections:
                    if conn.start_node == self.selected_node:
                        conn.start = self.selected_node.output_pin
                    if conn.end_node == self.selected_node:
                        conn.end = self.selected_node.input_pin
                self.Refresh()

# 主窗口
class MainFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, title="Unreal-Style Node Editor", size=(800, 600))
        NodeEditorPanel(self)
        self.Show()

if __name__ == "__main__":
    app = wx.App()
    MainFrame()
    app.MainLoop()

3. 核心功能说明

  • 节点拖拽:点击节点区域可拖动节点,关联连线会自动跟随更新坐标
  • 连线创建:点击红色输出引脚,拖拽到另一节点的绿色输入引脚,松开鼠标即可完成连接
  • 连线预览:拖拽过程中会显示红色虚线预览最终连线效果

4. 扩展方向

  • 给节点添加多组输入/输出引脚,为每个引脚添加唯一标识
  • 实现右键删除连线、节点的功能
  • 区分不同类型节点的样式(颜色、尺寸、引脚数量)
  • 添加菜单/工具栏实现节点的添加、删除操作

内容的提问来源于stack exchange,提问作者Swatcat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 22:21:00