.netvb.netwinformstablelayoutpanel

Dynamic TableLayoutPanel Controls Keep Border Width


By default I have a TableLayoutPanel with 1 column and three rows. The top and bottom rows have child TableLayoutPanels that contains buttons whereas the middle row has a TextBox. The buttons in the top and bottom rows are dynamically shown upon load based on values in My.Settings and by default there are five buttons (and therefore five columns).

The way that I'm dynamically setting the button's Text as well as if they're to be removed is as such (repeated four more times):

Dim visibleButtonCount As Integer = {My.Settings.ValueVisible1, My.Settings.ValueVisible2, My.Settings.ValueVisible3, My.Settings.ValueVisible4, My.Settings.ValueVisible5}.Where(Function(setting) setting).Count()
Dim buttonWidth As Double = 100 / visibleButtonCount

ButtonValueDown1.Text = $"- {My.Settings.Value1.ToString("N3")}"
ButtonValueUp1.Text = $"+ {My.Settings.Value1.ToString("N3")}"
If (Not My.Settings.ValueVisible1) Then
    ButtonValueDown1.Parent.Controls.Remove(ButtonValueDown1)
    ButtonValueUp1.Parent.Controls.Remove(ButtonValueUp1)

    With TableLayoutPanelDown.ColumnStyles.Item(0)
        .SizeType = SizeType.Absolute
        .Width = 0
    End With
    With TableLayoutPanelUp.ColumnStyles.Item(0)
        .SizeType = SizeType.Absolute
        .Width = 0
    End With
Else
    TableLayoutPanelDown.ColumnStyles.Item(0).Width = buttonWidth
    TableLayoutPanelUp.ColumnStyles.Item(0).Width = buttonWidth
End If

The issue that I'm running into is that whenever all 5 buttons are visible, the right-most Buttons in the top and bottom rows is flush with the TextBox in the middle row but whenever one or more of the Buttons are removed the right-most Buttons are no longer flush (see image). all buttons visible one or more buttons removed

What could be causing this? It is worth mentioning that all controls have a margin/padding of 0.


Solution

  • To allow a TableLayoutPanel to resize its Columns dynamically at run-time, when Controls of an undefined size are added to the TableLayoutPanel.Controls collection, one effective method is to set, at design-time, the Columns' TableLayoutStyle.SizeType to SizeType.Percent.
    When adding Columns using the designer, we can set the Percent value of each new Column to 100%. The TableLayoutPanel will automatically determine the correct percentage value, based on its current Size and the number of Columns added.

    In the current scenario, we have an outer TableLayoutPanel with one Column that hosts other TableLayoutPanels in some of its Cells.
    → The inner TableLayoutPanels are set to Dock = Fill, filling the Cell they occupy.
    → The inner TLPs will host a variable number of Controls, so they need to dynamically adjust the size of the child Controls to fill the width of the outer TLP container.

    Sample layout at design-time:

    TableLayoutPanel AutoSize DesignTime

    The upper, light gray, TableLayoutPanel in represent the tableLayoutPanelUp control in the sample code.

    Since the child Controls (Buttons, here) are added and/or removed at run-time, the inner TableLayoutPanel needs to resize them evenly to fill the outer TableLayoutPanel container size, to preserve the layout.

    ► The TableLayoutPanel can perform this layout as expected, provided that we specify the Column and Row (the Cell) that will contain a Control when one is added to its Controls collection.
    If we don't, the TableLayoutPanel cannot correctly determine the new size of its child Controls when it needs to resize them to fill the available space.


    In the sample code, the child Buttons are added to a List(Of Button) collection (for convenience), then the Buttons are added to a TableLayoutPanel (named tableLayoutPanelUp as in the question).

    ■ The child Controls are added in a SuspendLayout() / PerformLayout() section, to suspend the layout until all Controls are added and then perform the layout when all pieces are in place.

    ■ for each new Control, its Cell position is set explicitly, using the TableLayoutPanel's SetRow() and SetColumn() methods.

    ■ To remove a Control, the Control instance is specified using the Controls.Remove(Control) method (the Control is not disposed of, so it's still inside its container List) and the corresponding Column's ColumnStyles.Width (representing a Percent value) is set to 0.

    ■ When a child Control is added, again the SetRow() and SetColumn() are called to define the Cell that will contain the new Control. In this case, the Size (percentage) of the corresponding Column is set to 100 / tableLayoutPanelUp.ColumnCount. Since the TableLayoutPanel is docked, this will force it to evaluate the new values and generate a new layout, recalculating all values to fit the docking requirements.

    A visual sample of the result:

    TableLayoutPanel AutoSize Runtime

    In the sample code, the Add and Remove Buttons are named btnAddControl and btnRemoveControl, the ComboBox is named cboControlsIndexes

    Private tlpButtons As List(Of Button) = Nothing
    
    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
    
        tlpButtons = New List(Of Button)() From {
            New Button() With {.Dock = DockStyle.Fill, .FlatStyle = FlatStyle.Flat, .Text = "Button1"},
            New Button() With {.Dock = DockStyle.Fill, .FlatStyle = FlatStyle.Flat, .Text = "Button2"},
            New Button() With {.Dock = DockStyle.Fill, .FlatStyle = FlatStyle.Flat, .Text = "Button3"},
            New Button() With {.Dock = DockStyle.Fill, .FlatStyle = FlatStyle.Flat, .Text = "Button4"},
            New Button() With {.Dock = DockStyle.Fill, .FlatStyle = FlatStyle.Flat, .Text = "Button5"}
        }
    
        cboControlsIndexes.DisplayMember = "Text"
        cboControlsIndexes.DataSource = tlpButtons
    
        tableLayoutPanelUp.SuspendLayout()
    
        For i As Integer = 0 To tlpButtons.Count - 1
            tableLayoutPanelUp.Controls.Add(tlpButtons(i))
            tableLayoutPanelUp.SetRow(tlpButtons(i), 0)
            tableLayoutPanelUp.SetColumn(tlpButtons(i), i)
        Next
        tableLayoutPanelUp.ResumeLayout(True)
        tableLayoutPanelUp.PerformLayout()
    End Sub
    
    Private Sub btnRemoveControl_Click(sender As Object, e As EventArgs) Handles btnRemoveControl.Click
        Dim removeAtIndex As Integer = cboControlsIndexes.SelectedIndex
    
        tableLayoutPanelUp.Controls.Remove(tlpButtons(removeAtIndex))
        tableLayoutPanelUp.ColumnStyles(removeAtIndex).Width = 0
    End Sub
    
    Private Sub btnAddControl_Click(sender As Object, e As EventArgs) Handles btnAddControl.Click
        Dim addAtIndex As Integer = cboControlsIndexes.SelectedIndex
    
        tableLayoutPanelUp.Controls.Add(tlpButtons(addAtIndex))
        tableLayoutPanelUp.SetRow(tlpButtons(addAtIndex), 0)
        tableLayoutPanelUp.SetColumn(tlpButtons(addAtIndex), addAtIndex)
        tableLayoutPanelUp.ColumnStyles(addAtIndex).Width = 100 / tableLayoutPanelUp.ColumnCount
    End Sub