Quite often we need a VB control that has requirements that don't quite exist within the current control set, such as all data-entry controls be on a single page with scroll bars. Read on to learn more...
Quite often we have a need for a VB control that
has some requirements that don't quite exist within the current control set.
Recently a client required that all data-entry controls be on a single page,
not across multiple tabs, and the page should scroll much like the page you're reading
now.
Rather
than doing the quick large Picturebox inside a smaller Picturebox with
scrollers, and duplicating the same across multiple forms, I decided to create
a quick user control to simplify the effort on the second and third forms.
Little
did I know that I was in for a challenge, and spent much more time working on
this than planned.
The First Obstacle
Not many people are aware that user controls have two
working modes; Design mode and Running mode. The user-control code runs in both
modes however Debugging only works in running mode; code breaks will not
trigger during design time. This obviously leads to problems testing; however
you often can replicate design time actions in the run time to fully debug
specific options.
Most of
the time you need to set up the control properly, so to start, add a Component
class to your project and modify the code as below.
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.ComponentModel.Design
Imports System.Windows.Forms
<Designer("System.Windows.Forms.Design.ParentControlDesigner,System.Design", GetType(IDesigner))> _
Public Class TestControl
Inherits Control
End Class
This prepares the control for the
designer, and allows you to add controls to it. Now that our base user control
is ready, let's start with our experiments. (Thanks to Hannes for showing me
this when I started).
Experiment 1
First try was a user control with
a Picturebox inside a Picturebox. This works like a charm on the form.
In the
class Designer we add a Picturebox, only one for now so that we can test. But
aren't we doing a Picturebox inside a Picturebox? Yes we still going to use
that method and the Component class is the first Object and the smaller holder
of the two. Our Picturebox is the second and larger of the two, which will
ultimately hold the final controls.
To start
we need to initialize the controls.
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.Controls.Add(PictureBox1)
End Sub
And now let's expose a few
properties of the Picturebox so that we can play with it in the designer.
Public Property Z_Width() As Integer
Get
Return PictureBox1.Width
End Get
Set(ByVal value As Integer)
PictureBox1.Width = value
End Set
End Property
Public Property Z_Height() As Integer
Get
Return PictureBox1.Height
End Get
Set(ByVal value As Integer)
PictureBox1.Height = value
End Set
End Property
Public Property Z_Top() As Integer
Get
Return PictureBox1.Top
End Get
Set(ByVal value As Integer)
PictureBox1.Top = value
End Set
End Property
Public Property Z_Left() As Integer
Get
Return PictureBox1.Left
End Get
Set(ByVal value As Integer)
PictureBox1.Left = value
End Set
End Property
Lets test this quickly.
NOTE: even if we added the
Scrollers at this time, they are not active in the designer and you would still
need to adjust the position of the inner controls via the properties. So the
Scrollers are left out until we know where we going to go.
Build the
control, and then add it to a form, Adjust the Z_width and Z_Height so that our
Picturebox is wider and higher than the control. Now add a few other controls
to our control. Adjust Z_top and Z_left to move our inner control around.
Now we're
shifting the Picturebox about; remember, negative values will pull the inner
Picturebox up and left, exposing the hidden parts. However, there's a problem,
none of the added controls are shifting around.
The problem
is they are all children controls at the same level as the Picturebox, and not
children of the Picturebox, and because of this they do not move with it.
Experiment 2:
Let's try to make those controls
children of the Picturebox.
There's an event that is raised every time a control is added, so
let's use it.
Private Sub ControlScroll_ControlAdded(ByVal sender As Object, ByVal e As System.Windows.Forms.ControlEventArgs) Handles Me.ControlAdded
e.Control.BringToFront()
PictureBox1.Controls.Add(e.Control)
End If
End Sub
With this code, every time a
control is added to our user control we add it to the Picturebox, and make it a
child of the Picturebox.
Rebuild the control, add it to a form and add other controls to
it. Test the Z_top and Z_Left; the controls move with it. Looks like we on
track here. But wait! What happens when the form is forced to redraw, which can
happen for any reason. When this happens all the Sub controls disappear.
WHY! This is a little difficult to explain, and only if you understand
the Designer in VS.NET, will you fully grasp it. The designer code is
automatically regenerated, and any user modifications directly to the designer
code are not recommended. Now when we alter the parent of the control added to
our user control, (by adding it to the Picturebox) the change in the parent is
propagated to the form. This causes changes to the designer, and the lines of
code in the designer that add these controls to our user control are removed.
When the form is re-rendered, they are never added to our control.
So what now? We scrap the idea of using another control to handle
the moving of the subcontrols and take care of those ourselves.
Final Task:
Now we know exactly where we are
headed. First, we need to build a list of all the controls that are going to be
added and moved around our control; we also need to remember how big the view
space and the page space is, and a few other variables.
Private Class My_Control
Private _Point As Point
Public Control As Control
Public Property Location() As Point
Get
Return _Point
End Get
Set(ByVal value As Point)
_Point = value
End Set
End Property
Public Property X() As Integer
Get
Return _Point.X
End Get
Set(ByVal value As Integer)
_Point.X = value
End Set
End Property
Public Property Y() As Integer
Get
Return _Point.Y
End Get
Set(ByVal value As Integer)
_Point.Y = value
End Set
End Property
End Class
Private PageSize As Size
Private ViewSize As Size
Private My_Controls As New List(Of My_Control)
Private Scrolling As Boolean
Private _DoLayout As Boolean
Now when a Control is added to
our user control, we get all of its info and add it to our list of controls. We
also store a Point reference that is where the control's home position is. When
the Control is moved in the designer we store this position.
Private Sub ControlScroll_ControlAdded(ByVal sender As Object, ByVal e As System.Windows.Forms.ControlEventArgs) Handles Me.ControlAdded
e.Control.BringToFront()
If CheckItem(e.Control) Then
AddHandler e.Control.Move, AddressOf MoveAddedControl
Dim TmpControl As New My_Control
TmpControl.Control = e.Control
TmpControl.Y = TmpControl.Control.Location.Y
TmpControl.X = TmpControl.Control.Location.X
My_Controls.Add(TmpControl)
If _DoLayout Then
CalculateViewDiff()
End If
End If
End Sub
Private Sub MoveAddedControl(ByVal sender As Object, ByVal e As System.EventArgs)
If Not (Scrolling) Then
' Adjust the location only when the control is been moved outside this control
For Each Item In My_Controls
If Item.Control Is sender Then
Dim TmpLoc As Point
'If Item.Control.Location.Y < 0 Then
' Item.Control.Top = 0
'End If
'If Item.Control.Location.X < 0 Then
' Item.Control.Left = 0
'End If
TmpLoc.Y = Item.Control.Location.Y + VScrollBar1.Value
TmpLoc.X = Item.Control.Location.X + HScrollBar1.Value
Item.Location = TmpLoc
End If
Next
If _DoLayout Then
CalculateViewDiff()
End If
End If
End Sub
Private Sub ControlScroll_ControlRemoved(ByVal sender As Object, ByVal e As System.Windows.Forms.ControlEventArgs) Handles Me.ControlRemoved
For Each Item In My_Controls
If Item.Control Is e.Control Then
My_Controls.Remove(Item)
Exit For
End If
Next
If _DoLayout Then
CalculateViewDiff()
End If
End Sub
We must not forget to remove
reference to any control when it is removed from our control too. This is
important as we don't want to be trying to move a control that no longer
exists.
You will notice reference to the _DoLayout variable and
CalculateViewDiff sub. Let's look at those quickly.
When controls are being laid out they can be
told to B hold rendering, and when to continue rendering. So we need to remember
what state we are in and of course work accordingly.
Public Overloads Sub ResumeLayout(ByVal PerformLayout As Boolean)
_DoLayout = True
Me.PerformLayout()
Recalc_top()
CalculateViewDiff()
End Sub
Public Overloads Sub ResumeLayout()
_DoLayout = True
Recalc_top()
CalculateViewDiff()
End Sub
Public Overloads Sub SuspendLayout()
_DoLayout = False
End Sub
Public Overloads Sub PerformLayout()
If _DoLayout Then
For Each Citem In My_Controls
Citem.Control.PerformLayout()
Next
End If
End Sub
Public Overloads Sub PerformLayout(ByVal AffectedControl As Windows.Forms.Control, ByVal AffectedProperty As String)
If _DoLayout Then
For Each Citem In My_Controls
If Citem.Control Is AffectedControl Then
Citem.Control.PerformLayout(AffectedControl, AffectedProperty)
End If
Next
End If
End Sub
The CalculateViewDiff function
does most of the hard work. It works out how big the page needs to be to hold
all of the controls in their current position, and which scrollers need to be
visible.
Private Sub CalculateViewDiff()
Scrolling = True
' Lets calculate the size of the Back Page
Dim MaxHeight As Integer = 0
Dim MinHeight As Integer = 0
Dim MaxWidth As Integer = 0
Dim MinWidth As Integer = 0
For Each Item In My_Controls
If CheckItem(Item) Then
If Item.Control.Left - Item.Control.Padding.Left - Item.Control.Margin.Left < MinWidth Then
MinWidth = Item.Control.Left - Item.Control.Padding.Left - Item.Control.Margin.Left
End If
If Item.Control.Top - Item.Control.Padding.Top - Item.Control.Margin.Top < MinHeight Then
MinHeight = Item.Control.Top - Item.Control.Padding.Top - Item.Control.Margin.Top
End If
If Item.Control.Left + Item.Control.Width + Item.Control.Padding.Right + Item.Control.Margin.Right > MaxWidth Then
MaxWidth = Item.Control.Left + Item.Control.Width + Item.Control.Padding.Right + Item.Control.Margin.Right
End If
If Item.Control.Top + Item.Control.Height + Item.Control.Padding.Bottom + Item.Control.Margin.Bottom > MaxHeight Then
MaxHeight = Item.Control.Top + Item.Control.Height + Item.Control.Padding.Bottom + Item.Control.Margin.Bottom
End If
End If
Next
PageSize.Height = MaxHeight - MinHeight
PageSize.Width = MaxWidth - MinWidth
ViewSize = Me.Size
'Now are we bigger than the Control...
'check the width first
PictureBox1.Visible = False
CheckWidth()
' then check the height
If CheckHeight() Then
' then dbl check the width..
CheckWidth()
End If
Scrolling = False
End Sub
Rather than putting all the code
here, a full working user control ready to add to your application is in the
download.