试图在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 没有可比的好处;
我正在创建一个图形绘制应用程序,目前面临绘制从一个节点到另一个节点的边的问题。 问题是要在两个节点之间绘制边,我必须找到两个节点(圆圈)的交点和与它们中心相交的线,然后给出 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()
andy()
是所有 QWidget 的现有动态属性,您不应覆盖它们;paintEvent()
中使用的QPainter在函数returns时自动销毁,无需显式调用end()
;- 如果您使用小部件来更改这些标签的值,您仍然可以通过创建自定义属性甚至 Qt 属性来实现,然后在它们的 setter 函数中调用
self.update()
;对于这些情况,仅使用子部件的对象名称或 属性 setter/getters 没有可比的好处;