简体   繁体   English

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

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

I am creating a graph drawing app and currently facing a problem drawing the edges from one node to the other.我正在创建一个绘图应用程序,目前面临从一个节点到另一个节点绘制边的问题。 The problem is that to draw the edge between two nodes I must find the points of intersection of the two nodes (circles) and the line intersecting their centers, which then gives me 4 (x, y) points, then by finding the two closest points that is where I draw the edge.问题是要绘制两个节点之间的边,我必须找到两个节点(圆圈)的交点和与它们中心相交的线,然后给出 4 (x, y) 点,然后找到两个最接近的点点就是我画边的地方。 For this I made a function (No need to actually understand how it operates its just math):为此,我做了一个 function(不需要真正理解它是如何运行的,只是数学运算):

    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())]

at the end it returns a list of 4 values: x1, y1, x2, y2 which will be used to draw the edge.最后它返回一个包含 4 个值的列表:x1、y1、x2、y2 将用于绘制边缘。 The issue is that these values are always floats, and the method I use to draw the edges is with the QPainter and drawLine method which takes integers only not floats, so I thought I could probably round it to the nearest int and no one would notice because the pixels are so small.问题是这些值总是浮点数,我用来绘制边缘的方法是使用 QPainter 和 drawLine 方法,它只接受整数而不是浮点数,所以我想我可能可以将它四舍五入到最接近的 int 并且没有人会注意到因为像素太小了。 This results in the edge looking something like this:这导致边缘看起来像这样:

应用程序图片

As you can see the starting point of the edge goes inside the Node when its rounded so I was wondering if there could be a way to draw a line with floating point x, y coordinates or if there is a different way to do this entirely without the math that I wasn't aware of正如您所看到的,边的起点在变圆时进入节点内部,所以我想知道是否有一种方法可以用浮点 x、y 坐标绘制一条线,或者是否有其他方法可以完全不用我不知道的数学

EDIT: Here is a minimal reproducible example:编辑:这是一个最小的可重现示例:

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_())

The problem has nothing to do with floating point values: as you can see, the line is off by more than a pixel, which wouldn't be caused by "imprecise" integer values.该问题与浮点值无关:如您所见,该线偏离了一个像素以上,这不是由“不精确”的 integer 值引起的。
The main cause is the fact that you've set a border for the widget with a specific width.主要原因是您为具有特定宽度的小部件设置了边框

When a border is set from style sheets (or by the style when using QFrame subclasses), the actual final geometry includes the border width.当从样式表(或使用 QFrame 子类时通过样式)设置边框时,实际的最终几何图形包括边框宽度。 If you just print() the geometry() of those nodes after they've been shown, you'll see that their size is 106x106, which is the actual size plus twice the border (left+right for the width and top+bottom for the height).如果您在显示这些节点print()它们的geometry() ,您会看到它们的大小为 106x106,这是实际大小加上边框的两倍(宽度为左+右,顶部+底部为为高度)。

A simple workaround for your case is to consider those borders when preparing the values for the computation:针对您的情况的一个简单解决方法是在准备计算值时考虑这些边界:

    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
        # ...

Note that using widgets for complex custom drawings (and especially when dealing with multiple objects, advanced hierarchies and precise positioning), is normally discouraged: widgets are normally intended as standard interface elements and their implementation follows the same concept, which can cause problems if used in the "wrong" way (like what happened to you).请注意,通常不鼓励将小部件用于复杂的自定义绘图(尤其是在处理多个对象、高级层次结构和精确定位时):小部件通常旨在用作标准界面元素,并且它们的实现遵循相同的概念,如果使用可能会导致问题以“错误”的方式(就像发生在你身上的事)。

For the same reason, using child widgets with fixed geometries can cause unexpected problems (try setting the heuristic to a value with 3 or 4 digits and you'll see the result).出于同样的原因,使用具有固定几何形状的子部件可能会导致意外问题(尝试将启发式设置为 3 或 4 位数字的值,您将看到结果)。

A better solution is to completely draw the elements with the QPainter functions, and compute the coordinates according to the contents.更好的方案是完全用QPainter函数绘制元素,根据内容计算坐标。 Note that, while your problem was not caused by the usage of integer values, it must be considered that all QPainter methods that use numeric arguments for coordinates accept integers only, and if you want to use floating points you need to use the floating point Qt classes: QPointF , QLineF , QRectF .请注意,虽然您的问题不是由使用 integer 值引起的,但必须考虑所有使用数字 arguments 作为坐标的 QPainter 方法仅接受整数,如果您想使用浮点数,则需要使用浮点数 Qt类: QPointFQLineFQRectF

On the other hand, Qt provides simpler (and faster) methods to do connect two circles with a line that intersects their centers:另一方面,Qt 提供了更简单(也更快)的方法来用与圆心相交的线连接两个圆:

  • create a QLineF that connects the centers of the two circles;创建一个连接两个圆心的 QLineF;
  • get the angle of that line;得到那条线的角度
  • create two lines for the two circles with the static fromPolar() , which will be the lines from the center of the circles to their circumference, along with the connection of their centers;使用 static fromPolar()为两个圆创建两条线,这将是从圆心到圆周的线,以及它们中心的连接;
  • draw a line between the second point of those two lines;在这两条线的第二点之间画一条线;

Since we obviously don't need the style sheet anymore, we can subclass from QWidget instead of QFrame.由于我们显然不再需要样式表,因此我们可以从 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)

Final notes:最后说明:

  • in the long run, the whole custom QWidget implementation will result in much more complex issues;从长远来看,整个自定义 QWidget 实现将导致更复杂的问题; I strongly suggest you to switch to the Graphics View Framework , which also provides better mouse interaction and performance, especially if using the existing graphics items, like QGraphicsEllipseItem;我强烈建议您切换到Graphics View Framework ,它还提供更好的鼠标交互和性能,尤其是在使用现有图形项(如 QGraphicsEllipseItem)时;
  • x() and y() are existing, dynamic properties of all QWidgets, and you should not overwrite them; x()y()是所有 QWidget 的现有动态属性,您不应该覆盖它们;
  • the QPainter used in paintEvent() gets automatically destroyed when the function returns, there is no need to explicitly call end() ;当 function 返回时, paintEvent()中使用的 QPainter 会自动销毁,无需显式调用end()
  • if you used widgets in order to change the values of those labels, you can still do it by creating custom properties or even Qt properties, and then call self.update() in their setter functions;如果您使用小部件来更改这些标签的值,您仍然可以通过创建自定义属性甚至 Qt 属性来实现,然后在它们的设置函数中调用self.update() for these situations, using child widgets just for their object name or property setter/getters is not a comparable benefit;对于这些情况,仅仅为它们的 object 名称或属性设置器/获取器使用子部件是没有可比的好处的;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM