View Sidebar

A Million Little Pieces Of My Mind

Organica: SqueezePanel control

By: Paul S. Cilwa Viewed: 9/26/2021
Posted: 6/24/2021
Updated: 8/7/2021
Topics/Keywords: #Organica #Shell #UserControl Page Views: 222
Creating the basic SqueezePanel user control and tying it into the SqueezeBox container.

Now that we have the basic SqueezeBox control working (and no, we aren't done with it, yet), we are going to create a minimal, special control to reside within it. This control, called the SqueezePanel, is not a general-purpose control—That is, because of its specialization, there's truly no reason why anyone would ever want to use it in a stand-alone fashion. And, therefore, we don't want it to show up in the Toolbox—in fact, we'll want the SqueezeBox to create these panels automatically, when the SqueezeBox itself is deposited onto a form (or another container control). And the easiest way to make this happen, is to create the new control as a class member of SqueezeBox!.

A Nested Class

To define a nested class, all you have to do is place a regular class definition into an existing class definition. In my code, I added it just before the SqueezeBox class's End Class statement. The first few lines declare the class and attach a designer to it. (So, yes, we'll also have to add a simple designer; we'll get to that shortly.)

<Designer(GetType(SqueezeBoxDesigner))> Public Class SqueezeBox … <Designer(GetType(SqueezePanelDesigner))> Public Class SqueezePanel Inherits System.Windows.Forms.UserControl End Class End Class

Like the SqueezeBox, the SqueezePanel has a Style property, which can take any value from its Styles Enum. However, where the outer SqueezeBox style can be Horizontal or Vertical, the SqueezePanel styles have the following meanings, which will impact the meaning of the upcoming Extent property.

  • The Fixed style indicates that Extent is a specific number of pixels.
  • Percent means that Extent is a percentage of the SqueezeBox's available area.
  • Automatic means the SqueezePanel will adjust to fill the available space. Extent can be read but not changed except by the resizing of the SqueezeBox itself.
Public Enum Styles Fixed Percent Automatic End Enum … End Class End Class

We next declare a few local variables and the New() event handler. Most of these have to do with the panel's Extent, the panel's only adjustable dimension.

Friend MyParent As SqueezeBox Private MyStyle As Styles Private MyExtent As Int16 Private MyMinExtent As Int16 Private MyMaxExtent As Int16 Public Sub New(aParent As SqueezeBox) Me.MyParent = aParent Me.Name = "SqueezePanel" Me.Margin = New System.Windows.Forms.Padding(0) Me.Padding = New System.Windows.Forms.Padding(10) Me.Cursor = Cursors.Default End Sub … End Class End Class

The property management code for the Style property is straightforward enough.

<Category("Appearance")> Public Property Style As Styles Get Return MyStyle End Get Set(Value As Styles) MyStyle = Value End Set End Property … End Class End Class

Next, we need a little private function to make sure any value assigned to Extent isn't out of range, with the range defined as being between 1 and some arbitrary number. However, the programmer using the SqueezeBox control may not want to set a minimum, and/or a maximum; in which case MyMinExtent and MyMaxExtent will still contain zero values.

Private Function InRange(ByVal Value As Int16) As Int16 If MinExtent > 0 Then Value = Math.Max(Value, MinExtent) End If If MaxExtent > 0 Then Value = Math.Min(Value, MaxExtent) End If Return Value End Function … End Class End Class

The programmer will logically expect the size of the panel to change should they change the Extent value. However, it's important to remember that Extent is not a direct provider of the control's Size, which is a property of the type of a special class of its own (also called Size). Also, remember Extent can either refer to a number of pixels or a percentage of the available space. So, when a new value is assigned to Extent, it is converted as needed (in a function to be described shortly) to a Size object, which is then passed on to the base class's property. (To prevent anyone's circumventing this mechaniism, we will hide the Size property from the designer.)

<Category("Appearance")> Public Property Extent As Int16 Get Return MyExtent End Get Set(Value As Int16) MyExtent = InRange(Value) MyBase.Size = GetSize() Invalidate() End Set End Property … End Class End Class

The property management code for MinExtent and MaxExtent includes an interesting line (italicized):

<Category("Appearance")> Public Property MinExtent As Int16 Get Return MyMinExtent End Get Set(Value As Int16) MyMinExtent = Value Extent = Extent Invalidate() End Set End Property <Category("Appearance")> Public Property MaxExtent As Int16 Get Return MyMaxExtent End Get Set(Value As Int16) MyMaxExtent = Value Extent = Extent Invalidate() End Set End Property … End Class End Class

This line, Extent = Extent, looks faintly recursize, or at least redundant. However, remember the Extent property code ensures that Extent falls within the range specified by MinExtent and MaxExtent, which may change when those properties are changed. It also translates Extent's value to Size. So the interesting line makes perfect sense: "Take the current value of Extent, and assign it to itself, which will then subject it to possible new limitations and resize the panel as neccessary."

In previous code (property management for Extent), we invoked a to-be-implemented function called GetSize(). The purpose of this function is simple: To return a Size object appropriate to the requested Extent. What makes this interesting is that the construction of this object varies depending on the parent's (SqueezeBox) Style, that is, whether this panel is being displayed as a row, or as a column.

Friend Function GetSize() As Size Select Case MyParent.Style Case SqueezeBox.Styles.Columns Return New Size(GetExtentAsPixels(), MyParent.Height) Case Else Return New Size(MyParent.Width, GetExtentAsPixels()) End Select End Function … End Class End Class

However, this doesn't do the actual conversion of Extent to a pixel count. That's handled by a separate function, GetExtentAsPixels(). Note the use of a Percentage class function, described in a previous step.

Also, note that this returns only the BorderWidth if the panel's Style is Automatic. That's because the dimensions of an automatic panel must be set by the containing SqueezeBox itself, after tallying the Extents of all the other panels.

Friend Function GetExtentAsPixels() As Int16 Dim x As Int16 = MyExtent If Style = Styles.Automatic Then x += MyParent.BorderWidth End If Select Case Style Case SqueezeBox.Styles.Columns If Style = Styles.Percent Then x = Percentage.CalcValue(x, MyParent.Width) End If Case Else If Style = Styles.Percent Then x = Percentage.CalcValue(x, MyParent.Height) End If End Select Return x End Function End Class End Class

Setting Extents in the SqueezeBox Class

In the SqueezeBox class, we left stubs for CreatePanels()( and DistributePanels(). Now we can fill them in. Here is the code for CreatePanels():

Private Sub CreatePanels() If Not IsLoaded Then Exit Sub Me.SuspendLayout() Dim P As SqueezePanel For Each P In MyPanels Me.Controls.Remove(P) Next MyPanels = New List(Of SqueezePanel) For i As Byte = 1 To MyPanelCount P = New SqueezePanel(Me) With P .Style = SqueezePanel.Styles.Fixed .Extent = 100 .Location = New Point((i - 1) * (.Extent + BorderWidth), 0) .Margin = New System.Windows.Forms.Padding(0, 0, BorderWidth, 0) .BackColor = DefaultColors(i - 1) End With Me.Controls.Add(P) MyPanels.Add(P) Next ResumeLayout() End Sub

At this point, and remembering to Build the project first so that the most recent version of the SqueezeBox and SqueezePanel code, if we open our project's Frame form, we may see something like this:

Why don't the new panels extend the full height of the containing SqueezeBox? Well, that's because, in Design Mode, the Visual Studio environment has an initial, arbitrary size for forms that is subsequently updated by the values you've given the form's properties. The method that is invoked when the form is resized is DistributePanels(), currently, a mere stub. It's time to expand it.

Friend Sub DistributePanels() Me.SuspendLayout() If Not IsLoaded Then Exit Sub Dim P As SqueezePanel Dim AutoP As SqueezePanel = Nothing Dim PrevP As SqueezePanel = Nothing Dim TotalExtent As Int16 = 0 For Each P In MyPanels TotalExtent += IIf(P.Style = SqueezePanel.Styles.Automatic, 0, P.GetExtentAsPixels() + BorderWidth) If P.Style = SqueezePanel.Styles.Automatic Then AutoP = P Next P If AutoP IsNot Nothing Then AutoP.Extent = Width - TotalExtent - BorderWidth End If Dim i As Byte = 0 For Each P In MyPanels i += 1 If PrevP Is Nothing Then P.Location = New Point(0, 0) Else Dim L As New Point(PrevP.Location) If Style = Styles.Columns Then L.X += PrevP.Size.Width + BorderWidth Else L.Y += PrevP.Size.Height + BorderWidth End If P.Location = L End If P.Size = P.GetSize PrevP = P Next ResumeLayout(True) End Sub

This can be tested by again Building the project and opening (or tabbing to) the Frame designer.

And ya know what? Since we now have the panels displaying in the SqueezeBox, we can go ahead in the form designer and set its BackColor to Transparent.

Evenly Distributed by Percentage

So now we have four columns displayed, each of a fixed width. They do not spread across the SqueezeBox. But that's because they are all Fixed style. There are two ways to make sure the panels fill the available space: One is to have one panel be Automatic; the other is to make all the panels be Percent style and set the Extent to 25% each (four times 25% is 100%).

Private Sub CreatePanels() If Not IsLoaded Then Exit Sub Me.SuspendLayout() Dim P As SqueezePanel For Each P In MyPanels Me.Controls.Remove(P) Next MyPanels = New List(Of SqueezePanel) For i As Byte = 1 To MyPanelCount P = New SqueezePanel(Me) With P .Style = SqueezePanel.Styles.Percent .Extent = 25 .Location = New Point((i - 1) * (.Extent + BorderWidth), 0) .Margin = New System.Windows.Forms.Padding(0, 0, BorderWidth, 0) .BackColor = DefaultColors(i - 1) End With Me.Controls.Add(P) MyPanels.Add(P) Next ResumeLayout() End Sub

Real-Life Example

In reality, and specifically in the Organica project of which the SqueezeBox and SqueezePanels are a part, we'll want a combination of all three styles.

Private Sub CreatePanels() If Not IsLoaded Then Exit Sub Me.SuspendLayout() Dim P As SqueezePanel For Each P In MyPanels Me.Controls.Remove(P) Next MyPanels = New List(Of SqueezePanel) For i As Byte = 1 To MyPanelCount P = New SqueezePanel(Me) With P Select Case i Case 1 .Style = SqueezePanel.Styles.Percent .Extent = 10 Case 2 .Style = SqueezePanel.Styles.Fixed .Extent = 150 Case 3 .Style = SqueezePanel.Styles.Automatic Case Else .Style = SqueezePanel.Styles.Percent .Extent = 10 End Select .Location = New Point((i - 1) * (.Extent + BorderWidth), 0) .Margin = New System.Windows.Forms.Padding(0, 0, BorderWidth, 0) .BackColor = DefaultColors(i - 1) End With Me.Controls.Add(P) MyPanels.Add(P) Next ResumeLayout() End Sub

Next

Next we'll want to expose SqueezePanel properties to the designer.