I'm currently trying to add some custom controls to a panel in winforms.
Every control will be docked and build something like a "list".
Now i'm trying to implement a feature to select/deselect every control.
Its working fine, my problem is that it seems to be very slow sometimes.
Currently its about ~50 custom controls in the panel.
modtable.Click += (s, e) =>
{
foreach (Control m in pnl_ucMods.Controls)
{
if(m is ModTableEntry)
{
if(m != modtable)
{
((ModTableEntry)m).BackColor = SystemColors.Control;
}
else if (m == modtable && m.BackColor == SystemColors.Control)
m.BackColor = SystemColors.ActiveCaption;
else
m.BackColor = SystemColors.Control;
}
}
};
Whenever i click on one of the controls it will change the backcolor, on a second click it will change it back but thats only working if i wait like a second before i click again. If i click to fast, nothing happens and i have to click again.
I understand that winforms is not designed to have tons of controls and i understand that foreach will need some time to loop through all the controls, but maybe someone here has a small idea how to improve the code and maybe solve this problem.
tl;dr
Click on one of the custom controls in the panel will change its backcolor. (Selected)
Every other control will change the backcolor too (deselected)
If the clicked control is already selected, it will deselect.
EDIT:
A small example to test that problem.
Just create a new project, add the code and call it.
private void addPanels()
{
Panel newPanel = new Panel();
newPanel.AutoScroll = true;
newPanel.Dock = DockStyle.Fill;
this.Controls.Add(newPanel);
for (int i = 0; i < 50; i++)
{
Panel childPanel = new Panel();
childPanel.Size = new Size(100, 30);
childPanel.Dock = DockStyle.Top;
childPanel.Click += (s, e) =>
{
foreach (Control p in newPanel.Controls)
{
if (p is Panel)
{
if (p != childPanel)
((Panel)p).BackColor = SystemColors.Control;
else if (p == childPanel && p.BackColor == SystemColors.Control)
p.BackColor = SystemColors.ActiveCaption;
else
p.BackColor = SystemColors.Control;
}
}
};
newPanel.Controls.Add(childPanel);
}
}
Use MouseDown event instead of Click event.
When you click twice too fast, it would be a DoubleClick event and no other Click event will raise.
With your permission, Reza.
Related
I am creating dynamically buttons in windows forms by using the code below. I want to have a context menu with different options pop up when a button is being right clicked. I made the context menu strp and it shows up but i cant figure out how to make the different options perform the functions i have written for them. Any idea how i can connect the items to their functions ?
void AddButton(Shape s){
Button b = new Button();
b.Text = s.name;
b.Width = sceneItemsList.Width-6;
b.TabStop = false;
b.FlatStyle = FlatStyle.Flat;
b.BackColor = Color.LightGray;
b.FlatAppearance.BorderColor = Color.Black;
ContextMenuStrip cm = new ContextMenuStrip();
cm.Items.Add("Deselect");
cm.Items.Add("Edit");
if (s is GroupShape)
{
cm.Items.Add("Ungroup");
}
cm.Items.Add("Delete");
b.ContextMenuStrip = cm;
}
Figured it out! This is my code if anyone else is struggling with this. Basically you make a loop that goes through all the opptions and adds a click eventhadler
cm.Items[0].Click += (sender, e) => {
Console.WriteLine("ok");
};
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;
}
}
I have a form that contains a TableLayoutPanel with various controls and labels in it. One of them is a custom control that inherits from ComboBox that has extra auto-complete behavior (auto-completes on any text rather than just left to right). I didn't write the code for this control, so I'm not super familiar with how it works, but essentially upon clicking on the Combobox, it adds a ListBox below the ComboBox, within the same Panel of the TableLayoutPanel, that covers the normal drop down.
Unfortunately, the TableLayoutPanel prevents the ListBox from being fully visible when added, and only one item is shown. The goal is to get it to look like a normal ComboBox which would drop down to cover any controls below it.
Is there any way to allow a control that is in a TableLayoutPanel to overlap the TableLayoutPanel to get this to work as I want? I want to avoid any controls moving around due to the TableLayoutPanel growing to accommodate the ListBox.
Relevant code from the control:
void InitListControl()
{
if (listBoxChild == null)
{
// Find parent - or keep going up until you find the parent form
ComboParentForm = this.Parent;
if (ComboParentForm != null)
{
// Setup a messaage filter so we can listen to the keyboard
if (!MsgFilterActive)
{
Application.AddMessageFilter(this);
MsgFilterActive = true;
}
listBoxChild = listBoxChild = new ListBox();
listBoxChild.Visible = false;
listBoxChild.Click += listBox1_Click;
ComboParentForm.Controls.Add(listBoxChild);
ComboParentForm.Controls.SetChildIndex(listBoxChild, 0); // Put it at the front
}
}
}
void ComboListMatcher_TextChanged(object sender, EventArgs e)
{
if (IgnoreTextChange > 0)
{
IgnoreTextChange = 0;
return;
}
InitListControl();
if (listBoxChild == null)
return;
string SearchText = this.Text;
listBoxChild.Items.Clear();
// Don't show the list when nothing has been typed
if (!string.IsNullOrEmpty(SearchText))
{
foreach (string Item in this.Items)
{
if (Item != null && Item.ToLower().Contains(SearchText.ToLower()))
{
listBoxChild.Items.Add(Item);
listBoxChild.SelectedIndex = 0;
}
}
}
if (listBoxChild.Items.Count > 0)
{
Point PutItHere = new Point(this.Left, this.Bottom);
Control TheControlToMove = this;
PutItHere = this.Parent.PointToScreen(PutItHere);
TheControlToMove = listBoxChild;
PutItHere = ComboParentForm.PointToClient(PutItHere);
TheControlToMove.Anchor = ((System.Windows.Forms.AnchorStyles)
((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)));
TheControlToMove.BringToFront();
TheControlToMove.Show();
TheControlToMove.Left = PutItHere.X;
TheControlToMove.Top = PutItHere.Y;
TheControlToMove.Width = this.Width;
int TotalItemHeight = listBoxChild.ItemHeight * (listBoxChild.Items.Count + 1);
TheControlToMove.Height = Math.Min(ComboParentForm.ClientSize.Height - TheControlToMove.Top, TotalItemHeight);
}
else
HideTheList();
}
Images:
Desired behavior
Current behavior
Going on the suggestion from TaW, I came up with a tentative solution. This form isn't re-sizable but does auto-size so that it looks ok if the user changes their DPI in Windows.
To resolve this, I moved the control out of the TableLayoutPanel to an arbitrary position in the Parent of the TableLayoutPanel. On form loading, I summed the coordinates of the TableLayoutPanel and an empty panel in the cell that I wanted the control to be located on top of. This worked for my needs but it feels like a kludge.
The better solution is probably to use Control.PointToScreen and Control.PointToClient methods, however I wasn't able to get these methods to give me the correct coordinates.
I have a Panel named panel1. panel1 has a "mosuseHover" eventhandler .panel1 also has some controls like pictureBox , label etc.
When i move mouse on panel1 , the event fire correctly , but when the mouse courser goes on panel1 controls , like pictureBox , the event not work .
how can i make event to be invoke when mouse courser is on child controls.
I should note that i dont want create eventhandler for each child contol.
Best Regards
You can add an IMessageFilter to implement your own global MouseHover for your Panel like this:
//This code uses some reflection so you must add using System.Reflection
public partial class Form1 : Form, IMessageFilter
{
public Form1(){
InitializeComponent();
Application.AddMessageFilter(this);
}
public bool PreFilterMessage(ref Message m) {
Control c = Control.FromHandle(m.HWnd)
if (HasParent(c,panel1)){
if (m.Msg == 0x2a1){//WM_MOUSEHOVER = 0x2a1
//Fire the MouseHover event via Reflection
typeof(Panel).GetMethod("OnMouseHover", BindingFlags.NonPublic | BindingFlags.Instance)
.Invoke(panel1, new object[] {EventArgs.Empty});
}
}
return false;
}
//This method is used to check if a child control has some control as parent
private bool HasParent(Control child, Control parent) {
if (child == null) return false;
Control p = child.Parent;
while (p != null) {
if (p == parent) return true;
p = p.Parent;
}
return false;
}
}
NOTE: The code above is implemented to work for nested controls of any level in your Panel. If your panel contains only child controls stopping at level 1. You can change the code a bit by using c = Control.FromChildHandle(m.Hwnd) and check the control's parent by c==panel1 without having to use the HasParent method to check for child-ancestor relationship.
You may create an eventhandler on the children and simply call the panels handler with the same arguments.
Also have a look at this thread
I countered the same problem. I solved it by creating a MouseEnter event and in it I declared Control ctl = sender as Control; Then I called ctl's Parent, with, Control panel = ctl.Parent; Now do whatever you want, as in panel.BackColor = Color.Red;
Like this:
private void label_MouseEnter(object sender, System.EventArgs e)
{
Control ctl = sender as Control; // gets the label control
Control panel = ctl.Parent; // gets the label's parent control, (Panel in this case)
if (ctl != null)
{
ctl.Font = new Font(ctl.Font.Name, 11, FontStyle.Underline); // change the font of the Label and style...
panel.BackColor = Color.Blue; // change the panel's backColor
ctl.Cursor = Cursors.Hand;
// do more with the panel & label
}
}
But don't forget to iterate through all the controls and to get the Panel and whatever that's inside the Panel.
Like this:
public YourApp()
{
InitializeComponent();
foreach (Control objCtrl in this.Controls) // get all the controls in the form
{
if (objCtrl is Panel) // get the Panel
{
objCtrl.MouseEnter += new EventHandler(panel_MouseEnter); // do something to the panel on MouseEnter
objCtrl.MouseLeave += new EventHandler(panel_MouseLeave);
foreach (Control ctl in objCtrl.Controls) // get all the controls inside the Panel
{
if (ctl is Label) // get the label inside the panel
{
ctl.MouseEnter += new System.EventHandler(label_MouseEnter);
ctl.MouseLeave += new EventHandler(label_MouseLeave);
ctl.Click += new EventHandler(label_Click);
}
if (ctl is PictureBox) // get the pictureBox inside the panel
{
ctl.MouseEnter += new EventHandler(label_MouseEnter);
}
}
}
}
}
You can figure out the rest.
Hope this helps.
I use this solution(in code below) to add multiply buttons on panel. It works ok but it takes too long, when it tries to add a lot of buttons (for an example 40). I want to ask, if any one knows of a better solution for this situation? I was thinking of creating all possible buttons at program start-up, but in this case will start-up take too long, especially if there will be really lot of buttons (this scenario is possible)?
while (data.Read())
{
btnName = Convert.ToString(data["Name"]);
btnColor = (color == string.Empty) ? Convert.ToString(data["Color"]) : color;
categoryId = Convert.ToInt16(data["CategoryId"]);
//both category and article table's contains this data!
if (categoryId == articleCatId || cl == typeof(Category))
{
Button newbtn = new Button();
newbtn.TextAlign = ContentAlignment.MiddleCenter;
newbtn.Click += (sender, e) => method(sender, e);
newbtn.Text = btnName;
newbtn.Name = "button-" + btnName;
newbtn.Height = size;
newbtn.Width = size;
newbtn.Font = new Font("Microsoft Sans Serif", fontH);
newbtn.Location = new Point(paddingL, paddingT);
newbtn.BackColor = ColorTranslator.FromHtml(btnColor);
location.Controls.Add(newbtn);
num += 1;
if ((num - 1) / inline == 1) { paddingT += size; paddingL = 2; num = 1; }
else { paddingL = paddingL + size; }
}
}
You probably cannot reduce the number of buttons you need to create, so you then have some options to speed it up a little bit:
Add the buttons to an object that is not visible. Only when you're done adding buttons, you make the object visible.
Call SuspendLayout on the parent control to stop it from trying to layout itself. Then call ResumeLayout when you're done adding buttons.
Use a more lightweight control than a button, that is more appropriate for the task. For example a Listbox, Combobox, or several checkboxes or option buttons styled as normal buttons.
Write your own lightweight Button control that does exactly what you want but no more.