Overview of Graphical Drawing


The first thing to learn about writing graphics code is that Windows does not remember what an open window looks like if that window is obscured by other windows. When a covered-up window becomes visible, Windows will tell your application, "Your window (or some portion of it) has now become visible. Will you please draw it?" You only need draw the contents of your window. Windows itself takes care of the border of the window, the title bar, and all the other window features.

When you create a window into which you want to draw, you typically declare a class that derives from System.Windows.Forms.Form. If you are writing a custom control, you declare a class that derives from System.Windows.Forms.UserControl. In either case, you override the virtual function OnPaint(). Windows will call this function whenever any portion of your window needs to be repainted.

With this event, a PaintEventArgs class is passed as an argument. There are two pertinent pieces of information in PaintEventArgs: a Graphics object, and a ClipRectangle. You explore the Graphics class first and touch on clipping near the end of the chapter.

The Graphics Class

The Graphics class encapsulates a GDI+ drawing surface. There are three basic types of drawing surfaces:

  • Windows and controls on the screen

  • Pages being sent to a printer

  • Bitmaps and images in memory

The Graphics class provides functions to draw on any of these drawing surfaces. Among other capabilities, you can use it to draw arcs, curves, Bezier curves, ellipses, images, lines, rectangles, and text.

You can get a Graphics object for the window in two different ways. The first is to override the OnPaint() method. the Form class inherits the OnPaint() method from Control, and this method is the event handler for the Paint event that is raised whenever the control is redrawn. You can get the Graphics object from the PaintEventArgs that is passed in with the event:

 protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; // Do our drawing here. } 

At other times, you might want to draw directly into your window without waiting for the Paint event to be raised. This would be the case if you were writing code for selecting some graphical object on the window (similar to selecting icons in Windows Explorer) or dragging some object with the mouse. You can get a Graphics object by calling the CreateGraphics() method on the form, which is another method that Form inherits from Control:

 protected void Form1_Click (object sender, System.EventArgs e) { Graphics g = this.CreateGraphics(); // Do our drawing here. g.Dispose();   // this is important } 

Building an application that handles dragging and dropping is a somewhat involved affair and is beyond the scope of this chapter. In any case, this is a less common technique. You will do almost all of your drawing in response to an OnPaint() method.

Disposing of Objects

Everybody is familiar with the behavior of Windows when it runs out of resources. It starts to run very slowly, and sometimes applications will not be drawn correctly. Well-behaved applications free up their resources after they are done with them. When developing applications using the .NET Framework, there are several data types on which it is important to call the the Dispose() method or else some resources will not be freed. These classes implement the IDisposable interface, and Graphics is one of these classes.

When you get a Graphics object from the OnPaint() method, it was not created by you, so it is not your responsibility to call Dispose(), but if you call CreateGraphics( ), this is your responsibility.

Important

It is important to call the Dispose( ) method if you get a Graphics object by calling CreateGraphics( ).

The Dispose() method is automatically called in the destructor for the various classes that implement IDisposable. You might think that this removes your responsibility to call Dispose(), but it does not. The reason is that only the garbage collector (GC) ever calls the destructor, and you cannot guarantee when the GC will run. In particular, on a Windows 9X operating system with lots of memory, the GC may run very infrequently, and all resources may very well be used up before the GC runs. Although running out of memory triggers the GC to run, running out of resources does not. However, Windows 2000 and Windows XP are much less sensitive to running out of resources. According to the specifications, these two operating systems do not have any finite limits on these types of resources; however, I have seen Windows 2000 misbehave when too many applications were open, and closing some applications quickly restored correct behavior. In any case, it is better coding practice to manually dispose of any resource-hungry objects correctly and in a timely fashion.

There is another, easier way to deal with objects that need to be disposed of properly. You can use the using construct, which automatically calls Dispose() when an object goes out of scope. The following code shows the correct use of the using keyword in this context:

 using (Graphics g = this.CreateGraphics()) { g.DrawLine(Pens.Black, new Point(0, 0), new Point(3, 5)); } 

According to the documentation, the preceding code is precisely equivalent to:

 Graphics g = this.CreateGraphics(); try  { g.DrawLine(Pens.Black, new Point(0, 0), new Point(3, 5)); } finally  { if (g != null) ((IDisposable)g).Dispose(); } 

Don't confuse this use of the using keyword with the using directive that creates an alias for a names- pace or that permits the use of types in a namespace such that you do not need to fully qualify the use of the type. This is an entirely separate use of the using keyword — if you like, the block of code enclosed by the using keyword can be referred to as a using block.

Examples in this chapter handle calls to Dispose() using both styles. Sometimes, you call Dispose() directly, and other times you use the using block. The latter is a much cleaner solution, as you can see from the preceding code snippets, but there is no preferred method.

Before jumping into the first example, there are two other aspects of drawing graphics to examine — the coordinate system and colors.

Coordinate System

When designing a program that draws a complicated, intricate graphic, it is very important that your code draws exactly what you intend, and nothing but what you intend. It is possible for a single misplaced pixel to have a negative influence on the visual impact of a graphic, so it is important to understand exactly what pixels are drawn when invoking drawing operations. This is most important when creating custom controls, where you would draw lots of rectangles, horizontal lines, and vertical lines. Having a line run 1 pixel too long or fall 1 pixel short is very noticeable. However, this is somewhat less important with curves, diagonal lines, and other graphical operations.

GDI+ has a coordinate system based on imaginary grid lines that run through the center of the pixels. These lines are numbered starting at zero — the intersection of these grid lines in the upper-left pixel in any coordinate space is point X = 0, Y = 0. As a shorter notation, we can say point 1, 2, which is shorthand for saying X = 1, Y = 2. Each window into which you draw has its own coordinate space. If you create a custom control that can be used in other windows, this custom control itself has its own coordinate space. In other words, the upper-left pixel of the custom control is point 0, 0 when drawing in that custom control. You don't need to worry about where the custom control is placed on its containing window.

When drawing lines, GDI+ centers the pixels drawn on the grid line that you specify. When drawing a horizontal line with integer coordinates, it can be thought of that half of each pixel falls above the imaginary grid line, and half of each pixel falls below it. When you draw a horizontal line that is 1 pixel wide from point 1, 1 to point 5, 1, the pixels shown in Figure 30-1 will be drawn:

image from book
Figure 30-1

When you draw a vertical line that is 1 pixel wide and 4 pixels long, from point 2, 1 to point 2, 4, the pixels shown in Figure 30-2 are be colored in.

image from book
Figure 30-2

When you draw a diagonal line from point 1, 0 to point 4, 3, the pixels shown in Figure 30-3 are to be drawn.

image from book Figure 30-3

When you draw a rectangle with the upper-left corner at 1, 0 and a size of 5, 4, the rectangle drawn is shown in Figure 30-4.

image from book
Figure 30-4

There is something interesting to note here. A width of 5 was specified, and there are 6 pixels drawn in the horizontal direction. However, if you consider the grid lines running through the pixels, this rectangle is only 5 pixels wide, and the line drawn falls half a pixel outside and half a pixel inside of the grid line that you specified.

There is more to the story than this. If you draw with anti-aliasing, other pixels will be half colored in, creating the appearance of a smooth line, and partially avoiding having a stair step appearance on diagonal lines.

Figure 30-5 shows a line drawn without anti-aliasing.

image from book Figure 30-5

Figure 30-6 shows the same line drawn with anti-aliasing.

image from book
Figure 30-6

When viewed at a high resolution, this line will appear much smoother, without a stair step effect.

Understanding the relationship between the coordinates passed to drawing functions, and the resulting effect on the drawing surface makes it easy to visualize exactly which pixels will be affected by a given call to a drawing function.

There are three structs that you will use often to specify coordinates when drawing: Point, Size, and Rectangle.

Point

GDI+ uses Point to represent a single point with integer coordinates. This is a point in a two dimensional plane — a specification of a single pixel. Many GDI+ functions, such as DrawLine(), take a Point as an argument. You declare and construct a Point struct as follows:

 Point p = new Point(1, 1); 

There are public properties, X and Y, to get and set the X and Y coordinates of a Point.

Size

GDI+ uses Size to represent a size in pixels. A Size struct contains both width and height. You declare and construct a Size as follows:

 Size s = new Size(5, 5); 

There are public properties, Height and Width, to get and set the height and width of a Size.

Rectangle

GDI+ uses this structure in many different places to specify the coordinates of a rectangle. A Point structure defines the upper-left corner of the rectangle and a Size structure defines its size. There are two constructors for Rectangle. One takes as arguments the X position, the Y position, the width, and the height. The other takes a Point and a Size structure. Two examples of declaring and constructing a Rectangle are as follows:

 Rectangle r1 = new Rectangle(1, 2, 5, 6); Point p = new Point(1, 2); Size s = new Size(5, 6); Rectangle r2 = new Rectangle(p, s); 

There are public properties to get and set all aspects of the location and size of a Rectangle. In addition, there are other useful properties and methods to do such activities as determining if the rectangle intersects with another rectangle, taking the intersection of two rectangles, and taking the union of two rectangles.

GraphicsPaths

There are two more important data types that you can use as arguments to various drawing functions in GDI+. the GraphicsPath class represents a series of connected lines and curves. When constructing a path, you can add lines, Bezier curves, arcs, pie shapes, polygons, rectangles, and more. After constructing a complex path, you can draw the path with one operation: a call to DrawPath(). You can fill the path with a call to FillPath().

You construct a GraphicsPath using an array of points and PathTypes. PathTypes is a byte array, where each element in the array corresponds to an element in the array of points and gives additional information about how the path is to be constructed through each particular point. The information about the path through a point can be gleaned by using the PathPointType enumeration. For instance, if the point is the beginning of the path, the path type for that point is PathPointType.Start. If the point is a junction between two lines, the path type for that point is PathPointType.Line. If the point is used to construct a Bezier curve from the point before and after, the path type is PathPointType.Bezier.

In the following Try It Out, you will create a GraphicsPath object, and draw it to a window.

Try It Out – Creating a Graphics Path

image from book

Follow these steps to create and draw a GraphicsPath object:

  1. Create a new Windows application called DrawingPaths in the directory C:\BegVCSharp\ Chapter30.

  2. Add the following using directive for System.Drawing.Drawing2D to the top of the code:

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; 
  3. Enter the following code into the body of Form1:

     protected override void OnPaint (PaintEventArgs e) { GraphicsPath path; path = new GraphicsPath(new Point[]{ new Point(10, 10), new Point(150, 10), new Point(200, 150), new Point(10, 150), new Point(200, 160)},  new byte[] {(byte)PathPointType.Start, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line, (byte)PathPointType.Line }); e.Graphics.DrawPath(Pens.Black, path); } 

  4. Run the application, and you should see drawn the path shown in Figure 30-7.

    image from book Figure 30-7

How It Works

The code to construct this path is a quite complex. The constructor for GraphicsPath takes two arguments. The first argument is a Point array; here, you use the C# syntax for declaring and initializing the array in the same place, and create each new Point object as you go:

new Point[]{    new Point(10, 10),    new Point(150, 10),    new Point(200, 150),    new Point(10, 150),    new Point(200, 160) } 

The second argument is an array of bytes that you also construct right in place:

new byte[] {    (byte)PathPointType.Start,    (byte)PathPointType.Line,    (byte)PathPointType.Line,    (byte)PathPointType.Line,    (byte)PathPointType.Line }

Finally, you call the DrawPath() method:

e.Graphics.DrawPath(Pens.Black, path);
image from book

Regions

The Region class is a complex graphical shape comprising rectangles and paths. After constructing a Region, you can draw that region using the method FillRegion(). In the following Try It Out, you will create a Region object, and draw it to a window.

Try It Out – Creating a Region

image from book

The following code creates a region and adds a Rectangle and a GraphicsPath to it, and then fills that region with the color blue.

  1. Create a new Windows application called DrawingRegions in the directory C:\BegVCSharp\ Chapter30.

  2. Add a using directive for System.Drawing.Drawing2D to the top of the code:

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Drawing.Drawing2D; 
  3. Enter the following code into the body of Form1:

     protected override void OnPaint ( PaintEventArgs e) { Rectangle r1 = new Rectangle(10, 10, 50, 50); Rectangle r2 = new Rectangle(40, 40, 50, 50); Region r = new Region(r1); r.Union(r2); GraphicsPath path = new GraphicsPath(new Point[] { new Point(45, 45), new Point(145, 55), new Point(200, 150), new Point(75, 150), new Point(45, 45) }, new byte[] { (byte)PathPointType.Start, (byte)PathPointType.Bezier, (byte)PathPointType.Bezier, (byte)PathPointType.Bezier, (byte)PathPointType.Line }); r.Union(path); e.Graphics.FillRegion(Brushes.Blue, r); } 

  4. When you run this code, it will display as shown in Figure 30-8.

    image from book Figure 30-8

How It Works

The code to construct a region is also quite complex, though the most complex part of the example is constructing any paths that will go into the region, and you have already seen how to construct these from the previous example.

Constructing regions consists of constructing rectangles and paths, before calling the Union() method. If you desired the intersection of a rectangle and a path, you could have used the Intersection() method instead of the Union() method.

Further information on paths and regions is not particularly needed for an introduction to GDI+, so you will not explore them in any more depth in this chapter.

image from book

Colors

Many of the drawing operations in GDI+ involve a color. When drawing a line or rectangle, you need to specify what color it should be.

In GDI+, colors are encapsulated in the Color structure. You can create a color by passing red, green, and blue values to a function of the Color structure, but this is almost never necessary. the Color structure contains approximately 150 properties that get a large variety of preset colors. Forget about red, green, blue, yellow, and black — if you need to do some drawing in the color of LightGoldenrodYellow or LavenderBlush, there is a predefined color made just for you! You declare a variable of type Color and initialize it with a color from the Color structure as follows:

 Color redColor = Color.Red; Color anotherColor = Color.LightGoldenrodYellow; 

You're almost ready to do some drawing, but here are a couple of notes before going on.

Another way to represent a color is to break it into three components: Hue, Saturation, and Brightness. the Color structure contains utility methods to do this, namely GetHue(), GetSaturation(), and GetBrightness().

You can use the ColorDialog that you met in Chapter 29 to experiment with colors.

In the following Try It Out, you will create a color selection dialog and use it to see the relationship between colors defined using red, green, and blue, and colors defined using hue, saturation, and brightness.

Try It Out – Creating a Region

image from book

In a new Windows application, drag a ColorDialog control onto your form, and add a call to this.colorDialog1.ShowDialog(), after the InitializeComponent() call:

public Form1() {    InitializeComponent(); this.colorDialog1.ShowDialog(); }

When you put up the form, the color selection dialog will also be put up.

Run the application, and click the Define Custom Colors button. You will see a dialog box that allows you to pick a color using the mouse and see the RGB values for the color. You can also get the hue, saturation, and luminosity values for the color (where luminosity corresponds to brightness). You can also directly enter the RGB values and see the resulting color.

Note

Colors in GDI+ have a fourth component, the alpha component. Using this component, you can set the opacity of the color, which allows you to create fade in/fade out effects such as the menu effects in Windows 2000 and Windows XP. Explaining how to use the alpha component is beyond the scope of this chapter.

image from book




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net