文章描述:
在Qt开发的软件中,我们经常需要进行绘图操作;当然采用PyQtChart模块可以直接创建坐标进行曲线的绘制,然而本文不讲PyQtChart进行绘图的功能,而且针对我们QtWidget.QWidget等控件中重写paintEvent函数进行绘图来讲解。
那么就有人要问了,既然PyQtChart模块都已经很完善了,为什么我还要来讲解重绘paintEvent方法进行绘图呢?
这就是我要讲的原因了,PyQtChart进行绘图基本都是静态绘图,对于动态绘图大多数情况下都是通过信号发送进行页面的更新;然而对于数据量很大的时候并且每个数据是实时生成的情况下,处理信号的发送非常耗PC内存并且绘制的图形的线条有时会存在延时更新的情况,对于中低端的PC来说,这可以说是一大痛点。
举一个我之前的一个项目来说,如果你要实时显示你的鼠标的移动轨迹路线,并在窗口中显示出来。如果是采用PyQtChart模块来开发,首先你要安装,然后在窗体中设置QChart图标为背景图并把QChartView背景透明化等等一系列操作,很显然对于一个鼠标的轨迹路线显示,其实并没有那么麻烦,只需通过重写paintEvent()方法并添加逻辑即可
如果一个鼠标移动时,一定时间内上报的两个点间来绘制线条,采用QPainterPath().drawLine()方法来画线必然是一条直线,所以才会引出我想要讲的直线拟合曲线的两个方式。
注:本文主要是采用简易的方式来讲解这两个方法,当然不可能将我的项目放上来讲。
二次贝塞尔曲线和三次贝塞尔曲线的区别
(1)二次贝塞尔曲线:
结合QPainterPath().quadTo()方法来看,二次贝塞尔曲线需要一个控制点坐标及一个终点坐标;然后并不止如此,其实还需要一个起始点的坐标,然后通过起始点的坐标和终点坐标来计算控制点的坐标
def quadTo(self,
ctrlPt: QPointF | QPoint,
endPt: QPointF | QPoint) -> None
如下图,是一个采用二次贝塞尔曲线将两个点由直线拟合成曲线的图形
下面是代码案例:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPainterPath
from PyQt5.QtCore import Qt
class BezierFittingWidget(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(400, 400)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # 启用抗锯齿
start_point = (50, 50)
end_point = (300, 300)
# 计算控制点
control_point = ((start_point[0] + end_point[0]) / 2, start_point[1])
path = QPainterPath()
path.moveTo(start_point[0], start_point[1])
path.quadTo(control_point[0], control_point[1], end_point[0], end_point[1])
painter.drawPath(path)
if __name__ == '__main__':
app = QApplication([])
widget = BezierFittingWidget()
widget.show()
app.exec_()
下面是一个计算二次贝塞尔曲线中控制点的方法:
import math
def calculate_control_point(point1, point2):
# 计算中点
mid_x = (point1[0] + point2[0]) / 2
mid_y = (point1[1] + point2[1]) / 2
# 计算垂直偏移量
offset = math.sqrt((point2[0] - point1[0])**2 + (point2[1] - point1[1])**2) / 2
# 根据两点的斜率决定偏移方向
slope = (point2[1] - point1[1]) / (point2[0] - point1[0])
if slope == 0:
control_point = (mid_x, mid_y + offset)
else:
angle = math.atan(slope)
control_point = (mid_x + offset * math.cos(angle), mid_y + offset * math.sin(angle))
return control_point
(2)三次贝塞尔曲线
结合QPainterPath().cubicTo()方法来看,三次贝塞尔曲线需要两个控制点坐标及一个终点坐标;同样他也是需要一个起始点的坐标,然后通过起始点的坐标和终点坐标来计算两个控制点的坐标
def cubicTo(self,
ctrlPt1: QPointF | QPoint,
ctrlPt2: QPointF | QPoint,
endPt: QPointF | QPoint) -> None
如下图,是一个采用三次贝塞尔曲线将四个点由直线拟合成曲线的图形(共有三条贝塞尔曲线)
下面是代码案例:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPainterPath
from PyQt5.QtCore import Qt
import math
def calculate_cubic_bezier_control_points(start_point, end_point):
# 假设另外两个控制点为 control_point1 和 control_point2
# 计算中点
mid_x = (start_point[0] + end_point[0]) / 2
mid_y = (start_point[1] + end_point[1]) / 2
# 计算偏移量(可根据需求调整这个值来控制曲线的形状)
offset = math.sqrt((end_point[0] - start_point[0])**2 + (end_point[1] - start_point[1])**2) / 4
# 计算角度
angle = math.atan2(end_point[1] - start_point[1], end_point[0] - start_point[0])
# 计算控制点
control_point1 = (mid_x + offset * math.cos(angle + math.pi / 2), mid_y + offset * math.sin(angle + math.pi / 2))
control_point2 = (mid_x + offset * math.cos(angle - math.pi / 2), mid_y + offset * math.sin(angle - math.pi / 2))
return control_point1, control_point2
class FitCurveWidget(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(400, 400)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # 启用抗锯齿
# 方形的顶点坐标
square_points = [(100, 200), (200, 300), (300, 200), (200, 200)]
path = QPainterPath()
path.moveTo(square_points[0][0], square_points[0][1]) # 50,50
for i in range(1, len(square_points)):
# 计算控制点
result = calculate_cubic_bezier_control_points(square_points[i - 1], square_points[i])
print(result)
# 使用 cubicTo 绘制贝塞尔曲线拟合角
path.cubicTo(result[0][0], result[0][1], result[1][0], result[1][1], square_points[i][0], square_points[i][1])
painter.drawPath(path)
if __name__ == '__main__':
app = QApplication([])
widget = FitCurveWidget()
widget.show()
app.exec_()
下面是一个计算三次贝塞尔曲线中两个控制点的方法:
def calculate_cubic_bezier_control_points(start_point, end_point):
# 假设另外两个控制点为 control_point1 和 control_point2
# 计算中点
mid_x = (start_point[0] + end_point[0]) / 2
mid_y = (start_point[1] + end_point[1]) / 2
# 计算偏移量(可根据需求调整这个值来控制曲线的形状)
offset = math.sqrt((end_point[0] - start_point[0])**2 + (end_point[1] - start_point[1])**2) / 4
# 计算角度
angle = math.atan2(end_point[1] - start_point[1], end_point[0] - start_point[0])
# 计算控制点
control_point1 = (mid_x + offset * math.cos(angle + math.pi / 2), mid_y + offset * math.sin(angle + math.pi / 2))
control_point2 = (mid_x + offset * math.cos(angle - math.pi / 2), mid_y + offset * math.sin(angle - math.pi / 2))
return control_point1, control_point2
好了,以上就是我想要记录的相关知识了