Partager via


Chapter 6: Using Windows Direct2D

Modern computers have advanced graphics cards. To allow developers to use the full features of these graphics cards, Windows 7 provides the DirectX libraries, a collection of APIs for implementing high performance 2D and 3D graphics and multi-media capabilities in your applications. One of the APIs, Direct2D, as the name suggests, provides classes to draw in the x-y plane using hardware accelerated DirectX technologies. Another of the APIs, DirectWrite, provides support for high quality text rendering. This article describes how Direct2D and DirectWrite are used in the Hilo applications.

Using Direct2D

Direct2D is an object orientated library built using the Direct3D Application Programming Interface (API) that gives access to the drawing features of Graphics Processing Units (GPU) in modern graphics cards. The headers and libraries needed for C++ development for Direct2D are supplied as part of the Windows Software Development Kit (SDK) for Windows 7. The two main headers are d2d1.h and d2d1helper.h. The d2d1.h header file defines the main structures, enumerations, and interfaces in the library. The Direct2D objects are created through named methods on interfaces of other Direct2D objects, usually the D2D1 factory object created by a call to a global function called D2D1CreateFactory.

The d2d1helper.h header file defines a C++ namespace called D2D1 that contains helper classes and functions to make using Direct2D easier. For example, there is a class called D2D1::ColorF that provides access to named color values and D2D1::Matrix3x2F that encapsulates the operations that can be carried out on a D2D1_MATRIX_3X2_F structure.

Hilo also uses DirectWrite through the dwrite.h header file. Similar to Direct2D, DirectWrite objects are created through a factory object which itself is created through a global method called DWriteCreateFactory.

Initializing the Direct2D Library

The Direct2D and DirectWrite factory objects should only be created once in the process, and Hilo does this through a helper class called Direct2DUtility that has static methods. These methods have static variables that are initialized the first time the method is called, and returned from subsequent calls. These local static variables are ComPtr<> objects and since they are static, when the entry point function, _tWinMain, finishes, the destructor of ComPtr<> is called that releases the final reference on the Direct2D object.

The Direct2D method, D2D1CreateFactory, has a parameter that indicates whether the Direct2D objects will be called in a multi-threaded or single threaded environment. In the former case the Direct2D library will protect any thread-sensitive operations from multi-threaded access on all the objects created through the factory object. Hilo uses a worker thread to provide asynchronous loading of images, and so the Direct2D objects are created as multi-threaded aware.

Creating the Render Target

In Hilo, when a window is first created, the OnCreate method is called. Each of the Hilo objects that handle messages for child windows (for example, CarouselPaneMessageHandler and MediaPaneMessageHandler) use OnMethod to call the CreateDeviceIndependentResources method that obtains interface pointers to the Direct2D and DirectWrite factory objects.

The child window message handler classes in Hilo also have a method called CreateDeviceResources to create Direct2D and DirectWrite objects that are device dependent. Device dependent objects are created by the graphics card GPU and include the render target, brushes and bitmap objects. The render target object is where the drawing is carried out, the brushes and bitmaps are used to do the drawing. The lifetime of these GPU objects, and hence the wrapper Direct2D objects, is determined by the GPU. For performance reasons these objects should live as long as possible, but the GPU may indicate that these objects cannot be reused and must be recreated.

Listing 1 Hilo pattern for drawing windows

// Create render target and other resources if they have not been created already
HRESULT hr = CreateDeviceResources();
hr = m_renderTarget->BeginDraw();
// Do drawing here…
hr = m_renderTarget->EndDraw();

// If the GPU indicates the resources need re-creating, destroy the existing ones
if (hr == D2DERR_RECREATE_TARGET)
{
   DiscardDeviceResources();
}

Listing 1 shows the code pattern used in Hilo. The CreateDeviceResources method creates the resources if they are not already created. All drawing using Direct2D resources must be performed between calls to the ID2D1RenderTarget::BeginDraw and ID2D1RenderTarget::EndDraw methods. The EndDraw method returns a value indicating whether the Direct2D objects can be reused. If this method indicates that the GPU requires that the device dependent objects should be destroyed, then DiscardDeviceResources is called. In this case, they will be recreated by the call to the CreateDeviceResources method when the code in Listing 1 is called to draw the window.

Using the Coordinate System

Before you can draw on the render target you must determine where to draw. In Direct2D the drawing units are called device independent pixels (DIPs), a DIP is defined as 1/96 of a logical inch. Direct2D will scale the drawing units to actual pixels when the drawing occurs, and it does so by using the Windows 7 dots per inch (DPI) setting. When you draw text using DirectWrite you specify DIPs rather than points for the size of the font. DIPs are expressed as floating point numbers. By default, Direct2D coordinates have the x value increasing left to right, and the y value increasing top to bottom; the top left hand corner of a render target is x = 0.0f, y = 0.0f, this is shown in Figure 1.

Figure 1 Direct2D coordinates

Ff934857.a788921f-48b3-4ac9-85c1-2e9a3cba51de-thumb(en-us,MSDN.10).png

You can get the size of the window in DIPs by calling the ID2D1RenderTarget::GetSize method. The first time you call this method it will return the size calculated from the size of the window’s client area when the render target was first created. If the window changes size then Direct2D will scale its drawing to the new size of the window. This can have undesirable effects because items like text will be stretched according to how the size of the window changes. To get around this problem you can call the ID2D1HwndRenderTarget::Resize method and pass the size of the window in device pixel units.

Many of the items in Hilo are animated, and so their positions, size, even the opacity of the item changes with time. The change of these positions is calculated by the Windows Animation Manager and this Windows 7 component will be covered in detail in the next chapter. For now we will just say that the Hilo classes covering animation (for example, CarouselThumbnailAnimation is provided to animate the position of a folder on the carousel) have methods to allow the position of the animated items to be returned.

For more about Windows 7 coordinate system, see Learn to Program for Windows in C++, in the MSDN Library.

Using Direct2D Transforms

The discussion so far about Direct2D coordinates assume that no transforms are performed. The ID2D1RenderTarget::SetTransform method allows you to provide a D2D1_MATRIX_3X2_F structure that describes the transform through a matrix. The transform is affine, which means that the matrix contains information about translation as well as rotation and scaling. To make using these matrices easier, the d2d1helper.h header file derives a class called Matrix3x2F from the D2D1_MATRIX_3X2_F structure. The Matrix3x2F class has static methods to return the identity matrix (no transform) or the matrix that represents a rotate, skew, scaling, or a translation. The class also has an operator* that you can call to combine two matrices as a single matrix.

You can call the ID2D1RenderTarget::SetTransform method at any time and any drawing performed afterwards through the render target will be affected by the transform. In Hilo the identity matrix is used by default, but a rotation matrix is used in one situation: drawing the navigation arrows, as shown in Listing 2.

Listing 2 Example from mediapane.cpp showing the use of a transform

unsigned int currentPage = GetCurrentPageIndex();
unsigned int maxPages = GetMaxPagesCount();
if (currentPage > 0)
{
   // Show the left arrow
   m_renderTarget->DrawBitmap(
      m_arrowBitmap, leftArrowRectangle, 
      m_leftArrowSelected || m_leftArrowClicked ? 1.0f : 0.5f);
}

if (maxPages > 0 && currentPage < maxPages - 1)
{
   // Show the right arrow by rotating the bitmap 180 deg
   m_renderTarget->SetTransform(
      D2D1::Matrix3x2F::Rotation(180.0f,
         D2D1::Point2F(
            rightArrowRectangle.left + (rightArrowRectangle.right - rightArrowRectangle.left) / 2.0f, 
            rightArrowRectangle.top + (rightArrowRectangle.bottom - rightArrowRectangle.top) / 2.0f)));

      m_renderTarget->DrawBitmap(
         m_arrowBitmap, rightArrowRectangle, 
         m_rightArrowSelected || m_rightArrowClicked ? 1.0f : 0.25f);
}

Handling Mouse Messages

Hilo allows the user to interact with the application through mouse clicks and the touch screen. The messages for these interactions pass the position of the mouse or finger touches using device coordinates relative to the top left corner of the client area of the window. Hilo uses these positions to determine if an item (a folder, or an image) is selected, and to do this it has to be able to convert between device pixels and DIPs. The Direct2DUtility class provides two static methods to do this, as shown in Listing 3. These methods use 96 DPI for the render target and calls the ID2D1Factory::GetDesktopDpi method to obtain the resolution for the window.

Listing 3 The methods of Direct2DUtility to convert between device coordinates and DIPs

static POINT_2F GetMousePositionForCurrentDpi(LPARAM lParam)
{
   static POINT_2F dpi = {96, 96}; // The default DPI

   ComPtr<ID2D1Factory> factory;
   if (SUCCEEDED(GetD2DFactory(&factory)))
   {
      factory->GetDesktopDpi(&dpi.x, &dpi.y);
   }

   return D2D1::Point2F(
      static_cast<int>(static_cast<short>(LOWORD(lParam))) * 96 / dpi.x,
      static_cast<int>(static_cast<short>(HIWORD(lParam))) * 96 / dpi.y);
}

static POINT_2F GetMousePositionForCurrentDpi(float x, float y)
{
   static POINT_2F dpi = {96, 96}; // The default DPI
   ComPtr<ID2D1Factory> factory;

   f (SUCCEEDED(GetD2DFactory(&factory)))
   {
      factory->GetDesktopDpi(&dpi.x, &dpi.y);
   }

   return D2D1::Point2F(x * 96 / dpi.x, y * 96 / dpi.y);
}

Drawing Graphics Items

Several resources are needed in Hilo to draw items. To draw line items, filled items, or text you have to have a brush. If you already have a bitmap it can be drawn on the render target using a bitmap object. All of these items are device dependent resources and are created through method calls on a render target object. For example, all text is drawn using a font object and a brush. In Hilo the carousel draws font names using a black brush and Listing 4 shows the code to do this in CarouselPaneMessageHandler::CreateDeviceResources.

Listing 4 Code to create a solid color brush

if (SUCCEEDED(hr))
{
   hr = m_renderTarget->CreateSolidColorBrush(
      D2D1::ColorF(D2D1::ColorF::Black),
      &m_fontBrush
      );
}

The D2D1::ColorF class contains static members for named colors. The color value is a 32-bit integer, so you can provide the value instead of using this class.

Creating and Using a Linear Gradient Brush

Hilo also uses gradient brushes. For example, the carousel in the Hilo Browser uses a linear gradient brush to fill the background of the carousel pane and a radial gradient brush to draw the orbitals. To create a gradient brush you need to use an additional object called a gradient stop collection. As the name suggests this object contains information about the colors that are involved and how they change.

Listing 5 Creating a linear gradient brush

// Create gradient brush for background
ComPtr<ID2D1GradientStopCollection> gradientStopCollection;
D2D1_GRADIENT_STOP gradientStops[2];

if (SUCCEEDED(hr))
{
   gradientStops[0].color = ColorF(BackgroundColor);
   gradientStops[0].position = 0.0f;
   gradientStops[1].color = ColorF(ColorF::White);
   gradientStops[1].position = 0.25f;
}

if (SUCCEEDED(hr))
{
   hr = m_renderTarget->CreateGradientStopCollection(
      gradientStops,
      2,
      D2D1_GAMMA_2_2,
      D2D1_EXTEND_MODE_CLAMP,
      &gradientStopCollection
      );
};

if (SUCCEEDED(hr))
{
   hr = m_renderTarget->CreateLinearGradientBrush(
      LinearGradientBrushProperties(
         Point2F(m_renderTarget->GetSize().width, 0),
         Point2F(m_renderTarget->GetSize().width, m_renderTarget->GetSize().height)),
      gradientStopCollection,
      &m_backgroundLinearGradientBrush
      );
}

gradientStopCollection = nullptr;

Listing 5 defines two gradient stop structures and each structure gives details of the color and the position that this color extends along the gradient axis. The important details in Listing 5 are that the gradient starts with a light purple (BackgroundColor) which merges with the second color, white. The gradient stop for white is at 25 percent along the gradient axis so this means that the color is solid light purple at the top, and then fades to fully white at the 25 percent position, from the 25 percent position to the 100 percent position gradient is completely white. Figure 2 shows how gradient stops are related to the changes in color. The gradient axis will be defined in a moment; the gradient stops just determine how color changes along the axis.

Figure 2 Illustrating the features of linear gradient brushes

Ff934857.f3f5913c-393b-4066-996c-1a458f6bd2fc-thumb(en-us,MSDN.10).png

You can have any number of stops and Direct2D will attempt to merge the colors you provide. To do this you create a stop collection object passing the information about the color stops to the ID2D1RenderTarget::CreateGradientStopCollection method. Finally, the ID2D1RenderTarget::CreateLinearGradientBrush method is called to create the brush. This method requires information about the gradient axis. The gradient axis specifies in what direction the color changes. Hilo uses the helper method from the D2D1 namespace to initialize a D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES structure. This structure simply has two points, one describing the start and the other describing the end of the gradient axis.

The most obvious feature of the gradient axis is the angle of the axis. Listing 5 shows that the axis is a vertical line (the x position of the start point is the same as the end point), the code defines this line on the right side of the carousel window, but the same effect is achieved if the line had any x position. Figure 2 shows the gradient axis compared to the gradient stops. The gradient stops define the percentage change over the length of the gradient axis, but the CreateLinearGradientBrush method gives the actual length of the axis.

However, the gradient axis has some subtle features. The CreateLinearGradientBrush method simply creates the brush, it does not do any drawing. Listing 6 shows the code from CarouselPaneMessageHandler::DrawClientArea that uses the gradient brush. This code shows that the brush is used to paint an area that is exactly the same height as the gradient axis. The ID2D1LinearGradientBrush interface has methods to get and set the end points of the gradient axis, so you can change these points if the area is a different size at the time it is painted than when the brush is created. In the Hilo Browser code this situation will never occur and hence the brush is used with the same property values that it was created.

Listing 6 Using a linear gradient brush

D2D1_SIZE_F size = m_renderTarget->GetSize();
m_renderTarget->BeginDraw();
m_renderTarget->SetTransform(Matrix3x2F::Identity());
m_renderTarget->FillRectangle(RectF(
   0, 0, size.width, size.height), m_backgroundLinearGradientBrush);

// Other code

// End Direct2D rendering
hr = m_renderTarget->EndDraw(); 

Drawing Text

Direct2D allows you to draw text on the render target through the ID2D1RenderTarget::DrawText or DrawTextLayout methods. Before you can draw any text you must first create an object that holds information about the font to use and information like line spacing and text alignment. These objects are called text format objects and to create one you need to use DirectWrite. DirectWrite is similar to Direct2D in that the objects have COM-like interfaces but are created through a factory object created by a global function. In Hilo the DirectWrite factory object is created through the Direct2DUtility::GetDWriteFactory static method. Once you have an IDWriteFactory object you can create a text format object by calling the IDWriteFactory::CreateTextFormat method. Listing 7 shows the code used by the carousel panel in the Hilo Browser project to create the text format object. The parameters are self-explanatory, but it is worth pointing out that the size is in DIPs not points.

Listing 7 Creating a Text Format object

// member variables: 
//   ComPtr<IDWriteFactory> m_dWriteFactory;
//   ComPtr<IDWriteTextFormat> m_textFormat;

hr = m_dWriteFactory->CreateTextFormat(
   L"Arial",
   nullptr,
   DWRITE_FONT_WEIGHT_REGULAR,
   DWRITE_FONT_STYLE_NORMAL,
   DWRITE_FONT_STRETCH_NORMAL,
   12,
   L"en-us",
   &m_textFormat
   );

Once you have a text format object you can draw text to the render target with a call to DrawText. The simplest way to call this method is to pass five parameters: the string and its length, the text format object, a rectangle where to draw the text ,and a brush. The DrawTextLayout method is similar, but instead it is passed a text layout object that encapsulated the string, the text format object and the size of the area where to draw the text. Listing 8 shows the code from the CarouselThumbnail::CreateTextLayout method that creates a text layout object.

Listing 8 Creating a Text Layout object

HRESULT hr = S_OK;

// Set the text alignment
m_renderingParameters.textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);

if (nullptr == m_textLayout)
{
   hr = m_dWriteFactory->CreateTextLayout(
      m_thumbnailInfo.title.c_str(),
      static_cast<unsigned int>(m_thumbnailInfo.title.length()),
      m_renderingParameters.textFormat,
      m_rect.right - m_rect.left,
      16,
      &m_textLayout
      );
}

In this code the m_renderingParameters member variable is set by the carousel object with a reference to the rendering target, the text format object and the brush used to draw the text. The actual text for the object is drawn in the CarouselThumbnail::Draw method, shown in Listing 9. The first parameter is the location to draw the text (since the format object is set to center align text, Listing 8, this point is the center of the text). The second parameter is the text layout object which contains details of the text and the font to use, the third parameter is the brush and finally there is a parameter for options.

Listing 9 Drawing text

m_renderingParameters.renderTarget->DrawTextLayout(
   D2D1::Point2F(m_rect.left, m_rect.bottom),
   m_textLayout,
   m_renderingParameters.solidBrush,
   D2D1_DRAW_TEXT_OPTIONS_CLIP);

Next | Previous | Home