The implementation of this chapter is based on the application that was developed in Chapter 6. Therefore, we take the sample solution of Chapter 6 as a starting point. |
Let's start the implementation by updating the GUI as it was defined in the requirements analysis. First, we use a numeric up/down or spin control to enable the user to choose a line width. In addition, we will add a button that opens a color dialog, which in turn enables the user to select a drawing color.
Drag a NumericUpDown control and a GroupBox from the Toolbox onto the Graphics and Text tab control. Place the GroupBox around the NumericUpDown control as shown in the requirements. After that, drag a new button from the Toolbox to the tab control. Change the properties of the new elements to the values shown in Table 7.4.
Then add event handlers for the button click event of the button and the value changed event of the NumericUpDown control by double-clicking on the corresponding GUI element.
In the next step, we add a private property to the GraphicsComponent class. The property is used to hold the values of the user-selected color and line width. We have multiple options for how we can implement this. The first option is to simply add two properties and have each property hold one of the values. This means that we would add a selectedColor and a selectedLineWidth property.
This seems reasonable at first, but let's consider what we will use the properties for. Both values define how the graphics are drawn on the screen (with which color and line width). The actual drawing will be done using a pen. Therefore, another option is to use a Pen object to store the user-selected values. Using a pen to store the selected values gives us the flexibility to add other pen properties (which might be required in the next release) without having to add any new properties. To support new pen properties, we simply add a user interface control and store the selected value in the existing Pen object. The Pen object defaults to the drawing color black and the line width of 1. If the user selects different values, they will be stored in the Pen object.
GUI Element |
Property |
Value |
---|---|---|
NumericUpDown |
(Name) Minimum Maximum |
penNumericUpDown 1 20 |
GroupBox |
(Name) Text |
lineThickness Line Thickness |
Button |
(Name) Text |
selectColorButton Select Color |
To draw the components with the specified line width and color, we must change the GraphicsComponent class and the Draw method of the graphics components. Because the requirement states that all graphics in the list of graphics are to be drawn using the same color and line width, one Pen object can be shared among all the components. Therefore, the pen is provided by the GraphicsComponent class. Simply add a private static Pen property named drawingPen to the abstract GraphicsComponent class:
private static Pen drawingPen = new Pen(Color.Black, 1);
In addition, we implement the public accessor methods for the Pen property so that we can access the pen from the PhotoEditorForm class. We also change the various components. Right now they use Pens.Black in the draw methods, but in the new version they need to use the static drawingPen of the GraphicsComponent class. With these changes made, the graphics components are drawn with a Pen object instead of a black pen.
The graphical objects are then drawn with these properties as long as the user does not select any other color or line width.
In the next step, we add the event handlers for penNumericUpDown_ValueChanged and Button_Click. The implementation of penNumericUpDown_ValueChanged is fairly simple. The new value selected in the control is stored as the new width of the Pen object. In the case of the selectColorButton_Click event, we create an instance of the system-provided ColorDialog and show it on the screen. The dialog, provided by the .NET Framework, allows the user to select the color. When the user selects the color and closes the color dialog by clicking on the OK button, the selected color value is stored as the new pen color and the color dialog is disposed of.
Listing 7.1 shows the implementation of this functionality.
Listing 7.1 Event Handler Implementation
///
/// Event handler for the NumericUpDown control, which /// enables the user to select the line width of the pen. ///
private void penNumericUpDown_ValueChanged(object sender, System.EventArgs e) { try { GraphicsComponent.DrawingPen.Width = (float)penNumericUpDown.Value; DisplayImage(); } catch(Exception exception) { ExceptionManager.Publish(exception); } } ///
/// Opens a color dialog box to enable the user /// to select a color for the pen. ///
private void selectColorButton_Click(object sender, System.EventArgs e) { try { // Opens and shows a color dialog box. ColorDialog myDialog = new ColorDialog(); myDialog.ShowDialog(); // Sets the pen color to the selected color. GraphicsComponent.DrawingPen.Color = myDialog.Color; // Delete the color dialog. // Not really necessary because garbage collection // will take care of it anyway. myDialog.Dispose(); DisplayImage(); } catch(Exception exception) { ExceptionManager.Publish(exception); } }
With that change in place, the graphics components are drawn with the user-selected color and line width if specified.
7.6.1 Using Regions for the Frame Implementation
In addition to the capability of drawing the graphical objects with different line widths and colors, we implement a frame tool to support rectangular and ellipsoidal frames. The first step is, as always, to update the GUI with the new controls to access the functionality. Therefore, we add the elements listed in Table 7.5 to the GUI and arrange them as shown in the requirements. Also, we change the properties of the new controls to the values shown in the table.
Make sure that the radio buttons are placed on top of the group box. This is important because the .NET Framework provides radio buttons placed on a group box or a panel with exclusive selection properties. This means that at most one of the radio buttons that are placed on a specific panel or group box can be selected at any time. See the MSDN help pages for more details on this feature.
After the GUI is updated, we need to implement the event handlers for the CheckedChanged event of the two added radio buttons. We add the event handlers by double-clicking on the controls in the design view. The CheckedChange event is called every time the selection of radio buttons changes. Note that the radio buttons in the group box are already exclusive. This means that only one or none of the buttons can be selected at any time without us having to do anything.
GUI Element |
Property |
Value |
---|---|---|
RadioButton |
Text (Name) |
Rectangular Frame RectFrameRadioButton |
RadioButton |
Text (Name) |
Ellipsoidal Frame EllipseFrameRadioButton |
GroupBox |
Text |
Graphics Special Effects |
Within the event handler the selected tool needs to be changed to the corresponding frame tool. Therefore, we add two new values to the toolSelection enumeration. One is called RectFrameTool, and the other is EllipseFrameTool. In addition, the radio buttons are unset if a reset on the buttons is called. Therefore, change the resetButtons() method to also unset the radio buttons by setting the Checked property of the radio buttons to false. After all that, we implement the CheckedChanged event handlers, as shown in Listing 7.2.
Listing 7.2 Event Handler Implementation
///
/// Sets the currently selected tool to /// the RectangleFrameTool. ///
///F:image_graphics_special_effects private void RectFrameRadioButton_CheckedChanged(object sender, System.EventArgs e) { try { toolSelected = toolSelection.RectFrameTool; } catch(Exception exception) { ExceptionManager.Publish(exception); } } ///
/// Sets the currently selected tool to /// the EllipseFrameTool. ///
///F:image_graphics_special_effects /// private void EllipseFrameRadioButton_CheckedChanged(object sender, System.EventArgs e) { try { toolSelected = toolSelection.EllipseFrameTool; } catch(Exception exception) { ExceptionManager.Publish(exception); } }
Now that the new tools can be selected, the corresponding components must be implemented as children of the GraphicsComponent class.
The Ellipsoidal Frame Implementation
In this section we show the implementation of EllipseFrameComponent. The implementation uses advanced GDI+ features such as GraphicsPath, PathGradientBrush, and Region.
We create the new component as we did the other components. First, we add a new class to the photo editor application project that is named EllipseFrameComponent. Like all the components, this class is derived from the GraphicsComponent class. The responsibility of this class is to draw an ellipsoidal frame with the user-defined color and line width. As stated in the requirements, the frame width is assumed to be the entire area of the ellipse if the line width is set to 20. The region area is filled with a brush that is set to transparent color in the center of the shape and to opaque color at the border areas. Because the new class is derived from a class with abstract methods, we must implement the Draw method that overrides the GraphicsComponent-defined Draw method.
The ellipse frame component is more complex than the components we implemented in Chapter 6. Here, we want to draw a region with an ellipsoidal shape using a user-defined border width and a graded brush in a user-defined color. Last but not least, we must make some calculations before we can do the actual drawing. Therefore, we first need to define the region of the border we want to draw. This region consists of an inner and an outer border, where each border is defined by an ellipse.
We define two rectangle properties in the EllipseFrameComponent class. One rectangle is used to store the enclosing rectangle of the outer ellipse, which serves as the outer border of the frame. The second rectangle is used to describe the enclosing rectangle of the ellipse that describes the inner border of the frame. To draw the frame, we create a GraphicsPath instance from which the region of the border will be derived. Then we calculate the outer enclosing rectangle. This is similar to the calculation of the circle component shown in Chapter 6, but instead of drawing the circle, the calculated size is assigned to the outerRectangle property.
Now that the outer ellipse is defined we need to calculate the border width. Recall that a pen size of 20 means that the entire shape is handled as border area. Also consider the fact that the border appears on two sides of the image. These considerations result in a formula to calculate the border width and height:
border = (shape size * pen width) / (2 * 20)
Now that the border width and height are calculated, we can define the upper-left corner coordinates of the enclosing rectangle for the inner ellipse:
x inner = x outer rectangle + border width
y inner = y outer rectangle + border height
This gives us all the information we need to calculate the inner ellipse or, better, its enclosing rectangle. In the next step, we calculate the inner rectangle width and height:
width inner = width outer 2 * border width
height inner = height outer 2 * border width
This defines both of the enclosing rectangles for the inner and outer ellipse.
With the inner and outer ellipse known, we can calculate the resulting region of the border. The border is defined as the area of the outer ellipse excluding the area of the inner ellipse. We can calculate this region by using an xor operation on the outer and inner ellipse. After the border region is defined, it can be filled with a color using a PathGradientBrush.
So much for the theory. For the implementation, we add an ellipse to the graphics path with the outer enclosing rectangle as an argument. Then we create PathGradientBrush with the graphics path as the size argument. With the brush defined, the center color is set to transparent and the surround color is set to the user-selected color (which is black if none was selected). Add the inner ellipse to the graphics path, and use the new graphics path for the xor algebra operation on the region of the outer ellipse. This will create the region of the outer ellipse excluding the inner ellipse, which essentially is the border we want to draw. After that, call the FillRegion method on the Graphics object and provide the region and brush as arguments.
The complete implementation is shown in Listing 7.3.
Listing 7.3 The Ellipse Frame Component Implementation
using System; using System.Drawing; using System.Drawing.Drawing2D; namespace Photo_Editor_Application { ///
/// The EllipseFrameComponent class implements the /// functionality to draw an ellipse-shaped frame. ///
///F:image_graphics_special_effects public class EllipseFrameComponent : GraphicsComponent { ///
/// Constructor for circle component. ///
public EllipseFrameComponent() { } ///
/// Draws the circle in the provided Graphics object. /// Calculates the bounding box so that width and height are /// always > 0. ///
public override void Draw() { // Properties needed to draw the border. Region region; Rectangle outerRectangle; Rectangle innerRectangle = new Rectangle(); // Create a graphics path object GraphicsPath gp = new GraphicsPath(); int width = CalcViewportCoords(EndPoint).X CalcViewportCoords(StartPoint).X; int height = CalcViewportCoords(EndPoint).Y CalcViewportCoords(StartPoint).Y; // Check whether width and height are zero. // If so set them to 1. if (width == 0) width = 1; if (height == 0) height = 1; if(width < 0 && height < 0) outerRectangle = new Rectangle(CalcViewportCoords(EndPoint).X, CalcViewportCoords(EndPoint).Y, Math.Abs(width), Math.Abs(height)); else if (width < 0) outerRectangle = new Rectangle(CalcViewportCoords(EndPoint).X, CalcViewportCoords(StartPoint).Y, Math.Abs(width), height); else if (height < 0) outerRectangle = new Rectangle(CalcViewportCoords(StartPoint).X, CalcViewportCoords(EndPoint).Y, width, Math.Abs(height)); else outerRectangle = new Rectangle(CalcViewportCoords(StartPoint).X, CalcViewportCoords(StartPoint).Y, width, height); // Calculate the border width and height. // If the Line Thickness is set to the maximum value // of 20 then the whole rectangle is assumed to be border. int borderWidth = (int)(Math.Abs(width)*DrawingPen.Width)/40 ; int borderHeight = (int)(Math.Abs(height)*DrawingPen.Width)/40 ; // Set enclosing rectangle for inner ellipse, // taking into account the border width. innerRectangle.X = outerRectangle.X + borderWidth; innerRectangle.Y = outerRectangle.Y + borderHeight; // Border is on both sides. Adjust width and height of // inner ellipse accordingly. innerRectangle.Width = outerRectangle.Width - borderWidth*2; innerRectangle.Height = outerRectangle.Height borderHeight*2; // Add the enclosing Rectangle to the // GraphicsPath. gp.AddEllipse(outerRectangle); // Use a PathGradientBrush to fill the region. PathGradientBrush pgb = new PathGradientBrush(gp); // Set the center color to Transparent = Alpha equals 0. pgb.CenterColor = Color.Transparent; // More than one surround color can be defined. // In this example the colors are set to the // value the user selected or black as default. pgb.SurroundColors = new Color[] { DrawingPen.Color }; // Create a region the size of the outline rectangle region = new Region(gp); // reset the graphics path. gp.Reset(); // add the inner ellipse to the path gp.AddEllipse(innerRectangle); // subtract the inner ellipse from // the region by doing Xor. // The result is just the border with the specified size. region.Xor(gp); // Fill the region using the PathGradientBrush DeviceContext.FillRegion(pgb, region); // Dispose of the brush, graphics path, and the region. region.Dispose(); gp.Dispose(); pgb.Dispose(); } } }
Wiring the GUI Event Handlers and the New Components Together
Our final task is to create an instance of the component. The instance is created when the mouse down button event is encountered and one of the newly implemented tools is selected at the time of the mouse event. Therefore, simply add two case statements to the mouse down event handler of the customScrollableControl, as shown in Listing 7.4.
Listing 7.4 The Updated Mouse Down Event Handler
case toolSelection.RectFrameTool: compObject = new RectFrameComponent(); break; case toolSelection.EllipseFrameTool: compObject = new EllipseFrameComponent(); break;
The new components can now be used.
This implementation shows how easy it is to support new components. We extended the application by using of the Components design pattern, which enables the application to treat all components in the same way without having to know what type of component it is dealing with. Figure 7.5 shows the application with the newly added features.
Figure 7.5. Photo Editor Application
Introducing .NET
Introducing Software Engineering
A .NET Prototype
Project Planning
The Photo Editor Application
GDI+ Graphics Extensions
Advanced GDI+ Operations
Dynamic Loading of Components
Accessing System Resources
Performance Optimization, Multithreading, and Profiling
Building the Web Application with ASP.NET
Security and Database Access
Product Release