WinForms: variable number of dynamic TextBox controls - c#

I have to create variable number of Labels and next to them TextBox controls - arranging the whole thing into a column, each line a Label and a TextBox. If the my Main window is smaller than the total height of all the TextBox controls, somehow I need a scrollbar which can scroll the list of TextBoxes. Pressing the enter key would have to take the focus to the next TextBox and also scroll in case of too many TextBoxes.
This is a rather generic problem, I guess there are already some pre-baked solutions for this.
Any advice?

Use a TableLayoutPanel. You can dynamically add controls, specify their row/column, and it will maintain a scrollbar for you (with the appropriate settings). It has its quirks, but should suit for this case.
If you use the WinForms designer to place the TableLayoutPanel, then you can use it to also define the style of the columns. You can also vary the style of each row as suggested by Tcks.
To add the control with a specified row/column:
int column = 42;
int row = 7;
myTableLayoutPanel.Controls.Add(new TextBox(), column, row);

You can use TableLayoutPanel as container for controls (Labels and TextBoxes) and create them dynamicaly in code.
Example:
void Form1_Load( object sender, EventArgs e ) {
const int COUNT = 10;
TableLayoutPanel pnlContent = new TableLayoutPanel();
pnlContent.Dock = DockStyle.Fill;
pnlContent.AutoScroll = true;
pnlContent.AutoScrollMargin = new Size( 1, 1 );
pnlContent.AutoScrollMinSize = new Size( 1, 1 );
pnlContent.RowCount = COUNT;
pnlContent.ColumnCount = 3;
for ( int i = 0; i < pnlContent.ColumnCount; i++ ) {
pnlContent.ColumnStyles.Add( new ColumnStyle() );
}
pnlContent.ColumnStyles[0].Width = 100;
pnlContent.ColumnStyles[1].Width = 5;
pnlContent.ColumnStyles[2].SizeType = SizeType.Percent;
pnlContent.ColumnStyles[2].Width = 100;
this.Controls.Add( pnlContent );
for ( int i = 0; i < COUNT; i++ ) {
pnlContent.RowStyles.Add( new RowStyle( SizeType.Absolute, 20 ) );
Label lblTitle = new Label();
lblTitle.Text = string.Format( "Row {0}:", i + 1 );
lblTitle.TabIndex = (i * 2);
lblTitle.Margin = new Padding( 0 );
lblTitle.Dock = DockStyle.Fill;
pnlContent.Controls.Add( lblTitle, 0, i );
TextBox txtValue = new TextBox();
txtValue.TabIndex = (i * 2) + 1;
txtValue.Margin = new Padding( 0 );
txtValue.Dock = DockStyle.Fill;
txtValue.KeyDown += new KeyEventHandler( txtValue_KeyDown );
pnlContent.Controls.Add( txtValue, 2, i );
}
}
void txtValue_KeyDown( object sender, KeyEventArgs e ) {
if ( e.KeyCode == Keys.Enter ) {
SendKeys.Send( "{TAB}" );
}
}

Also, check the generated code of a window/usercontrol after adding some controls to it. It should give you a good idea of how this could be done dynamically. (I'm talking about the somename.cs.designer file)

Related

dynamically created labels overlap each other

I want to create labels that will follow one another. I have a grid name WordTemplateLayout to which I want to add the labels. I add them dynamically on the wpf window constructor after InitializeComponent() is called. Here is the method creating the labels:
private void CreateWordTemplate()
{
IList<char> template = CurrentGame.Template;
double widthPerBox = WordTemplateLayout.Width / template.Count;
//template.Count being a number, irrelevant to the question
for (int i = 0; i < template.Count; i++)
{
var templateVisual = new Label();
templateVisual.Name = "c" + i;
templateVisual.Width = widthPerBox;
templateVisual.Height = WordTemplateLayout.Height;
templateVisual.Background = new SolidColorBrush(Colors.Aqua);
WordTemplateLayout.Children.Add(templateVisual);
}
}
the problem being, that what actually appends is that instead of the labels lining up one after the other, they overlap each other:
The aqua box is all the labels overlap each other
so what I am asking, is how can I make the labels line up (horizontally) instead of to overlap?
As others have pointed out, you're better off using a StackPanel, or learning how to use viewmodels and data binding. Having said that, to answer your direct question, here's how you'd do it programmatically.
**NOTE: Ignore the 5 that I'm passing in to the methods, and instead use your template.Count. This is was just for me to get it to work on my end.
public MainWindow()
{
InitializeComponent();
CreateGridLayout(5);
CreateWordTemplate(5);
}
// Define the Grid layout
// If you want the labels to follow one another horizontally, define columns.
// If you want them stacked vertically, instead define rows.
private void CreateGridLayout(int count)
{
for (int i = 0; i < count; i++)
{
WordTemplateLayout.ColumnDefinitions.Add(new ColumnDefinition());
}
}
private void CreateWordTemplate(int count)
{
double widthPerBox = WordTemplateLayout.Width / 5;
for (int i = 0; i < 5; i++)
{
var templateVisual = new Label();
templateVisual.SetValue(Grid.ColumnProperty, i); // SET YOUR COLUMN HERE!
templateVisual.Name = "c" + i;
templateVisual.Width = widthPerBox;
templateVisual.Height = WordTemplateLayout.Height;
templateVisual.Background = new SolidColorBrush(Colors.Aqua);
templateVisual.Content = templateVisual.Name;
WordTemplateLayout.Children.Add(templateVisual);
}
}
you need to use another layout so the new elements will get in normal order. try stackPanel.
you can use grid and give each new label Row=index.
you can give each new label margin, like newLabel.margin-top = index*50

C# NET WinForms Alternative to DataGridView for displaying user controls

I'm writing a C# .NET WinForms app in which I have to create a new instance of a user control, which will contain several controls (TextBox, Button, CheckBox, etc.). The user controls must be created one at a time and stacked (arranged vertically).
Options I've tried:
FlowLayoutPanel doesn't have an index value that I can use to keep track of the many user controls that will be added when the user clicks the "Add New Item" button.
DataGridView doesn't have a column type to accommodate a user control. While the DataGridView's functionality is much closer to what I need, I haven't found any code to add a column of type UserControl.
Any ideas?
Maybe TableLayoutPanel is what you need.
protected override void OnLoad( EventArgs e )
{
var tableLayoutPanel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
AutoScroll = true
};
Controls.Add( tableLayoutPanel );
// To reset row and columns use this
// Reset row count and styles
tableLayoutPanel.RowCount = 0;
tableLayoutPanel.RowStyles.Clear();
// Reset columns count and styles
tableLayoutPanel.ColumnCount = 0;
tableLayoutPanel.ColumnStyles.Clear();
// For horizontal alignment we need add empty columns to fill space
// |___emty fill___|Realcoulmn|___empty fill___|
tableLayoutPanel.ColumnCount = 3;
tableLayoutPanel.ColumnStyles.Add( new ColumnStyle( SizeType.Percent, 100 ) ); // Fill space
tableLayoutPanel.ColumnStyles.Add( new ColumnStyle( SizeType.AutoSize ) ); // Real column with controls
tableLayoutPanel.ColumnStyles.Add( new ColumnStyle( SizeType.Percent, 100 ) ); // Fill space
tableLayoutPanel.SuspendLayout();
for ( var i = 0; i < 5; i++ )
{
AddControl( tableLayoutPanel, 1 );
}
tableLayoutPanel.ResumeLayout( true );
}
public void IterateOverControls( TableLayoutPanel table )
{
// Iterate over all rows
for ( var i = 0; i < table.RowCount; i++ )
{
var control = table.GetControlFromPosition( 1, i ); // Column 1
}
}
public void AddControl( TableLayoutPanel tableLayoutPanel, int column )
{
var btn = new Button { Text = "Hello vertical stack!" };
btn.Click += button_Click;
// Add row to tableLayoutPanel and set it style
tableLayoutPanel.RowCount++;
tableLayoutPanel.RowStyles.Add( new RowStyle( SizeType.Absolute, btn.Height + 5 ) );
// Add control to stack
tableLayoutPanel.Controls.Add( btn, column, tableLayoutPanel.RowCount - 1 );
}
private void button_Click( object sender, EventArgs e )
{
var btnControl = sender as Control;
if ( btnControl == null )
return;
var tableLayoutPanel = btnControl.Parent as TableLayoutPanel;
if ( tableLayoutPanel == null )
return;
AddControl( tableLayoutPanel, 1 );
}
}

C# DataGridView: GetCellDisplayRectangle.Location returns wrong values

In a DataGridView, when I adjust, at run time, the with of a column such way that its header's height is modified (because of switching from one line to two lines displaying or vice-versa), the GetCellDisplayRectangle.Location() still indicates the old vertical position of the cell(s). The dataGridView1.Refresh() does not help. Location information is refreshed if, for example, I resize the table dimensions (it is anchored to the form). Why is that? What can I do? Thanks.
Update:
What I want to achieve actually is to have time pickers in the second column of my DataGridView. I know is old topic and I know MSDN example which I don't like because it's huge, cumbersome and not exactly what I need. I use a simple solution: with each new DataGridView line I add DateTimePickers configured as I want to DataGridView controls and I locate them to the desired cell(s) position. This solution almost works: my problem is now that I have to relocate/resize/hide/show the pickers every time I scroll or I change the with/height of the cells.
Code to append DataGridView line:
List<DateTimePicker> tplist;
int index;
int x, y;
private void button_append_Click( object sender, EventArgs e )
{
DateTimePicker tp;
tp = new DateTimePicker();
tp.Format = DateTimePickerFormat.Custom;
tp.CustomFormat = "mm:ss";
tp.ShowUpDown = true;
tp.Value = DateTime.Now.Date;
tp.Value = new DateTime( 2000, 1, 1, 0, index, 0 );
dataGridView1.Controls.Add( tp );
dataGridView1.Rows.Add( new object[] { "line " +
Convert.ToString( index ),
"val " + Convert.ToString( index ) } );
Rectangle frame = dataGridView1.GetCellDisplayRectangle( 1,
dataGridView1.RowCount - 1, false );
tp.Size = frame.Size;
tp.Location = frame.Location;
if( dataGridView1.RowCount <=
dataGridView1.FirstDisplayedCell.RowIndex +
dataGridView1.DisplayedRowCount( true ) )
tp.Visible = true;
else tp.Visible = false;
tplist.Add( tp );
if( tplist.Count == 1 )
{ x = tplist[ 0 ].Location.X; y = tplist[ 0 ].Location.Y; }
index++;
}
The varible "index" is just for debugging.
Every time I change cell's geometry ory I remove lines or I scroll the DataGridView I call the function:
private void Adjust()
{
for( int i = 0; i < tplist.Count; i++ )
{
tplist[ i ].Location = dataGridView1.GetCellDisplayRectangle( 1, i, true ).Location;
if( tplist[ i ].Location.X == 0 )
{
tplist[ i ].Location = new Point( x, y );
}
else
{
x = tplist[ 0 ].Location.X; y = tplist[ 0 ].Location.Y;
}
tplist[ i ].Size = dataGridView1.GetCellDisplayRectangle( 1, i, false ).Size;
if( i >= dataGridView1.FirstDisplayedCell.RowIndex && i < dataGridView1.FirstDisplayedCell.RowIndex + dataGridView1.DisplayedRowCount( true ) )
tplist[ i ].Visible = true;
else tplist[ i ].Visible = false;
}
dataGridView1.Refresh();
}
Note that for first line (for first DataGridView line, not for the top visible line depending on scroll) I have to do some acrobatics specifically. This is because some times GetCellDisplayRectangle.Location returns 0,0 for cell 0,1. Anyway, I actually have problems with GetCellDisplayRectangle.Location for all the cells.

Tricky : Array of controls dynamically created causing problems

I put "Tricky" in the title because I'm aware that it will be hard to understand precisely what I want but I'll try to be clear.
I have a textBox with an event for TextChange which allow me to create the number of texboxes the user want (we will call it tChange).
Here is a part of the code for this event :
int tester;
bool flag = false;
if (!Int32.TryParse(tChange.Text, out tester))
{
flag = false;
return;
}
else
{
num = Convert.ToInt16(tChange.Text);
flag = true;
}
if (flag == true)
{
if (num >= 1)
{
for (int i = 0; i < num; i++)
{
this.Size = new Size(590, 225 + 105 * i);
textBoxesQ[i] = new TextBox();
this.Controls.Add(textBoxesQ[i]);
textBoxesQ[i].Size = new Size(45, 20);
textBoxesQ[i].Location = new Point(25, 100 + 100 * i - 1);
}
}
}
So the user enter the value and everything is OK. If he wants to change the number in the tChange, no problem too! The form is resize and the TextBoxes are created. However if he does this (change the value of tChange), everything goes wrong! Errors like
Index out of range
or I can't get the values from the TextBoxes etc..
I started thinking that the TexBoxes were created in front of the previous ones and the error came from this, so I tried to put the new ones to front, bring the old ones to front but none worked..
textBoxesQ[i].BringToFront();
textBoxesQ[i].SendToBack();
I also tried to delete the old ones before creating the new but I think that my code was wacky and it didn't work at all.
textBoxesQ[i].Dispose();
EDIT : As #Dr. Stitch said, It may come from not reinitializing the TextBoxes each time the text in tChange is changed. Now I just need to figure out how to make it happen.
You need to do three things when the number changes:
Remove any existing textboxes
Create a new array with the new size, and save a reference to that new array into your field (textBoxesQ)
Initialize the array
So something like:
if (textBoxesQ != null)
{
foreach (var textBox in textBoxesQ)
{
Controls.Remove(textBox);
}
}
textBoxesQ = new TextBox[size];
for (int i = 0; i < size; i++)
{
var textBox = new TextBox
{
Size = new Size(45, 20);
Location = new Point(25, 100 + 100 * i - 1);
};
Controls.Add(textBox);
textBoxesQ[i] = textBox;
}

How do I find and use a programatically-created control?

I've created a number of buttons on a form based on database entries, and they work just fine. Here's the code for creating them. As you can see I've given them a tag:
for (int i = 0; i <= count && i < 3; i++)
{
btnAdd.Text = dataTable.Rows[i]["deviceDescription"].ToString();
btnAdd.Location = new Point(x, y);
btnAdd.Tag = i;
this.Controls.Add(btnAdd);
}
I use these buttons for visualising a polling system. For example, I want the button to be green when everything is fine, and red when something is wrong.
So the problem I'm running into is referencing the buttons later so that I can change their properties. I've tried stuff like the following:
this.Invoke((MethodInvoker)delegate
{
// txtOutput1.Text = (result[4] == 0x00 ? "HIGH" : "LOW"); // runs on UI thread
Button foundButton = (Button)Controls.Find(buttonNumber.ToString(), true)[0];
if (result[4] == 0x00)
{
foundButton.BackColor = Color.Green;
}
else
{
foundButton.BackColor = Color.Red;
}
});
But to no avail... I've tried changing around the syntax of Controls.Find() but still have had no luck. Has anyone encountered this problem before or know what to do?
If you name your buttons when you create them then you can find them from the this.controls(...
like this
for (int i = 0; i <= count && i < 3; i++)
{
Button btnAdd = new Button();
btnAdd.Name="btn"+i;
btnAdd.Text = dataTable.Rows[i]["deviceDescription"].ToString();
btnAdd.Location = new Point(x, y);
btnAdd.Tag = i;
this.Controls.Add(btnAdd);
}
then you can find it like this
this.Controls["btn1"].Text="New Text";
or
for (int i = 0; i <= count && i < 3; i++)
{
//**EDIT** I added some exception catching here
if (this.Controls.ContainsKey("btn"+buttonNumber))
MessageBox.Show("btn"+buttonNumber + " Does not exist");
else
this.Controls["btn"+i].Text="I am Button "+i;
}
Put these buttons in a collection and also set the name of the Control rather than using its tag.
var myButtons = new List<Button>();
var btnAdd = new Button();
btnAdd.Text = dataTable.Rows[i]["deviceDescription"].ToString();
btnAdd.Location = new Point(x, y);
btnAdd.Name = i;
myButtons.Add(btnAdd);
To find the button use it.
Button foundButton = myButtons.Where(s => s.Name == buttonNumber.ToString());
Or Simply
Button foundButton = myButtons[buttonNumber];
In your case I would use a simple Dictionary to store and retrieve the buttons.
declaration:
IDictionary<int, Button> kpiButtons = new Dictionary<int, Button>();
usage:
Button btnFound = kpiButtons[i];
#Asif is right, but if you really want to utilize tag you can use next
var button = (from c in Controls.OfType<Button>()
where (c.Tag is int) && (int)c.Tag == buttonNumber
select c).FirstOrDefault();
I'd rather create small helper class with number, button reference and logic and keep collection of it on the form.

Categories

Resources