An ActiveX control can be 'dropped' into a form like any other control. Controls are not specific to VB, but can also be dropped into VBA forms in other Office applications. Parts of programs, like the Word Art feature packaged with Word, can be exposed as ActiveX objects and installed in any container that can hold ActiveX, including VB forms. ActiveX controls can be written either in Visual C++ or in VB. They can also be used by either language. (J++ can incorporate ActiveX, but the platform - independant versions can not, as of this writing.)
Perhaps the most exciting part of this technology from our point of view is that it gives us a framework to extend our classes into actual controls that can be used by our VB projects.
With just a small amount of study, you no doubt figured out what this code does. It reacts whenever a user clicks on a picture box, and it increments to the next qbcolor, recycling at 15. This is a pretty meaningless piece of code, yet it has some value as an example.
Let's make an ActiveX control with this same functionality.
Add a user control from the project menu. You will see the typical list of templates. Go with the generic 'user control' for now. As usual, the other options will be handy later, but often perform superflous features that are of little values when we're trying to orient ourselves.
When you look at the project window now, you will see that you now have two projects open at once. This has only been possible in the latest versions of VB, but it is sometimes necessary for ActiveX programming. What's going on here is we're actually writing two distinct programs. One of them (the exe) happens to use the other. (or will, when we add the code).
Note that the user control does have a visual interface which looks suspicously like a form, although it is not.
It would be great if the programmer could write this code in her form:
sub colorScroll1_change()
form1.backColor = colorScroll1.color
end sub
In order for this custom control to be of any use, it ought to expose
some kind of color property, so the form author can set a color based on
whatever the user selected. The purpose of this control is to shield the
programmer from worrying about qbColors. In addition to a color property,
we ought to have a few other standard properties, such as enabled, top
and left, visible, and so on.
We will also need to have some sort of event mechanism. The programmer
will be expecting a change event, so we ought to provide one.
The left hand panel contains a bunch of pre-defined properties, events,
and methods. The right hand panel consists of the set of these pre-defined
methods that will be used for your control. Note that I removed all
the properties, and all methods except 'enabled'. I wanted to see
what happens when I leave one. The property I really want (color)
does not exist on the list. The change event does exist, but I did
not see it when I wrote this example, so I added it later. The next
screen will allow me to add these custom members.
This form has a place to list any custom members. Note that the screen shot was taken AFTER I had added some members. When you click on the New button, you will see a little dialog like this:
It's pretty straightforward, as you can see. When we're done
adding members, we will go on to the next screen of the wizard...
As you can see, this screen allows us to 'map' members of the custom
control to similar members of the constituent controls. In many cases,
a member of the custom control will directly correlate with a member of
one of the controls on the custom control. In this example, all three
of my members will have an obvious mapping relationship, although you will
see that the wizard does not always get this right.
The change event of the colorScroller should be related to the change
event of the scrollbar inside the scroller. Likewise, the color of
the scroller will always have the same value as the foreColor property
of the picture box. The enabled property can be tied to the enabled
property of the userControl environment itself (the closest analogy to
a form in activeX control design). It is up to the programmer to ensure
that sensible mapping is done. We shouldn't try to map a color property
to the left property of some constituent control, for example, because
these things are not the same type of information. Generally, your
control members will be mapped to constituent members with the same or
very similar names. This is the case in our control.
If you set the mapping value to 'none' you will get another screen which will allow you to determine a little about the member, including whether it is read/write, and the comparison of its runtime and design-time characteristics, as well as a return type if it is a method. These will be used to generate code stubs you can fill in with your one code.
When we hit next, we come to the finished screen. This has an option to print out instructions for testing the control. These instructions are useful but can be obtuse. Read them, but don't be worried if you don't understand the whole thing.
'Event Declarations:
Event Change() 'MappingInfo=scrColor,scrColor,-1,Change
Private Sub scrColor_Change()
RaiseEvent Change
picColor.BackColor = QBColor(scrColor.Value)
End Sub
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=UserControl,UserControl,-1,Enabled
Public Property Get Enabled() As Boolean
Enabled = UserControl.Enabled
End Property
Public Property Let Enabled(ByVal New_Enabled As Boolean)
UserControl.Enabled() = New_Enabled
PropertyChanged "Enabled"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=picColor,picColor,-1,ForeColor
Public Property Get color() As OLE_COLOR
color = picColor.ForeColor
End Property
Public Property Let color(ByVal New_color As OLE_COLOR)
picColor.ForeColor() = New_color
PropertyChanged "color"
End Property
'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
UserControl.Enabled = PropBag.ReadProperty("Enabled",
True)
picColor.ForeColor = PropBag.ReadProperty("color",
&H80000012)
End Sub
'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
Call PropBag.WriteProperty("Enabled", UserControl.Enabled,
True)
Call PropBag.WriteProperty("color", picColor.ForeColor,
&H80000012)
End Sub
The original line is still there, in the scrColor_change event, but there's a lot of other stuff going on as well. Let's take a look at it.
When we test this code in a moment, we will see that it does not work perfectly. Look carefully at the relationship between the change event of the scrollbar and the change event of the object, and see if you can guess what the problem will be.
sub colorScroll1_change()
form1.backColor = colorScroll1.color
end sub
When we test the program, it does not work correctly!!!
When I ran it, I always got the form color changed to black, which
is qbColor zero. Interestingly, when I tried this with VB5, the results
were wrong in a different way. The color would change, but the form
color would always lag behind the scrollbar by one value.
The soloution can be found by looking closely at the scrColor_change event in the control code. There are two minor problems here.
After playing with debug.print, it became obvious that the color value was not automatically changing. Sure, we told the wizard to make picScroller.color integrated with picColor.foreColor, but it isn't happening properly. We'll need to give it a hand. Apparently, the property set of the scroller is not being called, so we'll force it by changing it as well as changing the color of the picture box. Here's my new code for scrColor_change()
RaiseEvent Change
picColor.BackColor = QBColor(scrColor.Value)
Me.color = picColor.BackColor ' I added this
This is a little better, but it causes the 'lag' behaviour that we saw in the VB5 version. When my scrollbar is on color 2, my form is on color 1. The form color is always one behind the scroll color.
The wizard put the raiseEvent call at the BEGINNING of the code. This tells the calling program to evaluate a change event before we have changed anything at this level. Clearly this is not correct. The raiseEvent should be the last thing this code does. After we move it to the bottom, we are ensured that the values have changed BEFORE the form gets a chance to deal with the colorScroller_change event.
Here's my final version of the code for scrColor_change():
Private Sub scrColor_Change()
picColor.BackColor = QBColor(scrColor.Value)
Me.color = picColor.BackColor ' I added this
RaiseEvent Change
'I moved this to the bottom
End Sub
Everything else was left alone.
Encapsulation:
Private Sub cmdLandA_Click()
plane.land imgAirportA
End Sub
Private Sub cmdLandB_Click()
plane.land imgAirportB
End Sub
Private Sub cmdLeft_Click()
plane.turnLeft
End Sub
Private Sub cmdRight_Click()
plane.turnRight
End Sub
Private Sub cmdTakeOff_Click()
plane.takeOff
End Sub
Private Sub Form_Load()
'set up a traditional cartesian coordinate system
acax.Scale (-100, 100)-(100, -100)
'the plane control already 'knows' its top and
'left, so we need not explicitly set them
End Sub
Private Sub Timer1_Timer()
plane.goForward
'the plane control cannot set its top and left, but
'it can tell us what they should be. We still must
'do the actual positioning from here.
plane.Move plane.newLeft, plane.newTop
End Sub
That is the FINISHED version of the form. If we design the control correctly, we will need nothing else in the form. Note that almost all the action is deferred to the control.
The land commands are especially exciting. Note that there are two, one for each airport. They will call the plane's land procedure, and send it a reference to the relevant airport image as a parameter. In this way, the airplane object can do all the landing calculations, and we need not do anything but tell it we intend to land, and which airport object we want it to land upon.
The turning and takeOff buttons are incredibly straightforward. They simply pass control to relevant methods of the plane object.
The timer event calls the plane.goForward method, which causes the plane to determine its own coordinates. We still have to set these values from the form, but that is reasonably easy to do. We'll discuss this more when we look at the move method of the aircraft control.
form_load simply sets up the scale to the expected cartesian coordinate system. Note that we no longer have to explicitly tell the control 'where it is', as it ought to know that already.
properties:
The directionTypes enum was just so useful that I copied it verbatim
from the earlier class file.
Using the activeX control wizard, I built a control with the properties
and events outlined above. As we go, we will explain why those particular
properties and events were chosen, and why we sometimes deviated from our
earlier members in the aircraft class file.
Here is a listing of the more straightforward parts of the control. I have not included the code for goForward or land, as they are a bit more involved.
Public Enum DirecTypes
NORTHWEST = 0
NORTH = 1
NORTHEAST = 2
EAST = 3
SOUTHEAST = 4
SOUTH = 5
SOUTHWEST = 6
WEST = 7
End Enum
' private variable (maybe it should be a property)
Private Const speedval = 5
'Default Property Values:
Const m_def_newTop = 0
Const m_def_newLeft = 0
Const m_def_Enabled = 0
Const m_def_direction = NORTH
Const m_def_moving = False
Const m_def_altitude = 0
'Property Variables:
Dim m_newTop As Integer
Dim m_newLeft As Integer
Dim m_Enabled As Boolean
Dim m_direction As DirecTypes
Dim m_moving As Boolean
Dim m_altitude As Integer
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=0,0,0,0
Public Property Get Enabled() As Boolean
Enabled = m_Enabled
End Property
Public Property Let Enabled(ByVal New_Enabled As Boolean)
m_Enabled = New_Enabled
PropertyChanged "Enabled"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=7,0,0,0
Public Property Get direction() As DirecTypes
'used when retrieving value of a property, on the right
side of an assignment.
direction = m_direction
End Property
Public Property Let direction(ByVal New_direction As DirecTypes)
If (New_direction < 0) Or (New_direction
> 7) Then
MsgBox
"New_direction is " + Str(New_direction)
MsgBox
"Illegal value! Setting direction to NORTH"
m_direction
= NORTH
Else
m_direction
= New_direction
End If
PropertyChanged "direction"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=7,0,0,0
Public Property Get newTop() As Integer
newTop = m_newTop
End Property
Public Property Let newTop(ByVal New_newTop As Integer)
m_newTop = New_newTop
PropertyChanged "newTop"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=7,0,0,0
Public Property Get newLeft() As Integer
newLeft = m_newLeft
End Property
Public Property Let newLeft(ByVal New_newLeft As Integer)
m_newLeft = New_newLeft
PropertyChanged "newLeft"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=5
Public Sub turnLeft()
If Me.direction > 0 Then
Me.direction = Me.direction - 1
Else
Me.direction = 7
End If
picPlane.Picture = imgStore(direction).Picture
End Sub
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=5
Public Sub turnRight()
If Me.direction < 7 Then
Me.direction = Me.direction + 1
Else
Me.direction = 0
End If
picPlane.Picture = imgStore(direction).Picture
End Sub
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=5
Public Sub takeOff()
Me.moving = True
Me.altitude = 5
End Sub
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=0,0,0,0
Public Property Get moving() As Boolean
moving = m_moving
End Property
Public Property Let moving(ByVal New_moving As Boolean)
m_moving = New_moving
PropertyChanged "moving"
End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=7,0,0,0
Public Property Get altitude() As Integer
altitude = m_altitude
End Property
Public Property Let altitude(ByVal New_altitude As Integer)
m_altitude = New_altitude
PropertyChanged "altitude"
End Property
'Initialize Properties for User Control
Private Sub UserControl_InitProperties()
m_Enabled = m_def_Enabled
m_direction = m_def_direction
m_moving = m_def_moving
m_altitude = m_def_altitude
m_newTop = m_def_newTop
m_newLeft = m_def_newLeft
End Sub
'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
m_Enabled = PropBag.ReadProperty("Enabled", m_def_Enabled)
m_direction = PropBag.ReadProperty("direction",
m_def_direction)
m_moving = PropBag.ReadProperty("moving", m_def_moving)
m_altitude = PropBag.ReadProperty("altitude",
m_def_altitude)
m_newTop = PropBag.ReadProperty("newTop", m_def_newTop)
m_newLeft = PropBag.ReadProperty("newLeft",
m_def_newLeft)
End Sub
'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
Call PropBag.WriteProperty("Enabled", m_Enabled,
m_def_Enabled)
Call PropBag.WriteProperty("direction", m_direction,
m_def_direction)
Call PropBag.WriteProperty("moving", m_moving,
m_def_moving)
Call PropBag.WriteProperty("altitude", m_altitude,
m_def_altitude)
Call PropBag.WriteProperty("newTop", m_newTop,
m_def_newTop)
Call PropBag.WriteProperty("newLeft", m_newLeft,
m_def_newLeft)
End Sub
The code is a bit ugly, as most of it came from the wizard, but you can see it's pretty much like it was. This would be enough to place on a simple form to test that at least the control is showing up OK and that the turn commands are working.
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MemberInfo=5
Public Sub goForward()
'analyzes direction, generates new values for left and top
'expects CARTESIAN coordinates (top is larger at top of
page)
Me.newLeft = Extender.Left
Me.newTop = Extender.Top
If (Me.moving = True) Then
Select Case Me.direction
Case NORTH
Me.newLeft = Extender.Left
+ 0
Me.newTop = Extender.Top
+ speedval
Case NORTHEAST
Me.newLeft = Extender.Left
+ speedval
Me.newTop = Extender.Top
+ speedval
Case EAST
Me.newLeft = Extender.Left
+ speedval
Me.newTop = Extender.Top
+ 0
Case SOUTHEAST
Me.newLeft = Extender.Left
+ speedval
Me.newTop = Extender.Top
- speedval
Case SOUTH
Me.newLeft = Extender.Left
+ 0
Me.newTop = Extender.Top
- speedval
Case SOUTHWEST
Me.newLeft = Extender.Left
- speedval
Me.newTop = Extender.Top
- speedval
Case WEST
Me.newLeft = Extender.Left
- speedval
Me.newTop = Extender.Top
+ 0
Case NORTHWEST
Me.newLeft = Extender.Left
- speedval
Me.newTop = Extender.Top
+ speedval
End Select
End If
End Sub
It is instructive to understand what we wish this code could do. We cannot quite get there, and had to make some compromises. Let's examine why.
It would have been great if we could have directly manipulated the .left
and .top properites of the control from its own code. Had this been
possible, the code for, say, case EAST, would just be this:
Case EAST
Me.Left = Me.Left +
speedval
Me.Top = Me.Top + 0
That makes all kind of sense, and it was my first attempt. However, the world is not that simple (of course). It turns out that the left and top properties of the control are NOT available directly to the control!! When you are designing the control, the left and top properties do not appear in the drop-down box in the editor, nor do they show up in the properties window. They do show up when we use the control in another container. Essentially, these guys are preperties of the object, but there value only has meaning when they are inside some kind of container. Remember, our form has a customized scale. How could the object know the appropriate top and left values if it is unaware of the scale of the container?
Fortunately, VB supplies us with a special object, called the Extender.
This thing supplies read-only values which correlate to the top, left,
and some other properties of the object which only make sense after the
object is in a container. By using the extender, I can read the current
values of top and left (and some other properties) but you cannot set
properties in the extender. In short, there is (at this time) no
way for a control to directly set it's own position in the container.
We can publish the updated left and top by setting up some other properties(which
is exactly what we will do), but we cannot set .left and .top directly.
So, the actual code for the east case is this:
Case EAST
Me.newLeft = Extender.Left
+ speedval
Me.newTop = Extender.Top
+ 0
The newLeft property is assigned a value. It will be up to the
form programmer to correlate the top and left properties to the newLeft,
newTop properties we are assigning here. Both properties will be
based on the current value of the Extender properties. So, we are
reading from Extender.left, and writing to Me.newLeft. This seems
a bit ugly, but it appears to be the best solution for now.
Back in the timer of the form, we will have the following code:
plane.goForward
plane.Move plane.newLeft, plane.newTop
The goForward method call activates the code we just created, but does not change the actual location of the plane. The move method is exactly the same move method used on any other controls. I will send it the newly calculated values of newLeft and newTop. This command causes the plane to actually move on the screen. It would be great if we could do it with one method call, but this isn't too bad.
The land method in our control will combine the form's cmd_land code with the object's landing method, and place all the code in the control. To do this, we will need to know the size and position of the aircraft as well as the size and position of the airport. The aircraft will be able to read its size and position from the extender object, but we will need to know about the airport. The solution to this is to pass a reference to the airport from the form code to the land method. In this way, the airplane can attempt to land on ANY image object (airport) that is passed to it. This ought to give us a form of polymorphism.
To bring in the parameter, we write it into the function definition,
like this:
Public Sub land(airport As Control)
Note that I imported airport as a control, so it could work with either a picture box or an image. Now I have a parameter named airport which acts just like a locally declared variable, except it reflects the properties of the original object on the form. (so THAT's why we learned to define controls as variables!!!)
The algorithm for landing is essentially a large error - detection scheme. We will only land if a number of conditions are met. The plane must be over the airport, it must be facing in an appropriate direction, and it must be at an appropriate altitude.
To simplify the code, we will generate a bunch of local variables. Here they are:
'make some variables to simplify analysis
Dim apTop As Integer
Dim apBottom As Integer
Dim apLeft As Integer
Dim apRight As Integer
Dim plnTop As Integer
Dim plnBottom As Integer
Dim plnLeft As Integer
Dim plnright As Integer
Dim apSizeOK As Boolean
Dim directionOK As Boolean
I used 'ap' to mean airport and 'pln' to mean plane. Now I want to assign values to these:
apTop = airport.Top
apBottom = airport.Top - airport.Height
apLeft = airport.Left
apRight = airport.Left + airport.Width
plnTop = Extender.Top
plnBottom = Extender.Top - Extender.Height
plnLeft = Extender.Left
plnright = Extender.Left + Extender.Width
apSizeOK = False
directionOK = True
The ap variables were derived from the airport parameter, and the pln variables were derived from the extender object. None of these variables is necessary, but they will make the conditions coming up a lot easier to write and debug.
The apSizeOK and directionOK variables are boolean variables I will use for some other tests. I want to give them default values that will make sense. We'll check the airport size soon, and if it is OK, we'll change the value of the apSizeOK variable. For now, I'm not going to mess with landing directions, so I'll just set it to true and leave it there.
The following code checks the airport, to see if it is big enough. The size checking routines won't work if the airport is the same size or smaller than the airplane, so we better check first to be sure the size is legal.
'Ensure airport is larger than plane!!
If airport.Width > Extender.Width Then
If airport.Height > Extender.Height Then
apSizeOK = True
End If
End If
This code simply sets a boolean variable that we will check in our
conditional structure below.
OK, here goes that huge pile of if structures:
If apSizeOK Then
If plnLeft > apLeft Then
If plnright < apRight Then
If plnTop < apTop
Then
If plnBottom
> apBottom Then
If Me.altitude <= 5 Then
If directionOK Then
'we can land safely
Me.moving = False
Me.altitude = 0
MsgBox "nice landing..."
Else
MsgBox "can't land in this direction!!"
End If 'directionOK
Else
MsgBox "altitude too high!!"
End If 'altitude
Else
MsgBox "too far south"
End If 'bottom
comparison
Else
MsgBox "too
far north"
End If 'top compare
Else
MsgBox "too far east"
End If 'right compare
Else
MsgBox "too far west"
End If 'left compare
Else
'apsize is not OK
MsgBox "the airport is too small for this plane!!"
End If 'airport size
This is long, but thankfully, because of our variables, it is not too difficult to read. As before, we have a large bank of nested if statements that handle all the various things that might cause a landing to not work. The landing itself is nestled at the very core of this structure, so it only happens when all the conditions are met. If any one condition is not met, it skips all the other checks, and sends an error code. Again, the messageboxes are a bit rude, but they serve our purposes here. Note that I added comments to my end if statements. That's pretty handy in a complex piece of code like this. It's nice to check and make sure everything lines up correctly.
That's basically it!!!
Certainly we might want to consider a speed property. This would be interesting, because the speed ought to be registered in knots or miles per hour. In these measuring schemes, a larger number means a faster speed. The internal way speed is measured is the number of pixels to move during a given clock cycle. In this scheme, smaller is faster. We would want the property to work so that the user could assign some kind of value in knots, and the local instance variable would appropriately be adjusted. This would have been very cumbersome with public instance variables, but should be manageable with property methods.
Another great feature would be the re-inclusion of direction handling for airports. Right now, the plane can land regardless of which direction it is facing. Different airports will have different runways available, and all runways will not always be open. It would be nice to have some way to also pass some value indicating the open runways to the land method. There are many ways we could do this. The easiest might be to redesign the method so it also requires an array eight boolean values: each one corresponding to a direction. Each of these values would be set to true if it is OK to land on the runway, and false if it is not. We might call this parameter 'runway'. This could add a challenging realism to the simulation. Inside the method, we would simply add a series of statements checking to see if the plane's current direction is equal to a directionOK variable set to true. The code might look like this:
if runway(me.direction) = true then
directionOk = true
else
directionOK = false
end if
There are ways to do this with less complex data structures, but they will require more complex control structures.
Of course, we could also make airports into a control type that would contain the image and the direction array. In that case, all we would have to do would be to pass the airport object to the plane's land method.
We might want to improve our simulation by having other targets besides airports. Frequently, aircraft are directed to specific areas of the sky, known as 'intersections,' and told to loiter there. It would be nice if there were some kind of loiter method that would simply cause the plane to circle, and a simplified version of the land method that would simply check to see if the plane was over some target without doing all the landing stuff.
We could also build in code to dynamically handle image sizes, to allow multiple image sets, to randomly generate 'emergency conditions' and many other things. As usual, the only limits are time and imagination.
When Windows (whichever flavor, from '95 upwards) encounters a control, that control must be registered to that particular computer. When it is registered, it is available to be manipulated through the registry, and it can be used by VB as well as other component containers (presuming it was set up properly). It is important to note that the activeX control resides in a file (or several files) on the local machine, and is registered specifically to that machine.
If you create an .exe project that incorporates an activeX control,
the control will be part of the resulting .exe.
If you create a program that only consists of an activex control, an
OCX file will be created. This file can be installed on another machine
and used in visual basic programs, or any other environment that allows
activeX controls. Theoretically, you can even place an activeX control
in an HTML document. When you go to the web page (In Internet explorer)
your browser will attempt to read the control and install it on the local
machine. It is only installed once, but once it is there, it has
all the power of any other control on your machine.
This is very different from the java scheme, which limits what an applet can do, and re-loads the applet every time.
Unfortunately, the process for setting this up is difficult and fraught
with opportunities for dissapointment. It seems to be very error-prone,
but perhaps that will change soon.
Provided that this part works ok, the control will be registered, and the web page can utilize it. It will not work at all with Netscape unless a special plug-in is added, and it does not work well even in Explorer when run in Mac or Unix operating systems.
Just keep in mind that VB itself was once a cutting - edge, emerging
technology. Back when we first saw VB 1.0, it had more promise than
actual practical use, and look what it has become.