This chapter steps you through a basic control project without going into too much detail about why things are the way they are. (We'll go back and cover each topic in detail later.) The objective is to familiarize you with how control development works in Visual Basic and to get you comfortable with the various terms and techniques you'll use on a regular basis.
The control you'll build in this chapter is designed to notify a company's employees of special events. The control could be deployed in either an executable application or a Web page. It is comprised of a graphical component and a textual component, combining the functionality of the standard Windows PictureBox and Label controls.
Start by creating a control project. To do this:
Figure 3.1 : New Project dialog box.
You're now in the Visual Basic IDE window (see Figure 3.2). On the right side is the Project Explorer, an outline list of all the files in your project. (If you can't see the Project Explorer, choose the menu command View, Project Explorer or use the keystroke shortcut Ctrl+R.)
Figure 3.2 : Visual Basic IDE.
You'll want to make sure that a few other Visual Basic windows are open before you start working. The Toolbox enables you to manipulate existing controls, as well as the control you're working on. To make the Toolbox visible, use the menu command View, Toolbox.
The Immediate window enables you to type in Visual Basic code while your project is running. This can be a great help when you're testing and debugging your code. To display the Immediate window, use the menu command View, Immediate Window or use the keystroke shortcut Ctrl+G.
Finally, for the purposes of this walk-through, you'll want to change the Visual Basic development environment from the default MDI to SDI. To do this (if you haven't done it already):
Figure 3.3 : Advanced Options dialog box.
The Project Explorer currently contains two files: the project file, called Project1, and a new control, currently called UserControl1. Start by changing the names of these objects to something more meaningful. To change the name of the project:
Figure 3.4 : Renamed project.
Now give the control a more meaningful (and festive) name. To do this:
When you're done, the Project Explorer will look as in Figure 3.5.
Figure 3.5 : Renamed control.
On the screen you'll see a window containing a nondescript gray rectangle. This is known as the control designer. The control designer is the visual representation of the control, analogous to a form in a Visual Basic EXE project.
Every control you create using Visual Basic has a designer-even controls designed to be invisible at runtime have them.
The HappyHour control you'll create will be comprised of two standard Windows controls: a PictureBox control and a Label control. The Label control will act as a caption for the graphic that appears in the PictureBox control. To add these controls to the form designer:
Figure 3.6 : PictureBox control in Toolbox.
|HappyHour.ctl||The control designer|
|HappyHourProject.vbp||The project file|
Later you'll add more files to the project and re-save them. The project file keeps references to all the files in your projects, so when you quit and restart Visual Basic, you won't have to re-load all the files in your project one at a time.
The visual design of this control is done for now. Close the HappyHour designer by clicking on its close button. The close buttons for the control designers-in both the MDI and SDI interfaces-are shown in Figures 3.7 and 3.8.
Figure 3.7 : Close button (SDI interface).
Figure 3.8 : Close button (MDI interface).
Even though your control technically doesn't do anything at this point, you can still instantiate it on a form and view its visual interface.
The problem, though, is that ActiveX control projects can't be run like conventional Visual Basic programs can; they can only be run in the context of a container. A container can be a form in a Visual Basic executable, an HTML Web page displayed in Microsoft Internet Explorer, or another application that supports ActiveX controls.
In order to test your controls without having to mess around with a completely different development environment, Visual Basic gives you the ability to insert an EXE project into your control project. Forms in an EXE project are identical to Visual Basic forms; they just can't be compiled into an actual EXE file under VB. To insert an EXE project into your current project:
You can't insert a control onto a form if that control's designer is open. When a control's designer is open, the control's icon in the Toolbox will appear disabled. To close the control designer, click on its close button.
Figure 3.9 : HappyHour control in the Toolbox.
Even though you haven't written any code yet, the HappyHour1 control already has a rudimentary set of properties and events. By looking at the Properties window, you can see that HappyHour1 has Height, Width, and Visible properties, as well as GotFocus and LostFocus events, among others. These properties and events are not really a part of your control, but rather passed through from the container in which your control resides. You can see these properties by looking in the Properties window, as illustrated in Figure 3.10.
Figure 3.10: Properties window of the HappyHour control.
I'll discuss more about the relationship between controls and containers in Chapter 7
Although you now have a basic control with a few properties and events, this isn't nearly enough. For one thing, if you resize HappyHour1 and make it smaller, the Label and PictureBox controls inside can easily get lost. And nothing makes a constituent control more angry, believe me.
For another thing, the important properties of the controls within HappyHour1 (such as the Caption property of the Label and the Picture property of the PictureBox control) can't be accessed.
We'll spend the rest of this chapter rectifying this situation. Start by closing Form1 by clicking on its close button, then returning to the control designer for HappyHour through the Project Explorer.
The easiest way to implement a property in a control you create is to use an existing property of another control. Controls included in your controls are called constituent controls; the process of hijacking their properties and events is referred to as delegation. In this case you'll delegate the Caption property of the constituent Label control to provide a Caption property for the HappyHour control.
To give the HappyHour control a Caption property, do the following:
Figure 3.11: The Add Procedure window.
Figure 3.12: Code window with Property Get and Property Let.
To cause the label's Caption property to be read whenever the HappyHour control's Caption property is accessed, modify the Property Get procedure like this:
Public Property Get Caption() As String Caption = lblCaption.Caption End Property
Note that you must change the first line of the procedure to be
defined As String instead of As Variant, since captions should
be treated as strings.
Remember, you can toggle between Full Module view and Procedure view by using the buttons in the lower left corner of the code window.
|Property Get versus Property Let|
The concepts of Property Get and Property Let were first introduced in Visual Basic 4.0. For some reason, when this feature was first introduced, I had trouble remembering that Property Get was the code that was run when the property was read, and Property Let was the code that was run when the property was altered. If you have this problem, too, and you're an old hand with Basic, it might help you to remember that Let is a Basic keyword from the old days of Basic programming. The code was the way you set the variable x to the value 5. So it is with properties, thereby proving that everything old is new again.
To know when to use Property Let vs Property Set, it helps to remember that assigning a value to an object variable always requires a Set statement; similarly, properties that represent objects (such as the Picture property of a PictureBox control) require a Property Set instead of a Property Let.
Let x = 5
Property Let is the procedure that runs when the value of a property is set or changed. To cause the label's Caption property to pass through to the HappyHour control's Caption property, alter the Property Let procedure so it looks like this:
Public Property Let Caption(ByVal NewCaption As String) lblCaption.Caption = NewCaption PropertyChanged "Caption" End Property
Note that the data type of the Property Let procedure was changed from Variant to String, to reflect the fact that captions are strings.
PropertyChanged looks like a statement, but it's actually a method of your control's container. The method sends a message to the container, informing it that a property of your control has been changed. This flags the container to trigger a WriteProperties event, which saves any design-time changes to this property that you make. (We'll deal with the WriteProperties event again later on.)
To test that the HappyHour control's new Caption property works properly, you can go back to the instance of your HappyHour control that exists on Form1. To do this:
Look in the Properties window. You should be able to see that there is now a Caption property available for HappyHour1. Try changing the property by typing something else in the Caption property. Your control should look something like Figure 3.13.
Figure 3.13: Caption property for HappyHour1.
We're finished exposing the HappyHour control's Caption property. We'll expose the Picture property of the PictureBox control through the HappyHour control later. For now, there's another problem to solve: resizing.
When HappyHour1 is resized, the two controls contained in it aren't resized along with it. This can cause the Label or the PictureBox to become lost if the control is made too small. And we can't have that. To rectify this, we can insert code that will cause the components of the HappyHour control to resize themselves. To do this:
Private Sub UserControl_Resize() 'To make this more concise as well as faster executing, 'You can also use a Move method instead of setting 'Width and Height separately. I did it this way for clarity. Picture1.Width = UserControl.ScaleWidth Picture1.Height = (UserControl.ScaleHeight / 3) * 2 lblCaption.Width = UserControl.ScaleWidth lblCaption.Height = UserControl.ScaleHeight / 3 lblCaption.Top = Picture1.ScaleHeight + 60 End Sub
This code will ensure that the width and height of the HappyHour
control will always be slightly less than the overall width of
In the preceding code example, you use the ScaleWidth and ScaleHeight properties of the UserControl because you don't know whether the user will reset the ScaleMode property to a measurement value other than the standard Visual Basic unit of measurement (the twip, equal to 1/1440th of an inch). Your control doesn't have a ScaleMode property yet, but writing code like this protects you in case you ever decide to give it one.
You can see how this code works if you close the control designer, return to Form1, and stretch the control HappyHour1. You should be able to see that the PictureBox and the Label stretch proportionately.
Controls that have a Caption property should supply a default value for their captions. The default caption for a CommandButton is Command1, the default caption for a Label is Label1, and so forth. You can supply the same functionality in your control by reading a property of the Extender object in your UserControl's InitProperties event.
Extender properties are supplied by the container in which your constituent control resides. So if you set the default Caption property of the HappyHour control to be equal to Extender.Name, you can achieve the effect that users expect. To do this:
Private Sub UserControl_InitProperties() Caption = Extender.Name End Sub
Figure 3.14: HappyHour control with default caption set.
You've probably noticed that the changes you've been making to the HappyHour1's Caption property have vanished into the ether each time you flip between the control designer and Form1-specifically, the Caption property has been wiped out each time you make a change to the control designer.
You can make design-time property settings persistent by writing code in the WriteProperties event of the UserControl. The WriteProperties event is triggered each time the user's design-time property settings need to be saved.
Property settings are made persistent by the use of the PropertyBag object. You have the ability to put data into the PropertyBag and take data out of the PropertyBag. The WriteProperty method of the PropertyBag object stores a property in the PropertyBag, while the ReadProperty method does the opposite. Either way, Visual Basic is left holding the PropertyBag, which is a good thing.
To make your control save its design-time Caption property, do the following:
Private Sub UserControl_WriteProperties(PropBag As PropertyBag) PropBag.WriteProperty "Caption", Caption, Extender.Name End Sub
Conversely, just as you must write code to save a design-time property change to the PropertyBag, you must also write code to retrieve previously set properties from the PropertyBag in your control's ReadProperties event. To do this, add the following code to your code window:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag) Caption = PropBag.ReadProperty("Caption", Extender.Name) End Sub
The value for Extender.Name is a parameter to the WriteProperty and ReadProperty methods so those methods can know whether the design-time properties have been changed by the user. If the properties haven't been changed (that is, they're equal to Extender.Name) then there's no need to write a specific entry to the PropertyBag. This makes the process of managing saved property settings more efficient.
Your control should now have the ability to retain the properties set for it at design time. To test this:
Now that you know how things work under the hood, try exposing a property of a constituent control the easy way, using the ActiveX Control Interface Wizard. This tool takes care of much of the busywork involved in control creation, particularly when your control is primarily comprised of one or more constituent controls.
You can use the ActiveX Control Interface Wizard to delegate a Picture property for your control to the constituent PictureBox control. To do this:
The wizard now goes to work, writing the code that will support the property mapping you asked for. When it is done writing code, the wizard displays a summary reminding you of what you need to do to complete your control project (see Figure 3.21). Note this information, then click on Close.
Figure 3.21: Interface Wizard Summary.
Open the HappyHour control and double-click on its designer to view its code. You should be able to see that the following code has been added to your project:
Private Sub UserControl_WriteProperties(PropBag As PropertyBag) PropBag.WriteProperty 'Caption&', Caption, Extender.Name Call PropBag.WriteProperty("Picture", Picture, Nothing) End Sub Private Sub UserControl_ReadProperties(PropBag As PropertyBag) Caption = PropBag.ReadProperty("Caption", Extender.Name) Set Picture = PropBag.ReadProperty("Picture", Nothing) End Sub
The WriteProperty and ReadProperty methods inserted by the wizard are essentially identical to the code you wrote for the Caption property; the wizard just uses an alternate syntax.
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES! 'MappingInfo=Picture1,Picture1,-1,Picture Public Property Get Picture() As Picture Set Picture = Picture1.Picture End Property Public Property Set Picture(ByVal New_Picture As Picture) Set Picture1.Picture = New_Picture PropertyChanged "Picture" End Property
The only really new thing here is the Property Set procedure. Property Set (rather than Property Let) is required here because the Picture property is an object, rather than a conventional value.
This code enables you to include a picture in your control at design time. To test this, you'll need some graphics file on your hard disk.
Support for GIF and JPEG images is a new feature in Visual Basic 5.0.
Figure 3.22: The finished product.
In this chapter you stepped through the creation of a very simple control. You added properties using two methods: manually, by writing code, and automatically, using the ActiveX Control Interface Wizard.
The next few chapters will discuss control properties, events, and methods in more detail. This discussion will enable you to create a richer, more full-featured control.