September 2009

Volume 24 Number 09

Windows with C++ - Drawing with Direct2D

By Kenny Kerr | September 2009

In the June issue of MSDN Magazine (msdn.microsoft.com/en-us/magazine/dd861344.aspx), I introduced Direct2D, a brand new 2-D graphics API designed to support the most demanding and visually rich desktop applications with the best possible performance. In that article, I described where Direct2D fits in among the various graphics APIs on Windows, its architecture and principles. In particular, I described in detail the fundamentals of using Direct2D reliably and efficiently for rendering inside of a window. This included creation of device-specific resources as well as device-independent resources and their respective lifecycles. If you haven't already done so, I'd encourage you to read that article before continuing on here, as this article very much builds upon the foundation laid out there.

Rendering and Control

It is helpful to think about Direct2D as a hardware-accelerated 2-D rendering API. Of course, it supports software fallback, but the point here is that Direct2D is about rendering. Unlike other graphics APIs on Windows, Direct2D takes a componentized approach to graphics. It does not provide its own APIs for encoding and decoding bitmaps, text layout, font management, animation, 3-D and so on. Rather, it focuses on rendering and control over the graphics processing unit (GPU) while providing first class hooks to other APIs that focus on things like text layout and imaging. Direct2D does, however, provide primitives for representing different types of brushes as well as simple and complex shapes, the building blocks for any 2-D graphics application.

In this article, I'm going to show you how to draw with Direct2D. I'll begin by introducing Direct2D's color structure and then
show you how to create various types of brushes. Unlike most of the other graphics APIs on Windows, Direct2D doesn't provide a "pen" primitive, so brushes are pretty important as they're used for all outline and filling tasks. With that out of the way, I'll show you how to draw primitive shapes.

Colors

Direct2D uses a simple structure that represents colors with floating-point color components. The D2D1_COLOR_F type is actually a typedef for the D3DCOLORVALUE structure used by Direct3D to describe color values. It includes individual floating-point values for the red, green, blue and alpha channels. Values range from 0.0 to 1.0 with 0.0 being black for the color channels and completely transparent for the alpha channel.

Here's what it looks like:

struct D3DCOLORVALUE
{
FLOAT r;
FLOAT g;
FLOAT b;
FLOAT a;
};

Direct2D provides the ColorF helper class in the D2D1 namespace that inherits from D2D1_COLOR_F and defines some common color constants, but more importantly provides a few helpful constructors that initialize the D2D1_COLOR_F structure. You can, for example, define red as follows:

const D2D1_COLOR_F red = D2D1::ColorF(1.0f, 0.0f, 0.0f);

Another constructor accepts a packed RGB value and converts it to the individual color channels. Here's red again:

D2D1::ColorF(0xFF0000)

This is exactly the same as using the following enum value:

D2D1::ColorF(D2D1::ColorF::Red)

Although concise, using the packed RGB representation does eat up a few more CPU cycles as the different color channels need to be extracted and converted to their floating point equivalents, so use it with care.

All of the constructors accept an optional alpha value that defaults to 1.0, or fully opaque. Thus, you can express semi-transparent blue as follows:

D2D1::ColorF(0.0f, 0.0f, 1.0f, 0.5f)

Apart from clearing a render target's drawing area, however, there is little you can do with a color directly. What you need are brushes.

Brushes

Unlike simple color structures, brushes are resources exposed through interfaces. They are used to draw lines, to draw and fill shapes, and to draw text. Interfaces that derive from ID2D1Brush represent the different types of brushes provided by Direct2D. The ID2D1Brush interface itself allows you to control opacity of a brush as a whole, rather than changing the alpha channel of the colors used by the brush. This can be particularly helpful with some of the more interesting types of brushes.

Here's how you might set the opacity of the brush to 50 percent:

CComPtr<ID2D1Brush> brush;
// Create brush here...
brush->SetOpacity(0.5f);

ID2D1Brush also allows you to control the transform applied to the brush since, unlike Windows Presentation Foundation (WPF), brushes adopt the coordinate system of the render target rather than that of any particular shape they may be drawing.

The various render target methods for creating brushes all accept an optional D2D1_BRUSH_PROPERTIES structure that can be used to set the initial opacity and transform.

All of the brushes provided by Direct2D are mutable and can efficiently change their characteristics, so you don't need to create new brushes with different characteristics. Keep this in mind as you design your applications. Brushes are also device-dependent resources, so they are bound by the lifetime of the render target that created them. In other words, you can reuse a particular brush for as long as its render target is valid but you must release it when the render target is released.

As its name suggests, the ID2D1SolidColorBrush interface represents a solid color brush. It adds methods for controlling the color used by the brush. A solid color brush is created with a render target's CreateSolidColorBrush method:

CComPtr<ID2D1RenderTarget> m_target;
// Create render target here...
const D2D1_COLOR_F color = D2D1::ColorF(D2D1::ColorF::Red);
CComPtr<ID2D1SolidColorBrush> m_brush;
HR(m_target->CreateSolidColorBrush(color, &m_brush));
This brush's initial color can also easily be changed using the SetColor method:
m_brush->SetColor(differentColor);

I'm going to leave a complete discussion of shapes for the next section, but for the sake of having something to see, I'll just fill a window's render target using a rectangle based on the size of the render target. Keep in mind that Direct2D uses device-independent pixels (DIPs). As a result, the size reported by a particular device, such as a desktop window's client area, may not match the size of the render target. Fortunately, it's very easy to get the size of the render target in DIPs using the GetSize method. GetSize returns a D2D1_SIZE_F structure that Direct2D uses to represent sizes with two floating point values named width and height. I can then provide a D2D1_RECT_F variable to describe the area to fill by using the RectF helper function and plugging in the size reported by the render target. Finally, I can use the render target's FillRectangle method to do the actual drawing.

Here's what the code looks like:

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_RECT_F rect = D2D1::RectF(0, 0, size.width, size.height);
m_target->FillRectangle(rect, m_brush);

Figure 1 shows what the window looks like with a solid green brush. Very exciting indeed!

Direct2D also provides two types of gradient brushes. A gradient brush is one that fills an area with colors blended along an axis. A linear gradient brush defines the axis as a straight line with a start and end point. A radial gradient brush defines the axis as an ellipse, where the colors radiate outward from some point relative to the center of the ellipse.

A gradient is defined as a series of relative positions from 0.0 to 1.0. Each has its own color and is called a gradient stop. It is possible to use positions outside of this range to produce various effects. To create a gradient brush, you must first create a gradient stop collection. Start by defining an array of D2D1_GRADIENT_STOP structures.

Here's an example:

const D2D1_GRADIENT_STOP gradientStops[] =
{
{ 0.0f, color1 },
{ 0.2f, color2 },
{ 0.3f, color3 },
{ 1.0f, color4 }
};

Next, call the render target's CreateGradientStopCollection method to create a collection object based on the array of gradient stops, as follows:

CComPtr<ID2D1GradientStopCollection> gradientStopsCollection;

HR(m_target->CreateGradientStopCollection(gradientStops,
_countof(gradientStops),
&gradientStopsCollection));

The first parameter is a pointer to the array and the second parameter provides the size of the array. Here I'm just using the standard _countof macro. The CreateGradientStopCollection method returns the new collection. To create a linear gradient brush, you also need to provide a D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES structure to indicate the start and end point of the gradient's axis. Unlike the gradient stop positions, these points are in the brush's coordinate space which is usually that of the render target unless a transform is set on the brush. For example, you could create the brush with an axis that runs from the top-left corner of the render target to the bottom-right corner as follows:

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_POINT_2F start = D2D1::Point2F(0.0f, 0.0f);
const D2D1_POINT_2F end = D2D1::Point2F(size.width, size.height);
const D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES properties = D2D1::LinearGradientBrushProperties(start, end);
HR(m_target->CreateLinearGradientBrush(properties,
gradientStopsCollection,
&m_brush));

LinearGradientBrushProperties is another helper function provided by Direct2D to initialize a D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES structure. The CreateLinearGradientBrush method accepts this along with the gradient stop collection shown earlier and returns an ID2D1LinearGradientBrush interface pointer representing the new brush.

Of course, the brush won't know if the size of the render target changes. If you wanted the end point of the axis to change as a window is resized, you could easily do so with the brush's SetEndPoint method. Similarly, you can change the axis' start point with the SetStartPoint method.

Here's what the drawing code looks like:

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_RECT_F rect = D2D1::RectF(0, 0, size.width, size.height);

m_brush->SetEndPoint(D2D1::Point2F(size.width, size.height));
m_target->FillRectangle(rect, m_brush);

Figure 2 shows what the window looks like with the linear gradient brush.

To create a radial gradient brush, you need to provide a D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES structure. This structure defines the ellipse as well as an offset relative to the center of the ellipse that represents the origin out of which the brush "radiates" color based on the gradient stop collection. The following example produces an ellipse that is centered in the render target, has an X and Y radius to match that of the render target, and an origin half way toward the bottom-right corner:

const D2D1_SIZE_F size = m_target->GetSize();

const D2D1_POINT_2F center = D2D1::Point2F(size.width / 2.0f, size.height / 2.0f);
const D2D1_POINT_2F offset = D2D1::Point2F(size.width * 0.25f, size.height * 0.25f);
const float radiusX = size.width / 2.0f;
const float radiusY = size.height / 2.0f;

const D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES properties = D2D1::RadialGradientBrushProperties(center,

offset,

radiusX,

radiusY);

HR(m_target->CreateRadialGradientBrush(properties,
gradientStopsCollection,
&m_brush));

RadialGradientBrushProperties is another helper function provided by Direct2D to initialize a D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES structure. The CreateRadialGradientBrush method accepts this along with the gradient stop collection shown earlier and returns an ID2D1RadialGradientBrush interface pointer representing the new brush.

You can also change the brush's ellipse and origin at any time, as follows:

m_brush->SetCenter(center);
m_brush->SetGradientOriginOffset(offset);
m_brush->SetRadiusX(radiusX);
m_brush->SetRadiusY(radiusY);

Figure 3 shows the radial gradient brush in action.

Direct2D also provides a bitmap brush, but I'll leave a discussion of Direct2D bitmaps for a future article.

Shapes

Thus far I've shown you only how to fill a rectangle so that I could focus on brushes, but Direct2D can do so much more than plain rectangles. For starters, primitives are provided for rectangles, rounded rectangles and ellipses. By primitives, I just mean that there is a plain old data structure for each of these.

D2D1_RECT_F represents a floating-point rectangle and is itself a typedef for D2D_RECT_F:

struct D2D_RECT_F
{
FLOAT left;
FLOAT top;
FLOAT right;
FLOAT bottom;
};

D2D1_ROUNDED_RECT represents a rounded rectangle and is defined as follows, with the radiuses defining the quarter ellipses that will be used to draw the corners:

struct D2D1_ROUNDED_RECT
{
D2D1_RECT_F rect;
FLOAT radiusX;
FLOAT radiusY;
};

D2D1_ELLIPSE represents an ellipse and is defined with a center point as well as radiuses:

struct D2D1_ELLIPSE
{
D2D1_POINT_2F point;
FLOAT radiusX;
FLOAT radiusY;
};

Although these structures may not seem too exciting, there are two reasons I mention them up front. First, they are used to create more sophisticated geometry objects. Second, if all you need is to fill or draw one of these primitives, you should stick with them as you will typically get better performance if you avoid the geometry objects.

Render targets provide a set of Fill- and Draw- methods for filling and outlining these primitives. So far I've shown you how to use the FillRectangle method. I won't bore you with examples of the FillRoundedRectangle and FillEllipse methods, as they work in exactly the same way. In contrast to the Fill- methods, the Draw- methods can be used to outline a particular shape and are quite versatile. Like the Fill- methods, the Draw- methods covering the three primitives all work in exactly the same way, so I'll cover only the DrawRectangle method.

In its simplest form, you can draw the outline of a rectangle as follows:

m_target->DrawRectangle(rect,
m_brush,
20.0f);

The third parameter specifies the width of the stroke that is drawn to outline the rectangle. The stroke itself is centered on the rectangle, so if you had filled the same rectangle you would see that the fill and the stroke overlap by 10.0 DIPs. An optional fourth parameter may be provided to control the style of the stroke that is drawn. This is useful if you need to draw a dashed line or just want to control the type of join at a shape's vertices.

Stroke style information is represented by the ID2D1StrokeStyle interface. Stroke style objects are device-independent resources, which mean they don't have to be re-created whenever the render target is invalidated. In that event, a new stroke style object is created using the Direct2D factory object's CreateStrokeStyle method.

If you just want the stroke to use a particular type of join, you can create a stroke style as follows:

D2D1_STROKE_STYLE_PROPERTIES properties = D2D1::StrokeStyleProperties();
properties.lineJoin = D2D1_LINE_JOIN_BEVEL;

HR(m_factory->CreateStrokeStyle(properties,
0, // dashes
0, // dash count
&m_strokeStyle));

The D2D1_STROKE_STYLE_PROPERTIES structure provides a variety of members to control various aspects of the stroke style, in particular the shape, or cap, at each end of an outline or dash as well as the dash style itself. The second and third parameter to the CreateStrokeStyle method are optional, and you need to provide them only if you are defining a custom dashed stroke style. To define a dashed stroke style, make sure you specify the dashes in pairs. The first element in each pair is the length of the dash and the second is the length of the space before the next dash. The values themselves are multiplied by the stroke width. You can specify as many pairs as you need to produce the desired pattern. Here's an example:

D2D1_STROKE_STYLE_PROPERTIES properties = D2D1::StrokeStyleProperties();
properties.dashStyle = D2D1_DASH_STYLE_CUSTOM;

float dashes[] =
{
2.0f, 1.0f,
3.0f, 1.0f,
};

HR(m_factory->CreateStrokeStyle(properties,
dashes,
_countof(dashes),
&m_strokeStyle));

You can choose from any of a number of different dash styles from the D2D1_DASH_STYLE enumeration instead of defining your own. Figure 4 shows some different stroke styles at work. If you look closely, you'll see that Direct2D automatically alpha blends with per-primitive antialiasing for the best looking results.

There's so much more that Direct2D can do for you, from complex geometries and transformations, to drawing text and bitmaps, and much more. I hope to cover these and more in future columns.


KENNY KERR* is a software craftsman specializing in software development for Windows. He has a passion for writing and teaching developers about programming and software design. Reach him at weblogs.asp.net/kennykerr.*