Rod Stephens
One of the strangest omissions in Microsofts 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 months control lets you solve the scrolled window problem once and for all. Just put it on your form, drop controls into it, and youre ready to scroll.
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.
Dont 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.
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 ScrolledWindows 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 dont 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 controls 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 bars 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 wasnt 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 bars Max, LargeChange, and Visible properties. Finally, it invokes the controls 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 picCorners 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 objects 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 havent yet been placed inside it.
To make the ScrolledWindow arrange its scroll bars after the contained controls are present, the UserControls 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
The second task the ScrolledWindow must perform is to scroll its contained controls. The ArrangeContainedControls subroutine, which follows, positions the controls based on the ScrolledWindows scroll bar values. It subtracts the current offset from each controls 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 bars 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
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
Rods 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.