How can I order a list of labels by their location on screen?
My labels move around the screen but only in X axis, this is the code that I have but I notice it isn't working.
labels.OrderBy(x => x.Location.X);
Thank you in advance!
Edit: This is how I'm testing if it works or not...
private void actualizarPosicoes() {
labels.OrderBy(x => x.Location.X);
MessageBox.Show(labels.First.Value.Text.ToString());
}
But I want to use it when a label is removed from the screen but first I have to make the OrderBy working.
private void reorder(Label lb) {
labels.OrderBy(x => x.Location.X);
var it = labels.GetEnumerator();
var node = it.Current;
while (it.MoveNext())
{
var nextNode = it.MoveNext();
if (nextNode != null)
{
if (nextNode.Equals(lb))
{
nextNode = it.MoveNext();
it.Current.Location = new Point(node.Right, 0);
}
}
node = it.Current;
}
}
I have a global linkedlist called labels:
private LinkedList<Label> labels;
I suggest you to use FlowLayoutPanel as container to your labels. Removing any label will automatically re-arrange other labels.
Here is sample - assume you have labelsFlowLayoutPanel to host labels and addLabelButton to add labels:
private void addLabelButton_Click(object sender, EventArgs e)
{
Label label = new Label();
label.Text = DateTime.Now.ToLongTimeString();
label.DoubleClick += label_DoubleClick;
labelsFlowLayoutPanel.Controls.Add(label);
}
void label_DoubleClick(object sender, EventArgs e)
{
Label label = (Label)sender;
labelsFlowLayoutPanel.Controls.Remove(label);
label.DoubleClick -= label_DoubleClick;
}
Each time you click addLabelButton new label is created and inserted to the right of other labels. Double-clicking on any label will remove it and re-arrange other labels.
Further info - Windows Forms Controls Lesson 5: How to use the FlowLayout Panel
Without offering any suggestions for a better approach, and to answer the question directly, the problem with you code is as follows:
The OrderBy method with not modify the instance is is called on, it will instead return a new IOrderedEnumerable<TSource> where TSource is the type of object, for example Label. This means that your code should look like this:
labels = labels.OrderBy(x => x.Location.X);
Depending on what type of object labels is, you may need to convert it to a List like so:
labels = labels.OrderBy(x => x.Location.X).ToList();
EDIT: I just realized you are using LinkedList, I would recommend changing this to private List<Label> labels;
I finally did it!
There is the answer if anyone else need it...
private void reorder() {
var it2 = labels.OrderBy(x => x.Location.X);
Label orderLb = it2.ElementAt(0);
for (int i = 1; i < labels.Count; i++)
{
it2.ElementAt(i).Location = new Point(orderLb.Right, 0);
orderLb = it2.ElementAt(i);
}
Thank you all for the help, really appreciate it!
Related
newbie programmer here after hours of searching has left me stumped.
I'm having trouble with referencing a control inside a tab created at RunTime with a button press. Basically what I have is a tabletop RPG calculator, using a Windows Form, that has a tabControl holding tab pages, with each tab page holding user-inputted stats for that individual enemy to be used in calculations.
The problem is that I want the user to be able to click a button to generate a new enemy tab page. Here is my code for generating an enemy tab page with a TextBox.
int enemyNumber = 0;
// Creates a new Enemy Tab
private void button2_Click_1(object sender, EventArgs e)
{
// Create a new TabPage
var newTabPage = new TabPage()
{
Text = "Enemy " + enemyNumber,
};
// Add Enemy Name Box
var newEnemyNameBox = new TextBox()
{
Name = "enemyNameBox" + enemyNumber,
Text = "",
Location = new Point(127, 11),
Size = new Size(133, 20)
};
// Add the controls to the new Enemy tab
newTabPage.Controls.Add(newEnemyNameBox);
// Add the TabPage to the TabControl
tabControl1.TabPages.Add(newTabPage);
// Increases the enemy's "reference number" by 1
// So that enemy tabs will be generated in order enemyTab0, enemyTab1, etc.
enemyNumber += 1;
}
This all works nicely. Unfortunately, after this point things have gotten ugly. I need to reference that TextBox named "enemyNameBox" + enemyNumber, and I'm not sure how to do so.
What I did was create "archVariables" to store the values from whatever enemy tab is selected, then use the appropriate archVariable in the program's calculations. IE: archEnemyName. The idea is that whatever tab the user is currently selected on (determined via SelectedIndex) the TextBox from that page will be used for the program's output.
Here are the two things I've tried after researching the matter:
// Attempt 1
private void defendCalcButton_Click(object sender, EventArgs e)
{
for (int i = 0; i < tabControl1.SelectedIndex; i++)
{
archEnemyNameBox = ((TextBox)Controls["enemyNameBox" + i]).Text;
}
}
This code simply throws a NullReferenceException when I press the button. So after researching more I tried this:
// Attempt 2
private void defendCalcButton_Click(object sender, EventArgs e)
{
for (int i = 0; i < tabControl1.SelectedIndex; i++)
{
TextBox tb2 = new TextBox();
tb2 = ((TextBox)(enemyTab.Controls.Find("enemyNameBox" + i, true)));
archEnemyNameBox = tb2.Text;
}
}
This time I got an Error: Cannot convert type 'System.Windows.Forms.Control[]' to 'System.Windows.Forms.TextBox'
I feel like the second method I have here is probably closer to the correct way to do this, but apparently I'm still not getting it right. I've learned a lot by searching the information on stackoverflow and msdn.microsoft but nothing has gotten me past this problem.
Any help would be appreciated.
basically the problem with your second attemp is that enemyTab.Controls.Find("enemyNameBox" + i, true) returns an array of Controls Control[] and you're trying to convert that to a Control here is the problem, you should get the first control in that array and then convert it to a Control so it should be like this:
private void defendCalcButton_Click(object sender, EventArgs e)
{
for (int i = 0; i < tabControl1.SelectedIndex; i++)
{
TextBox tb2 = new TextBox();
tb2 = ((TextBox)(enemyTab.Controls.Find("enemyNameBox" + i, true)[0]));
archEnemyNameBox = tb2.Text;
}
}
but it is not the BestWay to do so it seems that everytime a user adds a new tabPage it will have the same Controls right? so why not create an userControl with any Control you have on your TabPage? so when you press the user press to add a new tab your code should be like so:
private void CreateNewEnemyTab()
{
var newTabPage = new TabPage()
{
Text = "Enemy " + enemyNumber,
};
EnemyTabUserControl enemyTab = new EnemyTabUserControl(enemyNumber);
here the EnemyTabUserControl should have all the components you need;
newTabPage.Controls.Add(enemyTab);
tabControl1.TabPages.Add(newTabPage);
}
and the code to bring the TextBox from the current tab could be as follow (you are going to need to reference LINQ)
using System.Linq;
//First Lets create this property, it should return the selected EnemyTabUserControl inside the tabControl
public EnemyTabUserControl CurrentTab {
get {
return tabControl1.SelectedTab.Controls.OfType<EnemyTabUserControl>().First();
}
}
// then if we make the textbox you want to reference from outside the code we can do this
CurrentTab.NameOfTheTextBox;
Patrick has solved your fundamental problem, but I don't think you need the loop in there at all. Here I've broken the steps out so you can see what needs to happen a little better:
private void defendCalcButton_Click(object sender, EventArgs e)
{
Control[] matches = this.Controls.Find("enemyNameBox" + tabControl1.SelectedIndex.ToString(), true);
if (matches.Length > 0 && matches[0] is TextBox)
{
TextBox tb = (TextBox)matches[0];
archEnemyNameBox = tb.Text;
}
}
I got it to work flawlessly for the Y coordinate, here's what I've tried:
public Point GetCellPos(MouseEventArgs e){
ListViewHitTestInfo lit = listview1.HitTest(e.Location);
Point p = new Point
{
X = listview1.Left + lit.Item.Position.X,
Y = listview1.Top + lit.Item.Position.Y
};
return p;
}
And for some reason, it won't get me the correct X coordinate. As a side note, I didn't put the listview control in a container other than the parent/form, so what could I be doing wrong? Any help would be appreciated, please.
OKAY, I solved it!! The key turned out to be in the Rectangle prop called Bound! I kept on tinkering with the X, Y coords ignoring the Rectangle which is actually what I was looking for! But anyway, I'm posting my humble snippet for those who might encounter the same problem in the future:
private void SetControlOnCell(ListView lv, Control eControl, MouseEventArgs e) {
ListViewHitTestInfo lit = lv.HitTest(e.Location);
Point p = new Point
{
X = lv.Left + lit.SubItem.Bounds.Left + 1,
Y = lv.Top + lit.SubItem.Bounds.Top
};
int w = (lit.SubItem.Bounds.Left == 0) ? lv.Columns[0].Width : lit.SubItem.Bounds.Width;
int h = lit.SubItem.Bounds.Height;
eControl.Location = p;
eControl.Size = new Size(w, h);
if (!eControl.Visible) eControl.Visible = true;
if (eControl.Font != lit.SubItem.Font) eControl.Font = lit.SubItem.Font;
eControl.Text = lit.SubItem.Text;
eControl.Focus();
}
And you use it like:
private void listview1_MouseClick(object sender, MouseEventArgs e) {
SetControlOnCell(listview1, txtEditValue, e)
}
I'm quite happy with this already as it's served the purpose of showing a "pseudo" control on top of a clicked cell (like I wanted it to). However, I'm keeping this question thread open for some better solutions others here may have and be willing to share.
--
EDIT: Ok, since no one's turned up -- thread's resolved I guess.
Okay, so the way I do this in one of my apps is I have a ContextMenuStrip that I've made for when the user right-clicks on an item in my ListView. From this, you can customize the menu itself to have controls in it if needed, or simply have it open up a dialog to edit the item properties.
Here's my click handler:
private void listView1_MouseClick(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Right) {
var item = listView1.GetItemAt(e.Location.X, e.Location.Y);
if (item != null) {
menuItemSelected.Show(Cursor.Position);
menuItemSelected.Tag = item.Tag;
}
}
}
I store item-specific information in the Tag property, as this makes it very easy to use across my program.
You can replace menuItemSelected.Show() with something to display the control you want at the right location.
Note that this displays it exactly where the user right-clicks on the item, not the top-left corner of the item. If you want to do that replace Cursor.Position with item.Position.
I have a System.Windows.Forms.Panel with some content.
I am trying to programmatically scroll the panel (vertically) either up or down.
I have tried setting the AutoScrollPosition property to a new Point on the panel but that doesn't seem to do it.
I have the AutoScroll property set to true.
I even tried to set the VerticalScroll.Value twice as suggested here, but that doesn't seem to work either.
This is what I am currently doing:
//I have tried passing both positive and negative values.
panel.AutoScrollPosition = new Point(5, 10);
The X and Y values on AutoScrollPosition remain 0 and 0.
Any help or direction on this would be greatly appreciated it.
Thanks in advance,
Marwan
Here is a solution. I guess you can scroll your Panel by arbitrary position using Win32 however there is a simple trick to help you achieve your requirement here:
public void ScrollToBottom(Panel p){
using (Control c = new Control() { Parent = p, Dock = DockStyle.Bottom })
{
p.ScrollControlIntoView(c);
c.Parent = null;
}
}
//use the code
ScrollToBottom(yourPanel);
Or use extension method for convenience:
public static class PanelExtension {
public static void ScrollToBottom(this Panel p){
using (Control c = new Control() { Parent = p, Dock = DockStyle.Bottom })
{
p.ScrollControlIntoView(c);
c.Parent = null;
}
}
}
//Use the code
yourPanel.ScrollToBottom();
UPDATE
If you want to set the exact position, modifying the code above a little can help:
//This can help you control the scrollbar with scrolling up and down.
//The position is a little special.
//Position for scrolling up should be negative.
//Position for scrolling down should be positive
public static class PanelExtension {
public static void ScrollDown(this Panel p, int pos)
{
//pos passed in should be positive
using (Control c = new Control() { Parent = p, Height = 1, Top = p.ClientSize.Height + pos })
{
p.ScrollControlIntoView(c);
}
}
public static void ScrollUp(this Panel p, int pos)
{
//pos passed in should be negative
using (Control c = new Control() { Parent = p, Height = 1, Top = pos})
{
p.ScrollControlIntoView(c);
}
}
}
//use the code, suppose you have 2 buttons, up and down to control the scrollbar instead of clicking directly on the scrollbar arrows.
int i = 0;
private void buttonUp_Click(object sender, EventArgs e)
{
if (i >= 0) i = -1;
yourPanel.ScrollUp(i--);
}
private void buttonDown_Click(object sender, EventArgs e)
{
if (i < 0) i = 0;
yourPanel.ScrollDown(i++);
}
Another solution you may want to use is using Panel.VerticalScroll.Value. However I think you need more research to make it work as you expect. Because I can see once changing the Value, the scrollbar position and control position don't sync well. Notice that Panel.VerticalScroll.Value should be between Panel.VerticalScroll.Minimum and Panel.VerticalScroll.Maximum.
This surprisingly works! NOTE THE MINUS SIGN in the code. There is strange behavior in setting scroll position. If you set the position to exact value (50), it goes negative when you read it next time (-50). So you have to invert it before setting new scroll value.
Scroll down:
private void ButtonScrollDown_OnClick(object sender, EventArgs e)
{
Point current = yourScrollPanel.AutoScrollPosition;
Point scrolled = new Point(current.X, -current.Y + 10);
yourScrollPanel.AutoScrollPosition = scrolled;
}
Scroll up similarly, (-current.Y - 10)
If you have a class that derives from Panel, then call these two protected methods to scroll the panel:
// The bottom is off screen; scroll down. These coordinates must be negative or zero.
SetDisplayRectLocation(0, AutoScrollPosition.Y - item.BoundingRect.Bottom + ClientRectangle.Bottom);
AdjustFormScrollbars(true);
In my example, item.BoundingRect.Bottom is the Y coordinate of the bottom of a thumbnail, and I need to scroll the panel down so that the whole thumbnail is visible.
#King King's solution of creating a temporary Control just so that scrolling could be done seemed "heavy" to me. And #Hans Passant's suggestion of setting AutoScrollMinSize and AutoScrollPosition didn't work for me.
Leave AutoScroll to its default value of 'true'.
Try this:-
panel.ScrollControlIntoView(childcontrol);
This should work. childcontrol is the particular control that you want to show in your display area.
Setting the value of the HorizontalScroll property and then using the method ScrollControlIntoView works for me:
lpanel.HorizontalScroll.Value = 100;
lpanel.ScrollControlIntoView(lpanel);
Use #King King Answered Code and if you want to hide horizontal and vertical scroll bar, just apply the below code in the constructor or initialization.
yourPanel.AutoScroll = false;
yourPanel.HorizontalScroll.Maximum = 0;
yourPanel.HorizontalScroll.Visible = false;
yourPanel.VerticalScroll.Maximum = 0;
yourPanel.VerticalScroll.Visible = false;
yourPanel.AutoScroll = true;
I had an issue where I couldnt get my panel to scroll back to top . I tried many things to try and get the panel to scroll back to the top after populating it with many controls.
Nomatter what I did it always put the VScroll bar to the bottom.
After exhaustive testing I found it was because my controls had the TabStop property set to true (default on user controls) was causing the issue.
Setting TabStop to false fixed it.
Create an control that sits slightly outside the visible area (so -1 at the top and clientsize+1 ) and then call ScrollControlIntoView:
public static class PanelExtension {
public static void ScrollDown(this Panel p)
{
using (Control c = new Control() { Parent = p, Height = 1, Top = p.ClientSize.Height + 1 })
{
p.ScrollControlIntoView(c);
}
}
public static void ScrollUp(this Panel p )
{
using (Control c = new Control() { Parent = p, Height = 1, Top = -1})
{
p.ScrollControlIntoView(c);
}
}
}
//use the code, suppose you have 2 buttons, up and down to control the scrollbar instead of clicking directly on the scrollbar arrows.
private void buttonUp_Click(object sender, EventArgs e)
{
yourPanel.ScrollUp();
}
private void buttonDown_Click(object sender, EventArgs e)
{
yourPanel.ScrollDown();
}
with yourpanel.SetAutoScrollMargin(1, 1); you can set very fine scrolling steps and then take a timer to call the srolling when buttons are down
I'm displaying a set of search results in a ListView. The first column holds the search term, and the second shows the number of matches.
There are tens of thousands of rows, so the ListView is in virtual mode.
I'd like to change this so that the second column shows the matches as hyperlinks, in the same way as a LinkLabel shows links; when the user clicks on the link, I'd like to receive an event that will let me open up the match elsewhere in our application.
Is this possible, and if so, how?
EDIT: I don't think I've been sufficiently clear - I want multiple hyperlinks in a single column, just as it is possible to have multiple hyperlinks in a single LinkLabel.
You can easily fake it. Ensure that the list view items you add have UseItemStyleForSubItems = false so that you can set the sub-item's ForeColor to blue. Implement the MouseMove event so you can underline the "link" and change the cursor. For example:
ListViewItem.ListViewSubItem mSelected;
private void listView1_MouseMove(object sender, MouseEventArgs e) {
var info = listView1.HitTest(e.Location);
if (info.SubItem == mSelected) return;
if (mSelected != null) mSelected.Font = listView1.Font;
mSelected = null;
listView1.Cursor = Cursors.Default;
if (info.SubItem != null && info.Item.SubItems[1] == info.SubItem) {
info.SubItem.Font = new Font(info.SubItem.Font, FontStyle.Underline);
listView1.Cursor = Cursors.Hand;
mSelected = info.SubItem;
}
}
Note that this snippet checks if the 2nd column is hovered, tweak as needed.
Use ObjectListView -- an open source wrapper around a standard ListView. It supports links directly:
This recipe documents the (very simple) process and how you can customise it.
The other answers here are great, but if you don't want to have to hack some code together, look at the DataGridView control which has support for LinkLabel equivalent columns.
Using this control, you get all the functionality of the details view in a ListView, but with more customisation per row.
You can by inheriting the ListView control override the method OnDrawSubItem.
Here is a VERY simple example of how you might do:
public class MyListView : ListView
{
private Brush m_brush;
private Pen m_pen;
public MyListView()
{
this.OwnerDraw = true;
m_brush = new SolidBrush(Color.Blue);
m_pen = new Pen(m_brush)
}
protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e)
{
e.DrawDefault = true;
}
protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e)
{
if (e.ColumnIndex != 1) {
e.DrawDefault = true;
return;
}
// Draw the item's background.
e.DrawBackground();
var textSize = e.Graphics.MeasureString(e.SubItem.Text, e.SubItem.Font);
var textY = e.Bounds.Y + ((e.Bounds.Height - textSize.Height) / 2);
int textX = e.SubItem.Bounds.Location.X;
var lineY = textY + textSize.Height;
// Do the drawing of the underlined text.
e.Graphics.DrawString(e.SubItem.Text, e.SubItem.Font, m_brush, textX, textY);
e.Graphics.DrawLine(m_pen, textX, lineY, textX + textSize.Width, lineY);
}
}
You can set HotTracking to true so that when the user hovers mouse over the item it appears as link.
I'm trying to add an array of labels to a panel in my Form.
I chose a label because I could set colors for the text.
If there is a better way, please let me know.
The code below runs fine but will only display one label.
I set a breakpoint and looked at the array before adding and all the
elements are there.
However, only one label actually shows up on the Panel.
Here's the code.
int y = 0;
int index = 0;
Label[] labels = new Label[10];
//Add Spareboard Employees to Spare List
foreach (Employee employee in EmployeeList)
{
labels[index] = new Label();
labels[index].Text = employee.Name;
labels[index].ForeColor = Color.Red;
labels[index].Location = new Point(0, y);
y = y + 10;
++index;
}
// Add the Label control to the form.
SparePanel.Controls.AddRange(labels);
Thanks in advance
The default size of the label is too big and each label's bottom is covering up the top of the label below it. You should add something like this:
labels[index].Size = new Size(50, 12);
maybe
Label[] labels = new Label[10];
needs to be
Control[] labels = new Control[10];
As far as I know, you need to implement the IEnumerable Interface and IEnumerate.Compare() method too, in order to iterate a foreach loop over your Employee object.
public class Employee : IEnumerator
{
//Implement IEnumerate method here
}
I'm not that experienced though so don't take my word for it! I would put more detailed code but I don't have it to hand.
Another possibility (which you were also looking for) is to draw the strings directly on the UI without adding controls. Do it during the paint event of the panel.
private void SparePanel_Paint(object sender, PaintEventArgs e)
{
using (SolidBrush empBrush = new SolidBrush(Color.Red))
{
int y = 0;
foreach (Employee employee in EmployeeList)
{
e.Graphics.DrawString(employee.Name, ((Panel)sender).Font, empBrush, 0, y);
y += 10;
}
}
}