Let There Be Shapes!

One of the new features of the upcoming Qt 5.10 is the introduction of the shapes plugin to Qt Quick. This allows adding stroked and filled paths composed of lines, quadratic curves, cubic curves, and arcs into Qt Quick scenes. While this has always been possible to achieve via QQuickPaintedItem or the Canvas type, the Shape type provides a genuine first-class Qt Quick item that from the scene graph's perspective is backed by either actual geometry or a vendor-specific GPU accelerated path rendering approach (namely, GL_NV_path_rendering).

shape_tiger
The shapes example, running on an Android tablet

Why is This Great?

  • There is no rasterization involved (no QImage, no OpenGL framebuffer object), which is excellent news for those who are looking for shapes spanning a larger area of a possibly high resolution screen, or want to apply potentially animated transformations to the shapes in the scene.
  • The API is fully declarative and every property, including stroke and fill parameters, path element coordinates, control points, etc., can be bound to in QML expressions and can be animated using the usual tools of Qt Quick. Being declarative also means that changing a property leads to recalculating only the affected sets of the underlying data, something that has been traditionally problematic with imperative painting approaches (e.g. QPainter).
  • There are multiple implementations under the hood, with the front Qt Quick item API staying the same. The default, generic solution is to reuse the triangulator from QPainter's OpenGL backend in QtGui. For NVIDIA GPUs there is an alternative path using the GL_NV_path_rendering OpenGL extension. When using the software renderer of Qt Quick, a simple backend falling back to QPainter will be used. This also leaves the door open to seamlessly adding other path rendering approaches in the future.

Status and Documentation

Right now the feature is merged to the dev branch of qtdeclarative and will be part of 5.10 onces it branches off. The documentation snapshots are online as well:

(due to some minor issues with the documentation system some types in Particles get cross-linked in the Inherited By section and some other places, just ignore this for now)

The canonical example is called shapes and it lives under qtdeclarative/examples/quick as expected.

Let's See Some Code

Without further ado, let's look at some code snippets. The path specification reuses existing types from PathView, and should present no surprises. The rest is expected to be fairly self-explanatory. (check the docs above)

1. A simple triangle with animated stroke width and fill color.

shape1


Shape {
    id: tri
    anchors.fill: parent

ShapePath { id: tri_sp strokeColor: "red" strokeWidth: 4 SequentialAnimation on strokeWidth { running: tri.visible NumberAnimation { from: 1; to: 20; duration: 2000 } NumberAnimation { from: 20; to: 1; duration: 2000 } } ColorAnimation on fillColor { from: "blue"; to: "cyan"; duration: 2000; running: tri.visible }

startX: 10; startY: 10 PathLine { x: tri.width - 10; y: tri.height - 10 } PathLine { x: 10; y: tri.height - 10 } PathLine { x: 10; y: 10 } } }

2. Let's switch over to dash strokes and disable fill. Unlike with image-backed approaches, applying transformations to shapes are no problem.

shape2


Shape {
    id: tri2
    anchors.fill: parent

ShapePath { strokeColor: "red" strokeWidth: 4 strokeStyle: ShapePath.DashLine dashPattern: [ 1, 4 ] fillColor: "transparent"

startX: 10; startY: 10 PathLine { x: tri2.width - 10; y: tri2.height - 10 } PathLine { x: 10; y: tri2.height - 10 } PathLine { x: 10; y: 10 } }

SequentialAnimation on scale { running: tri2.visible NumberAnimation { from: 1; to: 4; duration: 2000; easing.type: Easing.InOutBounce } NumberAnimation { from: 4; to: 1; duration: 2000; easing.type: Easing.OutBack } } }

3. Shape comes with full linear gradient support. This works exactly like QLinearGradient in the QPainter world.

shape3-png


Shape {
    id: tri3
    anchors.fill: parent

ShapePath { strokeColor: "transparent"

fillGradient: LinearGradient { x1: 20; y1: 20 x2: 180; y2: 130 GradientStop { position: 0; color: "blue" } GradientStop { position: 0.2; color: "green" } GradientStop { position: 0.4; color: "red" } GradientStop { position: 0.6; color: "yellow" } GradientStop { position: 1; color: "cyan" } }

startX: 10; startY: 10 PathLine { x: tri3.width - 10; y: tri3.height - 10 } PathLine { x: 10; y: tri3.height - 10 } PathLine { x: 10; y: 10 } }

NumberAnimation on rotation { from: 0; to: 360; duration: 2000 running: tri3.visible } }

4. What about circles and ellipses? Just use two arcs. (note: one ShapePath with two PathArcs is sufficient for a typical circle or ellipse, here there are two ShapePath due to the different fill parameters)

shape4


Shape {
    id: circle
    anchors.fill: parent
    property real r: 60

ShapePath { strokeColor: "transparent" fillColor: "green"

startX: circle.width / 2 - circle.r startY: circle.height / 2 - circle.r PathArc { x: circle.width / 2 + circle.r y: circle.height / 2 + circle.r radiusX: circle.r; radiusY: circle.r useLargeArc: true } } ShapePath { strokeColor: "transparent" fillColor: "red"

startX: circle.width / 2 + circle.r startY: circle.height / 2 + circle.r PathArc { x: circle.width / 2 - circle.r y: circle.height / 2 - circle.r radiusX: circle.r; radiusY: circle.r useLargeArc: true } } }

5. Speaking of arcs, PathArc is modeled after SVG elliptical arcs. Qt 5.10 introduces one missing property, xAxisRotation.

shape5


Repeater {
    model: 2
    Shape {
        anchors.fill: parent

ShapePath { fillColor: "transparent" strokeColor: model.index === 0 ? "red" : "blue" strokeStyle: ShapePath.DashLine strokeWidth: 4

startX: 50; startY: 100 PathArc { x: 150; y: 100 radiusX: 50; radiusY: 20 xAxisRotation: model.index === 0 ? 0 : 45 } } } }

Repeater { model: 2 Shape { anchors.fill: parent

ShapePath { fillColor: "transparent" strokeColor: model.index === 0 ? "red" : "blue"

startX: 50; startY: 100 PathArc { x: 150; y: 100 radiusX: 50; radiusY: 20 xAxisRotation: model.index === 0 ? 0 : 45 direction: PathArc.Counterclockwise } } } }

6. Quadratic and cubic Bezier curves work as expected. Below is a quadratic curve with its control point animated.

shape6


Shape {
    id: quadCurve
    anchors.fill: parent

ShapePath { strokeWidth: 4 strokeColor: "black" fillGradient: LinearGradient { x1: 0; y1: 0; x2: 200; y2: 200 GradientStop { position: 0; color: "blue" } GradientStop { position: 1; color: "green" } }

startX: 50 startY: 150 PathQuad { x: 150; y: 150 controlX: quadCurveControlPoint.x; controlY: quadCurveControlPoint.y } } }

Rectangle { id: quadCurveControlPoint color: "red" width: 10 height: 10 y: 20 SequentialAnimation on x { loops: Animation.Infinite NumberAnimation { from: 0 to: quadCurve.width - quadCurveControlPoint.width duration: 5000 } NumberAnimation { from: quadCurve.width - quadCurveControlPoint.width to: 0 duration: 5000 } } }

7. The usual join and cap styles, that are probably familiar from QPainter and QPen, are available.

shape7


Shape {
    anchors.fill: parent

ShapePath { strokeColor: "red" strokeWidth: 20 fillColor: "transparent" joinStyle: ShapePath.RoundJoin

startX: 20; startY: 20 PathLine { x: 100; y: 100 } PathLine { x: 20; y: 150 } PathLine { x: 20; y: 20 } }

ShapePath { strokeColor: "black" strokeWidth: 20 capStyle: ShapePath.RoundCap

startX: 150; startY: 20 PathCubic { x: 150; y: 150; control1X: 120; control1Y: 50; control2X: 200 SequentialAnimation on control2Y { loops: Animation.Infinite NumberAnimation { from: 0; to: 200; duration: 5000 } NumberAnimation { from: 200; to: 0; duration: 5000 } } } } }

Any Downsides?

Does this mean the time has finally come to add hundreds of lines and curves and arcs to every Qt Quick scene out there?

Not necessarily.

Please do consider the potential performance implications before designing in a large number of shapes in a user interface. See the notes in the Shape documentation page.

In short, the most obvious gotchas are the following:

  • [generic backend] Shapes with a lot of ShapePath child objects will take longer to generate the geometry. The good news is that this can be mitigated by setting the asynchronous property to true which, as the name suggests, leads to spawning off worker threads without blocking the main UI. This comes at the cost of the shape appearing only after the non-asynchronous UI elements.
  • [GL_NV_path_rendering backend] Geometry generation is a non-issue here, however due to the way the "foreign" rendering commands are integrated with the Qt Quick scene graph, having a large number of Shape items in a scene may not scale very well since, unlike plain geometry-based scenegraph nodes, these involve a larger amount of logic and OpenGL state changes. Note that one Shape with several ShapePath children is not an issue here since that is really just one node in the scenegraph.
  • Antialiasing is currently covered only through multi or super sampling, either for the entire scene or for layers. Note that Qt 5.10 introduces a very handy property here: layer.samples can now be used to enable using multisample renderbuffers, when supported.
  • Shape is not a replacement for rectangles and rounded rectangles provided by Rectangle. Rectangle will always perform better and can provide some level of smoothing even without multisampling enabled.

Nevertheless we expect Shape to be highly useful to a large number of Qt Quick applications. The Qt 5.10 release is due H2 this year, so...stay tuned!


Blog Topics:

Comments