Search code examples
c#.netwinformstablelayoutpanel

How to adjust TableLayoutPanel to wrap content by height and weight? (dynamically)


I created a Form that I am going to use as pop-up.
I need that this pop-up contains title, ProgressBar and percentage text.

So, for this purpose, I choose to use TableLayoutPanel as a control in my Form.

All these UI elements are created in dynamically. For test now I am using Label instead of real ProgressBar.

public partial class TestFormDeleteIt : Form
{
    public TestFormDeleteIt()
    {
        InitializeComponent();

        AutoSize = true;
        var flp = CreateTableLayoutPanel();
        Controls.Add(flp);
    }

    private Control CreateTableLayoutPanel()
    {
        // TableLayoutPanel Initialization
        var panel = new TableLayoutPanel
        {
            ColumnCount = 2,
            RowCount = 2,
            Dock = DockStyle.Fill
        };

        //Adjust column size in percentage
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 70F));
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30F));

        //Adjust row size in percentage
        panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
        panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));

        //Fill content
        panel.Controls.Add(
            new Label()
            {
                Text = "state",
                Dock = DockStyle.Fill
            }, 
            /*column*/ 0, /*row*/ 0);

        panel.Controls.Add(
            new Label()
            {
                Text = "ProgressBar",
                Dock = DockStyle.Fill
            },
            /*column*/ 0, /*row*/ 1);

        panel.Controls.Add(
            new Label()
            {
                Text = "100%",
                Dock = DockStyle.Fill
            }, 
            /*column*/ 1, /*row*/ 1);
        return panel;
    }
}

So, as you can see I create TableLayoutPanel divided by rows and columns and for each view that it holds I set Dock = DockStyle.Fill, because I don't want to hardcode a fixed size.

What am I expecting is that each UI element fills a Cell in the TableLayoutPanel and the parent Form itself will adjust its size by the TableLayoutPanel that holds all the other Controlss.

But actually I get this result:

enter image description here

The Parent Form doesn't wrap content, it looks like it has fixed size and the TableLayoutPanel tries to fill the Form ClientArea. I've set AutoSize = true;, but it has no visible affect.

What am I doing wrong?

EDIT

Thanks, @Jimi now it looks like much better

enter image description here

I also added

MinimumSize = new System.Drawing.Size(600, Height)

to TLP, but question is - as you can see each row has like min height. And it looks like space between each row. Why is it happens?

EDIT2

I would like to notice that if I change this

panel.Controls.Add(
    new Label()
    {
        Text = "state",
        Dock = DockStyle.Fill
    },
    /*column*/ 0, /*row*/ 0);

to this:

var label1 = new Label()
{
    Text = "state",
    Dock = DockStyle.Fill
};

panel.SetRow(label1, 0);
panel.SetColumn(label1, 0);

it doesn't work... What am I doing wrong?

and here is my code now

public partial class TestFormDeleteIt : Form
{
    public TestFormDeleteIt()
    {
        InitializeComponent();

        AutoSize = true;
        AutoSizeMode = AutoSizeMode.GrowAndShrink;
        MinimumSize = new System.Drawing.Size(200, 0);

        var flp = CreateTableLayoutPanel();
        Controls.Add(flp);
    }

    private TableLayoutPanel CreateTableLayoutPanel()
    {
        // TableLayoutPanel Initialization
        var panel = new TableLayoutPanel
        {
            ColumnCount = 2,
            RowCount = 2,
            Dock = DockStyle.Fill
        };

        //Adjust column size in percentage
        panel.ColumnStyles.Clear();
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 90F));
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 10F));

        //Adjust row size in percentage
        panel.RowStyles.Clear();
        panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
        panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));

        //Fill content
        panel.Controls.Add(
            new Label()
            {
                Text = "state",
                Dock = DockStyle.Fill
            },
            /*column*/ 0, /*row*/ 0);

        panel.Controls.Add(
            CreateProgressBar(),
            /*column*/ 0, /*row*/ 1);

        panel.Controls.Add(
            new Label()
            {
                Text = "100%",
                Dock = DockStyle.Fill
            }, 
            /*column*/ 1, /*row*/ 1);

        panel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
        panel.AutoSize = true;

        return panel;
    }

    private Control CreateProgressBar() => new ProgressBar()
    {
        Visible = true,
        Dock = DockStyle.Top
    };
}

Problem is - that even if I set static width as 200 -> MinimumSize = new Size(200, 0) in my Form, when I open it I see that it takes like 1000.

enter image description here

Why is it happens?


Solution

  • The scenario:

    • A bare-bones Form, no specific dimensions applied. No child controls.
    • A TableLayoutPanel is build from scratch at run-time. No specific dimensions
      • Two Rows and two Columns are created with size mode set to percentage
      • Three Control are created from scratch and added to three of the TableLayoutPanel's Cells
        The TableLayoutPanel must resize itself to the new content, which doesn't have a specific size (except the default size applied to the Windows Forms Controls), without squeezing out of existence the new child Controls
    • The Form, after the TableLayoutPanel is added to its Controls collection, must auto-size itself to the size of its (only) child Control, keeping a newly set minimum Width, but no minimum or maximum Height (so it can expand vertically, if needed, but not horizontally)
    • Finally, the auto-sized TableLayoutPanel must be docked inside the auto-sized Form container and all parts need to cooperate to generate the expected Layout

    How to proceed:

    The sequence of the operations will clearly determine the final result.

    1. The Form initializes: it's construtor calls the standard InitializeComponent() method. There are no child Controls, so there's actually nothing to layout. The Form will simply set the Size defined in the Designer.
    2. We now instruct the Form to AutoSize to its content and also that it's allowed to both grow and shrink its Size to accomplish this task. The Form won't actually do anything right now, since it cannot perform its Layout at this time (unless explicitly instructed, using the PerformLayout() method)
    3. We set a minimum Size, constraining its Width, but not its Height (setting MinimumSize = new Size(200, 0), we define a minimum Width, but the Height dimension is free to grow and also shrink, here - setting 0 as value, means no restrictions).
    4. The TableLayoutPanel and its child Controls are now created, using default Sizes (Windows Forms Controls have a default Size when created). The Labels will AutoSize to the size of their Text: this is the default behavior.
    5. After all child Controls are added to the TLP, the TLP is instructed to AutoSize itself, also specifying that it can grow and shrink to perform the Layout.
    6. The TableLayoutPanel is added to the Form container. Now, when a Control is added to the Controls collection, the internal method instructs the ContainerControl to SuspendLayout(), position the new Control and ResumeLayou() (without also performing the Layout of the child Controls, since this has just happened): at this point, the Form, instructed to AutoSize to its content, will calculate its PreferredSize and then auto-size itself to the new content.
    7. The TableLayoutPanel is now set to Dock to the parent Container. The Container is already auto-sized to the size of the TableLayoutPanel, so the TLP just fixes its anchors to the current Size of the Form.

    The final Layout will be performed before the Form is shown: anyway, at this point, all the Controls involved are already sized and positioned as instructed.

    Extra:

    ▶ The TableLayoutPanel's SetColumn() and SetRow() are called explicitly. The reason is partailly explained in the Remarks section of the two methods:

    This method reapplies the table layout to all controls in the TableLayoutPanel.

    In the current context, calling these methods is a plus. It's not strictly needed, because we just add Controls to the TLP, never remove one.
    But, when Controls are added to and removed from the TLP while the TLP is set to AutoSize to its content (this applies to the current context), the TLP won't actually perform the Layout as expected: the Cells will, in most cases (yes, not always...) maintain their original Size when a Control is removed (removed or disposed, the result is the same).

    Some notes on the matter are found here:
    This code (different language but simple to read) can easily tested to reproduce the problem
    Dynamic TableLayoutPanel Controls Keep Border Width

    ▶ The ColumnStyles and RowStyles are cleared and new styles are added:
    - The original code actually adds new styles without removing the existing, default, ones: this can and will generate a layout problem in some situations. Some notes on the matter here:

    Remove Row inside TableLayoutPanel makes a layout problem
    Center multiple rows of controls in a FlowLayoutPanel


    public partial class TestFormDeleteIt : Form
    {
        protected internal ProgressBar pBar = null;
        protected internal Label lbl2 = null;
    
        public TestFormDeleteIt()
        {
            InitializeComponent();
    
            this.AutoSize = true;
            this.AutoSizeMode = AutoSizeMode.GrowAndShrink;
            this.MinimumSize = new Size(200, 0);
    
            var tlp = CreateTableLayoutPanel();
    
            tlp.AutoSizeMode = AutoSizeMode.GrowAndShrink;
            tlp.AutoSize = true;
    
            this.Controls.Add(tlp);
            tlp.Dock = DockStyle.Fill;
        }
    
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            Task.Run(() => UpdateProgress());
        }
    
        private TableLayoutPanel CreateTableLayoutPanel()
        {
            var panel = new TableLayoutPanel { ColumnCount = 2, RowCount = 2,
                CellBorderStyle = TableLayoutPanelCellBorderStyle.Single
            };
            panel.ColumnStyles.Clear();
            panel.RowStyles.Clear();
    
            panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 70F));
            panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30F));
    
            panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
            panel.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
    
            var lbl1 = new Label() { Text = "state", Dock = DockStyle.Fill };
            panel.Controls.Add(lbl1);
            panel.SetColumn(lbl1, 0);
            panel.SetRow(lbl1, 0);
    
            pBar = new ProgressBar() { Dock = DockStyle.Top };
            panel.Controls.Add(pBar);
            panel.SetColumn(pBar, 0);
            panel.SetRow(pBar, 1);
    
            lbl2 = new Label() { Text = "0%", Dock = DockStyle.Fill };
            panel.Controls.Add(lbl2);
            panel.SetColumn(lbl2, 1);
            panel.SetRow(lbl2, 1);
    
            return panel;
        }
    
        private async Task UpdateProgress()
        {
            for (int i = 1; i <= 100; i++) {
                BeginInvoke(new Action(() => {
                    pBar.Value = i;
                    lbl2.Text = (i / 100.0).ToString("P");
                }));
                await Task.Delay(50);
            }
        }
    }