Control of the Month: ScrolledWindow

Rod Stephens

One of the strangest omissions in Microsoft’s control toolkit is the scrolled window. Scrolled windows are all over Windows systems, are a standard part of Delphi, and have been used for years in other operating systems. Yet in Visual Basic, you need to build your own. This month, Rod Stephens (a.k.a. the "Control Connoisseur") shows how you can build this fundamental control. Hooray!

Adding a scrolled window to your VB program is a lot of work. You place controls inside a PictureBox, put that PictureBox inside another PictureBox, add some scroll bars to the sides, and possibly stick the whole thing in yet another PictureBox. You need to add code that sets the scroll bar Min, Max, SmallChange, and LargeChange properties, and you need to move the inner PictureBox appropriately when the user moves the scroll bars. By moving the inner PictureBox inside the outer one, you make the controls it contains seem to move. Unfortunately if you want another scrolled window, you need to duplicate all of this code. If you want a scrolled window in another program, you need to duplicate it again.

This month’s control lets you solve the scrolled window problem once and for all. Just put it on your form, drop controls into it, and you’re ready to scroll.

Contained controls

The UserControl object has a ControlContainer property. If you set this property to True when you build the control, then you can put other controls inside it at program design time. This works in the same way you can put controls in normal PictureBoxes and Frames. Because the ScrolledWindow control will hold other controls, you should set its ControlContainer property to True.

Once controls are placed inside the ScrolledWindow, the scrolled window itself can access those controls through its ContainedControls collection. This collection contains references to the controls placed inside the ScrolledWindow at program design time.

Don’t confuse these contained controls with the constituent controls you use to build the ScrolledWindow. You add constituent controls when you build the ScrolledWindow. You add contained controls when you use the ScrolledWindow to build a program. The ScrolledWindow accesses its constituent controls with the Controls collection. It accesses its contained controls with the ContainedControls collection.

Constituent controls

The ScrolledWindow control uses three constituent controls. Its two scroll bars, vbarWindow and hbarWindow, control the placement of the contained controls inside the ScrolledWindow. The picCorner control is a PictureBox used to cover any controls that would otherwise show in the lower right corner of the ScrolledWindow below the vertical scroll bar.

Figure 1 shows how these controls are arranged in the ScrolledWindow. The picCorner control covers parts of the contained control that might otherwise be visible. In this case, the contained picture would show a small square of white if picCorner were missing.

The ScrolledWindow control must perform two main tasks. First, it must arrange its constituent controls so it displays the appropriate scroll bars in the correct positions. Second, it must arrange its contained controls when the user scrolls them using the scroll bars.

Whenever the ScrolledWindow is resized, the control must determine which scroll bars it needs and where they should be positioned. The ArrangeConstituentControls subroutine shown in Listing 1 does this.

Listing 1. Arranging the ScrolledWindow’s constituent controls.

' The contained controls' current offsets
' from their original positions.
Private m_XOffset As Single
Private m_YOffset As Single
' Arrange the scroll bars and contained controls.
Public Sub ArrangeConstituentControls()
Dim need_hbar As Boolean
Dim need_vbar As Boolean
Dim need_wid As Single
Dim need_hgt As Single
Dim have_wid As Single
Dim have_hgt As Single
Dim wid As Single
Dim hgt As Single
Dim ctl As Control
   ' See if there are any contained controls.
   If ContainedControls.Count < 1 Then
      ' There are no contained controls.
      ' We don't need either scroll bar
      need_hbar = False
      need_vbar = False
   Else
      ' There are contained controls.
      ' Bound them.
      need_wid = 0
      need_hgt = 0
      ' Protect against controls missing some
      ' properties. For example, Timer has no
      ' Width and Height.
      On Error Resume Next
      For Each ctl In ContainedControls
         If TypeOf ctl Is Line Then
            If need_wid < ctl.X1 - m_XOffset Then _
               need_wid = ctl.X1 - m_XOffset
            If need_wid < ctl.X2 - m_XOffset Then _
               need_wid = ctl.X2 - m_XOffset
            If need_hgt < ctl.Y1 - m_YOffset Then _
               need_hgt = ctl.Y1 - m_YOffset
            If need_hgt < ctl.Y2 - m_YOffset Then _
               need_hgt = ctl.Y2 - m_YOffset
         Else
            If need_wid < ctl.Left + ctl.Width - _
               m_XOffset Then need_wid = _
               ctl.Left + ctl.Width - m_XOffset
            If need_hgt < ctl.Top + ctl.Height - _
               m_YOffset Then need_hgt = ctl.Top + _
               ctl.Height - m_YOffset
         End If
      Next ctl
      On Error GoTo 0
      ' See which scroll bars we need.
      have_wid = ScaleWidth
      have_hgt = ScaleHeight
      ' See if we need the horizontal scroll bar.
      If need_wid > have_wid Then
         ' We need the horizontal scroll bar.
         need_hbar = True
         ' Allow room for the scroll bar.
         have_hgt = have_hgt - hbarWindow.Height
      Else
         need_hbar = False
      End If
      ' See if we need the vertical scroll bar.
      If need_hgt > have_hgt Then
         ' We need the vertical scroll bar.
         need_vbar = True
         ' Allow room for the scroll bar.
         have_wid = have_wid - vbarWindow.Width
         ' See if we now need the horizontal scroll bar.
         If (Not need_hbar) And _
            (need_wid > have_wid) _
         Then
           ' We now need the horizontal scroll bar.
            need_hbar = True
           ' Allow room for the scroll bar.
            have_hgt = have_hgt - hbarWindow.Height
         End If
      Else
         need_vbar = False
      End If
   End If
   ' Display the needed scroll bars.
   If need_hbar Then
      ' Allow room for the other scroll bar
      ' if it is needed.
      If need_vbar Then
         wid = ScaleWidth - vbarWindow.Width
      Else
         wid = ScaleWidth
      End If
      ' Position the scroll bar.
      hbarWindow.Move 0, _
         ScaleHeight - hbarWindow.Height, _
         wid
      hbarWindow.Max = need_wid - have_wid
      hbarWindow.LargeChange = wid
      hbarWindow.Visible = True
      hbarWindow.ZOrder
   Else
      hbarWindow.Visible = False
   End If
   If need_vbar Then
      ' Allow room for the other scroll bar
      ' if it is needed.
      If need_hbar Then
         hgt = ScaleHeight - hbarWindow.Height
      Else
         hgt = ScaleHeight
      End If
      ' Position the scroll bar.
      vbarWindow.Move _
         ScaleWidth - vbarWindow.Width, _
         0, vbarWindow.Width, hgt
      vbarWindow.Max = need_hgt - have_hgt
      vbarWindow.LargeChange = hgt
      vbarWindow.Visible = True
      vbarWindow.ZOrder
   Else
      vbarWindow.Visible = False
   End If
   ' If both scroll bars are visible, put
   ' picCorner in the lower right corner.
   If need_hbar And need_vbar Then
      picCorner.Move ScaleWidth - vbarWindow.Width, _
         ScaleHeight - hbarWindow.Height, _
         vbarWindow.Width, hbarWindow.Height
      picCorner.Visible = True
      picCorner.ZOrder
   Else
      picCorner.Visible = False
   End If
End Sub

The routine begins by examining its contained controls to find the maximum X and Y values occupied by any control. It handles Line controls separately because they have X1, X2, Y1, and Y2 properties instead of the normal Left, Top, Width, and Height properties. The routine also uses an On Error Resume Next statement to protect itself from controls that have neither set of properties. For example, the Timer and Common Dialog controls don’t have Width or Height properties.

The m_XOffset and m_YOffset values are offsets from the controls to their original positions. For example, if the ScrolledWindow has moved each control 120 units to the right, m_XOffset is 120. Subtracting these values from the control’s positions lets the routine tell where the controls were originally positioned.

The routine then compares the space needed by the contained controls to the space available in the ScrolledWindow control. If the needed width is greater than the available width, the control sets need_hbar to True to indicate that it needs the horizontal scroll bar. It also subtracts the horizontal scroll bar’s height from the available height to leave room for that scroll bar.

Next, if the needed height is greater than the remaining available height, the routine sets need_vbar to True to indicate that it needs the vertical scroll bar. It subtracts the width of the vertical scroll bar from the available width to make room for that control.

Subtracting this width might make the available width too small, even though it wasn’t too small before. If the available width has just grown too small, the routine sets need_hbar to True.

Now that the routine knows which scroll bars it needs, it can position the constituent controls. If the horizontal scroll bar is needed, the code places it at the bottom edge of the ScrolledWindow. If the vertical scroll bar is also needed, it leaves room to the right. The routine sets the scroll bar’s Max, LargeChange, and Visible properties. Finally, it invokes the control’s ZOrder method to ensure that the scroll bar sits on top of any controls contained in the ScrolledWindow.

The routine then positions the vertical scroll bar similarly. Finally, if both scroll bars are visible, the routine positions the picCorner control in the lower right corner of the ScrolledWindow. It calls picCorner’s ZOrder method so it sits above any contained controls.

Subroutine ArrangeConstituentControls is public, so the program using the ScrolledWindow can call it directly. It would do this whenever it rearranged the controls within the ScrolledWindow. For example, if the program moves one of the contained controls, it should call ArrangeConstituentControls so the ScrolledWindow can rearrange itself. Whenever the ScrolledWindow changes size, it must also reevaluate its control arrangement. The UserControl object’s Resize event calls ArrangeConstituentControls to do this automatically. Unfortunately, the first time this event occurs, the ContainedControls collection is empty. At this point the ScrolledWindow has just been created, and the controls it contains haven’t yet been placed inside it.

To make the ScrolledWindow arrange its scroll bars after the contained controls are present, the UserControl’s Paint event handler also calls subroutine ArrangeConstituentControls the first time it occurs. The control uses a static variable to ensure that it calls ArrangeConstituentControls only the first time it receives a Paint event.

' Rearrange the scroll bars.
Private Sub UserControl_Resize()
   ArrangeConstituentControls
End Sub
' The first time this happens, arrange the scroll bars
Private Sub UserControl_Paint()
Static done_before As Boolean
   If done_before Then Exit Sub
   done_before = True
   ArrangeConstituentControls
End Sub

Arranging contained controls

The second task the ScrolledWindow must perform is to scroll its contained controls. The ArrangeContainedControls subroutine, which follows, positions the controls based on the ScrolledWindow’s scroll bar values. It subtracts the current offset from each control’s position and then adds a new offset based on the scroll bar values. After repositioning the contained controls, the routine saves the new offset values for the next time ArrangeConstituentControls or ArrangeContainedControls is invoked.

When the user drags or clicks on a scroll bar, the scroll bar’s Scroll or Change event handler invokes ArrangeContainedControls.

' Position the contained controls based on their
' current offsets and the scroll bar values.
Private Sub ArrangeContainedControls()
Dim dx As Single
Dim dy As Single
Dim ctl As Control
Dim is_visible As Boolean
   ' See how far we need to move the controls
   ' relative to their current positions.
   dx = -hbarWindow.Value - m_XOffset
   dy = -vbarWindow.Value - m_YOffset
   ' Position the controls.
   For Each ctl In ContainedControls
      ' See if the control is visible,
      ' guarding against controls like Timer
      ' that don't have a Visible property.
      On Error Resume Next
      is_visible = ctl.Visible
      If Err.Number <> 0 Then is_visible = False
      On Error GoTo 0
      ' If the control is visible, move it.
      If is_visible Then
         If TypeOf ctl Is Line Then
            ctl.X1 = ctl.X1 + dx
            ctl.X2 = ctl.X2 + dx
            ctl.Y1 = ctl.Y1 + dy
            ctl.Y2 = ctl.Y2 + dy
         Else
            ctl.Left = ctl.Left + dx
            ctl.Top = ctl.Top + dy
         End If
      End If
   Next ctl
   ' Save the new offsets for next time.
   m_XOffset = -hbarWindow.Value
   m_YOffset = -vbarWindow.Value
End Sub
Private Sub vbarWindow_Change()
   ArrangeContainedControls
End Sub
Private Sub vbarWindow_Scroll()
   ArrangeContainedControls
End Sub
Private Sub hbarWindow_Change()
   ArrangeContainedControls
End Sub
Private Sub hbarWindow_Scroll()
   ArrangeContainedControls
End Sub

Summary

Using this control, you can add a scrolled window quickly and easily to your VB projects. You no longer need to create a bunch of controls and manage all their resizing and scrolling events on a case-by-case basis. Instead, you can drop the ScrolledWindow onto your form and get on with building the main application.

Download ROD200.ZIP

Rod’s book Custom Controls Library shows how to build 101 different ActiveX controls like this one. Learn more or download some of the hundreds of example programs from his Web site at www.vb-helper.com. RodStephens@vb-helper.com.