Visual Basic Window/Form Routines
SendMessage: Move Controls to Simulate Form Scrolling
     
Posted:   Friday July 16, 2004
Updated:   Monday December 26, 2011
     
Applies to:   VB4-32, VB5, VB6
Developed with:   VB6, Windows XP
OS restrictions:   None
Author:   VBnet - Randy Birch, Dave Scarmozzino, Rui Trigueiros
     

Related:  

SendMessage: Creating a Scrollable Viewport to Simulate a Scrollable Form
ReleaseCapture: Simulating a Working Size-Grip on a VB Form

     
 Prerequisites
None.

Neither normal Visual Basic forms, nor VB MDI child forms, can scroll their contents natively. An MDI parent window can scroll its child windows, but only when the child exceeds or is positioned to fall outside the parent's client area. In order to give normal forms or MDI child windows the appearance of scrolling its constituent controls, one of two techniques needs to be employed.

The first technique is to create a scrollable viewport (as shown at SendMessage: Creating a Scrollable Viewport to Simulate a Scrollable Form) and as popularized in the MSKB article on scrollable viewports. In this technique, all controls are placed inside a container (such as a picture box) which in turn is placed into another picture box. The 'inner' picture box is sized as required to accommodate the controls contained within and, in response to scroll bar movement from scroll controls placed on the form, the larger inner picture box is repositioned to reveal different portions of its contained controls through the form-sized 'viewport' created by the 'outer' picture box. With this technique there is only one 'moving' control - the larger inner picture box.

The second method - shown here - is to place all controls directly on the form and then determine the total 'area' the controls occupy within the form's client area. On each scroll bar movement each control is moved (repositioned) appropriately within the exposed client area. This method does not require the controls be placed into containers as the viewport example does and is therefore a simpler method to employ, especially on quick data entry-type screens. Plus, you get to work physically with the controls on the form (rather than the pain of selected controls inside a container) which alone can speed up application design.

The code presented here is based on a demo originally published by Dave Scarmozzino (thescarms.com).  The principles from Dave's code are pretty much carried over here but I have made considerable changes to the original code correctly handle scrollbar sizing, work area calculation and control positioning regardless of form style, form caption size and border widths. And, because users expect to be able to use a grab handle to resize a sizable form, I've added code from ReleaseCapture: Simulating a Working Size-Grip on a VB Form to provide this functionality.

Positioning of the controls is achieved by looping through the controls within the client area and manipulating the top and left properties of those controls. However, some controls do not expose these properties (e.g. common dialog, timer, lines etc.), while others do not allow manipulation of these properties (e.g. toolbars and status bars set to align). In order to accommodate the peculiarity of different controls I resorted to a simple On Error Resume Next test which handles attempts to manipulate non-existent or read-only properties of the controls, albeit arguably not necessarily in the nicest way. While certainly a functional and solid solution, your programming practices and standards may dictate the need for an alternative action to circumvent the restrictions of some controls.

One other thing to make mention of: this is as simple a demo as possible to deliver the ability to simulate a scrolling form with a resizing handle. It does not take into account nor compensate for portions of the client area that may be occupied by alignable controls such as a status bar, toolbar or aligned picture box. This limitation can be worked around by additional handling in the appropriate events, but such is outside the scope of this hopefully "simple" demonstration.

Finally, the gripper handle shown is constructed by using Windows' Marlett font, just as Windows normally does in versions prior to Windows XP, or on a Windows XP box running in "standard" GUI mode. Windows XP in "themed" mode does not use Marlett for the gripper image but rather draws the gripper is a series of dots rather directly through APIs calls to the XP themes.dll.   So while it is possible to display the XP-style gripper on XP machines, the code required to do so is involved and is also outside the scope of this particular demo. Specific demos for that and other XP UI enhancements are in development. By the way, if you're using this code on a RTL system that displays the resizing grip handle at the bottom left of the form rather than the bottom right, the Marlett character to use below instead of "o" is "x".

Revision History

July 18.2004 Added code to enable resizing only when using a resizable form BorderStyle
July 18.2004 Added additional test for controls, and On Error statements to handle improper property references
July 19.2004     Fixed positioning bug when form maximized
July 19.2004 Added variable reset in form load to ensure horizontal and vertical positioning always affected the proper variable
July 19.2004 Added code to prevent the sizing cursor when the window is maximized
July 23.2004 Modified the code writing the fake gripper handle to position it at the bottom right of the picture box regardless of the size of the scrollbars
July 24.2004 A tweak here, a tweak there, mostly to reduce the duplication of code for the fake gripper
 BAS Module Code
None.

 Form Code
Add a horizontal and vertical scrollbar (HScroll1, VScroll1) and a picture box (Picture1) to a form to act as the scroll bars and resizing gripper handle. The position, size and initial properties of these three controls is of no importance, as all are properly set up in the form load and resize events.

With those three mandatory controls, add any number/combination of other controls representing a form to scroll - these controls can have any control names desired. The controls can also either be individual controls or part of a control array. Add the following code:


Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Copyright ©1996-2011 VBnet/Randy Birch, All Rights Reserved.
' Some pages may also contain other copyrights by the author.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Distribution: You can freely use this code in your own
'               applications, but you may not reproduce 
'               or publish this code on any web site,
'               online service, or distribute as source 
'               on any media without express permission.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'loosely based on code by Dave Scarmozzino (thescarms.com)

'The height and width of all information on
'the screen if it were all shown at one time
Private maxViewportHeight As Long
Private maxViewportWidth As Long

'flag to enables/disable the ability to
'resize the window using the 'fake' grabber
Private bWindowIsResizable As Boolean

Private Const WM_NCLBUTTONDOWN = &HA1
Private Const HTBOTTOMRIGHT = 17

Private Declare Function SendMessage Lib "user32" _
   Alias "SendMessageA" _
  (ByVal hwnd As Long, _
   ByVal wMsg As Long, _
   ByVal wParam As Long, _
   lParam As Any) As Long

Private Declare Function ReleaseCapture Lib "user32" () As Long



Private Sub Form_Load()
   
   Dim ctl As Control
   Dim maxh As Long
   Dim maxw As Long
      
  'these are the default controls widths 
  'and can be changed to suit your purpose 
   HScroll1.Height = 255
   VScroll1.Width = 255
   
  'set flag indicating a resizable window
   bWindowIsResizable = (Me.BorderStyle = vbSizable) Or _
                        (Me.BorderStyle = vbSizableToolWindow) And _
                        (Me.WindowState <> vbMaximized)
   
  'set up the picture box used to fill
  'the corner between the H and V scroll
  'bars, and if the window is sizable
  'print a 'gripper' image to the control
   With Picture1
      .ScaleMode = vbTwips   
      .AutoRedraw = True
      .ForeColor = &H80000015
      .BackColor = Me.BackColor
      .BorderStyle = 0
      'prepare to draw fake gripper image
      .Font.Name = "Marlett"      
      .Font.Size = 11
      .Font.Bold = False
   End With
  
  'we have to safely handle controls that 
  'do not expose referenced properties 
  '(such as timers, common dialog control, 
  'lines/shapes)
   On Error Resume Next
     
  'calc the largest size the form
  'has to 'scroll' to accommodate
  'all controls, but ignore controls embedded inside 
  'other containers (e.g. frames or picture boxes)
   For Each ctl In Me.Controls
      If Not (ctl Is VScroll1) And _
         Not (ctl Is HScroll1) And _
         Not (ctl Is Picture1) And _
         (TypeOf ctl.Container Is Form) Then
         
         If (ctl.Top + ctl.Height) > maxh Then maxh = (ctl.Top + ctl.Height)
         If (ctl.Left + ctl.Width) > maxw Then maxw = (ctl.Left + ctl.Width)

      End If
   Next
   
   On Error Goto 0
   
  'remember to allow for the size
  'of the scrollbars to ensure
  'everything fits on-screen when
  'scrolled
   maxViewportHeight = maxh + (HScroll1.Height * 2)
   maxViewportWidth = maxw + (VScroll1.Width * 2)
   
  'position and ensure the other controls
  'move behind the scrollbars and gripper pix box
   With VScroll1
      .Move Me.ScaleWidth - .Width, 0
      .ZOrder 0
   End With
   
   With HScroll1
      .Move 0, Me.ScaleHeight
      .ZOrder 0
   End With
   
   Picture1.ZOrder 0
 
End Sub


Private Sub Form_Resize()

  'don't attempt resizing if minimized!
   If Me.WindowState <> vbMinimized Then
   
      VScroll1.Left = Me.ScaleWidth - VScroll1.Width
      HScroll1.Top = Me.ScaleHeight - HScroll1.Height

     'if the form has been resized to
     'display all the controls, disable
     'the scrollbars
      VScroll1.Enabled = (maxViewportHeight - Me.ScaleHeight) >= 0
      HScroll1.Enabled = (maxViewportWidth - Me.ScaleWidth) >= 0

     'this prevents an error if
     'the form is sized too small
      If (Me.ScaleHeight > HScroll1.Height) And _
         (Me.ScaleWidth > VScroll1.Width) Then
          
        'because the window size has
        'changed we have to modify the
        'scrollbar parameters to reflect
        'the new form size and resize the
        'scrollbar to fill the client area
         With VScroll1
            .Top = 0
            .Height = (Me.ScaleHeight - HScroll1.Height)
            
            If VScroll1.Enabled Or _
               Me.WindowState = vbMaximized Then
               .Min = 0
               .Max = (maxViewportHeight - Me.ScaleHeight)
               .SmallChange = (Screen.TwipsPerPixelY * 10)
               .LargeChange = (Me.ScaleHeight - HScroll1.Height)
            End If  'VScroll1.Enabled
            
         End With
        
         With HScroll1
            .Left = 0
            .Width = (Me.ScaleWidth - VScroll1.Width)
            
            If HScroll1.Enabled Or _
               Me.WindowState = vbMaximized Then
               .Min = 0
               .Max = (maxViewportWidth - Me.ScaleWidth)
               .SmallChange = (Screen.TwipsPerPixelX * 10)
               .LargeChange = (Me.ScaleWidth - VScroll1.Width)
            End If  'HScroll1.Enabled
            
         End With
         
      End If  '(Me.ScaleHeight > HScroll1.Height) And ...

   End If  'Me.WindowState
   
  'Windows applications remove the gripper
  'if a resizable window is maximized, so
  'we'll do the same with our fake gripper.
  'This must execute regardless of window state
  'to ensure the gripper is displayed when valid
   bWindowIsResizable = (Me.BorderStyle = vbSizable Or _
                         Me.BorderStyle = vbSizableToolWindow) And Not _
                        (Me.WindowState = vbMaximized)
                        
  'if resizable is false then remove
  'the gripper handle mark, otherwise
  'ensure it is visible
   With Picture1

      .Move VScroll1.Left, HScroll1.Top, VScroll1.Width, HScroll1.Height

     'if sizable windows print the gripper image
      If bWindowIsResizable = True Then
         'by using the scale- and text- width/height,
         'we can ensure the fake grab handle is drawn at the 
         'bottom right of the picture box regardless of the 
         'width set for the scrollbars. (Try changing the  
         'scrollbar size to 500 in the Load event to demonstrate.)
         .Cls
          Picture1.CurrentX = Picture1.ScaleWidth - Picture1.TextWidth("o")
          Picture1.CurrentY = Picture1.ScaleHeight - Picture1.TextHeight("o")
          Picture1.Print "o"
      Else
         .Cls  'clear the pixbox
      End If  'bWindowIsResizable
      
   End With  'Picture1


End Sub


Private Sub Picture1_MouseDown(Button As Integer, _
			       Shift As Integer, _
			       x As Single, _
			       y As Single)

  'if a sizable window and not maximized then..
   If bWindowIsResizable Then
         
     '..fake a resize grabber action
      If Button = vbLeftButton Then
          ReleaseCapture
          SendMessage Me.hwnd, WM_NCLBUTTONDOWN, HTBOTTOMRIGHT, ByVal 0&
      End If  'Button
      
    End If  'bWindowIsResizable
    
End Sub


Private Sub Picture1_MouseMove(Button As Integer, _
                               Shift As Integer, _
                               x As Single, _
                               y As Single)

  'users expect a resizable window with a grip handle 
  'to display a resizing arrow. This code does this, 
  'and is equivalent to using:
  '  if bWindowIsResizable = True Then
  '     Picture1.MousePointer = vbSizeNWSE
  '  else
  '     Picture1.MousePointer = vbDefault
  '  end if
  '
  'because:
  'True (-1) AND vbSizeNWSE (8) = 8 (or vbSizeNWSE)
  'False (0) AND vbSizeNWSE (8) = 0 (or vbDefault)
  
   Picture1.MousePointer = (bWindowIsResizable And vbSizeNWSE)

End Sub


Private Sub VScroll1_Change()
    Call DoFormScroll
End Sub


Private Sub VScroll1_Scroll()
    Call DoFormScroll
End Sub


Private Sub HScroll1_Change()
    Call DoFormScroll
End Sub


Private Sub HScroll1_Scroll()
    Call DoFormScroll
End Sub


Private Sub DoFormScroll()

   Dim ctl As Control
   
  'just like Load, handle errors for controls 
  'not exposing touched properties  
   On Local Error Resume Next
   
  'saves scroll state between calls
   Static oldVpos As Long
   Static oldHpos As Long
   
  'Move each control in response to
  'a change in scrollbar positions.
  '
  'The code below skips over the two scroll bars
  'and the picture box used for the fake gripper 
  'handle by excluding those controls by name. 
  'If you rename these controls for your final 
  'application it is recommended that a global 
  'form Replace be performed to change the 
  'control names of these controls to ensure 
  'the references here, in the Load event, 
  'the Resize event, and the control events 
  'themselves all reflect your new control names.
   For Each ctl In Me.Controls
      If Not (ctl Is VScroll1) And _
         Not (ctl Is HScroll1) And _
         Not (ctl Is Picture1) And _
         (TypeOf ctl.Container Is Form) Then
         
         ctl.Top = ctl.Top + oldVpos - VScroll1.Value
         ctl.Left = ctl.Left + oldHpos - HScroll1.Value

      End If
   Next
   
  'we need to save the positions
   oldVpos = VScroll1.Value
   oldHpos = HScroll1.Value
         
End Sub
 Comments
The demo uses On Local Error (or rather just On Error since the 'Local' part was depreciated early in VB, but I continue to use it to show that I expect all errors to be handled locally within the routine defining the error handler - a sort of self-documenting code) in order to circumvent the need for an excessively large and all-inclusive "If (Not ctl is <controlname>)" statement in DoFormScroll and the Load event to handle controls that do not expose a settable Top and Left property. If On Error isn't your cup-of-tea, feel free to increase the If..Then block to explicitly name all the controls on your form.

In addition, VB's line controls present another issue, namely their positioning via X1.X2/Y1.Y2 properties rather than the usual left/top etc. The error traps in the code will correctly catch references these controls, but because the code does not contain explicit X1.X2/Y1.Y2 positioning code a form's lines will not scroll using the code above. If this type of control is important in your application, you will need to further extend the If..Then test such as:

   For Each ctl In Me.Controls
      If Not (ctl Is VScroll1) And _
         Not (ctl Is HScroll1) And _
         Not (ctl Is Picture1) And _
         (TypeOf ctl.Container Is Form) Then
         
         If Not (TypeOf ctl Is Line) Then
            ctl.Top = ctl.Top + oldVpos - VScroll1.Value
            ctl.Left = ctl.Left + oldHpos - HScroll1.Value
          Else
            ctl.X1 = ctl.X1 + oldHpos - HScroll1.Value
            ctl.X2 = ctl.X2 + oldHpos - HScroll1.Value
            ctl.Y1 = ctl.Y1 + oldVpos - VScroll1.Value
            ctl.Y2 = ctl.Y2 + oldVpos - VScroll1.Value
         End If
      End If
   Next

A similar test is also required in the Load event control loop to properly include the area taken by the lines in the calculation of the client's scrollable real estate.


 
 

PayPal Link
Make payments with PayPal - it's fast, free and secure!

 
 
 
 

Copyright ©1996-2011 VBnet and Randy Birch. All Rights Reserved.
Terms of Use  |  Your Privacy

 

Hit Counter