Topics in This Chapter
Graphics Overview: The first step in working with GDI+ is to understand how to create and use a Graphics
object. This section looks at how this object is created and used to handle the Paint
event.
Using the Graphics Object to Create Shapes: .NET offers a variety of standard geometric shapes that can be drawn in outline form or filled in. The GraphicsPath
class serves as a container that enables geometric shapes to be connected.
Using Pens and Brushes: Pens are used to draw shapes in outline form in different colors and widths; a brush is used to fill in shapes and create solid and gradient patterns.
Color: Colors may be defined according to red/green/blue (RGB) values or hue/saturation/brightness (HSB) values. Our project example illustrates RGB and HSB color spaces. By representing a color as an object, .NET permits it to be transformed by changing property values.
Images: .NET includes methods to load, display, and transform images. The most useful of these is the Graphics.DrawImage
method that allows images to be magnified, reduced, and rotated.
Very few programmers are artists, and only a minority of developers is involved in the world of gaming where graphics have an obvious justification. Yet, there is something compelling about writing an application that draws on a computer screen. For one thing, it's not difficult. An array of built-in functions makes it easy to create geometric objects, color them, and even animate them. In this regard, .NET should satisfy the would-be artist that resides in many programmers.
To understand the .NET graphics model, it is useful to look at its predecessor—the Win32 Graphical Device Interface (GDI). This API introduced a large set of drawing objects that could be used to create device independent graphics. The idea was to draw to a logical coordinate system rather than a device specific coordinate system—freeing the developer to concentrate on the program logic and not device details. .NET essentially takes this API, wraps it up in classes that make it easier to work with, and adds a wealth of new features.
The graphics classes are collectively called GDI+. This chapter looks at the underlying principles that govern the use of the GDI+, and then examines the classes and the functionality they provide. Several programming examples are included that should provide the tools you will need to further explore the .NET graphics namespaces.
Keep in mind that GDI+ is not restricted to WinForms
applications. Its members are also available to applications that need to create images dynamically for the Internet (Web Forms and Web Services).You should also recognize that GDI+ is useful for more than just games or graphics applications. Knowledge of its classes is essential if you want to design your own controls or modify the appearance of existing ones.
The types that make up GDI+ are contained in the gdiplus.dll
file. .NET neatly separates the classes and enumerations into logically named namespaces that reflect their use. As Figure 8-1 shows, the GDI+ functions fall into three broad categories: two-dimensional vector graphics, image manipulation, and typography (the combining of fonts and text strings to produce text output).
This figure does not depict inheritance, but a general hierarchical relationship between the GDI+ namespaces. System.Drawing
is placed at the top of the chart because it contains the basic objects required for any graphic output: Pen
, Brush
, Color
, and Font
. But most importantly, it contains the Graphics
class. This class is an abstract representation of the surface or canvas on which you draw. The first requirement for any drawing operation is to get an instance of this class, so the Graphics
object is clearly a fruitful place to begin the discussion of .NET graphics.
Drawing requires a surface to draw on, a coordinate system for positioning and aligning shapes, and a tool to perform the drawing. GDI+ encapsulates this functionality in the System.Drawing.Graphics
class. Its static methods allow a Graphics
object to be created for images and controls; and its instance methods support the drawing of various shapes such as circles, triangles, and text. If you are familiar with using the Win32 API for graphics, you will recognize that this corresponds closely to a device context in GDI. But the Graphics
object is of a simpler design. A device context is a structure that maintains state information about a drawing and is passed as an argument to drawing functions. The Graphics
object represents the drawing surface and provides methods for drawing on it.
Let's see how code gains access to a Graphics
object. Your application most likely will work with a Graphics
object inside the scope of an event handler, where the object is passed as a member of an EventArgs
parameter. The Paint
event, which occurs each time a control is drawn, is by far the most common source of Graphics
objects. Other events that have a Graphics
object sent to their event handler include PaintValue
, BeginPrint
, EndPrint
, and PrintDocument.PrintPage
. The latter three are crucial to printing and are discussed in the next chapter.
Although you cannot directly instantiate an object from the Graphics
class, you can use methods provided by the Graphics
and Control
classes to create an object. The most frequently used is Control.CreateGraphics
—an instance method that returns a graphics object for the control calling the method. The Graphics
class includes the FromHwnd
method that relies on passing a control's Handle
to obtain a Graphics
object related to the control. Let's look at both approaches.
The easiest way to create a Graphics
object for a control is to use its CreateGraphics
method. This method requires no parameters and is inherited from the Control
class by all controls. To demonstrate, let's create an example that draws on a Panel
control when the top button is clicked and refreshes all or part of the panel in response to another button click. The user interface to this program is shown in Figure 8-2 and will be used in subsequent examples.
Listing 8-1 contains the code for the Click
event handlers associated with each button. When the Decorate Panel button (btnDecor)
is clicked, a Graphics
object is created and used to draw a rectangle around the edge of the panel as well as a horizontal line through the middle. When the Refresh button (btnRefresh)
is clicked, the panel's Invalidate
method is called to redraw all or half of the panel. (More on the Invalidate
command is coming shortly.)
Example 8-1. Using Control.CreateGraphics
to Obtain a Graphics
Object
using System.Drawing; // private void btnDecor_Click(object sender, System.EventArgs e) { // Create a graphics object to draw on panel1 Graphics cg = this.panel1.CreateGraphics(); try ( int pWidth = panel1.ClientRectangle.Width; int pHeight = panel1.ClientRectangle.Height; // Draw a rectangle around border cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4); // Draw a horizontal line through the middle cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4, (pHeight-4)/2); } finally { cg.Dispose(); // You should always dispose of object } } private void btnRefresh_Click(object sender, System.EventArgs e) { // Invokes Invalidate to repaint the panel control if (this.radAll.Checked) // Radio button - All { // Redraw panel1 this.panel1.Invalidate(); } else { // Redraw left half of panel1 Rectangle r = new Rectangle(0,0,panel1.ClientRectangle.Width/2, ClientRectangle.Height); this.panel1.Invalidate(r); // Repaint area r this.panel1.Update(); // Force Paint event } }
The btnDecor Click
event handler uses the DrawRectangle
and DrawLine
methods to adorn panel1
. Their parameters—the coordinates that define the shapes—are derived from the dimensions of the containing panel control. When the drawing is completed, the Dispose
method is used to clean up system resources held by the object. (Refer to Chapter 4, “Working with Objects in C#,” for a discussion of the IDisposable
interface.) You should always dispose of the Graphics
object when finished with it. The try-finally
construct ensures that Dispose
is called even if an interrupt occurs. As shown in the next example, a using
statement provides an equivalent alternative to try
-finally
.
The btnRefresh Click
event handler is presented as a way to provide insight into how forms and controls are drawn and refreshed in a WinForms environment. A form and its child controls are drawn (displayed) in response to a Paint
event. Each control has an associated method that is responsible for drawing the control when the event occurs. The Paint
event is triggered when a form or control is uncovered, resized, or minimized and restored.
The Graphics
class has three static methods that provide a way to obtain a Graphics
object:
Graphics.FromHdc
. Creates the Graphics
object from a specified handle to a Win32 device context. This is used primarily for interoperating with GDI.
Graphics.FromImage
. Creates a Graphics
object from an instance of a .NET graphic object such as a Bitmap
or Image
. It is often used in ASP.NET (Internet) applications to dynamically create images and graphs that can be served to a Web browser. This is done by creating an empty Bitmap
object, obtaining a Graphics
object using FromImage
, drawing to the Bitmap
, and then saving the Bitmap
in one of the standard image formats.
Graphics.FromHwnd
. Creates the Graphics
object from a handle to a Window, Form
, or control. This is similar to GDI programming that requires a handle to a device context in order to display output to a specific device.
Each control inherits the Handle
property from the Control
class. This property can be used with the FromHwnd
method as an alternative to the Control.CreateGraphics
method. The following routine uses this approach to draw lines on panel1
when a MouseDown
event occurs on the panel (see Figure 8-3).
Note that the Graphics
object is created inside a using
statement. This statement generates the same code as a try-finally
construct that includes a g.Dispose()
statement in the finally
block.
A Paint
event is triggered in a WinForms application when a form or control needs to be partially or fully redrawn. This normally occurs during the natural use of a GUI application as the window is moved, resized, and hidden behind other windows. Importantly, a Paint
event can also be triggered programatically by a call to a control's Invalidate
method.
The Control.Invalidate
method triggers a Paint
event request. The btnRefresh_Click
event handler in Listing 8-1 showed two overloads of the method. The parameterless version requests that the entire panel control be redrawn; the second specifies that only the portion of the control's region specified by a rectangle be redrawn.
Here are some of the overloads for this method:
public void Invalidate() public void Invalidate(bool invalidatechildren) public void Invalidate(Rectangle rc) public void Invalidate(Rectangle rc, bool invalidatechildren)
Note: Passing a true
value for the invalidatechildren
parameter causes all child controls to be redrawn.
Invalidate
requests a Paint
event, but does not force one. It permits the operating system to take care of more important events before invoking the Paint
event. To force immediate action on the paint request, follow the Invalidate
statement with a call to Control.Update
.
Let's look at what happens on panel1
after a Paint
event occurs. Figure 8-4 shows the consequences of repainting the left half of the control. The results are probably not what you desire: half of the rectangle and line are now gone. This is because the control's paint event handler knows only how to redraw the control. It has no knowledge of any drawing that may occur outside of its scope. An easy solution in this case is to call a method to redraw the rectangle and line after calling Invalidate
. But what happens if Windows invokes the Paint
event because half of the form is covered and uncovered by another window? This clears the control and our code is unaware it needs to redraw the rectangle and line. The solution is to handle the drawing within the Paint
event handler.
When a form is resized, regions within the original area are not redrawn. To force all of a control or form to be redrawn, pass the following arguments to its SetStyle
method. Only use this when necessary, because it slows down the paint process.
this.SetStyle(ControlStyles.ResizeRedraw, true);
After the Paint
event occurs, a data class PaintEventArgs
is passed as a parameter to the Paint event handler. This class provides access to the Graphics
object and to a rectangle ClipRectangle
that defines the area where drawing may occur. Together, these properties make it a simple task to perform all the painting within the scope of the event handler.
Let's see how to rectify the problem in the preceding example, where our drawing on panel1
disappears each time the paint event occurs. The solution, of course, is to perform the drawing inside the paint event handler. To do this, first register our event handler with the PaintEventHandler
delegate:
this.panel1.Paint += new PaintEventHandler(paint_Panel);
Next, set up the event handler with the code to draw a rectangle and horizontal line on the panel. The Graphics
object is made available through the PaintEventArgs
parameter.
private void paint_Panel( object sender, PaintEventArgs e) { Graphics cg = e.Graphics; int pWidth = panel1.ClientRectangle.Width; int pHeight = panel1.ClientRectangle.Height; cg.DrawRectangle(Pens.Black,2,2,pWidth-4, pHeight-4); cg.DrawLine(Pens.Red,2,(pHeight-4)/2,pWidth-4, (pHeight-4)/2); base.OnPaint(e); // Call base class implementation }
The Control.OnPaint
method is called when a Paint
event occurs. Its role is not to implement any functionality, but to invoke the delegates registered for the event. To ensure these delegates are called, you should normally invoke the OnPaint
method within the event handler. The exception to this rule is: To avoid screen flickering, do not call this method if painting the entire surface of a control.
Painting is a slow and expensive operation. For this reason, PaintEventArgs
provides the ClipRectangle
property to define the area that is displayed when drawing occurs. Any drawing outside this area is automatically clipped. However, it is important to realize that clipping affects what is displayed—it does not prevent the drawing code from being executed. Thus, if you have a time-consuming custom paint routine, the entire painting process will occur each time the routine is called, unless you include logic to paint only what is needed.
The following example illustrates how to draw selectively. It paints a pattern of semi-randomly colored rectangles onto a form's panel (see Figure 8-5). Before each rectangle is drawn, a check is made to confirm that the rectangle is in the clipping area.
private void paint_Panel( object sender, PaintEventArgs e) { Graphics g = e.Graphics; for (int i = 0; i< this.panel1.Width;i+=20) { for (int j=0; j< his.panel1.Height;j+=20) { Rectangle r= new Rectangle(i,j,20,20); if (r.IntersectsWith(e.ClipRectangle)) { // FromArgb is discussed in Color section Brush b = new SolidBrush(Color.FromArgb((i*j)%255, (i+j)%255, ((i+j)*j)%255)); g.FillRectangle(b,r); g.DrawRectangle(Pens.White,r); } } } }
The key to this code is the Rectangle.IntersectsWith
method that checks for the intersection of two rectangles. In this case, it tests for overlap between the rectangle to be drawn and the clip area. If the rectangle intersects the clip area, it needs to be drawn. Thus, the method can be used to limit the portion of the screen that has to be repainted. To test the effects, this code was run with and without the IntersectsWith
method. When included, the event handler required 0 to 17 milliseconds—depending on the size of the area to be repainted. When run without IntersectsWith
, the event handler required 17 milliseconds to redraw all the rectangles.
Another approach to providing custom painting for a form or control is to create a subclass that overrides the base class's OnPaint
method. In this example, myPanel
is derived from the Panel
class and overrides the OnPaint
method to draw a custom diagonal line through the center of the panel.
// New class myPanel inherits from base class Panel public class myPanel: Panel { protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.DrawLine(Pens.Aqua,0,0,this.Width,this.Height); base.OnPaint(e); } }
Unless the new subclass is added to a class library for use in other applications, it is simpler to write an event handler to provide custom painting.
Let's look at the details of actually drawing with the Graphics
object. The Graphics
class contains several methods for rendering basic geometric patterns. In addition to the DrawLine
method used in previous examples, there are methods for drawing rectangles, polygons, ellipses, curves, and other basic shapes. In general, each shape can be drawn as an outline using the Pen
class, or filled in using the Brush
class. The DrawEllipse
and FillEllipse
methods are examples of this. Let's look at some examples.
Figure 8-6 demonstrates some of the basic shapes that can be drawn with the Graphics
methods. The shapes vary, but the syntax for each method is quite similar. Each accepts a drawing object—either a pen or brush—to use for rendering the shape, and the coordinates that determine the size and positions of the shape. A Rectangle
object is often used to provide a shape's boundary.
The following code segments draw the shapes shown in Figure 8-6. To keep things simple, the variables x and y are used to specify the location where the shape is drawn. These are set to the coordinates of the upper-left corner of a shape.
Pen blkPen = new Pen(Brushes.Black,2 ); // width=2 // Set this to draw smooth lines g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // (1) Draw Circle and Draw Filled Circle Rectangle r = new Rectangle(new Point(x,y),new Size(40,40)); g.DrawEllipse(blkPen, r); g.FillEllipse(Brushes.Black,x,y+60,40,40); // (2) Draw Ellipse and Filled Ellipse int w = 60; int h= 40; Rectangle r = new Rectangle(new Point(x,y),new Size(w,h)); g.DrawEllipse(blkPen, r); r = new Rectangle(new Point(x,y+60), new Size(w,h)); g.FillEllipse(Brushes.Red, r); // (3) Draw Polygon Point pt1 = new Point(x, y); Point pt2 = new Point(x+22, y+12); Point pt3 = new Point(x+22, y+32); Point pt4 = new Point(x, y+44); Point pt5 = new Point(x-22, y+32); Point pt6 = new Point(x-22, y+12); Point[] myPoints = {pt1, pt2, pt3, pt4, pt5, pt6}; g.DrawPolygon(blkPen, myPoints); // Points would be changed so as not to draw over // original polygon g.FillPolygon(Brushes.Black, myPoints); // (4)Draw Pie Shape and filled pie Rectangle r = new Rectangle( new Point(x,y),new Size(80,80)); // Create start and sweep angles int startAngle = 0; // Clockwise from x-axis int sweepAngle = -60; // Clockwise from start angle g.DrawPie(blkPen, r, startAngle, sweepAngle); g.FillPie(Brushes.Black, x,y+60,80,80,startAngle, sweepAngle); // (5) Draw Rectangle and Rectangle with beveled edges blkPen.Width=5; // make pen thicker to show bevel g.DrawRectangle(blkPen,x,y,50,40); blkPen.LineJoin = LineJoin.Bevel; g.DrawRectangle(blkPen,x,y+60,50,40); // (6) Draw Arc and Filled Pie startAngle=45; sweepAngle=180; g.DrawArc(blkPen, x,y,40,40,startAngle, sweepAngle); g.FillPie(Brushes.Black, x,y+60,40,40,startAngle,sweepAngle);
These code segments illustrate how easy it is to create simple shapes with a minimum of code. .NET also makes it easy to create more complex shapes by combining primitive shapes using the GraphicsPath
class.
The GraphicsPath
class, which is a member of the System.Drawing.Drawing2D
namespace, is used to create a container for a collection of primitive shapes. Succinctly, it permits you to add basic shapes to its collection and then to treat the collection as a single entity for the purpose of drawing and filling the overall shape. Before looking at a code example, you should be aware of some of the basic features of the GraphicsPath
class:
It automatically connects the last point of a line or arc to the first point of a succeeding line or arc.
Its CloseFigure
method can be used to automatically close open shapes, such as an arc. The first and last points of the shape are connected.
Its StartFigure
method prevents the previous line from being automatically connected to the next line.
Its Dispose
method should always be called when the object is no longer in use.
The following code creates and displays the Infinity Cross shown in Figure 8-7. It is constructed by adding five polygons to the GraphicsPath
container object. The Graphics
object then draws the outline and fills in the cross.
// g is the Graphics object g.SmoothingMode = SmoothingMode.AntiAlias; // Define five polygons Point[] ptsT= {new Point(120,20),new Point(160,20), new Point(140,50)}; Point[] ptsL= {new Point(90,50),new Point(90,90), new Point(120,70)}; Point[] ptsB= {new Point(120,120),new Point(160,120), new Point(140,90)}; Point[] ptsR= {new Point(190,90), new Point(190,50), new Point(160, 70)}; Point[] ptsCenter = {new Point(140,50), new Point(120,70), new Point(140,90), new Point(160,70)}; // Create the GraphicsPath object and add the polygons to it GraphicsPath gp = new GraphicsPath(); gp.AddPolygon(ptsT); // Add top polygon gp.AddPolygon(ptsL); // Add left polygon gp.AddPolygon(ptsB); // Add bottom polygon gp.AddPolygon(ptsR); // Add right polygon gp.AddPolygon(ptsCenter); g.DrawPath(new Pen(Color.Red,2),gp); // Draw GraphicsPath g.FillPath(Brushes.Gold,gp); // Fill the polygons
Instead of drawing and filling each polygon separately, we use a single DrawPath
and FillPath
statement to do the job.
The GraphicsPath
class has several methods worth exploring—AddCircle
, AddArc
, AddEllipse
, AddString
, Warp
, and others—for applications that require the complex manipulation of shapes. One of the more interesting is the Transform
method that can be used to rotate or shift the coordinates of a DrawPath
object. This following code segment offers a taste of how it works. A transformation matrix is created with values that shift the x coordinates by 50 units and leave the y coordinates unchanged. This Transform
method applies the matrix to the DrawPath
and shifts the coordinates; the shape is then drawn 50 units to the right of the first shape.
Matrix translateMatrix = new Matrix(); translateMatrix.Translate(50, 0); // Offset x coordinate by 50 gp.Transform(translateMatrix); // Transform path g.DrawPath(Pens.Orange,gp); // Display at new location
One of the reasons for placing shapes on a form is to permit a user to trigger an action by clicking a shape—as if she had clicked a button. Unlike a control, you cannot associate an event with a shape. Instead, you associate a MouseDown
event with the container that holds the shape(s). Recall that a MouseDown
event handler receives the x and y coordinates where the event occurs. After it has these, it is a simple process to use the rectangle and GraphicsPath
methods to verify whether a point falls within their area:
bool Rectangle.Contains(Point(x,y)) bool GraphicsPath.IsVisible(Point(x,y))
To illustrate, consider an application that displays a map of US states and responds to a click on a state by displaying the name of the state capital. The map image is placed in a PictureBox
, and rectangles and polygons are drawn on the states to set up the hit areas that respond to a MouseDown
event. Figure 8-8 shows how the picture box's paint handler routine draws rectangles on three states and a polygon on Florida. (Of course, the shapes would not be visible in the actual application.) To respond to a pressed mouse key, set up a delegate to call an event handler when the MouseDown
event occurs:
this.pictureBox1.MouseDown += new MouseEventHandler(down_Picture);
The following code implements event handler logic to determine if the mouse down occurs within the boundary of any shape.
private void down_Picture( object sender, MouseEventArgs e) { // Rectangles and GraphicsPath gp are defined // as class variables if (rectNC.Contains(e.X,e.Y) ) { MessageBox.Show("Capital: Raleigh"); } else if(rectSC.Contains(e.X,e.Y)) { MessageBox.Show("Capital: Columbia");} else if(rectGA.Contains(e.X,e.Y)) { MessageBox.Show("Capital: Atlanta");} else if(gp.IsVisible(e.X,e.Y)) {MessageBox.Show("Capital: Tallahassee");} }
After you have a basic understanding of how to create and use shapes, the next step is to enhance these shapes with eye catching graphical effects such as gradients, textured colors, and different line styles and widths. This requires an understanding of the System.Drawing
classes: Pen
, Brush
, and Color
.
The Graphics
object must receive an instance of the Pen
class to draw a shape's outline. Our examples thus far have used a static property of the Pens
class—Pens.Blue
, for example—to create a Pen
object that is passed to the Graphics
object. This is convenient, but in many cases you will want to create your own Pen
object in order to use non-standard colors and take advantage of the Pen
properties.
Constructors:
public Pen (Color color); public Pen (Color color, single width); public Pen (Brush brush); public Pen (Brush brush, single width);
Example:
Pen p1 = new Pen(Color.Red, 5); Pen p2 = new Pen(Color.Red); // Default width of 1
The constructors allow you to create a Pen
object of a specified color and width. You can also set its attributes based on a Brush
object, which we cover later in this section. Note that the Pen
class inherits the IDisposable
interface, which means that you should always call the Pen
object's Dispose
method when finished with it.
Besides color and width, the Pen
class offers a variety of properties that allow you to control the appearance of the lines and curves the Pen
object draws. Table 8-1 contains a partial list of these properties.
Table 8-1. Selected Pen Properties
Member | Description |
---|---|
| Determines how a line is drawn for closed shapes. Specifically, it specifies whether the line is drawn on the bounding perimeter or inside it. |
| Color used to draw the shape or text. |
The cap style used at the beginning and end of dashes in a dashed line. A cap style is a graphic shape such as an arrow. | |
| Distance from start of a line to the beginning of its dash pattern. |
| The type of dashed lines used. This is based on the |
| Specifies how a line is filled—for example, textured, solid, or gradient. It is determined by the |
| The cap style used at the beginning and end of lines. This comes from the |
| Floating point value used to set width of |
Let's look at some of the more interesting properties in detail.
This property defines the line style, which can be Solid
, Dash
, Dot
, DashDot
, DashDotDot
, or Custom
(see Figure 8-9). The property's value comes from the DashStyle
enumeration.
Pen p1 = new Pen(Color.Black, 3); p1.DashStyle = DashStyle.Dash; g.DrawLine(p1,20,20,180,20);
These properties define the shape used to begin and end a line. The value comes from the LineCap
enumeration, which includes ArrowAnchor
, DiamondAnchor
, Round
, RoundAnchor
, Square
, SquareAnchor
, and Triangle
. Examples of the DiamondAnchor
and RoundAnchor
are shown in Figure 8-9. The following code is used to create the lines in the figure:
Graphics g = pictureBox1.CreateGraphics(); Pen p1 = new Pen(Color.Black, 5); p1.StartCap = LineCap.DiamondAnchor; p1.EndCap = LineCap.RoundAnchor; int yLine = 20; foreach(string ds in Enum.GetNames(typeof(DashStyle))) { if (ds != "Custom") // Ignore Custom DashStyle type { // Parse creates an enum type from a string p1.DashStyle = (DashStyle)Enum.Parse( typeof(DashStyle), ds); g.DrawLine(p1,20,yLine,120,yLine); g.DrawString(ds,new Font("Arial",10),Brushes.Black, 140,yLine-8); yLine += 20; } }
The code loops through the DashStyle
enumeration and draws a line for each enum
value except Custom
. It also uses the DrawString
method to display the name of the enumeration values. This method is discussed in Chapter 9.
Brush
objects are used by these Graphics
methods to create filled geometric shapes:
FillClosedCurve FillEllipse FillPath FillPie FillPolygon FillRectangle FillRectangles FillRegion
All of these receive a Brush
object as their first argument. As with the Pen
class, the easiest way to provide a brush is to use a predefined object that represents one of the standard colors—for example, Brushes.AntiqueWhite
. To create more interesting effects, such as fills with patterns and gradients, it is necessary to instantiate your own Brush
type. Unlike the Pen
class, you cannot create an instance of the abstract Brush
class; instead, you use one of its inheriting classes summarized in Table 8-2.
Table 8-2. Brush Types That Derive from the Brush
Class
Brush Type | Description |
---|---|
| Defines a brush of a single color. It has a single constructor:
|
| Uses a preexisting image ( Image img = Image.FromFile("c:\flower.jpg"); Brush b = new TextureBrush(img); |
| Defines a rectangular brush with a foreground color, background color, and hatch style. Located in the |
| Supports either a two-color or multi-color gradient. All linear gradients occur along a line defined by two points or a rectangle. Located in the |
| Fills the interior of a |
Note that all Brush
classes have a Dispose
method that should be called to destroy the Brush
object when it is no longer needed.
The two most popular of these classes are HatchBrush
, which is handy for creating charts, and LinearGradientBrush
, for customizing the background of controls. Let's take a closer look at both of these.
As the name implies, this class fills the interior of a shape with a hatched appearance.
Constructors:
public HatchBrush(HatchStyle hStyle, Color forecolor) public HatchBrush(HatchStyle hstyle, Color forecolor, Color backcolor)
Parameters:
|
|
| The color of the lines that are drawn. |
| Color of the space between the lines (black is default). |
The predefined HatchStyle
patterns make it a simple process to create elaborate, multi-color fill patterns. The following code is used to create the DarkVertical
and DottedDiamond
rectangles at the top of each column in Figure 8-10.
Graphics g = pictureBox1.CreateGraphics(); // Fill Rectangle with DarkVertical pattern Brush b = new HatchBrush(HatchStyle.DarkVertical, Color.Blue,Color.LightGray); g.FillRectangle(b,20,20,80,60); // Fill Rectangle with DottedDiamond pattern b = new HatchBrush(HatchStyle.DottedDiamond, Color.Blue,Color.LightGray); g.FillRectangle(b,120,20,80,60);
In its simplest form, this class creates a transition that takes one color and gradually blends it into a second color. The direction of the transition can be set to horizontal, vertical, or any specified angle. The location where the transition begins can be set to a focal point other than the beginning of the area to be filled in. In cases where the gradient must be tiled to completely fill an area, options are available to control how each repeat is displayed. These options can be confusing, so let's begin with how to create a gradient brush and then work with examples that demonstrate the more useful properties and methods of the LinearGradientBrush
class.
Constructors:
public LinearGradientBrush(Rectangle rect, Color color1, Color color2, LinearGradientMode linearGradientMode) public LinearGradientBrush(Rectangle rect, Color color1, Color color2, float angle)
Parameters:
| Rectangle specifying the bounds of the gradient. |
| The start color in the gradient. |
| The end color in the gradient. |
| The angle in degrees moving clockwise from the x axis. |
| A |
There is no substitute for experimentation when it comes to understanding graphics related concepts. Figure 8-11 shows the output from filling a rectangle with various configurations of a LinearGradientBrush
object. Here is the code that creates these examples:
// Draw rectangles filled with gradient in a pictureBox
Graphics g = pictureBox1.CreateGraphics();
Size sz = new Size(100,80);
Rectangle rb = new Rectangle(new Point(20,20),sz);
// (1) Vertical Gradient (90 degrees)
LinearGradientBrush b = new
LinearGradientBrush(rb,Color.DarkBlue,Color.LightBlue,90);
g.FillRectangle(b,rb);
rb.X=140;
// (2) Horizontal Gradient
b = new LinearGradientBrush(rb,Color.DarkBlue,
Color.LightBlue,0);
g.FillRectangle(b,rb);
rb.Y = 120;
rb.X = 20;
// (3) Horizontal with center focal point
b = new LinearGradientBrush(rb,Color.DarkBlue,
Color.LightBlue,0);
// Place end color at position (0-1) within brush
b.SetBlendTriangularShape(.5f);
g.FillRectangle(b,rb);
Figure 8-11. LinearGradientBrush
examples: (1) Vertical
, (2) Horizontal
, (3) Focus Point
, (4) Tiling
The main point of interest in this code is the use of the SetBlendTriangularShape
method to create the blending effect shown in the third rectangle in Figure 8-11. This method takes an argument between 0 and 1.0 that specifies a relative focus point where the end color is displayed. The gradient then “falls off” on either side of this point to the start color.
The fourth rectangle in the figure is created by repeating the original brush pattern. The following code defines a small gradient brush that is used to fill a larger rectangle:
// Tiling Example – create small rectangle for gradient brush Rectangle rb1 = new Rectangle(new Point(0,0),new Size(20,20)); b = new LinearGradientBrush(rb,Color.DarkBlue, Color.LightBlue,0); b.WrapMode = WrapMode.TileFlipX; // Fill larger rectangle with repeats of small gradient rectangle g.FillRectangle(b,rb);
Notice how the light and dark colors are reversed horizontally before each repeat occurs: [light-dark][dark-light]. The WrapMode
property determines how the repeated gradient is displayed. In this example, it is set to the WrapMode enum
value of TileFlipX
, which causes the gradient to be reversed horizontally before repeating. The most useful enum
values include the following:
| Repeats the gradient. |
| Reverses the gradient horizontally before repeating. |
| Reverses the gradient horizontally and vertically before repeating. |
| Reverses the gradient vertically before repeating. |
It takes only a few lines of code to create a LinearGradientBrush
object that creates a multi-color gradient. The key is to set the LinearGradientBrush.InterpolationColors
property to an instance of a ColorBlend
class that specifies the colors to be used. As the following code shows, the ColorBlend
class contains an array of colors and an array of values that indicate the relative position (0–1) of each color on the gradient line. This example creates a gradient with a transition from red to white to blue—with white in the middle.
b = new LinearGradientBrush(rb,Color.Empty,Color.Empty,0); ColorBlend myBlend = new ColorBlend(); // Specify colors to include in gradient myBlend.Colors = new Color[] {Color.Red, Color.White, Color.Blue,}; // Position of colors in gradient myBlend.Positions = new float[] {0f, .5f, 1f}; b.InterpolationColors = myBlend; // Overrides constructor colors
.NET implements the Color
object as a structure that includes a large number of colors predefined as static
properties. For example, when a reference is made to Color.Indigo
, the returned value is simply the Indigo
property. However, there is more to the structure than just a list of color properties. Other properties and methods permit you to deconstruct a color value into its internal byte representation or build a color from numeric values. To appreciate this, let's look at how colors are represented.
Computers—as opposed to the world of printing—use the RGB (red/green/blue) color system to create a 32-bit unsigned integer value that represents a color. Think of RGB as a three-dimensional space with the red, green, and blue values along each axis. Any point within that space represents a unique RGB coordinate value. Throw in a fourth component, the alpha value—that specifies the color's transparency—and you have the 4-byte AlphaRGB (ARGB) value that defines a color. For example, Indigo has RGB values of 75, 0, 130, and an alpha value of 255 (no transparency). This is represented by the hex value 4B0082FF.
Colors can also be represented by the HSL (hue/saturation/luminosity) and HSB (hue/saturation/brightness) color spaces. While RGB values follow no easily discernible pattern, HSL and HSB are based on the standard color wheel (see Figure 8-12) that presents colors in an orderly progression that makes it easy to visualize the sequence. Hue is represented as an angle going counterclockwise around the wheel. The saturation is the distance from the center of the wheel toward the outer edge. Colors on the outer edge have full saturation. Brightness measures the intensity of a color. Colors shown on the wheel have 100% brightness, which decreases as they are darkened (black has 0% brightness). There is no standard for assigning HSB/HSL values. Programs often use values between 0 and 255 to correspond to the RGB numbering scheme. As we will see, .NET assigns the actual angle to the hue, and values between 0 and 1 to the saturation and brightness.
The Color
structure provides three static methods for creating a Color
object: FromName
, FromKnownColor
, and FromArgb
. FromName
takes the name of a color as a string argument and creates a new struct: Color magenta = Color.FromName("Magenta")
. The name must match one in the KnownColor
enumeration values, which is an enumeration of all the colors represented as properties in the Color
and SystemColor
structures.
FromKnownColor
takes a KnownColor
enumeration value as its argument and produces a struct for that color:
Color magenta = Color.FromKnownColor(KnownColor.Magenta);
FromArgb
allows you to specify a color by RGB and alpha values, which makes it easy to change the transparency of an existing color. Here are some of its overloads:
// (r, g, b) Color slate1 = Color.FromArgb (112, 128, 144); // (alpha, r, g, b) Color slate2 = Color.FromArgb (255, 112, 128, 144); // (alpha, Color) Color lightslate = Color.FromArgb(50, slate2 );
The Color structure has four properties that return the ARGB values of a color: Color.A
, Color.R
, Color.G
, and Color.B
. All of these properties have a value in the range 0 to 255.
Color slateGray = Color.FromArgb(255,112,128,144); byte a = slateGray.A; // 255 byte r = slateGray.R; // 112 byte g = slateGray.G; // 128 byte b = slateGray.B; // 144
The individual HSB values of a color can be extracted using the Color.GetHue
, GetSaturation
, and GetBrightness
methods. The hue is measured in degrees as a value from 0.0 to 360.0. Saturation and brightness have values between 0 and 1.
Color slateGray = Color.FromArgb(255,112,128,144); float hue = slateGray.GetHue(); // 210 degrees float sat = slateGray.GetSaturation(); // .125 float brt = slateGray.GetBrightness(); // .501
Observe in Figure 8-12 that the hue of 210 degrees (moving clockwise from 0) falls between cyan and blue on the circle—which is where you would expect to find a slate gray color.
The best way to grasp how the color spaces relate to actual .NET colors is to visually associate the colors with their RGB and HSB values. .NET offers a ColorDialog
class that can be used to display available colors; however, it does not identify colors by the system-defined names that most developers work with. So, let's build a simple color viewer that displays colors by name and at the same time demonstrates how to use the GDI+ types discussed in this section.
Figure 8-13 shows the user interface for this application. It consists of a TreeView
on the right that contains all the colors in the KnownColor
enumeration organized into 12 color groups. These groups correspond to the primary, secondary, and tertiary[1] colors of the color wheel. In terms of hues, each section is 30 degrees on the circle. The interface also contains two panels, a larger one in which a selected color is displayed, and a smaller one that displays a brightness gradient for the selected color. This is created using a multi-color gradient comprising black and white at each end, and the color at a focus point determined by its Brightness
value. The remainder of the screen displays RGB and HSB values obtained using the properties and methods discussed earlier.
The code for this application is shown in Listings 8-2 and 8-3. The former contains code to populate the TreeNode
structure with the color nodes; Listing 8-3 shows the methods used to display the selected color and its color space values. The routine code for laying out controls on a form is excluded.
Example 8-2. Color Viewer: Populate TreeNode
with All Colors
using System.Drawing; using System.Windows.Forms; using System.Drawing.Drawing2D; public class Form1 : Form { public Form1() { InitializeComponent(); // Lay out controls on Form1 // Set up event handler to be fired when node is selected colorTree.AfterSelect += new TreeViewEventHandler(ColorTree_AfterSelect); BuildWheel(); // Create Parent Nodes for 12 categories } [STAThread] static void Main() { Application.Run(new Form1()); } // Parent Nodes in TreeView for each segment of color wheel private void BuildWheel() { TreeNode tNode; tNode = colorTree.Nodes.Add("Red"); tNode = colorTree.Nodes.Add("Orange"); tNode = colorTree.Nodes.Add("Yellow"); // Remainder of nodes are added here .... } private void button1_Click(object sender, System.EventArgs e) { // Add Colors to TreeNode Structure // Loop through KnownColor enum values Array objColor = Enum.GetValues(typeof(KnownColor)); for (int i=0; i < objColor.Length; i++) { KnownColor kc = (KnownColor) objColor.GetValue(i); Color c = Color.FromKnownColor(kc); if (!c.IsSystemColor) // Exclude System UI colors { InsertColor(c, c.GetHue()); } } } private void InsertColor(Color myColor, float hue) { TreeNode tNode; TreeNode cNode = new TreeNode(); // Offset is used to start color categories at 345 degrees float hueOffset = hue + 15; if (hueOffset >360) hueOffset -= 360; // Get one of 12 color categories int colorCat = (int)(hueOffset -.1)/30; tNode = colorTree.Nodes[colorCat]; // Get parent node // Add HSB values to node's Tag HSB nodeHSB = new HSB(hue, myColor.GetSaturation(), myColor.GetBrightness()); cNode.Tag = nodeHSB; // Tag contains HSB values cNode.Text = myColor.Name; int nodeCt = tNode.Nodes.Count; bool insert=false; // Insert colors in ascending hue value for (int i=0; i< nodeCt && insert==false ;i++) { nodeHSB = (HSB)tNode.Nodes[i].Tag; if (hue < nodeHSB.Hue) { tNode.Nodes.Insert(i,cNode); insert = true; } } if (!insert) tNode.Nodes.Add(cNode); } public struct HSB { public float Hue; public float Saturation; public float Brightness; public HSB(float H, float S, float B) { Hue = H; Saturation = S; Brightness = B; } } // ---> Methods to Display Colors go here }
When the application is executed, the form's constructor calls BuildWheel
to create the tree structure of parent nodes that represent the 12 color categories. Then, when the Build Color Tree button is clicked, the Click
event handler loops through the KnownColor
enum value (excluding system colors) and calls InsertColor
to insert the color under the correct parent node. The Tag
field of the added node (color) is set to an HSB struct
that contains the hue, saturation, and brightness for the color. Nodes are stored in ascending order of Hue value. (See Chapter 7, “Windows Forms Controls,” for a discussion of the TreeNode
control.)
Listing 8-3 contains the code for both the node selection event handler and for ShowColor
, the method that displays the color, draws the brightness scale, and fills all the text boxes with RGB and HSB values.
Example 8-3. Color Viewer: Display Selected Color and Values
private void ColorTree_AfterSelect(Object sender, TreeViewEventArgs e) // Event handler for AfterSelect event { // Call method to display color and info if (e.Node.Parent != null) ShowColor(e.Node); } private void ShowColor(TreeNode viewNode) { Graphics g = panel1.CreateGraphics(); // Color panel Graphics g2 = panel2.CreateGraphics(); // Brightness panel try { // Convert node's text value to Color object Color myColor = Color.FromName(viewNode.Text); Brush b = new SolidBrush(myColor); // Display selected color g.FillRectangle(b, 0,0,panel1.Width,panel1.Height); HSB hsbVal= (HSB) viewNode.Tag; // Convert hue to value between 0 and 255 for displaying int huescaled = (int) (hsbVal.Hue / 360 * 255); hText.Text = huescaled.ToString(); sText.Text = hsbVal.Saturation.ToString(); lText.Text = hsbVal.Brightness.ToString(); rText.Text = myColor.R.ToString(); gText.Text = myColor.G.ToString(); bText.Text = myColor.B.ToString(); // Draw Brightness scale Rectangle rect = new Rectangle(new Point(0,0), new Size(panel2.Width, panel2.Height)); // Create multi-color brush gradient for brightness scale LinearGradientBrush bg = new LinearGradientBrush(rect, Color.Empty, Color.Empty,90); ColorBlend myBlend = new ColorBlend(); myBlend.Colors = new Color[] {Color.White, myColor, Color.Black}; myBlend.Positions = new float[]{0f, 1-hsbVal.Brightness,1f}; bg.InterpolationColors = myBlend; g2.FillRectangle(bg, rect); // Draw marker on brightness scale showing current color int colorPt = (int)((1–hsbVal.Brightness)* panel1.Height); g2.FillRectangle(Brushes.White,0,colorPt,10,2); b.Dispose(); bg.Dispose(); } finally { g.Dispose(); g2.Dispose(); } }
The code incorporates several of the concepts already discussed in this section. Its main purpose is to demonstrate how Color
, Graphics
, and Brush
objects work together. A SolidBrush
is used to fill panel1
with a color sample, a gradient brush creates the brightness scale, and Color
properties provide the RGB values displayed on the screen.
GDI+ provides a wide range of functionality for working with images in a runtime environment. It includes support for the following:
The standard image formats such as GIF and JPG files.
Creating images dynamically in memory that can then be displayed in a WinForms environment or served up as images on a Web server or Web Service.
Using images as a surface to draw on.
The two most important classes for handling images are the Image
and Bitmap
class. Image
is an abstract class that serves as a base class for the derived Bitmap
class. It provides useful methods for loading and storing images, as well as gleaning information about an image, such as its height and width. But for the most part, working with images requires the creation of objects that represent raster images. This responsibility devolves to the Bitmap
class, and we use it exclusively in this section.
Tasks associated with using images in applications fall into three general categories:
Loading and storing images. Images can be retrieved from files, from a stream of bytes, from the system clipboard, or from resource files. After you have loaded or created an image, it is easily saved into a specified image format using the Save
method of the Image
or Bitmap
class.
Displaying an image. Images are dynamically displayed by writing them to the surface area of a form or control encapsulated by a Graphics
object.
Manipulating an image. An image is represented by an array of bits in memory. These bits can be manipulated in an unlimited number of ways to transform the image. Traditional image operations include resizing, cropping, rotating, and skewing. GDI+ also supports changing an image's overall transparency or resolution and altering individual bits within an image.
The easiest way to bring an image into memory is to pass the name of the file to a Bitmap
constructor. Alternatively, you can use the FromFile
method inherited from the Image
class.
string fname = "c:\globe.gif";
Bitmap bmp = new Bitmap(fname);
bmp = (Bitmap)Bitmap.FromFile(fname); // Cast to convert Image
In both cases, the image stored in bmp
is the same size as the image in the file. Another Bitmap
constructor can be used to scale the image as it is loaded. This code loads and scales an image to half its size:
int w = Image.FromFile(fname).Width; int h = Image.FromFile(fname).Height; Size sz= new Size(w/2,h/2); bmp = new Bitmap(Image.FromFile(fname), sz); //Scales
GDI+ support images in several standard formats: bitmaps (BMP), Graphics Interchange Format (GIF), Joint Photographic Experts Group (JPEG), Portable Network Graphics (PNG), and the Tag Image File Format (TIFF). These are used for both loading and storing images and, in fact, make it quite simple to convert one format to another. Here is an example that loads a GIF file and stores it in JPEG format:
string fname = "c:\globe.gif"; bmp = new Bitmap(Image.FromFile(fname)); bmp.Save("c:\globe.jpg", System.Drawing.Imaging.ImageFormat.Jpeg); // Compare size of old and new file FileInfo fi= new FileInfo(fname); int old = (int) fi.Length; fi = new FileInfo("c:\globe.jpg"); string msg = String.Format("Original: {0} New: {1}",old,fi.Length); MessageBox.Show(msg); // ---> Original: 28996 New: 6736
The Save
method has five overloads; its simplest forms take the name of the file to be written to as its first parameter and an optional ImageFormat
type as its second. The ImageFormat
class has several properties that specify the format of the image output file. If you have any experience with image files, you already know that the format plays a key role in the size of the file. In this example, the new JPEG file is less than one-fourth the size of the original GIF file.
To support multiple file formats, GDI+ uses encoders to save images to a file and decoders to load images. These are referred to generically as codecs (code-decode). An advantage of using codecs is that new image formats can be supported by writing a decoder and encoder for them.
.NET provides the ImageCodecInfo
class to provide information about installed image codecs. Most applications allow GDI+ to control all aspects of loading and storing image files, and have no need for the codecs information. However, you may want to use it to discover what codecs are available on your machine. The following code loops through and displays the list of installed encoders (see Figure 8-14):
// Using System.Drawing.Imaging
string myList="";
foreach(ImageCodecInfo co in ImageCodecInfo.GetImageEncoders())
myList = myList +"
"+co.CodecName;
Console.WriteLine(myList);
The DrawImage
method of the Graphics
object is used to display an image on the Graphics
object's surface. These two statements load an image and draw it full size at the upper-left corner of the graphics surface (0,0). If the Graphics
object surface is smaller than the image, the image is cropped (see the first figure in the following example).
Bitmap bmp = new Bitmap("C:\globe.gif"); g.DrawImage(bmp,0,0); // Draw at coordinates 0,0
DrawImage
has some 30 overloaded versions that give you a range of control over sizing, placement, and image selection. Many of these include a destination rectangle, which forces the source image to be resized to fit the rectangle. Other variations include a source rectangle that permits you to specify a portion of the source image to be displayed; and some include both a destination and source rectangle.
The following examples capture most of the basic effects that can be achieved. Note that the source image is 192×160 pixels for all examples, and the destination panel is 96×80 pixels.
The source image is drawn at its full size on the target surface. Cropping occurs because the destination panel is smaller than the source image.
Graphics g = panel1.CreateGraphics(); g.DrawImage(bmp,0,0);
The source image is scaled to fit the destination rectangle.
Rectangle dRect = new Rectangle(new Point(0,0), new Size(panel1.Width,panel1.Height)); g.DrawImage(bmp, dRect); //Or panel1.ClientRectangle
Part of the source rectangle (left corner = 100,0)
is selected.
Rectangle sRect = new Rectangle(new Point(100,0), new Size(192,160)); g.DrawImage(bmp,0,0,sRect,GraphicsUnit.Pixel);
Combines examples 2 and 3: A portion of the source rectangle is scaled to fit dimensions of destination rectangle.
g.DrawImage(bmp,dRect,sRect, GraphicsUnit.Pixel);
The destination points specify where the upper-left, upper-right, and lower-left point of the original are placed. The fourth point is determined automatically in order to create a parallelogram.
Point[]dp = {new Point(10,0),new Point(80,10), new Point(0,70)}; // ul, ur, ll g.DrawImage(bmp,dp);
The DrawImage
variations shown here illustrate many familiar image effects: zoom in and zoom out are achieved by defining a destination rectangle larger (zoom in) or smaller (zoom out) than the source image; image skewing and rotation are products of mapping the corners of the original image to three destination points, as shown in the figure to the left of Example 5.
Icons (.ico
files) do not inherit from the Image
class and cannot be rendered by the DrawImage
method; instead, they use the Graphics.DrawIcon
method. To display one, create an Icon
object and pass the file name or Stream
object containing the image to the constructor. Then, use DrawIcon
to display the image at a desired location.
Icon icon = new Icon("c:\clock.ico"); g.DrawIcon(icon,120,220); // Display at x=120, y=220 icon.Dispose(); // Always dispose of object g.Dispose();
.NET also supports some more advanced image manipulation techniques that allow an application to rotate, mirror, flip, and change individual pixels in an image. We'll look at these techniques and also examine the advantage of building an image in memory before displaying it to a physical device.
Operations that rotate or skew an image normally rely on the DrawImage
overload that maps three corners of the original image to destination points that define a parallelogram.
void DrawImage(Image image, Point destPoints[])
Recall from an earlier example that the destination points are the new coordinates of the upper-left, upper-right, and lower-left corners of the source image. Figure 8-15 illustrates the effects that can be achieved by altering the destination points.
The following code is used to create a mirrored image from the original image. Think of the image as a page that has been turned over from left to right: points a and b are switched, and point c is now the lower-right edge.
Bitmap bmp = new Bitmap(fname); // Get image // Mirror Image Point ptA = new Point(bmp.Width,0); // Upper left Point ptB = new Point(0,0); // Upper right Point ptC = new Point(bmp.Width, bmp.Height); // Lower left Point[]dp = {ptA,ptB,ptC}; g.DrawImage(bmp,dp);
Many of these same effects can be achieved using the Bitmap.RotateFlip
method, which has this signature:
Public void RotateFlip(RotateFlipType rft)
RotateFlipType
is an enumeration that indicates how many degrees to rotate the image and whether to “flip” it after rotating (available rotations are 90, 180, and 270 degrees). Here are a couple of examples:
// Rotate 90 degrees bmp.RotateFlip(RotateFlipType.Rotate90FlipNone); // Rotate 90 degrees and flip along the vertical axis bmp.RotateFlip(RotateFlipType.Rotate90FlipY); // Flip horizontally (mirror) bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);
The most important thing to recognize about this method is that it changes the actual image in memory—as opposed to DrawImage
, which simply changes it on the drawing surface. For example, if you rotate an image 90 degrees and then rotate it 90 degrees again, the image will be rotated a total of 180 degrees in memory.
All of the preceding examples are based on drawing directly to a visible panel control on a form. It is also possible to load an image into, or draw your own image onto, an internal Bitmap
object before displaying it. This can offer several advantages:
It permits you to create images such as graphs dynamically and display them in the application or load them into Web pages for display.
It improves performance by permitting the application to respond to a Paint
event by redrawing a stored image, rather than having to reconstruct the image from scratch.
It permits you to keep the current “state” of the image. As long as all transformations, such as rotating or changing colors, are made first to the Bitmap
object in memory, it will always represent the current state of the image.
To demonstrate, let's input a two-color image, place it in memory, change pixels in it, and write it to a panel. Figure 8-16 shows the initial image and the final image after pixels are swapped.
The following code creates a Bitmap
object bmpMem
that serves as a buffer where the pixels are swapped on the flag before it is displayed. We use the Graphics.FromImage
method to obtain a Graphics
object that can write to the image in memory. Other new features to note are the use of GetPixel
and SetPixel
to read and write pixels on the image.
Graphics g = pan.CreateGraphics(); // Create from a panel Bitmap bmp = new Bitmap("c:\flag.gif"); g.DrawImage(bmp,0,0); // Draw flag to panel Bitmap bmpMem = new Bitmap(bmp.Width,bmp.Height); Graphics gMem = Graphics.FromImage(bmpMem); gMem.DrawImage(bmp,0,0); // Draw flag to memory // Define a color object for the red pixels Color cnRed = Color.FromArgb(255,214,41,33); // a,r,g,b // Loop through all pixels in image and swap them for (int y=0; y<bmpMem.Height; y++) { for (int x=0; x<bmpMem.Width; x++) { Color px = bmpMem.GetPixel(x,y); if(px.G > 240) bmpMem.SetPixel(x,y, cnRed); // Set white to red else bmpMem.SetPixel(x,y,Color.White); // Set red to white } } g.DrawImage(bmpMem,0,0); // Display reversed flag on panel gMem.Dispose(); g.Dispose();
This application brings together many of the concepts presented in this chapter: handling the Paint
event; using Invalidation
to clear portions of a screen; and using DrawImage
to rotate, flip, and zoom in and out on an image.
The screen for the program is shown in Figure 8-17. It consists of a menu with three main selections: File
is used to load an image; Image
has options to mirror, flip, or copy an image; and Screen
refreshes the screen. The panel control on the left serves as the main viewing window into which an image is loaded. The Image menu options are applied to this panel. The smaller panel is where part of the main image is copied. The + and – buttons zoom in and out, respectively.
The copying process is the most interesting part of the application. A user selects a rectangular area of the image by pressing the mouse button and dragging the mouse over the image. When the mouse button is raised, the selected area can be copied to the smaller panel by choosing Image-Copy from the menu.
The code for this project is presented in three sections: the menu operations, drawing the selection rectangle, and the functions associated with the small panel (panel2
).
The following fields are defined at the form level to make them available for menu operations:
private Bitmap bmp; // Holds original loaded image private Bitmap newbmp; // Holds latest version of image private bool imageStatus = false; // Indicates image is loaded private int resizeLevel; // Level image magnified/reduced
Listing 8-4 contains the code for the menu options to load an image into the viewing panel, flip an image, mirror an image, and refresh the viewing panel. The Image-Copy option is discussed in the code related to manipulating the image on panel2
.
Example 8-4. Image Viewer: Menu Items
Private void menuItem6_Click(object sender, System.EventArgs e) { // Load image from file OpenFileDialog fd = new OpenFileDialog(); fd.InitialDirectory = "c:\" ; fd.Filter = "Image Files | *.JPG;*.GIF"; if (fd.ShowDialog() == DialogResult.OK) { string fname= fd.FileName; using(Graphics g = panel1.CreateGraphics()) { bmp = new Bitmap(fname); // Load image from file newBmp = bmp; // Save copy of image // Clear main panel before drawing to it g.FillRectangle(Brushes.White,0,0, panel1.Width,panel1.Height ); Rectangle r = new Rectangle(0,0,bmp.Width, bmp.Height); g.DrawImage(bmp,r); // Draw image on panel ImageStatus = true; // Indicates image exists // Clear small panel Graphics gClear= panel2.CreateGraphics(); g.FillRectangle(Brushes.White,0,0, panel2.Width,panel2.Height ); gClear.Dispose(); } } } private void menuItem4_Click(object sender, System.EventArgs e) { // Mirror image Graphics g= panel1.CreateGraphics(); int h = newBmp.Height; int w = newBmp.Width; Point[] destPts = { new Point(w,0), new Point(0,0), new Point(w,h) }; Bitmap tempBmp = new Bitmap(w,h); Graphics gr= Graphics.FromImage(tempBmp); gr.DrawImage(newBmp, destPts); // Mirror temporary image g.DrawImage(tempBmp,0,0); // Draw image on panel newBmp = tempBmp; // Set to mirrored image g.Dispose(); gr.Dispose(); } private void menuItem3_Click(object sender, System.EventArgs e) { // Flip image vertically newBmp.RotateFlip(RotateFlipType.RotateNoneFlipY); Graphics g = panel1.CreateGraphics(); g.DrawImage(newBmp,0,0); g.Dispose(); } private void menuItem9_Click(object sender, System.EventArgs e) { // Refresh Screen panel1.Invalidate(); // Redraw entire panel panel1.Update(); selectStatus = false; // Refreshing removes selected area }
The file loading routine displays a dialog box for the user to enter the image file name. If this image file exists, it is opened and displayed in panel1
. If the image is larger than the panel, it is cropped.
The method that mirrors an image first creates a temporary Bitmap
and uses DrawImage
, as described earlier, to mirror the image to its surface. The mirrored image is displayed in the panel and saved in newBmp
. Flipping could be done in a similar way, but for demonstration purposes, we use the RotateFlip
method to directly transform newBmp
before it is displayed.
The screen refresh routine simply calls Invalidate
and Update
to redraw the image on panel1
. The main effect of this is to remove any selection rectangle (discussed next) that has been drawn on the image.
Listing 8-5 contains event handlers for Paint
, MouseDown
, MouseUp
, and MouseMove
. These routines permit the user to select a rectangular area on the image that can then be copied to panel2
. The event handling routines are associated with the events using this code in the constructor:
panel1.MouseDown += new MouseEventHandler(Mouse_Down); panel1.MouseUp += new MouseEventHandler(Mouse_Up); panel1.MouseMove += new MouseEventHandler(Mouse_Move); panel1.Paint += new PaintEventHandler(RePaint);
The following fields, defined at the form level, are used to keep status information:
private Point lastPoint = Point.Empty; // Tracks mouse movement private Point origPoint = Point.Empty; // Mouse down coordinates private Rectangle rectSel; // Selected area private bool selectStatus = false; // True if area selected
When a MouseDown
occurs, origPoint
is set to the x,y coordinates and serves as the origin of the rectangle that is to be drawn. Dragging the mouse results in a rectangle being displayed that tracks the mouse movement. The MouseMove
event handler must draw the rectangle at the new position and erase the previous rectangle. It uses lastPoint
and origPoint
to determine the part of the image to redraw in order to erase the previous rectangle. The new rectangle is determined by the current mouse coordinates and origPoint
. When the MouseUp
event occurs, rectSel
is set to the final rectangle.
Example 8-5. Image Viewer: Select Area of Image to Copy
private void RePaint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; // Redraw part of current image to panel if (ImageStatus) g.DrawImage(newBmp, e.ClipRectangle,e.ClipRectangle, GraphicsUnit.Pixel); base.OnPaint(e); } private void Mouse_Down(object sender, MouseEventArgs e) { if (lastPoint != Point.Empty) { panel1.Invalidate(rectSel); // Clear previous rect. panel1.Update(); } lastPoint.X= e.X; lastPoint.Y= e.Y; origPoint = lastPoint; // Save origin of selected area selectStatus=true; } private void Mouse_Up(object sender, MouseEventArgs e) { // Selected area complete. Define it as a rectangle. rectSel.X = e.X; if (e.X > origPoint.X) rectSel.X = origPoint.X; rectSel.Y = origPoint.Y; rectSel.Width = Math.Abs(e.X- origPoint.X)+1; rectSel.Height= Math.Abs(e.Y - origPoint.Y)+1; origPoint = Point.Empty; if (rectSel.Width < 2) selectStatus=false; } private void Mouse_Move(object sender, MouseEventArgs e) { // Tracks mouse movement to draw bounding rectangle if (origPoint != Point.Empty) { Rectangle r; Rectangle rd; // Get rectangle area to invalidate int xop = origPoint.X; if (xop > lastPoint.X) xop= lastPoint.X; int w = Math.Abs(origPoint.X - lastPoint.X)+1; int h = lastPoint.Y - origPoint.Y+1; r = new Rectangle(xop,origPoint.Y,w,h); // Get rectangle area to draw xop = e.X >= origPoint.X ? origPoint.X:e.X; w = Math.Abs(origPoint.X - e.X); h = e.Y - origPoint.Y; rd = new Rectangle(xop, origPoint.Y,w,h); Graphics g = panel1.CreateGraphics(); // Redraw image over previous rectangle g.DrawImage(newBmp,r,r); // Draw rectangle around selected area g.DrawRectangle(Pens.Red,rd); g.Dispose(); lastPoint.X= e.X; lastPoint.Y= e.Y; } }
The logic for creating the rectangles is based on establishing a point of origin where the first MouseDown
occurs. The subsequent rectangles then attempt to use that point's coordinates for the upper left corner. However, if the mouse is moved to the left of the origin, the upper left corner must be based on this x value. This is why the MouseUp
and MouseMove
routines check to see if the current x coordinate e.x
is less than that of the origin.
The following code is executed when Image – Copy is selected from the menu. The selected area is defined by the rectangle rectSel
. The image bounded by this rectangle is drawn to a temporary Bitmap
, which is then drawn to panel2
. A copy of the contents of panel2
is always maintained in the Bitmap smallBmp
.
if (selectStatus) { Graphics g = panel2.CreateGraphics(); g.FillRectangle(Brushes.White,panel2.ClientRectangle); Rectangle rd = new Rectangle(0,0,rectSel.Width,rectSel.Height); Bitmap temp = new Bitmap(rectSel.Width,rectSel.Height); Graphics gi = Graphics.FromImage(temp); // Draw selected portion of image onto temp gi.DrawImage(newBmp,rd,rectSel,GraphicsUnit.Pixel); smallBmp = temp; // save image displayed on panel2 // Draw image onto panel2 g.DrawImage(smallBmp,rd); g.Dispose(); resizeLevel = 0; // Keeps track of magnification/reduction }
The plus (+) and minus (–) buttons are used to enlarge or reduce the image on panel2
. The actual enlargement or reduction is performed in memory on smallBmp
, which holds the original copied image. This is then drawn to the small panel. As shown in the code here, the magnification algorithm is quite simple: The width and height of the original image are increased in increments of .25 and used as the dimensions of the target rectangle.
// Enlarge image Graphics g = panel2.CreateGraphics(); if (smallBmp != null) { resizeLevel= resizeLevel+1; float fac= (float) (1.0+(resizeLevel*.25)); int w = (int)(smallBmp.Width*fac); int h = (int)(smallBmp.Height*fac); Rectangle rd= new Rectangle(0,0,w,h); // Destination rect. Bitmap tempBmp = new Bitmap(w,h); Graphics gi = Graphics.FromImage(tempBmp); // Draw enlarged image to tempBmp Bitmap gi.DrawImage(smallBmp,rd); g.DrawImage(tempBmp,rd); // Display enlarged image gi.Dispose(); } g.Dispose();
The code to reduce the image is similar, except that the width and height of the target rectangle are decremented by a factor of .25:
resizeLevel= (resizeLevel>-3)?resizeLevel-1:resizeLevel; float fac= (float) (1.0+(resizeLevel*.25)); int w = (int)(smallBmp.Width*fac); int h =(int) (smallBmp.Height*fac);
As we have seen in the preceding examples, Graphics.DrawImage
is an easy-to-use method for drawing to a visible external device or to a Bitmap
object in memory. As a rule, it meets the graphics demands of most programs. However, there are situations where a more flexible or faster method is required. One of the more common graphics requirements is to perform a screen capture of an entire display or a portion of a form used as a drawing area. Unfortunately, GDI+ does not provide a direct way to copy bits from the screen memory. You may also have a graphics-intensive application that requires the constant redrawing of complex images. In both cases, the solution may well be to use GDI—specifically the BitBlt
function.
If you have worked with the Win32 API, you are undoubtedly familiar with BitBlt
. If not, BitBlt
, which is short for Bit Block Transfer, is a very fast method for copying bits to and from a screen's memory, usually with the support of the graphics card. In fact, the DrawImage
method uses BitBlt
underneath to perform its operations.
Even though it is part of the Win32 API, .NET makes it easy to use the BitBlt
function. The first step is to use the System.Runtime.InteropServices
namespace to provide the DllImportAttribute
for the function. This attribute makes the Win32 API available to managed code.
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")] private static extern int BitBlt( IntPtr hDestDC, // Handle to target device context int xDest, // x coordinate of destination int yDest, // y coordinate of destination int nWidth, // Width of memory being copied int nHeight, // Height of memory being copied IntPtr hSrcDC, // Handle to source device context int xSrc, // x coordinate of image source int ySrc, // y coordinate of image source System.Int32 dwRop // Copy is specified by 0x00CC0020 );
This function copies a rectangular bitmap from a source to a destination. The source and destination are designated by handles to their device context. (In Windows, a device context is a data structure that describes the object's drawing surface and where to locate it in memory.) The type of bit transfer performed is determined by the value of the dwRop
parameter. A simple copy takes the value shown in the declaration. By changing this value, you can specify that the source and target bits be combined by AND, OR, XOR, and other logical operators.
Using bitBlt
is straightforward. In this example, the contents of a panel are copied to a Bitmap
object in memory. Creating the Graphics
object for the panel
and Bitmap
should be familiar territory. Next, use the Graphics
object's GetHdc
method to obtain a handle for the device context for the panel and Bitmap
. These are then passed to the bitBlt
function along with a ropType
argument that tells the function to perform a straight copy operation.
// Draw an image on to a panel Graphics g = panel1.CreateGraphics(); g.DrawLine(Pens.Black,10,10,50,50); g.DrawEllipse(Pens.Blue,0,30,40,30); // Create a memory Bitmap object to be the destination Bitmap fxMem = new Bitmap(panel1.Width,panel1.Height); Graphics gfxMem = Graphics.FromImage(fxMem); int ropType= 0x00CC0020; // perform a copy operation // Get references to the device context for the source and target IntPtr HDCSrc= g.GetHdc(); IntPtr HDCMem= gfxMem.GetHdc(); // Copy a rectangular area from the panel of size 100 x 100 bitBlt(HDCMem,0,0,100,100,HDCSrc,0,0,ropType); // Release resources when finished g.ReleaseHdc(HDCSrc); gfxMem.ReleaseHdc(HDCMem); g.Dispose(); gfxMem.Dispose();
Always pair each GetHdc
with a ReleaseHdc
, and only place calls to GDI functions within their scope. GDI+ operations between the statements are ignored.
GDI+ supports a wide range of graphics-related operations ranging from drawing to image manipulation. All require use of the Graphics
object that encapsulates the drawing surface and provides methods for drawing geometric shapes and images to the abstract surface. The graphic is typically rendered to a form or control on the form to be displayed in a user interface.
The task of drawing is simplified by the availability of methods to create predefined geometric shapes such as lines, ellipses, and rectangles. These shapes are outlined using a Pen
object that can take virtually any color or width, and can be drawn as a solid line or broken into a series of dashes, dots, and combinations of these. The shapes are filled with special brush objects such as SolidBrush
, TextureBrush
, LinearGradientBrush
, and HatchBrush
.
The Image
and Bitmap
classes are used in .NET to represent raster-based images. Most of the standard image formats—BMP, GIF, JPG, and PNG—are supported. After an image is loaded or drawn, the Graphics.DrawImage
method may be used to rotate, skew, mirror, resize, and crop images as it displays them to the Graphics
object's surface.
When custom graphics are included in a form or control, it is necessary to respond to the Paint
event, to redraw all or part of the graphics on demand. A program can force this event for a control by invoking the Control.Invalidate
method.
[1] Tertiary colors are red-orange, yellow-orange, yellow-green, blue-green, blue-violet, and red-violet.