Path

Path is by far the most powerful shape. All of the shapes we have looked at up to now have been supplied for convenience, because it is possible to draw all of them with a Path. Path also makes it possible to draw considerably more complex shapes than is possible with the previous shapes we have seen.

As mentioned earlier, the various classes derived from Shape are essentially high-level wrappers around underlying geometry objects. Path is explicit about this—its shape is defined by its Data property, which is of type Geometry. As we saw in the sidebar earlier, a Geometry object describes a particular shape. Table 13-3 shows the various concrete classes for representing different kinds of shapes.

* WPF doesn't document whether the positive direction is clockwise or counterclockwise. This is because it doesn't matter—as long as you are consistent, the final outcome is the same either way.

Table 13-3. Geometry types

Type

CombinedGeometry

EllipseGeometry

GeometryGroup

LineGeometry

PathGeometry

RectangleGeometry

StreamGeometry

Usage

Combines two geometry objects using set operations such as intersection or union An ellipse

Combines multiple geometries into one multifigure geometry A single straight line

Defines shapes with any combination of straight lines, elliptical arcs, and Bezier curves A rectangle

More efficient alternative to PathGeometry—can define all the same shapes, but cannot modify the shapes after creation

Three geometry types—RectangleGeometry, EllipseGeometry, and LineGeometry— correspond to the Rectangle, Ellipse, and Line shape types shown earlier. So this Rectangle:

<Rectangle Fill="Blue" Width="40" Height="80" />

is effectively shorthand for this Path:

<Path Fill="Blue"> <Path.Data>

<RectangleGeometry Rect="0, 0, 40, 80" /> </Path.Data> </Path>

You might be wondering when you would ever use the RectangleGeometry, EllipseGeometry, or LineGeometry in a Path instead of the simpler Rectangle, Ellipse, and Line. One reason is that Path lets you use a special kind of geometry object called a GeometryGroup to create a shape with multiple geometries.

There is a significant difference between using multiple distinct shapes, and having a single shape with multiple geometries. Look at Example 13-17, for instance.

Example 13-17. Two Ellipse elements <Canvas>

<Ellipse Fill="Cyan" Stroke="Black" Width="40" Height="80" /> <Ellipse Canvas.Left="10" Canvas.Top="10" Fill="Cyan" Stroke="Black"

This draws two ellipses, one on top of the other. They both have a black outline, so you can see the smaller one inside the larger one, as Figure 13-20 shows.

Because the Ellipse shape is just a simple way of creating an EllipseGeometry, the code in Example 13-17 is equivalent to the code in Example 13-18. (As you can see, using a Path is considerably more verbose. This is why the Ellipse and other simple shapes are provided.)

Figure 13-20. Two Ellipse elements

Example 13-18. Two Paths with EllipseGeometry elements <Canvas>

<Path Fill="Cyan" Stroke="Black"> <Path.Data>

<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" /> </Path.Data> </Path>

<Path Fill="Cyan" Stroke="Black"> <Path.Data>

<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" /> </Path.Data> </Path> </Canvas>

Figure 13-20. Two Ellipse elements

Example 13-18. Two Paths with EllipseGeometry elements <Canvas>

<Path Fill="Cyan" Stroke="Black"> <Path.Data>

<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" /> </Path.Data> </Path>

<Path Fill="Cyan" Stroke="Black"> <Path.Data>

<EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" /> </Path.Data> </Path> </Canvas>

Because the code in Example 13-18 is equivalent to that in Example 13-17, it results in exactly the same output, as previously shown in Figure 13-20. So far, using geometries instead of shapes hasn't made a difference in the rendered results. This is because we are still using multiple shapes. So we will now show how you can put both ellipses into a single Path, and see how this affects the results. Example 13-19 shows the modified markup.

Example 13-19. One Path with two EllipseGeometry elements <Canvas>

<Path Fill="Cyan" Stroke="Black"> <Path.Data> <GeometryGroup>

<EllipseGeometry Center="20, 40" RadiusX="20" RadiusY="40" /> <EllipseGeometry Center="20, 40" RadiusX="10" RadiusY="30" /> </GeometryGroup> </Path.Data> </Path> </Canvas>

This version has just a single path. Its Data property contains a GeometryGroup. This allows any number of geometry objects to be added to the same path. Here we have added the two EllipseGeometry elements that were previously in two separate paths. The result, shown in Figure 13-21, is clearly different from the one in Figure 13-20— there is now a hole in the middle of the shape. Because the default even-odd fill rule was in play, the smaller ellipse makes a hole in the larger one. (GeometryGroup has a FillRule property that lets you choose the nonzero rule instead if you need to.)

Figure 13-21. Path with two geometries

You can create shapes with holes only by combining multiple figures into a single shape. You could try to get a similar effect to that shown in Figure 13-21 by drawing the inner Ellipse with a Fill color of White, but that trick fails to work as soon as you draw the shape on top of something else, as Figure 13-22 shows.

Figure 13-22. Spot the fake hole

You might be wondering whether you could just draw the inner ellipse using the Transparent color, but that doesn't work either—if you tried this, you'd still see all of the larger ellipse, rather than what is behind it. Drawing something as totally transparent has the same effect as drawing nothing at all—that's what transparency means. Only by knocking a hole in the shape can we see through it. To understand why, think about the drawing process. When it renders our elements to the screen, WPF draws the items one after the other. It starts with whatever's at the back—the text, in this case. Then it draws the shape on top of the text, which effectively obliterates the text that was underneath the shape. (It's still there in the element tree, of course, so WPF can always redraw it later if you change or remove the shape.) Because you just drew over the text, you can't draw another shape on top to "undraw" a hole into the first shape. So, if you want a hole in a shape, you'd better make sure that the hole is there before you draw it!

This is not to say you'd never use the Transparent color. It has a couple of uses. An animation might fade from a nontransparent color to Transparent in order to make an element disappear gradually. Also, objects that are Transparent are invisible to the eye, but not to the mouse—WPF's input system (which was described in Chapter 4) treats all brushes as equal, ignoring transparency. So the Transparent color provides a way of making invisible clickable targets.

We have not yet looked at the most flexible geometry: PathGeometry. This is the underlying geometry used by Polyline and Polygon, but it can draw many more shapes besides.

A PathGeometry contains one or more PathFigure objects, and each PathFigure represents a single open or closed shape in the path. To define the shape of each figure's outline, you use a sequence of PathSegment objects. Like GeometryGroup, PathGeometry also has a FillRule property to set the behavior for overlapping figures. Again, this defaults to the even-odd rule.

PathGeometry's ability to contain multiple figures overlaps slightly with GeometryGroup's ability to contain multiple geometries. This is just for convenience—if you need to make a shape where every piece will be a PathGeometry object, it is more compact to have a single PathGeometry with multiple PathFigures. If you just want a group of simpler geometries like LineGeometry or RectangleGeometry, it is simpler to use a GeometryGroup and avoid PathGeometry altogether.

Example 13-20 shows a simple path. This contains just a single figure in the shape of a square.

Example 13-20. A square Path

<Path Fill="Cyan" Stroke="Black"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0,0" IsClosed="True"> <PathFigure.Segments> <LineSegment Point="50,0" /> <LineSegment Point="50,50" /> <LineSegment Point="0,50" /> </PathFigure.Segments> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path>

Figure 13-23 shows the result. This seems like a vast amount of effort for such a simple result—we've used 15 lines of markup to achieve what we could have achieved with a single Rectangle element. This is why WPF supplies classes for the simpler shapes and geometries. You don't strictly need any of them because you can use Path and PathGeometry instead, but the simpler shapes require much less effort. Normally you would use Path only for more complex shapes.

Figure 13-23. A square Path

Even though Example 13-20 produces a very simple result, it illustrates most of the important features of a Path with a PathGeometry. As with all the previous examples, the geometry is in the path's Data property. The PathGeometry is a collection of PathFigures, so all of the interesting data is inside its Figures property. This example contains just one PathFigure, but you can add as many as you like. The shape of the PathFigure is determined by the items in its Segments property.

The starting point of a PathFigure is determined by its StartPoint property. One or more segments describe the figure's shape. In Example 13-20, these are all LineSegments because the shape has only straight edges, but several types of curves are also on offer. This particular figure is a closed shape, which is determined by the IsClosed property.

You might be wondering why LineSegments don't work like the Line shape or a LineGeometry. With those types, we specify start and end points, as in Example 13-11. This seems simpler than LineSegment, which needs us to specify a StartPoint in the PathFigure. However, line segments in a PathFigure can't work like that because there cannot be any gaps in the outline of a figure. With the Line element, each Line is a distinct shape in its own right, but with a PathFigure, each segment is a part of the shape's outline. To define a figure fully and unambiguously, each segment must start off from where the previous one finished. This is why the LineSegment only specifies an end point for the line. All of the segment types work this way.

Example 13-20 isn't very exciting; it just uses straight line segments. We can create much more interesting shapes by using one of the curved segment types instead. Table 13-4 shows all of the segment types.

Table 13-4. Segment types

Segment type Usage

LineSegment Single straight line

PolyLineSegment Sequence of straight lines

ArcSegment Elliptical arc

BezierSegment Cubic Bezier curve

OuadraticBezierSegment Quadratic Bezier curve

PolyBezierSegment Sequence of cubic Bezier curves

PolyOuadraticBezierSegment Sequence of quadratic Bezier curves

ArcSegment lets you add elliptical curves to the edge of a shape. ArcSegment is a little more complex to use than a simple LineSegment. As well as specifying the end point of the segment, we must also specify two radii for the ellipse with the Size property.

The ellipse size and the line start and end points don't provide enough information to define the curve unambiguously, because there are several ways to draw an elliptical arc given these constraints. Consider a segment with a particular start and end point, and a given size and orientation of ellipse. For this segment, there will usually be two ways in which we can position the ellipse so that both the start and end points lie on the boundary of the ellipse, as Figure 13-24 shows. In other words, there will be two ways of "slicing" an ellipse with a particular line.

Figure 13-24. Potential ellipse positions

For each way of slicing the ellipse, there will be two resulting arc segments, a small one and a large one. This means that there are four ways in which the curve could be drawn between two points.

The ArcSegment provides two flags that enable you to select which of the curves you require. IsLargeArc determines whether you get the larger or smaller slice size. SweepDirection chooses on which side of the line the slice is drawn. Example 13-21 shows markup for all four combinations of these flags. It also shows the whole ellipse.

Example 13-21. ArcSegments <Canvas>

<Ellipse Fill="Cyan" Stroke="Black" Width="140" Height="60" /> <Path Fill="Cyan" Stroke="Black" Canvas.Left="180"> <Path.Data> <PathGeometry> <PathFigure StartPoint="0,11" IsClosed="True"> <ArcSegment Point="50,61" Size="70,30"

SweepDirection="Counterclockwise" IsLargeArc="False" />

</PathFigure>

<PathFigure StartPoint="30,11" IsClosed="True"> <ArcSegment Point="80,61" Size="70,30"

SweepDirection="Clockwise" IsLargeArc="True" />

</PathFigure>

<PathFigure StartPoint="240,1" IsClosed="True"> <ArcSegment Point="290,51" Size="70,30"

SweepDirection="Counterclockwise" IsLargeArc="True" />

</PathFigure>

<PathFigure StartPoint="280,1" IsClosed="True"> <ArcSegment Point="330,51" Size="70,30"

SweepDirection="Clockwise" IsLargeArc="False" />

</PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas>

You may be wondering why the Ellipse has a width of 140 and a height of 60, which is double the Size of each ArcSegment. This is because the ArcSegment interprets the Size as the two radii of the ellipse, whereas the Width and Height properties on the Ellipse indicate the total size.

Figure 13-25 shows the results, and as you can see, each shape has one straight diagonal line and one elliptical curve. The straight line edge has the same length and orientation in all four cases. The curved edge is from different parts of the same ellipse.

Figure 13-25. An ellipse and four arcs from that ellipse

Figure 13-25. An ellipse and four arcs from that ellipse

In Figure 13-25, the ellipse's axes are horizontal and vertical. Sometimes you will want to use an ellipse where the axes are not aligned with your main drawing axes. ArcSegment provides a RotationAngle property, allowing you to specify the amount of rotation required in degrees.

Figure 13-26 shows four elliptical arcs. These use the same start and end points as Figure 13-25, and the same ellipse size. The only difference is that a RotationAngle of 45 degrees has been specified, rotating the ellipse before slicing it.

Figure 13-26. Four arcs from a rotated ellipse

There are two degenerate cases in which there will not be two ways of slicing the ellipse. The first is when the slice cuts the ellipse exactly in half. In this case, the IsLargeArc flag is irrelevant, because both slices are exactly the same size.

The other case is when the ellipse is too small—if the widest point at which the ellipse could be sliced is narrower than the segment is long, there is no way in which the segment can be drawn correctly. (If you do make the ellipse too small, WPF seems to scale the ellipse so that it is large enough, preserving the aspect ratio between the x- and y-axes.) You should avoid this.

The remaining curve types (BezierSegment, PolyBezierSegment, OuadraticBezierSegment, and PolyOuadraticBezierSegment) are variations on the same theme. They all draw Bezier curves.

Bezier curves

Bezier curves are curved line segments joining two points using a particular mathematical formula. It is not necessary to understand the details of the formula in order to use Bezier curves. What makes Bezier curves useful is that they offer a fair amount of flexibility in the shape of the curve. This has made them very popular—most vector drawing programs offer them.*

Figure 13-27 shows a variety of Bezier curve segments. Each of the five lines shown here is a single BezierSegment.

Figure 13-27. Bezier curve segments

As with all of the segment types, a BezierSegment starts from where the preceding segment left off, and defines a new end point. It also requires two "control points" to be defined, and it is these that determine the shape of the curve. Figure 13-28 shows the same curves again, but with the control points drawn on. It also shows lines connecting the control points to the segment end points, because this makes it easier to see how the control points affect the curve shapes.

Figure 13-28. Bézier curves with control points shown

The most obvious way in which the control points influence the shapes of these curves is that they determine the tangent. At the start and end of each segment, the direction in which the curve runs at that point is exactly the same as the direction of the line joining the start point to the corresponding control point.

There is a second, less obvious way in which control points work. The distance between the start or end point and its corresponding control point (i.e., the length of

* If you'd like to understand the formula for Bézier curves, http://mathworld.wolfram.com/BezierCurve.html (http://tinysells.com/69) and http://en.wikipedia.org/wiki/B%C3%A9zier_curve (http://tinysells.com/70) both provide good descriptions.

the straight lines added on Figure 13-28) also has an effect. This essentially determines how extreme the curvature is.

Figure 13-29 shows a set of Bezier curves similar to those in Figure 13-28. The tangents of both ends of the lines remain the same, but in each case, the distance between the start point and the first control point is reduced to one-quarter of what it was before, whereas the other is the same as before. As you can see, this reduces the influence of the first control point. In all four cases, the shape of the curve is dominated by the control point that is farther from its end point.

Figure 13-29. Bézier curves with less extreme control points

Example 13-22 shows the markup for the second curve segment in Figure 13-28. The Pointl property determines the location of the first control point—the one associated with the start point. Point2 positions the second control point. Point3 is the end point. (To keep things clear, the examples in this section just show the relevant PathFigure elements. If you want to see these shapes, you would of course need to put them inside a PathGeometry inside a Path, just as with the previous examples.)

Example 13-22. BezierSegment

<PathFigure StartPoint="0,50">

<BezierSegment Point1="60,50" Point2="100,0" Point3="100,50" /> </PathFigure>

Flexible though Bezier curves are, you will rarely use just a single one. When defining shapes with curved edges, it is normal for a shape to have many Bezier curves defining its edge. WPF therefore supplies a PolyBezierSegment type, which allows multiple curves to be represented in a single segment. It defines a single Points property, which is an array of Point structures. Each Bezier curve requires three entries in this array: two control points and an end point. (As always, each segment starts from where the previous one left off.) Example 13-23 shows an example segment with two curves. Figure 13-30 shows the results.

Example 13-23. PolyBezierSegment

<PathFigure StartPoint="0,0"> <PolyBezierSegment> <PolyBezierSegment.Points> <Point X="0" Y="l0"/> <Point X="20" Y="l0"/> <Point X="40" Y="l0"/> <Point X="60" Y="10"/>

Example 13-23. PolyBezierSegment (continued)

<Point X="120" Y="15"/> <Point X="100" Y="50"/> </PolyBezierSegment.Points> </PolyBezierSegment> </PathFigure>

<Point X="120" Y="15"/> <Point X="100" Y="50"/> </PolyBezierSegment.Points> </PolyBezierSegment> </PathFigure>

Figure 13-30. PolyBezierSegment

Figure 13-30. PolyBezierSegment

This markup is less convenient than simply using a sequence of BezierSegment elements, which rather defeats the point. Fortunately, you can provide all of the point data in string form. This is equivalent to Example 13-23:

<PathFigure StartPoint="0,0">

<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" /> </PathFigure>

Also, if you are generating coordinates from code, dealing with a single PolyBezierSegment and passing it an array of Point data is often easier than working with lots of individual segments.

Cubic Bezier curves provide a lot of control over the shape of the line. However, you might not always need that level of flexibility. The OuadraticBezierSegment uses a simpler equation that uses just one control point to define the shape of the curve. This does not offer the same range of curve shapes as a cubic Bezier curve, but if all you want is a simple shape, this reduces the number of coordinate pairs you need to provide by one-third.

OuadraticBezierSegment is similar in use to the normal BezierSegment. The only difference is that it has no Point3 property—just Point1 and Point2. Point1 is the single control point, and Point2 is the end point. PolyOuadraticBezierSegment is the multi-curve equivalent. You use this in exactly the same way as PolyBezierSegment, except you need to provide only two points for each segment.

Combining shapes

Geometries can perform one more trick that we have not yet examined. We can combine geometries to form new geometries. This is different from adding two geometries to a GeometryGroup—it is possible to combine pairs of geometries in a way that forms a single geometry with a whole new shape.

Examples 13-24 and 13-25 define paths, both of which make use of the same RectangleGeometry and EllipseGeometry. The difference is that Example 13-24 puts both into a GeometryGroup, while Example 13-25 puts them into a CombinedGeometry.

Example 13-24. Multiple geometries

<Path Fill="Cyan" Stroke="Black"> <Path.Data> <GeometryGroup> <RectangleGeometry Rect="0)0)50,50" />

<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" /> </GeometryGroup> </Path.Data> </Path>

Example 13-25. Combined geometries

<Path Fill="Cyan" Stroke="Black"> <Path.Data>

<CombinedGeometry GeometryCombineMode="Exclude">

<CombinedGeometry.Geometry1>

<RectangleGeometry Rect="0,0,50,50" /> </CombinedGeometry.Geometry1> <CombinedGeometry.Geometry2>

<EllipseGeometry Center="50,25" RadiusX="30" RadiusY="10" /> </CombinedGeometry.Geometry2> </CombinedGeometry> </Path.Data> </Path>

Figure 13-31 shows the results of Examples 13-24 and 13-25. Whereas the GeometryGroup has resulted in a shape with multiple figures (taking the default fill rule into account), the CombinedGeometry has produced a single figure. The ellipse geometry has taken a bite out of the rectangle geometry. This is just one of the ways in which geometries can be combined. The GeometryCombineMode property determines which is used, and Figure 13-32 shows all four available modes.

Figure 13-31. Grouping and combining geometries

Figure 13-31. Grouping and combining geometries

Figure 13-32. Combine modes: Union, Intersect, Xor, and Exclude

Union builds a shape in which any point that was inside either of the two original shapes will also be inside the new shape. Intersect creates a shape where only points that were inside both shapes will be in the new shape. Xor creates a shape where points that were in one shape or the other, but not both, will be in the new shape. Exclude creates a shape where points inside the first shape but not inside the second will be included.

Path geometry text format

We have now looked at all of the features that Path has to offer. As you have seen, we can end up with some pretty verbose markup. Fortunately, there is a shorthand mechanism that allows us to exploit most of the features we have seen without having to type quite so much.

So far, we have been setting the Data property using XAML's property element syntax. (See Appendix A for more details on this syntax.) However, we can supply a string instead. Example 13-26 shows both techniques. As you can see, the string form is some 12 lines shorter.

Example 13-26. Path.Data as text <!-- Longhand -->

<Path Fill="Cyan" Stroke="Black"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0,0" IsClosed="True"> <LineSegment Point="50,0" /> <LineSegment Point="50,50" /> <LineSegment Point="0,50" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path>

<Path Fill="Cyan" Stroke="Black" Data="M 0,0 L 50,0 50,50 0,50 Z" />

The syntax for the text form of the Path.Data property is simple. The string must contain a sequence of commands. A command is a letter followed by some numeric parameters. The number of parameters required is determined by the chosen command. Lines require just a coordinate pair. Curves require more data.

If you omit the letter, the same command will be used as last time. For instance, Example 13-26 uses the L command—this is short for Line, and it represents a LineSegment. This requires only two numbers: the coordinates of the line end point. And yet, in our example, there are six numbers. This simply indicates that there are three lines in a row. Table 13-5 lists the commands, their equivalent segment types where applicable, and their usage.

Table 13-5. Path.Data commands

Command

Command name

Segment type

Parameters

M (or m)

Move

Coordinate pair: the StartPoint for a new PathFigure

L (or l)

Line

LineSegment

Coordinate pair: end point

H (or h)

Horizontal line

LineSegment

Single coordinate: end x coordinate (y coordinate will be the same as before)

V (or v)

Vertical line

LineSegment

Single coordinate: end y coordinate (x coordinate will be the same as before)

C (or c)

Cubic Bezier curve

BezierSegment

Three coordinate pairs: two control points and one end point

Q (or q)

Quadratic Bezier curve

QuadraticBezierSegment

Two coordinate pairs: control pointand end point

S (or s)

Smooth Bezier curve

BezierSegment

Two coordinate pairs: second control point and end point (first control point generated automatically)

T (or t)

Smooth quadratic Bezier curve

QuadraticBezierSegment

One coordinate pair: end point (control point generated automatically)

A (or a)

Elliptical arc

ArcSegment

Seven numbers: x radius, y radius,

RotationAngle, IsLargeArc, SweepDirection, and end point coordinate pair

Z (or z)

Close path

None

F0

Even-odd fill rule

None

F1

Nonzero fill rule

None

The commands M, Z, F0, and F1 do not correspond to segments. The M command causes a new PathFigure to be started, enabling multiple figures to be represented in this compact text format. Z sets the current figure's IsClosed property to true. F0 and F1 set the FillRule of the PathGeometry.

Notice that there are two ways to specify a BezierSegment. The C command lets you provide all of the control points. The S command generates the first control point for you—it looks at the preceding segment and makes the first control point a mirror image of the preceding one. This ensures that the segment's tangent aligns with the preceding segment's tangent, resulting in a smooth join between the lines.

Quadratic Bezier segments have a similar facility: the 0 command lets you specify the control point, whereas the T command generates the control point for you in a way that guarantees a smooth line.

You can specify any of these commands in either uppercase or lowercase. In the uppercase form, coordinates are relative to the position of the Path element. If the command is lowercase, the coordinates are taken to be relative to the end point of the preceding segment in the path.

As well as being offered for the Path.Data property, this path syntax can also be used directly with a PathGeometry—its Figures property supports the same syntax. Another geometry type also supports this mini path language: StreamGeometry. This geometry type can represent all the same shapes as a PathGeometry, but you cannot modify it once it has been created. This is because it does not support the object model of path figures and segments—from markup, it only supports the path syntax. (If you are using code, you also can build a StreamGeometry with a StreamGeometryContext object, which lets you describe the shape with a series of method calls.)

Because a StreamGeometry is immutable and because it does not maintain a tree of objects representing the shape, it can use a more efficient internal representation than a PathGeometry. If you are working with very complex shapes, or a large number of shapes, this can significantly improve performance. If you are using XAML in such scenarios, you should prefer the path syntax over the object tree, because when you set Path.Data with the path syntax, WPF creates a StreamGeometry instead of a PathGeometry.

We have now examined all of the shapes on offer. However, not all visuals are best represented with scalable shapes—sometimes we need to work with bitmap images.

Was this article helpful?

0 0
Project Management Made Easy

Project Management Made Easy

What you need to know about… Project Management Made Easy! Project management consists of more than just a large building project and can encompass small projects as well. No matter what the size of your project, you need to have some sort of project management. How you manage your project has everything to do with its outcome.

Get My Free Ebook


Post a comment