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]};
Related
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;
}
}
I am encountering a strange phenomenon: I have a WinForms application with four GroupBoxes, all four initially empty. I use this to track new followers/unfollowers on Twitter, planning on expanding its use once this functions properly.
It does work properly for new followers. For these I have a GroupBox called grpFollow, to which I add LinkLabels with the ScreenNames of my new followers like this:
var folTop = new Point(grpFollow.Left + 5, grpFollow.Top + 5);
lblFollowers.Text = Properties.Settings.Default.FollowersNow.Count.ToString();
lblFriends.Text = Properties.Settings.Default.FriendsNow.Count.ToString();
var ctr = 1;
foreach (var fol in newFollowers)
{
var kvp = LookupUser(fol);
if (string.IsNullOrEmpty(kvp.Key)) continue;
var linklabel = new LinkLabel()
{
Text = kvp.Value,
Width = 200,
Height = 15,
Location = folTop,
Visible = true,
Name = $"follbl{ctr}"
};
ctr++;
linklabel.Links.Add(0, linklabel.Width-1, $"https://twitter.com/{kvp.Key}");
linklabel.Click += Linklabel_Click;
grpFollow.Controls.Add(linklabel);
folTop.Y += 25;
}
LookupUser is just a function that passes the user id to the Twitter API and returns the name & screen_name of that user. Works fine, no problem. LinkLabels added nicely, no problem there either.
The trouble is with the other group boxes, e.g. the one for new friends:
folTop = new Point(grpFriends.Left + 15, grpFriends.Top + 15);
ctr = 1;
foreach (var fol in newFriends)
{
var kvp = LookupUser(fol);
if (string.IsNullOrEmpty(kvp.Key)) continue;
var llabel = new LinkLabel()
{
Text = kvp.Value,
Width = 200,
Height = 15,
Location = folTop,
Visible = true,
Name = $"frdlbl{ctr}"
};
ctr++;
llabel.Links.Add(0, llabel.Width - 1, $"https://twitter.com/{kvp.Key}");
llabel.Click += Linklabel_Click;
grpFriends.Controls.Add(llabel);
folTop.Y += 25;
}
As you can see, the logic is identical (because I want to extract this part to a separate method to avoid repetition). The location is set relative to the grpFriends group box, everything else is the same. Yet, the LinkLabel does not show, i.e. the second group box remains (visually) empty!
I have set a breakpoint to check what might go wrong. I single stepped through: the correct screen name is being retrieved, the location is correct, the control is added - but nothing ever shows up.
P.S: This code is in the RunWorkerCompleted method of a background worker, no further code is executed after this point.
Any idea why the Label isn't displayed?
Edit: I'll be damned!
I just changed the location of the grpFriends LinkLabel to 10,10: it appears, juuust clipping the lower border of my friends' group.
Now here is where this gets weird for me:
As you can see, the group has a Y value of 351. Point (10,10) should not even be in the box. So, it seems that the location is the culprit and the original code created the label outside the form.
Replacing grpFriends.Top as Y value with grpFriends.Top - grpFriends.Height got me closer. The LinkLabel is farther down from the top than I'd like but that's not so bad.
Very strange.
Okay, thanks to #raBinn I figured it out. The mistake here was me assuming, the LinkLabels needed a location relative to the Form. However, when adding them to the Controls collection of a GroupBox, they automatically assume a location relative to the GroupBox, not the Form.
So, my code is now basically the same for all four group boxes and looks like this:
PopulateGroup(newFollowers, grpFollow);
PopulateGroup(unFollow, grpLost);
PopulateGroup(newFriends, grpFriends);
PopulateGroup(unFriend, grpDitched);
And:
private void PopulateGroup(List<string> collPeople, GroupBox groupBox)
{
var folTop = new Point(12, 25);
foreach (var fol in collPeople)
{
var kvp = LookupUser(fol);
if (string.IsNullOrEmpty(kvp.Key)) continue;
var linklabel = new LinkLabel()
{
Text = kvp.Value,
Width = 200,
Height = 15,
Location = folTop
};
ctr++;
linklabel.Links.Add(0, linklabel.Width - 1, $"https://twitter.com/{kvp.Key}");
linklabel.Click += Linklabel_Click;
groupBox.Controls.Add(linklabel);
folTop.Y += 25;
}
}
So it doesn't really matter whether the group box is on the left or right, top or bottom of the form...
I'm new working with C# and I'm asking on here because I didn't find a solution searching in google and other questions on SO, I will explain what my example application does:
When I run it it display a form with a textbox by default, this textbox always will be shown, after type some text and press enter it will generate a new textbox and a new button (all the controls even the default textbox are inside a panel), and the new textboxes have the same functionality as the default textbox, when I click on the button generated next to its textbox it removes the button itself and the textbox but after that if I remove some random textboxes it leaves a space between these controls, how can reorganize this content to dont let space between them?
As you can see in the image, can you tell me how can fix this or give me an advice to achieve this? thank you, by the way this is the method I use to generate the buttons and textboxes
private void GenerarTextBox()
{
panelContenedor.VerticalScroll.Value = panelContenedor.VerticalScroll.Minimum;
TextBox tb = new TextBox();
tb.Text = "Prueba " + id;
tb.Name = "txtBox" + id;
tb.KeyDown += new KeyEventHandler(TextBox_Keydown);
Button bt = new Button();
bt.Cursor = Cursors.Hand;
bt.Text = "X";
bt.Name = "btnPrueba" + id;
bt.Click += new EventHandler(ClickBotones);
Point p = new Point(20, 30 * id);
Point pb = new Point(130, 30 * id);
tb.Location = p;
bt.Location = pb;
panelContenedor.Controls.Add(tb);
panelContenedor.Controls.Add(bt);
tb.Focus();
id++;
}
And this to remove the textboxes and the buttons
private void ClickBotones(object sender, EventArgs e)
{
Button bt = sender as Button;
string nombreBoton = bt.Name;
string idBoton = nombreBoton.Substring(9);
string nombreTextBox = "txtBox" + idBoton;
foreach (Control item in panelContenedor.Controls.OfType<Control>())
{
if (item.Name == nombreTextBox)
{
panelContenedor.Controls.Remove(item);
panelContenedor.Controls.Remove(bt);
}
}
}
You could place your dynamic controls on a FlowLayoutPanel. Either directly or grouped together in a Panel or UserControl.
Set the FlowDirection property of the FlowLayoutPanel to TopDown. The FlowLayoutPanel will then arrange your controls automatically. You can also set the WrapContents property to False and AutoScroll to true to make the scroll bar appear.
Alternatively you can use FlowDirection = LeftToRight, place the text box and the button directly on the FlowLayoutPanel and let the child controls wrap (WrapContents = True). In the child controls, a new property FlowBreak appears. It can be set to True for the last control to appear in a row and let the next one wrap independently of the width of the FlowLayoutPanel.
You can also play with the Margin property of the child controls to control their layout in the FlowLayoutPanel as the Location property becomes useless.
The FlowLayoutPanel (as well as the Panel) is available in the Toolbox in the section "Containers".
When you delete the controls, you need to do a recalc of the positions. So when you have added them in sequence, you can go with:
bool repos = false;
Point p;
foreach (Control item in panelContenedor.Controls.OfType<Control>())
{
if (repos)
{
Point tmp = item.Location;
item.Location = p;
p = tmp;
}
if (item.Name == nombreTextBox)
{
panelContenedor.Controls.Remove(item);
panelContenedor.Controls.Remove(bt);
repos = true;
p = item.Location;
}
}
So I'm trying to add dynamically inputs and retreive date from them and only do an action when a user presses enter in the input. So, what I'm currently doing is appending the inputs to a stacklayout. which works fine. Also the naming works. I use the following function;
private void GenerateGTKInputs()
{
// Based on the settings for the tour
// we generate the correct inputs in the stacklayout given in the XAML
// First: clear all the children
stackpanel_gtk.Children.Clear();
if (inp_team_number.Text != "")
{
// get the data for the part and the class etc...
var data_gtk = tour_settings[(Convert.ToInt32(inp_team_number.Text.Substring(0, 1)) - 1)].tour_data[inp_tour_part.SelectedIndex].gtks;
// Now: Make the layout
foreach (var item in data_gtk)
{
// Stack panel (main 'div')
StackPanel main_stack_panel = new StackPanel()
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Left
};
// Text blok with the name of the GTK
TextBlock gtk_name = new TextBlock()
{
FontWeight = FontWeights.Bold,
Text = "GTK " + item.gtk
};
// Input field
Xceed.Wpf.Toolkit.MaskedTextBox input = new Xceed.Wpf.Toolkit.MaskedTextBox()
{
Margin = new Thickness(15, 0, 0, 0),
Width = 40,
Height = Double.NaN, // Automatic height
TextAlignment = TextAlignment.Center,
Mask = "00:00",
Name = "gtk_" + item.gtk
};
// Add to the main stack panel
main_stack_panel.Children.Add(gtk_name);
main_stack_panel.Children.Add(input);
// Append to the main main frame
stackpanel_gtk.Children.Add(main_stack_panel);
}
}
}
Now as you can see, I'm giving them a name, but I have no clue what so ever on how to "bind" an trigger event (KeyDown) with a check on enter button press with dynamic names. Could anyone help me out here?
You "bind" a trigger event by adding to the appropriate event of the control - in this case you need to create a method like :
private void OnKeyDown(object sender, System.Windows.Input.KeyEventArgs keyEventArgs)
{
// Get reference to the input control that fired the event
Xceed.Wpf.Toolkit.MaskedTextBox input = (Xceed.Wpf.Toolkit.MaskedTextBox)sender;
// input.Name can now be used
}
and add this to the KeyDown event :
input.KeyDown += OnKeyDown;
You can chain as many event handlers as you want by adding further handlers in this fashion.
This can be done at any time after you create the control. To "unbind" the event you "subtract" it from the event :
input.KeyDown -= OnKeyDown;
I have a form over which I have a panel. When a button is clicked, I turn my panel to invisible and I show a DataGridView ( a table from sql ). My problem is... is there any way to put a button somewhere on my form so I can go back to the main menu ( on click, to set the panel visibility to true ).
I tried using DataGridViewButtons but it's not ok. I want just one single button, anywhere on my form, called Back that would take me back. Somehow, it won't let me place a button anywhere unless I have a panel. Is there any solution?
I can add any code if necessary, just tell me which
menu.Visible = false;
DataGridView dgw = new DataGridView();
dgw.Parent = this;
dgw.Location = new Point(
this.ClientSize.Width / 3 - dgw.Size.Width / 2,
this.ClientSize.Height / 4 - dgw.Size.Height / 2);
dgw.Anchor = AnchorStyles.None;
dgw.AutoSize = true;
dgw.BackColor = Color.FromArgb(210, 121, 84);
dgw.Text = "Login";
dgw.Padding = new Padding(10);
dgw.BorderStyle = BorderStyle.FixedSingle;
While it is far more easy to create your form from the designer, placing a Button on the form code is as simple as creating the datagrid.
private void CreateButton()
{
var button = new Button
{
Text = "Press me",
Name = "SomeButtonName",
Location = new Point(10,10),
Size = new Size(100,20),
Visible = true,
};
button.Click += (s, e) => this.ButtonClicked();
this.Controls.Add(button);
}
private void ButtonClicked()
{
// this will fire on click
}
You can call `this.CreateButton()' everywhere you'll like.