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

PyQt技术实现:创建随按钮移动的LineItem

解决连线跟随按钮移动的问题

当然可以直接修改LineItem的位置来实现连线随按钮移动!完全不用删除重建,咱们只需要让每条连线记住它绑定的两个按钮,然后在按钮移动时实时更新连线的起点和终点就好。我帮你调整一下代码,核心思路如下:

关键修改点

  • 给GraphicsLineItem增加属性,存储它连接的两个按钮对象,这样能随时获取按钮的实时位置
  • 把原来记录坐标的逻辑改成记录按钮引用,避免坐标过时
  • 按钮移动时触发连线的位置更新,这里我们通过重写DragButton的moveEvent来实现

修改后的完整代码

import sys, os
from PyQt5 import QtCore, QtGui, QtWidgets

can_draw = 0
start_btn = None  # 改为存储按钮对象
end_btn = None

class DragButton(QtWidgets.QPushButton):
    moved = QtCore.pyqtSignal()  # 新增移动信号,用来通知连线更新

    def __init__(self, title, parent=None):
        super().__init__(title, parent)
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showMenu)

    def showMenu(self):
        menu = QtWidgets.QMenu()
        menu.addAction("connect", self.connectLine)
        menu.exec_(self.cursor().pos())

    def mouseMoveEvent(self, e):
        if e.buttons() != QtCore.Qt.LeftButton:
            return
        mimeData = QtCore.QMimeData()
        drag = QtGui.QDrag(self)
        drag.setMimeData(mimeData)
        drag.setHotSpot(e.pos() - self.rect().topLeft())
        dropAction = drag.exec_(QtCore.Qt.MoveAction)

    def moveEvent(self, e):
        super().moveEvent(e)
        self.moved.emit()  # 按钮移动时发送信号

    def connectLine(self):
        global can_draw, start_btn, end_btn
        view = self.parent()
        can_draw +=1
        if can_draw == 1:
            start_btn = self  # 记录第一个按钮
            print("选中第一个按钮")
        if can_draw == 2:
            end_btn = self  # 记录第二个按钮
            can_draw -= 2
            view.createLineItem(start_btn, end_btn)  # 把按钮传给创建连线的方法
            start_btn = None
            end_btn = None

class GraphicsLineItem(QtWidgets.QGraphicsLineItem):
    def __init__(self, start_btn, end_btn, parent=None):
        super().__init__(parent)
        self.start_btn = start_btn
        self.end_btn = end_btn
        # 绑定按钮的移动信号到更新方法
        self.start_btn.moved.connect(self.updateLine)
        self.end_btn.moved.connect(self.updateLine)
        self.updateLine()  # 初始化时绘制第一条线

    def updateLine(self):
        # 获取按钮在场景中的位置(取按钮中心更合理)
        view = self.start_btn.parent()
        start_pos = QtCore.QPointF(view.mapToScene(self.start_btn.pos()) + QtCore.QPointF(self.start_btn.width()/2, self.start_btn.height()/2))
        end_pos = QtCore.QPointF(view.mapToScene(self.end_btn.pos()) + QtCore.QPointF(self.end_btn.width()/2, self.end_btn.height()/2))
        self.setLine(QtCore.QLineF(start_pos, end_pos))

    def contextMenuEvent(self, event):
        menu = QtWidgets.QMenu()
        menu.addAction("Delete", self.remove)
        menu.exec_(self.cursor().pos())

    def remove(self):
        # 移除前断开信号连接,避免内存泄漏
        self.start_btn.moved.disconnect(self.updateLine)
        self.end_btn.moved.disconnect(self.updateLine)
        self.scene().removeItem(self)

    def shape(self):
        p = super(GraphicsLineItem, self).shape()
        stroker = QtGui.QPainterPathStroker()
        stroker.setWidth(20)
        return stroker.createStroke(p)

class View(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super(View, self).__init__(parent)
        self.setScene(QtWidgets.QGraphicsScene(self))
        self.setAcceptDrops(True)
        self.setSceneRect(QtCore.QRectF(self.viewport().rect()))
        self.btn1 = QtWidgets.QPushButton("Start")
        self.btn1.setGeometry(230, 80, 100, 30)
        self.btn1.setCheckable(True)
        self.btn1.clicked.connect(self.add_Text)

    def createLineItem(self, start_btn, end_btn):
        # 创建连线时传入两个按钮
        line_item = GraphicsLineItem(start_btn, end_btn)
        self.scene().addItem(line_item)

    def clearScene(self):
        self.scene().clear()

    def add_Text(self):
        self.button = DragButton('Text', self)
        self.button.setGeometry(230, 80, 100, 30)
        self.button.show()

    def dragEnterEvent(self, e):
        e.accept()

    def dragMoveEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        btn = e.source()
        position = e.pos()
        btn.move(position - btn.rect().topLeft())  # 修正位置,让鼠标落点对应按钮的拖动点
        e.setDropAction(QtCore.Qt.MoveAction)
        e.accept()

class Window(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.view = View()
        self.button = QtWidgets.QPushButton(
            "Clear View", clicked=self.view.scene().clear )
        self.btn1 = QtWidgets.QPushButton("add button")
        self.btn1.setCheckable(True)
        self.btn1.clicked.connect(self.view.add_Text)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.view)
        layout.addWidget(self.btn1)
        layout.addWidget(self.button)  # 把Clear按钮加回去

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.resize(640, 480)
    window.show()
    sys.exit(app.exec_())

代码说明

  1. 按钮移动信号:给DragButton添加了moved信号,每次按钮移动(不管是拖拽还是其他方式)都会触发这个信号
  2. 连线绑定按钮:GraphicsLineItem初始化时接收两个按钮对象,并且绑定它们的moved信号到自己的updateLine方法
  3. 实时更新连线:updateLine方法会获取两个按钮的当前位置(这里取按钮中心,比取左上角更合理),然后调用setLine方法直接修改连线的坐标,完全不需要删除重建
  4. 修正拖拽位置:原来的dropEvent里直接btn.move(position)会导致按钮位置偏移,改成btn.move(position - btn.rect().topLeft())让鼠标落点和按钮的拖动点对应,体验更自然

这样修改后,你拖动任意一个按钮,对应的连线都会实时跟着移动,完全符合你的需求!

内容的提问来源于stack exchange,提问作者於二號

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:49:53