如何在QT中绘制一条经过多个点的平滑曲线?
How to draw a smooth curved line that goes through several points in QT?
有没有办法在QT中通过一组点绘制一条平滑的线?
在 运行 时间内设置点的数量和位置。
目前,我绘制了一个 QPainterPath,其中包含从一点到另一点的 lineTo,创建一条路径。我确实使用渲染提示抗锯齿,但路径仍然是锯齿状的。
我见过 QSplineSeries,它似乎提供了这种弯曲的路径,但它在 Qt4.8 中不可用,这是我正在使用的 QT 版本。
另一个经常被建议的选项是使用贝塞尔曲线,但那些使用一个起点和终点以及两个控制点,所以我需要为每个段(每个 lineTo)计算它并以某种方式计算那些控制点我暂时没有。
我认为 Qt 4.8 中没有开箱即用的解决方案(您已经注意到 QSplineSeries
是 Qt 5.x 的一项功能)。另外 QSplineSeries
是 QtCharts
模块的一部分,它是一个商业模块(如 QtDataVisualization
),所以除非你有商业许可证或者你的项目是 GPL,否则你不能使用它。
你必须手动完成它,即通过它所需的数学运算并自己实现它(或者找到一个很好的实现(甚至不需要在 C++ 中,更不用说与 Qt 兼容了))。
既然你提到了贝塞尔曲线,我建议给出 composite Bezier curve a shot. I remember implementing that thing for a project I worked on. It required some...work. :D This article 可能会帮助你开始。
贝塞尔曲线实际上是 B 样条曲线(如果我没记错的话)。尤其是如果您可以解决一定程度的平滑度不足问题,您可以非常快速地生成复合贝塞尔曲线。考虑到它们的稳健性和受欢迎程度,我 100% 确定您可以在网上找到一个不错的实现。可能对 Qt 不友好,但如果编写得当,您应该能够立即调整代码。
This looks quite promising (it's in ActionScript but meh). Or you can given the QPainterPath::cubicTo()
一个可以为您创建贝塞尔曲线的镜头,您也可以提供计算曲线所需的两个控制点。
几乎每个人都使用三次插值来完成这项任务,您的选择是贝塞尔曲线或 Catmull-Rom 样条。如果您必须击中每个点,那么您需要保持 "handles" 或贝塞尔曲线控制点之间的直线。然后您使用最小二乘法进行拟合,您发现这有点复杂。
Catmull Rom 样条的优点是它们只需要两个额外的控制点(开始和结束,只需镜像点即可创建它们)。只要点相当平滑,线就会表现得很好。 QT 图形不太可能直接绘制 CatMull Rom 样条曲线,因此转换为 Beziers,这是一种标准的已发布方法,您可以很容易地从 Catmull Rom 转到 Bezier,但不是相反 - 并非每个 Bezier 都可以用 Catmull Rom 表示只有几点。
您可以使用其他插值方法,eq quintic,如果三次不能给您想要的曲线。
最后我实施了某种变通方法,基本上采用两条连接线,删除它们之间的连接点并用曲线替换它。由于我有很多细线,这样的变化是不可见的,所以我删除了所有非常短的线并重新连接了开口端。该功能主要由 Bojan Kverh 提供,查看他的教程:https://www.toptal.com/c-plus-plus/rounded-corners-bezier-curves-qpainter
这里是函数:
namespace
{
float distance(const QPointF& pt1, const QPointF& pt2)
{
float hd = (pt1.x() - pt2.x()) * (pt1.x() - pt2.x());
float vd = (pt1.y() - pt2.y()) * (pt1.y() - pt2.y());
return std::sqrt(hd + vd);
}
QPointF getLineStart(const QPointF& pt1, const QPointF& pt2)
{
QPointF pt;
float rat = 10.0 / distance(pt1, pt2);
if (rat > 0.5) {
rat = 0.5;
}
pt.setX((1.0 - rat) * pt1.x() + rat * pt2.x());
pt.setY((1.0 - rat) * pt1.y() + rat * pt2.y());
return pt;
}
QPointF getLineEnd(const QPointF& pt1, const QPointF& pt2)
{
QPointF pt;
float rat = 10.0 / distance(pt1, pt2);
if (rat > 0.5) {
rat = 0.5;
}
pt.setX(rat * pt1.x() + (1.0 - rat)*pt2.x());
pt.setY(rat * pt1.y() + (1.0 - rat)*pt2.y());
return pt;
}
}
void PainterPath::smoothOut(const float& factor)
{
QList<QPointF> points;
QPointF p;
for (int i = 0; i < mPath->elementCount() - 1; i++) {
p = QPointF(mPath->elementAt(i).x, mPath->elementAt(i).y);
// Except for first and last points, check what the distance between two
// points is and if its less then min, don't add them to the list.
if (points.count() > 1 && (i < mPath->elementCount() - 2) && (distance(points.last(), p) < factor)) {
continue;
}
points.append(p);
}
// Don't proceed if we only have 3 or less points.
if (points.count() < 3) {
return;
}
QPointF pt1;
QPointF pt2;
QPainterPath* path = new QPainterPath();
for (int i = 0; i < points.count() - 1; i++) {
pt1 = getLineStart(points[i], points[i + 1]);
if (i == 0) {
path->moveTo(pt1);
} else {
path->quadTo(points[i], pt1);
}
pt2 = getLineEnd(points[i], points[i + 1]);
path->lineTo(pt2);
}
delete mPath;
mPath = path;
prepareGeometryChange();
}
有没有办法在QT中通过一组点绘制一条平滑的线? 在 运行 时间内设置点的数量和位置。
目前,我绘制了一个 QPainterPath,其中包含从一点到另一点的 lineTo,创建一条路径。我确实使用渲染提示抗锯齿,但路径仍然是锯齿状的。
我见过 QSplineSeries,它似乎提供了这种弯曲的路径,但它在 Qt4.8 中不可用,这是我正在使用的 QT 版本。
另一个经常被建议的选项是使用贝塞尔曲线,但那些使用一个起点和终点以及两个控制点,所以我需要为每个段(每个 lineTo)计算它并以某种方式计算那些控制点我暂时没有。
我认为 Qt 4.8 中没有开箱即用的解决方案(您已经注意到 QSplineSeries
是 Qt 5.x 的一项功能)。另外 QSplineSeries
是 QtCharts
模块的一部分,它是一个商业模块(如 QtDataVisualization
),所以除非你有商业许可证或者你的项目是 GPL,否则你不能使用它。
你必须手动完成它,即通过它所需的数学运算并自己实现它(或者找到一个很好的实现(甚至不需要在 C++ 中,更不用说与 Qt 兼容了))。
既然你提到了贝塞尔曲线,我建议给出 composite Bezier curve a shot. I remember implementing that thing for a project I worked on. It required some...work. :D This article 可能会帮助你开始。
贝塞尔曲线实际上是 B 样条曲线(如果我没记错的话)。尤其是如果您可以解决一定程度的平滑度不足问题,您可以非常快速地生成复合贝塞尔曲线。考虑到它们的稳健性和受欢迎程度,我 100% 确定您可以在网上找到一个不错的实现。可能对 Qt 不友好,但如果编写得当,您应该能够立即调整代码。
This looks quite promising (it's in ActionScript but meh). Or you can given the QPainterPath::cubicTo()
一个可以为您创建贝塞尔曲线的镜头,您也可以提供计算曲线所需的两个控制点。
几乎每个人都使用三次插值来完成这项任务,您的选择是贝塞尔曲线或 Catmull-Rom 样条。如果您必须击中每个点,那么您需要保持 "handles" 或贝塞尔曲线控制点之间的直线。然后您使用最小二乘法进行拟合,您发现这有点复杂。
Catmull Rom 样条的优点是它们只需要两个额外的控制点(开始和结束,只需镜像点即可创建它们)。只要点相当平滑,线就会表现得很好。 QT 图形不太可能直接绘制 CatMull Rom 样条曲线,因此转换为 Beziers,这是一种标准的已发布方法,您可以很容易地从 Catmull Rom 转到 Bezier,但不是相反 - 并非每个 Bezier 都可以用 Catmull Rom 表示只有几点。
您可以使用其他插值方法,eq quintic,如果三次不能给您想要的曲线。
最后我实施了某种变通方法,基本上采用两条连接线,删除它们之间的连接点并用曲线替换它。由于我有很多细线,这样的变化是不可见的,所以我删除了所有非常短的线并重新连接了开口端。该功能主要由 Bojan Kverh 提供,查看他的教程:https://www.toptal.com/c-plus-plus/rounded-corners-bezier-curves-qpainter
这里是函数:
namespace
{
float distance(const QPointF& pt1, const QPointF& pt2)
{
float hd = (pt1.x() - pt2.x()) * (pt1.x() - pt2.x());
float vd = (pt1.y() - pt2.y()) * (pt1.y() - pt2.y());
return std::sqrt(hd + vd);
}
QPointF getLineStart(const QPointF& pt1, const QPointF& pt2)
{
QPointF pt;
float rat = 10.0 / distance(pt1, pt2);
if (rat > 0.5) {
rat = 0.5;
}
pt.setX((1.0 - rat) * pt1.x() + rat * pt2.x());
pt.setY((1.0 - rat) * pt1.y() + rat * pt2.y());
return pt;
}
QPointF getLineEnd(const QPointF& pt1, const QPointF& pt2)
{
QPointF pt;
float rat = 10.0 / distance(pt1, pt2);
if (rat > 0.5) {
rat = 0.5;
}
pt.setX(rat * pt1.x() + (1.0 - rat)*pt2.x());
pt.setY(rat * pt1.y() + (1.0 - rat)*pt2.y());
return pt;
}
}
void PainterPath::smoothOut(const float& factor)
{
QList<QPointF> points;
QPointF p;
for (int i = 0; i < mPath->elementCount() - 1; i++) {
p = QPointF(mPath->elementAt(i).x, mPath->elementAt(i).y);
// Except for first and last points, check what the distance between two
// points is and if its less then min, don't add them to the list.
if (points.count() > 1 && (i < mPath->elementCount() - 2) && (distance(points.last(), p) < factor)) {
continue;
}
points.append(p);
}
// Don't proceed if we only have 3 or less points.
if (points.count() < 3) {
return;
}
QPointF pt1;
QPointF pt2;
QPainterPath* path = new QPainterPath();
for (int i = 0; i < points.count() - 1; i++) {
pt1 = getLineStart(points[i], points[i + 1]);
if (i == 0) {
path->moveTo(pt1);
} else {
path->quadTo(points[i], pt1);
}
pt2 = getLineEnd(points[i], points[i + 1]);
path->lineTo(pt2);
}
delete mPath;
mPath = path;
prepareGeometryChange();
}