Vous êtes sur la page 1sur 22

1

EDais Advanced classes tutorial

Advanced classes in VB

Written by Mike D Sutton Of EDais

Http://www.mvps.org/EDais/

EDais@mvps.org

- 31.05.2002 -

Revision 2:
- 13.03.2004 -

Http://www.mvps.org/EDais/
2
EDais Advanced classes tutorial
Introduction
Introduction

This tutorial assumes a reasonable knowledge of VB, usage of classes and their
general structure. Also a basic understanding of OO (Object orientation) terminology
and implementation would be useful but not essential. Knowledge of GDI
programming would also be helpful since the document references a number of GDI
API’s however it’s again not necessarily essential.

Http://www.mvps.org/EDais/
3
EDais Advanced classes tutorial

An introduction to OO
Chapter I

In VB (Note; VB.NET includes a full OO model) we only get given a small subset of
the wonderful world of OO, which can be extremely powerful in creating dynamic
applications. OO is typically broken down into three sections; “Encapsulation”,
“Inheritance” and “Polymorphism”, in VB we only get offered Encapsulation. Here’s
a definition of the three terms: (Note; This is in my own words so it may not be a
‘text-book’ definition and doubtlessly would be disputed by OO purists, so feel free
to consult other documentation on the subject as well as this)

Encapsulation:
This is the essence of a class and thus the part that VB supports for us since it
supports the class structure. You can most likely work out the meaning of the term
by looking at the word – encapsulate – The dictionary defines this as “Enclose in or
as in a capsule” and this is exactly what a class does. If you think of the class itself
as the “Capsule” then the data and methods contained within it are hidden from the
outside world, accessible only through what’s known as the “Public interface” which
are the public properties, methods and events exposed by the class. In essence the
internal workings and data stored within a class are known only to itself, the outside
world only gets access to what is specifically exposed.

Inheritance:
This is where the object is abstracted into a more generalised form then further
classes a built using the generalised class as a base. These further classes “Inherit”
the functionality of the base class and can also include further functionality of their
own thus extending the base class. These classes are called “Derived” classes as they
derive their functionality from their parent class.
An example of this would be the base class “Vehicle”. Inherited from that would be
the class “Car” which would support everything a vehicle does, and extra things a car
does. The class “Ferrari” would be inherited from the Car class adding more things
specific to Ferrari model cars and so on.

Polymorphism:
This is where a method from a parent class is re-defined in a class derived from it,
changing or extending its functionality.
For example, a basic Vehicle class would be created with very few parameters since it
would have to cover any number of things further down the inheritance tree, however
when going down to an inherited class, the derived classed would override the
inherited initialisation method, adding further more specific parameters dependant on
the object in question.

Whilst VB only natively supports encapsulation, we can emulate the other OO


features by programming them, which expands the possibilities of VB and gives us
greater flexibility in writing large applications. Some of these methods may be
frowned upon or considered bad practice for some reason of other but I’m providing
this document as an example of how it is possible. Also this is by no means a true
implementation of OO, nor am I claming it to be before I get any flames from
programmers of languages with a full support of OO!

Http://www.mvps.org/EDais/
4
EDais Advanced classes tutorial

One of the first times I was ever properly introduced to OOP was actually on a course
a few years ago up at Manchester (UK) University on Java programming of all things.
The course was split up into two sections, the first teaching Java syntax and one on
OO principle, the latter of which revolutionised the way I write applications - After
having worked out how to implement those features in the language that I was
familiar with! One of the examples they used to describe OO principle is that of a
Pen with derived classes “Red pen”, “Green pen” and so on. (of course this would
really only be done with a single class with a property “colour” but that’s beside the
point ;)
The pen class understands things like “Pen down”, “Pen up”, “Move pen” and so on,
the classes derived from this now already understand all of these methods, but can
also add their own functionality of changing the colour that’s being drawn.
In a similar fashion I’m going to use the example of a “Shape” that requires points to
be drawn. We’ll then derive classes from that such as “Circle” or “Rectangle” that
take the base class and add functionality to it for drawing specific shapes.

Http://www.mvps.org/EDais/
5
EDais Advanced classes tutorial

Creating the abstract base class


Chapter I I

If you’ve not already, fire up VB, start a new “Standard EXE” project and add a new
class to the project naming it “clsShape”
Ok, time to think about what is general to a shape. Every shape be it a line, circle,
triangle etc. will have a number of points associated with it that define where it is and
how big so we can generalise that into saying that a shape has ‘some’ points, so
declare some space for them. Each shape can also have a colour so we can tell them
apart (You could also add the ability for them to have variable line and/or fill styles
and colours but for simplicities sake we’ll leave these out for now).
Before we start coding, I recommend grabbing a useful little utility off my site called
the “Class property generator” or “PropGen” for short, which does the grunt work in
creating property statements as this can get very tedious especially when you’re
creating a lot of them. Simply type the variable declarations directly into the
application as you would in VB and it generates the methods for you in real time,
allowing you to simply copy and paste the full declares into VB (If you want a
property to be read-only then simply delete the Property Set/Let statement.)
Ok, regardless of if you’re using PropGen or not, you should now have something
along the lines of:

Private Type PointAPI


X As Long
Y As Long
End Type

Dim ShapePoints() As PointAPI


Dim m_NumPoints As Long
Dim m_ShapeCol As Long

Public Property Get NumPoints() As Long


NumPoints = m_NumPoints
End Property
Public Property Let NumPoints(ByVal inNew As Long)
m_NumPoints = inNew
End Property

Public Property Get ShapeCol() As Long


ShapeCol = m_ShapeCol
End Property
Public Property Let ShapeCol(ByVal inNew As Long)
m_ShapeCol = inNew
End Property

Whilst the shape has point locations, we can also give it an overall X and Y
coordinate that offset the entire shape in any direction, allowing us to clone the shape
and simply move it rather than having to move each point in turn. Rather than using
the property generator for this, I’m going to code it by hand since I want to store the
position in a single PointAPI type rather than two longs: (You can by all means use
PropGen to create the shell functions and just change the relevant parts, that’s what I
did for this tutorial)

Dim m_ShapePos As PointAPI

Http://www.mvps.org/EDais/
6
EDais Advanced classes tutorial

Public Property Get ShapePosX() As Long


ShapePosX = m_ShapePos.X
End Property
Public Property Let ShapePosX(ByVal inNew As Long)
m_ShapePos.X = inNew
End Property

Public Property Get ShapePosY() As Long


ShapePosY = m_ShapePos.Y
End Property
Public Property Let ShapePosY(ByVal inNew As Long)
m_ShapePos.Y = inNew
End Property

Any shape will be required to draw itself onto a device context so we must have a
function called Draw() which accepts a handle to a device context to draw on and
returns a Boolean to report if the drawing operation succeeded correctly or not. We
can also create the GDI objects here, select them into the DC and clean up afterwards,
the shell of the function would look like this:

Public Function Draw(ByVal inDC As Long) As Boolean


Dim hPen As Long, OldPen As Long
Dim hBrush As Long, OldBrush As Long

hPen = CreatePen(PS_SOLID, 2, m_ShapeCol)


hBrush = CreateBrush(BS_SOLID, m_ShapeCol, HS_SOLID)
OldPen = SelectObject(inDC, hPen)
OldBrush = SelectObject(inDC, hBrush)

' Ready to draw

Call DeleteObject(SelectObject(inDC, OldBrush))


Call DeleteObject(SelectObject(inDC, OldPen))
End Function

You’ll need the CreatePen(), CreateBrushIndirect(), SelectObject() and


DeleteObject() API function declarations and also the LOGBRUSH type declaration.
I’ve wrapped the CreateBrushIndirect() function to make is slightly easier to use (You
don’t have to mess around with a logical brush structure every time), here’s the
function:

Private Function CreateBrush(ByVal inStyle As Long, _


ByVal inColour As Long, ByVal inHatch As Long) As Long
Dim TempStruct As LogBrush

With TempStruct
.lbStyle = inStyle
.lbColor = inColour
.lbHatch = inHatch
End With

CreateBrush = CreateBrushIndirect(TempStruct)
End Function

You’ll notice this is declared privately and thus is encapsulation at work since the
class doesn’t expose it yet can still use it’s functionality.

Http://www.mvps.org/EDais/
7
EDais Advanced classes tutorial

You’ll also notice that I’ve not put any drawing code into the class, instead put a
comment where the drawing code would go. This is because this class is not
responsible for handling the drawing since that varies for each shape; we’ll cover
exactly how that’s done in the next chapter.

We’ll now need a method to add points to the class since otherwise we’ll never have
anything to draw. This can be written as an ‘intelligent’ function, since the
NumPoints variable is exposed in the public interface the number of points (And thus
the size of the internal point array) can be set via this. If however we try to add a
point beyond the current size of the array we can both add a point to the array and
increment NumPoints. First up, we’ll need to make sure setting NumPoints properly
re-declares the array and that it can’t be set to a silly value:

Public Property Let NumPoints(ByVal inNew As Long)


m_NumPoints = IIf(inNew < 0, 0, inNew)
ReDim Preserve ShapePoints(IIf(m_NumPoints, m_NumPoints – 1, 0)) As PointAPI
End Property

This uses two ‘immediate-if’ statements that deal with the logic, the first simply
checks to see if the value is a negative and if so assigns the value of 0 instead (A
shape with a negative number of points is impossible). The second checks on the
value to re-declare the array to (Since if you try and declare an array with 0 points (-
1) then VB will throw and error – ReDim(0) actually declares an array with 1 point).
Now to code the AddPoint() function which will take two Long parameters for the X
and Y coordinates and return the index of the point it added (Just in case anyone ever
wanted it.) At this point we need to add another variable to the class which will hold
the number of points currently assigned in the array, otherwise we’d never know how
many have already been set and thus which one in the array to set, this is global to the
class so put it at the top beneath where the point list is declared (I prefer to keep
public and non-public variables separate):

Dim LastPoint As Long

Now the AddPoint() function will need to check to see if there are enough points
declared in the internal array before attempting to assign it and if not, it will need to
add one. At this point you could put in another ReDim() statement however this
would be duplicating the code in the Property Let statement we just re-wrote and
wherever possible you should try and re-use code as this allows for easy maintenance
and rapid development. There’s a sneaky way of re-using this code since we can talk
to the Property Let statement rather than using the variable it’s mapped to directly,
here’s the code:

If (LastPoint = m_NumPoints) Then Me.NumPoints = m_NumPoints + 1

You’ll see here that rather than incrementing the m_NumPoints variable and re-
declaring the array, all we have to do is use the Property Let statement of the class
(Me.NumPoints), which does it for us - Nice!
The rest of the function is dead easy, so here it is:

Public Function AddPoint(ByVal inX As Long, ByVal inY As Long) As Long


If (LastPoint = m_NumPoints) Then Me.NumPoints = m_NumPoints + 1

Http://www.mvps.org/EDais/
8
EDais Advanced classes tutorial

With ShapePoints(LastPoint)
.X = inX
.Y = inY
End With

LastPoint = LastPoint + 1
AddPoint = LastPoint ' Return index
End Function

We’ll need to cope with the case where we have a number of points declared but then
NumPoints gets set to a lower number, thus making LastPoint refer to a point out of
the bounds of the array, simply add this line to the end of the NumPoints Property Let
method:

If (LastPoint > inNew) Then LastPoint = inNew

This allows us to put point data into the class, but we’ll also need to extract it when
drawing so I’ve created a subroutine that returns the X and Y coordinates of any
given point:

Public Function GetPoint(ByVal inIndex As Long, _


ByRef outX As Long, ByRef outY As Long) As Boolean
If ((inIndex < 1) Or (inIndex > LastPoint)) Then Exit Function
outX = ShapePoints(inIndex).X
outY = ShapePoints(inIndex).Y
GetPoint = True
End Function

They also perform some basic checking to make sure the requested point isn’t out of
bounds, not the fastest method but it’ll do fine for now.

As far as the base class goes, this is about all we need. In the next chapter we’ll go
about creating a class derived from this one and getting the two to communicate.
Here’s a visual representation of what we’ve just created:

You’ll see I’ve segregated the internal information into three sections; Private data &
methods, Member variables and Public methods. The private data & methods never
get exposed since these are encapsulated within the class. The rest gets exposed as
needed through the public interface, which you can see on the right, represented by
the two thick arrows for the properties and methods of the class respectively.

Http://www.mvps.org/EDais/
9
EDais Advanced classes tutorial

Creating the derived class


Chapter I I I

In the last chapter you learned how to create the base shape class, which is pretty
much useless on it’s own... This is generally the case for classes high up in the
inheritance tree since they must be required to handle all kinds of different situations
and thus can’t do very much in the way of specific tasks. We’ll now write a useful
class that extends on the functionality of the base class.

Add a new class to the project and call it “clsRectangle”. In other languages you can
create a class through code and tell it which class to inherit from, however in VB
every class is created ‘dumb’ of the existence of any other classes. As such we must
explicitly tell it that a class exists of the type we want to inherit from and keep a
pointer to it throughout the lifetime of the class. We’ll thus also need to create the
inherited class on the initialise event of the derived class and de-reference it on the
terminate event:

Dim BaseClass As clsShape

Private Sub Class_Initialize()


Set BaseClass = New clsShape
End Sub

Private Sub Class_Terminate()


Set BaseClass = Nothing
End Sub

Now, to inherit functionality from that base class we can create what I call ‘pass-
through’ methods, which expose the functionality of the base class through the
inherited class. This also allows you to expose only as much of the underlying
interface as you want to. An example of a pass through method would be:

Public Property Get ShapePosX() As Long


ShapePosX = BaseClass.ShapePosX
End Property
Public Property Let ShapePosX(ByVal inNew As Long)
BaseClass.ShapePosX = inNew
End Property

As you can see, this looks like a basic property Get/Let statement, but it passes it’s
values through to the base class, which does the validation (If any) and storage
internally.
Here are the other two pass-through properties of the base class to save you typing
them out, add all three to the new class:

Public Property Get ShapePosY() As Long


ShapePosY = BaseClass.ShapePosY
End Property
Public Property Let ShapePosY(ByVal inNew As Long)
BaseClass.ShapePosY = inNew
End Property

Public Property Get ShapeCol() As Long


ShapeCol = BaseClass.ShapeCol

Http://www.mvps.org/EDais/
10
EDais Advanced classes tutorial

End Property
Public Property Let ShapeCol(ByVal inNew As Long)
BaseClass.ShapeCol = inNew
End Property

Notice that we don’t expose the NumPoints property (And similarly the AddPoint() or
GetPoint() functions) since a rectangle will always have 2 points (Top left and bottom
right).
In the shape class there was no creation method since there wasn’t really anything we
could do to initialise the shape, however with a rectangle we can assign a position,
width and height so add a Create() method now:

Public Sub Create(ByVal inX As Long, ByVal inY As Long, _


ByVal inWidth As Long, ByVal inHeight As Long)

With BaseClass
.ShapePosX = inX
.ShapePosY = inY
Call .AddPoint(0, 0)
Call .AddPoint(inWidth, inHeight)
End With
End Sub

Note: This “Create” method is designed to emulate what’s known as a constructor


function in OO terminology, which simply initialises the object in a single line.
This simply set’s the X and Y coordinates of the shape, and then adds the two points
(The first point is created at 0, 0 since it’s offset by the X and Y properties when
drawn.)
Now, we come to implement the Draw method and we come across a problem.
Whilst we can propagate down the inheritance tree through pass-through methods,
going the other way is a little more complex. Thankfully though there is a solution –
Events as depicted in the following illustration:

Events, like methods, are called asynchronously meaning that code execution stops in
the method that raised the event until all the code in event handler for the event has
completed. In this respect we can add an event to the base class to let the inherited
class know that it’s set up the drawing surface and it’s ready to go. Using this same
interface we can pass the result of the drawing operation back down through the event
to return its success or failure via a parameter passed by reference.
In case you’re at all confused about the difference between passing a parameter by
value (ByVal) or by reference (ByRef) then I’ll explain them here since it’s important

Http://www.mvps.org/EDais/
11
EDais Advanced classes tutorial

to understand how this technique works, if you already understand it then skip the
next paragraph.
In VB we’re hidden from the complexities of memory and pointers since it’s all dealt
with for us behind the scenes, however sometimes we need to take a little more
control to tweak the way things work. When we create a variable what happens is we
ask VB which in turn asks the operating system to allocate it some memory to store it
in. VB gets the address to this piece of memory in the form of a “pointer” to it, which
is nothing more complicated than just a big number – Think of it in the same way as
you would your house address but in the computers case there’s a few billion houses
on ‘Memory street’! Going back to the house analogy again, if someone wanted to
know where you lived you could either tell them the address or walk them to the door
and show them the house in person, this effectively the difference between ByRef and
ByVal. When we pass a parameter by reference we actually pass a pointer to that
data in memory, telling VB where to go to get to the actual value of the variable.
When a parameter is passed by value we simply pass the value of the variable and as
such there is no link back to the original. This is the important difference between the
two methods – A variable passed by reference to a method can be changed by that
method, one passed by value cannot. By default parameters are passed by reference
in VB6 (Note: In VB.NET the default was changed to by value) but in most cases it
makes sense to pass parameters by value unless they explicitly need to be changed
within the method.
Add this function declaration to the base class:

Public Event DrawNow(ByVal inDC As Long, ByRef outResult As Boolean)

Note that we’re passing the outResult parameter by reference so we’re actually
passing a pointer to the variable rather than the value itself, this is very important as it
allows us to data both ways, in this case we can pass the DC into the event and the
result can be passed back.
In the Draw() event of the base class, I added a comment to show where the drawing
code would go. Raise the event there, passing the inDC parameter as the first
parameter and the function name (Return value) as the second parameter:

RaiseEvent DrawNow(inDC, Draw)

It may seem a bit odd to pass the function name as the second parameter, but here’s
what’s happening:

• Rectangle class gets told to Draw() onto a DC.


• Rectangle class tells base class to Draw(), passing DC handle as parameter.
• Base class set’s up the drawing DC and raises the DrawNow() event telling
the inherited class it’s ready to draw, passing back the DC handle and a
pointer to it’s return value.
• Inherited class responds to event and draws onto the DC, passing the result
down through the event to the base class.
• Base class cleans up DC and quits, returning drawing result to inherited class.
• Inherited class passes drawing result through to whatever called it in the first
place.

Http://www.mvps.org/EDais/
12
EDais Advanced classes tutorial

Wow, complicated... Don’t worry; it’s not too bad once you’ve done it a few times.
We’ll need to tell the inherited class to respond to the base classes event now, so
we’ll need to add “WithEvents” to its declaration:

Dim WithEvents BaseClass As clsShape

Now you’ll find the BaseClass on the Object dropdown with it’s DrawNow event
exposed and ready to respond to, so we’ll add the drawing code there. Of course
you’ll need the Rectange() API declaration for this so copy that into the top of the
rectangle class, and add a call to it, passing the offset shape points:

Private Sub BaseClass_DrawNow(ByVal inDC As Long, ByRef outResult As Boolean)


Dim BasePts(3) As Long

With BaseClass ' Grab the shape points


Call .GetPoint(0, BasePts(0), BasePts(1))
Call .GetPoint(1, BasePts(2), BasePts(3))

outResult = Rectangle(inDC, _
BasePts(0) + .ShapePosX, BasePts(1) + .ShapePosY, _
BasePts(2) + .ShapePosX, BasePts(3) + .ShapePosY) <> 0
End With
End Sub

Finally pass the Draw() method through as usual to base class so it can set the DC up
for us:

Public Function Draw(ByVal inDC As Long) As Boolean


Draw = BaseClass.Draw(inDC)
End Function

That’s all there is to it. Let’s create a simple project to test this out, add a picture box
to the form, set it’s AutoRedraw property to True then paste this code into the form
and run:

Private Sub Form_Load()


Dim MyClass As New clsRectangle

Form1.AutoRedraw = True

With MyClass
Call .Create(10, 20, 30, 40)
.ShapeCol = vbRed
Call .Draw(Form1.hDC)
End With

Call Form1.Refresh

Set MyClass = Nothing


End Sub

Hopefully you could see a red rectangle being drawn, if not then check back through
the code to make sure you’ve not missed something and if all else fails, grab my copy
of the code and see what’s different.

Http://www.mvps.org/EDais/
13
EDais Advanced classes tutorial

Writing more derived classes


Chapter I V

Rather than going on to more new things, I’m going to go over the last chapter again,
in creating a second shape derived from the base class, this time a circle class. If
you’re satisfied that you understand the principle then just grab my code for the class
and move onto the next chapter since nothing new will be covered here.
If you’ve decided to stick with this for now then add a new class called “clsCircle” to
the project and add the BaseClass definition, the initialise/terminate events of the
class and the standard pass-through properties as we did in the previous chapter:

Dim WithEvents BaseClass As clsShape

' Public interface to member variables


Public Property Get ShapeCol() As Long
ShapeCol = BaseClass.ShapeCol
End Property
Public Property Let ShapeCol(ByVal inNew As Long)
BaseClass.ShapeCol = inNew
End Property

Public Property Get ShapePosX() As Long


ShapePosX = BaseClass.ShapePosX
End Property
Public Property Let ShapePosX(ByVal inNew As Long)
BaseClass.ShapePosX = inNew
End Property

Public Property Get ShapePosY() As Long


ShapePosY = BaseClass.ShapePosY
End Property
Public Property Let ShapePosY(ByVal inNew As Long)
BaseClass.ShapePosY = inNew
End Property

' Class event handlers


Private Sub Class_Initialize()
Set BaseClass = New clsShape
End Sub

Private Sub Class_Terminate()


Set BaseClass = Nothing
End Sub

Now, there are two ways of creating a circle, the first being a centre point, width and
height and the second a corner-to-corner method a-la the Rectangle. The latter is the
method the GDI Ellipse() API uses but I prefer the former since I find it more
intuitive. As such I’ll add methods for both and the centre-out creation method will
translate its points into corner-to-corner mode:

Public Sub CreateCtoC(ByVal inXa As Long, ByVal inYa As Long, _


ByVal inXb As Long, ByVal inYb As Long)

With BaseClass
.ShapePosX = inXa
.ShapePosY = inYa
Call .AddPoint(0, 0)

Http://www.mvps.org/EDais/
14
EDais Advanced classes tutorial

Call .AddPoint((inXb - inXa), (inYb - inYa))


End With
End Sub

Public Sub CreateCOut(ByVal inX As Long, ByVal inY As Long, _


ByVal inWidth As Long, ByVal inHeight As Long)

With BaseClass
.ShapePosX = inX - (inWidth \ 2)
.ShapePosY = inY - (inHeight \ 2)
Call .AddPoint(0, 0)
Call .AddPoint(inWidth, inHeight)
End With
End Sub

This may look a little strange since they’re re-mapping their coordinates to local
space but it makes a lot of sense in the long run. Ok, we have the public interface set
up and the shape created, now all that remains is to draw. We’re going to need to
draw a circle so we’ll need a declare to the Ellipse() API before we can go about
drawing so copy and paste that into the top of the circle class now.
The Draw() function will pass through to the base class in exactly the same way as we
did in the rectangle class:

Public Function Draw(ByRef inDC As Long) As Boolean


Draw = BaseClass.Draw(inDC)
End Function

Now finally we need to respond to the base classes DrawNow event where we will do
the drawing, so select the class from the Object drop-down (Make sure you’ve
declared it WithEvents) and you’ll be put into the event handler since its the only
event exposed by the base class. Because the Ellipse() API works corner-to-corner in
the same way as the Rectangle() API, we can simply copy and paste the code from
the rectangle class and just change the call from Rectangle() to Ellipse():

Private Sub BaseClass_DrawNow(ByVal inDC As Long, ByRef outResult As Boolean)


Dim BasePts(3) As Long

With BaseClass
Call .GetPoint(0, BasePts(0), BasePts(1))
Call .GetPoint(1, BasePts(2), BasePts(3))

outResult = Ellipse(inDC, _
BasePts(0) + .ShapePosX, BasePts(1) + .ShapePosY, _
BasePts(2) + .ShapePosX, BasePts(3) + .ShapePosY) <> 0
End With
End Sub

And we’re done!


To test it, change the code in the form to:

Private Sub Form_Load()


Dim MyClass As New clsCircle

Form1.AutoRedraw = True

With MyClass

Http://www.mvps.org/EDais/
15
EDais Advanced classes tutorial

.ShapeCol = vbGreen
Call .CreateCOut(15, 20, 30, 40)
Call .Draw(Form1.hDC)
End With

Call Form1.Refresh

Set MyClass = Nothing


End Sub

You should see a green circle being drawn but if you don’t then check your code
against mine and see what differs.

Http://www.mvps.org/EDais/
16
EDais Advanced classes tutorial

The power of a common interface


Chapter V

At this point I wouldn’t blame you for thinking that whilst it cut’s down on a bit of
programming this whole derived class business just seems like a lot of hard work and
fuss over something trivial. This method of programming has a few more tricks up its
proverbial sleeves yet to try and win you over which I’ll discuss now.
It may seem a little silly adding the same code in every class since this method is
supposed to cut down on the amount of coding rather than creating more but it does
allow us to do some cool things by abstracting the data types. Since all the objects
we’ve created have a .Draw() method and their colour can be changed in a generic
way then it actually doesn’t matter which shape object we’re dealing with, we can
just tell it to draw and it will work out what it is and what it should be doing. There
are a couple of ways this can be demonstrated; the first is to coerce the object into a
variant type:

Private Function DrawShape(ByRef inShape As Variant) As Boolean


DrawShape = inShape.Draw(Form1.hDC)
End Function

No matter which shape class we send this function, it will draw onto the specified DC
(Or return False if for some reason the drawing failed). If we later create a Polygon
class that uses the same common interface, we can still send it to this function to tell
it to draw without changing a simple thing in the underlying application.
You may or may not find this interesting or particularly useful but the thing I’m
trying to emphasise here is abstraction - we’re abstracting the problem down from
how to draw a circle or how to draw a rectangle to simply how to draw a ‘shape’.
This may or may not seem important for the time being since this example most
likely had no relevance to anything you’d ever need to code, but numerous
programming tasks can be broken down into simpler area’s to which this abstraction
approach fit’s very well.
The second way we can take advantage of this new technique of abstraction is by
using VB’s Collection object. Don’t worry if you’ve never used them before, up until
a week ago neither had I (At least creating and using them usefully in an application),
they’re very easy though, here’s an example:

Private Sub Form_Load()


Dim MyRect As New clsRectangle
Dim MyCirc As New clsCircle
Dim Shapes As Collection
Dim DrawShapes As Long

' Create two shapes


Call MyRect.Create(10, 10, 50, 30)
Call MyCirc.CreateCOut(35, 60, 50, 30)

' Create a collection of 'shapes' and push the individual shape objects into it
Set Shapes = New Collection
Call Shapes.Add(MyRect)
Call Shapes.Add(MyCirc)

Form1.AutoRedraw = True

' Draw all the shape objects regardless of what they are

Http://www.mvps.org/EDais/
17
EDais Advanced classes tutorial

For DrawShapes = 1 To Shapes.Count


With Shapes(DrawShapes)
.ShapeCol = Rnd() * vbWhite
Call .Draw(Form1.hDC)
End With
Next DrawShapes

Call Form1.Refresh

' Clean up
Set Shapes = Nothing
Set MyRect = Nothing
Set MyCirc = Nothing
End Sub

Compare that to all the complexity and coding of doing all that using the raw API
calls, pens, brushes etc and this is all completely reusable.
Here’s some food for thought, using this interface hit-testing multiple shapes would
be ridiculously easy using this technique; just add a “HitTest()” method to each class
that tests to see if an (X, Y) coordinate is within it’s internal shape then simply loop
through all the shapes in the shapes collection on the MouseDown() event until one
returns true – you clicked that one!
Multiple selections would also be easy, just add a property to the base class
“Selected” and expose it through each of the derived classes. If the Ctrl key is
pressed in the above hit testing example (If (Shift = vbCtrlMask) Then '...) then
simply set the Selected property of the clicked shape object (Doesn’t matter what it
is!) to being true, then in your MouseMove() event simply iterate all the shapes in the
collection and move all those with the Selected flag set to true. A few lines of code
and suddenly you have a very powerful and completely generic solution, and of
course if in build 1.1 you want to add 3 more shape’s then as long as they support the
common interface you won’t have to change even 1 line of code for the current hit
testing, multiple selection, redrawing etc, but not only that - they will already all be
supported in these situations!
This is only for this particular example but the possibilities for using this in other
situations makes for some quite interesting and powerful applications. Also
developing using these kind of techniques will make migrating to other OO languages
a lot easier since you’ll already be thinking (and programming) in the right way.

Http://www.mvps.org/EDais/
18
EDais Advanced classes tutorial

Interfaces and wrapped collections


Chapter V I

So far we’ve seen how to create multiple classes that expose the same functions,
however because VB is incapable of supporting a true base class we must refer to
them either by their individual class types or as Variants. The latter approach works
ok but it’s somewhat clunky and since Variants can hold different variables types we
get no intellisense, which is somewhat counterintuitive to work with.
Whilst VB doesn’t allow us to derive our classes from a common base class, it does
have support for interfaces which we can use to simulate the same kind of behaviour.
An interface is really just a blueprint for a class in the same way as a class is the
blueprint for an object, and is used as a ‘contract’ that any class implementing it must
agree to adhere to.
To create the interface our shape objects will derive from, simply add a new class
module to the VB project and name it “IShape” – Interface names are generally
prefixed “I”.
The interface itself contains only the public properties and methods we wish to
expose, so you’ll want something like this:

Public Property Get ShapeCol() As Long


End Property
Public Property Let ShapeCol(ByVal inNew As Long)
End Property

Public Property Get ShapePosX() As Long


End Property
Public Property Let ShapePosX(ByVal inNew As Long)
End Property

Public Property Get ShapePosY() As Long


End Property
Public Property Let ShapePosY(ByVal inNew As Long)
End Property

Public Function Draw(ByVal inDC As Long) As Boolean


End Function

For one of our shape classes to implement this interface, we simply add this line at
the start of the class:

Implements IShape

If you try running the application at this point, it will error and tell you that your class
does not expose some of the functionality in the interface so you must always
implement everything. Rather than copying out all the function headers for each
method inside the interface, you’ll find a new entry called “IShape” in the left
dropdown menu in the code editor, and selecting the function names from the right
one will fill in the function stubs for you. Now all that’s left is to move the
functionality from the class itself into these new method stubs:

Private Function IShape_Draw(ByVal inDC As Long) As Boolean


IShape_Draw = BaseClass.Draw(inDC)
End Function

Http://www.mvps.org/EDais/
19
EDais Advanced classes tutorial

Private Property Get IShape_ShapeCol() As Long


IShape_ShapeCol = BaseClass.ShapeCol
End Property
Private Property Let IShape_ShapeCol(ByVal inNew As Long)
BaseClass.ShapeCol = inNew
End Property

Private Property Get IShape_ShapePosX() As Long


IShape_ShapePosX = BaseClass.ShapePosX
End Property
Private Property Let IShape_ShapePosX(ByVal inNew As Long)
BaseClass.ShapePosX = inNew
End Property

Private Property Get IShape_ShapePosY() As Long


IShape_ShapePosY = BaseClass.ShapePosY
End Property
Private Property Let IShape_ShapePosY(ByVal inNew As Long)
BaseClass.ShapePosY = inNew
End Property

The class itself should now have no properties and no Draw() method, they are now
only accessible through the interface. Basically we’ve not really added anything to
the class itself, just changed things around a little but in exchange we can now refer to
any class that implements this interface simply as an “IShape”, regardless of what it
actually is.
Perform this modification on the Rectangle and Circle classes, then we’ll modify the
last example to take the new interface into account.
Since the ShapeCol property and Draw() methods are now exposed through the
interface rather than the classes public interface, we can’t simply go through the
collection and access them directly off each object but instead must cast them to a
temporary variable declared as an IShape:

Dim TempCast As IShape

...

Set TempCast = Shapes(DrawShapes)

With TempCast
.ShapeCol = Rnd() * vbWhite
Call .Draw(Form1.hDC)
End With

Set TempCast = Nothing

It may seem like a step in the wrong direction since we now have more code to have
to deal with, but you may notice that inside the “With” bock we now get
intellisense/code-completion on the interfaces methods which is IMO a bigger step
forward and worth the extra couple of lines.
One thing still remains a little messy here though, and that’s the Collection object
which could in theory hold anything and again we get no intellisense on it since it’s
just a collection of ‘stuff’.
What we really want is a hard-typed collection and for that we need another class that
wraps a standard Collection object, add a new class module and call it “clsShapes”.

Http://www.mvps.org/EDais/
20
EDais Advanced classes tutorial

If you hit F2 now and have a look at the standard Collection object in the object
browser, you’ll see that it has 4 methods; “Add”, “Count”, “Item” and “Remove”,
however if you opposite click and choose “Show hidden members” you’ll see a 5’th,
“_NewEnum”, which we’ll get to in a bit.
Fist off though, we’ll use a Collection as our base class and expose its 4 public
methods, however rather than using Variants as the contents we’ll use IShape’s:

Dim m_Collection As Collection

Public Property Get Count() As Long


Count = m_Collection.Count
End Property

Public Property Get Item(ByVal inIdx As Long) As IShape


Set Item = m_Collection.Item(inIdx)
End Property

Public Sub Add(ByRef inNew As IShape)


Call m_Collection.Add(inNew)
End Sub

Public Sub Remove(ByVal inIdx As Long)


Call m_Collection.Remove(inIdx)
End Sub

Private Sub Class_Initialize() ' Instantiate base class


Set m_Collection = New Collection
End Sub

Private Sub Class_Terminate() ' Terminate base class


Set m_Collection = Nothing
End Sub

This doesn’t expose functionality for a keyed collection, but you can add that later if
you wish.
The obscure 5’th method we saw in the object browser is what’s responsible for
giving us “For ... Each” functionality to iterate the objects within the collection, and
since we’re only going to be working with objects it makes good sense to add it here
too:

Public Function NewEnum() As IUnknown ' Required for "For ... Each"
Set NewEnum = m_Collection.[_NewEnum]
End Function

One more thing remains before VB will recognise this as a valid enumerator, and
that’s to set its procedure ID by opening “Tools -> Procedure Attributes”.
Select the “NewEnum” method, expand the “Advanced” section at the bottom and set
the procedure ID to -4 which is the special code for an enumerator in VB. Since this
is a bit of an odd function and only of any use to VB, we’ll also check the “Hide this
member” checkbox here to avoid cluttering the public interface.
At this point the Shapes collection is complete, so we can go back to the original code
and use it instead of the generic Collection object:

Dim MyRect As New clsRectangle


Dim MyCirc As New clsCircle

Http://www.mvps.org/EDais/
21
EDais Advanced classes tutorial

Dim Shapes As New clsShapes


Dim Shape As IShape

' Create two shapes


Call MyRect.Create(10, 10, 50, 30)
Call MyCirc.CreateCOut(35, 60, 50, 30)

' Push individual shapes into shapes collection


Call Shapes.Add(MyRect)
Call Shapes.Add(MyCirc)

Form1.AutoRedraw = True

' Draw all the shape objects regardless of what they are
For Each Shape In Shapes
With Shape
.ShapeCol = Rnd() * vbWhite
Call .Draw(Form1.hDC)
End With
Next Shape

Call Form1.Refresh

Set Shapes = Nothing


Set MyRect = Nothing
Set MyCirc = Nothing

The majority of the code looks the same, but the shape drawing loop is a lot nicer and
demonstrates the use of the enumerator.
Drawing all the shapes in a shapes collection seems like it would be a pretty common
requirement, and since we now have our own collection object we can add that
functionality right into it:

Public Sub Draw(ByVal inDC As Long)


Dim Shape As IShape

For Each Shape In m_Collection


Call Shape.Draw(inDC)
Next Shape
End Sub

Now the drawing code can be shortened down to:

' Set some random colours for the shapes


For Each Shape In Shapes
Shape.ShapeCol = Rnd() * vbWhite
Next Shape

' Draw all shapes in collection to DC


Call Shapes.Draw(Form1.hDC)

Since every shape in the collection has a ShapePosX and ShapePosY property that
can move the origin of the shape, we could also add a very simple method to the class
that moves all the shapes within it:

Public Sub Offset(ByVal inX As Long, ByVal inY As Long)


Dim Shape As IShape

Http://www.mvps.org/EDais/
22
EDais Advanced classes tutorial

For Each Shape In m_Collection


Shape.ShapePosX = Shape.ShapePosX + inX
Shape.ShapePosY = Shape.ShapePosY + inY
Next Shape
End Sub

Now we can call this from the form to draw multiple instances of the shapes:

Dim LoopDraw As Long

...

For LoopDraw = 0 To 9
' Set some random colours for the shapes
For Each Shape In Shapes
Shape.ShapeCol = Rnd() * vbWhite
Next Shape

' Draw all shapes in collection to DC then nudge them


Call Shapes.Draw(Form1.hDC)
Call Shapes.Offset(5, 5)
Next LoopDraw

I hope this has helped outline some of the possibilities of the technique, feel free to
contact me if you still have questions about anything covered in the tutorial.

Mike D Sutton – EDais 2004

Http://www.mvps.org/EDais/

Vous aimerez peut-être aussi