Transparent Controls
Visual Basic offers you
many ways to create irregularly shaped controls. To begin with, if you set the
BackStyle property of the UserControl object to 0-Transparent, the background
of the control-that is, the portion of the control that isn't occupied by constituent
controls-becomes transparent and lets the user see what's behind the control
itself. When a control has a transparent background, all the mouse events go
directly to the container form or to the control that happens to be under the
ActiveX control in the z-order. In addition, Visual Basic ignores the BackColor
and Picture properties for such an ActiveX control and all the output
from graphic methods is invisible. Not surprisingly, transparent controls are
also more demanding in terms of CPU time because, while repainting, Visual Basic
has to clip all the areas that don't belong to the controls.
Using Label
and Shape controls
If your transparent control
includes one or more Label controls that use a TrueType font and whose BackStyle
property is also set to 0-Transparent, Visual Basic clips all the pixels
around the characters in the Label. Only the caption of the Label is considered
to belong to the ActiveX control, and all the other pixels in the Label are transparent.
For example, if you click inside a letter Oin the caption, a Click
event is raised in the parent form or in the control that shows through.
I noticed that this feature works decently only with larger font sizes, however.
You can create a large variety
of nonrectangular controls using Shape controls as constituent controls. (You
can see one example on the companion CD.) If you set the Shape control's BackStyle
property to 0-Transparent, all the pixels that fall outside the Shape control
are transparent. For example, to create an elliptical radio button, you drop
a Shape1 constituent control, set its Shape property to 2-Oval, and set
both the UserControl's and Shape control's BackStyle property to 0-Transparent.
Then you need only some code that resizes the Shape control when the UserControl
resizes and that refreshes the control's appearance when the Value property
changes. Following is a partial listing for the UserControl code module.
' Change the color when the control is clicked.
Private Sub UserControl_Click()
Value = True
RaiseEvent Click
End Sub
Private Sub UserControl_Resize()
Shape1.Move 0, 0, ScaleWidth, ScaleHeight
End Sub
Public Sub Refresh()
' TrueColor and FalseColor are Public properties.
Shape1.BackColor = IIf(m_Value, TrueColor, FalseColor)
Shape1.FillColor = Shape1.BackColor
End Sub
' Value is also the default property.
Public Property Get Value() As OLE_OPTEXCLUSIVE
Value = m_Value
End Property
Public Property Let Value(ByVal New_Value As OLE_OPTEXCLUSIVE)
m_Value = New_Value
Refresh
PropertyChanged "Value"
End Property
The problem with using Shape
controls to define irregularly shaped controls is that you can't easily use graphic
methods to draw over them. The reason is that Visual Basic redraws the Shape
control after raising the Paint event, so the Shape control covers the
graphic you've produced in the Paint event. An easy way to work around
this limitation is to activate a Timer in the Paint event and let the
drawing occur in the Timer's Timer procedure, some milliseconds after
the standard Paint event. Use this code as a guideline:
Private Sub UserControl_Paint()
Timer1.Interval = 1 ' One millisecond is enough.
Timer1.Enabled = True
End Sub
Private Sub Timer1_Timer()
Timer1.Enabled = False ' Fire just once.
' Draw some lines, just to show that it's possible.
Dim i As Long
For i = 0 To ScaleWidth Step 4
Line (i, 0)-(i, ScaleHeight)
Next
End Sub
|
As far as I know, the only
other way to solve this problem is by subclassing the UserControl to run some
code after the standard processing of the Paint event. (Subclassing techniques
are described in the Appendix.)
Using the MaskPicture
and MaskColor properties
If the shape of your transparent
control is too irregular to be rendered with one Shape control (or even with
a group of Shape controls), your next best choice is to assign a bitmap to the
MaskPicture property and then to assign the color that should be considered
as transparent to the MaskColor property. The bitmap is used as a mask,
and for each pixel in the bitmap whose color matches MaskColor, the corresponding
pixel on the UserControl becomes transparent. (Constituent controls are never
transparent, even if they fall outside the mask region.) You also need to set
the Backstyle property to 0-Transparent for this technique to work correctly.
Using this process, you
can create ActiveX controls of any shape, including ones that have holes in them.
Probably the only serious limitation of this approach is that you can't easily
create a mask bitmap that resizes with the control because you can assign the
MaskPicture property a bitmap, GIF, or JPEG image, but not a metafile.
Lightweight
Controls
Visual Basic 6 permits you
to write lightweight ActiveX controls that consume fewer resources at run time
and therefore load and unload faster. The UserControl object exposes two new
properties that let you fine-tune this capability.
The HasDC
and Windowless properties
The HasDC property
determines whether the UserControl creates a permanent Windows device context
or uses a temporary device context when the control is redrawn and during event
procedures. Setting this property to False can improve performance on
systems with less memory. For more information about this property, see the "Fine-Tuning
the Performance of Forms" section in Chapter 2.
Setting the Windowless
property to True creates an ActiveX control that doesn't actually create
a window and therefore consumes even fewer resources. A windowless control has
a couple of limitations, however. It must be user-drawn or contain only other
windowless controls, and it can't work as a container for other controls. You
can't place regular constituent controls on a windowless ActiveX control, and
you can't set the Windowless property to True if the UserControl already
includes nonwindowless constituent controls. Image, Label, Shape, Line, and Timer
are the only intrinsic controls that you can place over a windowless UserControl.
If you need features that these controls don't provide, have a look at the Windowless
control library mentioned in the "Limitations and Workarounds" section
earlier in this chapter.
Not all containers support
windowless controls. Among the environments that do are Visual Basic 5 and 6,
Internet Explorer 4 or later, and all the environments based on Visual Basic
for Applications. Interestingly, when a windowless control runs in an environment
that doesn't support this feature, the windowless control automatically turns
into a regular control that's backed up by a real window.
A windowless control doesn't
expose an hWnd property, so you can't call API functions to augment its
functionality. (In some cases, you can use the ContainerHwnd property
instead.) Moreover, the EditAtDesign and BorderStyle properties
are disabled for windowless ActiveX controls. The HasDC property is usually
ignored as well because windowless controls never have a permanent device context.
But you should set this property to False because if the control runs in an environment
that doesn't support windowless ActiveX controls, it won't, at least, use resources
for a permanent device context.
Transparent
windowless controls
You can create a windowless
control that has a transparent background by setting its BackStyle property
to 0-Transparent and assigning a suitable bitmap to the MaskPicture. But
you should also consider the new HitTest event and the HitBehavior
and ClipBehavior properties.
Before I show you how to
use these new members, you need to understand what the four regions associated
with a control are. (See Figure 17-9.) The Mask region is the nontransparent
portion of a control, which includes all the constituent controls and other areas
that contain the output from graphic methods. (In regular controls, this is the
only existing region.) The Outside region is the area outside the Mask
region, while the Transparent region is any area inside the Mask region
that doesn't belong to the control (the holes in the control). Finally, the Close
region is an area that encircles the Mask region and whose width is determined
by the author of the ActiveX control.
Figure 17-9.
The four regions associated with a transparent control.
The problem with managing
mouse actions over a transparent control is that Visual Basic doesn't know anything
about the Close and Transparent regions, and it can only determine whether the
mouse cursor is on the Mask region or in the Outside region. The problem is even
worse when there are multiple overlapping controls, each one with its own Close
or Transparent region, because Visual Basic has to decide which one will receive
the mouse event. To let the control decide whether it wants to manage the mouse
action, Visual Basic fires one or more HitTest events in all the controls
that are under the mouse cursor, in their z-order. (That is, it fires the first
event in the control that's on top of all others.) The HitTest event receives
the x and y coordinates of the mouse cursor and a HitTest argument:
Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
' Here you manage the mouse activity for the ActiveX control.
End Sub
|
The possible values for
HitResult are 0-vbHitResultOutside, 1-vbHitResultTransparent, 2-vbHitResultClose,
and 3-vbHitResultHit. Visual Basic raises the HitTest event multiple times,
according to the following schema:
-
A first pass is made through
the controls from the topmost to the bottommost control in the z-order; if
any control returns HitResult = 3, it receives the mouse event and no
more HitTest events are raised.
-
If no control returns HitResult =
3, a second pass is performed; if any control returns HitResult = 2,
it receives the mouse event and no more HitTest events are raised.
-
If no control returns HitResult
= 2, one more pass is performed; if any control returns HitResult =
1, it receives the mouse event.
-
Otherwise, the parent form or the container
control receives the mouse event.
Since Visual Basic knows
only about the Mask and Outside regions, the value of HitResult that it
passes to the HitTest event can only be 0 or 3. If you want to notify
Visual Basic that your control has a Close or Transparent region, you must do
so by code. In practice, you test the x and y coordinates and assign
a suitable value to HitResult, as shown in the following code:
' A control with a circular transparent hole in it.
Sub UserControl_HitTest(X As Single, Y As Single, HitResult As Integer)
Const HOLE_RADIUS = 200, CLOSEREGION_WIDTH = 10
Const HOLE_X = 500, HOLE_Y = 400
Dim distance As Single
distance = Sqr((X _ HOLE_X) ^ 2 + (Y _ HOLE_Y) ^ 2)
If distance < HOLE_RADIUS Then
' The mouse is over the transparent hole.
If distance > HOLE_RADIUS _ CLOSEREGION_WIDTH Then
HitResult = vbHitResultClose
Else
HitResult = vbHitResultTransparent
End If
Else
' Otherwise use the value passed to the event (0 or 3).
End If
End Sub
Not surprisingly, all these
operations can add considerable overhead and slow down the application. Moreover,
Visual Basic needs to clip the output accounting for the mask defined by MaskPicture
for constituent controls and the output of graphic methods. To keep this
overhead to a minimum, you can modify Visual Basic's default behavior by means
of the ClipBehavior and HitBehavior properties.
The ClipBehavior
property affects how Visual Basic clips the output of graphic methods. The default
value is 1-UseRegion, which means that the output of a graphic method is clipped
to fit the Mask region. The value 0-None doesn't perform clipping at all, and
graphic output is visible also on the Mask and Transparent regions.
The HitBehavior property
determines how the HitResult argument is evaluated before calling the
HitTest event. When HitBehavior = 1-UseRegion (the default value),
Visual Basic sets HitResult = 3 only for points inside the Mask region.
If you set HitBehavior = 2-UsePaint, Visual Basic also considers the points
produced by graphic methods in the Paint event. Finally, if HitBehavior
= 0-None, Visual Basic doesn't even attempt to evaluate HitResult and
always passes a 0 value to the HitTest event.
If your Mask region isn't
complex and you can easily describe it in code, you can often improve the performance
of your ActiveX control by setting HitBehavior = 0-UseNone. In this case,
Visual Basic always passes 0 to the HitResult argument, and you change
it to account for your Mask, Close, and Transparent regions. If the Mask region
is complex and includes irregular figures, you should set ClipBehavior =
0-None, thus saving Visual Basic the overhead needed to distinguish between the
Mask and Outside regions.
You can easily create controls
with hot spots using ClipBehavior = 0-None and HitBehavior = 1-UseRegion.
In practice, you draw your control over its entire client area and use the MaskPicture
property to define the areas that react to the mouse.