Wt provides a vector graphics painting system, which depending on the browser support, uses inline SVG, inline VML, HTML5 <canvas> or a raster image to paint the graphics.
The Wt API provides different classes that provide support for vector
graphics painting. To use the paint system, you need to specialize
WPaintedWidget and use a WPainter to paint the
contents of the widget inside its WPaintedWidget::paintEvent()
.
In addition, a PDF backend is included in the library, which can be used
To use inline SVG, you need to enable XHTML support in your configuration
file (wt_config.xml
) by enabling send-xhtml-mimetype .
The painter class WPainter provides a vector graphics interface for painting. It has to be used in conjunction with a WPaintDevice, onto which it paints:
begin()
.
A typical use is to instantiate a WPainter
from within a
specialized WPaintedWidget::paintEvent()
implementation, to
paint on the given paint device, but you can also use a painter to paint
directly to a particular paint device of choice, for example to create
SVG, PDF or PNG images (as resources). A painted widget can dynamically
be updated as shown in the following example.
The painter maintains the state such a...
setPen()
,setBrush()
,
setFont()
,
worldTransform()
,setClipping()
and setClipPath()
).save()
and later restored
using restore()
.
The painting system distinguishes between different coordinate types:
device->width().toPixels(), device->height().toPixels()
for
the bottom right corner.translate()
,
rotate()
, and scale()
).
The device coordinates are defined in terms of pixels. Even though most
underlying devices are actual vector graphics formats, when used in
conjunction with a WPaintedWidget
, these vector graphics are
rendered by the browser onto a pixel-based canvas (like the rest of the
user-interface). The coordinates are defined such that integer values
correspond to an imaginary raster which separates the individual pixels,
as in the figure below.
The device coordinate system for a 6x5 pixel device
As a consequence, to avoid anti-aliasing effects when drawing straight lines of width one pixel, you will need to use vertices that indicate the middle of a pixel to get a crisp one-pixel wide line, as shown in the above picture.
You can map logical coordinates onto device coordinates by setting a
viewPort()
and a window()
; this defines a viewPort
transformation.
You can define how the current local coordinates map onto
logical coordinates by changing the world transformation using
translate()
, rotate()
, scale()
and
setWorldTransform()
operations.
The painter provides support for clipping using an arbitrary WPainterPath. Please note that the WVmlImage paint device only has limited support for clipping.
#include <Wt/WBrush.h>
#include <Wt/WColor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
#include <Wt/WPainterPath.h>
#include <Wt/WPen.h>
#include <Wt/WPointF.h>
#include <Wt/WSpinBox.h>
class ShapesWidget : public Wt::WPaintedWidget
{
public:
ShapesWidget()
: WPaintedWidget()
{
resize(310, 400); // Provide a default size.
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
painter.setPen(Wt::WColor(Wt::StandardColor::Red));
// DRAWING PRIMITIVE SHAPES
// Draw a line from (0, 0) to (200, 0) and then 30 px downwards.
painter.drawLine(0, 0, 200, 0);
painter.drawLine(200, 0, 200, 30);
// Draw and fill a rectangle from (0, 25) with width 80 and height 25
// using the current pen (red) and brush (default: white).
painter.drawRect(0, 25, 80, 50);
// Set the current brush with the global color name 'green'.
painter.setBrush(Wt::WBrush(Wt::WColor(Wt::StandardColor::Green)));
// Draw the same rectangle from (100, 25) using the current pen and brush.
painter.drawRect(100, 25, 80, 50);
// Fill a rectangle from (100, 25) with width 80 and height 25.
// Choose a color with red=0, green=255, blue=0, alpha=64.
painter.fillRect(220, 25, 80, 50, Wt::WBrush(Wt::WColor(0, 255, 0, 64)));
// Draw the outline of an ellipse.
painter.drawEllipse(0, 100, 80, 50);
// Draw the upper segment of the ellipse (angle = 180 x 1/16th of a degree)
painter.drawChord(100, 100, 80, 50, 0, 180*16);
// Draw an open arc and a closed arc.
painter.drawArc(220, 100, 50, 50, 90*16, 90*16);
painter.drawArc(240, 100, 50, 50, 0, 90*16);
painter.drawLine(265, 100, 265, 125);
painter.drawLine(265, 125, 290, 125);
// Draw a 6-point polygon and fill it with the current brush.
const Wt::WPointF points[]
= { Wt::WPointF(120, 170), Wt::WPointF(160, 170),
Wt::WPointF(180, 204.6), Wt::WPointF(160, 239.2),
Wt::WPointF(120, 239.2), Wt::WPointF(100, 204.6) };
painter.drawPolygon(points, 6);
// DRAWING SHAPES USING A PATH
// Create an ellipse path and fill it.
Wt::WPainterPath filledEllipsePath = Wt::WPainterPath();
filledEllipsePath.addEllipse(0, 180, 80, 50);
filledEllipsePath.closeSubPath();
painter.drawPath(filledEllipsePath);
// Create a new path for a triangle.
Wt::WPainterPath filledTrianglePath = Wt::WPainterPath();
filledTrianglePath.moveTo(0, 270);
filledTrianglePath.lineTo(80,270);
filledTrianglePath.lineTo(0, 350);
filledTrianglePath.closeSubPath();
// Draw the path and fill it.
painter.drawPath(filledTrianglePath);
// strokePath draws a path but doesn't fill it.
Wt::WPainterPath strokedTrianglePath = Wt::WPainterPath();
strokedTrianglePath.moveTo(100,270);
strokedTrianglePath.lineTo(100,350);
strokedTrianglePath.lineTo(20, 350);
strokedTrianglePath.closeSubPath();
Wt::WPen pen = Wt::WPen();
pen.setWidth(3);
painter.strokePath(strokedTrianglePath, pen);
// Draw a balloon with quadratic bezier curves.
Wt::WPainterPath quadraticCurvePath = Wt::WPainterPath();
quadraticCurvePath.moveTo(250,150);
quadraticCurvePath.quadTo(200,150, 200,187.5);
quadraticCurvePath.quadTo(200,225, 225,225);
quadraticCurvePath.quadTo(225,245, 205,250);
quadraticCurvePath.quadTo(235,245, 240,225);
quadraticCurvePath.quadTo(300,225, 300,187.5);
quadraticCurvePath.quadTo(300,150, 250,150);
painter.strokePath(quadraticCurvePath, pen);
// Draw a heart with cubic bezier curves.
Wt::WPainterPath bezierCurvePath = Wt::WPainterPath();
bezierCurvePath.moveTo( 255,285);
bezierCurvePath.cubicTo(255,282, 250,270, 230,270);
bezierCurvePath.cubicTo(200,270, 200,307.5,200,307.5);
bezierCurvePath.cubicTo(200,325, 220,357, 255,365);
bezierCurvePath.cubicTo(290,347, 310,325, 310,307.5);
bezierCurvePath.cubicTo(310,307.5,310,270, 290,270);
bezierCurvePath.cubicTo(265,270, 255,282, 255,285);
painter.setBrush(Wt::WBrush(Wt::WColor(Wt::StandardColor::Red)));
painter.drawPath(bezierCurvePath);
}
};
auto container = std::make_unique<Wt::WContainerWidget>();
container->addNew<ShapesWidget>();
Besides the different transformation methods which you can apply to a drawing (i.e. translate, rotate, and scale), there are also two other methods which are indispensable once you start generating more complex drawings:
WPainter::save()
to save the current painter state on a
stack, and
WPainter::restore()
to restore a painter state from the
stack.
WPainter::setClipping()
and
WPainter::setClipPath()
).
The method WPainter::translate()
translates the origin of the
logical coordinate system to a new location relative to the current
logical coordinate system. It's a good practice to save the painter
state before doing any transformations because usually, it's easier to
call restore()
compared to a reverse translation to return to
the original state. In addition, if you're translating in a loop you might
end up missing a part of your drawing because it was drawn outside the
paint device's edge.
The method WPainter::rotate()
rotates the logical coordinate
system around its origin by an angle. The angle is specified in degrees,
and positive values are clockwise. Use this method to rotate an object
around its origin before it is drawn on the paint device.
The method WPainter::scale()
scales the logical coordinate
system around its origin, by a factor in the X and Y directions.
Use this method to scale what you draw on the paint device.
#include <Wt/WBrush.h>
#include <Wt/WColor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
class TransformationsWidget : public Wt::WPaintedWidget
{
public:
TransformationsWidget()
: WPaintedWidget()
{
resize(300, 500); // Provide a default size.
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
painter.setPen(Wt::WPen(Wt::WColor(Wt::StandardColor::Red)));
painter.setBrush(Wt::WBrush(Wt::WColor(Wt::StandardColor::Black)));
// SAVE AND RESTORE CANVAS STATE EXAMPLE
painter.save();
painter.setPen(Wt::WPen(Wt::WColor(Wt::StandardColor::White)));
// Draw and fill a rectangle with the current brush.
painter.drawRect(0,0,100,100);
painter.save(); // Save the canvas state on a stack.
painter.setBrush(Wt::WBrush(Wt::WColor(Wt::StandardColor::Yellow))); // Change the fill style.
// Draw a rectangle with the current settings.
painter.drawRect(10,10,80,80);
painter.save(); // Save the current canvas state.
painter.setBrush(Wt::WBrush(Wt::WColor(Wt::StandardColor::Red))); // Change the fill style.
painter.drawRect(20,20,60,60);
painter.restore(); // Restore the previous canvas state.
painter.drawRect(30,30,40,40);
painter.restore(); // Restore the original canvas state.
painter.drawRect(40,40,20,20);
painter.restore(); // Restore the original pen.
// TRANSLATING
for (int i = 0; i < 2; i++) {
painter.save();
painter.translate(i*100, 130);
drawFilledPolygon(painter, Wt::WColor(0,255,0, 255 - i*200));
painter.restore();
}
// ROTATING
painter.translate(0, 300); // Translate the origin for a Y-offset of rings
painter.save();
painter.translate(90, 0); // Translate the origin for a X-offset of rings
for (int ring = 1; ring < 6; ring++) { // Loop through rings
painter.save();
painter.setBrush(Wt::WBrush(Wt::WColor(51*ring, (255-51*ring), 255)));
for (int j = 0; j < ring * 6; j++) { // Draw individual dots:
painter.rotate(360 / (ring*6)); // 1. Rotate full circle in ring*6 steps
painter.drawEllipse(0, ring*12.5, 10, 10); // 2. Draw a dot.
}
painter.restore();
}
painter.restore(); // Reset the X-offset.
// SCALING
painter.save();
painter.translate(0,100);
drawFilledPolygon(painter, Wt::WColor(0,255,0, 255)); // no scaling.
painter.translate(100,0);
painter.scale(1.2, 1); // with X scaling
drawFilledPolygon(painter, Wt::WColor(0,255,0, 55));
painter.restore();
}
private:
void drawFilledPolygon(Wt::WPainter &painter, const Wt::WColor& color) {
painter.setBrush(color);
const Wt::WPointF points[]
= { Wt::WPointF(20, 0), Wt::WPointF(60, 0),
Wt::WPointF(80, 34.6), Wt::WPointF(60, 69.2),
Wt::WPointF(20, 69.2), Wt::WPointF(0, 34.6),
Wt::WPointF(20, 0) };
painter.drawPolygon(points, 6);
}
};
auto container = std::make_unique<Wt::WContainerWidget>();
container->addNew<TransformationsWidget>();
Use the method WPainter::resetTransform()
to reset the current
transformation to the identity transformation matrix, so that the logical
coordinate system coincides with the device coordinate system.
A clipping path is like a normal WPainterPath but it acts as mask to hide unwanted parts of shapes. The path is specified in local coordinates.
You can set a clipping path with the method setClipPath()
.
To apply clipping you still have to enable clipping using
setClipping(true)
.
#include <Wt/WBrush.h>
#include <Wt/WCalendar.h>
#include <Wt/WColor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
#include <algorithm>
#include <cstdlib>
class ClippingWidget : public Wt::WPaintedWidget
{
public:
ClippingWidget()
: WPaintedWidget()
{
resize(310, 150); // Provide a default size.
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
for (int i = 0; i < 2; i++) { // Create two separate drawings.
painter.translate(i*160, 0);
// Draw the background
painter.fillRect(0, 0, 150, 150, Wt::WBrush(Wt::WColor(Wt::StandardColor::Black)));
// Create a path and fill it with blue.
Wt::WPainterPath path;
path.addEllipse(15, 15, 120, 120);
painter.fillPath(path, Wt::WBrush(Wt::WColor(Wt::StandardColor::Blue)));
// Use the previously defined path also for clipping.
painter.setClipPath(path);
painter.setClipping(i != 0); // Clipping is applied from the 2nd drawing.
drawStars(painter);
}
}
private:
void drawStar(Wt::WPainter& painter, double radius) {
painter.save();
Wt::WPainterPath circlePath;
circlePath.addEllipse(0, 0, radius, radius);
circlePath.closeSubPath();
painter.fillPath(circlePath, Wt::WBrush(Wt::WColor(Wt::StandardColor::White)));
painter.restore();
}
void drawStars(Wt::WPainter& painter) {
std::srand(Wt::WDateTime::currentDateTime().toTime_t());
painter.save();
painter.translate(75,75);
for (int star = 1; star < 50; star++){
painter.save();
painter.translate(75 - std::rand() % 150 + 1,
75 - std::rand() % 150 + 1);
drawStar(painter, std::max(0, std::rand() % 4) + 2);
painter.restore();
}
painter.restore();
}
};
auto container = std::make_unique<Wt::WContainerWidget>();
container->addNew<ClippingWidget>();
Until now we used a limited number of line and fill styles. Now we will explore other options to make a drawing more attractive by examples.
PenCapStyle
(flat, square or round)
PenJoinStyle
(miter, bevel or round)
strokePath()
#include <Wt/WBrush.h>
#include <Wt/WColor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
class StyleWidget : public Wt::WPaintedWidget
{
public:
StyleWidget()
: WPaintedWidget()
{
resize(310, 1140); // Provide a default size.
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
// Draw a grid of rectangles; each one in a different color.
for (int row = 0; row < 6; row++)
for (int col = 0; col < 6; col++) {
// Generate a unique RGB color for each square. Only the red and
// green values are modified; the blue channel has a fixed value.
Wt::WBrush brush(Wt::WColor(255 - 42*row, 255 - 42*col, 0));
painter.fillRect(row*25, col*25, 25, 25, brush);
}
painter.translate(0, 160);
// Draw a grid of circles similar to the above example but now using the
// strokePath method.
Wt::WPen pen;
pen.setWidth(3);
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 6; col++) {
// Generate a unique RGB color for each circle. Only the green and
// blue values are modified; the red channel has a fixed value.
Wt::WPainterPath path;
path.addEllipse(3 + col*25, 3 + row*25, 20, 20);
pen.setColor(Wt::WColor(0, 255 - 42*row, 255 - 42*col));
painter.strokePath(path, pen);
}
}
painter.translate(0, 160);
// Transparency example with rectangles
// Create a background composed of 4 stacked rectangles.
painter.fillRect(0, 0, 150, 37.5, Wt::WBrush(Wt::WColor(Wt::StandardColor::Yellow)));
painter.fillRect(0, 37.5, 150, 37.5, Wt::WBrush(Wt::WColor(Wt::StandardColor::Green)));
painter.fillRect(0, 75, 150, 37.5, Wt::WBrush(Wt::WColor(Wt::StandardColor::Blue)));
painter.fillRect(0, 112.5, 150, 37.5, Wt::WBrush(Wt::WColor(Wt::StandardColor::Red)));
// On top of these draw semi transparent rectangles with increasing opacity
for (int i = 0; i < 10; i++) {
Wt::WBrush brush = Wt::WBrush(Wt::WColor(255, 255, 255, 255/10*i));
for (int j = 0; j < 4; j++) {
Wt::WPainterPath path;
path.addRect(5 + i*14, 5 + j*37.5, 14, 27.5);
painter.fillPath(path, brush);
}
}
painter.translate(0, 160);
// Transparency example with circles
// Create a square composed of four different colored squares.
painter.fillRect(0, 0, 75, 75, Wt::WBrush(Wt::WColor(Wt::StandardColor::Yellow)));
painter.fillRect(75, 0, 75, 75, Wt::WBrush(Wt::WColor(Wt::StandardColor::Green)));
painter.fillRect(0, 75, 75, 75, Wt::WBrush(Wt::WColor(Wt::StandardColor::Blue)));
painter.fillRect(75, 75, 75, 75,Wt::WBrush(Wt::WColor(Wt::StandardColor::Red)));
// On top of these draw a set of semi-transparant white circles with
// increasing diameter. The final result is a radial gradient.
for (int i = 1; i < 8; i++) {
Wt::WPainterPath path;
path.addEllipse(75 - i*10, 75 - i*10, i*20, i*20);
Wt::WBrush brush = Wt::WBrush(Wt::WColor(255, 255, 255, 50));
painter.fillPath(path, brush);
}
painter.translate(0, 170);
// Gradient Brush example
// Rectangle with a linear gradient from left top to right bottom
painter.setPen(Wt::WPen(Wt::PenStyle::None));
Wt::WGradient linGrad;
linGrad.setLinearGradient(0, 0, 100, 150);
linGrad.addColorStop(0, Wt::WColor(255, 0, 0, 255));
linGrad.addColorStop(0.5, Wt::WColor(0, 0, 255, 255));
linGrad.addColorStop(1, Wt::WColor(0, 255, 0, 255));
Wt::WBrush linearGradientBrush(linGrad);
painter.setBrush(linearGradientBrush);
painter.drawRect(0, 0, 100, 150);
// Circle with a radial gradient
Wt::WGradient radGrad;
radGrad.setRadialGradient(170, 100, 50, 130, 130);
radGrad.addColorStop(0.2, Wt::WColor(255, 0, 0, 255));
radGrad.addColorStop(0.9, Wt::WColor(0, 0, 255, 255));
radGrad.addColorStop(1, Wt::WColor(0, 0, 255, 0));
Wt::WBrush radialGradientBrush(radGrad);
painter.setBrush(radialGradientBrush);
painter.drawEllipse(120, 50, 100, 100);
painter.translate(0, 170);
// LineWidth example
// You can use WPainter::drawLine() or WPainter::strokePath() to draw a
// line. Using strokePath() you can draw thicker lines.
// The line is centered on the path. In other words, the area that's drawn
// extends to half the line width on either side of the path. Because
// canvas coordinates do not directly reference pixels, you have to take
// special care to obtain crisp horizontal and vertical lines.
// All lines with an odd integer width thickness in the example below do
// not appear crisp, because of the path's positioning.
for (int i = 0; i < 11; i++) {
Wt::WPainterPath path;
path.moveTo(i*14, 0);
path.lineTo(i*14, 150);
pen = Wt::WPen();
pen.setWidth(i+1);
painter.strokePath(path, pen);
}
painter.translate(160, 0);
// LineWidth example with crisp lines
// To obtain a crisp line for an odd integer width thickness line you have
// to be very precise in your path creation, e.g. a 1.0 width line will
// extend half a unit to either side of the path.
for (int i = 0; i < 11; i++) {
Wt::WPainterPath path;
if (i % 2 == 0) {
path.moveTo(i*14-0.5, 0);
path.lineTo(i*14-0.5, 150);
} else {
path.moveTo(i*14, 0);
path.lineTo(i*14, 150);
}
pen = Wt::WPen();
pen.setCapStyle(Wt::PenCapStyle::Flat); // Now, all lines will have equal length.
pen.setWidth(i+1);
painter.strokePath(path, pen);
}
painter.translate(-160, 170);
// PenCapStyle example
// The PenCapStyle can be FlatCap, SquareCap or RoundCap.
// Start with drawing guides:
Wt::WPainterPath guidePath;
guidePath.moveTo(0, 10);
guidePath.lineTo(150,10);
guidePath.moveTo(0,140);
guidePath.lineTo(150,140);
pen = Wt::WPen(Wt::WColor(Wt::StandardColor::Blue));
painter.strokePath(guidePath, pen);
// Draw lines with different cap styles
// Create three parallel paths:
std::vector<Wt::WPainterPath> paths;
for (int i = 0; i < 3; i++) {
Wt::WPainterPath path;
path.moveTo(25+i*50, 10);
path.lineTo(25+i*50, 140);
paths.push_back(path);
}
pen = Wt::WPen();
pen.setWidth(20);
pen.setCapStyle(Wt::PenCapStyle::Flat);
painter.strokePath(paths[0], pen);
pen = Wt::WPen();
pen.setWidth(20);
pen.setCapStyle(Wt::PenCapStyle::Square);
painter.strokePath(paths[1], pen);
pen = Wt::WPen();
pen.setWidth(20);
pen.setCapStyle(Wt::PenCapStyle::Round);
painter.strokePath(paths[2], pen);
painter.translate(0, 170);
// PenJoinStyle example
// The PenJoinStyle can be MiterJoin, BevelJoin or RoundJoin.
// Create three parallel paths:
paths.clear();
for (int i = 0; i < 3; i++) {
Wt::WPainterPath path;
path.moveTo(15, 5+i*40);
path.lineTo(45, 45+i*40);
path.lineTo(75, 5+i*40);
path.lineTo(105,45+i*40);
path.lineTo(135, 5+i*40);
paths.push_back(path);
}
// Draw the first path with miter joins.
// The connected segments are joined by extending their outside edges to
// connect at a single point.
pen = Wt::WPen();
pen.setWidth(20);
pen.setJoinStyle(Wt::PenJoinStyle::Miter);
painter.strokePath(paths[0], pen);
// Draw the second path with bevel joins.
// An additional triangular area is filled between the common endpoint of
// connected segments and the separate outside rectangular corners of each
// segment.
pen = Wt::WPen();
pen.setWidth(20);
pen.setJoinStyle(Wt::PenJoinStyle::Bevel);
painter.strokePath(paths[1], pen);
// Draw the third path with round joins.
// The corners of the shape are rounded off by filling an aditonal sector
// of disc centered at the common endpoint of connected segments. The
// radius of the rounded corners is equal to the line width.
pen = Wt::WPen();
pen.setWidth(20);
pen.setJoinStyle(Wt::PenJoinStyle::Round);
painter.strokePath(paths[2], pen);
}
};
auto container = std::make_unique<Wt::WContainerWidget>();
container->addNew<StyleWidget>();
One of the more fun features of WPainter is that you can
render one or more images on a painting using the method
drawImage()
. Images can be used for dynamic photo compositing or
as a background for a drawing (e.g. with shapes, graphs, etc.).
An image is specified in terms of a URL, and the width and height.
An image can be drawn in different ways:
#include <Wt/WBrush.h>
#include <Wt/WColor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
class PaintingImagesWidget : public Wt::WPaintedWidget
{
public:
PaintingImagesWidget()
: WPaintedWidget()
{
resize(639, 1310); // Provide a default size.
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
Wt::WPainter::Image image("pics/sintel_trailer.jpg", 639, 354);
painter.drawImage(0.0, 0.0, image);
// Draw a part of the same image starting at (110, 75) and put it at
// (0, 364).
painter.drawImage(0.0, 364.0, image, 110.0, 75.0, 130.0, 110.0);
// Draw the same part using WPointF for the starting point and WRectF for
// the source rectangle.
Wt::WPointF location = Wt::WPointF(0.0, 484.0);
Wt::WRectF sourceRect = Wt::WRectF(110.0, 75.0, 130.0, 110.0);
painter.drawImage(location, image, sourceRect);
// Draw the image in a rectangle.
Wt::WRectF destinationRect = Wt::WRectF(0.0, 604.0, 130.0, 110.0);
painter.drawImage(destinationRect, image);
// Draw a part of the image in a rectangle - scaling down
sourceRect = Wt::WRectF(60.0, 80.0, 220.0, 180.0);
destinationRect = Wt::WRectF(0.0, 724.0, 130.0, 110.0);
painter.drawImage(destinationRect, image, sourceRect);
// Draw a part of the image in a rectangle - scaling up
sourceRect = Wt::WRectF(294.0, 226.0, 265.0, 41.0);
destinationRect = Wt::WRectF(0.0, 844.0, 639.0, 110.0);
painter.drawImage(destinationRect, image, sourceRect);
painter.translate(0,964);
// Draw the image and add shapes to it.
painter.drawImage(0.0, 0.0, image);
Wt::WPainterPath path;
path.addEllipse(369, 91, 116, 116);
path.addRect(294, 226, 265, 41);
path.moveTo(92,330);
path.lineTo(66,261);
path.lineTo(122,176);
path.lineTo(143,33);
path.lineTo(164,33);
path.lineTo(157,88);
path.lineTo(210,90);
path.lineTo(263,264);
path.lineTo(228,330);
path.lineTo(92,330);
Wt::WPen pen = Wt::WPen(Wt::WColor(Wt::StandardColor::Red));
pen.setWidth(3);
painter.strokePath(path, pen);
}
};
auto container = std::make_unique<Wt::WContainerWidget>();
container->addNew<PaintingImagesWidget>();
In order to avoid server round-trips, it's possible to change some of the properties of a WPaintedWidget and redraw it entirely on the client side. The interactive features of WCartesianChart use this functionality, for example.
#include <Wt/WContainerWidget.h>
#include <Wt/WJavaScript.h>
#include <Wt/WPaintDevice.h>
#include <Wt/WPaintedWidget.h>
#include <Wt/WPainter.h>
#include <Wt/WSlider.h>
#include <Wt/WSpinBox.h>
#include <cmath>
#define M_PI 3.14159265358979323846
class PaintingInteractiveWidget : public Wt::WPaintedWidget
{
public:
PaintingInteractiveWidget()
: WPaintedWidget(),
rotateSlot(1, this)
{
resize(300, 300); // Provide a default size.
transform = createJSTransform(); // Create a client side transform
rotateSlot.setJavaScript(
"function(o,e,deg) {"
"if (" + objJsRef() + ") {"
"var rad = deg / 180 * Math.PI;"
"var c = Math.cos(rad);"
"var s = Math.sin(rad);" +
// You can assign a new 6 element array to a transform,
// or change its individual elements.
transform.jsRef() + " = [c,-s,s,c,0,0];" +
repaintSlot().execJs() + ";"
"}"
"}");
}
void rotate(int degrees)
{
double radians = degrees / 180.0 * M_PI;
double c = std::cos(radians);
double s = std::sin(radians);
// Changes in value will be synced from server to client
transform.setValue(Wt::WTransform(c, -s, s, c, 0, 0));
update();
}
Wt::JSlot rotateSlot;
protected:
void paintEvent(Wt::WPaintDevice *paintDevice) {
Wt::WPainter painter(paintDevice);
painter.translate(150, 150);
// Set a 5px wide pen
Wt::WPen pen;
pen.setWidth(5);
painter.setPen(pen);
// Draw an arrow shape
Wt::WPainterPath path;
path.moveTo(-50,100);
path.lineTo(50,100);
path.lineTo(50,20);
path.lineTo(100,20);
path.lineTo(0,-100);
path.lineTo(-100,20);
path.lineTo(-50, 20);
path.lineTo(-50,100);
path.lineTo(50, 100);
// Client side transforms can be set with
// setWorldTransform, or applied to a path
// with the map() function. In the latter case,
// line thickness will not be affected by the
// transform.
Wt::WPainterPath transformedPath =
transform.value().map(path);
painter.drawPath(transformedPath);
}
private:
Wt::WJavaScriptHandle<Wt::WTransform> transform;
};
auto container = std::make_unique<Wt::WContainerWidget>();
PaintingInteractiveWidget *widget = container->addNew<PaintingInteractiveWidget>();
Wt::WSpinBox *sb = container->addNew<Wt::WSpinBox>();
sb->setWidth(300);
sb->setRange(0, 360);
sb->setValue(0);
Wt::WSlider *slider = container->addNew<Wt::WSlider>(Wt::Orientation::Horizontal);
slider->resize(300, 50);
slider->setRange(0, 360);
// This will not cause a server roundtrip
slider->sliderMoved().connect(widget->rotateSlot);
sb->valueChanged().connect([=] {
slider->setValue(sb->value());
widget->rotate(sb->value());
});
sb->enterPressed().connect([=] {
slider->setValue(sb->value());
widget->rotate(sb->value());
});
slider->valueChanged().connect([=] {
sb->setValue(slider->value());
widget->rotate(slider->value());
});