Visual Basic Concepts

Creating Lightweight Controls

Lightweight controls, sometimes referred to as windowless controls, differ from regular controls in one significant way: They don't have a window handle (hWnd property). Because of this they use fewer system resources, making them ideal for Internet applications, distributed applications, or any application where system resources may be a limiting factor. Examples of lightweight controls include the Label control and the Image control.

When designing a user control, you should consider creating a lightweight control unless your requirements meet one or more of the following criteria:

  • Your control will contain constituent controls for which lightweight versions aren't available. A lightweight control can only contain other lightweight controls.

  • Your control will act as a design-time container for other controls. Because you can't predict what kind of controls a developer may place on your control, it can't be a lightweight control.

  • Your control needs to make calls to the Windows API that require a window handle (hWnd) as an argument. A lightweight control doesn't have an hWnd property; it returns the hWnd of its container, which may not produce the desired results.

  • Your control needs to implement the BorderStyle property or the EditAtDesignTime property. These properties are disabled when the Windowless property is set to True.

You create a lightweight user control by setting the Windowless property to True at design time. Lightweight user controls can only contain other lightweight controls: If you attempt to place a windowed control on a lightweight user control, you will get a design-time error. The same is true if you attempt to set the Windowless property to True on a user control that already contains windowed controls.

Not all containers support lightweight controls. When your lightweight user control runs in a host that doesn't support it, it will automatically run in windowed mode. The system will assign it an hWnd on the fly. Hosts that are known to support lightweight controls include Visual Basic 4.0 or later, Internet Explorer 4.0 or later, and Visual Basic for Applications.

Creating Lightweight Controls with Transparent Backgrounds

Lightweight controls expose several new properties and a new event that are useful in creating controls with transparent backgrounds. With a windowed control, the MaskPicture and MaskColor properties are used to determine if a mouse click occurs within a valid area of the control's window; if not, the event is routed to the form. Because a lightweight control doesn't have a window, it's up to the control to determine what should receive the click event. The HitTest event provides notification when the cursor is over your control; you control the behavior of the HitTest event by setting a combination of several properties.

Control Regions

To understand how a lightweight control manages hit testing, you must first be familiar with the possible regions of a user control. A control has four possible regions:

  • Mask region – the painted (nontransparent) area on the control, including the union of all contained controls or any areas on a user-drawn control that have been drawn upon.

  • Outside region – the area outside of the mask area.

  • Transparent region – the area inside a mask region that isn't painted — for example, the area inside a circle. This area is determined by the developer of the control.

  • Close region – the area around the edges of the mask region. The width of the close region is determined by the control developer.

Figure 9.8

The actual composition of the mask region is dependent upon the setting of the BackStyle property:

  • If the BackStyle is Transparent, the MaskPicture and MaskColor properties plus any constituent controls make up the mask region. The transparent region consists of any areas that match the MaskColor.

  • If the BackStyle is Opaque, the entire surface of the control is the mask region; there is no transparent region.

Visual Basic doesn't know about the Transparent and Close regions: You have to define these regions yourself and test for them in the HitTest event.

The HitTest Event

The HitTest event is fired whenever the cursor is over a lightweight user control when the BackStyle is set to Transparent. You can add code to the HitTest event to determine whether mouse messages will be received by your control or passed on to the next object beneath it in the ZOrder. The HitTest event fires before any other mouse messages.

Because it's possible to layer several controls at design time, portions of other controls may be visible beneath the transparent areas of your control. In some cases, you might want to test for the existence of another control under your control. If one exists, you might allow it to receive the mouse events; if not, you might want to handle the mouse events yourself. The HitTest event gives you a second (and third) chance to handle the events if there is nothing underneath or if the controls underneath decline the events.

The HitTest event takes three arguments: the x and y coordinates specifying the location of the cursor, and a HitResult argument that corresponds to a control region. There are four possible settings for HitResult:

  • 3 (constant vbHitResultHit) – the cursor is over the mask region of the control; the control receives all mouse messages.

  • 2 (constant vbHitResultClose) – the cursor is over the close region of the control; mouse messages are ignored until the second pass.

  • 1 (constant vbHitResultTransparent) – the cursor is over the transparent region of the control; mouse messages are ignored until the third pass.

  • 0 (constant vbHitResultOutside) – the cursor is over the outside region the control; mouse messages are always ignored.

Hit testing is performed in the following order for layered controls:

  • A first pass is made through the ZOrder; the topmost control returning a HitResult of 3 (Hit) receives mouse messages.

  • If no control returns a HitResult of 3, a second pass is made; the topmost control returning a HitResult of 2 (Close) receives the mouse messages.

  • If no control returns a HitResult of 2, a third pass is made; the topmost control returning a HitResult of 1 (Transparent) receives the mouse messages.

  • If no control returns a HitResult of 1, the underlying container receives the mouse messages.

There's just one problem with this — the HitTest event will never return a HitResult of 1 or 2 because Visual Basic doesn't know about the close or transparent regions. You need to define these regions yourself in code; then if the X and Y coordinates fall within your defined region, you change the HitResult accordingly. The following code shows a HitTest event for a control with a square "hole" in its center:

Private Sub MyControl_HitTest(X As Single, _
   Y As Single, HitResult As Integer)

   ' Determine if the X,Y coordinates fall
   ' within the Close region.
   If (X > 200 And X < 210) Or (X > 390 And X < 400) Then
      If (Y > 200 And Y < 210) Or (Y > 390 And Y < 400) Then
         ' Coordinates are within the Close region.
         HitResult = vbHitResultClose
         ' We got a hit, so we can exit.
         Exit Sub
      End If
   End If

   ' Now check for the Transparent region.
   If (X > 210 And X < 390) Then
      If (Y > 210 And Y < 390) Then
         ' Coordinates are within the Transparent region.
         HitResult = vbHitResultTransparent
      End If
   End If
End Sub

In the above example, we first test to see if the coordinates fall within our Close region, in this case within 10 twips of the edge of our square hole. If so, we change the HitResult to 2 (vbHitResultClose) and exit. If not, we test to see if the coordinates are within the Transparent region, and if so we change the HitResult to 1 (vbHitResultTransparent). If neither test returns a hit, we go with the HitResult that was passed in.

Of course, in the case of a square region, it's fairly easy to determine the regions in code. As you might imagine, this procedure would be much more complex for a circular region, an irregularly shaped region, or for multiple regions within a control.

There's one more aspect to consider: With complex code being executed in the HitTest event, and with the HitTest event firing repeatedly as the cursor moves over your control, you might suspect that performance might be less than optimal. Your suspicion would be correct.

Performance Considerations for Hit Testing

Hit testing can be a pretty expensive operation in terms of performance. This isn't surprising when you consider what's going on behind the scenes. First of all, Visual Basic must perform calculations to clip your control to the mask defined by MaskPicture and MaskColor. Next, if your control contains other controls or user-drawn graphics, it must also clip around them. Finally, if you've defined Close and Transparent regions in your control, it needs to execute the code to check for those regions. With the HitTest event being fired repeatedly, this process could cause your control to perform very slowly.

Fortunately user controls have two additional properties, ClipBehavior and HitBehavior, that can be set in combination to improve performance. The following table shows the results of different pairs of settings:

  0 ClipBehavior None 1 ClipBehavior Use Region
0 HitBehavior None Hit test always returns 0 - no clipping. Hit test always returns 0 - clipped to mask region.
1 HitBehavior Use Region Hit test returns 0 or 3 - no clipping. (Default) Hit test returns 0 or 3 - clipped to mask region.
2 HitBehavior Use Paint Hit test returns 0 or 3 - clipped to paint. Hit test returns 0 or 3 - clipped to paint inside mask region.

When HitBehavior is set to 0 (vbHitBehaviorNone), the HitTest event will always return a HitResult of 0 (Outside). You can add code to determine where the hit occurred and change the HitResult accordingly. This may improve performance if there isn't a lot of code in the HitTest event procedure.

Setting ClipBehavior to 0 (vbClipBehaviorNone) can often improve performance because Visual Basic doesn't need to determine the boundaries between the Mask and Outside regions. This is especially true if the Mask region is complex — for instance, when it includes TrueType® fonts or irregular shapes.

If your control will use hot spots (in other words, only portions of the visible area will accept clicks), setting ClipBehavior to 0 and HitBehavior to 1 (vbUseRegion) can improve performance.

Setting HitBehavior to 2 (vbUsePaint) may be necessary for user-drawn controls where the visible portion of the control isn't the same as the Mask region. This is the most expensive operation because the control needs to repaint each time the HitTest event is executed.

Important   When a lightweight user control is sited on a container that doesn't support the Windowless property, some HitBehavior and ClipBehavior settings may cause unpredictable results. If your control will be used in containers that don't support lightweight controls, you should use the default settings.