Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

GDI+ Brushes and Matrices

0.00/5 (No votes)
30 May 2001 2  
Using GDI+ to draw solid/gradient filled and textured shapes

Sample Image - GDIPlusBrushes.jpg

Introduction

Welcome to my second article on GDI+. This tutorial assumes you know everything covered in the first article, namely that you know where to get GDI+, how to set it up, how to create a project that correctly initialises and shuts it down, and how the Color class works.

OK, you're still with me. This article will cover three of the five brush types ( SolidBrush, LinearGradientBrush and TextureBrush), with PathGradientBrush fitting more into the next tutorial, on using Pens and Path's, and HatchBrush being left out mainly because I didn't find it interesting, and certainly it is pretty self explanatory once you've gone through this tutorial.

Before I present any brushes, I want to present three methods that we will be using on the canvas in the first example, and on the brush for the other two.

Status RotateTransform(
  REAL angle,
  MatrixOrder order
)

Status ScaleTransform(
  REAL sx,
  REAL sy,
  MatrixOrder order
)

Status TranslateTransform(
  REAL dx,
  REAL dy,
  MatrixOrder order
)

First, let me explain that a REAL is a typedef for a float. The MatrixOrder is a little more interesting, it is a constant with the possible values MatrixOrderPrepend ( the default) or MatrixOrderAppend.

Anyone familiar with 3D graphics will recognise these methods - Rotate obviously rotates an object, Scale allows us to resize an object, and Translate moves an object. Actually it would be more correct to say that our point of reference is modified, and this is reflected in the drawing operations which follow. The reason for the MatrixOrder would also be obvious to anyone who has done 3D work. Imagine I am going to do a Rotate and a Translate. If I rotate first, what my object considers to be up is changed, and so the direction of my Translate is going to be affected. If I rotate by 45 degrees and apply a Translate of (100, 0), I will move 100 units on the diagonal, but if I reverse the order I will move hard right and then rotate. The example program allows you to experiment with the order of operations and see how the end result differs. In 'real life' you can define two operations of each type, a prepend and an append. It is also possible to create a matrix, perform operations on it, and then pass it in to an object.

On to our first brush then. It's the SolidBrush, which has an empty constructor, and one that takes a COLOR as it's parameter. This brush is roughly the equivelant of a CBrush - it's only methods are GetColor amd SetColor. To draw a solid red rectangle, for example, you can do this

SolidBrush brush(Color(255,255,0,0));
graphics.FillRectangle(&brush, 300, 300, 100, 50);  
   // Note the last two parameters are Width and Height, NOT X2/Y2 

As this class uses solid colours, it does not benefit from any sort of matrix operation, so when you are drawing using the solid brush, these operations are performed on the canvas in the demo program. The methods used are in the code as follows:
graphics.ScaleTransform(m_ScaleX, m_ScaleY, m_AScale ? 
	MatrixOrderPrepend : MatrixOrderAppend);
				
graphics.TranslateTransform(m_TransX, m_TransY, m_ATrans ? 
	MatrixOrderPrepend : MatrixOrderAppend);

graphics.RotateTransform(m_Rotate, m_ARotate ? 
	MatrixOrderPrepend : MatrixOrderAppend);
Note that these operations can be performed on the canvas equally if you use another brush type in your own code.

The next class is far more interesting: LinearGradientBrush. It has a hefty seven constructors available, which allow us to preset a number of options, all of which are available later through Set methods. All of these methods conspire to do the same thing: set up a brush which has two colours know to it, and in some cases a gradient mode. The gradient mode can be set to one of the following:

LinearGradientModeVertical
LinearGradientModeHorizontal
LinearGradientModeBackwardDiagonal
LinearGradientModeForwardDiagonal

There is also a constructor which takes an angle for the gradient, which is IMHO far more flexible. This is the constructor we use in the demo program, like this:

LinearGradientBrush brush(rc,            // Rect of gradient

	Color(m_Alpha1, m_Red1, m_Green1, m_Blue1), // First colour

	Color(m_Alpha2, m_Red2, m_Green2, m_Blue2), // Second colour

	m_Rotate,                        // Angle of gradient

	TRUE);                           // Is angle Scalable

As you can see, there are two variables not yet explained. The first is a rect which defines the size of a gradient. By making this different to the Rect of the object you are drawing, you can make a gradient repeat itself. The same effect can be had by modifying the scale variable with a number between 0 and 1. The BOOL at the end has the name isAngleScalable.

Another varibale you can enter into the example program is Blend Focus, a value between 0 and 1. This value specifies at what point in the fill the gradient has reached a point of 50% gradient. This is accomplished with the following code:

REAL blend1[3] = {0, .5, 1.0};
REAL blend2[3] = {0, m_Focus, 1.0};
brush.SetBlend(blend1, blend2, 3);

This is just the tip of the cool stuff you can do with this function. Three is the minimum number of values you can specify, with the first array specifying the percentages at different points, the second the points in terms of percentage along. You can see that this is a powerful and flexible function which opens all sorts of doors.

Additionally you can set the blend type from the menu to be normal, bell or triangle. In all cases the focus variable is used, as follows:

case bell:
	brush.SetBlendBellShape(m_Focus);
	break;
case triangle:
	brush.SetBlendTriangularShape(m_Focus);
	break;

If you play with the matrix operations in this part of the example program, you'll find you can do all sorts of interesting things. For example, setting a scale less than one allows you to create a repeating gradient fill. These operations are performed on the brush as follows:

brush.TranslateTransform(m_TransX, m_TransY, m_ATrans  ? 
	MatrixOrderPrepend : MatrixOrderAppend);
brush.ScaleTransform(m_ScaleX, m_ScaleY, m_AScale ? 
	MatrixOrderPrepend : MatrixOrderAppend);

Note it is possible to rotate a blend, but this does not create the same effect as specifying a rotation in the constructor, and I have left it out, as applying the same value in two spots does not give a good representation of what it does.

The example program also draws a black line pattern in the background, allowing you to see the effect of a gradient in the alpha channel. LinearGradientBrush has many capabilities I have not covered, such as setting gamma correction, interpolation colours, &tc.

The final, and most powerful, brush is TextureBrush. In order to cover it, I must first explain another GDI+ class - Image. This class can be constructed from an IStream, which is a COM interface, or a filepath. There is another class, called Bitmap, derived from Image, which can be constructed from a HBITMAP, a surface (DirectDraw), an icon, a pointer to bit data, and more. In the example program I provide a picture of my daughter which is hard coded as the bitmap. This class will load a number of image formats, including jpeg, tiff, bmp, png, &tc. It also supports setting up of other codecs, for example I will end up putting one together for Targa, which we use a lot at work. What is cool about Image is that it can also save in these formats. If you want to load an Image from a user specified file, you'll need to convert a string to a WCHAR string, which is done as follows:

// Assuming the string 's' contains our filepath

WCHAR*  filename = new WCHAR[s.GetLength()+1];
mbstowcs(filename, s, s.GetLength()+1);

Image image(filename);
delete filename;

Once we have an Image object initialised, we can create our texture brush as follows:

Image image(L"image.jpg");

Rect rct(0, 0, image.GetWidth(), image.GetHeight());

TextureBrush brush(ℑ,rct);

Note this works similar to the gradient - if we specify a Rect the same as we are drawing, the image will not scale to fit, but the tiling will not work. In order to get an image to scale to a rectangle, we need to use Translate and Scale.

As well as scaling, rotating and translating our image, we can set the wrap mode, which is available to you through the menu. The possible values are:

WrapModeClamp       (Draws the image at 0,0 at correct size, once only)
WrapModeTile        (Draws the image over and over in all directions)
WrapModeTileFlipX   (As above, but every second image is flipped in the X plane)
WrapModeTileFlipY   (As WrapModeTile, but every second image flipped in the Y plane)
WrapModeTileFlipXY  (As WrapModeTile, flipping both in the X and Y plane 
                     every second tile)

You'll also find that rotation, scaling and translation present a number of possibilities in combination with the above.

The example program provided allows you to experiment with different colour/alpha values as well as different values for all three matrix operations. Hopefully playing with this will give you a good idea as to how all these variables fit together to produce a final result, especially what happens when you change a matrix order of operation.

There are two other sets of options provided, one in the menu, the other in the dialog. Firstly, in the menu you can choose the smoothing mode to be one of the following:

graphics.SetSmoothingMode(SmoothingModeHighSpeed);
graphics.SetSmoothingMode(SmoothingModeAntiAlias);
graphics.SetSmoothingMode(SmoothingModeHighQuality);

If you are unfamiliar with the concept of antialiasing, imagine opening Paint and draw a diagonal line that is less than 45 degrees in pitch. You'd expect to see a jagged line, with sharp jumps, made more so by lower screen resolution or zooming. This is caused by an obvious fact: our screens have defined resolutions and this combined with our monitor size sets a pixel size, the minimum size of a 'dot' on the screen. Anti-aliasing attempts to do better by dealing in imaginary 'sub pixels', and setting pixel values to be a fraction of the R/G/B value we are using, depending on how much many 'sub pixels' are in the pixel we are about to draw. It makes no difference on a square, so choose 'ellipse' from the shape menu, and then you will see the difference between the two modes. The last mode is high quality, which according to the help files is an advanced form of anti-aliasing especially designed to take advantage of the capabilities of LCD screens. I don't have an LCD screen, so they look the same to me ;0)

Finally, there is a group of options on the bottom, which allow you to experiment with the DrawImage function. There are 16 versions of this function, the one we are using has this prototype:

Status DrawImage(
  Image* image,
  const PointF* destPointsF,
  INT count,
  REAL srcxR,
  REAL srcyR,
  REAL srcwidthR,
  REAL srcheightR,
  Unit srcUnit,
  ImageAttributes* imageAttributes,
  DrawImageAbort callback,
  VOID* callbackData)

The ability to specify a callback makes this a very powerful function, but we will limit ourselves to just two of the options available. First of all, if we pass in three points, they specify the top left, top right and bottom *left* ( NOT right, as you might think ) corners of a parellelogram. To see this in action, just check 'Parallelogram'.

You'll note that you are now looking at a different image: it has magenta all around it. If you check Use Colour Key, the magenta is not drawn, and we see the dialog underneath. This is done by setting the transparent colour in the ImageAttributes object we then pass in. In fact, we can set a range by specifying two colours, like this:

Status SetColorKey(
  const Color& colorLow,
  const Color& colorHigh,
  ColorAdjustType type)

The set colour button allows you to change the colour, which we pass as both parameters. You can replace image.bmp in the program directory to try other images if you want to.

Other things you can do with the ImageProperties object is set colour threshold, map colour swapping ( i.e. turn red into blue), set gamma (intensity), colour wrap mode, etc. It is a very powerful tool and certainly deserves a lot more attention than I have given it here. It can be used with DrawImage or with a TextureBrush, allowing it to be used in conjuction with the texture wrap modes/matrix operations provided there.

I hope I've whetted your appetite with this tutorial, which gets more into some of the exciting possibilities opened by GDI+. Unless I get flamed to death for these two articles, I'll be presenting some more shortly, firstly on using Pens ( which can also be texture wrapped ) and path objects to draw things. I will probably also cover how to create a soft brush ( even though it's frustrating that I had to roll my own and now Microsoft are giving them away ;0) Thanks for reading - if you have any questions, I virtually live here, so I'm sure to answer anything sent to me via email, or in the comments section here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here