Dr. Dobb's Journal March 1997
Most people think component software is a good idea. It is easy to obtain impressive results using components. However, only a few people are creating components, because it is just too hard. The fitness gurus on television say, "No pain, no gain." My creed is, "No pain...No siree Bob!" Although I have written ActiveX controls using C++, it isn't much fun.
All of this changes with the release of Microsoft's Visual Basic 5, which lets you create controls as easily as you currently create form-based applications. VB5 can create ActiveX controls that, in theory, can be used in VB, web pages, C++, Delphi, PowerBuilder, or any other environment that understands ActiveX controls.
If you are already a VB programmer, you'll have little or no trouble creating controls. If you haven't tried VB, it isn't difficult to get started. Also, Microsoft has introduced a new user interface that is more comfortable for programmers accustomed to the Microsoft Developer's Studio program. If you prefer the traditional VB interface, however, you can still approximate it.
Typical of component software, the fundamental pieces of an ActiveX control are properties, methods, and events. VB5-created ActiveX controls are no different.
To get the most from an ActiveX control, you should carefully plan what properties, methods, and events it will handle. You can tweak things later, but it helps if you have a good idea of what you want to use from the start. The example control I'll show you in this article is a simple scanning bar of lights (see Figure 1). This component exposes four properties:
The control also supports the Tick event, which fires each time the lights change state (that is, once for each period set by Delay). The control doesn't have any methods.
Once you have a plan, you can fire up VB5. From the start-up dialog, select ActiveX Control under the New tab. The program will create an empty project. As Figure 2 shows, the toolbox on the left is familiar enough. At the top right is the project window, which shows a hierarchical list of the projects you have open. VB5 lets you open multiple projects at once. This is especially useful if you have a control in one project and a test program that uses the control in another.
Notice in Figure 2 that the familiar property browser window appears. Beneath that is a description window that describes the selected property. When you create a component, you'll want to control what appears in this window when programmers use your component. Farther down is a layout window that shows where your form will appear when it runs. If you right click on this window, you can create grid lines to show the common screen resolutions and select options for where your forms will appear when the program runs.
In the center of the screen, you'll see windows that hold forms and Basic code. By default, the code window shows you all the code at once and draws lines between sections. If you prefer the old style, click the small button in the bottom-left corner of the code window. You can easily move, resize, or hide any of the windows or toolbars. In short, you can customize everything.
To create the LEDBAR control (the scanning lights), I used the normal Basic shape component. Each of the 20 lights is a rectangle shape. I created the first one, copied it to the clipboard, and pasted it to form a control array. I then pasted it 19 more times to complete the array. By using a control array, I can refer to each light as an element in an array (or collection, if you prefer). The array's name is LED0 and the elements range from 0 to 19.
Obviously, the control needs a timer. Each time the timer expires, the control should turn off the current light and then turn on the light to the right or left of the current light (depending on the setting of the Direction flag).
You could write code to take care of the logic. However, you'll need some of the properties (Direction, for example) that don't yet exist. To define properties, methods, and events, you'll use the Interface Wizard (from the Add-Ins menu). This wizard (see Figures 3-6) allows you to select members (that is, properties, methods, and events) that many controls support (Figure 3). You can also create custom members (Figure 4). On the next screen (Figure 5) you attach the members to corresponding members in the components contained by the control. I attached the Delay property directly to the timer component and the ForeColor property to the UserControl component (UserControl corresponds to a form in a regular VB program).
At this point, it is tempting to attach the Tick event to the timer's Timer event. You can do this as long as you haven't already put a Timer handler in the code. If you have an existing handler, the Wizard will write an extra event handler that causes a compile error.
On the final wizard screen (Figure 6) you can define any unattached members. The description text you define here will appear in the design environment (below the property browser in Figure 2). You can also specify types, properties, and the like. The wizard creates variables for your unattached properties. For example, the Direction property causes the wizard to create a variable named m_Direction. Methods get skeletal function definitions that you must complete. The wizard also handles events. Of course, any members that you connect to other components don't show up on this screen.
After you complete this wizard, all of your external members are complete. Of course, you'll need to write code that handles any custom methods, and fire any custom events at the appropriate time. You also have to write all the other code that makes your control work.
The code to handle the light bar is fairly simple. A Timer event handler cycles the lights based on the m_Direction flag; see Listing One The complete source code and related files are available electronically; see "Availability," page 3. Also, the UserControl_Initialize event takes care of some setup issues. If you attached the Tick event to the Timer control, you already have a Timer handler that contains the line RaiseEvent Tick. Simply add your code in the same handler. If you didn't hook up the Tick event, you can now create a Timer handler and add the FireEvent line to raise your custom event.
With those handlers in place, the control will work as advertised. However, you have to make the control the exact size of the 20 shape controls. Not very programmer friendly. To improve the behavior, add a UserControl_Resize event handler to dynamically resize the shape controls.
To calculate the new size and position for each shape control, take the ScaleWidth of the UserControl object and round it so that it is divisible by 20 (the number of shape controls). Then divide it by 20. Given the width of each control, it is a simple matter to decide where the left edge of each control should be. You can set the height of the shape controls to the UserControl's ScaleHeight property.
To wrap it up, set the ToolboxIcon property to a bitmap so that your control shows up with your choice of pictures in toolboxes. You can also edit the project properties and change the name of the control.
If you want a property sheet for the control, run the property sheet wizard. It will allow you to select a standard page to select colors (the foreground color in this case). You can also define custom pages and place properties (like Direction and Hold) on them. The wizard automatically creates appropriate forms that you can customize.
Once you have finished, you can easily test the control. Simply select Add Project from the File menu. When prompted, tell the environment that you want an .EXE file project. If you close the control's form you'll see the control appear in the toolbox. Grab it and place it on the form as you would any other component. Use the object browser to set the properties and double click the control to write event handlers.
If you want to use the control in a regular project, generate an OCX file (you'll find the choice on the File menu). Then you'll have to add the component to your toolbox using the Components command on the Project menu. Once you add it to the toolbox, you are ready to go. Other environments may require you to register your control. That's no problem since VB5 automatically generates self-registration code. You only need to run REGSRVR32.EXE and specify the OCX file that contains your control.
What happens if you want to distribute your control to other users? VB5 provides a special tool to create distributions. You run this tool directly from the Start menu (or Program Manager if you are using NT 3.51). It allows you to create setup files for your project, or files appropriate to download over the Internet. You simply tell it what project you want to distribute and it does the rest.
All this sounds too good to be true, right? Well, there is one catch. Controls created with this beta require a special run-time DLL before they can run. If you are using your control with conventional programs, that isn't a big problem. However, if you want users to download your control over the Internet, it could be annoying. The good news is that once users download the DLL once, they don't have to do it again. Then, they only need to download your control.
Speaking of the Internet, you can set up asynchronous properties using VB5. This advanced technique allows you to load a large property (perhaps a picture) asynchronously over the network.
OLE controls are usable almost everywhere. Now they are easy to create too. Look for a major influx of components along with the release of VB5. Give it a try. You can be writing your own components in no time at all.
DDJ
VERSION 5.00Begin VB.UserControl LedBar
BackColor = &H00FFFFFF&
ClientHeight = 432
ClientLeft = 0
ClientTop = 0
ClientWidth = 4788
FillColor = &H00FFFFFF&
PropertyPages = "ledbar.ctx":0000
ScaleHeight = 432
ScaleWidth = 4788
ToolboxBitmap = "ledbar.ctx":001A
Begin VB.Timer Timer1
Interval = 125
Left = 4212
Top = 0
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 19
Left = 4560
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 18
Left = 4320
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 17
Left = 4080
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 16
Left = 3840
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 15
Left = 3600
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 14
Left = 3360
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 13
Left = 3120
Top = 0
Width = 252
End
Begin VB.Shape LED0
BackColor = &H00000000&
FillStyle = 0 'Solid
Height = 492
Index = 12
Left = 2880
Top = 0
Width = 252
End
Begin VB.Shape LED0
BackColor = &H00000000&
FillStyle = 0 'Solid
Height = 492
Index = 11
Left = 2640
Top = 0
Width = 252
End
Begin VB.Shape LED0
BackColor = &H00000000&
FillStyle = 0 'Solid
Height = 492
Index = 10
Left = 2400
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 9
Left = 2160
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 8
Left = 1920
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 7
Left = 1680
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 6
Left = 1440
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 5
Left = 1200
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 4
Left = 960
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 3
Left = 720
Top = 0
Width = 252
End
Begin VB.Shape LED0
FillStyle = 0 'Solid
Height = 492
Index = 2
Left = 480
Top = 0
Width = 252
End
Begin VB.Shape LED0
BackColor = &H00000000&
FillStyle = 0 'Solid
Height = 492
Index = 1
Left = 240
Top = 0
Width = 252
End
Begin VB.Shape LED0
BackColor = &H000000FF&
FillColor = &H000000FF&
FillStyle = 0 'Solid
Height = 492
Index = 0
Left = 0
Top = 0
Width = 252
End
End
Attribute VB_Name = "LedBar"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = True
Attribute VB_Ext_KEY = "PropPageWizardRun" ,"Yes"
Dim n As Integer
'Default Property Values:
Const m_def_Direction = True
Const m_def_Hold = False
'Const m_def_ForeColor = 255
'Property Variables:
Dim m_Direction As Boolean
Dim m_Hold As Boolean
'Dim m_ForeColor As OLE_COLOR
'Event Declarations:
Event Tick()
'Event Timer() 'MappingInfo=Timer1,Timer1,-1,Timer
Private Sub Timer1_Timer()
If m_Hold = True Then Exit Sub
LED0(n).FillColor = vbBlack
If m_Direction Then
If n = 19 Then n = 0 Else n = n + 1
Else
If n = 0 Then n = 19 Else n = n - 1
End If
LED0(n).FillColor = UserControl.ForeColor
RaiseEvent Tick
End Sub
Private Sub UserControl_Initialize()
n = 0
LED0(0).FillColor = UserControl.ForeColor
Hold = False
End Sub
Private Sub UserControl_Resize()
For i = 0 To 19
LED0(i).Width = (ScaleWidth \ 20) * 20 / 20!
LED0(i).Height = ScaleHeight
LED0(i).Top = 0
LED0(i).Left = i * (ScaleWidth \ 20) * 20 / 20!
Next i
End Sub
''WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
''MappingInfo=UserControl,UserControl,-1,BackColor
'Public Property Get BackColor() As OLE_COLOR
' BackColor = UserControl.BackColor
'End Property
'Public Property Let BackColor(ByVal New_BackColor As OLE_COLOR)
' UserControl.BackColor() = New_BackColor
' PropertyChanged "BackColor"
'End Property
'Public Property Get ForeColor() As OLE_COLOR
' ForeColor = m_ForeColor
'End Property
'Public Property Let ForeColor(ByVal New_ForeColor As OLE_COLOR)
' m_ForeColor = New_ForeColor
' PropertyChanged "ForeColor"
'End Property
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=Timer1,Timer1,-1,Interval
Public Property Get Delay() As Long
Attribute Delay.VB_Description = "Returns/sets number of milliseconds between
calls to a Timer control's Timer event."
Delay = Timer1.Interval
End Property
Public Property Let Delay(ByVal New_Delay As Long)
Timer1.Interval() = New_Delay
PropertyChanged "Delay"
End Property
'Initialize Properties for User Control
Private Sub UserControl_InitProperties()
' m_ForeColor = m_def_ForeColor
m_Hold = m_def_Hold
m_Direction = m_def_Direction
End Sub
'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
' UserControl.BackColor = PropBag.ReadProperty("BackColor", &HFFFFFF)
' m_ForeColor = PropBag.ReadProperty("ForeColor", m_def_ForeColor)
Timer1.Interval = PropBag.ReadProperty("Delay", 125)
UserControl.ForeColor = PropBag.ReadProperty("ForeColor", &H80000012)
m_Hold = PropBag.ReadProperty("Hold", m_def_Hold)
m_Direction = PropBag.ReadProperty("Direction", m_def_Direction)
End Sub
'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
' Call PropBag.WriteProperty("BackColor", UserControl.BackColor, &HFFFFFF)
' Call PropBag.WriteProperty("ForeColor", m_ForeColor, m_def_ForeColor)
Call PropBag.WriteProperty("Delay", Timer1.Interval, 125)
Call PropBag.WriteProperty("ForeColor", UserControl.ForeColor, &H80000012)
Call PropBag.WriteProperty("Hold", m_Hold, m_def_Hold)
Call PropBag.WriteProperty("Direction", m_Direction, m_def_Direction)
End Sub
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=UserControl,UserControl,-1,ForeColor
Public Property Get ForeColor() As OLE_COLOR
Attribute ForeColor.VB_Description = "Returns/sets foreground color used to
display text and graphics in an object."
ForeColor = UserControl.ForeColor
End Property
Public Property Let ForeColor(ByVal New_ForeColor As OLE_COLOR)
UserControl.ForeColor() = New_ForeColor
PropertyChanged "ForeColor"
End Property
Public Property Get Hold() As Boolean
Attribute Hold.VB_Description = "Set to TRUE to freeze LEDs"
Hold = m_Hold
End Property
Public Property Let Hold(ByVal New_Hold As Boolean)
m_Hold = New_Hold
PropertyChanged "Hold"
End Property
Public Property Get Direction() As Boolean
Attribute Direction.VB_Description = "True for left to right,
False right to left"
Direction = m_Direction
End Property
Public Property Let Direction(ByVal New_Direction As Boolean)
m_Direction = New_Direction PropertyChanged "Direction"
End Property