Back to Blog home

Let There Be Shapes!

Published on Friday July 07, 2017 by Laszlo Agocs in Declarative UI Qt Quick Graphics Dev Loop OpenGL UI Painting | Comments

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!

Subscribe to Our Blog

Stay up to date with the latest marketing, sales and service tips and news.