Supercharge VB's Forms With ActiveXBuild 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 benefitespecially when it comes to testing.What hasn't changed since VB 1.0 is its formsthey'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.
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 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 directlyit 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:
And
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 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 codeif 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 normaljust no events and no timers. Modify the standard ShowAbout procedure in your control like this:
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 yearsthe Enum statement. It allows you to create an enumerated variable type, such as the Centering property in the XForm control:
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:
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 typesto a point. The stdPicture object is used in the XForm's picture 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 propertiesyou 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 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 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:
This event is raised from one of three methods and also accepts data. Form1 can ping Form2 using the simplest of these methods:
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:
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:
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:
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:
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 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:
The change to the FormType property raises the XForm_BuildScreen event:
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.
|