试图在pyqt5中用浮点x、y值画一条线

trying to draw a line with floating point x, y values in pyqt5

我正在创建一个图形绘制应用程序,目前面临绘制从一个节点到另一个节点的边的问题。 问题是要在两个节点之间绘制边,我必须找到两个节点(圆圈)的交点和与它们中心相交的线,然后给出 4 (x, y) 点,然后找到两个最接近的点点就是我画边的地方。 为此,我做了一个函数(不需要真正理解它是如何运行的):

    def computeIntersections(self, node1, node2):
        a, b = node1.x, node1.y
        c, d = node2.x, node2.y
        r1 = node1.radius
        r2 = node2.radius

        m = (d - b) / (c - a)

        g = 1+m**2

        xa = g
        xb = -(2*a*g)
        xc = g*a**2 - r1**2

        x1Node1 = (-xb + sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        x2Node1 = (-xb - sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        y1Node1 = m*(x1Node1 - a) + b
        y2Node1 = m*(x2Node1 - a) + b

        xa = g
        xb = -(2*c*g)
        xc = g*c**2 - r2**2

        x1Node2 = (-xb + sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        x2Node2 = (-xb - sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        y1Node2 = m*(x1Node2 - a) + b
        y2Node2 = m*(x2Node2 - a) + b

        node1Intersections = [(x1Node1, y1Node1), (x2Node1, y2Node1)]
        node2Intersections = [(x1Node2, y1Node2), (x2Node2, y2Node2)]

        distances = {}
        for point1 in node1Intersections:
            for point2 in node2Intersections:
                x1 = point1[0]; y1 = point1[1]
                x2 = point2[0]; y2 = point2[1]

                distance = sqrt((x1 - x2)**2 + (y1 - y2)**2)

                distances[distance] = (x1, y1, x2, y2)

        return distances[min(distances.keys())]

最后是 returns 4 个值的列表:x1、y1、x2、y2 将用于绘制边缘。 问题是这些值总是浮点数,我用来绘制边缘的方法是使用 QPainter 和 drawLine 方法,它只接受整数而不是浮点数,所以我想我可能可以将它四舍五入到最接近的 int 并且没有人会注意到因为像素太小了。这导致边缘看起来像这样:

如您所见,边的起点在节点变圆时进入节点内部,所以我想知道是否可以使用浮点 x、y 坐标绘制一条线,或者是否有其他方法完全不用我不知道的数学就可以做到这一点

编辑: 这是一个最小的可重现示例:

from PyQt5.QtWidgets import QLabel, QFrame, QApplication, QWidget
from PyQt5.QtGui import QPainter
from math import sqrt
import sys

class Window(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.setGeometry(200, 200, 800, 800)

        self.node1 = Node(self, 'A', 5, 100, 100)
        self.node2 = Node(self, 'B', 20, 700, 700)

    def computeIntersections(self, node1, node2):
        a, b = node1.x, node1.y
        c, d = node2.x, node2.y
        r1 = node1.radius
        r2 = node2.radius

        m = (d - b) / (c - a)

        g = 1+m**2

        xa = g
        xb = -(2*a*g)
        xc = g*a**2 - r1**2

        x1Node1 = (-xb + sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        x2Node1 = (-xb - sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        y1Node1 = m*(x1Node1 - a) + b
        y2Node1 = m*(x2Node1 - a) + b

        xa = g
        xb = -(2*c*g)
        xc = g*c**2 - r2**2

        x1Node2 = (-xb + sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        x2Node2 = (-xb - sqrt(xb**2 - 4*xa*xc)) / (2*xa)
        y1Node2 = m*(x1Node2 - a) + b
        y2Node2 = m*(x2Node2 - a) + b

        node1Intersections = [(x1Node1, y1Node1), (x2Node1, y2Node1)]
        node2Intersections = [(x1Node2, y1Node2), (x2Node2, y2Node2)]

        distances = {}
        for point1 in node1Intersections:
            for point2 in node2Intersections:
                x1 = point1[0]; y1 = point1[1]
                x2 = point2[0]; y2 = point2[1]

                distance = sqrt((x1 - x2)**2 + (y1 - y2)**2)

                distances[distance] = (x1, y1, x2, y2)

        return distances[min(distances.keys())]

    def paintEvent(self, event):
        x1, y1, x2, y2 = [round(n) for n in self.computeIntersections(self.node1, self.node2)]
        qp = QPainter()
        qp.begin(self)
        qp.drawLine(x1, y1, x2, y2)
        qp.end()

class Node(QFrame):
    def __init__(self, parent, name, heuristic, x, y):
        super().__init__(parent)

        self.parent = parent
        self.name = name
        self.heuristic = heuristic
        self.x = x
        self.y = y
        self.radius = 50

        self.initializeNode()
    
    def initializeNode(self):
        self.setGeometry(self.x-self.radius, self.y-self.radius, self.radius*2, self.radius*2)

        self.nodeName = QLabel(self.name, self)
        self.nodeName.setObjectName('nodeName')
        self.nodeName.setGeometry(25, 15, 50, 50)

        self.nodeHeuristic = QLabel(str(self.heuristic), self)
        self.nodeHeuristic.setObjectName('nodeHeuristic')
        self.nodeHeuristic.setGeometry(40, 70, 20, 20)

        self.setDefaultStyle()


    def setDefaultStyle(self):
        self.setStyleSheet(defaultStyle)

defaultStyle = """
    QFrame {
        border: 3px solid black;
        min-height: 100px;
        min-width: 100px;
        border-radius: 53px;
    }

    QFrame#nodeName {
        qproperty-alignment: AlignCenter;
        border: 0px;
        font-size: 30px;
        min-height: 50px;
        min-width: 50px;
    }

    QFrame#nodeHeuristic {
        height:10px;
        width:10px;
        min-height: 20px;
        min-width: 20px;
        border-radius: 12px;
        qproperty-alignment: AlignCenter;
        font-size: 15px;
    }
"""

app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

该问题与浮点值无关:如您所见,线条偏离了一个像素以上,这不是由“不精确”的整数值引起的。
主要原因是您为具有特定宽度的小部件设置了 border

当根据样式 sheets(或使用 QFrame sub类 时的样式)设置边框时,实际的 final 几何图形包括边框宽度。如果你只是 print() 这些节点的 geometry() 它们被显示之后,你会看到它们的大小是 106x106,这是实际大小加上边框的两倍(宽度为左+右,高度为上+下)。

针对您的情况的一个简单解决方法是在准备计算值时考虑这些边界:

    def computeIntersections(self, node1, node2):
        border = node1.frameWidth()
        a, b = node1.x + border, node1.y + border
        c, d = node2.x + border, node2.y + border
        r1 = node1.radius + border
        r2 = node2.radius + border
        # ...

请注意,通常不鼓励将小部件用于复杂的自定义绘图(尤其是在处理多个对象、高级层次结构和精确定位时):小部件通常旨在用作标准界面元素,并且它们的实现遵循相同的概念,即如果以“错误”方式使用(例如发生在您身上的事情),可能会导致问题。

出于同样的原因,使用具有固定几何形状的子控件可能会导致意外问题(尝试将启发式设置为 3 或 4 位数字的值,您会看到结果)。

更好的方案是完全用QPainter函数绘制元素,根据内容计算坐标。请注意,虽然您的问题不是由使用整数值引起的,但必须考虑所有使用数字参数作为坐标的 QPainter 方法只接受整数,如果您想使用浮点数,则需要使用浮点数 Qt 类: QPointF, QLineF, QRectF.

另一方面,Qt 提供了更简单(和更快)的方法来用与圆心相交的线连接两个圆:

  • 创建一个连接两个圆心的 QLineF;
  • 得到那条线的角度
  • 用静态fromPolar()为两个圆创建两条线,即从圆心到圆周的线,以及圆心的连线;
  • 在这两条线的第二个点之间画一条线;

由于我们显然不再需要样式 sheet,我们可以从 QWidget 继承 QWidget 而不是 QFrame。

class Window(QWidget):
    def __init__(self):
        QWidget.__init__(self)
        self.setGeometry(200, 200, 800, 800)

        self.node1 = Node(self, 'A', 5, 100, 100)
        self.node2 = Node(self, 'B', 20, 700, 700)

    def paintEvent(self, event):
        qp = QPainter(self)
        qp.setRenderHints(qp.Antialiasing)

        # the angle between the two centers
        angle = QLineF(self.node1.center, self.node2.center).angle()

        # a line that uses the same angle and is equal to the radius
        first = QLineF.fromPolar(self.node1.radius, angle)
        # fromPolar always has the first point at (0, 0), so we need to
        # translate it to the center of the first circle
        startRadius = first.translated(self.node1.center)

        # the same as above, but with an inverted angle
        second = QLineF.fromPolar(self.node2.radius, angle + 180)
        endRadius = second.translated(self.node2.center)

        # draw the line connecting the end points of each radius
        qp.drawLine(startRadius.p2(), endRadius.p2())


class Node(QWidget):
    def __init__(self, parent, name, heuristic, x, y):
        super().__init__(parent)

        self.parent = parent
        self.name = name
        self.heuristic = heuristic
        self.center = QPoint(x, y)
        self.radius = 50
        self.setGeometry(x - 50, y - 50, 100, 100)

    def paintEvent(self, event):
        qp = QPainter(self)
        qp.setRenderHints(qp.Antialiasing)
        qp.setPen(QPen(Qt.black, 3))
        # the circle is drawn inside the widget rectangle, so we need to draw
        # a circle that is smaller by half of the pen size
        margin = 1.5
        size = self.radius * 2 - margin * 2
        qp.drawEllipse(QRectF(margin, margin, size, size))
        
        font = self.font()
        font.setPointSize(30)
        qp.setFont(font)

        # a horizontally centered rectangle that is slightly above the
        # center, considering the font size
        nameHeight = QFontMetrics(font).ascent()
        nameRect = QRectF(0, self.height() / 2 - nameHeight, 
            self.width(), 30)
        qp.drawText(nameRect, Qt.AlignCenter, self.name)

        font.setPointSize(15)
        qp.setFont(font)
        fm = QFontMetrics(font)

        # the circle must contain the text, we need to add some margin
        heuText = ' ' + str(self.heuristic) + ' '
        heuSize = max(fm.height(), fm.horizontalAdvance(heuText))
        # the rect of the circle, placed at half its height from the bottom;
        # note that I used QRectF for precision
        heuRect = QRectF(self.width() / 2 - heuSize / 2, 
            self.height() - heuSize * 1.5, 
            heuSize, heuSize)
        qp.drawEllipse(heuRect)
        qp.drawText(heuRect, Qt.AlignCenter, heuText)

最后的笔记:

  • 在长运行中,整个自定义 QWidget 实现将导致更复杂的问题;我强烈建议您切换到 Graphics View Framework,它还提供更好的鼠标交互和性能,尤其是在使用现有图形项时,如 QGraphicsEllipseItem;
  • x() and y() 是所有 QWidget 的现有动态属性,您不应覆盖它们;
  • paintEvent()中使用的QPainter在函数returns时自动销毁,无需显式调用end()
  • 如果您使用小部件来更改这些标签的值,您仍然可以通过创建自定义属性甚至 Qt 属性来实现,然后在它们的 setter 函数中调用 self.update();对于这些情况,仅使用子部件的对象名称或 属性 setter/getters 没有可比的好处;