March 1999

Supercharge VB's Forms With ActiveX

Build a high-powered ActiveX control that gives new life to Visual Basic's forms.

by Matthew B. Butler

Reprinted with permission from Visual Basic Programmer's Journal, Mar 1999, Volume 9, Issue 3, Copyright 1999, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com.

One of the hot capabilities that VB5 and VB6 added to your repertoire is ActiveX control development. But, unlike objects without inheritance and class-only DLLs, you're not hamstrung from the jump. In fact, VB's ability to load more than one project at a time means you can debug your controls along with your application. This is a significant benefit—especially when it comes to testing.

What hasn't changed since VB 1.0 is its forms—they're still just glorified containers. What we really need are forms that can talk to each other, pass data, organize the controls they hold, manage their children, and be configured on the fly.

So, why use a control rather than a class to do these things? First, you can drop a control onto a form without having to instance a class object. Second, controls have persistent properties that classes lack. Persistent properties are handy when you want to include picture or icon properties without carrying the files in the project. Third, controls have inherent container abilities that classes can't easily support.

In this article, I'll discuss some advanced concepts in control development, and I'll show you how to build the XForm control that enhances VB's forms. If you're new to ActiveX controls or to object-oriented programming, you might want to read a primer before diving in (see the Resources box for ideas on where to look).

I'll begin by showing you how to subclass from within a control. If you haven't tried subclassing yet, start with Francesco Balena's article Subclass Forms to Create New Events" [VBPJ January 1998]. Balena teaches you the theory behind subclassing and how to use it.

Like subclassing from a class module, subclassing from within a control leaves you stuck using a BAS module to hold the substitute window procedure. You might think that a BAS module in a control is contained within that control. Unfortunately, it isn't. Every instance of that control uses the variables and functions in the BAS module, which means your subclasser still has to support multiple forms. Maintaining an array of forms that the control has subclassed allows your control to support multiple forms.

But, a shark might be cruising the subclassing waters. Unlike code objects, you have no choice over the order in which a form creates or destroys controls. So, two completely different controls that subclass the same form might not be unloaded in the order that you expect. Because each control has its own global array, neither one knows whether it holds the original window procedure.

 
Figure 1 The Subclassing Mechanism Gone Awry Click here

For example, if the form creates control A first, then B, then C, control A will have the original window procedure. As each control terminates, it replaces the current window procedure with the one it got from Windows. So, unless control A terminates last, the real window procedure won't be restored. This is evil (see Figure 1). I haven't yet found any documentation from Microsoft that explains the order that controls are loaded and unloaded. To be safe, I've added this property to all of my subclassing controls:

Public Property Get FormSubClasser() _
   as Boolean
   FormSubClasser = True
End Property

When my controls subclass a form, they cycle through all the form's controls and check them for this property. If they find more than one (they'll also find themselves in the list of controls), then they know that another control has the original window procedure. As the controls terminate, they only replace the window procedure if they have the original (see Listing 1).

Process Your Form's Messages
With that settled, you have to decide where to process your form's messages. If you put the message-handling functions in the BAS module, you have to write procedures that expose each of your control's properties, methods, and events to the BAS module. This involves a lot of work if you need to access more than just a few items. A better solution is to build the message handlers into the control itself and have your window procedure call them.

Either way, you need access to both the form and your control. The good news is that you can use the built-in UserControl object to do this. You build your control on this platform. It manages the behavior of your control and provides access to the outside world. The bad news is that the BAS module can't see it directly—it will have to be passed in.

This is where the Me keyword saves you trouble. Me references your control and can be passed into the BAS module. But how you pass it makes a world of difference. Consider these two functions where the Me keyword is passed as the parameter:

Public Function Generic(poObject as Object)
   ...
End Function

And

Public Function Specific(poObject as XForm)
   ...
End Function

The poObject parameters do not contain the same object references. The poObject in Generic contains a reference to the control's public interface, while the one in Specific contains a reference to the UserControl object. Friend procedures built into the control are accessible only to the Specific function. This is a vital distinction that allows you to make your message handlers' Friend functions that are encapsulated inside your control. Also, be sure to save the object reference using the same type declaration to preserve the reference correctly.

The final step is to store the UserControl reference using the ObjPtr function. If you don't, you'll create a circular reference between your control and the BAS module that prevents the control's Terminate event from firing. Use the stored reference to call a single message handler in the control when your window procedure receives a message for the form (see Listing 2). This approach means that the subclassing module is nearly generic and can be used in any control.

Building a Better About Box
Although it might seem silly to worry about the effectiveness of the About box, sometimes these silly concerns lead to solutions to more serious problems. It annoys me that the vbModal parameter doesn't make the About boxes modal to the VB Integrated Development Environment (IDE). Controls written in C++ benefit from the Microsoft Foundation Classes (MFC) About class, which lets you create modal About boxes.

Because VB probably won't ever use MFC classes, you'll have to use sleight of hand. You can disable the VB IDE with the EnableWindow API and still execute code. One side effect, however, is that you suspend all the events and timers. The trick is to put VB to sleep as soon as the form is loaded and never stop executing code—if you do, you're hosed. As long as you maintain "a stream of consciousness," you can check for shutdown messages sent to the form and for button hits. The graphics will all still work, so everything looks normal—just no events and no timers. Modify the standard ShowAbout procedure in your control like this:

Public Sub ShowAbout
   On Error Resume Next
   frmAbout.Show
End Sub

You need to trap for errors because the form will unload before this call ever returns.

The About form's Form_Load event calls the WaitForMe procedure (see Listing 3). It subclasses the form, shuts down VB, and loops until the form is closed by the Command menu or through the Close button. You can add animation to the form as long as you don't count on using a timer. The source code for this control illustrates how to do this and is useful for building specialized message or input boxes (for details, see the Download Free Code box at the end of the article).

One of the most powerful features recently added to VB is something that C++ programmers have enjoyed for years—the Enum statement. It allows you to create an enumerated variable type, such as the Centering property in the XForm control:

Public Enum Centering
   [No Centering] = 0
   [Over Parent] = 1
   [On Screen] = 2
End Enum

Ordinarily, you'd use underbars between words, but the brackets around each item allow you to use spaces. In the Property Browser, you'll see a drop-down list that contains these three items. More importantly, a public enum adds its keywords to VB as if they were part of the language. The Centering property's procedure definitions look like this:

Public Property Let Centering( _
   piCenter As Centering)
      'Check for a valid centering
      'argument
   If piCenter < 0 Or piCenter > 2 _
       Then
      Err.Raise 380
   EndIf

   'Save the centering choice
End Property

Public Property Get Centering() As _
   Centering
   Centering = ciCentering
End Property

Enumerated types aren't automatically checked for validity, so you have to do this manually, as shown in the Property Let procedure. Visual Basic does a great of job handling object types—to a point. The stdPicture object is used in the XForm's picture property:

Public Property Set Picture( _
   poPicture as stdPicture)

   'Save the picture object...
End Property
Public Property Get Picture() as _
         stdPicture
   'Retrieve the picture object...
End Property

The Property Browser automatically puts a button in the property field to browse for a standard picture, although you can't specify which type of picture. One possible solution to this is to build your own wrapper class that handles the browser dialog.

You can also use code classes as properties—you just can't make them public. This isn't exactly evil, but it's close. You can work around this problem and get early binding by building the class into your control and including the class module in your project. Although the need for this is limited, it can be useful.

The VB Help doesn't document two property types: OLE_OPTEXCLUSIVE and OLE_TRISTATE. The Property Browser displays the OLE_OPTEXCLUSIVE type as a Boolean. When you place more than one control with OLE_OPTEXCLUSIVE as the default property on a single container, only one control at a time can be true. This allows you to set up controls that work like option buttons. OLE_TRISTATE is an enumerated type that has three states: 0-Unchecked, 1-Checked, and 2-Gray. It's displayed as a drop-down listbox in the Property Browser.

The Procedure Attributes dialog lets you hide control features from the Object Browser, as well as hide properties from the Property Browser. When the data on the form changes, the DataChanged property on the XForm control is used as a flag. Because it's only valid when the application is running, the DataChanged property doesn't belong in the Property Browser. The Don't Show in Property Browser option allows you to keep it from showing up there.

The Procedure ID selection also deserves mention. Standard properties, such as the Appearance property, methods (Refresh), and events (Click), work in VB in a certain way. If you put a Refresh method into your control, you need to give it the appropriate ID. This doesn't change the way the property or method works, but it lets containers know that your Refresh method works as expected.

Playing Nice in the Sandbox
Because ActiveX controls are really DLLs in disguise, they have a base address. You'll find the DLL Base Address on the Compile tab of the Project Properties dialog. This hex number (&H11000000 by default) is the address used when the control is first loaded. If the base address is already used, time is wasted while the DLL loads to a new address. Memory is also wasted because applications load their own DLL image into memory for their own use rather than sharing an instance.

Many developers probably never change the DLL base address. Change yours. Valid addresses range from &H10000000 to &H80000000 and sit on any 64K boundary. The last four digits must be 0000.

Version compatibility is also a significant issue if you plan to distribute your controls. The No Compatibility and Project Compatibility options work fine during development, but are useless otherwise. Binary Compatibility makes Visual Basic check for backward compatibility each time the control is compiled. You'll be warned if that compatibility is broken. If your control has been distributed, pay attention to this. You can easily render another developer's project unusable because something is missing or changed. Rather than eliminating an obsolete function or property, it's better to comment out its code and hide it from the Object and Property Browsers.

The XForm Control
The XForm control incorporates many of the techniques programmers have used for years to enhance their forms. These techniques include: using the Change event on a hidden textbox to pass information, altering the form's dimensions in the Resize event, or looping through the Forms collection to close other forms.

You should put everything into a single control, because it standardizes the interface between forms. Because several developers often work on the same project, this is an enormous time-saver. A standard interface means that forms deal with each other in the same way, so there's no searching for hidden textboxes and no digging for functionality. ActiveX controls also provide backward compatibility through version checking. Take a look at this example of interform communication. Here, the MessageReceived event replaces the hidden textbox:

Private Sub XForm1_MessageReceived( _
   ByVal Message As String, _
   ByVal DataElement As Variant)
...
End Sub

This event is raised from one of three methods and also accepts data. Form1 can ping Form2 using the simplest of these methods:

Dim oObject as object
...
Call Form2.XForm.Ping("Update", _
       oObject)
...

By giving every XForm control the same name, you create a standard interface. If you don't do this, the GetXFormControl method searches the target form for its XForm. Here's the code you would use if you don't use the same name:

Call Form1.XForm.GetXFormControl( _
   Form2).Ping("Update", oObject)

It's faster to use the same names, but this functionality is encapsulated in the MessageForm method anyway (see Listing 4).

MDI forms maintain an internal list of their child forms. If the parent is closed, then the children will be closed too. This is a nice feature for normal forms, even if they are MDI children themselves. Because I use data objects in almost every application, I often have a summary form that lists information from the data objects. From there, users can bring up one or more forms to edit the data. If they close the summary screen before closing the edit screens, I need a way to close the edits automatically.

The XForm control maintains an internal list of child forms called from the parent. Rather than use form references, it holds their window handles to avoid circular references. The NewChild property is called to add the child form's handle to the list. A typical instancing of an edit screen might look like this:

Private Sub Command1_Click()
   Dim oForm as form

   Set oForm = New Form1

   oForm.Show
   Me.XForm1.NewChild = oForm.hWnd
End Sub

When the parent form closes, the DestroyChildren procedure uses the SendMessage API to send each child form the WM_CLOSE message. Because child forms might validate data or otherwise halt their own closing, the GetWindowLong API checks for their demise (see Listing 5).

The XForm control can also handle the process for shutting down a form. Validating data, writing data to a database, and closing child forms have all been rolled into one method. The Form_QueryUnload event can call the UnloadForm method:

Private Sub Form_QueryUnload( _
   Cancel As Integer, _
   UnloadMode As Integer)

   Cancel = XForm1.UnloadForm
End Sub

The UnloadForm method raises the ValidateData and SaveData events where I validate the form's data fields and save them. Code placed in these events can cancel the form's unloading by returning False for Success:

Private Sub XForm1_SaveData( _
   Success As Boolean)
   ...
   Success = False
End Sub
Private Sub XForm1_ValidateData( _
   Success As Boolean)
   ...
   Success = False
End Sub

Assuming that the data is validated and saved successfully, the DestroyChildren method is called to shut down any child screens that this form has created. Note that each child form can follow this same process and cancel the parent form's closing by canceling its own closing.

Back to Subclassing School
When working with objects, I use a lot of listview controls. Rather than make the form fill the screen to show every field, I load the form so it's smaller and allow the user to change the screen size to see more or less data. By subclassing the form, the XForm control can intercept the WM_GETMINMAXINFO message and limit the size of the form. Doing this during subclassing helps to avoid the substantial flicker generated when you do it in the Form_Resize event.

By intercepting the WM_SIZING message and using a simple elastic function, XForm shifts the positions of the controls it contains as the screen grows or shrinks. The XForm control also raises the XForm_ElasticForm so you can make screen-specific changes. Finally, the XForm control handles on-the-fly configuration of the form. I try to make every form do as many tasks as possible (within reason, of course). I set up a default configuration and then switch to another configuration as needed.

Form configuration also solves another problem. I have a library of forms that I use in many different projects. Rather than copying the forms into a new project, I leave them in a common directory and link them into the project. These forms are updated once and can be included in every application simply by recompiling them. However, each application might have a different icon, color scheme, and so on. This is where the XForm_BuildScreen event and Setup method come in. The Setup method is called when the form is first loaded:

Private Sub Command3_Click()
   With Form2
      Call.XForm1.Setup( _
         NewParentForm:=Me, _
         NewFormType:=1)
      .Show
   End With
End Sub

The change to the FormType property raises the XForm_BuildScreen event:

Private Sub XForm1_BuildScreen(_
   ByVal ParentForm As Object, _
   ByVal FormType As String)

   Me.Icon = ParentForm.Icon

   Select Case FormType
      Case "Basic"
         ...
      Case "Advanced"
         ...
      Case Else
   End Select

   ...
End Sub

You can make whatever changes need to be made to the screen's presentation from here.

The XForm control only scratches the surface of how you can enhance forms. Whether you need interform communication or control of a form's behavior, you can do it easily with a single control. It just takes planning and a few tricks.

Fortunately, VB5 and VB6 take much of the agony out of the process. As long as you keep the "gotchas" in mind (see the sidebar, "The Seven Gotchas of ActiveX"), you'll be cranking out high-powered objects by the truckload. With VB's ActiveX control development, you finally get to run with the big dogs.


Matthew Butler is a senior software developer who specializes in object-oriented programming and enterprise applications. Reach him by e-mail at matthewbutler@yahoo.com.