Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically adding controls to TableLayoutPanel behaves differently based on originating thread

Tags:

c#

.net

winforms

I am creating a WinForm project that displays messages from a server and a client. Client messages are added via standard UI events like Click and KeyPress. Server messages are added via a background thread that is listening for messages and "alerts" the UI when they arrive. The "alert" happens through an EventHandler that then makes use of the Invoke method since we are requesting to modify the UI from a thread that did not create it.

When messages are added from the client they appear as desired (i.e. correctly sized, wrapping and resizing performs correctly). When messages are added from the background thread via Invoke only the first message displays correctly and even then only initially. Once the form is resized the wrapping stops working and the first message is clipped based on how much space is available. Messages after the first get sized completely incorrectly in that it appears that their height gets set to 1 pixel or something very small so that only the tops of the characters from the first line can be seen. Since I am new to WinForms I am suspicious that I am doing something wrong as far as the correct way to programmatically add controls to the UI.

Both the client and the background thread call the same method to add the controls to the form; only the background calls it via Invoke. I am at a loss as to why the added controls behave differently based on the event they originated from because either way they are being added the exact same way. My only thought is that I am missing something obvious because I am new to WinForms.

Anyway, enough of the explanation, here is the relevant code:

private void AddMessage(string message)
{
    tableLayoutPanel1.SuspendLayout();

    RichTextBox rtb = new RichTextBox();
    rtb.AppendText(message);
    rtb.Multiline = true;
    rtb.WordWrap = true;
    rtb.Dock = DockStyle.Top;

    rtb.ReadOnly = true;
    rtb.BorderStyle = BorderStyle.None;
    rtb.ScrollBars = RichTextBoxScrollBars.None;
    rtb.BackColor = Color.LightBlue;
    rtb.Font = new Font("Tahoma", 8, FontStyle.Italic);
    rtb.Resize += rtb_Resize;
    rtb.MinimumSize = new Size(150, 15);
    rtb.Margin = new Padding(1, 0, 0, 5);
    rtb.Padding = new Padding(3, 3, 3, 3);
    rtb.Height = rtb.GetPositionFromCharIndex(rtb.Text.Length).Y;

    tableLayoutPanel1.RowStyles.Add(new RowStyle(SizeType.AutoSize));
    tableLayoutPanel1.RowCount = tableLayoutPanel1.RowStyles.Count;
    tableLayoutPanel1.Controls.Add(rtb);
    tableLayoutPanel1.SetColumnSpan(rtb, 2);

    tableLayoutPanel1.ResumeLayout(true);
    tableLayoutPanel1.AutoScrollPosition = new Point(0, tableLayoutPanel1.VerticalScroll.Maximum);
}

private void richTextBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    if (e.KeyChar == 13 && !String.IsNullOrEmpty(richTextBox1.Text))
    {
        AddMessage(richTextBox1.Text);
        richTextBox1.Clear();
    }
}

public void PostMessage(object sender, MessageEventArgs e)
{
    BeginInvoke(new AddMsg(AddMessage), new object[] { e.Message });
}

public delegate void AddMsg(string m);

private void rtb_Resize(object sender, EventArgs e)
{
    var rtb = (RichTextBox) sender;
    rtb.Height = rtb.GetPositionFromCharIndex(rtb.Text.Length).Y;
}

Ok, now some more explanation. Client messages originate from richTextBox1_KeyPress and server messages originate from PostMessage (PostMessage is assigned to the EventHandler for the background thread when it gets a message). Both as you can see call AddMessage which adds a RichTextBox with the message to the TableLayoutPanel. The rtb_Resize method contains logic for a way I found online to get a RickTextBox to auto size since apparently it does not support this; maybe this a possible source to my problem.

I'll also provide the designer code below for the TableLayoutPanel since this seems to be helpful and relevant from other posts I've seen on here.

// 
// tableLayoutPanel1
// 
this.tableLayoutPanel1.AutoScroll = true;
this.tableLayoutPanel1.AutoSize = true;
this.tableLayoutPanel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.tableLayoutPanel1.BackColor = System.Drawing.Color.White;
this.tableLayoutPanel1.ColumnCount = 2;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 50F));
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(1, 10);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(5, 5, 12, 5);
this.tableLayoutPanel1.RowCount = 1;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.Size = new System.Drawing.Size(272, 276);
this.tableLayoutPanel1.TabIndex = 0;

The TableLayoutPanel does contain an initial row since the designer does not allow you to have one with no rows, but I clear all the rows out in the Form's constructor after the standard call to InitializeComponent.

Hopefully I've provided enough here to give people the information they need but please let me know if you have any questions or need to see other parts of the code and I will provide them as quickly as possible. Also, if it matters I'm using Visual Studio 2008 and .NET 3.5.

Thank you.

like image 939
asymptoticFault Avatar asked Oct 26 '11 20:10

asymptoticFault


1 Answers

Based on what I can read, you are adding the controls to the TableLayoutPanel correctly, however one thing to keep in mind is that the TableLayoutPanel is not designed to be modified programmatically, due to the unusual way that it handles its RowStyles, as you have discovered.

The Answer

That being said, the reason the controls aren't wrapping as expected is because your Resize handler is occurring too "late". Your rtb_resize event handler is probably being called after the TableLayoutPanel has already laid itself out. You could in theory solve this problem by forcing the TableLayoutPanel to re-perform its layout after all of its inner controls have been laid out. To accomplish this, you will need to handle the SizeChanged event of the TableLayoutPanel, and within the handler, re-adjust the size of all the inner controls, and then call tableLayoutPanel1.PerformLayout through a BeginInvoke.

Here is some sample code:

bool _layingOut = false;

void tableLayoutPanel1_SizeChanged(object sender, EventArgs e)
{
     if(_layingOut)
          return;

     //TODO: resize your inner controls here

     //this will force the TableLayoutPanel to lay itself out a second time
     _layingOut = true;
     tableLayoutPanel1.BeginInvoke(new Action(() => tableLayoutPanel1.PerformLayout());
     _layingOut = false;
}

Another Way

However, I would suggest finding another approach that doesn't involve a TableLayoutPanel. A more ideal solution in WinForms if you need a special layout, is to create your own LayoutEngine class. This allows you to write a method called Layout that WinForms will call out to whenever a control needs to be re-laid out, such as during a resize. Then you can simply specify the location and size of each child control in any way you see fit. That is essentially all the TableLayoutPanel is doing anyway.

To learn more about creating a LayoutEngine, and for a surprisingly decent MSDN example, see: http://msdn.microsoft.com/en-us/library/system.windows.forms.layout.layoutengine(v=VS.90).aspx

like image 147
Kevin McCormick Avatar answered Oct 16 '22 12:10

Kevin McCormick