Dynamic and Immutable UIElement Arrays - c#

I have a WrapPanel that contains multiple Canvas of the same size. Each Canvas has some UIElements (i.e. TextBox, TextBlock, Buttons etc) as children. The creation of each Canvas (including its UIElement children) and the number of Canvas to be created are all done in run-time code behind (no XAML).
Initially I did the following, which works:
// declare as class properties, so all function can access them
WrapPanel wp = new WrapPanel();
Canvas[] cv = new Canvas[500];
TextBox[] tb = new TextBox[500];
// A function (e.g. a Button_Click event) that generates multiple Canvas in a WrapPanel
for (int i = 0; i<myInt; i++)
{
cv[i] = new Canvas();
tb[i] = new TextBox();
cv[i].Children.Add(tb[i]);
wp.Children.Add(cv[i]);
}
The above code is straight forwards works OK - Until I implement add, minus and destroy buttons where I could
1. Add an additional `Canvas` on a click event
2. Remove the last `Canvas` on a click event
3. Destroy a specific `Canvas` in the `WrapPanel` on a click event (may ba a little cross icon in each `Canvas`)
If I process some combination of the above 3 actions, I could easily create UIElements of the same index or create Canvas that goes out of the range of what it had been declared initially.
I looked into List however, each Canvas have different properties (each also has UIElement Children with different properties) and I can't figure out how List would solve it. A way for me to go around that is to declare a super large Array size for Canvas (e.g. Canvas[] cv = new Canvas[99999] but I though that's not very efficient.
Also, if I use List, how could I change properties of a specific UIElement after the they are generated? E.g. If i add 10 Canvas and add to List, and after they are all generated, I need to select the 5th Canvas and change a TextBox.Text, how do I access it like I did in an Array (i.e. tb[5].Text = "Hello")?
Can anyone show me some approaches to this problem?

Just a direct translation on how to do this with a list instead below. Given your code I don't know why you want to keep track of the canvas and textbox'es in a list - you can just access the children collection of the WrapPanel directly instead - let's assume you do need these separate collections for now.
WrapPanel wp = new WrapPanel();
List<Canvas> cvList = new List<Canvas>();
List<TextBox> tbList = new List<TextBox>();
public void Init()
{
int myInt = 500;
// in a function (e.g. a Button_Click event) to generate the multiple Canvas in a WrapPanel
for (int i = 0; i < myInt; i++)
{
Canvas cv = new Canvas();
TextBox tb = new TextBox();
cv.Children.Add(tb);
wp.Children.Add(cv);
cvList.Add(cv);
tbList.Add(tb);
}
}
public void AddCanvas()
{
Canvas cv = new Canvas();
TextBox tb = new TextBox();
cv.Children.Add(tb);
wp.Children.Add(cv);
cvList.Add(cv);
tbList.Add(tb);
}
public void RemoveCanvas()
{
wp.Children.RemoveAt(wp.Children.Count-1);
cvList.RemoveAt(cvList.Count - 1);
tbList.RemoveAt(cvList.Count - 1);
}
Edit for added comment:
E.g. If i add 10 Canvas, and after
they are all generated, I need to
select the 5th Canvas and change a
TextBox.Text, how do I access it like
I did in an Array (i.e. tb[5].Text =
"Hello")?
You can just access the children directly. You know you only added Canvas elements to your WrapPanel. So you could do (wp is the WrapPanel again):
TextBox textbox = (wp.Children[5] as Canvas).Children[0] as TextBox;
textbox.Text = "Hello";

Just operate directly on the WrapPanel's Children collection.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AddCanvasToWrapPanel(this.TestWrapPanel);
RemoveLastCanvasFromWrapPanel(this.TestWrapPanel);
AddCanvasToWrapPanel(this.TestWrapPanel);
DestroyCanvasAtWrapPanelIndex(this.TestWrapPanel, 0);
}
private void AddCanvasToWrapPanel(WrapPanel wp)
{
TextBox t = new TextBox();
Canvas c = new Canvas();
c.Children.Add(t);
wp.Children.Add(c);
}
private void RemoveLastCanvasFromWrapPanel(WrapPanel wp)
{
wp.Children.RemoveAt(wp.Children.Count - 1);
}
private void DestroyCanvasAtWrapPanelIndex(WrapPanel wp, int index)
{
wp.Children.RemoveAt(index);
}
}
}

Related

Dynamically Generate Groupboxes

I'm working on an inventory program and have finished the main functionality as a command line console app. I am now working on a version for winforms. I want to enable it to dynamically generate a Groupbox that holds some textboxes. I'd rather not design 50+ lines of multiple textboxes. Keep in mind I'm rather new to programming, having started with C# a year ago. I know next to nothing on Winforms.
I've tried to use dynamic item = new Groupbox();as a similar method allowed generation of objects at runtime. In the command line app, the way it works is that based on information given, a certain amount of objects are passed into the list _AllItems. I was thinking of generating the Groupboxes by using:
private void InitializeGroupBox()
{
foreach (Product product in Product._AllItems)
{
dynamic Item = new GroupBox();
}
}
But I have the feeling I'm nowhere near the correct method. Thanks to anybody who helps.
You will need to learn a bit more, but here is what I usually do to achieve what you asked.
internal class DynamicForm : Form
{
private FlowLayoutPanel mFlowLayoutPanel;
public DynamicForm()
{
mFlowLayoutPanel = new FlowLayoutPanel();
mFlowLayoutPanel.Dock = DockStyle.Fill;
// Add to this Form
this.Controls.Add(mFlowLayoutPanel);
InitializeGroupBox();
}
private void InitializeGroupBox()
{
mFlowLayoutPanel.SuspendLayout(); // Performance
for (int i = 1; i <= 20; i++) {
var groupBox = new GroupBox();
groupBox.Text = "GroupBox #" + i;
groupBox.Size = new Size(200, 50);
var textBox = new TextBox();
textBox.Dock = DockStyle.Fill;
// Add the TextBox to GroupBox
groupBox.Controls.Add(textBox);
// Add to this Form
mFlowLayoutPanel.Controls.Add(groupBox);
}
mFlowLayoutPanel.ResumeLayout(); // after suspend, resume!
}
}

Center multiple rows of controls in a FlowLayoutPanel

I'm trying to make a panel that would host dynamically added controls. There are two caveats:
There are going to be a lot of controls, so the panel should wrap the elements into new rows as it reaches its width limits and scroll vertically.
Controls can change in size, which would change the number of elements
that can fit into a single row.
I've seen a couple proposed solutions to center dynamic controls in a Form and rejected those for following reasons:
TableLayoutPanel - main issue I have with using this are the events when
elements grown and have to shift from 3-2 grid to 2-4, as
TableLayoutPanel does not seem to deal well with those.
AutoSize FlowLayoutPanel that can grow and shrink inside of
TableLayoutControl - my main problem with this solution is that it
only centers one row inside the Form, once it wraps to a new row, the
elements start to align to the right side. I suppose I can dynamically
add new FlowLayoutPanels to new rows of a TableLayoutControl, but then
I have a similar issue as the first scenario where I need to manually
redistribute elements between rows if they grow/shrink in size.
I was wondering if I'm missing some functionality that can help me handle grows/shrink event without creating my own variation of TableLayoutPanel?
Edit:
Below is a draft of functionality:
A - Two elements centered in panel
B - Third element added, all three are centered
C - Forth element added, wrapped to a new row and centered
D - Elements enlarged, now wraps on the second element, centered
Here's an example that reproduces the behaviour you described.
It makes use of a TableLayoutPanel which hosts multiple FlowLayoutPanels.
One important detail is the anchoring of the child FlowLayoutPanels: they need to be anchored to Top-Bottom: this causes the panel to be positioned in the center of a TableLayoutPanel Row.
Note that, in the Form constructor, one of the RowStyles is removed. This is also very important: the TLP (which is quite the eccentric guy), even if you have just one Row (or one Column, same thing), will keep 2 RowStyles. The second style will be applied to the first Row you add; just to the first one, not the others: this can screw up the layout.
Another anomaly, it doesn't provide a method to remove a Row, so I've made one. It's functional but bare-bones and needs to be extended, including further validations.
See the graphic sample about the current functionality. If you need help in implementing something else, leave a comment.
To build this add the following controls to a Form (here, called FLPTest1):
Add one Panel, set Dock.Bottom. Right click and SendToBack(),
Add a TableLayoutPanel (here, called tlp1), set:
AutoScroll = true, AutoSize = true,
AutoSizeMode = GrowAndShrink, Dock.Fill
Keep 1 Column, set to AutoSize and one Row, set to AutoSize
Add a FlowLayoutPanel (here, called flp1), positioned inside the TableLayoutPanel. It's not actually necessary, just for this sample code
Set its Anchor to Top, Bottom <= this is !important, the layout won't work correctly without it: it allows to center the FLP inside the TLP Row,
AutoSize = true, AutoSizeMode = GrowAndShrink
Add a Button (called btnAddControl)
Add a second Button (called btnRemoveControl)
Add a CheckBox (called chkRandom)
Paste the code here inside a Form's code file
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public partial class TLPTest1 : Form
{
public TLPTest1()
{
InitializeComponent();
tlp1.RowStyles.RemoveAt(1);
}
private void TLPTest1_Load(object sender, EventArgs e)
{
PictureBox pBox = new PictureBox() {
Anchor = AnchorStyles.None,
BackColor = Color.Orange,
MinimumSize = new Size(125, 125),
Size = new Size(125, 125),
};
flp1.Controls.Add(pBox);
tlp1.Controls.Add(flp1);
}
Random rnd = new Random();
Size[] sizes = new Size[] { new Size(75, 75), new Size(100, 100), new Size(125, 125)};
Color[] colors = new Color[] { Color.Red, Color.LightGreen, Color.YellowGreen, Color.SteelBlue };
Control selectedObject = null;
private void btnAddControl_Click(object sender, EventArgs e)
{
Size size = new Size(125, 125);
if (chkRandom.Checked) size = sizes[rnd.Next(sizes.Length)];
var pBox = new PictureBox() {
Anchor = AnchorStyles.None,
BackColor = colors[rnd.Next(colors.Length)],
MinimumSize = size,
Size = size
};
bool drawborder = false;
// Just for testing - use standard delegates instead of Lambdas in real code
pBox.MouseEnter += (s, evt) => { drawborder = true; pBox.Invalidate(); };
pBox.MouseLeave += (s, evt) => { drawborder = false; pBox.Invalidate(); };
pBox.MouseDown += (s, evt) => { selectedObject = pBox; pBox.Invalidate(); };
pBox.Paint += (s, evt) => { if (drawborder) {
ControlPaint.DrawBorder(evt.Graphics, pBox.ClientRectangle,
Color.White, ButtonBorderStyle.Solid);
}
};
var ctl = tlp1.GetControlFromPosition(0, tlp1.RowCount - 1);
int overallWith = ctl.Controls.OfType<Control>().Sum(c => c.Width + c.Margin.Left + c.Margin.Right);
overallWith += (ctl.Margin.Right + ctl.Margin.Left);
if ((overallWith + pBox.Size.Width + pBox.Margin.Left + pBox.Margin.Right) >= tlp1.Width) {
var flp = new FlowLayoutPanel() {
Anchor = AnchorStyles.Top | AnchorStyles.Bottom,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
};
flp.Controls.Add(pBox);
tlp1.SuspendLayout();
tlp1.RowCount += 1;
tlp1.Controls.Add(flp, 0, tlp1.RowCount - 1);
tlp1.ResumeLayout(true);
}
else {
ctl.Controls.Add(pBox);
}
}
private void btnRemoveControl_Click(object sender, EventArgs e)
{
if (selectedObject is null) return;
Control parent = selectedObject.Parent;
selectedObject.Dispose();
if (parent?.Controls.Count == 0) {
TLPRemoveRow(tlp1, parent);
parent.Dispose();
}
}
private void TLPRemoveRow(TableLayoutPanel tlp, Control control)
{
int ctlPosition = tlp.GetRow(control);
if (ctlPosition < tlp.RowCount - 1) {
for (int i = ctlPosition; i < tlp.RowCount - 1; i++) {
tlp.SetRow(tlp.GetControlFromPosition(0, i + 1), i);
}
}
tlp.RowCount -= 1;
}
}

Placing multiple instances of same control on form

I am making an application in winforms which shows a blueprint in a picturebox, and I need to place parts on it programmatically. These parts needs to be clickable (thus they should be a user control), and then fire the corresponding click event (clicking on a part should display information unique to that part). I could say that I want to place custom buttons on my picture. Now, of course, I need only one click event, and change the displayed information according to selection, though I don't know how to "link" this event to each created button.
I have a list of parts right next to the picturebox, and selecting a part should make the associated control to appear on the form (and deselecting it should remove it, or at least make it hidden). At first, I thought I will create one control during design, and make it appear/disappear and relocate it with each selection. The problem is, that the user should be able to select multiple parts, and the program should show all selected parts on the blueprint.
As each blueprint is different, the number of parts cannot be defined in advance. Is it possible, to create multiple instances of the same control on the run? Or is there a workaround?
If you use controls for your picture elements( you do not determine anything from coordinates of mouse click) and each picture element is associated with only one menu control, then I can propose you to use the Tag property to associate the corresponding menu controls:
public Form1()
{
InitializeComponent();
this.CreatePictureRelatedControls();
}
private void CreatePictureRelatedControls()
{
Int32 xPictureControls = 50,
yPictureControls = 50,
xAssociatedControls = 200,
yAssociatedControls = 50,
yMargin = 10;
Int32 controlWidth = 125,
controlHeight = 20;
Int32 controlCount = 3;
// ---------Associated controls-----------------
var associatedControls = new Button[controlCount];
// Loop - creating associated controls
for (int i = 0; i < associatedControls.Length; i++)
{
var associatedButton = new Button()
{
Left = xAssociatedControls,
Top = yAssociatedControls + (i * (controlWidth + yMargin)),
Width = controlWidth,
Height = controlHeight,
Text = String.Format("associated control {0}", i),
Visible = false
};
// Event handler for associated button
associatedButton.Click += (sender, eventArgs) =>
{
MessageBox.Show(((Control)sender).Text, "Associated control clicked");
};
associatedControls[i] = associatedButton;
}
// ----------------- Picture controls ---------------
var pictureControls = new Button[controlCount];
// Loop - creating picture controls
for (int i = 0; i < pictureControls.Length; i++)
{
var pictureButton = new Button()
{
Left = xPictureControls,
Top = yPictureControls + (i * (controlWidth + yMargin)),
Width = controlWidth,
Height = controlHeight,
Text = String.Format("picture part button {0}", i),
// Use of tag property to associate the controls
Tag = associatedControls[i],
Visible = true
};
// Event hadler for picture button
pictureButton.Click += (sender, eventArgs) =>
{
Control senderControl = (Control)sender;
Control associatedControl = (Control)senderControl.Tag;
associatedControl.Visible = !associatedControl.Visible;
};
pictureControls[i] = pictureButton;
}
this.Controls.AddRange(associatedControls);
this.Controls.AddRange(pictureControls);
}
P.S. If you need to associate multiple controls then you can just set Tag property to some collection:
button.Tag = new Control[] {associated[1], associated[3]};

What kind of list control allows each item to have a different height?

What control should I use to make a list of items display? I want to be able to adjust the height of the item so its height can take up more space then another item in the same control list of items.
I looked at listboxes, but you can't adjust the size of the items. I've considered making blank entries for placeholders to be grouped as the same item but would rather not if possible.
What this control is to be used for is to represent chunks of time from the beginning of the day (the top) to the end of the day (the bottom).
I looked at listboxes, but you can't adjust the size of the items
But you can. Set the DrawMode property to OwnerDrawVariable.
You of course need to tell it how tall each item needs to be, that requires implementing the MeasureItem event. And of course you need to draw it, so you fill up the space that you reserved with MeasureItem, that requires implementing the DrawItem event. You'll find an excellent example in the MSDN Library article for MeasureItem.
A custom panel does it like a charm:
public class AutoPanel : Panel
{
public AutoPanel()
{
AutoScroll = true;
}
private int _nextOffset = 0;
public int ItemMarginX = 5;
public int ItemMarginY = 5;
public void Add(Control child)
{
child.Location = new Point(ItemMarginX, _nextOffset);
_nextOffset += (child.Height + ItemMarginY);
Controls.Add(child);
}
}
Add it to your form and add items to it like this:
panel1.Add(new Button { Text = "smaller item", Height = 20 });
panel1.Add(new Button { Text = "medium item", Height = 23 });
panel1.Add(new Button { Text = "larger item", Height = 32 });

How to access TabPages that are created in real-time?

Some inits done earlier in the code...
private List<System.Windows.Forms.TabPage> tab_pages = new List<System.Windows.Forms.TabPage>();
int tab_increment = 0;
Somewhere in the code, I create a bunch of tab pages in real-time.
for (i=0; i<5; i++)
{
tab_pages.Add( new System.Windows.Forms.TabPage() );
tab_pages[tab_increment].Location = new System.Drawing.Point(4, 22);
tab_pages[tab_increment].Name = 1 + tab_increment.ToString();
tab_pages[tab_increment].Size = new System.Drawing.Size(501, 281);
tab_pages[tab_increment].Text = tab_increment.ToString();
this.tabControl.Controls.Add(tab_pages[tab_increment]);
tab_increment += 1;
}
Now I would like to access elements that are these tab pages. Also let's pretend that I created different elements on each page (example, tabPage[0] a button, tabPage[1] a checkbox, etc), how do I access them knowing that everything was added dynamically?
Check this approach:
void Walk(Control control)
{
foreach (Control c in control.Controls)
{
//just walking through controls...
//...do something
//but remember, it could contain containers itself (say, groupbox or panel, etc.)...so, do a recursion
if (c.Controls.Count > 0)
Walk(c);
}
//or
foreach (Button btn in control.Controls.OfType<Button>())
{
//an example of how to walk through controls sub array of certain type
//this loop won't have a single iteration if this page contains no Buttons
//..so you can replace Button
//and have some certain code for different types of controls
}
}
And launch it for tabcontrol:
foreach (TabPage page in tabControl1.TabPages)
Walk(page);
I guess there is no special need to have separate collection of tabpages for one tabcontrol, as soon as it has TabPages property.
In the code above I used Enumerable.OfType Method to get a subcollection of controls of certain type.
As for your code, try this:
for (int i = 0; i < 5; i++)
{
this.tabControl.Controls.Add(new System.Windows.Forms.TabPage());
this.tabControl.TabPages[i].Text = i.ToString();
//...do whatever you need
//...
//besdies, I think, ther's no need in tab_increment...loop index works well enough
}
In order to add pages, I think that using
tabControl.TabPages.Add(new TabPage("Name"));
or in your case
this.tabControl.TabPages.Add(tab_pages[tab_increment]);
is more suitable.
In order to access them you could use
TabPage tp = tabControl.TabPages[i]; //where i is the index of your TabPage
and you can use TabPage.Controls.Add of the Controls property to add any Control on the TabPage like:
Button btn = new Button();
btn.Name = "Button name";
tp.Controls.Add(btn);
You can use the Controls property on the TabPage object. Each control in the collection is given to you as a Control, and it is up to you to cast them to the type that you want.

Categories

Resources