Introduction
In this article, I hope to show you some of the really amazing stuff that GDI+ can do. I have been working on a paint program for 18 months now, and I am amazed at how much stuff I had to learn the hard way which is now just a piece of cake, because GDI+ does the work for you. The subject of this article is a simple paint program, which allows you to load and save images, as well as create new ones, and draw on them free hand, creating lines/filled shapes and gradient filled shapes, and also a soft brush (a brush which is solid in the centre and has progressively more transparency towards its edge.)
I will assume you are familiar with concepts presented in my first two articles, and I suggest for homework you refer to them and implement some of the tools covered in my GDI Brushes and Matrices article to Doodle. You'll find it won't take much to create something easily as good as the Paint package that comes with Windows.
I suppose if you're like me, you'll start by downloading the executable and playing with it to see if it does stuff you want to learn. Go ahead - when Doodle starts, it will present you with a default canvass of 350 x 350 pixels. Click on View/Pen Options to choose what tool you are using, and choose the colour/alpha value. The second colour value option is for the gradient fill - the fill works from the edges to the centre. You can load existing images or create one of a different size by clicking Load or New respectively.
There, got that out of your system? It's just the tip of what we can do, I promise you. Let's quickly talk about what I am going to cover in this article.
- Loading/Saving Images
- Hatch Brushes
- Path Gradient Brushes
- Paths
- Pens
- Drawing Lines
First of all, let's talk hatch brushes. In our OnEraseBackground
function, we select a HatchBrush
and use it to draw the SDI area not covered by the bitmap. These have come a long way from GDI, which offered six hatch values. GDI+ offers 52, as follows:
HatchStyleHorizontal = 0,
HatchStyleVertical = 1,
HatchStyleForwardDiagonal = 2,
HatchStyleBackwardDiagonal = 3,
HatchStyleCross = 4,
HatchStyleDiagonalCross = 5
HatchStyle05Percent = 6,,
HatchStyle10Percent = 7,
HatchStyle20Percent = 8,
HatchStyle25Percent = 9,
HatchStyle30Percent = 10,
HatchStyle40Percent = 11,
HatchStyle50Percent = 12,
HatchStyle60Percent = 13,
HatchStyle70Percent = 14,
HatchStyle75Percent = 15,
HatchStyle80Percent = 16,
HatchStyle90Percent = 17,
HatchStyleLightDownwardDiagonal = 18,
HatchStyleLightUpwardDiagonal = 19,
HatchStyleDarkDownwardDiagonal = 20,
HatchStyleDarkUpwardDiagonal = 21,
HatchStyleWideDownwardDiagonal = 22,
HatchStyleWideUpwardDiagonal = 23,
HatchStyleLightVertical = 24,
HatchStyleLightHorizontal = 25,
HatchStyleNarrowVertical = 26,
HatchStyleNarrowHorizontal = 27,
HatchStyleDarkVertical = 28,
HatchStyleDarkHorizontal = 29,
HatchStyleDashedDownwardDiagonal = 30,
HatchStyleDashedUpwardDiagonal = 31,
HatchStyleDashedHorizontal = 32,
HatchStyleDashedVertical = 33,
HatchStyleSmallConfetti = 34,
HatchStyleLargeConfetti = 35,
HatchStyleZigZag = 36,
HatchStyleWave = 37,
HatchStyleDiagonalBrick = 38,
HatchStyleHorizontalBrick = 39,
HatchStyleWeave = 40,
HatchStylePlaid = 41,
HatchStyleDivot = 42,
HatchStyleDottedGrid = 43,
HatchStyleDottedDiamond = 44,
HatchStyleShingle = 45,
HatchStyleTrellis = 46,
HatchStyleSphere = 47,
HatchStyleSmallGrid = 48,
HatchStyleSmallCheckerBoard = 49,
HatchStyleLargeCheckerBoard = 50,
HatchStyleOutlinedDiamond = 51,
HatchStyleSolidDiamond = 52,
HatchStyleTotal,
HatchStyleLargeGrid = HatchStyleCross,
HatchStyleMin = HatchStyleHorizontal,
HatchStyleMax = HatchStyleTotal - 1
A menu option to change the hatch style would be a very easy thing to add to the program, and would allow you to explore some of the great new styles. But it gets better. The HatchBrush
constructor looks like this:
HatchBrush(
HatchStyle hatchStyle,
const Color& foreColor,
const Color& backColor
)
In a nutshell, this means as well as the hatch style, you get to specify the colours with which it is drawn. All of the Graphics::Fill
XXX methods will accept a HatchBrush
, allowing you to draw any shape you like using these patterns.
Although there is no reason for Doodle not to offer shapes drawn with a linear gradient, I have covered the LinearGradientBrush
previously, and so in Doodle I used the PathGradientBrush
.
The PathGradientBrush
constructors look like this:
PathGradientBrush(const GraphicsPath* path)
PathGradientBrush(const Point* points, INT count, WrapMode wrapMode)
PathGradientBrush(const PointF* pointsF, INT count, WrapMode wrapMode)
Although the methods that accept points accept a wrapmode, which looks more useful, the fact is that we can get the points out of a GraphicsPath
, using a GraphicsPathIterator
, and the GraphicsPath
object itself has some very cool options that we will now discuss, although Doodle makes no use of them, it can easily be made to.
GraphicsPath(FillMode fillMode)
The FillMode
enumeration allows us to specify alternate or winding filling. Alternate
is the default method, it fills every second area, in other words if a line crosses an odd number of path segments, the start point is inside the closed region and is therefore part of the fill or clipping area. The winding method also considers the direction of each segment. Once you have a GraphicsPath
object, you can use the following methods to add to it.
GraphicsPath::AddArc
GraphicsPath::AddBezier
GraphicsPath::AddBeziers
GraphicsPath::AddClosedCurve
GraphicsPath::AddCurve
GraphicsPath::AddEllipse
GraphicsPath::AddLine
GraphicsPath::AddLines
GraphicsPath::AddPath
GraphicsPath::AddPie
GraphicsPath::AddPolygon
GraphicsPath::AddRectangle
GraphicsPath::AddRectangle
GraphicsPath::AddString
Typically, a lot of these functions have four overloads, that take a rect built of REAL
s or INT
s, or the corresponding points in either REAL
or INT
form. Obviously, some of them require only two (AddLine
), or have more specific data requirements (AddString
, AddCurve
, etc.) We will be using AddLine
in Doodle, but I hope you can see the possibilities as we discuss some of the things a GraphicsPath
can do.
Some of the methods of GraphicsPath
include:
Status Flatten(const Matrix* matrix, REAL flatness)
- allows you to provide an optional transformation matrix and turn curves into a collection of points. Status GetBounds(Rect* bounds, const Matrix* matrix, const Pen* pen)
- fills the bounds variable with the Rect
that defines the size of the path Status Reverse()
- reverses the order of the points in the path Status Transform(const Matrix* matrix)
- applies a matrix, allowing for scaling, rotation and translation Status Warp(const PointF* destPoints, INT count, const RectF& srcRect, const Matrix* matrix, WarpMode warpMode, REAL flatness)
- fills destPoints
with a copy of the path, with an optional transformation matrix applied, curves flattened if desired, using perspective or bilinear warping. I can't wait to try this on some string
s... Status Widen(const Pen* pen, const Matrix* matrix, REAL flatness )
- widen a path by the width of the pen specified
Wow - looks like fun, doesn't it ? In our OnMouseMove
function, you'll notice that we AddLine
between the last point and the current one, so our end result is a path that follows the journey our mouse has made since we pressed the left button. When we lift the mouse, we check the drawing mode, and if it is filled shapes, we do this:
SolidBrush brush(m_Colour);
graphics.FillPath(&brush, &m_Path);
So the end result is a filled shape in our main colour. If we selected a gradient brush, then things become more interesting. This introduces the PathGradientBrush
. This brush fills the path using our secondary colour as a centre, and our primary colour on the outer edges. We have already discussed its constructors, its other methods allow us to do things like specify the blend rate, the centre point, the blend style, gamma correction, wrap mode, matrix transformations, etc. We also use it to create a soft brush as follows:
GraphicsPath path;
path.AddEllipse(point.x, point.y, m_Width, m_Width);
PathGradientBrush brush(&path);
brush.SetCenterColor(Color(255, m_Colour.GetRed(), m_Colour.GetGreen(),
m_Colour.GetBlue()));
Color colors[] = { Color(0, m_Colour.GetRed(), m_Colour.GetGreen(),
m_Colour.GetBlue()) };
INT count = 1;
brush.SetSurroundColors(colors, &count);
graphics.FillEllipse(&brush, point.x, point.y, m_Width, m_Width);
path.Reset();
The idea of a soft brush is simple - we draw in a colour, and the colour blends into the image because the brush is solid in the middle and fades around the edges. To create this effect in GDI+, we simply make our centre colour full alpha on (255), and the edges the same colour but full alpha off (0). Then it's a simple case of creating the desired shape (usually a circle) into a path and applying the path to construct our brush. This, in particular, was a lot of work in GDI - my soft brush class in my paint program is about 10% of its original size using these methods.
As we move our mouse in Doodle, we draw a line using the most basic object available to us: a Pen
set to a hard colour. The Pen
constructors look like this:
Pen(const Brush* brush, REAL width)
- allows you to use a brush to draw textured lines/gradient lines/hatched lines Pen(const Color& color, REAL width)
- the basic constructor we use to draw solid lines Pen(const Pen& pen)
- make a copy Pen(GpPen* nativePen, Status status)
- wherever you see the word 'native
' in a method, it is for internal use, do not use it.
Pens can also do some cool things. In Doodle, we offer four line styles. In fact, there are eleven, as follows:
LineCapFlat = 0,
LineCapSquare = 1,
LineCapRound = 2,
LineCapTriangle = 3,
LineCapNoAnchor = 0x10,
LineCapSquareAnchor = 0x11,
LineCapRoundAnchor = 0x12,
LineCapDiamondAnchor = 0x13,
LineCapArrowAnchor = 0x14,
LineCapCustom = 0xff,
LineCapAnchorMask = 0xf0
I can't see what LineCapTriangle
does, if anyone else can, please enlighten me. I suspect it is to be added. The XXXAnchor
line caps create an anchor, i.e., a shape that is larger than the line being drawn. Line caps are specified using one of the following methods:
Status SetLineCap(LineCap startCap, LineCap endCap, DashCapCap dashCap)
Status SetEndCap(LineCap endCap)
Status SetStartCap(LineCap startCap)
Status SetDashCap(DashCap dashCap)
Start and End cap are self evident, dash cap works with these methods:
Status SetDashOffset(REAL dashOffset)
Status SetDashPattern(const REAL* dashArray, INT count)
Status SetDashStyle(DashStyle dashStyle)
enum DashStyle{
DashStyleSolid = 0,
DashStyleDash = 1,
DashStyleDot = 2,
DashStyleDashDot = 3,
DashStyleDashDotDot = 4,
DashStyleCustom = 5
};
and specifies how the ends of the dashes are capped. When drawing line sequences, you can also specify how the line joins are drawn, which I'll leave you to implement in Doodle. You could also implement the other line caps and differing caps on the start and end of lines. You would want to do this in a line mode, where you stretch out lines, because doing so in a free mode would just look ugly, unless you used the path to apply the caps when a line is finished and not before.
Finally, the most request GDI feature of all: loading and saving images. In hindsight, I guess it seems odd that GDI did not offer this, but certainly the boys at Redmond have now made it easy to load bitmaps from disk and save them out again. Loading is a piece of cake, you simply use this method:
Bitmap(const WCHAR* filename, BOOL useIcm)
Converting a CString
to WCHAR
was covered in my GDI+ Brushes and Matrices article, but I have since discovered that CString
does it all for you. The CString
constructor will accept a BSTR
, and to convert back, the AllocSysString
function returns a BSTR
. So the end result looks like this:
CString filename(lpszPathName);
m_Bitmap = Bitmap::FromFile(filename.AllocSysString());
The default value of useIcm
is false
, ICM is a colour correction method. Bitmap
is inherited from Image
and as well as loading from disk, adds methods for Get/SetPixel
and LockBits
, which gives us easy access to the raw data if we need it (for example to apply filters).
Saving is a little more involved, but not much. We require a function to access the Clsid
of the encoder we require. GDI+ does not have one, but fortunately, one is provided in the documentation:
int GetCodecClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0;
UINT size = 0;
ImageCodecInfo* pImageCodecInfo = NULL;
GetImageEncodersSize(&num, &size);
if(size == 0)
return -1;
pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if(pImageCodecInfo == NULL)
return -1;
GetImageEncoders(num, size, pImageCodecInfo);
for(UINT j = 0; j < num; ++j)
{
if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
{
*pClsid = pImageCodecInfo[j].Clsid;
return j;
}
}
return -1;
}
Now to save a file, we use the Image::Save
function, which has the following prototype:
Status Save(
const WCHAR* filename,
const CLSID* clsidEncoder,
const EncoderParameters* encoderParams
)
The EncoderParameters
parameter defaults to NULL
, so that just leaves us the job of getting a Clsid
. The helper function requires a Clsid*
to fill, and a string
. The values for this string
are:
image/jpeg
image/gif
image/tiff
image/png
image/bmp
In Doodle, we check the last three characters of the filename given and then load a Clsid
accordingly. We default to bmp
if no recognised format was found. This area of the program could do with some prettying up, in the sense that we have not taken the time to put the available file types into our file dialog. I will mention at this stage that all through Doodle the idea is to present how things are done in an uncluttered way, so very little error checking is done. GDI+ is very COM like, in that most functions return a Status
object and use references or pointers passed in to provide non-error related return values. In the 'real world', you would check the return values in order to ensure that your code was behaving as expected.
One last tip - I spent a day trying to get the file save to work before I realised that I had placed the code in the OnSaveDocument
function of my CDocument
class. If you do this, make sure you don't call the base class - it attempts to serialise the file and as the Serialise
function is empty, overwrote my file. Amusingly, I fixed exactly the same problem for someone else earlier in the week, when I spotted it right away. The moral is: when using something new, don't assume it is broken too quickly - if I'd had a closer look at my code instead of assuming the Save
method was being quirky, I should have found it just as easily as I did earlier in the week.
Well, we've covered a lot of ground this time - loading and saving files, creating filled shapes, soft brushes and more. I'd recommend anyone serious about learning GDI+ refer to my GDI+ Brushes and Matrices article and apply the techniques there to Doodle. The best way to learn is to do, and you've got all the code between the two articles, so it should be an easy start. Good luck !!
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.