This application puts together many of the principals we've shown you in this book and shows off some of the features of Windows Forms in a complete application context.
Form Paint is a Multiple Document Interface (MDI) application that allows you to paint images with brushes and shapes. It has customized menus and shows off some of the power of GDI+ also.
Getting started, create an application in VS.NET and immediately set the forms IsMDIContainer to true. This makes it a main parent window for other form-based documents that are hosted in it.
The name, Form1, must be changed to MainForm and the title of the window changed to FormPaint. Remember that when the name of the MainForm changes, you must also manually modify the static Main method as follows:
[STAThread] static void Main() { Application.Run(new MainForm()); // change the application name }
Drag a MainMenu from the toolbox onto the MainForm. Type the word &File as the menu name and add three menu items—&New, &Open, and &Exit.
Drag an OpenFileDialog and a SaveFileDialog from the toolbox onto the MainForm. These will show up in the icon tray below the form.
Double-click the Open menu item in the main menu and type the following into the handler provided:
this.openFileDialog1.Filter="Bitmap files(*.BMP)|*.bmp"; this.openFileDialog1.ShowDialog();
Create a second menu called Windows. This will be used to keep track of the MDI child forms in the application. This is done automatically for you if you set the MDIList property of the menu to true.
Next, add a new Windows Form to the project, call it ChildForm. Set the BackColor to green.
Add a private Image variable called myImage to the child form and a public accessor property as follows:
private Image myImage; public Image Image { set{ myImage =value;} get{ return myImage;} }
Now, double-click the OpenFileDialog in the icon tray. This action will create a FileOk handler for you. Type the following into the resulting handler:
ChildForm c=new ChildForm(); c.MdiParent=this; c.Image=Image.FromFile(this.openFileDialog1.FileName); c.Text=this.openFileDialog1.FileName); c.Show();
This will open a file, create a child form, and load the image into the child form. The child form now needs to display the image. Select the ChildForm design page, click the Events button in the Property Browser, and double-click the Paint event. This creates a PaintEventHandler delegate for the ChildForm and opens the editor at the correct place in the file. Type the following into the handler:
e.Graphics.DrawImage(myImage,0,0,myImage.Width,myImage.Height);
This is a good place to stop and take stock of the project. Compile and run the code using the F5 key. You'll see an application that lets you load and display images in multiple windows. Figure 3.6.4 shows this basic application in action.
This portion of the program is available as FormPaint Step1 on the Sams Web site associated with this book.
If you've run the fledgling FormPaint program and loaded up a few images, you'll have noticed that the windows will resize. But, when they become too small to display the picture, they simply chop it off without giving you a chance to see the sides with a scrollbar. We'll correct this quickly by adding some simple code to the ChildForm.
First, in the Image property set accessor for the ChildForm, we'll add a command to set the AutoScrollMinSize property to the size of the image in myImage. The following code snippet shows the added functionality on line 6.
1: public Image Image 2: { 3: set 4: { 5: myImage =value; 6: this.AutoScrollMinSize=myImage.Size; 7: } 8: get{ return myImage;} 9: }
Now, whenever the client rectangle size drops below the minimum size in either dimension, the corresponding scroll bar will appear.
A second change must be made to the OnPaint handler to position the scrollbars. Here, we need to offset the image by the value of the scrollbar position so that the image is painted correctly. The form also provides a property for the scrollbar positions that is shown in the following code snippet.
1: private void ChildForm_Paint(object sender, System.Windows.Forms.PaintEventArgs e) 2: { 3: e.Graphics.DrawImage(myImage, 4: this.AutoScrollPosition.X, 5: this.AutoScrollPosition.Y, 6: myImage.Width, 7: myImage.Height); 8: }
On lines 4 and 5, you can see that the X and Y values of the form's AutoScrollPosition property offset the origin of the image by the correct amount.
So far, the program can only load an image that exists on disk. We need to create an image, perhaps with a choice of sizes, and to allow that image to be saved to disk also.
Creating the image can be accomplished as follows. Using the MainForm design page, select and double-click the New menu entry in the File menu. This will create a handler that we'll fill in later.
Now, right-click the FormPaint project and choose Add, New Item. Choose a new Form and call it NewImageDialog.cs. Drag three radio buttons and two buttons from the toolbox onto the dialog. Then arrange them, as shown in Figure 3.6.5
The DialogResult properties of the OK and Cancel buttons must be set to DialogResult.Ok and DialogResult.Cancel respectively. To retrieve the setting from the radio buttons, we can add a simple property that returns a size. Hand-edit the NewImageDialog.cs code and add the following public property:
public Size ImageSize { get { if(this.radioButton2.Checked) return new Size(800,600); if(this.radioButton3.Checked) return new Size(1024,768); return new Size(640,480); } }
Now we can return to the handler we created earlier and fill it out to create a blank image. Find the handler in the MainForm.cs file and add the necessary code as follows:
1: private void menuItem2_Click(object sender, System.EventArgs e) 2: { 3: // To create a new file... 4: NewImageDialog dlg = new NewImageDialog(); 5: if(dlg.ShowDialog()==DialogResult.OK) 6: { 7: ChildForm c=new ChildForm(); 8: c.MdiParent=this; 9: c.Image=new Bitmap(dlg.ImageSize.Width,dlg.ImageSize.Height); 10: Graphics g=Graphics.FromImage(c.Image); 11: g.FillRectangle(new SolidBrush(Color.White),0,0, c.Image.Width,c.Image.Height); 12: c.Show(); 13: } 14: }
Saving the file or saving “as” a different file will be performed through the main File menu. Add a Save and Save As menu item, remembering to use the ampersand before the key-select characters—for example, &Save and Save &As. For esthetic reasons, you might also want to move these into a sensible place on the menu and place a separator between the file and exit functions.
When you've created these entries, go ahead and double-click each one in turn to create the handlers for them. The following code snippet shows the two save handlers:
1: private void menuItem6_Click(object sender, System.EventArgs e) 2: { 3: //Save the image file. 4: ChildForm child = (ChildForm)this.ActiveMdiChild; 5: child.Image.Save(child.Text); 6: } 7: 8: private void menuItem7_Click(object sender, System.EventArgs e) 9: { 10: //Save the image file as a different filename 11: ChildForm child = (ChildForm)this.ActiveMdiChild; 12: this.saveFileDialog1.FileName=child.Text; 13: if(this.saveFileDialog1.ShowDialog()==DialogResult.OK) 14: { 15: child.Image.Save(this.saveFileDialog1.FileName); 16: child.Text=this.saveFileDialog1.FileName; 17: } 18: }
Lines 1–6 are the simple save handler. The filename is already known, so a save is performed using the titlebar text of the ChildForm.
Lines 8–18 use this filename as a starting point but invoke the SaveFileDialog to allow the user to choose a new name.
This behavior is fine if you can guarantee that a ChildWindow is always open from which to get the text. If you use the handlers on an empty main form, though, an exception will occur because there is no active MDI child. This problem can be overcome in one of two ways. Either write an exception handling try...catch block into both handlers or, alternatively, never allow the handler to be called if there are no MDI children. The second method is best for the purposes of this demonstration because it allows us to use the menu pop-up events correctly.
You might remember from Chapter 3.1, “Introduction to Windows Forms,” that the menu Popup event is fired when the user selects the top-level menu on the toolbar just before the menu is drawn. This gives us the chance to modify the menu behavior to suit the current circumstances. The following code snippet shows the Popup handler for the File menu item.
1: private void menuItem1_Popup(object sender, System.EventArgs e) 2: { 3: if(this.MdiChildren.Length!=0) 4: { 5: menuItem6.Enabled=true; 6: menuItem7.Enabled=true; 7: } 8: else 9: { 10: menuItem6.Enabled=false; 11: menuItem7.Enabled=false; 12: } 13: }
The handler checks to see if there are any MDI children in the main form (line 3). If there are, it enables both the save menus on lines 5 and 6. If there are none, it disables these menus on lines 10 and 11.
This part of the application is available as FormPaint Step2 on the Sams Web site associated with this book.
With the image loading, display, and saving in place, we can get on with the task of adding the rest of the user interface items that will be used to drive the program. We need a tool palette; in this case, a simple one will suffice to demonstrate principles, a status bar, some mouse handling, and a custom button class based on UserControl.
A big advantage to user controls in .NET is their ease of reuse. A quick side-trip into a custom control project can result in a handy component that you'll use many times in various projects. These controls play nicely in the design environment and can be shipped as separate assemblies if you want.
To begin with, add a new C# control library project to the current solution. Remember to choose the Add to Current Solution radio button on the wizard dialog. Call the project FormPaintControl. You'll be presented with a wizard to choose the type of control to add. Select a new UserControl and name it CustomImageButton.cs.
Listing 3.6.5 shows the full source code of the CustomImageButton control.
Beginning with line 1, note the added use of namespaces in the control. The default is only to use the System namespace.
Line 10 defines the FormPaintControl namespace with line 15 beginning the control itself—CustomImageButton.
Lines 17 through 20 define private data members to store information used in the drawing of the control. There is an Image, a transparent color key, and two Boolean flags for an owner draw property and to decide whether the button should be rendered down or up.
Public accessor properties for the relevant members ensure that the control will integrate nicely with the design environment and give some user feedback. Notice the Category and Description attributes preceding the property definitions on lines 26, 42, and 58.
Line 70 defines the OnSizeChanged method. When this control is used in Design mode, it needs to paint itself whenever it's resized. It should also call the base class method to ensure that any other delegates added to the SizeChanged event will also be called.
Line 77 defines a new method that draws a dotted line around the control when it has the focus. This is called from the OnPaint method.
The method on line 86 ensures that the control is redrawn when focus is lost.
The OnPaint method, beginning on line 92, has to contend with normal drawing of the control and drawing of the control when it's used in the design environment. This means that in the initial instance, the control will be instantiated by the designer with an empty image property. So that exceptions do not occur, the control will draw a red rectangle with a red cross in it if there is no image with which to paint. This is accomplished on lines 102–122. Otherwise, normal drawing takes place on lines 118–123 if the button is down or focused. Lines 127–132 draw the button in its up position. Note the use of the ControlPaint method on lines 127 and 128 that draws a “disabled” image behind the main bitmap as a drop shadow. This method also calls the base OnPaint method to ensure correct painting when other paint handler delegates are added to the Paint event.
The standard control added all lines from 138–150, and our additions continue with the MouseEnter and MouseLeave handlers on lines 152 and 159, respectively. These make the button depress whenever the mouse floats over it.
On line 167, there is an attribute to allow the Text property to be seen in the property browser. The UserControl from which this class is derived hides this property, so we need to override it (lines 170–180) and make it visible in the browser. Notice that the implementation calls the base class explicitly. On line 169, the DesignerSerializationVisibility attribute is used to ensure that the text is stored in the InitializeComponent method.
The rest of the file is standard stuff added by the control wizard and includes a Dispose method and an InitializeComponent method.
Compile the control and then right-click the toolbox, select Customize Toolbox and you'll see the dialog shown in Figure 3.6.6.
To see the CustomImageButton, you must first find the compiled DLL that contains the control. This could be
<your Projects> FormPaint FormPaintControl Debug Bin FormPaintControl.dll
Ensure that the check box is ticked and the control will appear in your toolbox.
NOTE
You can make this a permanent tool to use in your programs if you change to Release mode, compile the control, and then select the release version of the FormPaintControl DLL. You might also want to copy the release DLL to a place on your hard disk where it's unlikely to be erased.
To the MainForm, add a panel and dock it to the right side of the form. This area will be used to host our simple tool palette. Drag a status bar from the toolbox onto the form, select the Panels collection in the property browser, and click the button in the combo-box that will appear. You'll see a dialog that will allow you to add one or more StatusBarPanel objects; for now, just add one. Remember to set the StatusBar objects ShowPanels property to true.
Drag four CustomImageControl objects onto the panel and arrange them vertically to make a place for selecting drawing tools. Initially, each will be displayed as a red rectangle with a cross in it.
Using the property browser, add images to each of the buttons' Image properties. We selected a paintbrush, rectangle, ellipse, and eraser image for our simple tool palette. Figure 3.6.7 shows these four images. Note that the outside, unused portion of the image will be color-keyed magenta to use as a transparency key.
When four images, brush.bmp, rect.bmp, ellipse.bmp, and eraser.bmp are added to their buttons, the Paint method will display them for us, even in the Designer.
The design should look similar to the one in Figure 3.6.8.
The CustomImageControl can hold text for us so we can use this to create a simple UI integration and feedback mechanism. We'll store information in the text to annotate the button with tooltips and status bar feedback. Later, we'll use the same feedback mechanism to enhance the menus.
In the Text property of the four buttons, the following text will provide more than just a button caption. The text will contain a caption, tooltip, and status bar text separated by the | vertical line (character 124). For an example, the following line shows the format.
Caption Text|Status Bar feedback|Tooltip text
For each text property in turn, add the relevant line using the property browser:
CustomImageButton1.Text "|Apply color with a brush tool|Paintbrush" CustomImageButton2.Text "|Draw a rectangle|Rectangle" CustomImageButton3.Text "|Draw an ellipse|Ellipse" CustomImageButton 4.Text "|Erase an area|Eraser"
Note that none of the text entries have a caption portion, because a text caption is unnecessary.
A tooltip gives added useful feedback to the user and our tool palette items have text that they can display when the mouse hovers over them, so the FormPaint program uses a simple method to display tooltip and status bar text. Taking advantage of the String classes' ability to split strings at given delimiters, we can create a simple class that will extract the tooltip, status bar, and caption text for us. Add a new C# class to the project and call it CSTSplit for caption–status–tooltip splitter.
The CSTSplit class is simple and is shown in Listing 3.6.6.
The constructor on line 10 splits the string provided into an array of up to three strings divided at the vertical line delimiters.
The Caption, Status, and Tooltip properties (lines 19, 29, and 39, respectively) retrieve the correct string or provide a blank if no text for that portion of the string was included.
The initial use for this class will be to add tooltips to the four buttons in the tool palette. This is accomplished in the constructor of the MainForm, just after the InitializeComponent call. To display the tooltips, drag a ToolTip object from the toolbox onto the MainForm design page. The ToolTip will appear in the icon tray below the main window. Then add the following code to the MainForm constructor:
CSTSplit splitter=new CSTSplit(this.customImageButton1.Text); this.toolTip1.SetToolTip(this.customImageButton1,splitter.Tooltip); splitter=new CSTSplit(this.customImageButton2.Text); this.toolTip1.SetToolTip(this.customImageButton2,splitter.Tooltip); splitter=new CSTSplit(this.customImageButton3.Text); this.toolTip1.SetToolTip(this.customImageButton3,splitter.Tooltip); splitter=new CSTSplit(this.customImageButton4.Text); this.toolTip1.SetToolTip(this.customImageButton4,splitter.Tooltip);
You can see that each CustomImageButton in turn is interrogated for its text, and the ToolTip property from the CSTSplit object returns the correct text that is handed to the ToolTip's ShowTip method.
Now, when the mouse rests on the control for half a second or more, a tooltip will be displayed containing the correct text.
Each of these controls contain text for the status bar also. To make this text show itself, select the first CustomImageButton control in the designer and type the method name ShowStatus into the MouseEnter event in the property browser. A handler will be created that should be filled out as follows.
private void ShowStatus(object sender, System.EventArgs e) { Control c=(Control)sender; CSTSplit splitter=new CSTSplit(c.Text); this.statusBarPanel1.Text=splitter.Status; }
Now, compiling and running the program will show that the tooltips and status bar messages are functioning correctly.
This version of the code is saved as FormPaint Step3.
The program is now in a state where we can begin to do real work. We need to be able to select individual properties for the tools and to apply them to the bitmap image.
The Paintbrush object requires several properties, such as brush shape, color, and size. The ellipse and rectangle tool only need to define line color, fill color, and line thickness. The eraser will have a size and shape, like the paintbrush, but will always erase to white.
So that we can use the PropertyGrid to select these properties, we'll create a class for each of these tools that will be used to retain the user selections.
The three tool description objects are shown in Listings 3.6.7, 3.6.8 and 3.6.9.
NOTE
Notice the Shape enumeration at the beginning of this file.
Notice that all these objects have a Clone() method. This is used when the properties are edited in a dialog box, which we'll define next, and the user has the option to click the Cancel button. If the dialog is cancelled, the clone of the object is discarded.
The editing for all tool properties is accomplished with the same dialog box. This simple form hosts a property grid that deals with all the editing of the properties within the classes.
This dialog, ToolProperties, is created with the IDE as follows.
Drag two buttons from the toolbar to the dialog, label them Ok and Cancel, and then set their DialogResult properties to the corresponding value.
Drag and position a PropertyGrid object onto the design surface. The final result should look similar to the image in Figure 3.6.9.
This dialog needs some added behavior, so we'll add a property to set and retrieve the object that the property grid edits. The following code snippet shows this simple accessor:
public object Object { get{ return this.propertyGrid1.SelectedObject;} set{ this.propertyGrid1.SelectedObject=value;} }
After this accessor is in place, we can add the functionality for editing and using the properties.
The MainForm must be modified to hold the properties and the tool currently in use. The code snippets that follow show these additions.
The Tool structure is added to the FormPaint namespace and is used to define which tool is in use:
public enum Tool { Paintbrush, Rectangle, Ellipse, Eraser }
The following private members are added to the MainForm class:
private Tool currentTool; private PaintBrushProperties paintbrushProperties; private ShapeProperties shapeProperties; private EraserProperties eraserProperties;
The following additions are made to the MainForm constructor:
paintbrushProperties = new PaintBrushProperties(); shapeProperties = new ShapeProperties(); eraserProperties = new EraserProperties();
To use a tool, the user will single click it. To edit the properties for a tool, the user will double-click. To do this, add a click handler to each of the four tool buttons on the main form. The following code snippet shows how to fill these out:
private void ClickPaintbrush(object sender, System.EventArgs e) { currentTool=Tool.Paintbrush; } private void ClickRectangle(object sender, System.EventArgs e) { currentTool=Tool.Rectangle; } private void ClickEllipse(object sender, System.EventArgs e) { currentTool=Tool.Ellipse; } private void ClickEraser(object sender, System.EventArgs e) { currentTool=Tool.Eraser; }
You can see that these handlers simply change the values of the currentTool member in the MainForm.
All four buttons now need to be tied to the same DoubleClick handler. Begin by creating a handler, called OnToolProperties, for one button and filling it out as follows:
private void OnToolProperties(object sender, System.EventArgs e) { ToolProperties dlg=new ToolProperties(); switch(currentTool) { case Tool.Paintbrush: dlg.Object=paintbrushProperties.Clone(); break; case Tool.Rectangle: dlg.Object=shapeProperties.Clone(); break; case Tool.Ellipse: dlg.Object=shapeProperties.Clone(); break; case Tool.Eraser: dlg.Object=eraserProperties.Clone(); break; } if(dlg.ShowDialog()==DialogResult.OK) { switch(currentTool) { case Tool.Paintbrush: paintbrushProperties=(PaintBrushProperties)dlg.Object; break; case Tool.Rectangle: shapeProperties=(ShapeProperties)dlg.Object; break; case Tool.Ellipse: shapeProperties=(ShapeProperties)dlg.Object; break; case Tool.Eraser: eraserProperties=(EraserProperties)dlg.Object; break; } } }
This handler decides which of the property classes to use, hands the correct class to the editor, invokes the editor and then discards or replaces the edited property, depending on the DialogReturn value.
To allow the ChildForm to access the MainForm variables, such as the current drawing tool or the brush and shape properties, we have added some accessor properties. In addition, there is an accessor that allows the ChildForm to place text in the MainForm's status bar. This is useful for user feedback. The snippet that follows contains the final additions to the class:
public Tool CurrentTool { get{ return currentTool;} } public ShapeProperties ShapeProperties { get{ return shapeProperties;} } public EraserProperties EraserProperties { get{ return eraserProperties;} } public PaintBrushProperties PaintBrushProperties { get{ return paintbrushProperties;} } public string StatusText { set{ this.statusBarPanel1.Text=value;} }
Running the application now will allow you to test the tool selection and property editing.
Now we can finally put paint on our bitmaps. This is all done by handlers in the ChildForm class.
This class performs two basic operations. The first is to place a shaped blob of color wherever the mouse is when the mouse button is pressed. The other is to allow the user to place a geometric shape and size it by dragging.
Listing 3.6.10 shows the full source of the ChildForm class interspersed with analysis.
Finally, the last few methods deal with saving the file if it has been altered (lines 360–381). This handler is called before the form closes to see if it is allowed to continue. The message box used on lines 365–369 determines if the user wants to save or discard the image or cancel the close altogether. In the case of a cancel, the CancelEventArgs provided through the delegate call must be updated. This happens on line 373.
The OnPaintBackground override is provided to eliminate flicker. With a green background and the default settings, the whole bitmap is over-painted with green before the image is redrawn. This caused a nasty flicker unless the steps taken here are used. This method calculates the region of screen outside of the image by using the complement of this area and the area covered by the image (lines 386–391) and only repaints the piece required.
The OnSizeChanged override of the base class method in lines 395–399 ensures that the background is correctly repainted by just invalidating the window.
The image in Figure 3.6.10 shows FormPaint working, albeit with some abysmal artistic talent.
13.59.212.54