GDI+

GDI+ is a graphics device interface that lets you present information on a screen or on a printer. Like its predecessor, GDI, it lets you display information without having to talk directly to the device involved. However, GDI+ is even easier to use than GDI because you simply use the classes and methods provided by the System.Drawing namespace to get your work done. Not only won’t you need to know how to talk to any specific hardware device, but you won’t have to be concerned with device contexts, pen positions, and other requirements that GDI imposed on you.

Using GDI+, you can draw lines and shapes, fill closed shapes such as rectangles and ellipses, display text on a screen or a printer, animate images, and create screen savers. In the samples that follow, we’ll demonstrate these capabilities.

Application #85 Work with GDI+ Pens

This sample demonstrates most of the features available when using the GDI+ Pen object, which you use to draw lines and shapes. In succeeding samples, we’ll look at working with brushes, text, images, screen savers, and animation.

Building Upon…

Application #1: Use Arrays

Application #29: Use the ListBox and ComboBox

New Concepts

To draw lines or curves, you need two objects: a Graphics object and a Pen object. The Graphics object has methods such as DrawLine, DrawEllipse, DrawRectangle, and others. A Graphics object is always associated with a specific object—such as a form or a PictureBox—that you can think of as the canvas on which it draws. You can think of the Graphics object as something like your hand, which you use to draw lines and shapes.

Like your hand, a Graphics object needs a drawing instrument to actually do a drawing. That’s where the Pen object comes in. It has properties that let you determine its width, its color, the style of line it draws, how intersecting lines should be joined, and much more. Some key properties of the Pen object include the following:

One other object that will figure largely in your GDI+ work is the Point object. It represents a single point on the drawing surface, indicated by x- and y-coordinates, expressed as integers. A companion object, PointF, accepts floating-point numbers for its coordinates. Figure 10-1 shows three lines drawn using several of these key properties.

Figure 10-1: With GDI+ pens, you can choose from a wide variety of line styles, start and end caps, and more.

Code Walkthrough

The heart of the sample is the RedrawPicture procedure, which collects all the user- defined information and uses it to create a Pen object. The Pen object is then used to draw one of three different kinds of drawings. As you can see in the following code, this procedure handles almost all events triggered by the user interface, specifically the change events of combo boxes and other controls for which the user makes choices.

PrivateSubRedrawPicture(ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs)_ HandlesMyBase.Activated,comboShape.SelectedIndexChanged,_ updownWidth.ValueChanged,txtColor.TextChanged,_ comboAlignment.SelectedIndexChanged,_ comboStartCap.SelectedIndexChanged,comboEndCap.SelectedIndexCha nged,_ comboDashCap.SelectedIndexChanged,_ comboLineJoin.SelectedIndexChanged,_ comboLineStyle.SelectedIndexChanged,updownMiterLimit.ValueChang ed,_ comboTransform.SelectedIndexChanged,comboBrush.SelectedIndexCha nged

To draw, we must first create the Graphics object that we’ll use to draw on the PictureBox (which is named pbLines). Then we use the Graphics object Clear method to remove anything that might already be in the picture box. Next, we get rid of any current transform on the Pen object, and we set the DashPattern that will be implemented when the user selects a custom line style.

m_graphic=pbLines.CreateGraphics() m_graphic.Clear(pbLines.BackColor) pbLines.Refresh() m_Pen.ResetTransform() m_Pen.DashPattern=NewSingle(){0.5,0.25,0.75,1.5}

A pen can have either a color or a brush assigned to it, but not both. We determine which of the two to use depending on the user’s choice in the radioColor check box. If Brush is checked, we’ll select the kind of brush the user indicated in the Brush combo box. There are four choices, which are described in the following paragraph.

A Solid brush is just what it sounds like. A Hatch brush can have a variety of styles, including Horizontal (a pattern of horizontal lines), ZigZag (horizontal lines composed of zigzags), and Plaid (the style used in our sample application). The Texture brush “paints” using an image as its base “pigment.” The Gradient brush in this example contains Alice Blue in the upper left corner of the PictureBox and ends with Dark Blue in the lower right, with a gradual transition in between.

  Tip

Assigning a Color to a pen is identical to assigning it a SolidBrush.

IfradioColor.CheckedThen m_Pen.Color=m_penColor Else SelectCasecomboBrush.Text Case "Solid" m_penBrush=NewSolidBrush(m_penColor) Case "Hatch" m_penBrush=NewHatchBrush(HatchStyle.Plaid, m_penColor) Case "Texture" m_penBrush=NewTextureBrush(_ NewBitmap("..WaterLilies.jpg"),WrapMode.Tile) Case "Gradient" m_penBrush=NewLinearGradientBrush(_ NewPoint(0,0),_ NewPoint(pbLines.Width,pbLines.Height),_ Color.AliceBlue,Color.DarkBlue) EndSelect m_Pen.Brush=m_penBrush EndIf

Now we set the properties of the pen. Together, these properties give us complete control over the look of the lines that will be drawn with the pen. Notice that, because we loaded the combo boxes with shared constants from pen-related enumerations, we’re able to use the selected item from each combo box directly. If we had loaded them with text strings, we would have had to do something like a Select Case to use their values.

m_Pen.Width=updownWidth.Value m_Pen.DashStyle=CType(comboLineStyle.SelectedItem,DashStyle) m_Pen.MiterLimit=updownMiterLimit.Value m_Pen.StartCap=CType(comboStartCap.SelectedItem,LineCap) m_Pen.EndCap=CType(comboEndCap.SelectedItem,LineCap) m_Pen.DashCap=CType(comboDashCap.SelectedItem,DashCap) m_Pen.LineJoin=CType(comboLineJoin.SelectedItem,LineJoin) m_Pen.Alignment=CType(comboAlignment.SelectedItem,PenAlignmen t)

Transforms are used for some advanced features of pens. You can, for instance, create a calligraphic-style pen with ScaleTransform. In this sample, if the user chooses None, we remove any transforms that might previously have been applied to the pen, restoring it to normal. If Scale is chosen, we make the width of the pen half as thin as normal and double its height. This yields a calligraphic look.

For Rotate, we rotate the brush by 45 degrees, and for Translate, we apply a transformation that expands the drawn object horizontally by a factor of 2 and vertically by a factor of 4. Both RotateTransform and TranslateTransform take effect only if the underlying brush supports them.

SelectCasecomboTransform.Text Case "None" m_Pen.ResetTransform() Case "Scale" m_Pen.ScaleTransform(0.5,2) Case "Rotate" m_Pen.RotateTransform(45) Case "Translate" m_Pen.TranslateTransform(2,4) EndSelect

Now that the Pen has been defined and its properties have been set, we can use the Graphics object DrawLine to draw the desired lines on the PictureBox. We’ll also draw matching thin black lines, using the same coordinates, to let the user see where the line was intended to go and to show what various properties do.

In the following code, we draw three simple lines using the user-defined pen. The DrawLine method has several overloads, including one which accepts a Pen object, followed by x- and y-coordinates for the beginning point of the line and x- and y-coordinates for its ending point. So our first line starts at pixel 35 on both the x- and y-axes, and it extends to a point 35 pixels less than the width of the PictureBox and 35 pixels down on the y-axis. In short, it’s a straight horizontal line. The second line is a vertical line, and the last one is a diagonal line.

Then we’ll draw the same three lines using the thin black pen (declared earlier at the class level) so that the user can see the effects.

IfMe.comboShape.Text= "Lines" Then Withm_graphic .DrawLine(m_Pen,35,35,pbLines.Width-35,35) .DrawLine(m_Pen,35,80,35,pbLines.Height-35) .DrawLine(m_Pen,90,80,pbLines.Width-35,_ pbLines.Height-35) EndWith Withm_graphic .DrawLine(m_BlackThinPen,35,35,pbLines.Width- 35,35) .DrawLine(m_BlackThinPen,35,80,35,pbLines.Height- 35) .DrawLine(m_BlackThinPen,90,80,pbLines.Width-35,_ pbLines.Height-35) EndWith

In our next option, we create a more complex shape by using an array of Points to define a multisegment line. Note that even though this line has several segments, it is a single line. If several independent lines were used instead, even if they connected, the end and start caps (if any) would be placed on each independent line. Here they are placed only on the beginning and end of the compound line.

We use the DrawLines method of the Graphics object (not to be confused with DrawLine), which lets you draw a continuous line that connects a series of points. To do that, it accepts a Pen object and an array of Point or PointF objects. As you might recall, a point on our drawing surface is represented by a pair of x- and y- coordinates. We’re using PointF objects because they allow floating-point coordinates, unlike Point objects, which allow only integers. We might need floating-point numbers because we’re doing math with the PictureBox width and height.

ElseIfMe.comboShape.Text= "IntersectingLines" Then DimptArray(5)AsPointF ptArray(0)=NewPointF(35,35) ptArray(1)=NewPointF(70,pbLines.Height-75) ptArray(2)=NewPointF(100,35) ptArray(3)=NewPointF(pbLines.Width- 40,pbLines.Height2) ptArray(4)=NewPointF(pbLines.Width2,pbLines.Height 2) ptArray(5)=NewPointF(pbLines.Width-25,25) m_graphic.DrawLines(m_Pen,ptArray) m_graphic.DrawLines(m_BlackThinPen,ptArray)

Our final option is to draw a circle and a curve. The DrawEllipse method will produce the circle if we provide it with a pen and a Rectangle object that will form a bounding box for the ellipse. Or we can give it starting x- and y-coordinates and the width and height of the imaginary bounding rectangle, which is what we’ve chosen to do here. The DrawArc method is identical to DrawEllipse, except that we must additionally tell it where to start and stop drawing the arc. So we give it a StartAngle, which tells it how many degrees from the x-axis to start drawing, and a SweepAngle, which represents the number of degrees from the StartAngle to the end of the arc.

ElseIfMe.comboShape.Text= "CirclesandCurves" Then m_graphic.DrawEllipse(m_Pen,25,25,200,200) m_graphic.DrawArc(m_Pen,25,25,CInt(pbLines.Width*1.5) ,_ pbLines.Height-55,90,180) m_graphic.DrawEllipse(m_BlackThinPen,25,25,200,200) m_graphic.DrawArc(m_BlackThinPen,25,25,CInt(pbLines.Width *_ 1.5),pbLines.Height-55,90,180) EndIf EndSub

Conclusion

In this sample application, we’ve shown you how GDI+ makes working with graphics even simpler than it was with GDI. It does so by using .NET Framework classes and methods. You’ve seen that to draw lines and curves, you need a Graphics object, which provides the drawing methods, and a Pen object, which holds the characteristics of the line you’re going to draw. A Graphics object is always associated with a specific object, such as a form or a PictureBox.

Application #86 Work with GDI+ Brushes

In the previous sample, we showed how .NET gives us easy access to GDI+ via a comprehensive set of classes and methods from the System.Drawing namespace. You saw that to draw you need a Graphics object and a Pen. In this sample, we’ll show you how to use the Brush object to fill closed shapes such as rectangles and ellipses.

Building Upon…

Application #85: Work with GDI+ Pens

New Concepts

Whereas you use a pen to draw the outline of a shape, you use a brush to fill the interior of such a shape. You can choose to have both a border and a fill or to have only one of the two. To fill a shape, you need a Graphics object and a Brush. The Graphics object provides methods such as FillRectangle and FillEllipse, while the Brush gives you a way to specify the color, pattern, and other properties of the fill.

Five kinds of brushes are available, two in the System.Drawing namespace and three in System.Drawing.Drawing2D. They are as follows:

Figure 10-2 shows a TextureBrush being used to fill a pair of ellipses using a photograph as its drawing source.

Figure 10-2: A TextureBrush lets you use a graphical image as the source for your drawing.

Code Walkthrough

The RedrawPicture procedure provides the meat of the demonstration. It creates one of the five types of brushes and assigns the appropriate user-defined properties to the brush. The brush is then assigned to m_Brush, which is used to draw one of three different shapes. There is also code to ensure that the user interface (UI) displays only the options that are appropriate for the type of brush being used. As you’ll notice, this procedure handles virtually all events fired by the UI.

PrivateSubRedrawPicture(ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs)HandlesMyBase.Activated,_ cboBrushType.SelectedIndexChanged,cboDrawing.SelectedIndexChang ed,_ txtColor1.TextChanged,cboWrapMode.SelectedIndexChanged,_ cboHatchStyle.SelectedIndexChanged,txtColor2.TextChanged,_ cboGradientMode.SelectedIndexChanged,nudRotation.ValueChanged, _ nudGradientBlend.ValueChanged,MyBase.Resize

After clearing the picture box and the status bar, we’re ready to get to the business of creating a brush.

SolidBrush

The first option is a solid brush, based on the user-selected color. We’ll assign it to m_brush, our class-level brush variable, because doing that will make IntelliSense help available on the brush object.

SelectCasecboBrushType.Text Case "Solid" ⋮ DimmySolidBrushAsNewSolidBrush(m_Color1) m_Brush=mySolidBrush

HatchBrush

The second option is to create a new HatchBrush using the user-selected colors for foreground and background color settings. Because the HatchStyle property is read- only, it must be set as we instantiate the HatchBrush.

Case "Hatch" ⋮ DimmyHatchBrushAsNewHatchBrush(_ CType(cboHatchStyle.SelectedItem,HatchStyle),_ m_Color1,m_Color2) m_Brush=myHatchBrush

TextureBrush

The third option is to create a new TextureBrush based on a bitmap picture of water lilies. The bitmap can also be a pattern you’ve created. The WrapMode determines how the brush will be tiled if it’s not spread over the entire graphics area. The RotateTransform method rotates the brush by the user-specified amount. We could also have used a ScaleTransform to re-shape the brush. For example, this statement cuts the width of the brush in half and doubles the height: myTextureBrush.ScaleTransform(0.5F, 2.0F)

  Caution

Be cautious when creating a TextureBrush. If you define a Rectangle larger than the bitmap, it will trigger an OutOfMemory exception.

Case "Texture" ⋮ DimmyTextureBrushAsNewTextureBrush(_ NewBitmap("..WaterLilies.jpg"),m_BrushSize) myTextureBrush.WrapMode=CType(cboWrapMode.SelectedItem ,_ WrapMode) myTextureBrush.RotateTransform(nudRotation.Value) m_Brush=myTextureBrush

LinearGradientBrush

Our next option is to create a new LinearGradientBrush. The brush is based on a size defined by a rectangle. In this case, we’re using the user-defined m_BrushSize. Two colors are used, one for defining the start color of the gradient and one for defining the end color.

  Tip

You can create more advanced gradients by using the Blend property, which lets you specify the relative intensity of each of the colors along the gradient path.

We define the LinearGradientMode in the constructor. This controls the direction of the gradient. We could have used an angle, but for simplicity that isn’t done here. The WrapMode determines how the gradient will be tiled if it is not spread over the entire graphics area. The LinearGradientBrush can use all values for WrapMode except Clamp.

To set the point where the blending will focus, you can use any value between 0 and 1. The default is 1.

Case "LinearGradient" ⋮ DimmyLinearGradientBrushAsNewLinearGradientBrush(_ m_BrushSize,m_Color1,m_Color2,_ CType(cboGradientMode.SelectedItem,LinearGradient Mode)) IfCType(cboWrapMode.SelectedItem,WrapMode)<>_ WrapMode.ClampThen myLinearGradientBrush.WrapMode=_ CType(cboWrapMode.SelectedItem,WrapMode) Else Me.sbrDrawingStatus.Text+=_ "ALinearGradientBrushcannotusethe " &_ "ClampWrapMode." EndIf myLinearGradientBrush.RotateTransform(nudRotation.Valu e) myLinearGradientBrush.SetBlendTriangularShape(_ nudGradientBlend.Value) m_Brush=myLinearGradientBrush

For more advanced uses, you can use the SetSigmaBellShape method to set where the center of the gradient occurs, as in this line of code: myLinearGradientBrush.SetSigmaBellShape(0.2)

PathGradient

The last option lets us create a path by defining a set of points and then follow that path by using PathGradient. In cases like this, you’ll often define and use a GraphicsPath object instead of a set of points, but in this case, we’re using a simple triangle. Once we’ve defined the triangle, we create a new PathGradientBrush based on the path just created. Anything not bounded by the path will be transparent instead of containing coloring.

The colors for the PathGradient are defined differently than other gradients because we can use different colors for each side. In this case, we’re using only one color, but we could assign a different color to each side of the path. The CenterColor is the color that the edges blend into. SurroundColors is an array of colors that defines the colors around the edge. We could also set the CenterPoint property somewhere other than the center of the path (even outside the rectangle bounding the path)—for example: myPathGradientBrush.CenterPoint = New PointF(50, 50)

Case "PathGradient" ⋮ DimpathPoint()AsPoint={NewPoint(0,m_BrushSize.Hei ght),_ NewPoint(m_BrushSize.Width,m_BrushSize.Height),_ NewPoint(m_BrushSize.Width,0)} DimmyPathGradientBrushAsNewPathGradientBrush(pathPoi nt) myPathGradientBrush.CenterColor=m_Color1 myPathGradientBrush.SurroundColors=NewColor(){m_Co lor2} myPathGradientBrush.WrapMode=_ CType(cboWrapMode.SelectedItem,WrapMode) myPathGradientBrush.RotateTransform(nudRotation.Value) myPathGradientBrush.SetBlendTriangularShape(_ nudGradientBlend.Value) m_Brush=myPathGradientBrush

Conclusion

In this sample application, we’ve shown you how to use a Brush to fill the interiors of shapes. A HatchBrush is a brush with a variety of possible patterns. A TextureBrush is one whose source is an image. Gradient brushes let you create fills that transition smoothly from one color to another.

Application #87 Work with GDI+ Text

You might find it surprising that to display text on a form, PictureBox, or some other surface you need to draw it—but that’s exactly the way it is. This sample shows some of the many features available when using GDI+ to work with text.

Building Upon…

Application #85: Work with GDI+ Pens

New Concepts

Fonts and text are rendered by means of GDI+, and the major method you’ll use to display text to the screen is the DrawString method of the Graphics object. You need four essential items to display text using DrawString:

Whereas you use a Pen to draw lines and curves, you need a Brush to draw text. As always, the Brush is a holder for characteristics of the drawing, while the Graphics object provides the methods for doing the drawing.

You have a variety of options when displaying text. You always display text within a bounding rectangle, within which you can use hatch brushes, texture brushes, gradient brushes, and of course, solid brushes. You must carefully measure the size of the text you intend to display, because the size of the bounding rectangle will determine whether the text is appropriately positioned on the screen and whether it will fit in the space provided. Figure 10-3 shows text reflected by the TranslateTransform method.

Figure 10-3: When you draw text with GDI+ brushes, you can use

transformations to show the text in a variety of orientations.

Code Walkthrough

We’ll examine several options for displaying text on a drawing surface, illustrating the point we made earlier—that text must be drawn.

Block Text

Our first option creates the sample text with a block appearance. We begin by setting up the brushes we’ll use for the foreground and background. We need a font object for the text, so we create one, using the values the user has set. Notice how we’ve used the CheckState of the chkBold check box. CheckState.Checked is equal to 1 and Unchecked is 0. FontStyle.Bold is equal to 1 and Regular is 0. So we simply cast the check state of the check box to a font style and we’ve declared whether we want boldface type or not. Then we create a Graphics object from the picture box and clear the box.

PrivateSubbtnBlockText_Click(... DimmyForeBrushAsBrush=Brushes.Aquamarine DimmyBackBrushAsBrush=Brushes.Black DimmyStyleAsFontStyle=CType(chkBold.CheckState,FontStyle) DimmyFontAsNewFont("TimesNewRoman",Me.nudFontSize.Value, _ myStyle) DimgAsGraphics=picDemoArea.CreateGraphics() g.Clear(Color.White)

We need to determine the size that will be required to draw the sample text. The MeasureString method provides the exact dimensions of the provided string, so you can determine whether it will fit in the area where you want to display it. MeasureString returns its results in a SizeF structure, which has Width and Height properties. Using those properties, we calculate the x- and y-coordinates for the starting point of our display.

DimtextSizeAsSizeF=g.MeasureString(Me.txtSample.Text,myFo nt) DimxLocationAsSingle=(picDemoArea.Width-textSize.Width)/ 2 DimyLocationAsSingle=(picDemoArea.Height- textSize.Height)/3

Now we’re ready to display the text. We’ll draw the black background first. To get the block effect, we draw the text repeatedly from the offset in the lower left up to the point where the main text will be drawn. Because people tend to think of light as coming from the upper left, we’ve subtracted the offset depth from the x dimension instead of adding it. If we had added it, the block might look more like a shadow.

DimiAsInteger Fori=CInt(nudBlockDepth.Value)To0Step-1 g.DrawString(txtSample.Text,myFont,myBackBrush,_ xLocation-i,yLocation+i) Next

Finally, we draw the white main text over the black text. Note how we provide the DrawString method with the text we want to display, the font to use, the brush to draw with, and the coordinates at which to draw.

g.DrawString(txtSample.Text,myFont,myForeBrush,xLocation, _ yLocation) EndSub

Brush Text

Here we’ll display the sample text by using either a Hatch or Gradient brush. In creating a HatchBrush, we must specify the style of hatch we want (in this case, Diagonal Brick), as well as foreground and background colors. Before we create the LinearGradientBrush, we want to provide a boundary within which the gradient will be displayed. So we create a rectangle that’s the size of our text. Then we pass that rectangle as the first argument to the constructor of the LinearGradientBrush, along with the starting and ending colors of the gradient and the orientation of the gradient (in this case, Forward Diagonal).

PrivateSubbtnBrushText_Click(... ⋮ DimtextSizeAsSizeF=g.MeasureString(Me.txtSample.Text,myFont ) DimmyBrushAsBrush IfMe.optHatch.CheckedThen myBrush=NewHatchBrush(HatchStyle.DiagonalBrick,_ Color.Yellow,Color.Blue) Else DimgradientRectangleAsNewRectangleF(NewPointF(0,0),t extSize) myBrush=NewLinearGradientBrush(gradientRectangle,Col or.Blue,_ Color.Yellow,LinearGradientMode.ForwardDiagonal) EndIf g.DrawString(txtSample.Text,myFont,myBrush,_ (picDemoArea.Width-textSize.Width)/2,_ (picDemoArea.Height-textSize.Height)/3) EndSub

Embossed Text

The following procedure creates the sample text with an embossed look. To create the effect, the sample text is drawn twice. It’s drawn first in black, offset slightly from the drawing starting point, and then drawn again in white, the current background color. This gives the impression that the text is raised. To give the impression of engraving instead of embossing, simply use the negative of the offset value.

PrivateSubbtnEmboss_Click(... DimmyBackBrushAsBrush=Brushes.Black DimmyForeBrushAsBrush=Brushes.White ⋮ DimtextSizeAsSizeF=g.MeasureString(Me.txtSample.Text,myFon t) xLocation=(picDemoArea.Width-textSize.Width)/2 yLocation=(picDemoArea.Height-textSize.Height)/3

We’ll draw the black background first. (Note: if you subtract the nudEmbossDepth value instead of adding it, you’ll get an Engraved effect. Try it with the Depth control on the form.) Finally, we draw the white main text over the black text.

g.DrawString(txtSample.Text,myFont,myBackBrush,_ xLocation+Me.nudEmbossDepth.Value,_ yLocation+Me.nudEmbossDepth.Value) g.DrawString(txtSample.Text,myFont,myForeBrush,xLocation,yLo cation) EndSub

Reflected Text

This example reflects text around the baseline of the characters. It’s more advanced than most of the other examples and requires careful measurement of the text. Because we’ll be scaling, and scaling effects the entire Graphics object not just the text, we need to reposition the origin of the Graphics object from (0,0) to the (xLocation, yLocation) point. If we don’t, when we attempt to flip the text with a scaling transform, it will merely draw the reflected text at (xLocation, -yLocation), which is outside the viewable area.

PrivateSubbtnReflectedText_Click(... DimmyBackBrushAsBrush=Brushes.Gray DimmyForeBrushAsBrush=Brushes.Black ⋮ g.TranslateTransform(xLocation,yLocation)

Reflecting around the origin still poses problems. The origin represents the upper left corner of the text’s bounding rectangle. This means the reflection will occur at the top of the original drawing. This is not how people are used to seeing reflected text. So we need to determine where to draw the text, and we can do that only when we’ve calculated the height required by the drawing.

This is not as simple as it might seem. The Height returned from the MeasureString method includes some extra spacing for descenders and white space. But we want only the height from the baseline (which is the line on which all caps sit). Any characters with descenders drop below the baseline. To calculate the height above the baseline, we need to use the GetCellAscent method. Because GetCellAscent returns a Design Metric value, it must be converted to pixels and scaled for the font size.

DimlineAscentAsInteger DimlineSpacingAsInteger DimlineHeightAsSingle DimtextHeightAsSingle lineAscent=myFont.FontFamily.GetCellAscent(myFont.Style) lineSpacing=myFont.FontFamily.GetLineSpacing(myFont.Style) lineHeight=myFont.GetHeight(g) textHeight=lineHeight*lineAscent/lineSpacing

  Tip

Reflection looks best with characters that can be reflected over the baseline nicely—like capital letters. Characters with descenders look odd. To fix that, factor in the height of the descenders as you calculate the text height, and then you’ll reflect across the lowest descender height. Here’s how:

DimlineDescentAsInteger lineDescent=myFont.FontFamily.GetCellDescent(myFont.Style) textHeight=lineHeight*(lineAscent+lineDescent)/lineSpacing

We’ll draw the reflected text first so that we can demonstrate the use of the GraphicsState object. A GraphicsState object maintains the state of the Graphics object as it currently stands. You can then scale, resize, and otherwise transform the Graphics object. You can immediately go back to a previous state by using the Restore method of the Graphics object. Had we drawn the main one first, we would not have needed the Restore method or the GraphicsState object.

First we’ll save the graphics state so that we can restore it later. To draw the reflection, we’ll use the ScaleTransform method with a negative value. Using -1 will reflect the text with no distortion. Then we restore the previous state and draw the main text.

DimmyStateAsGraphicsState=g.Save() g.ScaleTransform(1,-1.0F) g.DrawString(txtSample.Text,myFont,myBackBrush,0,- textHeight) g.Restore(myState) g.DrawString(txtSample.Text,myFont,myForeBrush,0,- textHeight) EndSub

Shadowed Text

This example draws the sample text with a solid brush and a shadow. To create the shadow, the sample text is drawn twice. The first time it’s offset and drawn in gray, and then it’s drawn again normally in black.

PrivateSubbtnShadowText_Click(... DimmyShadowBrushAsBrush=Brushes.Gray DimmyForeBrushAsBrush=Brushes.Black ⋮ g.DrawString(txtSample.Text,myFont,myShadowBrush,_ xLocation+Me.nudShadowDepth.Value,_ yLocation+Me.nudShadowDepth.Value) g.DrawString(txtSample.Text,myFont,myForeBrush,xLocation,yLo cation) EndSub

Sheared Text

The following procedure shears the text so that it appears angled. This requires the use of a Matrix, which will define the shear. Because we’ll be scaling, and scaling affects the entire Graphics object and not just the text, we need to reposition the origin of the Graphics object from (0, 0) to the (xLocation, yLocation) point.

PrivateSubbtnShearText_Click(... ⋮ g.TranslateTransform(xLocation,yLocation)

Now we set a reference to the Transform object for the current Graphics object, Shear it by the specified amount, and finally, draw the main text.

DimmyTransformAsMatrix=g.Transform myTransform.Shear(nudSkew.Value,0) g.Transform=myTransform g.DrawString(txtSample.Text,myFont,myForeBrush,0,0) EndSub

Simple Text

The following procedure simply takes the lines of text in the text box and places them in the picDemoArea PictureBox. The text will word wrap as necessary, but it will not scroll.

PrivateSubbtnSimpleText_Click(... ⋮ g.DrawString(txtLongText.Text,myFont,myForeBrush,_ NewRectangleF(0,0,picDemoArea.Width,picDemoArea.Height)) EndSub

Conclusion

In this sample application, you’ve seen that you display text within a bounding rectangle, whose size you must measure with the MeasureString method to determine whether the text is appropriately positioned on the screen and whether it will fit in the space provided. You’ve also seen that you can use any font on your system when you’re drawing text. Powerful scaling and transformation methods let you twist, bend, shear, and distort text into a variety of shapes.

Application #88 Work with GDI+ to Manipulate Images

This sample shows you how to manipulate images using GDI+, including changing the size of an image and rotating, zooming, and cropping an image.

Building Upon…

Application #85: Work with GDI+ Pens

New Concepts

GDI+ provides new ways to work with images. This example illustrates several of them.

Changing the Size of an Image

Once you’ve loaded your image into a PictureBox, you can apply one of four SizeMode settings to the PictureBox:

Rotating Images

The Image class RotateFlip method lets you rotate an image, flip it, or both. You provide the method with a RotateFlipType argument (for example, Rotate180FlipX) that determines how many degrees you want to rotate the image and the axis, if any, on which you want to flip it. Choices include rotation by 90, 180, and 270 degrees; flipping on the x-axis, y-axis, or both; and combinations of all these options. Rotation is always clockwise.

Zooming and Cropping Images

Zooming means resizing the image so that it appears that the user’s perspective has changed by being either brought closer to or moved farther away from the picture.

Cropping consists of creating a new image at a size you specify and filling it with the portion of the old image that will fit the dimensions of the new one. If you want to allow the user to undo the crop, you need to create a variable that holds a copy of the bitmap before it’s cropped. Figure 10-4 shows an image that is both rotated and cropped.

Figure 10-4: The WaterLilies.jpg, which is bigger than the PictureBox, is centered within it, and its edges are cropped. The inset shows a portion of the image after it is rotated by 90 degrees.

Code Walkthrough

Let’s examine some ways we can manipulate an image, starting with resizing it.

Image Sizing

All four size settings mentioned previously are represented by four Size Mode radio buttons on the sample form. The SizeModeRadioButtons_CheckedChanged procedure handles the CheckedChanged events of all four radio buttons, and it adjusts the PictureBox.SizeMode property accordingly. Each radio button stores one of the values of the PictureBoxSizeMode enumeration (whose values are 0 through 3) in its Tag property.

PrivateSubSizeModeRadioButtons_CheckedChanged(... DimoptAsRadioButton=CType(sender,RadioButton) DimsmAsPictureBoxSizeMode=CType(opt.Tag,PictureBoxSizeMode ) Ifopt.CheckedThen picImage.SizeMode=sm

You must manually reset the PictureBox to its original size if AutoSize has been set. It will not automatically return to the size set in the designer.

Ifsm=PictureBoxSizeMode.AutoSizeThen btnFit.Enabled=False Else btnFit.Enabled=True picImage.Width=PICTUREBOX_WIDTH picImage.Height=PICTUREBOX_HEIGHT EndIf EndIf EndSub

The Fit procedure makes the image fit properly in the PictureBox. You might think that AutoSize would make the image appear in the PictureBox according to its true aspect ratio within the fixed bounds of the PictureBox. But AutoSize simply expands or shrinks the PictureBox itself. So the Fit procedure determines whether the image is smaller than the PictureBox. If it is, and if Fit was called by the Zoom In button, it centers the image.

PrivateSubFit() IfpicImage.Image.WidthpicImage.Image.HeightIfNotIsFitForZoomInThen picImage.SizeMode=PictureBoxSizeMode.CenterImage EndIf EndIf CalculateAspectRatioAndSetDimensions() EndSub

Image Rotation

As we mentioned earlier, rotation is always clockwise when you use the RotateFlip method, and in btnRotateRight_Click in the following code, we’re simply rotating the image by 90 degrees. In btnRotateLeft_Click, we’re achieving the left rotation by rotating the image by 270 degrees, which is the same as if we had rotated it counter- clockwise by 90 degrees. Note that in each case we need to refresh the PictureBox after the rotation.

PrivateSubbtnRotateRight_Click(... picImage.Image.RotateFlip(RotateFlipType.Rotate90FlipNone) picImage.Refresh() EndSub PrivateSubbtnRotateLeft_Click(... picImage.Image.RotateFlip(RotateFlipType.Rotate270FlipNone) picImage.Refresh() EndSub

Zooming

To zoom, we resize the image, as shown in the following procedure. When zooming in or out, the SizeMode controls on the sample form are disabled. Otherwise, the zooming won’t work as anticipated. The following If test ensures that the initial Zoom In transition is smooth. Without the test, if the SizeMode is something other than AutoSize, the image can appear to Zoom Out on the first click, and then Zoom In on subsequent clicks.

PrivateSubbtnZoomIn_Click(... IfgrpSizeMode.EnabledThen picImage.SizeMode=PictureBoxSizeMode.AutoSize EndIf grpSizeMode.Enabled=False btnFit.Enabled=True IsFitForZoomIn=True

The StretchImage mode works best for zooming because the image is forced to conform to the size of the PictureBox. Zoom works best if you first fit the image according to its true aspect ratio. (See CalculateAspectRatioAndSetDimensions.)

When it’s time to actually do the zoom, you do it by simply adjusting the image’s dimensions up or down—in this case, by 25 percent. You could, of course, choose any increment you want, including letting the user enter it.

picImage.SizeMode=PictureBoxSizeMode.StretchImage Fit() picImage.Width=CInt(picImage.Width*1.25) picImage.Height=CInt(picImage.Height*1.25) EndSub PrivateSubbtnZoomOut_Click(... grpSizeMode.Enabled=False btnFit.Enabled=True Fit() picImage.SizeMode=PictureBoxSizeMode.StretchImage picImage.Width=CInt(picImage.Width/1.25) picImage.Height=CInt(picImage.Height/1.25) EndSub

The following procedure calculates and returns the image’s aspect ratio and sets its proper dimensions. It’s used by the Fit procedure and also for saving thumbnails of images.

PrivateFunctionCalculateAspectRatioAndSetDimensions()AsDouble DimratioAsDouble IfpicImage.Image.Width>picImage.Image.HeightThen ratio=picImage.Image.Width/_ picImage.Image.Height picImage.Height=CInt(CDbl(picImage.Width)/ratio) Else ratio=picImage.Image.Height/_ picImage.Image.Width picImage.Width=CInt(CDbl(picImage.Height)/ratio) EndIf Returnratio EndFunction

Cropping

In our example, we accept upper left x- and y-coordinates from the user, along with the width and height of the desired new image. Then we create a rectangle defined by those coordinates (relative to the upper left corner of the PictureBox) and the desired width and height.

PrivateSubbtnCrop_Click(... IfIsValidCropValues()Then imgUndo=picImage.Image btnUndo.Enabled=True DimrecSourceAsNewRectangle(CInt(txtXCoord.Text),_ CInt(txtYCoord.Text),CInt(txtWidth.Text),_ CInt(txtHeight.Text))

  Caution

You might be tempted to create a Graphics object off the PictureBox (rather than a new Bitmap) and then to clear the PictureBox and draw the cropped image onto it, like this:

DimgrPicImageAsGraphics=picImage.CreateGraphics grPicImage.Clear(picImage.BackColor) grPicImage.DrawImage(picImage.Image,0,0,recSource,_ GraphicsUnit.Pixel)

This will appear to work, but as soon as you use any of the other controls on the form you’ll see that the PictureBox actually still contains the original image, not the cropped one.

Then we create a new, blank Bitmap on which we will draw the cropped image. We get a Graphics object from the Bitmap for drawing, and we draw the image in the upper left corner of the Bitmap. Finally, we set the PictureBox image to the new cropped image.

DimbmpCroppedAsNewBitmap(CInt(txtWidth.Text),_ CInt(txtHeight.Text)) DimgrBitmapAsGraphics=Graphics.FromImage(bmpCropped) grBitmap.DrawImage(picImage.Image,0,0,recSource,_ GraphicsUnit.Pixel) picImage.Image=bmpCropped EndIf EndSub

Conclusion

In this sample application, we’ve shown you that an image in a PictureBox can be resized several ways by setting the SizeMode property of the PictureBox. You’ve seen that you can rotate and flip an image at the same time, and the RotateFlip method accepts a variety of RotateFlipType arguments that let you turn and flip an image in any direction you want. Zooming means resizing the image. Be careful about the first user click after choosing Zoom In if the SizeMode is something other than AutoSize.

Application #89 Create a Screen Saver with GDI+

You might think there’s something mysterious about a Microsoft Windows screen saver, but it’s really nothing more than a Windows application in disguise. You can easily build your own screen saver by creating a regular Windows application with a form that’s displayed in dialog mode, maximized to fill the screen, and waiting for a cue to be dismissed. The cue can be a mouse click or a movement of the mouse. This sample shows you how to create and deploy a screen saver with Visual Basic .NET.

Building Upon…

Application #82: Serialize Objects

Application #85: Work with GDI+ Pens

Application #86: Work with GDI+ Brushes

Application #88: Work with GDI+ to Manipulate Images

New Concepts

You’ll want your screen saver form not to have a title bar, not to show up in the taskbar, and not to be “touchable” in any way by the user (except to dismiss it with a mouse click or a mouse movement). So you’ll need to set the following form properties:

Code Walkthrough

The sample solution has two projects: Create a Screensaver with GDI+.vbproj and GDI+ Screen Saver.vbproj. The second one is the screen-saver project, while the first is a small installation program to install your screen saver once you’ve completed it. The procedures we’ll describe next are in the screen-saver project, except for those in the “Deploying the Screen Saver” section. Figure 10-5 shows the screen saver form during development, small and sizeable.

Figure 10-5: While you’re developing your screen saver, keep the form small and sizeable so that you can test and debug it easily.

Application Entry Point

Sub Main is the entry point into our application, the first procedure that executes when the screen-saver program is run. The STAThread attribute means this application will run in a single-threaded apartment, and it’s needed for COM interoperability reasons. Windows will pass parameters to this program whenever a user is setting up the screen saver using the Display Properties | Screen Saver property screen. The parameters will be passed in an array named args, and Windows will pass a /p, /c, or /s argument, depending on how the screen saver should behave. We’ll explain each argument in the following paragraphs.

First we need to determine whether an argument was passed and, if so, which one. A /p argument means we’re being asked to show a preview of the screen saver. We haven’t implemented the preview functionality here because it involves creating and joining threads and is beyond the scope of this sample, so we’ll simply exit the application.

SharedSubMain(ByValargsAsString()) Ifargs.Length>0Then Ifargs(0).ToLower= "/p" Then Application.Exit() EndIf

If the screen saver should offer a form for user options, Windows passes a /c, so we’ll create and display a frmOptions form, and then exit when the form is closed. If the screen saver should simply execute normally, Windows passes a /s, so we’ll create and display a screenSaverForm and then exit when the form is closed.

Ifargs(0).ToLower.Trim().Substring(0,2)= "/c" Then DimuserOptionsFormAsNewfrmOptions() userOptionsForm.ShowDialog() Application.Exit() EndIf Ifargs(0).ToLower= "/s" Then DimscreenSaverFormAsNewfrmSceenSaver() screenSaverForm.ShowDialog() Application.Exit() EndIf

If there are no arguments, we’ll simply execute the screen saver normally, because it means that the user double-clicked the .scr or .exe file or ran it from within Visual Studio. We know this because otherwise Windows would have passed a parameter to the application.

Else DimscreenSaverFormAsNewfrmSceenSaver() screenSaverForm.ShowDialog() Application.Exit() EndIf EndSub

Normal Operation

When the screen-saver form is loaded, we initialize it by creating the Graphics object we’ll use for drawing, loading the user’s saved options (creating an options file if one does not exist), setting the speed based on the user-defined options, and enabling the timer. The speed setting dictates how quickly shapes will be displayed when the screen saver is active.

PrivateSubfrmSceenSaver_Load(... m_Graphics=Me.CreateGraphics() m_Options.LoadOptions() SelectCasem_Options.Speed Case "Slow" Me.tmrUpdateScreen.Interval=500 Case "Fast" Me.tmrUpdateScreen.Interval=100 CaseElse Me.tmrUpdateScreen.Interval=200 EndSelect Me.tmrUpdateScreen.Enabled=True EndSub

All subsequent operations are in response to events. When the timer ticks, a new shape is drawn on the screen, and this continues until a mouse button is clicked or the mouse is moved over the form.

PrivateSubtmrUpdateScreen_Tick(... DrawShape() EndSub

The DrawShape subroutine draws a randomly colored, randomly sized shape to the screen, based on some user-defined parameters. It starts by computing the largest possible values for the screen, as indicated by the form width and height. (Remember that the form is maximized.) Note that x1, x2, y1, and y2 are coordinates for random points to be generated later, myRect is the rectangle within which the shapes will be drawn, and myColor is the color to be used to draw the shapes.

PrivateSubDrawShape() DimmaxXAsInteger=Me.Width DimmaxYAsInteger=Me.Height Dimx1,x2,y1,y2AsInteger DimmyRectAsRectangle DimmyColorAsColor

Next we generate some random numbers ranging between zero and the maximums we determined earlier, and we create a rectangle based on the generated coordinates.

x1=m_Random.Next(0,maxX) x2=m_Random.Next(0,maxX) y1=m_Random.Next(0,maxY) y2=m_Random.Next(0,maxY) myRect=NewRectangle(Math.Min(x1,x2),Math.Min(y1,y2),_ Math.Abs(x1-x2),Math.Abs(y1-y2))

We’ll select a color at random for the shape we’re about to draw. If the user wants transparent shapes, we’ll allow the transparency to be randomly generated as well. If not, we’ll set the Alpha to 255 (the maximum). Alpha, which is the first argument to the Color.FromArgb function, determines how opaque the shape will be.

Ifm_Options.IsTransparentThen myColor=Color.FromArgb(m_Random.Next(255),m_Random.Ne xt(255),_ m_Random.Next(255),m_Random.Next(255)) Else myColor=Color.FromArgb(255,m_Random.Next(255),_ m_Random.Next(255),m_Random.Next(255)) EndIf

Finally, we draw an ellipse or rectangle based on user-defined options.

Ifm_Options.Shape= "Ellipses" Then m_Graphics.FillEllipse(NewSolidBrush(myColor),myRect) Else m_Graphics.FillRectangle(NewSolidBrush(myColor),myRect) EndIf EndSub

Ending the Application

When the user clicks a button or moves the mouse, we want the screen saver to quit. The following routines accomplish that. The first one simply exits if any mouse button is clicked on the screen saver form.

PrivateSubfrmSceenSaver_MouseDown(... Application.Exit() EndSub

The second one responds to a mouse movement. Because the MouseMove event can sometimes be fired by very trivial moves of the mouse, we’ll verify that the mouse has actually been moved by at least a few pixels before exiting. To do that, we store the current location of the mouse, turn on a switch that shows we’re tracking the mouse movements, and exit only if the mouse has been moved at least 10 pixels.

PrivateSubfrmSceenSaver_MouseMove(... IfNotm_IsActiveThen Me.m_MouseLocation=NewPoint(e.X,e.Y) m_IsActive=True Else IfMath.Abs(e.X-Me.m_MouseLocation.X)>10Or_ Math.Abs(e.Y-Me.m_MouseLocation.Y)>10Then Application.Exit() EndIf EndIf EndSub

Setting Options

We want the user to be able to choose screen-saver options such as how fast shapes should be drawn on the screen, whether they should be rectangles or ellipses, and whether the shapes should be transparent. So we provide an Options form, which gets called when the screen-saver program is invoked with the /c argument.

We’ve defined a class named Options (which you can see in Options.vb) to hold the preferences as properties and provide methods for loading and saving the preferences. Creating a class makes it easy to save the options to disk in XML format by serializing the class and then later retrieving them by deserialization. (See “Application #82: Serialize Objects” for more details.)

The btnOK_Click procedure in frmOptions creates an Options object and sets its values to the user-selected values on the form. It then saves the choices to disk.

PrivateSubbtnOK_Click(... DimmyOptionsAsNewOptions() IfMe.optEllipses.CheckedThen myOptions.Shape= "Ellipses" Else myOptions.Shape= "Rectangles" EndIf myOptions.IsTransparent=Me.chkTransparent.Checked myOptions.Speed=Me.cboSpeed.Text myOptions.SaveOptions() Me.Close() EndSub

The Form_Load event procedure of frmOptions loads the current user- defined options and sets the controls on the form accordingly. The Load method of the Options class always returns values, even if the options file doesn’t currently exist.

PrivateSubfrmOptions_Load(... DimmyOptionsAsNewOptions() myOptions.LoadOptions() Me.cboSpeed.Text=myOptions.Speed Me.chkTransparent.Checked=myOptions.IsTransparent IfmyOptions.Shape= "Ellipses" Then Me.optEllipses.Checked=True Else Me.optRectangles.Checked=True EndIf EndSub

Deploying the Screen Saver

Screen savers live in the Windows System directory, which by default is C:WindowsSystem32. To install your new screen saver, you simply have to copy the .exe application file to the System directory, changing the extension from .exe to .scr. In the sample solution, we’ve provided a project that copies the file to its correct location. It assumes you have a copy of “101 VB.NET Sample Applications Screensaver.scr” in the root directory of the 89 Create a Screensaver with GDI+ project.

If you don’t, copy “101 VB.NET Sample Applications Screensaver.exe” from GDI+ Screen Saverin to the root directory of the 89 Create a Screensaver with GDI+ project and change the extension to .scr.

Once you’re sure the screen-saver file is in place, the btnInstall_Click procedure of frmMain handles the installation for you. Notice the use of Environment.CurrentDirectory (the folder where the executable is running) and Environment.SystemDirectory (the Windows System directory).

PrivateSubbtnInstall_Click(... DimfileNameAsString=_ "101VB.NETSampleApplicationsScreensaver.scr" DimsourceFileAsString=_ Environment.CurrentDirectory& ".." &fileName DimdestFileAsString=Environment.SystemDirectory& "" &fileName Try File.Copy(sourceFile,destFile,True) CatchexAsException MsgBox(ex.ToString(),MsgBoxStyle.Exclamation,Me.Text) EndTry EndSub

Conclusion

In this sample application, you’ve seen that a screen saver is simply a Windows Application in disguise. Once you change its file extension from .exe to .scr and put it in the Windows System directory, it will show up on the list of screen savers in Control Panel | Display. You’ve seen that Windows passes /p, /c, or /s arguments to the screen saver to indicate how it should behave. They indicate Preview, Set Options, and Normal, respectively. We’ve also shown you that generating random colors and even random levels of transparency is easy with the Random class and the FromArgb function of the Color class.

Application #90 Animation

This application demonstrates how to do animation with GDI+, including classic frame animation, drawing and moving a shape on the screen, and animating text with a gradient fill. The sample form offers three animations: a winking eye, a bouncing ball, and a text animation. (The text animation is shown in Figure 10-6.)

Building Upon…

Application #85: Work with GDI+ Pens

Application #86: Work with GDI+ Brushes

Application #88: Work with GDI+ to Manipulate Images

Figure 10-6: Creating an animated gradient in text or any other shape is easy—you simply change its point of origin in a loop.

New Concepts

Once you know how to manipulate images with the Framework GID-related classes, you’ll probably want to make some of them come alive with animation. With a few carefully chosen methods and some basic mathematical skills, you can bring an image to life. We’ll demonstrate the principles in the code walkthrough.

Code Walkthrough

The animations shown here require a timer whose TimerOnTick procedure triggers the image’s movement. Before starting the motion, however, some initial preparation is required, including clearing existing drawings when switching from one animation to another, computing the size of the bouncing ball, and so on.

Setting Up

The OnResize procedure fills the bill in handling the setup chores. This method overrides the OnResize method in the base Control class. OnResize raises the Resize event, which occurs when the control (in this case, the Form) is resized. That means it will be called when the form is loaded because the Resize event fires as a part of that process. We will also call this procedure whenever a radio button is clicked to change animations.

First, if Wink is chosen, we’ll simply clear the form.

  Tip

To clear the form, you could also use grfx.Clear(Me.BackColor) or Me.Invalidate().

ProtectedOverridesSubOnResize(ByValeaAsEventArgs) IfoptWink.CheckedThen DimgrfxAsGraphics=CreateGraphics() Me.Refresh() grfx.Dispose()

If the user selects the Bouncing Ball, we have much more to do. First we erase any existing drawings. Next, we determine the size of the ball by setting the radius of the ball to a fraction of either the width or height of the client area, whichever is less. Then we set the width and height of the ball by multiplying the radius we calculated earlier by the horizontal and vertical resolution of the Graphics object.

ElseIfoptBall.CheckedThen DimgrfxAsGraphics=CreateGraphics() grfx.Clear(Me.BackColor) DimdblRadiusAsDouble=Math.Min(ClientSize.Width/ grfx.DpiX,_ ClientSize.Height/grfx.DpiY)/intBallSize intBallRadiusX=CInt(dblRadius*grfx.DpiX) intBallRadiusY=CInt(dblRadius*grfx.DpiY) grfx.Dispose()

Now we’ll set the distance the ball moves to 1 pixel or a fraction of the ball’s size, whichever is greater. This means that the distance the ball moves each time it is drawn is proportional to its size, which is, in turn, proportional to the size of the client area. Thus, when the client area is shrunk, the ball slows down, and when it is increased, the ball speeds up—resulting in an apparent constant speed no matter what size the form is set to.

intBallMoveX=CInt(Math.Max(1,intBallRadiusX/ intMoveSize)) intBallMoveY=CInt(Math.Max(1,intBallRadiusY/ intMoveSize))

Notice that the value of the ball’s movement also serves as the margin around the ball, which determines the size of the actual bitmap on which the ball is drawn. So the distance the ball moves is exactly equal to the size of the bitmap, which permits the previous image of the ball to be erased before the next image is drawn—all without an inordinate amount of flickering. We determine the actual size of the Bitmap on which the ball is drawn by adding the margins to the ball’s dimensions.

intBitmapWidthMargin=intBallMoveX intBitmapHeightMargin=intBallMoveY intBallBitmapWidth=2*(intBallRadiusX+intBitmapWidthMar gin) intBallBitmapHeight=2*(intBallRadiusY+intBitmapHeightM argin)

Now that we have the size of the ball, we create a new bitmap, passing in the dimensions we just calculated. Then we obtain the Graphics object exposed by the Bitmap, clear the existing ball, and draw the new ball. Finally, we reset the ball’s position to the center of the client area.

myBitmap=NewBitmap(intBallBitmapWidth,intBallBitmapHeigh t) grfx=Graphics.FromImage(myBitmap) Withgrfx .Clear(Me.BackColor) .FillEllipse(Brushes.Red,NewRectangle(intBallMoveX,_ intBallMoveY,2*intBallRadiusX,2*intBallRadiusY )) .Dispose() EndWith intBallPositionX=CInt(ClientSize.Width/2) intBallPositionY=CInt(ClientSize.Height/2)

In the final choice in the If statement, we simply clear the form if the user selects the Animated Text option.

ElseIfoptText.CheckedThen DimgrfxAsGraphics=CreateGraphics() grfx.Clear(Me.BackColor) EndIf EndSub

Frame Animation

Now that the setup is done, the bulk of the work is passed to the following TimerOnTick procedure, which handles the Tick event for the Timer. This is where the animation takes place, so it’s the heart of our application.

The first option handles the winking eye. To create the illusion of winking, we’ll display a sequence of four images stored in an array, each one showing a different stage of the wink. We draw each image with the DrawImage method of a Graphics object, using overload #8, which takes the image to be displayed, the x- and y- coordinates (which in this case will center the image in the client area), and the width and height of the image. Note intCurrentImage, a class-level counter that begins at 1 and determines which of the four images in the array will be displayed as the animation progresses.

ProtectedOverridableSubTimerOnTick(... IfoptWink.CheckedThen DimgrfxAsGraphics=CreateGraphics() DimimgAsImage=arrImages(intCurrentImage) grfx.DrawImage(img,CInt((ClientSize.Width-img.Width)/ 2),_ CInt((ClientSize.Height-img.Height)/2),img.Width,_ img.Height)

Each time the timer ticks, we bump up the image counter intImageIncrement, which is a class-level variable that lets us control the animation order. When we get to the last image of the four, we reverse the order so that the eye closes, and when we get back to the first image, we reverse the animation order again so that the eye re- opens.

intCurrentImage+=intImageIncrement IfintCurrentImage=3Then intImageIncrement=-1 ElseIfintCurrentImage=0Then intImageIncrement=1 EndIf

Bouncing Ball

The next option enables the bouncing ball. First we create a Graphics object, and then we use it to draw the bitmap containing the ball on the Form. Then we increment the ball’s position by the distance it has moved in both the x and y directions after being redrawn.

ElseIfoptBall.CheckedThen DimgrfxAsGraphics=CreateGraphics() grfx.DrawImage(myBitmap,_ CInt(intBallPositionX-intBallBitmapWidth/2),_ CInt(intBallPositionY-intBallBitmapHeight/2),_ intBallBitmapWidth,intBallBitmapHeight) grfx.Dispose() intBallPositionX+=intBallMoveX intBallPositionY+=intBallMoveY

  Tip

You should always call Dispose for objects that expose this method instead of waiting for the Garbage Collector to do it for you. This almost always increases your application’s performance.

When the ball hits a boundary, we want to reverse its direction. So when its x- axis position puts it beyond the width of the form, or less than zero, we invert intBallMoveX, which controls its direction. We do the same for the y-axis (inverting intBallMoveY), but we set the upper boundary at 40 instead of zero so that the ball doesn’t bounce into the controls at the top of the form.

IfintBallPositionX+intBallRadiusX>=ClientSize.Width_ OrintBallPositionX-intBallRadiusX<=0Then intBallMoveX=-intBallMoveX Beep() EndIf IfintBallPositionY+intBallRadiusY>=ClientSize.Height_ OrintBallPositionY-intBallRadiusY<=40Then intBallMoveY=-intBallMoveY Beep() EndIf

Animated Text

For the final option, animating a gradient with text, we begin by setting the font type and the text to be displayed, and we determine the size of the text with the MeasureString method. Keep in mind that MeasureString returns its results in a SizeF structure, which has Width and Height properties. Using those properties, we calculate the x- and y-coordinates for the starting point of our display, which in this case is the center of the client area.

ElseIfoptText.CheckedThen DimgrfxAsGraphics=CreateGraphics() DimfontAsNewfont("MicrosoftSansSerif",96,_ FontStyle.Bold,GraphicsUnit.Point) DimstrTextAsString= "GDI+!" DimsizfTextAsNewSizeF(grfx.MeasureString(strText,font) ) DimptfTextStartAsNewPointF(_ CSng(ClientSize.Width-sizfText.Width)/2,_ CSng(ClientSize.Height-sizfText.Height)/2)

We’ll set the start point of the gradient to the upper left of the text’s boundary rectangle. We’ll set the end point’s x coordinate to a changing value so that we can achieve the animation effect. Once we’ve instantiated the LinearGradientBrush, we draw the text using Blue for the gradient’s starting color and use the form’s background color for the ending color.

DimptfGradientStartAsNewPointF(0,0) DimptfGradientEndAsNewPointF(intCurrentGradientShift,20 0) DimgrBrushAsNewLinearGradientBrush(ptfGradientStar t,_ ptfGradientEnd,Color.Blue,Me.BackColor) grfx.DrawString(strText,font,grBrush,ptfTextStart) grfx.Dispose()

Now we’ll animate the gradient by moving its starting point based on the value in intCurrentGradientShift, which is incremented or decremented by intGradientStep each time this procedure gets called. Once intCurrentGradientShift gets to 500, we reverse its direction.

intCurrentGradientShift+=intGradientStep IfintCurrentGradientShift=500Then intGradientStep=-5 ElseIfintCurrentGradientShift=-50Then intGradientStep=5 EndIf EndIf EndSub

Conclusion

In this sample application, you’ve seen that you can achieve image animation just by drawing a sequence of images to the screen. You’ve also seen that you might sometimes need more complex computations when you need to precisely place an image in a series of progressive locations, as with our bouncing ball example. We showed you that to clear a form, you can use Graphics.Clear(Me.BackColor), Me.Refresh(), or Me.Invalidate(). And you saw that creating an animated gradient is easy. You simply change its origin in a loop.

Категории