The Windows Forms namespace in .NET includes a number of classes for building Windows-based applications. One such class is the PictureBox control, which displays an image within a control window. This article shows how to extend the PictureBox control in a custom PhotoBox control that can preserve the aspect ratio of a displayed image, and discusses how to use this control in the Windows Forms Designer window of Visual Studio .NET.
Figure 1 shows a photograph in a standard PictureBox control with the SizeMode property of StretchImage. This setting stretches and distorts the image to exactly fit the bounds of the window. Figure 2 shows the same photograph using the PhotoBox control built in this article. As you can see, our new control will preserve the relationship of objects within the image, which is what we mean by preserving the aspect ratio.
It is a bit surprising that Microsoft failed to address this issue in the original PictureBox control. Fortunately, .NET controls are designed to permit extensions such as the one we present here, so a workaround is possible.
For the purposes of this article I will assume you have some understanding of C# and the use of class members such as properties and events. I also presume that you know how to create a Windows Forms application and define event handlers in Visual Studio .NET.
The PictureBox Control
Before we create our new control, a brief discussion of the PictureBox control is in order. This control inherits from the Windows Forms Control class, and provides the Image property to retrieve or assign the image to display, and the SizeMode property to retrieve or assign how the image should be displayed.
As an example, an application might define an Open menu that uses the OpenFileDialog class to accept an image from the user. Listing 1 shows a possible Click event handler for such a menu. This code creates a Bitmap object from a selected file and assigns it to the Image property of a PictureBox control. Of course, in production code, you would want to handle any exceptions that might occur while opening the file or loading the image.
You can create an application much like the one shown in Figure 1 by adding a PictureBox control to a form with its Dock property set to Fill and its SizeMode property set to StretchImage. Create a menu bar with an Open menu, and use the code from Listing 1 as a Click event handler for this menu.
Run the program and open a photographic image. As you resize the form, notice how the image is stretched and distorted to exactly fit the window. The remainder of this article shows you how to create a custom control similar to the one shown in Figure 2, which can scale an image within the window. We will call this the PhotoBox control.
Creating the PhotoBox Control
A custom control in .NET is created by deriving a new class from an existing control class. The Windows Forms namespace provides the Control class for basic control behavior, and the UserControl class is used for customized combinations of controls. For our purposes, we would like to extend the PictureBox class, so we define our new PhotoBox class as follows:
public class PhotoBox : System.Windows.Forms.PictureBox
{
// Definition of class
}
To define this class in Visual Studio .NET, create a new project called MyControlLibrary as a Windows Control Library. In this library Visual Studio will define a UserControl1 class, which you should delete. Then add a new User-Control class, called PhotoBox, to the library. Note that the new class automatically appears in the Toolbox window under the Windows Forms tab whenever a form is displayed. An existing library can also be added to the Toolbox window by right-clicking the window and selecting the Customize Toolbox... option.
Visual Studio .NET will derive your class from the UserControl class. Simply modify the base class in the PhotoBox.cs file to use the PictureBox class instead, as shown in the prior code excerpt.
Adding a New SizeMode Setting
Our implementation requires a new SizeMode setting, which we will call ScaleImage. The PictureBoxSizeMode enumeration defines the four possible settings supported by the PictureBox control, namely the Normal, StretchImage, AutoSize, and CenterImage members. To create a new setting, we will create a PhotoBoxSizeMode enumeration that provides an additional member. This enumeration is shown in Listing 2.
Note how the four members defined by the PictureBoxSizeMode enumeration are repeated in Listing 2, and how their values are assigned to the corresponding PictureBoxSizeMode values. This will permit us to use these values with either the PictureBox base class or the PhotoBox class interchangeably. Our new SizeMode setting is defined by the ScaleImage enumeration member.
Implementing the PhotoBox class
The new PhotoBox class inherits most of its behavior from the PictureBox class. We need to implement the behavior for our ScaleImage setting, but would also like to preserve the existing behavior for the common SizeMode settings. There are three important aspects of our implementation: a new SizeMode property, a new drawing behavior, and a new resize behavior. The complete listing of the PhotoBox class is shown in Listing 3. The following sections discuss these aspects of this listing.
The SizeMode Property
First, take a look at the SizeMode property. This uses an internal private _sizeMode member to hold the actual value represented by this property. We use the new keyword to indicate that our property replaces the existing SizeMode property in the base PictureBox class.
Also important here are the attributes defined in brackets prior to the property definition. These define how a visual designer such as Visual Studio .NET presents this property in the Properties window. A short list of some useful attributes defined by the System.ComponentModel namespace, including the ones used by our PhotoBox class, follows. You can see the complete list by displaying the classes derived from the Attribute class in the online documentation for the .NET Framework SDK.
BrowsableAttribute: Determines whether a property or event should appear in the Properties window
CategoryAttribute: Determines which category the property or event belongs to
DefaultEventAttribute: Defines the default event for a class. In Visual Studio .NET, a double-click on the control in the designer window adds a handler for the default event
DefaultPropertyAttribute: Defines the default property for a class
DefaultValueAttribute: Defines the default value of a property. Nondefault values are displayed in bold in the Properties window
DescriptionAttribute: Defines a short description of a property or event
ReadOnlyAttribute: Defines whether the property should be treated as read-only by Visual Studio .NET
In C#, the "Attribute" section of these classes may be omitted when defining an attribute, which is how we use them in our code.
The OnPaint() Method
In Listing 3, we override the OnPaint() and OnResize() methods to add the proper behavior for our ScaleImage enumeration to the painting and resize logic, respectively. We'll examine the OnPaint() method first.
The OnPaint() method begins by assigning a value to the base.SizeMode property and invoking the base.OnPaint() method. The base keyword accesses the base class, in this case the PictureBox class. These lines preserve the existing PictureBox paint behavior by drawing the image for all SizeMode property values other than the ScaleImage setting, and invoking any Paint event handlers registered with the class.
In the case where the SizeMode property is set to the ScaleImage setting, the subsequent lines implement this setting by first clearing the drawing area, and then scaling the image to fit within the control. This uses a private ScaleToFit() method to calculate the rectangle within the control where the image should be drawn, and then the Graphics.DrawImage() method to draw the actual image on the screen. We will not discuss these methods in detail here. The ScaleToFit() method is a standard image calculation that you can examine at your leisure; the DrawImage() method is provided by the Graphics class for drawing image objects on a GDI+ drawing surface.
Note that the logic for our ScaleImage setting has a slight performance problem. We set the base.SizeMode value to Normal to ensure that the PictureBox.OnPaint() method doesn't throw an exception. This has the side effect of drawing the image in Normal mode, after which our code redraws the image in ScaleImage mode. A more robust implementation might bypass the PictureBox control entirely and inherit from the Control class directly. We didn't do this here since we would then have to implement the BorderStyle property and all of the SizeMode settings, making for a rather long article. The point here is to show how custom controls can be created and integrated into the Properties window of Visual Studio .NET, which our PhotoBox implementation does quite well.
The OnResize() method
The OnResize() method in Listing 3 simply overrides the base method of the same name. This method ensures that the client area of the control is invalidated whenever the SizeMode property is set to ScaleImage. The base.OnResize() method is called to ensure the logic in the base class is executed and any Resize event handlers are called.
Using the PhotoBox Control
The PhotoBox control can be used in Visual Studio .NET just like any other Windows Forms control. Create a new Windows Application project and drag a PhotoBox control from the Toolbox window onto the form. Create the form shown in Figure 2 and use an Open menu with a Click event handler similar to the one shown in Listing 1. Run the application and watch how a displayed image changes as the application window is resized.
Also examine the properties of the PhotoBox control in the Properties window of Visual Studio .NET, as illustrated in Figure 3. Note how our SizeMode property appears in the Behavior category, shows the ScaleImage setting as the default value (since it isn't in bold), and displays the assigned text in the description pane at the bottom of the window. When you click on the down arrow for this property, the members of the PhotoBoxSizeMode enumeration are displayed to the user.
Topics for Further Reading
For further information on custom controls or other topics covered in this article, check out the online documentation and future editions of .NET Developer's Journal. Specific topics related to this discussion include the use of <summary> comments to provide documentation on the members of the control, and the creation of a ControlDesigner class to customize the design-time behavior of a custom control in Visual Studio .NET.
Author Bio
Erik Brown is a principal at E.E. Brown Consulting, providing intellectual property and development assistance to innovative companies. He is the author of Windows Forms Programming with C# from Manning Publications. (www.manning.com/eebrown)
erikebrown@eebrown.com
Listing 1: Possible Click event handler for an Open menu
private void menuOpen_Click
(object sender, System.EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "JPG Files (*.jpg)|*.jpg";
if (dlg.ShowDialog() == DialogResult.OK)
{
// Load the image (might throw an exception)
Listing 2: The new enumeration defined for our custom control
pictureBox1.Image = new Bitmap(dlg.OpenFile());
}
}
public enum PhotoBoxSizeMode
{
Normal = PictureBoxSizeMode.Normal,
StretchImage = PictureBoxSizeMode.StretchImage,
AutoSize = PictureBoxSizeMode.AutoSize,
CenterImage = PictureBoxSizeMode.CenterImage,
ScaleImage
}
Listing 3: The PhotoBox control
public class PhotoBox : System.Windows.Forms.PictureBox
{
private PhotoBoxSizeMode _sizeMode
= PhotoBoxSizeMode.ScaleImage;
[
Category("Behavior"),
Description("Controls how the image is "
+ "drawn within the control."),
DefaultValue(PhotoBoxSizeMode.ScaleImage),
]
public new PhotoBoxSizeMode SizeMode
{
get { return _sizeMode; }
set
{
_sizeMode = value;
this.Invalidate();
}
}
private Rectangle ScaleToFit(
Rectangle targetArea, System.Drawing.Image img)
{
Rectangle r = new Rectangle(
targetArea.Location, targetArea.Size);
// Determine best fit: width or height
if (r.Height * img.Width > r.Width * img.Height)
{
// Use width, determine height
r.Height = r.Width * img.Height / img.Width;
r.Y += (targetArea.Height - r.Height) / 2;
}
else
{
// Use height, determine width
r.Width = r.Height * img.Width / img.Height;
r.X += (targetArea.Width - r.Width) / 2;
}
return r;
}
protected override void OnPaint(PaintEventArgs e)
{
if (SizeMode == PhotoBoxSizeMode.ScaleImage)
base.SizeMode = PictureBoxSizeMode.Normal;
else
base.SizeMode = (PictureBoxSizeMode)_sizeMode;
// Call base class, invoke Paint handlers
base.OnPaint(e);
if (SizeMode == PhotoBoxSizeMode.ScaleImage
&& Image != null)
{
// Clear background
e.Graphics.Clear(SystemColors.Control);
// Implement ScaleImage drawing
Rectangle paintRec
= ScaleToFit(ClientRectangle, Image);
e.Graphics.DrawImage(this.Image, paintRec);
}
}
protected override void OnResize(EventArgs e)
{
if (SizeMode == PhotoBoxSizeMode.ScaleImage)
this.Invalidate(); // redraw image
base.OnResize(e);
}
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com