Contrary to the way it may seem, the word lightweight in lightweight
controls refers to their impact on system resources, not their
functionality. Lightweight controls are just like regular controls without
a window handle (hWnd
property).
Instead, Visual Basic, or any other code language, draws the control directly on the container's window, avoiding the overhead of an additional window handle. These windowless controls usually load faster, making them ideal for applications where system resources are a limiting factor.
In this article, we'll uncover the details of these system-saving controls, and then create a lightweight progress bar as an example. Because we'll explain some of the more complicated procedures in this article, you'll want to have a good familiarity with creating regular ActiveX controls.
hWnd
property.
BorderStyle
or
EditAtDesignTime
properties. With these caveats, most Visual Basic programming books highly recommend using lightweight controls. Unfortunately, this is usually the last reference to these controls, with no mention of how exactly to create them.
Line, Circle
, and
DrawEdge
functions we used, see this month's accompanying
article "Draw
windowless control GUIs". For now, let's cover some of the sticky
points of drawing an improved progress bar.
Figure A: When we tried to call the Rectangle API function from
a private sub, Visual Basic triggered a runtime error.
Apparently a windowless control can only access the hDC
within one of its own event subroutines-a private sub
can't.
As a result, we changed the drawing function, ShowValue()
, to
include the old Line()
method. Of course, now every mention
of how VB only includes this method for backward compatibility makes us
cringe.
To enable Visual Basic to do so, we created the
AvailableWidth()
function shown in Listing A. Notice that it
bases its calculations on the UserControl's ScaleWidth
property, which should be in pixel units. The Offset()
function in Listing A calculates the space around each segment in the
progress bar. As the bar gets larger, the offset increases.
Listing A: The AvailableWidth() and Offset() functions
Private Function AvailableWidth() As Long
AvailableWidth = UserControl.ScaleWidth - _
6 - (2 * Offset)
End Function
Private Function Offset() As Long
Dim H As Long
H = UserControl.ScaleHeight
Select Case H
Case Is < 10: Offset = 0
Case Is < 20: Offset = 1
Case Is >= 20: Offset = 2
End Select
End Function
BorderStyle
property. As a result, we'll use the
DrawEdge()
API function to draw our own border. Fortunately,
DrawEdge()
requires the Device Context (hDc
) and
not the window handle. For more information on how to use this API
function see, "Draw
windowless control GUIs". At this point, we're ready to build the
control.
Windowless
property to
True
. Then, name the control WLProgressBar. Table A
lists additional property settings. We'll explain the reasons behind the
DrawStyle, DrawWidth, FillColor,
and FillStyle
settings later.
Table A: WLProgressBar Properties
Property |
Setting |
DrawStyle | 0 - Solid |
DrawWidth | 2 |
FillColor | Highlight (system) |
FillStyle | 0 - Solid |
ScaleMode | 3 - Pixel |
Windowless | True |
Listing B: Code to generate the appropriate number and width of
ticks
|
As you can see, the code covers two possible scenarios:
Later on, the Paint()
event's code determines the actual
width from the entire number of ticks and uses that dimension to calculate
the progress bar's left and right values, as shown in Listing C.
Listing C: Calculate bar size
Wdth = mStep * nTicks
With r
.Left = 2
.Right = .Left + Wdth + (2 * lOffset) + 2
.Top = 1
.Bottom = ScaleHeight - 1
TickTop = .Top + lOffset + 1
Hght = .Bottom - .Top - (2 * lOffset) - 2
End With
Finally, the code adds the ticks to the UserControl. In
design mode, (Ambient.UserMode = False
), it draws the entire
bar so the developer can see the completed display. To do so, the
Paint()
event uses the Line
method like so: For i = 0 To nTicks - 1
X = Tick0 + i * mStep
Line (X, TickTop)-Step(mStep, Hght), _
LineColor, B
Next
This code draws boxes with dimensions mStep
and
Hght
. Since we set the FillStyle
property to
Solid
, Visual Basic fills the inside of the box with the
current FillColor
. By setting the DrawStyle
to
Solid
and the DrawWidth
to 2, we ensured a wide
border around the fill. Setting the LineColor
equal to the
BackColor
makes the border blend in with the background and
the boxes stand out from each other just as they do in the CC ProgressBar.
In run mode (Ambient.UserMode=True
, the Line
method draws the boxes as needed to indicate the current progress. See the
sample code for details.
DrawEdge()
API to draw a nice border around some boxes, the
lightweight control would be really, well…lightweight. To give a developer
control over the progress bar's features, we must expose properties by
creating public properties. In WLProgress, we used the following
properties, similar to the CC ProgressBar's: Appearance,
BorderStyle, Max, Min, Scrolling,
and Value
. In
addition, we created several custom properties, as shown in Table B.
Tip: To give a public property a list of dropdown options
in the Properties Window, create a Public Enum . For
instance, if we wanted developers to select the UserControl's
Appearance property values from a finite list, we could use code
like this:
|
Table B: WLProgress bar's custom properties
Property |
Purpose |
FillColor | Changes color for segments or smooth bar |
Number | Sets a fixed number of ticks, or allows automatic calculation (Number = 0) |
Shape | Sets rectangle or circle segments |
Step | Sets size of ticks |
Appearance
and BorderStyle
lend
themselves to easy validation. The property must conform to one of the
Enum
values and doesn't depend on any other property. For
invalid values, you take the same action in the
ReadProperties
procedure at both design time and runtime-set
the property to the default value. Of course, at design time, you may want
to notify the developer that the control will use the default value.
For this type of validation, place the code in the Property
Let
procedure, as in:
Public Property Let Appearance _
(ByVal NewValue As pbAppears)
If NewValue = pb3D Or NewValue = pbFlat Then
mAppears = NewValue
Else
If Not Ambient.UserMode Then
MsgBox "Appearance value was invalid. " & _
"It will be reset to default value."
End If
mAppears = pb3D
End If
UserControl.Refresh
PropertyChanged "Appearance"
End Property
Then, in the ReadProperties
event, read the
saved value into the public property, and the Property Let
procedure validates it by default. In design mode, the code alerts the
developer with an informative message. In runtime, the code quietly sets
the value to the default.
Max
and Min
properties depend on each other for
validation, you can't test them individually in the
ReadProperties
event. As a result, our code reads them into
the member variables (mMax
and mMin
) and then
calls helper routines. If either of them are invalid, the helper routine
raises an error and passes it to the calling routine. See the sample code
for details. Notice that the code takes a different action when the
property is set outside of the ReadProperties
event. With a
validation helper routine, you have more flexibility when responding to
invalid values.
Number
and Step
not only depend on each other,
but also on the UserControl's current size, which can change at any time.
Because of this behavior, the ReadProperties
event can only
validate that these properties are within reason. For instance,
Step
cannot be so small that the segments would be invisible,
nor so large that a tick becomes wider than the control itself. Therefore,
unreasonable values get reset to the defaults.
In addition, when Step
changes at design time or runtime,
the PropertyLet
procedure calls ValidateStep
to
determine if it is within bounds. If not, the procedure exits without
changing the property, as seen in Listing D.
Listing D: Validating Step values
|
However, the validation doesn't stop here. When the Paint
event calculates the number and size of the ticks, the Step
value may also change. Normally we don't recommend changing a property
value outside of a Property Let
procedure. But putting the
tick calculation code inside the Step
and Number
validation code results in duplication and, perhaps, unnecessary
complexity. We've taken the precaution of using temporary variables for
Step
and Number
in the tick calculation. When
alterations result in a property change, the code updates the member
variable and calls the PropertyChanged
event. Still, this may
not be the best approach and we welcome any suggestions you have on this
subject.
Value
property
before it calls the ShowValue
subroutine, as seen in Listing
E.
If the property is out of bounds, the control calls
ShowMessage
instead, which displays a friendly message. The
container application may still crash, but at least it's provided with as
much information as possible. (The CCProgressBar crashes any time it
receives an invalid value, regardless.)
Listing E: Validating the Value property
Public Property Let Value(ByVal iValue As Integer)
Dim ErMsg As String
If iValue = 0 Then
bPBarStart = False
Call ShowValue(0, False)
mpbVal = 0
Exit Property
End If
If (iValue >= mMin And iValue <= mMax) Then
mpbVal = iValue
Call ShowValue(mpbVal, False)
Else
ErMsg = " Value is out of range"
Call ShowMessage(ErMsg)
End If
End Property
HitTest
method describes pitfalls that
could also decrease lightweight performance. Finally, not all containers
support windowless controls, and by default, will be treated as a windowed
control. This eliminates the point of creating the control to begin with.
hWnd
arguments didn't limit our example, but it did lead to some twists and
turns. And, in the process, we created a lightweight progress bar that
exposes increased functionality to developers.
Copyright © 2000, ZD
Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD
Inc. Reproduction in whole or in part in any form or medium without
express written permission of ZD Inc. is prohibited. All other product
names and logos are trademarks or registered trademarks of their
respective owners.