|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 |
|
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".
|
||||||||||||||||||
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. |
||||||||||||||||||
|
|
|
|||||
|
|||||
|
|||||
Copyright ©1996-2011 VBnet and Randy Birch. All Rights Reserved. |