Edit ListView Group Headers like we can edit ListViewItems - c#

In a WinForms app, we can re-name ListView Items by clicking them twice. Can we somehow rename Group Headers the same way? Is there a way to enable this?

I guess it is doable after all, albeit not by simply enabling a property..
Note: The code below assumes that the ListView is in Details mode!
The trick to tell a Group from emtpy space is to test the right side of the ListView. Another trick is to wait a little: The click will select the Group Items. Only after that can we proceed..
Here is an example that overlays the Group with a TextBox:
// class variable to test if have been hit twice in a row
ListViewGroup lastHitGroup = null;
private void listView1_MouseDown(object sender, MouseEventArgs e)
{
// check left side to see if we are at the empty space
ListViewItem lvi = listView1.GetItemAt(4, e.Y);
// yes, no action! reset group
if (lvi != null) { lastHitGroup = null; return; }
// get the height of an Item
int ih = listView1.GetItemRect(0).Height;
// to get the group we need to check the next item:
ListViewItem lviNext = listView1.GetItemAt(4, e.Y + ih);
// no next item, maybe the group is emtpy, no action
if (lviNext == null) return;
// this is our group
ListViewGroup editedGroup = lviNext.Group;
// is this the 2nd time?
if (lastHitGroup != editedGroup) {lastHitGroup = editedGroup; return;}
// we overlay a TextBox
TextBox tb = new TextBox();
tb.Parent = listView1;
// set width as you like!
tb.Height = ih;
// we position it over the group header and show it
tb.Location = new Point(0, lviNext.Position.Y - ih - 4);
tb.Show();
// we need two events to quit editing
tb.KeyPress += (ss, ee) =>
{
if (ee.KeyChar == (char)13) // success
{
if (editedGroup != null && tb.Text.Length > 0)
editedGroup.Header = tb.Text;
tb.Hide();
ee.Handled = true;
}
else if (ee.KeyChar == (char)27) // abort
{
tb.Text = ""; tb.Hide(); ee.Handled = true;
}
};
tb.LostFocus += (ss, ee) => // more success
{
if (editedGroup != null && tb.Text.Length > 0)
editedGroup.Header = tb.Text;
tb.Hide();
};
// we need to wait a little until the group items have been selected
Timer lvTimer = new Timer();
lvTimer.Interval = 333; // could take longer for a huge number of items!
lvTimer.Tick += (ss,ee) => { tb.Focus(); lvTimer.Stop();};
lvTimer.Start();
}

Related

Why does autocomplete in my Winforms ComboBox change the cursor location when typing?

I have a WinForms ComboBox within an Outlook Add-in which populates the drop-down with user names, when the user types names in the edit box. When the user reaches the third character. The data source of the ComboBox is assigned at this point, and the curser jumps to the beginning of the edit box instead of staying at the end.
The behavior I want is that the cursor stays at the end of the string I am typing, even when the drop-down is populated with more data.
I've tried hacking this with send keys, but it doesn't always work. The code which reads the keys below is in the key pressed event.
private void comboBox1_KeyPress(object sender, KeyEventArgs e)
{
var acceptableKeys = ConfigurationManager.AppSettings["AcceptableKeys"];
if (cmbAssignedTo.Text.Length > 2 && acceptableKeys.Contains(e.KeyCode.ToString().ToUpper()) && e.Modifiers == Keys.None)
{
var request = RestHandler.CreateRequest(ConfigurationManager.AppSettings["ContactsSearchResource"] + cmbAssignedTo.Text.Trim(), Method.GET);
var response = RestHandler.ExecuteRequest(request, ConfigurationManager.AppSettings["myServiceURL"]);
this.cmbAssignedTo.DataSourceChanged -= new System.EventHandler(this.cmbAssignedTo_DataSourceChanged);
//Assign a new data source
DataHandler.UpdateComboboxDataSource(cmbAssignedTo, response.Content);
this.cmbAssignedTo.DataSourceChanged += new System.EventHandler(this.cmbAssignedTo_DataSourceChanged);
}
e.Handled = true;
}
Edit
internal static void UpdateComboboxDataSource(ComboBox cmbAssignedTo, string data)
{
var list = BuildAssignmentList(data);
if ((list.Count() == 0 && cmbAssignedTo.Items.Count == 0) || list.Count() > 0)
{
var savedText = cmbAssignedTo.Text;
cmbAssignedTo.DataSource = list;
cmbAssignedTo.SelectedValue = "";
cmbAssignedTo.Text = savedText;
SendKeys.Send("{end}");
}
if (cmbAssignedTo.Items.Count > 0)
{
cmbAssignedTo.DroppedDown = true;
Cursor.Current = Cursors.Default;
}
}
I don't see how I can update the dropdown without changing the DataSource and that change appears to cause the cursor to jump. Should I try different event than KeyPressed? Is there some other solution I'm missing?
As another hack you can play with ComboBox's SelectionStart property:
int i = comboBox1.SelectionStart;
comboBox1.DataSource = new System.Collections.Generic.List<string>(){"aaaaaa", "bbbbbb", "ccccccc"};
comboBox1.SelectionStart = i;
This code changes the DataSource and retain the cursor position. If you want cursor to be always at end - set SelectionStart to comboBox1.Text.Length.
UPD: To fight against the "first item selection" you may use another hack:
private bool cbLock = false;
private void comboBox1_TextChanged(object sender, EventArgs e)
{
// lock is required, as this event also will occur when changing the selected index
if (cbLock)
return;
cbLock = true;
int i = comboBox1.SelectionStart;
// store the typed string before changing DS
string text = comboBox1.Text.Substring(0, i);
List<string> ds = new System.Collections.Generic.List<string>() { "aaaaaa", "aaabbb", "aaacccc" };
comboBox1.DataSource = ds;
// select first match manually
for (int index = 0; index < ds.Count; index++)
{
string s = ds[index];
if (s.StartsWith(text))
{
comboBox1.SelectedIndex = index;
break;
}
}
// restore cursor position and free the lock
comboBox1.SelectionStart = i;
cbLock = false;
}
When typing "aaab" it selects the "aaabbb" string.

C# objectlistview undefined offset in cell editor

I use TreeListView component from ObjectListView library. I make cell values editable and when i double click on them TextBox will appear with odd offset.
I want to remove this offset, but how?
before start editing
after start editing
As you can see first row, second column ("My new device"), TextBox is appeared with offset.
P.S. Editing work as expected. Only offset is annoying me
P.P.S. As you can see the offset depends of first column offset. How i can change it to zero?
After long searches i found solution!
Source code of OLV, ObjectListView.cs
public virtual void StartCellEdit(OLVListItem item, int subItemIndex) {
OLVColumn column = this.GetColumn(subItemIndex);
Control c = this.GetCellEditor(item, subItemIndex);
Rectangle cellBounds = this.CalculateCellBounds(item, subItemIndex);
c.Bounds = this.CalculateCellEditorBounds(item, subItemIndex, c.PreferredSize);
// Try to align the control as the column is aligned. Not all controls support this property
Munger.PutProperty(c, "TextAlign", column.TextAlign);
// Give the control the value from the model
this.SetControlValue(c, column.GetValue(item.RowObject), column.GetStringValue(item.RowObject));
// Give the outside world the chance to munge with the process
this.CellEditEventArgs = new CellEditEventArgs(column, c, cellBounds, item, subItemIndex);
this.OnCellEditStarting(this.CellEditEventArgs);
if (this.CellEditEventArgs.Cancel)
return;
// The event handler may have completely changed the control, so we need to remember it
this.cellEditor = this.CellEditEventArgs.Control;
this.Invalidate();
this.Controls.Add(this.cellEditor);
this.ConfigureControl();
this.PauseAnimations(true);
}
I saw CellEditEventArgs contains Control, that drawed in area named Bounds.
This function in the source file append offset to control's bounds:
protected Rectangle CalculateCellEditorBoundsStandard(OLVListItem item, int subItemIndex, Rectangle cellBounds, Size preferredSize) {
if (this.View == View.Tile)
return cellBounds;
// Center the editor vertically
if (cellBounds.Height != preferredSize.Height)
cellBounds.Y += (cellBounds.Height - preferredSize.Height) / 2;
// Only Details view needs more processing
if (this.View != View.Details)
return cellBounds;
// Allow for image (if there is one).
int offset = 0;
object imageSelector = null;
if (subItemIndex == 0)
imageSelector = item.ImageSelector;
else {
// We only check for subitem images if we are owner drawn or showing subitem images
if (this.OwnerDraw || this.ShowImagesOnSubItems)
imageSelector = item.GetSubItem(subItemIndex).ImageSelector;
}
if (this.GetActualImageIndex(imageSelector) != -1) {
offset += this.SmallImageSize.Width + 2;
}
// Allow for checkbox
if (this.CheckBoxes && this.StateImageList != null && subItemIndex == 0) {
offset += this.StateImageList.ImageSize.Width + 2;
}
// Allow for indent (first column only)
if (subItemIndex == 0 && item.IndentCount > 0) {
offset += (this.SmallImageSize.Width * item.IndentCount);
}
// Do the adjustment
if (offset > 0) {
cellBounds.X += offset;
cellBounds.Width -= offset;
}
return cellBounds;
}
We can see, that offset appending to every cell (not only first). Also we can se, that CellEditEventArgs contains CellBounds.
So we can just reset Control.Bounds to CellBounds value:
//...
dataTreeView.CellEditStarting += DisableInputValueForCollections;
//...
private static void DisableInputValueForCollections(object sender, CellEditEventArgs e)
{
RemoveExtraOffsetForNotFirstColumnInputControl(e);
var node = e.RowObject as DataTreeNode;
if (node != null && e.Column.AspectName == "Value")
{
if (node.IsContainer()) e.Cancel = true;
}
}
//...
private static void RemoveExtraOffsetForNotFirstColumnInputControl(CellEditEventArgs e)
{
if (e.Column.AspectName != "Name")
{
e.Control.Bounds = e.CellBounds;
}
}
//...
The issue still exists in OLV v2.9.1. I worked around it slightly different than muzagursiy did.
olv.CellEditStarting += (sender, args) =>
{
// Left align the edit control
args.Control.Location = args.CellBounds.Location;
// Readjust the size of the control to fill the whole cell if CellEditUseWholeCellEffective is enabled
if (args.Column.CellEditUseWholeCellEffective)
{
args.Control.Size = args.CellBounds.Size;
}
};

Selecting the next radio button in a group

Is there a standard way to programmatically select/check the next radio button in a group of radio buttons? The behaviour I'm looking for is similar to the default arrow key press event for radio buttons that are grouped in a container: When I press the arrow keys, the next (or previous) radio button is automatically selected and checked.
I made this in such way:
var rads = panel1.Controls.OfType<RadioButton>(); // Get all radioButtons of desired panel
rads.OrderBy(r => r.Top); // sort them. Please specify what you mean "next" here. I assume that you need next one at the bottom
// find first checked and set checked for next one
for (int i = 0; i < rads.Count()-1; i++)
{
if (rads.ElementAt(i).Checked)
{
rads.ElementAt(i + 1).Checked = true;
return;
}
}
You can only check one RadioButton per container. That's the point of a radio button. If you want to use a CheckBox instead you could use the following code:
foreach (CheckBox control in Controls.OfType<CheckBox>())
{
control.Checked = true;
}
If you want the controls checked sequentailly you can do
new Thread(() =>
{
foreach (CheckBox control in Controls.OfType<CheckBox>())
{
control.BeginInvoke((MethodInvoker) (() => control.Checked = true));
Thread.Sleep(500);
}
}).Start();
Reading your original post another time, I'm struggling to understand exactly what you mean. Could you please elaborate so I can update my response?
Quickest solution: (if your application has focus...)
//assuming you are at the first radio button
SendKeys({DOWN});
More Difficult Solution:
http://msdn.microsoft.com/en-us/library/system.windows.automation.automationelement.findall.aspx
//Write method to get window element for the window you wish to manipulate
//Open an instance of notepad and the WindowTitle is: "Untitled - Notepad"
//you could use other means of getting to the Window element ...
AutomationElement windowElement = getWindowElement("Untitled - Notepad");
//Use System.Windows.Automation to find all radio buttons in the WindowElement
//pass the window element into this method
//This method will return all of the radio buttons in the element that is passed in
//however, if you have a Pane inside of the WIndow and then, the buttons are contained
//in the pane, you will have to get to the pane and then pass the pane into the findradiobuttons method
AutomationElementCollection radioButtons = FindRadioButtons(windowElement);
//could iterate through the radioButtons to determine which is selected...
//then select the next index etc.
//then programmatically select the radio button
//pass the selected radioButton AutomationElement into a method that Invokes the Click etc.
clickButtonUsingUIAutomation(radioButtons[0]);
/// <summary>
/// Finds all enabled buttons in the specified window element.
/// </summary>
/// <param name="elementWindowElement">An application or dialog window.</param>
/// <returns>A collection of elements that meet the conditions.</returns>
AutomationElementCollection FindRadioButtons(AutomationElement elementWindowElement)
{
if (elementWindowElement == null)
{
throw new ArgumentException();
}
Condition conditions = new AndCondition(
new PropertyCondition(AutomationElement.IsEnabledProperty, true),
new PropertyCondition(AutomationElement.ControlTypeProperty,
ControlType.RadioButton)
);
// Find all children that match the specified conditions.
AutomationElementCollection elementCollection =
elementWindowElement.FindAll(TreeScope.Children, conditions);
return elementCollection;
}
private AutomationElement getWindowElement(string windowTitle)
{
AutomationElement root = AutomationElement.RootElement;
AutomationElement result = null;
foreach (AutomationElement window in root.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window)))
{
try
{
if (window.Current.Name.Contains(windowTitle) && window.Current.IsKeyboardFocusable)
{
result = window;
break;
}
}
catch (Exception e)
{
throw;
}
}
return result;
}
private void ClickButtonUsingUIAutomation(AutomationElement control)
{
// Test for the control patterns of interest for this sample.
object objPattern;
ExpandCollapsePattern expcolPattern;
if (true == control.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out objPattern))
{
expcolPattern = objPattern as ExpandCollapsePattern;
if (expcolPattern.Current.ExpandCollapseState != ExpandCollapseState.LeafNode)
{
Button expcolButton = new Button();
//expcolButton.Margin = new Thickness(0, 0, 0, 5);
expcolButton.Height = 20;
expcolButton.Width = 100;
//expcolButton.Content = "ExpandCollapse";
expcolButton.Tag = expcolPattern;
expcolPattern.Expand();
//SelectListItem(control, "ProcessMethods");
//expcolButton.Click += new RoutedEventHandler(ExpandCollapse_Click);
//clientTreeViews[treeviewIndex].Children.Add(expcolButton);
}
}
TogglePattern togPattern;
if (true == control.TryGetCurrentPattern(TogglePattern.Pattern, out objPattern))
{
togPattern = objPattern as TogglePattern;
Button togButton = new Button();
//togButton.Margin = new Thickness(0, 0, 0, 5);
togButton.Height = 20;
togButton.Width = 100;
//togButton.Content = "Toggle";
togButton.Tag = togPattern;
togPattern.Toggle();
//togButton.Click += new RoutedEventHandler(Toggle_Click);
//clientTreeViews[treeviewIndex].Children.Add(togButton);
}
InvokePattern invPattern;
if (true == control.TryGetCurrentPattern(InvokePattern.Pattern, out objPattern))
{
invPattern = objPattern as InvokePattern;
Button invButton = new Button();
//invButton.Margin = new Thickness(0);
invButton.Height = 20;
invButton.Width = 100;
//invButton.Content = "Invoke";
invButton.Tag = invPattern;
//invButton.Click += new EventHandler(Invoke_Click);
invPattern.Invoke();
//clientTreeViews[treeviewIndex].Children.Add(invButton);
}
}
So, it seems as if there is no standard way to handle this. I ended up taking inspiration from Andrey's solution and writing an extension method that can be called from any particular RadioButton within a group of RadioButtons.
public static void CheckNextInGroup(this RadioButton radioButton, bool forward) {
var parent = radioButton.Parent;
var radioButtons = parent.Controls.OfType<RadioButton>(); //get all RadioButtons in the relevant container
var ordered = radioButtons.OrderBy(i => i.TabIndex).ThenBy(i => parent.Controls.GetChildIndex(i)).ToList(); //Sort them like Windows does
var indexChecked = ordered.IndexOf(radioButtons.Single(i => i.Checked)); //Find the index of the one currently checked
var indexDesired = (indexChecked + (forward ? 1 : -1)) % ordered.Count; //This allows you to step forward and loop back to the first RadioButton
if (indexDesired < 0) indexDesired += ordered.Count; //Allows you to step backwards to loop to the last RadioButton
ordered[indexDesired].Checked = true;
}
Then, from anywhere that has access to your particular RadioButton, you can cause the next or previous RadioButton in its collection to get checked. Like this:
radioButton1.CheckNextInGroup(true); //Checks the next one in the collection
radioButton1.CheckNextInGroup(false); //Checks the previous one in the collection

c#: show tooltip on subitem

I have a listview, and in one of the columns (not the first) I want to display an error code.
What I haven't been able to do is get the ToolTip to display. I have
this.lstList.ShowItemToolTips = true;
...
ListViewItem value = lstList.Items.Add(name, name, 0);
...
if (lstList.Columns.Contains(lstColErrorCode))
{
value.SubItems.Add(new ListViewItem.ListViewSubItem(value, errorCode.ToString()));
value.ToolTipText = errorCode.ToString("X");
}
I would like to get the hex value of the code to be shown on the tooltip above the decimal value, but it shows above the name.
I haven't been able to get anything I tried to work (like trying to get the coordinates of the subitem). I would appreciate any suggestion.
this code works for me
ToolTip toolTip1 = new ToolTip();
void initMethod()
{
lstList.MouseMove += new MouseEventHandler(lstList_MouseMove);//mousemove handler
this.lstList.ShowItemToolTips = true;
toolTip1.SetToolTip(lstList,"");// init the tooltip
...
ListViewItem value = lstList.Items.Add(name, name, 0);
...
if (lstList.Columns.Contains(lstColErrorCode))
{
ListViewItem.ListViewSubItem lvs = value.SubItems.Add(new ListViewItem.ListViewSubItem(value, errorCode.ToString()));
lvs.Tag = "mydecimal"; // only the decimal subitem will be tooltiped
}
}
the mousemove event from the listview:
void lstList_MouseMove(object sender, MouseEventArgs e)
{
ListViewItem item = lstList.GetItemAt(e.X, e.Y);
ListViewHitTestInfo info = lstList.HitTest(e.X, e.Y);
if ((item != null) && (info.SubItem != null) && (info.SubItem.Tag!=null) && (info.SubItem.Tag.ToString() == "mydecimal"))
{
toolTip1.SetToolTip(lstList,((decimal)info.SubItem.Text).ToString("X"));
}
else
{
toolTip1.SetToolTip(lstList, "");
}
}

How to change properties of a Control that is in List<UIControl> without using Loop?

I have the following code where a click event will dynamically create additional Canvas to the WrapPanel, and each Canvas contains a TextBox and a Button. Once the Button on one Canvas is click, TextBox.Text and Button.Content change from "Foo" to "Jesus".
The below code works, but it's not ideal. Because each property Change ("Foo" to "Jesus), I have to run a loop. I have to run two loops just to change the text on the TextBox and Button. Is there a direct way to change the Properties other then a Loop? My actually application contains 30+ controls in a Canvas, I don't want to run 30+ loops each time just to change some text.
List<Canvas> cvList = new List<Canvas>();
List<TextBox> tbList = new List<TextBox>();
List<Button> FooList = new List<Button>();
WrapPanel wp = new WrapPanel();
private void createbtn1_Click(object sender, RoutedEventArgs e)
{
Canvas cv = new Canvas();
StackPanel sp = new StackPanel();
TextBox tb = new TextBox();
Button Foo = new Button();
sp.Orientation = Orientation.Vertical;
sp.Children.Add(tb);
sp.Children.Add(Foo);
cv.Children.Add(sp);
wp.Children.Add(cv);
cvList.Add(cv);
tbList.Add(tb);
FooList.Add(Foo);
cv.Width = 100;
cv.Height = 100;
tb.Text = "#" + (cvList.IndexOf(cv)+1);
tb.Width = 50;
tb.Height = 30;
Foo.Content = "Foo";
Foo.Click += destroy_Click;
}
private void Foo_Click(object sender, RoutedEventArgs e)
{
Button b = sender as Button;
var bIndex = FooList.IndexOf(b);
foreach (TextBox t in tbList)
{
if (tbList.IndexOf(t) == bIndex)
{
t.Text = "Jesus";
}
}
foreach (Button f in FooList)
{
if (FooList.IndexOf(t) == bIndex)
{
t.Content = "Jesus";
}
}
}
Just access the text boxes by index and set the content of the button directly:
if(bIndex < tbList.Count && bIndex != -1)
tbList[bIndex].Text = "Jesus";
if(b != null && bIndex != -1)
b.Content = "Jesus";
why can't you just get the item at the index and set that items text:
tbList[bindex].Text="Jesus";
As for setting the buttons content, you already have the button from the click event, so just use that:
b.Content = "Jesus";
You current code just loops through each item in the list and gets the index of the item and sees if it is the index you want. Accessing by the indexer of the list directly will give you what you want.
You will probably want to do some error checking, but that is not currently done in your existing code either.
Some info on using indexers from MSDN

Categories