February 2000

Conserve System Resources with Lightweight Controls

by Todd Ryan

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.

What exactly is a lightweight control?

Lightweight controls have the following requirements:

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.

A lightweight ProgressBar with more control

As you probably know, the Microsoft Common Controls ProgressBar gives you scant control over its attributes, such as the number and size of its segments. To rectify this behavior, we'll add the following features to our lightweight version:

Drawing the control via code

When drawing a control from scratch, you'll find it easier to develop the drawing code in a standard EXE form. That way, you can test the results immediately, instead of using standard ActiveX debugging methods. Due to space considerations, we won't discuss the details of drawing on a standard form in this article. However, for more information on the 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.

Using Line() instead of Rectangle()

When we developed the drawing code in a standard EXE, we used the Rectangle API in a private sub to draw the segments. It worked in the EXE, but generated the error in Figure A in the windowless control. The VB documentation on trappable errors doesn't contain an explanation, but the error message provides some meaningful insight.

Figure A: When we tried to call the Rectangle API function from a private sub, Visual Basic triggered a runtime error.
[ Figure A ]

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.

Displaying an even number of segments

In our opinion, the CC ProgressBar has an annoying habit of showing only a partial segment at the end of the bar. In our control, we want a whole number of segments, or ticks, to fit snugly inside. To achieve this effect, Visual Basic will need to calculate the size of the bar first.

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

Adding a border

Now that we have a way to determine the available space for the bar, we'll want a nice border around it-just like the CC ProgressBar. But as we mentioned, windowless controls can't access the 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.

Building the windowless control

To begin, open an Active X Control Project and set the UserControl's 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

The Paint event

Listing B shows a partial sample of code that draws the control. Much of it concerns itself with calculating the appropriate number and width of the ticks based on the available space in the control. The actual drawing functions are quite simple. (You can find the completed code in this month's download sample.)

Listing B: Code to generate the appropriate number and width of ticks
Private Sub UserControl_Paint()
' First line dimmed for DrawEdge API
Dim lEdge As Long, lFlag As Long, r As RECT
Dim lAvailWdth As Long, lNum As Long, lOffset As Long
Dim ltempStep As Long

lOffset = Offset
lAvailWdth = AvailableWidth

If lAvailWdth < mMinStep Then
 b2Small = True
 Exit Sub
End If

lNum = mNumber
ltempStep = mStep

If lNum <> 0 Then
' A specific Number is selected
 If (lNum > lAvailWdth \ mMinStep) Then
  'Selected number is too large.
  'Display as many as possible.
  ltempStep = mMinStep
  nTicks = lAvailWdth \ ltempStep
 Else
  ltempStep = lAvailWdth \ lNum
  nTicks = lNum
 End If
Else
 ' Number = 0 AutoCalc
 nTicks = lAvailWdth \ ltempStep
 If nTicks < mMinTicks Then
  First reduce the Step Size to create MinTicks
  ltempStep = lAvailWdth \ mMinTicks
  nTicks = mMinTicks
  If ltempStep < mMinStep Then
   nTicks = lAvailWdth \ mMinStep
   ltempStep = mMinStep
   If nTicks = 0 Then
    b2Small = True ' If too small, don't draw
    Exit Sub
   End If
  End If
 End If
End If

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.

Exposing UserControl properties

If all we used was the 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:
Public Enum pbAppears
 pbFlat = 0
 pb3D = 1
End Enum

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

Validating property values

Now that our lightweight control exposes properties, we should provide code to validate any changes. Properties like 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.

Single dependent property validation: Max and Min

Because the 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.

Multi-dependent validation: Number and Step

Even more interesting, 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
Public Property Let Step(ByVal lValue As Long)
On Error Resume Next
If mNumber <> 0 Then
 If Not Ambient.UserMode Then
  MsgBox "Step cannot be set when Number > 0." & _
  vbCrLf & "Set Number = 0, if you need to set Step."
 End If
 Exit Property
Else
 Call ValidateStep(lValue)
 If Err.Number <> 0 Then
  If Not Ambient.UserMode Then
   MsgBox Err.Source & " " & Err.Description
  End If
  Err.Number = 0
  Exit Property   'EXIT without changing Step
 Else
 mStep = lValue
End If
UserControl.Refresh
PropertyChanged "Step"
End Property

Private Sub ValidateStep(ByVal lWidth As Long)
If lWidth < 4 Then
 Err.Raise 380, errSource, vbCrLf & _
  "Step value must be 4 Pixels or greater."
ElseIf lWidth > AvailableWidth Then
 Err.Raise 380, errSource, vbCrLf & _
  "Maximum Step value must be less than the" _
  & " ProgressBar width in Pixels."
End If
End Sub

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.

Ensuring the correct Value property

Our progress bar's real business end consists of showing the current value assigned by the container application. Under normal circumstances the UserControl's user should be responsible for providing validation code for this setting. But what if an unexpected condition arises? To accommodate such unforeseen circumstances, our control validates the 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

To window or not to window

When you create your own windowless controls, you'll want to first determine if doing so really gains you anything. We've read accounts where certain lightweight controls were actually slower than their windowed counterparts. Also, the Microsoft documentation on the 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.

Summary

In this article, we've only scratched the surface of conserving system resources with lightweight controls. We found that drawing a lightweight could be as easy (or as difficult) as drawing a windowed control. The absence of API functions that used 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.