.
In Depth Banner
Skip Navigation LinksWelcome > In Depth articles > SimpleDraw

Select your preferred language

SimpleDraw, a Windows Forms Graphics Editor Example.

Windows is an event driven system. Your code should be designed to respond to the events as they arrive, service them as quickly as possible and then do nothing until the next event arrives.

Furthermore, each event has a specific context and so you should try, wherever possible, to limit the actions of your event handler code to the specific task the event requires.

A classic example of bad event management is performing complex drawing tasks in the MouseMove event handler or performing file access tasks in the Paint event handler.

Maintaining state.

Event driven programs rely on state information for their continuity. A graphics program that draws a line for example, will see literally hundreds of mouse move events during the time it takes for the user to draw a line across the screen. The important part of this operation is to maintain the state of the operation until the user decides the line is in the right place and releases the mouse button. Such a line would have to be continuously drawn, erased and redrawn to provide user feedback.

The sequence of events would be as follows:

1.    User depresses the mouse button. This is the start point of the line. For its state information, the program stores this start point and signals the fact that the user needs feedback

2.    mouse is moved. This is the end point of the line. the area under the current line and, if any, the old line to be removed is invalidated. For its state information, the program stores the current point and calculates an invalidation rectangle which is used during the paint routine.

3.    Paint event draws the invalid background and checks state to see if the user needs feedback. If yes, the feedback is drawn Paint uses the feedback flag, the stored start and end points plus the invalid rectangle to accomplish its draw cycle. (Events 2 and 3 repeat as many times as necessary).

4.    User releases the mouse button and the final drawing is done.

Following the lead of the event loop and constructing an application to do no more but no less than what is required at any particular instant is the secret to creating great Windows Forms applications.

Discussing the demo

To give you a good idea of what's involved in making a Windows Forms application that works within the event driven system as proposed above, the SimpleDraw application performs many of the tasks that cause the most trouble.

This is an MDI application that enables you to open and modify images or create new ones and save them. The screen shot in figure 1 shows the final application in action.

Figure 1: SimpleDraw.

The salient points of the application are listed below.

  • The MDI application supports multiple open files and a mechanism is provided that maintains the context of each child form so that a colour or brush chosen for a form will be maintained even when the user switches to a different form and uses a different tool.
  • The toolbar uses an owner-drawn dropdown menu to show the brush type or line type selected.
  • The child forms use scrollbars to move around the image. Scrollbar settings and mouse positions are taken into consideration when drawing on screen.
  • The application maintains an image in the background that is used to accumulate pixels on as drawing progresses and for clearing the invalid portions of the screen during rubber banding operations.
  • A rubber banded line, rectangle and ellipse tool is provided to illustrate the correct method of rubber banding under GDI+. This is to say that all painting is done in the paint routine and XOR'ed drawing is not used.
  • Properties are used to enable the child form to communicate with the parent form. Specifically the child form updates the parent's status bar for position feedback.
  • Images may be opened, modified and saved back to the same filename.

MDI application specifics.

Basic events in the form of menu clicks, toolbar use and so-on are the fundamental form of communication between the user and the application. In the case of an MDI application, the main parent form manages all menus and status bars. A child form can have a menu, but it's contents will be merged wit that of the main form to make a single compound menu. When setting up the menu's it is usual for the main form to contain only those items which can be done without a child form open such as create a new file, open an existing file and exit from the application. SimpleDraw's  main form menu's are shown in figure 2.

Figure 2. Main form menu items.

The child forms menu items will contain all the additional things you can do when a child form is open, such as save the file. Figure 3 shows the child form menus.

Figure 3. The child form menu

Note that in both cases, the topmost "File" menu item will have the MergeType property set to MergeItems and each of the items in the two menus will be given a merge order number. In this case the merge orders are:

1.                New

2.                Open

3.                Save *

99.           The separator above the Exit

100.      Exit

*Save is from the child form

Now, whenever the child form is active, the menus will be merged in the correct order and the events directed to the appropriate place in the main form or the child form.

Where are my settings?

SimpleDraw uses a system whereby each of the child forms could maintain their own state information and remember which tools and colours were selected so that these could be saved whenever a child form was deactivated and restored when reactivated. To do this, I created some properties in the main form that could be accessed by the child form.

In the main form, these fields and properties are responsible for maintaining the current tool and colour settings.

    public enum Tools

    {

      SmallBrush,

      LargeBrush,

      ThinLine,

      ThickLine,

      ThickDottedLine,

      Brush,

      Line,

      Rectangle,

      Ellipse

    }

 

    Tools _brushTool=Tools.SmallBrush;

    Tools _lineTool=Tools.ThinLine;

    Tools _currentTool=Tools.SmallBrush;

 

    public Tools LineTool

    {

      get{return _lineTool;}

      set

      {

        _lineTool=value;

      }

    }

 

    public Tools BrushTool

    {

      get{return _brushTool;}

      set

      {

        _brushTool=value;

      }

    }

 

    public Tools CurrentTool

    {

      get{return _currentTool;}

      set

      {

        _currentTool=value;

        UpdateToolButton();

      }

    }

 

    Color _currentColor;

 

    public Color CurrentColor

    {

      get{return _currentColor;}

      set{_currentColor=value;}

    }

Then, in the child form, the following code saves and restores the settings of the tools and colour selection.

    protected MainForm GetParent()

    {

      return (MainForm)(Parent.Parent);

    }

 

    private void ImageEditorForm_Activated(object sender, System.EventArgs e)

    {

      GetParent().CurrentTool=myCurrentTool;

      GetParent().BrushTool=myBrushTool;

      GetParent().LineTool=myLineTool;

      GetParent().CurrentColor=myCurrentColor;

      GetParent().UpdateToolButton();

    }

 

    MainForm.Tools myCurrentTool=MainForm.Tools.Brush;

    MainForm.Tools myBrushTool=MainForm.Tools.SmallBrush;

    MainForm.Tools myLineTool=MainForm.Tools.ThinLine;

    Color myCurrentColor=Color.Black;

 

    private void ImageEditorForm_Deactivate(object sender, System.EventArgs e)

    {

      myCurrentTool=GetParent().CurrentTool;

      myBrushTool=GetParent().BrushTool;

      myLineTool=GetParent().LineTool;

      myCurrentColor=GetParent().CurrentColor;

    }

An important thing to note is the use of the parent-child relationship of the different forms. The child needs to know which tools are selected so the Parent object is used. MDI applications have three layers. The main form, the MdiClient which manages the client area on which child forms are displayed and the child form itself. To find the main form, the child has to get the "grandfather" or parent of its parent.

The ToolBar

Simple in operation, the standard Windows Forms ToolBar provides a good user interface which can be adapted simply to make it more interesting.

In SimpleDraw, the toolbar uses owner-drawn menu items to provide a graphical representation of the tool or brush selected. Depending on whether the brush tool or a line drawing tool is required, a separate context menu is used to provide brush sizes or line styles.

The toolbar images are created as normal and stored in an ImageList object. There are images for all the tools plus all the brush and line styles. In the case of buttons 1-4, the operation is simple. 5 is a separator and 6 is a dropdown button that holds one of two context menus, one for brushes and one for lines.

The context menu items are all marked as owner draw and the following code is used to paint the graphical menu contents.

    private void ButtonMenuItems_MeasureItem(object sender, System.Windows.Forms.MeasureItemEventArgs e)

    {

      e.ItemHeight=32;

      e.ItemWidth=32;

    }

 

    private void ButtonMenuItems_DrawItem(object sender,  System.Windows.Forms.DrawItemEventArgs e)

    {

      ImageAttributes ia=new ImageAttributes();

      if((e.State&DrawItemState.Selected)>0)

        e.Graphics.FillRectangle(Brushes.LightBlue,e.Bounds);

      else

        e.Graphics.FillRectangle(Brushes.White,e.Bounds);

 

      Image _img=null;

      switch(((MenuItem)sender).Text)

      {

        case "SmallBrush":

          _img=this.imageList1.Images[4];

          break;

        case "LargeBrush":

          _img=this.imageList1.Images[5];

          break;

        case "ThinLine":

          _img=this.imageList1.Images[6];

          break;

        case "ThickLine":

          _img=this.imageList1.Images[7];

          break;

        case "ThickDottedLine":

          _img=this.imageList1.Images[8];

          break;

      }

      e.Graphics.DrawImage(_img, e.Bounds, 0, 0, _img.Width, _img.Height, GraphicsUnit.Pixel, ia );

    }

 

Whenever a button is selected to choose a drawing tool, the button pushed state is updated and the correct dropdown menu is associated with the button.

    private void toolBar1_ButtonClick(object sender, System.Windows.Forms.ToolBarButtonClickEventArgs e)

    {

      //Ensure all buttons are up

      foreach(ToolBarButton b in this.toolBar1.Buttons)

        b.Pushed=false;

 

      //behaviour for the colour palette button is different

      if((string)e.Button.Tag=="Color")

      {

        //A drawing colour is chosen

        ColorDialog dlg=new ColorDialog();

        dlg.Color=_currentColor;

        if(dlg.ShowDialog()==DialogResult.OK)

          _currentColor=dlg.Color;

        this.statusBar1.Refresh();;

      }

      else

      {

        //the tools are changed and the button pressed to give feedback

        switch((string)e.Button.Tag)

        {

          case "Brush":

            this.CurrentTool=Tools.Brush;

            e.Button.Pushed=true;

            break;

          case "Line":

            this.CurrentTool=Tools.Line;

            e.Button.Pushed=true;

            break;

          case "Rect":

            this.CurrentTool=Tools.Rectangle;

            e.Button.Pushed=true;

            break;

          case "Ellipse":

            this.CurrentTool=Tools.Ellipse;

            e.Button.Pushed=true;

            break;

        }

      }

    }

The CurrrentTool property selects the correct dropdown menu via the UpdateToolButton method.

    public Tools CurrentTool

    {

      get{return _currentTool;}

      set

      {

        _currentTool=value;

        UpdateToolButton();

      }

    }

 

    public void UpdateToolButton()

    {

      switch(_currentTool)

      {

        case Tools.Brush:

        {

          this.toolBar1.Buttons[5].DropDownMenu=this.contextMenu1;

          switch(_brushTool)

          {

            case Tools.SmallBrush:

              this.toolBar1.Buttons[5].ImageIndex=4;

              break;

            case Tools.LargeBrush:

              this.toolBar1.Buttons[5].ImageIndex=5;

              break;

          }

        }

        break;

        case Tools.Line:

        case Tools.Rectangle:

        case Tools.Ellipse:

        {

          this.toolBar1.Buttons[5].DropDownMenu=this.contextMenu2;

          switch(_lineTool)

          {

            case Tools.ThinLine:

              this.toolBar1.Buttons[5].ImageIndex=6;

              break;

            case Tools.ThickLine:

              this.toolBar1.Buttons[5].ImageIndex=7;

              break;

            case Tools.ThickDottedLine:

              this.toolBar1.Buttons[5].ImageIndex=8;

              break;

          }

          break;

        }

      }

    }

Creating or opening a file

When a new file is created, a blank 800 by 600 image is created. When an existing image is opened the image file is loaded, whatever it's size.

    private void menuItem2_Click(object sender, System.EventArgs e)

    {

      //New file

      ImageEditorForm f=new ImageEditorForm();

      f.Filename="Untitled.bmp";

      f.CreateNew();

      f.MdiParent=this;

      f.MouseMove+=new MouseEventHandler(f_MouseMove);

      f.Show();

    }

 

    private void menuItem3_Click(object sender, System.EventArgs e)

    {

      //Open file

      OpenFileDialog dlg=new OpenFileDialog();

      dlg.Filter="Image files|*.bmp;*.jpg;*.gif;*.tif";

      if(dlg.ShowDialog()==DialogResult.OK)

      {

        ImageEditorForm f=new ImageEditorForm();

        f.Filename=dlg.FileName;

        f.CreateFile();

        f.MdiParent=this;

        f.MouseMove+=new MouseEventHandler(f_MouseMove);

        f.Show();

      }

    }

 

Managing the status bar

The status bar contained in the main form shows two pieces of information. The pixel position of the mouse on the page, adjusted for scrollbar position, and the current drawing colour.

Mouse position is obtained by wiring an event handler to the MouseMove event of each child as it is created as shown in the previous listing.

    //Handles mouse moves from the child

    private void f_MouseMove(object sender, MouseEventArgs e)

    {

      this.PositionIndicator=new Point(e.X,e.Y);

    }

 

    public Point PositionIndicator

    {

      set{

        this.PositionPanel.Text=string.Format("{0},{1}",value.X,value.Y);

        this.statusBar1.Refresh();

      }

    }

The colour indicator is an owner drawn status bar panel which is filled with the current colour.

    private void statusBar1_DrawItem(object sender, System.Windows.Forms.StatusBarDrawItemEventArgs e)

    {

      SolidBrush br=new SolidBrush(this.CurrentColor);

      e.Graphics.FillRectangle(br,e.Bounds);

    }

Note that if a child form is closed, it should unwire its handler so that the Garbage Collector can dispose of it correctly. In the main form, the UnHook method performs this.

    public void UnHook(ImageEditorForm f)

    {

      f.MouseMove-=new MouseEventHandler(f_MouseMove);

    }

and in the child form, the UnHook method is called from the Closed event handler

    private void ImageEditorForm_Closed(object sender, System.EventArgs e)

    {

      GetParent().UnHook(this);

    }

The Editing Form

Now we can shift focus to the image editing form which is responsible for the main bulk of the work. When a new image is needed the child form is created and the CreateNew method called which creates a new 800 by 600 bitmap. _target is a bitmap used to hold the offscreen bitmap that is the target for all drawing operations.

    Bitmap _target;

 

    public void CreateNew()

    {

      _target=new Bitmap(800,600);

      Graphics g=Graphics.FromImage(_target);

      g.Clear(Color.White);

      g.Dispose();

      SetScrollBars();

      this.Text=Filename;

      _dirty=false;

    }

The scrollbars are initialized to cope with the image size so that the user can scroll around.

    protected void SetScrollBars()

    {

      this.AutoScroll=true;

      this.AutoScrollMinSize=new Size(_target.Width,_target.Height);

    }

Loading the image.

When an image is loaded a few tricks must be used to ensure that problems don't occur later on.

Primarily, an image must be able to be edited and saved back to the same file if the user so desires. GDI+ bitmaps loaded from a file normally keep the file open and locked for the lifetime of the bitmap so when an attempt is made to write the file back to disc, an exception is thrown. Secondly, not all images are suitable for editing. For example a GIF file is an indexed colour format and so you cannot obtain a Graphics for this type of image and draw on it. Therefore we can do a couple of things to prevent these problems. Opening the file using a file stream, loading the stream contents into the bitmap and explicitly closing the stream gets rid of problem number one. The second problem can be prevented by making a 32 bit per pixel image the same size as the original and drawing the newly opened bitmap onto it. This leaves us with an image that is unencumbered by file locks and in the correct format for editing.

    public void CreateFile()

    {

      FileStream fs=new FileStream(this.Filename,FileMode.Open,FileAccess.Read);

      Bitmap bm=(Bitmap)Image.FromStream(fs);

      fs.Close();

      _target=new Bitmap(bm.Width,bm.Height);

      Graphics g=Graphics.FromImage(_target);

      g.InterpolationMode=InterpolationMode.Low;

      g.DrawImage(bm,new Rectangle(0,0,_target.Width,_target.Height),0,0,bm.Width,bm.Height,GraphicsUnit.Pixel);

      g.Dispose();

      bm.Dispose();

      SetScrollBars();

      this.Text=Filename;

      _dirty=false;

    }

When a file is created or loaded, the _dirty flag is set to false. When an operation takes place which changes the file _dirty is set true to signify that a save might be required.

Painting the image.

The output of the image is done by the Paint event handler. A Matrix is used to offset the origin of the page by the amount currently in the scrollbar position.

    private void ImageEditorForm_Paint(object sender, PaintEventArgs e)

    {

      Matrix mx=new Matrix(1,0,0,1,this.AutoScrollPosition.X,this.AutoScrollPosition.Y);

      e.Graphics.Transform=mx;

      e.Graphics.DrawImage(_target, e.Graphics.ClipBounds, e.Graphics.ClipBounds, GraphicsUnit.Pixel);

 

The rest of the Paint handler routine is dedicated to user feedback and brush painting which we'll get to shortly.

Mouse movement and user feedback.

The mouse handling is relatively simple and no attempt is made to draw anything in the mouse event handlers. This is of paramount importance for a well-behaved event driven program.

MouseDown stores a starting position, used as a reference in subsequent mouse moves, and signals that the user requires feedback. In order to compensate for the scrollbar offset if any, the mouse position must be translated to it's actual position on the page. To do this, the BacktrackMouse method is provided.

    protected MouseEventArgs BacktrackMouse(MouseEventArgs e)

    {

      Matrix mx=new Matrix(1,0,0,1,this.AutoScrollPosition.X,this.AutoScrollPosition.Y);

      mx.Invert();

      Point[] pa=new Point[]{new Point(e.X,e.Y)};

      mx.TransformPoints(pa);

      return new MouseEventArgs(e.Button,e.Clicks,pa[0].X,pa[0].Y,e.Delta);

    }

 

    private void ImageEditorForm_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _feedback=true;

      _unmodifiedStartPos=new Point(e.X,e.Y);

      MouseEventArgs et=BacktrackMouse(e);

      _startPos=new Point(et.X,et.Y);

      _dirty=false;

    }

To backtrack the mouse's current position on the control surface to the relative position on the image being edited, the Matrix class is used to transform the mouse coordinates. A matrix identical to that used in the drawing method is created but then inverted and used to transform a point taken from the mouse event arguments.

MouseMove works out the amount of screen real-estate that needs to be invalidated in this pass, combines this with the area invalidated last pass and invalidates the union of the two. Figure 4 shows this.

Figure 4. Invalidating the right bits.

In the case of the user drawing a rubber-banded object, the full area to invalidate, including the area covered by the previous drawing operation and the current operation must be calculated. The event handler then stores the current rectangle so that the next event knows about it. In this example a Queue object is used to form a FIFO that maintains the chain of current and previous invalid rectangles. In reality, this is overkill for this particular application but included as an example of using the useful .NET collection classes in a graphics context.

Failure to invalidate the whole area as shown will result in traces of the portions of the ellipses in the lighter blue area being left on the screen

Note also how the invalid rectangles are calculated using the unadulterated mouse positions and that the real rectangles are made slightly larger to accommodate a wider pen if used.

    protected override void OnMouseMove(MouseEventArgs e)

    {

      _unmodifiedCurrPos=new Point(e.X,e.Y);

      MouseEventArgs et=BacktrackMouse(e);

      this._currentPos=new Point(et.X,et.Y);

 

      if(_feedback)

      {

        Rectangle InvalidRect=new Rectangle(

          Math.Min(this._unmodifiedStartPos.X,this._unmodifiedCurrPos.X)-5,

          Math.Min(this._unmodifiedStartPos.Y,this._unmodifiedCurrPos.Y)-5,

          Math.Abs(_unmodifiedStartPos.X-_unmodifiedCurrPos.X)+10,

          Math.Abs(_unmodifiedStartPos.Y-_unmodifiedCurrPos.Y)+10);

        this._invalidationQueue.Enqueue(InvalidRect);

 

        Invalidate(Rectangle.Union((Rectangle)_invalidationQueue.Dequeue(),InvalidRect));

      }

      MouseEventArgs et=new MouseEventArgs(e.Button,e.Clicks,pa[0].X,pa[0].Y,e.Delta);

      base.OnMouseMove(et);

    }

User feedback involves painting on the screen and so is provided by the Paint handler method. There are two types of feedback.

The first, used by the brush tool, simply draws the brush onto the image. Because the brush will leave a trace wherever the mouse is pressed, the identical drawing operation is carried out on the background image. The second, and most important form of feedback is the rubber-banded feedback for lines, rectangles and ellipses.

While feedback is being provided, the area calculated in the mouse move event and shown in figure 4 is copied from the background image to the screen and then the rubber-banded line drawn over the top of this. This enables you to provide a much more realistic and WYSIWYG appearance than that afforded by the old method which combined the feedback artifacts with the screen using an exclusive-or method in which a second application of the feedback graphic undid the changes to the screen. Using this more up-to-date method, you will be able to create effects using graphics, text or a mixture of both, all with the same simple principles.

The complete paint routine and its helper routines are shown below. Note that drawing a line or rectangle is done with a separate method and can work on the screen or background image by virtue of the Graphics object passed in. These methods are also used in the MouseUp handler to draw the final version of the image.

    private void DrawSmallBrush(Graphics g)

    {

      Pen p=new Pen(GetParent().CurrentColor,2);

      p.EndCap=LineCap.Round;

            g.DrawLine(p,this._startPos,this._currentPos);

      p.Dispose();

    }

 

    private void DrawLargeBrush(Graphics g)

    {

      Pen p=new Pen(GetParent().CurrentColor,6);

      p.EndCap=LineCap.Round;

            g.DrawLine(p,this._startPos,this._currentPos);

      p.Dispose();

    }

 

    private Pen CreateLinePen()

    {

      switch(GetParent().LineTool)

      {

        case MainForm.Tools.ThinLine:

          return new Pen(GetParent().CurrentColor,1);

        case MainForm.Tools.ThickLine:

          return new Pen(GetParent().CurrentColor,5);

        case MainForm.Tools.ThickDottedLine:

          Pen p=new Pen(GetParent().CurrentColor,5);

          p.DashStyle=DashStyle.Dot;

          return p;

      }

      return new Pen(GetParent().CurrentColor,1);

     

    }

 

    private void DrawLine(Graphics g)

    {

      Pen p=this.CreateLinePen();

      g.DrawLine(p,this._startPos,this._currentPos);

      p.Dispose();

    }

 

    private void DrawRectangle(Graphics g)

    {

      Pen p=this.CreateLinePen();

      Rectangle rc=new Rectangle(

        Math.Min(this._startPos.X,this._currentPos.X),

        Math.Min(this._startPos.Y,this._currentPos.Y),

        Math.Abs(this._startPos.X-this._currentPos.X),

        Math.Abs(this._startPos.Y-this._currentPos.Y));

      g.DrawRectangle(p,rc);

      p.Dispose();

    }

 

    private void DrawEllipse(Graphics g)

    {

      Pen p=this.CreateLinePen();

      Rectangle rc=new Rectangle(

        Math.Min(this._startPos.X,this._currentPos.X),

        Math.Min(this._startPos.Y,this._currentPos.Y),

        Math.Abs(this._startPos.X-this._currentPos.X),

        Math.Abs(this._startPos.Y-this._currentPos.Y));

      g.DrawEllipse(p,rc);

      p.Dispose();

    }

 

    private void ImageEditorForm_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

    {

      Matrix mx=new Matrix(1,0,0,1,this.AutoScrollPosition.X,this.AutoScrollPosition.Y);

      e.Graphics.Transform=mx;

      e.Graphics.DrawImage(_target, e.Graphics.ClipBounds, e.Graphics.ClipBounds, GraphicsUnit.Pixel);

      Graphics tg=Graphics.FromImage(_target);

      if(_feedback==true)

      {

        switch(GetParent().CurrentTool)

        {

          case MainForm.Tools.Brush:

          switch(GetParent().BrushTool)

          {

            case MainForm.Tools.SmallBrush:

              DrawSmallBrush(e.Graphics);

              DrawSmallBrush(tg);

              this._startPos=this._currentPos;

              this._unmodifiedStartPos=this._unmodifiedCurrPos;

              break;

            case MainForm.Tools.LargeBrush:

              DrawLargeBrush(e.Graphics);

              DrawLargeBrush(tg);

              this._startPos=this._currentPos;

              this._unmodifiedStartPos=this._unmodifiedCurrPos;

              break;

          }

            break;

          case MainForm.Tools.Line:

            DrawLine(e.Graphics);

            break;

          case MainForm.Tools.Rectangle:

            DrawRectangle(e.Graphics);

            break;

          case MainForm.Tools.Ellipse:

            DrawEllipse(e.Graphics);

            break;

        }

      }

      tg.Dispose();

    }

MouseUp is handled very simply. In the case of a brush operation, no further work is necessary because the changes were made on both foreground and background at once. When a feedback operation was in progress, the background remained unchanged to be used as the clean area under the invalid rectangles. Now, the drawing operation can be finalized by drawing the artifact onto the background image.

    private void ImageEditorForm_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      _feedback=false;

      Graphics g=Graphics.FromImage(_target);

      switch(GetParent().CurrentTool)

      {

        case MainForm.Tools.Line:

          DrawLine(g);

          break;

        case MainForm.Tools.Rectangle:

          DrawRectangle(g);

          break;

        case MainForm.Tools.Ellipse:

          DrawEllipse(g);

          break;

      }

      g.Dispose();

 

      Invalidate();

    }

 

Saving the work

Finally, the work needs to be saved when the user is happy with it. GDI+ affords a simple and flexible method of doing this. The File Save code is shown below. It includes a handler for the Form.Closing event which checks to see if the file has been altered and request the user to save the work if necessary.

    private DialogResult DoSave()

    {

      SaveFileDialog dlg=new SaveFileDialog();

      dlg.FileName=this.Filename;

      DialogResult result=dlg.ShowDialog();

      if(result==DialogResult.OK)

      {

        ImageFormat f = ImageFormat.Jpeg;

        switch(Path.GetExtension(dlg.FileName).ToLower())

        {

          case ".bmp":

            f=ImageFormat.Bmp;

            break;

          case ".tif":

            f=ImageFormat.Tiff;

            break;

          case ".gif":

            f=ImageFormat.Gif;

            break;

        }

        _target.Save(dlg.FileName,f);

        _dirty=false;

      }

      return result;

    }

 

    private void menuItem2_Click(object sender, System.EventArgs e)

    {

      DoSave();

    }

 

    private void ImageEditorForm_Closing(object sender, System.ComponentModel.CancelEventArgs e)

    {

      if(_dirty)

      {

        switch(MessageBox.Show("The file has changed, do you wish to save your work","File changed",MessageBoxButtons.YesNoCancel,MessageBoxIcon.Question))

        {

          case DialogResult.Yes:

            if(DoSave()==DialogResult.Cancel)

              e.Cancel=true;

            break;

          case DialogResult.No:

            break;

          case DialogResult.Cancel:

            e.Cancel=true;

            break;

        }

      }

    }

 

Summary

While by no means a complete editing package, this program is a good start as a Windows Forms graphics editor and demonstrates the basic functions. In particular it illustrates that event handling in a Windows Forms application can be performed in context without a loss of performance and that you don't need to use kludgy interop calls to create a great user feedback experience.

The Visual Studio 2003 project for this application can be downloaded from below locations.

.

Return to the main index

Copyright © Bob Powell 2000-.  All rights reserved.