|
|
|
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
End Enum
Dim _brushTool As
Tools = Tools.SmallBrush
Dim _lineTool As
Tools = Tools.ThinLine
Dim _currentTool As
Tools = Tools.SmallBrush
Public Property
LineTool() As Tools
Get
Return
_lineTool
End
Get
Set(ByVal Value As Tools)
_lineTool
= Value
End
Set
End Property
Public Property
BrushTool() As Tools
Get
Return
_brushTool
End
Get
Set(ByVal Value As Tools)
_brushTool
= Value
End
Set
End Property
Public Property
CurrentTool() As Tools
Get
Return
_currentTool
End
Get
Set(ByVal Value As Tools)
_currentTool
= Value
UpdateToolButton()
End
Set
End Property
Dim _currentColor
As Color
Public Property
CurrentColor() As Color
Get
Return
_currentColor
End
Get
Set(ByVal Value As Color)
_currentColor
= Value
End
Set
End Property
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 Function
GetParent() As MainForm
Return
CType(Parent.Parent, MainForm)
End Function
Private Sub ImageEditorForm_Activated(ByVal
sender As Object,
ByVal e As
System.EventArgs) Handles MyBase.Activated
GetParent().CurrentTool =
myCurrentTool
GetParent().BrushTool =
myBrushTool
GetParent().LineTool =
myLineTool
GetParent().CurrentColor
= myCurrentColor
GetParent().UpdateToolButton()
End Sub
Dim myCurrentTool
As MainForm.Tools = MainForm.Tools.Brush
Dim myBrushTool As
MainForm.Tools = MainForm.Tools.SmallBrush
Dim myLineTool As
MainForm.Tools = MainForm.Tools.ThinLine
Dim
myCurrentColor As Color = Color.Black
Private Sub ImageEditorForm_Deactivate(ByVal
sender As Object,
ByVal e As
System.EventArgs) Handles MyBase.Deactivate
myCurrentTool =
GetParent().CurrentTool
myBrushTool =
GetParent().BrushTool
myLineTool =
GetParent().LineTool
myCurrentColor =
GetParent().CurrentColor
End Sub
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 Sub ButtonMenuItems_MeasureItem(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.MeasureItemEventArgs) Handles
menuItem6.MeasureItem, menuItem7.MeasureItem, menuItem8.MeasureItem,
menuItem9.MeasureItem, menuItem10.MeasureItem
e.ItemHeight = 32
e.ItemWidth = 32
End Sub
Private Sub ButtonMenuItems_DrawItem(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.DrawItemEventArgs) Handles
menuItem6.DrawItem, menuItem7.DrawItem, menuItem8.DrawItem,
menuItem9.DrawItem, menuItem10.DrawItem
Dim
ia As ImageAttributes = New
ImageAttributes
If
(e.State And DrawItemState.Selected) >
0 Then
e.Graphics.FillRectangle(Brushes.LightBlue,
e.Bounds)
Else
e.Graphics.FillRectangle(Brushes.White,
e.Bounds)
End
If
Dim
_img As Image = Nothing
Select
Case (CType(sender,
MenuItem)).Text
Case
"SmallBrush"
_img = Me.imageList1.Images(4)
Case
"LargeBrush"
_img = Me.imageList1.Images(5)
Case
"ThinLine"
_img = Me.imageList1.Images(6)
Case
"ThickLine"
_img = Me.imageList1.Images(7)
Case
"ThickDottedLine"
_img
= Me.imageList1.Images(8)
End
Select
e.Graphics.DrawImage(_img,
e.Bounds, 0, 0, _img.Width, _img.Height, GraphicsUnit.Pixel, ia)
End Sub
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 Sub toolBar1_ButtonClick(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.ToolBarButtonClickEventArgs) Handles
toolBar1.ButtonClick
'Ensure
all buttons are up
Dim
b As ToolBarButton
For
Each b In Me.toolBar1.Buttons
b.Pushed
= False
Next
'behaviour
for the colour palette button is different
If
CType(e.Button.Tag = "Color", String)
Then
'A
drawing colour is chosen
Dim
dlg As ColorDialog = New
ColorDialog
dlg.Color
= _currentColor
If
dlg.ShowDialog() = DialogResult.OK Then
_currentColor = dlg.Color
End
If
Me.statusBar1.Refresh()
Else
'the
tools are changed and the button pressed to give feedback
Select
Case CType(e.Button.Tag,
String)
Case "Brush"
Me.CurrentTool = Tools.Brush
e.Button.Pushed
= True
Case "Line"
Me.CurrentTool = Tools.Line
e.Button.Pushed = True
Case "Rect"
Me.CurrentTool = Tools.Rectangle
e.Button.Pushed = True
Case "Ellipse"
Me.CurrentTool = Tools.Ellipse
e.Button.Pushed = True
End
Select
End
If
End Sub
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 Property
CurrentTool() As Tools
Get
Return
_currentTool
End
Get
Set(ByVal Value As Tools)
_currentTool
= Value
UpdateToolButton()
End
Set
End Property
Public Sub
UpdateToolButton()
Select
Case _currentTool
Case
Tools.Brush
Me.toolBar1.Buttons(5).DropDownMenu
= Me.contextMenu1
Select Case
_brushTool
Case Tools.SmallBrush
Me.toolBar1.Buttons(5).ImageIndex =
4
Exit Sub
Case Tools.LargeBrush
Me.toolBar1.Buttons(5).ImageIndex =
5
Exit Sub
End Select
Case
Tools.Line
Case
Tools.Rectangle
Case
Tools.Ellipse
Me.toolBar1.Buttons(5).DropDownMenu
= Me.contextMenu2
Select Case
_lineTool
Case Tools.ThinLine
Me.toolBar1.Buttons(5).ImageIndex =
6
Exit Sub
Case Tools.ThickLine
Me.toolBar1.Buttons(5).ImageIndex =
7
Exit Sub
Case Tools.ThickDottedLine
Me.toolBar1.Buttons(5).ImageIndex =
8
Exit
Sub
End Select
End
Select
End Sub
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 Sub menuItem2_Click(ByVal sender As
Object, ByVal
e As System.EventArgs) Handles
MenuItem2.Click
'New
file
Dim
f As ImageEditorForm = New
ImageEditorForm
f.Filename = "Untitled.bmp"
f.CreateNew()
f.MdiParent = Me
AddHandler
f.MouseMove, AddressOf f_MouseMove
f.Show()
End Sub
Private Sub menuItem3_Click(ByVal sender As
Object, ByVal
e As System.EventArgs) Handles
MenuItem3.Click
'Open
file
Dim
dlg As OpenFileDialog = New
OpenFileDialog
dlg.Filter = "Image
files|*.bmp;*.jpg;*.gif;*.tif"
If
dlg.ShowDialog() = DialogResult.OK Then
Dim
f As ImageEditorForm = New
ImageEditorForm
f.Filename
= dlg.FileName
f.CreateFile()
f.MdiParent
= Me
AddHandler
f.MouseMove, AddressOf f_MouseMove
f.Show()
End
If
End Sub
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.
Private Sub f_MouseMove(ByVal sender As
Object, ByVal
e As MouseEventArgs)
Me.PositionIndicator
= New Point(e.X, e.Y)
End Sub
Public WriteOnly
Property PositionIndicator() As
Point
Set(ByVal Value As Point)
Me.PositionPanel.Text
= String.Format("{0},{1}",
Value.X, Value.Y)
Me.statusBar1.Refresh()
End
Set
End Property
//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 Sub statusBar1_DrawItem(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.StatusBarDrawItemEventArgs) Handles
statusBar1.DrawItem
Dim
br As SolidBrush = New
SolidBrush(Me.CurrentColor)
e.Graphics.FillRectangle(br,
e.Bounds)
br.Dispose()
End Sub
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 Sub
UnHook(ByVal f As
ImageEditorForm)
RemoveHandler
f.MouseMove, AddressOf f_MouseMove
End Sub
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 Sub ImageEditorForm_Closed(ByVal
sender As Object,
ByVal e As
System.EventArgs)
GetParent().UnHook(Me)
End Sub
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.
Public Sub
CreateNew()
_target = New
Bitmap(800, 600)
Dim
g As Graphics = Graphics.FromImage(_target)
g.Clear(Color.White)
g.Dispose()
SetScrollBars()
Me.Text
= Filename
_dirty = False
End Sub
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 Sub
SetScrollBars()
Me.AutoScroll
= True
Me.AutoScrollMinSize
= New Size(_target.Width, _target.Height)
End Sub
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 Sub
CreateFile()
Dim
fs As FileStream = New
FileStream(Me.Filename, FileMode.Open,
FileAccess.Read)
Dim
bm As Bitmap = CType(Image.FromStream(fs),
Bitmap)
fs.Close()
_target = New
Bitmap(bm.Width, bm.Height)
Dim
g As Graphics = 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()
Me.Text
= Filename
_dirty = False
End Sub
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 Sub ImageEditorForm_Paint(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.PaintEventArgs) Handles
MyBase.Paint
Dim
mx As Matrix = New
Matrix(1, 0, 0, 1, Me.AutoScrollPosition.X,
Me.AutoScrollPosition.Y)
e.Graphics.Transform = mx
e.Graphics.DrawImage(_target,
e.Graphics.ClipBounds, e.Graphics.ClipBounds, GraphicsUnit.Pixel)
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 Function
BacktrackMouse(ByVal e As
MouseEventArgs) As MouseEventArgs
Dim
mx As Matrix = New
Matrix(1, 0, 0, 1, Me.AutoScrollPosition.X,
Me.AutoScrollPosition.Y)
mx.Invert()
Dim
pa() As Point = 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)
End Function
Protected Overloads
Overrides Sub
OnMouseDown(ByVal e As
System.Windows.Forms.MouseEventArgs)
_feedback = True
_dirty = True
_unmodifiedStartPos = New
Point(e.X, e.Y)
Dim
et As MouseEventArgs = BacktrackMouse(e)
_startPos = New
Point(et.X, et.Y)
MyBase.OnMouseDown(e)
End Sub
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 Overloads
Overrides Sub
OnMouseMove(ByVal e As
MouseEventArgs)
_unmodifiedCurrPos = New
Point(e.X, e.Y)
Dim
et As MouseEventArgs = BacktrackMouse(e)
Me._currentPos
= New Point(et.X, et.Y)
If
_feedback = True Then
Dim
InvalidRect As New
Rectangle( _
Math.Min(Me._unmodifiedStartPos.X,
Me._unmodifiedCurrPos.X) - 5, _
Math.Min(Me._unmodifiedStartPos.Y,
Me._unmodifiedCurrPos.Y) - 5, _
Math.Abs(_unmodifiedStartPos.X
- _unmodifiedCurrPos.X) + 10, _
Math.Abs(_unmodifiedStartPos.Y
- _unmodifiedCurrPos.Y) + 10)
Me._invalidationQueue.Enqueue(InvalidRect)
Invalidate(Rectangle.Union(CType(_invalidationQueue.Dequeue(),
Rectangle), InvalidRect))
End
If
MyBase.OnMouseMove(et)
End Sub
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 Sub DrawSmallBrush(ByVal g As
Graphics)
Dim
p As Pen = New
Pen(GetParent().CurrentColor, 2)
p.EndCap = LineCap.Round
g.DrawLine(p, Me._startPos,
Me._currentPos)
p.Dispose()
End Sub
Private Sub DrawLargeBrush(ByVal g As
Graphics)
Dim
p As Pen = New
Pen(GetParent().CurrentColor, 6)
p.EndCap = LineCap.Round
g.DrawLine(p, Me._startPos,
Me._currentPos)
p.Dispose()
End Sub
Private Function CreateLinePen() As Pen
Select
Case 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
Dim p As
Pen = New Pen(GetParent().CurrentColor, 5)
p.DashStyle = DashStyle.Dot
Return p
End
Select
Return
New Pen(GetParent().CurrentColor, 1)
End Function
Private Sub DrawLine(ByVal g As
Graphics)
Dim
p As Pen = Me.CreateLinePen()
g.DrawLine(p, Me._startPos,
Me._currentPos)
p.Dispose()
End Sub
Private Sub DrawRectangle(ByVal g As
Graphics)
Dim
p As Pen = Me.CreateLinePen()
Dim
rc As Rectangle = New
Rectangle( _
Math.Min(Me._startPos.X,
Me._currentPos.X), _
Math.Min(Me._startPos.Y,
Me._currentPos.Y), _
Math.Abs(Me._startPos.X
- Me._currentPos.X), _
Math.Abs(Me._startPos.Y
- Me._currentPos.Y))
g.DrawRectangle(p, rc)
p.Dispose()
End Sub
Private Sub DrawEllipse(ByVal g As
Graphics)
Dim
p As Pen = Me.CreateLinePen()
Dim
rc As Rectangle = New
Rectangle( _
Math.Min(Me._startPos.X,
Me._currentPos.X), _
Math.Min(Me._startPos.Y,
Me._currentPos.Y), _
Math.Abs(Me._startPos.X
- Me._currentPos.X), _
Math.Abs(Me._startPos.Y
- Me._currentPos.Y))
g.DrawEllipse(p, rc)
p.Dispose()
End Sub
Private Sub ImageEditorForm_Paint(ByVal
sender As Object,
ByVal e As
System.Windows.Forms.PaintEventArgs) Handles
MyBase.Paint
Dim
mx As Matrix = New
Matrix(1, 0, 0, 1, Me.AutoScrollPosition.X,
Me.AutoScrollPosition.Y)
e.Graphics.Transform = mx
e.Graphics.DrawImage(_target,
e.Graphics.ClipBounds, e.Graphics.ClipBounds, GraphicsUnit.Pixel)
Dim
tg As Graphics =
Graphics.FromImage(_target)
If
_feedback = True Then
Select
Case GetParent().CurrentTool
Case MainForm.Tools.Brush
Select
Case GetParent().BrushTool
Case MainForm.Tools.SmallBrush
DrawSmallBrush(e.Graphics)
DrawSmallBrush(tg)
Me._startPos = Me._currentPos
Me._unmodifiedStartPos = Me._unmodifiedCurrPos
Case MainForm.Tools.LargeBrush
DrawLargeBrush(e.Graphics)
DrawLargeBrush(tg)
Me._startPos
= Me._currentPos
Me._unmodifiedStartPos = Me._unmodifiedCurrPos
End Select
Case MainForm.Tools.Line
DrawLine(e.Graphics)
Case MainForm.Tools.Rectangle
DrawRectangle(e.Graphics)
Case MainForm.Tools.Ellipse
DrawEllipse(e.Graphics)
End
Select
End
If
tg.Dispose()
End Sub
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.
Protected Overloads
Overrides Sub
OnMouseUp(ByVal e As
System.Windows.Forms.MouseEventArgs)
_feedback = False
Dim
g As Graphics = Graphics.FromImage(_target)
Select
Case GetParent().CurrentTool
Case
MainForm.Tools.Line
DrawLine(g)
Case
MainForm.Tools.Rectangle
DrawRectangle(g)
Case
MainForm.Tools.Ellipse
DrawEllipse(g)
End
Select
g.Dispose()
Invalidate()
MyBase.OnMouseUp(e)
End Sub
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 Function DoSave() As DialogResult
Dim
dlg As SaveFileDialog = New
SaveFileDialog
dlg.FileName = Me.Filename
Dim
result As DialogResult = dlg.ShowDialog()
If
(result = DialogResult.OK) Then
Dim
f As ImageFormat = ImageFormat.Jpeg
Select
Case
Path.GetExtension(dlg.FileName).ToLower()
Case ".bmp"
f = ImageFormat.Bmp
Case ".tif"
f = ImageFormat.Tiff
Case ".gif"
f = ImageFormat.Gif
End
Select
_target.Save(dlg.FileName,
f)
_dirty
= False
End
If
Return
result
End Function
Private Sub menuItem2_Click(ByVal sender As
Object, ByVal
e As System.EventArgs) Handles
menuItem2.Click
DoSave()
End Sub
Private Sub ImageEditorForm_Closing(ByVal
sender As Object,
ByVal e As
System.ComponentModel.CancelEventArgs) Handles
MyBase.Closing
If
(_dirty = True) Then
Select
Case 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)
Then
e.Cancel = True
End If
Case DialogResult.No
Return
Case DialogResult.Cancel
e.Cancel = True
End
Select
End
If
End Sub
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