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

Kivy节点生成器报错:'FloatLayout'对象无add_node属性

问题:Kivy节点生成器应用报错AttributeError: 'FloatLayout' object has no attribute 'add_node'

错误信息

/USER/Documents/python/test/Nueva carpeta (2)/connection_nodes_app.py", line 167, in <module> ConnectionNodesApp().run() File "C:\Users\USER\anaconda3\envs\kivy\lib\site-packages\kivy\app.py", line 955, in run self._run_prepare() File "C:\Users\USER\anaconda3\envs\kivy\lib\site-packages\kivy\app.py", line 925, in _run_prepare root = self.build() File "c:/Users/USER/Documents/python/test/Nueva carpeta (2)/connection_nodes_app.py", line 158, in build network_canvas.add_node(text="Start Node") AttributeError: 'FloatLayout' object has no attribute 'add_node'

用户代码

# connection_nodes_app.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Line, Color
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup
from kivy.lang import Builder

Builder.load_string(
    """
<Node>:
    size_hint: None, None
    size: 100, 60
    BoxLayout:
        orientation: 'horizontal'
        size: root.size
        canvas.before:
            Color:
                rgba: 1, 1, 1, 0.8
            Rectangle:
                size: self.size
                pos: self.pos
        Label:
            id: label
            text: root.text
            size_hint_x: 0.7
        Button:
            text: "Edit"
            size_hint_x: 0.3
            on_release: root.edit_node()

<Network>:
    BoxLayout:
        orientation: 'vertical'
        spacing: 10
        padding: 10
        Button:
            text: "Add Node"
            size_hint_y: None
            height: 50
            on_release: app.add_node()
        ScrollView:
            NetworkCanvas:
                id: network_layout
                FloatLayout:
                    size_hint: None, None
                    size: 2000, 2000
"""
)


class Node(Widget):
    def __init__(self, text="Node", **kwargs):
        super().__init__(**kwargs)
        self.text = text
        self.connections = []
        self._dragging = False

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self._dragging = True
            touch.grab(self)
            return True
        return super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if touch.grab_current is self and self._dragging:
            self.center_x += touch.dx
            self.center_y += touch.dy
            for conn in self.connections:
                conn.update()
            return True
        return super().on_touch_move(touch)

    def on_touch_up(self, touch):
        if touch.grab_current is self and self._dragging:
            self._dragging = False
            touch.ungrab(self)
            return True
        return super().on_touch_up(touch)

    def edit_node(self):
        popup = Popup(title="Edit Node")
        layout = BoxLayout(orientation="vertical", spacing=10, padding=10)
        text_input = TextInput(text=self.ids.label.text)
        save_button = Button(text="Save", size_hint_y=None, height=40)

        def save_text(instance):
            self.ids.label.text = text_input.text
            popup.dismiss()

        save_button.bind(on_release=save_text)
        layout.add_widget(text_input)
        layout.add_widget(save_button)
        popup.content = layout
        popup.size_hint = (0.5, 0.5)
        popup.open()


class ConnectionLine:
    def __init__(self, start_node, end_node, canvas):
        self.start_node = start_node
        self.end_node = end_node
        self.canvas = canvas
        with self.canvas:
            Color(1, 0, 0, 1)
            self.line = Line(
                points=[*self.start_node.center, *self.end_node.center], width=2
            )
        self.start_node.connections.append(self)
        self.end_node.connections.append(self)

    def update(self):
        self.line.points = [*self.start_node.center, *self.end_node.center]


class NetworkCanvas(FloatLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.selected_node = None
        self.nodes = []

    def add_node(self, text="New Node", pos=(100, 100)):
        node = Node(text=text, pos=pos)
        self.nodes.append(node)
        self.add_widget(node)

    def connect_nodes(self, node1, node2):
        ConnectionLine(node1, node2, self.canvas)

    def on_touch_down(self, touch):
        if super().on_touch_down(touch) is False:
            if touch.button == "right":
                for node in self.nodes:
                    if node.collide_point(*touch.pos):
                        if not self.selected_node:
                            self.selected_node = node
                        else:
                            self.connect_nodes(self.selected_node, node)
                            self.selected_node = None
                        return True
                self.selected_node = None
        return True


class Network(FloatLayout):
    pass


class ConnectionNodesApp(App):
    def build(self):
        layout = Network()
        network_canvas = layout.ids.network_layout.children[0]
        network_canvas.add_node(text="Start Node")
        return layout

    def add_node(self):
        network_canvas = self.root.ids.network_layout.children[0]
        network_canvas.add_node(text="New Node")


if __name__ == "__main__":
    ConnectionNodesApp().run()

问题分析与修复方案

错误原因

你在build和add_node方法中,错误获取了network_layout的第一个子控件(即KV定义里那个size:2000,2000的FloatLayout),而非NetworkCanvas实例本身。FloatLayout没有你定义的add_node方法,因此抛出AttributeError。同时,KV中NetworkCanvas直接嵌套的FloatLayout会覆盖自身布局空间,导致节点无法正确显示在滚动区域内。

修复步骤

  1. 修正KV结构:给NetworkCanvas内部的FloatLayout添加id,方便后续引用
  2. 调整节点添加逻辑:将节点添加到内部FloatLayout中,而非NetworkCanvas自身
  3. 修正实例获取方式:直接使用layout.ids.network_layout获取NetworkCanvas实例
  4. 调整连线绘制图层:使用内部FloatLayout的canvas绘制连线,确保显示正确

修复后的完整代码

# connection_nodes_app.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Line, Color
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup
from kivy.lang import Builder

Builder.load_string(
    """
<Node>:
    size_hint: None, None
    size: 100, 60
    BoxLayout:
        orientation: 'horizontal'
        size: root.size
        canvas.before:
            Color:
                rgba: 1, 1, 1, 0.8
            Rectangle:
                size: self.size
                pos: self.pos
        Label:
            id: label
            text: root.text
            size_hint_x: 0.7
        Button:
            text: "Edit"
            size_hint_x: 0.3
            on_release: root.edit_node()

<Network>:
    BoxLayout:
        orientation: 'vertical'
        spacing: 10
        padding: 10
        Button:
            text: "Add Node"
            size_hint_y: None
            height: 50
            on_release: app.add_node()
        ScrollView:
            NetworkCanvas:
                id: network_layout
                FloatLayout:
                    id: canvas_container
                    size_hint: None, None
                    size: 2000, 2000
"""
)


class Node(Widget):
    def __init__(self, text="Node", **kwargs):
        super().__init__(**kwargs)
        self.text = text
        self.connections = []
        self._dragging = False

    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self._dragging = True
            touch.grab(self)
            return True
        return super().on_touch_down(touch)

    def on_touch_move(self, touch):
        if touch.grab_current is self and self._dragging:
            self.center_x += touch.dx
            self.center_y += touch.dy
            for conn in self.connections:
                conn.update()
            return True
        return super().on_touch_move(touch)

    def on_touch_up(self, touch):
        if touch.grab_current is self and self._dragging:
            self._dragging = False
            touch.ungrab(self)
            return True
        return super().on_touch_up(touch)

    def edit_node(self):
        popup = Popup(title="Edit Node")
        layout = BoxLayout(orientation="vertical", spacing=10, padding=10)
        text_input = TextInput(text=self.ids.label.text)
        save_button = Button(text="Save", size_hint_y=None, height=40)

        def save_text(instance):
            self.ids.label.text = text_input.text
            popup.dismiss()

        save_button.bind(on_release=save_text)
        layout.add_widget(text_input)
        layout.add_widget(save_button)
        popup.content = layout
        popup.size_hint = (0.5, 0.5)
        popup.open()


class ConnectionLine:
    def __init__(self, start_node, end_node, canvas):
        self.start_node = start_node
        self.end_node = end_node
        self.canvas = canvas
        with self.canvas:
            Color(1, 0, 0, 1)
            self.line = Line(
                points=[*self.start_node.center, *self.end_node.center], width=2
            )
        self.start_node.connections.append(self)
        self.end_node.connections.append(self)

    def update(self):
        self.line.points = [*self.start_node.center, *self.end_node.center]


class NetworkCanvas(FloatLayout):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.selected_node = None
        self.nodes = []

    def add_node(self, text="New Node", pos=(100, 100)):
        node = Node(text=text, pos=pos)
        self.nodes.append(node)
        self.ids.canvas_container.add_widget(node)

    def connect_nodes(self, node1, node2):
        ConnectionLine(node1, node2, self.ids.canvas_container.canvas)

    def on_touch_down(self, touch):
        if super().on_touch_down(touch) is False:
            if touch.button == "right":
                for node in self.nodes:
                    if node.collide_point(*touch.pos):
                        if not self.selected_node:
                            self.selected_node = node
                        else:
                            self.connect_nodes(self.selected_node, node)
                            self.selected_node = None
                        return True
                self.selected_node = None
        return True


class Network(FloatLayout):
    pass


class ConnectionNodesApp(App):
    def build(self):
        layout = Network()
        network_canvas = layout.ids.network_layout
        network_canvas.add_node(text="Start Node")
        return layout

    def add_node(self):
        network_canvas = self.root.ids.network_layout
        network_canvas.add_node(text="New Node")


if __name__ == "__main__":
    ConnectionNodesApp().run()

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 17:35:53