Click here to Skip to main content
Click here to Skip to main content

Doodle - a basic paint package in GDI+

, 3 Jun 2001
Rate this:
Please Sign up or sign in to vote.
Using GDI+ to create a paint program with soft brushes and loading/saving images.
<!-- Download Links --> <!-- Article image -->

Sample Image - Doodle.jpg

<!-- Main HTML starts here -->

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 it's 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. Lets 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::FillXXX 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 REALs or INTs, 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) - allow 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 mtrix, 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 derised, using perspective or biliear warping.  I can't wait to try this on some strings...
 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 it's constructors, it's 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 it's 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) - where-ever 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 simple 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;          // number of image encoders
   UINT  size = 0;         // size of the image encoder array in bytes

   ImageCodecInfo* pImageCodecInfo = NULL;

   GetImageEncodersSize(&num, &size);
   if(size == 0)
      return -1;  // Failure

   pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
   if(pImageCodecInfo == NULL)
      return -1;  // Failure

   GetImageEncoders(num, size, pImageCodecInfo);

   for(UINT j = 0; j < num; ++j)
   {
      if( wcscmp(pImageCodecInfo[j].MimeType, format) == 0 )
      {
         *pClsid = pImageCodecInfo[j].Clsid;
         return j;  // Success
      }    
   } // for

   return -1;  // Failure

} // GetCodecClsid

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

Share

About the Author

Christian Graus
Software Developer (Senior)
Australia Australia
Programming computers ( self taught ) since about 1984 when I bought my first Apple ][. Was working on a GUI library to interface Win32 to Python, and writing graphics filters in my spare time, and then building n-tiered apps using asp, atl and asp.net in my job at Dytech. After 4 years there, I've started working from home, at first for Code Project and now for a vet telemedicine company. I owned part of a company that sells client education software in the vet market, but we sold that and I worked for the owners for five years before leaving to get away from the travel, and spend more time with my family. I now work for a company here in Hobart, doing all sorts of Microsoft based stuff in C++ and C#, with a lot of T-SQL in the mix.

Comments and Discussions

 
GeneralMy vote of 5 Pinmembermanoj kumar choubey20-Feb-12 21:01 
GeneralMy vote of 1 Pinmemberjessca23-Feb-10 0:15 
Generalerror C2065: 'ImageCodecInfo' : undeclared identifier PinmemberMember 48187723-May-08 1:13 
GeneralRe: error C2065: 'ImageCodecInfo' : undeclared identifier Pinmembertttdolph8-Jun-11 9:12 
Generalerror C2065: 'ImageCodecInfo' : undeclared identifier PinmemberMember 48187723-May-08 1:13 
GeneralDraw lines like a rope Pinmemberlaxmangehlot11-Apr-07 20:51 
Questionsave as bitmap PinmemberMember #38030007-Feb-07 22:45 
AnswerRe: save as bitmap PinstaffChristian Graus7-Feb-07 23:23 
QuestionRe: error - gdiplusinit.h Pinmembermla15412-Oct-06 7:53 
AnswerRe: error - gdiplusinit.h PinstaffChristian Graus12-Oct-06 7:56 
AnswerRe: error - gdiplusinit.h Pinmemberlaxmangehlot11-Apr-07 20:42 
GeneralRe: error - gdiplusinit.h, Resolved! Pinmembermla15412-Apr-07 3:16 
QuestionBrush overwrite effect PinmemberSoth12324-Sep-05 2:12 
QuestionAnother memory leak??? PinmemberWernbrake6-Aug-05 11:31 
AnswerRe: Another memory leak??? PinmemberChristian Graus7-Aug-05 13:33 
AnswerRe: Another memory leak??? PinmemberJames R. Twine7-Jan-06 16:53 
GeneralMemory Leak. Pinmemberlightdoll26-Jan-05 1:31 
GeneralRe: Memory Leak. Pinmemberlightdoll26-Jan-05 2:11 
GeneralRe: Memory Leak. PinmemberChristian Graus26-Jan-05 9:45 
GeneralRe: Memory Leak. Pinsusslightdoll28-Jan-05 7:07 
QuestionWhat is the differrece about &quot;using namespace Gdiplus&quot;? Please tell me. Pinmemberipichet21-Oct-04 21:54 
GeneralSoftbrush as a generic brush/pen Pinmembergary ng28-Dec-02 22:24 
GeneralRe: Softbrush as a generic brush/pen PinmemberChristian Graus4-Jan-03 0:54 
GeneralRe: Softbrush as a generic brush/pen Pinmembergary ng4-Jan-03 14:19 
QuestionHow to use GDI+ to display a color TIFF file Pinmemberzxz13-Dec-02 11:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141022.2 | Last Updated 4 Jun 2001
Article Copyright 2001 by Christian Graus
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid