The problem with using PathCurve is that it constructs a curve that always passes through all points, by using "symmetrical" Bézier curves.
Interestingly enough, your original question mentioned trying to get a Catmull-Rom spline, which is a possible case of Cubic Hermite spline, which, in turn, like most cubic splines, is often considered as a conceptual synonim of Bézier cubic curves.
Simply putting four points in a "Z" shape won't be appropriate, though, especially if you want a specific orientation for the beginning and ending of the path (horizontal, in this case).
Consider the following example:

Even though the first and last two points are placed at the same y coordinate, the curve begins and ends by going a bit above or below that vertical position. Such a curve is conceptually optimal (it often has the shorter or more effective "smooth" path between a set of given points) but it's not usually preferable for UI purposes.
It would be tempting to put the second point a bit lower and the third a bit higher, but it won't improve the situation. Here is an example that also shows the control points used to make the curve to "smoothly" pass through all given points:

The control points of each curve are placed perpendicularly to the angle formed between the previous and following point, and considering the distance between them. The following image shows the correlation:

Even trying to add more points won't solve the problem:

And here is the same path showing how the control points are placed:

Even though it would be possible to compute the position of the intermediate points so that the curve immediately goes on the "right" direction, such computations are all but immediate, as they require considering the involved angles and lengths between each segment and then doing a "reverse" computation to get back the coordinates from the possible control points.
A more appropriate solution would be to consider the possibility of using PathQuad (which results in a quadratic Bézier, having a single control point) or PathCubic (for a cubic Bézier, with two control points), possibly considering the possibility of adding PathLine elements for straight lines before, between and after curves.
In reality, most of the times using two cubic curves is simpler and gets the best results, the only problem is to properly compute the position of the control points.
For a "node-connection" system, it's normal to have a curve going on the right of the starting point and ending on the left of the target, so we can actually make a path made of two PathLines (for the extremities) and two PathCubic:

The first curve starts from the second point (after the first PathLine), and goes exactly in the middle of the original two points, while the second curve does the opposite. The control points for each curve are vertically aligned to the y coordinate of the start/end points and the middle, and are horizontally placed to some "extent" going outside the margin.
It's important to realize that it's usually not enough to just use the relative positions from the origin (start/end) points, because if the start is on the left of the end, the aligned control points will create an oddly shaped curve:

Therefore, as soon as the first control point of the first curve and the second of the last are horizontally far apart, the orientation of the other control points must be swapped:

Note that, in reality, a curve similar to the above could be drawn with a single cubic Bézier:

But since we have a path with predefined element types, we need to make an interpolation of the two existing curves, even if we don't really need them; this has the benefit of having more control over the shape of each curve.
The structure is then the following:
- a PathLine that, from the beginning of the path, goes on the right by a certain amount;
- a PathCubic that always targets the middle of the start and end point, with its first control point at the same Y of the start and its X further on the right of the above line, and the second control point aligned depending on the curve;
- a PathCubic that is the opposite of the above, targeting the point where the final line would begin (at the left of the end), and with the control points symmetrically reversed to the first curve: the first cp is the "mirrored" version of the second of the previous curve (with the center as reference), while the second is horizontally positioned opposite to the target and vertically aligned to the end;
- a PathLine that implicitly starts at the left of the end of the path (where the second PathCubic ends) and ends with the final point, as opposed to the first line;
Remember that ShapePath inherits from Path, which behaves like QPainterPath: every new "relative draw" element (lines, curves) always implies that it begins at the previous point in the path (with 0, 0 implied as its start), with the exception of "polygons" (rectangles, non-regular polygons and ellipses); therefore a PathLine only specifies its "end point" and always assumes its start as the previous point in the path, just like elements such as PathQuad, PathCubic, PathCurve, etc. do.
Declaring all these values within each element is possible, but a lot of computations and checks would be unnecessarily done more than once, leading to a lot of boilerplate code that would affect performance. Using a function is much simpler, because the above computations/checks can be done preliminarly and be kept as local variables or provide immediate assignment to avoid checking them again and again; that approach also allows to make some fine-tuning to the curve variables which would be otherwise more difficult and riskier to maintain.
In the following example, the start and end points have been made movable by adding a MouseArea, and the updatePath() function is called every time one of them is moved (other than on start up).
The extend variable indicates the extent of the horizontal lines on the right of the first element and the left of the last, while the minCurve is used as reference for the control point position. You can obviously play around with those values to see the differences.
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
Page {
property int extend: 45
property int minCurve: 15
function updatePath() {
// coordinates of the "center" between the start and end points
var midX = portPath.startX + (portB.x - portA.x) / 2
var midY = portPath.startY + (portB.y - portA.y) / 2
// the theoretical horizontal position of the control points used as
// reference: the 1st cp of the start curve, and the 2nd of the end
var startCp = portPath.startX + extend + minCurve
var endCp = endLine.x - extend - minCurve
startCurve.x = midX
startCurve.y = midY
if (startCp < endCp) {
// the theoretical startCp is on the *left* of endCp, so we
// should have a path similar to the last example above;
// the other control points are placed at the same Y of the
// start or end points; the "dist" variable is considered in case
// the two control points are far apart enough, providing a more
// "smooth" slope in the initial and final parts of the curves
var dist = (endLine.x - portPath.startX) / 2 - extend
if (dist < minCurve) { dist = minCurve }
startCurve.control1X = midX - dist
startCurve.control2X = midX
endCurve.control1X = midX
endCurve.control2X = midX + dist
startCurve.control2Y = portPath.startY
endCurve.control1Y = endCurve.control2Y
} else {
// the opposite, where the 1st cp of the start curve is on the
// *right* of the 2nd cp of the end curve; the "diff" variable
// is conceptually similar to the "dist" above
var diff = (startCp - endCp) / 25
startCurve.control1X = startCp + diff
startCurve.control2X = startCurve.control1X
endCurve.control1X = endCurve.control2X = endCp - diff
startCurve.control2Y = endCurve.control1Y = midY
}
}
Shape
{
Rectangle
{
id: portA;
x: 500; y: 200
width: 14; height: 14;
radius: 7;
color: "#29b6f6"
MouseArea {
anchors.fill: parent
drag.target: parent
drag.threshold: 0
onPositionChanged: updatePath()
}
}
Rectangle
{
id: portB;
x: 220; y: 300
width: 14; height: 14;
radius: 7;
color: "#29b6f6"
MouseArea {
anchors.fill: parent
drag.target: parent
drag.threshold: 0
onPositionChanged: updatePath()
}
}
ShapePath {
id: portPath
strokeWidth: 3
strokeColor: "darkgray"
fillColor: "transparent"
startX: portA.x + portA.width / 2
startY: portA.y + portA.height / 2
PathLine {
id: startLine
x: portPath.startX + extend
y: portPath.startY
}
PathCubic {
id: startCurve
control1Y: portPath.startY
}
PathCubic {
id: endCurve
x: portB.x + portB.width / 2 - extend
y: portB.y + portB.height / 2
control2Y: y
}
PathLine {
id: endLine
x: portB.x + portB.width / 2
y: endCurve.y
}
}
}
Component.onCompleted: updatePath()
}
Here is how the above will show on start up:

And when swapping the horizontal position:

Thanks to Stephen Quan's repository, you can also Try it Online!
PathCurvesmoothly connects all points by creating symmetrical quadratic curves that smoothly connect points based on the angle between iterations every three consecutive points (except for the start and end): the control points are aligned on a line that is perpendicular to that angle, which is why the first and last points result in oddly shaped curves and the middle part is oblique. In order to get the result you want, you should compose the path by using a mix ofPathLineandPathQuadobjects instead.