Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual Basic 2005 (2006)

.pdf
Скачиваний:
226
Добавлен:
17.08.2013
Размер:
14.97 Mб
Скачать

Chapter 14

6.Finally, select (PaintCanvas Events) in the Class Name combo box and the Paint event in the Method Name combo box. Add the following highlighted code:

Private Sub PaintCanvas_Paint(ByVal sender As Object, _

ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint

‘Go through the list

For Each objGraphicsItem As GraphicsItem In GraphicsItems ‘Ask each item to draw itself objGraphicsItem.Draw(e.Graphics)

Next End Sub

7.Run the project and draw on the control by holding down the left mouse button and dragging the mouse over the surface.

You now have a working paint program, but you’ll notice that the more you paint the more it flickers. This illustrates an important aspect of drawing, as you’ll see when you fix it. For now, look at what you’ve done.

How It Works

When the user moves the mouse over the control, an event called MouseMove is fired. You have hooked into this event by adding the event handler for the MouseMove event. When this event handler is fired, you check to see whether the left mouse button is down, and if it is, you pass the System.Windows.

Forms.MouseEventArgs object that you were given over to your private DoMousePaint method.

Private Sub PaintCanvas_MouseMove(ByVal sender As Object, _

ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove

‘Is the left mouse button down?

If e.Button = MouseButtons.Left Then DoMousePaint(e)

End If End Sub

DoMousePaint is the method that you’ll use to handle the drawing process. In this case, whenever the MouseMove event is received, you want to create a new GraphicsCircle item and add it to the list of vectors that make up your image.

As DoMousePaint will ultimately do more than add circles to the vector list, you need to do things in a (seemingly) counterintuitive order. The first thing you need is to declare an object to hold the new GraphicsItem class that will be created — so declare objGraphicsItem:

Private Sub DoMousePaint(ByVal e As MouseEventArgs) ‘Store the new item somewhere

Dim objGraphicsItem As GraphicsItem

Then you look at your GraphicTool property to determine what you’re supposed to be drawing. At this point, because you only have one tool defined, this will always be a circle:

‘What tool are you using? Select Case GraphicTool

436

Programming Custom Graphics

‘CirclePen

Case GraphicTools.CirclePen

‘Create a new graphics circle

Dim objGraphicsCircle As New GraphicsCircle()

After you have the GraphicsCircle, you call the SetPoint method, which, if you recall, was defined on GraphicsItem. This method is responsible for determining the point on the canvas where the item should appear. You give SetPoint the current drawing size and color, and tell it to draw a filled shape.

‘Set the point for drawing objGraphicsCircle.SetPoint(e.X, e.Y, GraphicSize, _

GraphicColor, True)

After you have called SetPoint, you store the GraphicsCircle in objGraphicsItem and close the

Select . . . End Select statement.

‘Store this for addition objGraphicsItem = objGraphicsCircle

End Select

If a new GraphicsItem was stored in objGraphicsItem, you have to add it to the list.

‘Were you given an item?

If objGraphicsItem IsNot Nothing Then

‘Add it to the list

GraphicsItems.Add(objGraphicsItem)

Finally, you have to invalidate the control. You have to do this to tell Windows that something about the appearance of the control has changed. The program will not tell the control to paint itself unless something has told Windows that the control needs painting. Calling Me.Invalidate in this way tells Windows that the appearance of the control is “invalid” and therefore needs updating.

‘Invalidate the control Me.Invalidate()

End If

Although you can invalidate the control with the Invalidate method, the control will be invalidated whenever Windows detects it needs redrawing. This may happen when the window is restored after being minimized; another window obscures an area that’s been made visible, and so on.

That covers everything from the user dragging the mouse over the control to adding a new GraphicsCircle item to the list. Now what?

With the control marked as requiring painting, it’s up to Windows to choose a time for the window to be painted. To increase the performance of the windowing subsystem, windows are drawn only when the system has enough “spare time” to do it. Painting is not considered to be a crucial task to the operating

437

Chapter 14

system. You cannot rely on painting being done immediately, or within a given time-span of your marking something as invalid. At some point, the control will be asked to paint itself. You may have noticed this effect when your computer is being used heavily — an image on the screen will appear to “freeze” for a period before the display is updated.

Do not try to force Windows to paint when it doesn’t want to. There are thousands of lines of optimization code in the Windows operating system to make sure that things are painted at absolutely the best time. Invalidate your control when you need to flag something as needing to be redrawn, and let nature take its course.

When it is ready, the Paint event will be called. You tap into this event by adding an event handler for the Paint event. All that you have to do is loop through the entire array of GraphicsItem objects that you’ve collected in GraphicsItems and ask each one to draw itself.

Private Sub PaintCanvas_Paint(ByVal sender As Object, _

ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint

‘Go through the list

For Each objGraphicsItem As GraphicsItem In GraphicsItems ‘Ask each item to draw itself objGraphicsItem.Draw(e.Graphics)

Next End Sub

The Paint event passes through its parameters as a PaintEventArgs object. This object, among other things, contains a property called Graphics. This property returns a System.Drawing.Graphics object.

When you have hold of a graphics object, you are able to draw to the control, the form, the printer, or whatever it is that’s given you as an object. This object contains a bundle of methods and properties that are actually used for painting. To keep in line with the principle of “only painting when needed,” in typical day-to-day work you shouldn’t try to create or otherwise obtain one of these objects. If you’re given one, then it’s time to paint!

Now that you know how the painting works, let’s see whether you can get rid of the flickering!

Invalidation

This example you have been working on is designed to flicker and slow down to illustrate an important consideration that you need to bear in mind when drawing controls: Do the least amount of work possible! Drawing to the screen is slow. The less you draw, the faster the performance of your application should be and the better it should look on the screen.

The control flickers because painting is a two-stage process. Before you’re asked to paint, Windows automatically erases the region behind the area that needs to be painted. This means the whole control flashes white as everything is erased and then you fill in the details.

What you want to do is to invalidate only the area that contains the new GraphicsItem. When you invalidate the control, you don’t have to invalidate the whole thing. If you want, you can just invalidate a small area, as you do in the next Try It Out.

438

Programming Custom Graphics

Try It Out

Invalidating a Small Area

1.In the PaintCanvas class, find the DoMousePaint method. Modify the Me.Invalidate method at the end to include this parameter to invalidate just a Rectangle:

‘Invalidate the Control

Me.Invalidate(objGraphicsItem.Rectangle)

2.Run the project. You’ll notice now that when you paint it doesn’t flicker.

How It Works

After you call SetPoint on the new GraphicsCircle object, the Rectangle property is updated to contain the bounding rectangle of the circle.

This time, when you call the Me.Invalidate method, you pass this rectangle in. In this way, only a tiny area of the control is invalidated, therefore, only that tiny area is erased. After it is erased, you get the opportunity to draw your circle.

Optimized Drawing

You’ll notice that if you draw a lot on the control, after a while the edge of the line starts to become almost jagged. What you’re experiencing here is that as the GraphicsItems list grows, more calls to FillEllipse are made. Because drawing on the screen is slow, the more you have to do this, the longer the drawing process takes to aggregate. This lengthened drawing process prevents all of the MouseMove events from being fired, and so the line appears to stutter. In the following Try It Out section you see how you can avoid this problem.

Try It Out

Optimized Drawing

1.Find the PaintCanvas_Paint method on the PaintCanvas class. Add this code as highlighted:

Private Sub PaintCanvas_Paint(ByVal sender As Object, _

ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint

‘Go through the list

For Each objGraphicsItem As GraphicsItem In GraphicsItems

‘Do we need to be drawn?

If e.ClipRectangle.IntersectsWith(objGraphicsItem.Rectangle) Then ‘Ask each item to draw itself objGraphicsItem.Draw(e.Graphics)

End If

Next

End Sub

2.Run the project. You should now find that the drawing process is smoother.

439

Chapter 14

How It Works

The PaintEventArgs object contains another property called ClipRectangle. This rectangle describes the area of the control that has been invalidated and is known as the clipping rectangle. The Rectangle class contains a method called IntersectsWith that can tell whether two given rectangles overlap and returns a Boolean value indicating whether they intersect.

As you know, a rectangle describes the bounds of each of your GraphicsItem objects, so you can use this rectangle with IntersectsWith. If the GraphicsItem overlaps, it needs drawing; otherwise, you move on to the next control.

The two techniques you’ve seen here — invalidating only what changes and drawing only what falls into the invalidated region — are by far the two most important techniques you’ll come across when painting. If you skip either of these, your control has a good chance of being sluggish and flickering.

Choosing Colors

Now that you can do some basic painting, you’ll build a control that lets you choose the color that you’re painting in. Like a lot of graphics programs, you’ll build this so that you have a palette of different colors and you’re able to choose two at a time — one for the left mouse button and one for the right.

There are a number of different ways to build this control, and perhaps the most logical is to create a control that contains a bundle of Button controls, each configured so that it displays the color that it represents. However, this example shows you how to build a control completely from scratch. The techniques that you’ll learn here will be really useful if you want to roll your own controls that display a picture of something and have hot regions on them. Hot regions are regions that fire an event when you click them. What you’re doing might seem a little obscure, but it’s a great example!

Creating the ColorPalette Control and Sizing the Control

To create the color palette control in the next Try It Out, you’re going to need two classes. One, derived from UserControl and named ColorPalette, will provide the user interface (UI) for the palette itself. The other, named ColorPaletteButton, will be used to display the actual color box on the palette.

Since you are handling the layout of the buttons on the control, you need to respond to the Resize event. This event is fired whenever the user changes the size of the control. You can hook into this event by adding an event handler for the Resize event.

When Resize is fired, you need to alter the position of each of the buttons, starting in the top left corner and continuing in strips across the whole width of the control. When you’ve filled up one row, you need to start a new row.

Try It Out

Creating the ColorPalette Control

1.In the Solution Explorer, add a new class to the WroxPaint project named ColorPaletteButton.vb and add the following highlighted code to it:

Public Class ColorPaletteButton

‘Public members

Public Color As Color = System.Drawing.Color.Black Public Rectangle As Rectangle

440

Programming Custom Graphics

‘Constructor

Public Sub New(ByVal newColor As Color) Color = newColor

End Sub

‘Move the button to the given position

Public Sub SetPosition(ByVal x As Integer, ByVal y As Integer, _ ByVal buttonSize As Integer)

‘Update the members

Rectangle = New Rectangle(x, y, buttonSize, buttonSize) End Sub

‘Draw the button

Public Sub Draw(ByVal graphics As Graphics) ‘Draw the color block

Dim objSolidBrush As New SolidBrush(Color) graphics.FillRectangle(objSolidBrush, Rectangle)

‘Draw an edge around the control

Dim objPen As New Pen(System.Drawing.Color.Black) graphics.DrawRectangle(objPen, Rectangle)

End Sub End Class

2.Now add a user control to the WroxPaint project named ColorPalette. Right-click the control and choose View Code from the context menu. Add these members to the top of the class definition:

Public Class ColorPalette

‘Public members

Public Buttons As New ArrayList() Public ButtonSize As Integer = 15

Public ButtonSpacing As Integer = 5

Public LeftColor As Color = Color.Black

Public RightColor As Color = Color.White

Here is what the members will do:

Buttons holds a list of the buttons on the palette.

ButtonSize defines the size of each of the buttons on the palette.

ButtonSpacing defines the gap between each button.

LeftColor holds the current color that is assigned to the left mouse button.

RightColor holds the current color that is assigned to the right mouse button.

3.Next, add this method to the class:

‘Add a new color button to the control Public Sub AddColor(ByVal newColor As Color)

‘Create the button

Dim objColorPaletteButton As New ColorPaletteButton(newColor)

441

Chapter 14

‘Add it to the list Buttons.Add(objColorPaletteButton)

End Sub

4.When you create the control, you want a set of basic colors to be always available. Add this code for the constructor to the class. This will create ten basic colors. After you type Public Sub New and press Enter, the unhighlighted code that follows will automatically be added to the constructor. Add the following highlighted code to your constructor:

Public Sub New()

This call is required by the Windows Form Designer. InitializeComponent()

Add any initialization after the InitializeComponent() call.

‘Add the colors AddColor(Color.Black) AddColor(Color.White) AddColor(Color.Red) AddColor(Color.Blue) AddColor(Color.Green) AddColor(Color.Gray) AddColor(Color.DarkRed) AddColor(Color.DarkBlue) AddColor(Color.DarkGreen) AddColor(Color.DarkGray)

End Sub

5.In the Code Editor for the ColorPalette class, select (ColorPalette Events) in the Class Name combo box and the Resize event in the Method Name combo box. Add this highlighted code to the Resize event handler:

Private Sub ColorPalette_Resize(ByVal sender As Object, _

ByVal e As System.EventArgs) Handles Me.Resize

‘Declare variables to hold the position Dim intX As Integer

Dim intY As Integer

‘Go through the array and position the buttons

For Each objColorPaletteButton As ColorPaletteButton In Buttons

‘Position the button objColorPaletteButton.SetPosition(intX, intY, ButtonSize)

‘Move to the next one

intX += (ButtonSize + ButtonSpacing)

‘Do we need to go down to the next row If intX + ButtonSize > Width Then

‘Move y

intY += (ButtonSize + ButtonSpacing)

442

Programming Custom Graphics

‘Reset x intX = 0

End If

Next

‘Redraw

Me.Invalidate() End Sub

6.You still need to paint the control. Select (ColorPalette Events) in the Class Name combo box and the Paint event in the Method Name combo box. Add this highlighted code:

Private Sub ColorPalette_Paint(ByVal sender As Object, _

ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint

‘Loop through the buttons

For Each objColorPaletteButton As ColorPaletteButton In Buttons

‘Do we need to draw?

If e.ClipRectangle.IntersectsWith(objColorPaletteButton.Rectangle) Then objColorPaletteButton.Draw(e.Graphics)

End If

Next

End Sub

7.Before you can draw the control onto Form1, you need to build the project. Select Build Build WroxPaint from the menu.

8.After the project has been built, open the Designer for Form1. Click the PaintCanvas control on Form1 and, in the Properties window, set the Dock property to None. Now resize the form to add a little space at the bottom and make the form wider if so desired.

9.In the ToolBox under the WroxPaint Components tab, drag a ColorPalette control to the bottom of you form and set its Name property to paletteColor. Now set its Dock property to Bottom.

10.Now click the PaintCanvus control, resize it if necessary, and set its Anchor property to Top, Bottom, Left, Right. Your form should now look similar to Figure 14-3.

11.If you now try to rearrange the form a little, you should see that your sizing code has proven successful.

How It Works

Hopefully, the behavior of ColorPaletteButton shouldn’t be too much of a mystery. You have members on the class that hold the color and a rectangle, and you also provide a constructor that automatically populates the color:

Public Class ColorPaletteButton

‘Public members

Public Color As Color = System.Drawing.Color.Black

Public Rectangle As Rectangle

443

Chapter 14

‘Constructor

Public Sub New(ByVal newColor As Color) Color = newColor

End Sub

Figure 14-3

When the button is asked to paint itself, all you do is draw one filled rectangle of the color specified in the Color property using the FillRectangle method, and for neatness you surround it with a black border using the DrawRectangle method:

‘Draw the button

Public Sub Draw(ByVal graphics As Graphics) ‘Draw the color block

Dim objSolidBrush As New SolidBrush(Color) graphics.FillRectangle(objSolidBrush, Rectangle)

‘Draw an edge around the control

Dim objPen As New Pen(System.Drawing.Color.Black) graphics.DrawRectangle(objPen, Rectangle)

End Sub

When you resize the form (a subject you’ll deal with soon), you pass the top left corner of the button through to SetPosition. All this method does is update the Rectangle property:

‘Move the button to the given position

Public Sub SetPosition(ByVal x As Integer, ByVal y As Integer, _ ByVal buttonSize As Integer)

‘Update the members

Rectangle = New Rectangle(x, y, buttonSize, buttonSize) End Sub

The ColorPalette_Resize method is perhaps the most interesting method here. This is a common algorithm used whenever you need to manage the position of controls or other graphic objects. You know the size of each object (in your case it’s a combination of ButtonSize and ButtonSpacing) and

444

Programming Custom Graphics

you know the bounds of the control. All you do is start in the top left and keep moving right until you have no more space, in which case you flip down to the next row. Here is how you start — you set up a loop that iterates through all of the buttons:

Private Sub ColorPalette_Resize(ByVal sender As Object, _

ByVal e As System.EventArgs) Handles Me.Resize

‘Declare variables to hold the position Dim intX As Integer

Dim intY As Integer

‘Go through the array and position the buttons

For Each objColorPaletteButton As ColorPaletteButton In Buttons

Throughout the loop, intX and intY hold the current coordinates of the top left corner of the control. When you start, this is (0,0) or, rather, the very top left of the client area of the control. For each button, you call SetPosition, passing in the current coordinates together with the size of the button:

‘Position the button objColorPaletteButton.SetPosition(intX, intY, ButtonSize)

After each button, you move intX to the right. In addition to adjusting by the size of the button, you also add a small gap to make the control more esthetically pleasing:

‘Move to the next one

intX += (ButtonSize + ButtonSpacing)

If you detect that you don’t have enough space to fit the next control completely on the current row, you adjust intY down to the next row and reset intX back to the beginning:

‘Do we need to go down to the next row If intX + ButtonSize > Width Then

‘Move y

intY += (ButtonSize + ButtonSpacing)

‘Reset x intX = 0

End If

Next

Finally, after you’ve moved all of the buttons, you invalidate the control so that you can see the changes.

‘Redraw

Me.Invalidate() End Sub

445