I'm currently working on a listview in winform c#
and everytime I click on an empty space on the listview,
the selected item is lost.
The listview control has a HideSelection property that defaults to True. Make it False and you're good to go... in some cases this is enough.
You have to inherit from the ListView class and do some low-level message processing
class ListViewThatKeepsSelection : ListView
{
protected override void WndProc(ref Message m)
{
// Suppress mouse messages that are OUTSIDE of the items area
if (m.Msg >= 0x201 && m.Msg <= 0x209)
{
Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
var hit = this.HitTest(pos);
switch (hit.Location)
{
case ListViewHitTestLocations.AboveClientArea:
case ListViewHitTestLocations.BelowClientArea:
case ListViewHitTestLocations.LeftOfClientArea:
case ListViewHitTestLocations.RightOfClientArea:
case ListViewHitTestLocations.None:
return;
}
}
base.WndProc(ref m);
}
}
I thought there was a property that prevented this from happening, but now I can't find it.
You could try this:
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListView listView = sender as ListView;
if (listView.SelectedItems.Count == 0)
foreach (object item in e.RemovedItems)
listView.SelectedItems.Add(item);
}
This is much harder to do in WinForms than in WPF. WinForms has a SelectedIndexChanged event which doesn't tell you anything about what was already selected, plus it is fired every time a row is selected or deselected.
So if a row is selected and you select a different row, you receive two SelectedIndexChanged events:
one after the selected row is deselected
another when the new row is selected.
The problem is that, during event #1, the ListView has nothing selected and you don't know if event #2 is coming that will select the second row.
The best you can do is wait until your application is idle (a few milliseconds after the selection has changed), and if the listview still has nothing selected, put back the last selected row.
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
ListView lv = (ListView)sender;
if (lv.SelectedIndices.Count == 0)
{
if (!this.appIdleEventScheduled)
{
this.appIdleEventScheduled = true;
this.listViewToMunge = lv;
Application.Idle += new EventHandler(Application_Idle);
}
}
else
this.lastSelectedIndex = lv.SelectedIndices[0];
}
void Application_Idle(object sender, EventArgs e)
{
Application.Idle -= new EventHandler(Application_Idle);
this.appIdleEventScheduled = false;
if (listViewToMunge.SelectedIndices.Count == 0)
listViewToMunge.SelectedIndices.Add(this.lastSelectedIndex);
}
private bool appIdleEventScheduled = false;
private int lastSelectedIndex = -1;
private ListView listViewToMunge;
I accomplished it like this:
private void lvReads_MouseUp(object sender, MouseEventArgs e)
{
if (lvReads.SelectedItems.Count == 0)
if (lvReads.Items.Count > 0)
lvReads.Items.Find(currentName, false)[0].Selected = true;
}
and
private void lvReads_SelectedIndexChanged(object sender, EventArgs e)
{
if (lvReads.SelectedItems.Count == 1)
{
selectedIndex = lvReads.SelectedIndices[0];
if (currentName != lvReads.Items[selectedIndex].Name)
{
//load item
}
currentName = lvReads.Items[selectedIndex].Name;
}
}
I know that question asked 10 years ago. But I face the same problem and found a simple and elegant solution just now and sincerely want to share it.
There is a code (in VB.NET but its no big problem to write the same in C#):
Public Class SettingsBox ' Form that contains ListView (lvScreen)
Private nScreenTracer As Integer
Private nSelectedScreen As Integer
Private Sub lvScreen_ItemSelectionChanged(sender As Object,
e As ListViewItemSelectionChangedEventArgs) Handles lvScreen.ItemSelectionChanged
If e.IsSelected Then nScreenTracer = e.Item.Index
End Sub
Private Sub lvScreen_MouseDown(sender As Object,
e As MouseEventArgs) Handles lvScreen.MouseDown
nScreenTracer = -1
End Sub
Private Sub lvScreen_MouseUp(sender As Object,
e As MouseEventArgs) Handles lvScreen.MouseUp
If nScreenTracer = -1 Then
lvScreen.SelectedIndices.Add(nSelectedScreen)
Else
nSelectedScreen = nScreenTracer
End If
End Sub
End Class
This solution works for a single item selection but also can be simply redesigned with List(Of Integer) for multi-select
Here are simple, but a bit hack way, based on LVN_ITEMCHANGED event realization (Sorry - plain C, not C#, but i hope it's understandable):
case LVN_ITEMCHANGED:
{
NMLISTVIEW* lPoint = (LPNMLISTVIEW)lParam;
if (lPoint->uNewState & LVIS_SELECTED && lPoint->iItem != -1) {
// Select other item...
eItem = (int)lPoint->lParam;
SendMessage(GetParent(hDlg), WM_APP + 2, 0, 1);
}
else {
if (!lPoint->uNewState && ListView_GetItemState(((NMHDR*)lParam)->hwndFrom, lPoint->iItem, LVIS_FOCUSED)) {
// Click on empty space
ListView_SetItemState(((NMHDR*)lParam)->hwndFrom, lPoint->iItem, LVIS_SELECTED, LVIS_SELECTED);
}
else
// Click on other item
ListView_SetItemState(((NMHDR*)lParam)->hwndFrom, lPoint->iItem, 0, LVIS_SELECTED);
}
} break;
Related
I'm trying to do a drag and drop of multiple items between ListBox in windows forms. The problem I'm having is if I select multiple items holding the Shift key and try to drag and drop it without release the the key I get an error. Actually the SelectedIndices and SelectedItems just show 1 item, the one I clicked first, even though multiple items are highlighted in the ListBox.
I'm using SelectionMode = MultiExtended
void ZListBox_MouseMove(object sender, MouseEventArgs e)
{
if (isDraggingPoint.HasValue && e.Button == MouseButtons.Left && SelectedIndex >= 0)
{
var pointToClient = PointToClient(MousePosition);
if (isDraggingPoint.Value.Y != pointToClient.Y)
{
lastIndexItemOver = -1;
isDraggingPoint = null;
var dropResult = DoDragDrop(SelectedItems, DragDropEffects.Copy);
}
}
}
It seems that if I don't release the left mouse button before I do "DoDragDrop", the items aren't selected and also if I try to get the SelectedIndices from the other ListBox, the Count is the number of "selected items", but when I try to navigate the list, I get a IndexOutOfRangeException.
Is there any work around for it?
Sample code to reproduce the issue:
(To reproduce:
1- Select an item
2- Hold shift and click in another item, than without release shift and mouse button, drag this item (if you have a breakpoint inside the 'if', you'll see just 1 item on SelectedItems))
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Load += Form1_Load;
}
private void Form1_Load(object sender, EventArgs e)
{
var someList = new List<ListItemsTest>();
someList.Add(new ListItemsTest() { ID = 1, Name = "Name 1" });
someList.Add(new ListItemsTest() { ID = 2, Name = "Name 2" });
someList.Add(new ListItemsTest() { ID = 3, Name = "Name 3" });
someList.Add(new ListItemsTest() { ID = 4, Name = "Name 4" });
someList.Add(new ListItemsTest() { ID = 5, Name = "Name 5" });
listBox1.DisplayMember = "Name";
listBox1.ValueMember = "ID";
listBox1.DataSource = someList;
listBox1.SelectionMode = SelectionMode.MultiExtended;
listBox1.MouseMove += ListBox1_MouseMove;
listBox1.AllowDrop = true;
}
void ListBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && listBox1.SelectedIndex >= 0)
{
var dropResult = DoDragDrop(listBox1.SelectedItems, DragDropEffects.Copy);
}
}
public class ListItemsTest
{
public int ID { get; set; }
public string Name { get; set; }
}
}
Another example, in case you need to know what Items were selected in a ListBox, using the SHIFT key to create an extended selection, even if you don't need to initiate a Draq&Drop operation:
Using the data sample you provided in the question, a List.
In the example, a List<int> (lbSelectedIndexes) is used to keep track of what items are currently selected in the ListBox. This List is filled only when the selection is performed using the SHIFT key, or after a Drag&Drop operation is initiated. This can be useful to determine the type of selection.
In all other cases the List<int> is empty and the SelectedItems and SelectedIndices collections can be used to determine the items currently selected.
The SystemInformation.DragSize value is also used to determine if the drag operation should be initiated when the Mouse Pointer is moved while the Left Button is pressed.
When the Drag&Drop operation is started, a new DataObject is filled with ListBox Items corresponding to the current selection, no matter how the selection was performed.
The DragDropEffects is set to DragDropEffects.Copy.
Point lbMouseDownPosition = Point.Empty;
List<int> lbSelectedIndexes = new List<int>();
private void listBox1_MouseDown(object sender, MouseEventArgs e)
{
var lb = sender as ListBox;
lbMouseDownPosition = e.Location;
lbSelectedIndexes = new List<int>();
int idx = lb.IndexFromPoint(e.Location);
if (ModifierKeys == Keys.Shift && idx != lb.SelectedIndex) {
lbSelectedIndexes.AddRange(Enumerable.Range(
Math.Min(idx, lb.SelectedIndex),
Math.Abs((idx - lb.SelectedIndex)) + 1).ToArray());
}
}
private void listBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left &&
((Math.Abs(e.X - lbMouseDownPosition.X) > SystemInformation.DragSize.Width) ||
(Math.Abs(e.Y - lbMouseDownPosition.Y) > SystemInformation.DragSize.Height)))
{
var lb = sender as ListBox;
DataObject obj = new DataObject();
if (lbSelectedIndexes.Count == 0) {
lbSelectedIndexes = lb.SelectedIndices.OfType<int>().ToList();
}
List<object> selection = lb.Items.OfType<object>().Where((item, idx) =>
lbSelectedIndexes.IndexOf(idx) >= 0).ToList();
obj.SetData(typeof(IList<ListItemsTest>), selection);
lb.DoDragDrop(obj, DragDropEffects.Copy);
}
}
To test the results, drop another ListBox (listBox2, here) on the Form, set its AlloDrop property to true and subscribe to the DragEnter and DragDrop events.
When the Mouse Pointer enters the second ListBox client area, the DragDropEffects.Copy effect is triggered if the e.Data.GetDataPresent() method detects that the dragged object contains a List<ListItemsTest>.
If the Data format is accepted, the Data Object is transformed back to a List<ListItemsTest> - using the IDataObject.GetData() method - and set as the DataSource of listBox2.
private void listBox2_DragDrop(object sender, DragEventArgs e)
{
ListBox lb = sender as ListBox;
if (e.Data != null && e.Data.GetDataPresent(typeof(IList<ListItemsTest>))) {
lb.DisplayMember = "Name";
lb.ValueMember = "ID";
lb.DataSource = e.Data.GetData(typeof(IList<ListItemsTest>));
}
}
private void listBox2_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(typeof(IList<ListItemsTest>))) {
e.Effect = DragDropEffects.Copy;
}
}
I see the issue now. The funny thing is, pressing the Ctrl key to select items in the list works normal, but the Shift key doesn't. My solution would be to recreate your own SelectedItems collection:
void listBox1_MouseMove(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left && listBox1.SelectedItems.Count > 0) {
int mouseIndex = listBox1.IndexFromPoint(e.Location);
if (mouseIndex > -1) {
ListBox.SelectedObjectCollection x = new ListBox.SelectedObjectCollection(listBox1);
if (Control.ModifierKeys == Keys.Shift) {
int i1 = Math.Min(listBox1.SelectedIndex, mouseIndex);
int i2 = Math.Max(listBox1.SelectedIndex, mouseIndex);
for (int i = i1; i <= i2; ++i) {
x.Add(listBox1.Items[i]);
}
} else {
x = listBox1.SelectedItems;
}
var dropResult = DoDragDrop(x, DragDropEffects.Move);
}
}
}
Just to let you know I found another solution. If I set Capture = false in the MouseDown event, Items will work as expected and we don't need to do the manual selection.
Eg:
void ZListBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Capture = false;
}
}
Hope it helps!
I have a ListBox that I wanted to be drag and drop sortable, and also have extended multiselect. I was getting the same issue as the OP, where the selected items was bombing out when using shift+click to select the items. If I add in a check to make sure no modifier keys are pressed on mouse down before initiating the DoDragDrop, it solves the problem.
(this is a VB.net code snippet, but you get the idea)
Private Sub lbClasses_MouseDown(sender As Object, e As MouseEventArgs) Handles lbClasses.MouseDown
If e.Button = MouseButtons.Left AndAlso Control.ModifierKeys = 0 Then
If Not IsNothing(lbClasses.SelectedItem) Then
lbClasses.DoDragDrop(lbClasses.SelectedItem, DragDropEffects.Move)
End If
End If
End Sub
I have a datagridview in a .NET winform app. I would like to rightclick on a row and have a menu pop up. Then i would like to select things such as copy, validate, etc
How do i make A) a menu pop up B) find which row was right clicked. I know i could use selectedIndex but i should be able to right click without changing what is selected? right now i could use selected index but if there is a way to get the data without changing what is selected then that would be useful.
You can use the CellMouseEnter and CellMouseLeave to track the row number that the mouse is currently hovering over.
Then use a ContextMenu object to display you popup menu, customised for the current row.
Here's a quick and dirty example of what I mean...
private void dataGridView1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
ContextMenu m = new ContextMenu();
m.MenuItems.Add(new MenuItem("Cut"));
m.MenuItems.Add(new MenuItem("Copy"));
m.MenuItems.Add(new MenuItem("Paste"));
int currentMouseOverRow = dataGridView1.HitTest(e.X,e.Y).RowIndex;
if (currentMouseOverRow >= 0)
{
m.MenuItems.Add(new MenuItem(string.Format("Do something to row {0}", currentMouseOverRow.ToString())));
}
m.Show(dataGridView1, new Point(e.X, e.Y));
}
}
While this question is old, the answers aren't proper. Context menus have their own events on DataGridView. There is an event for row context menu and cell context menu.
The reason for which these answers aren't proper is they do not account for different operation schemes. Accessibility options, remote connections, or Metro/Mono/Web/WPF porting might not work and keyboard shortcuts will down right fail (Shift+F10 or Context Menu key).
Cell selection on right mouse click has to be handled manually. Showing the context menu does not need to be handled as this is handled by the UI.
This completely mimics the approach used by Microsoft Excel. If a cell is part of a selected range, the cell selection doesn't change and neither does CurrentCell. If it isn't, the old range is cleared and the cell is selected and becomes CurrentCell.
If you are unclear on this, CurrentCell is where the keyboard has focus when you press the arrow keys. Selected is whether it is part of SelectedCells. The context menu will show on right click as handled by the UI.
private void dgvAccount_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.ColumnIndex != -1 && e.RowIndex != -1 && e.Button == System.Windows.Forms.MouseButtons.Right)
{
DataGridViewCell c = (sender as DataGridView)[e.ColumnIndex, e.RowIndex];
if (!c.Selected)
{
c.DataGridView.ClearSelection();
c.DataGridView.CurrentCell = c;
c.Selected = true;
}
}
}
Keyboard shortcuts do not show the context menu by default, so we have to add them in.
private void dgvAccount_KeyDown(object sender, KeyEventArgs e)
{
if ((e.KeyCode == Keys.F10 && e.Shift) || e.KeyCode == Keys.Apps)
{
e.SuppressKeyPress = true;
DataGridViewCell currentCell = (sender as DataGridView).CurrentCell;
if (currentCell != null)
{
ContextMenuStrip cms = currentCell.ContextMenuStrip;
if (cms != null)
{
Rectangle r = currentCell.DataGridView.GetCellDisplayRectangle(currentCell.ColumnIndex, currentCell.RowIndex, false);
Point p = new Point(r.X + r.Width, r.Y + r.Height);
cms.Show(currentCell.DataGridView, p);
}
}
}
}
I've reworked this code to work statically, so you can copy and paste them into any event.
The key is to use CellContextMenuStripNeeded since this will give you the context menu.
Here's an example using CellContextMenuStripNeeded where you can specify which context menu to show if you want to have different ones per row.
In this context MultiSelect is True and SelectionMode is FullRowSelect. This is just for the example and not a limitation.
private void dgvAccount_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
{
DataGridView dgv = (DataGridView)sender;
if (e.RowIndex == -1 || e.ColumnIndex == -1)
return;
bool isPayment = true;
bool isCharge = true;
foreach (DataGridViewRow row in dgv.SelectedRows)
{
if ((string)row.Cells["P/C"].Value == "C")
isPayment = false;
else if ((string)row.Cells["P/C"].Value == "P")
isCharge = false;
}
if (isPayment)
e.ContextMenuStrip = cmsAccountPayment;
else if (isCharge)
e.ContextMenuStrip = cmsAccountCharge;
}
private void cmsAccountPayment_Opening(object sender, CancelEventArgs e)
{
int itemCount = dgvAccount.SelectedRows.Count;
string voidPaymentText = "&Void Payment"; // to be localized
if (itemCount > 1)
voidPaymentText = "&Void Payments"; // to be localized
if (tsmiVoidPayment.Text != voidPaymentText) // avoid possible flicker
tsmiVoidPayment.Text = voidPaymentText;
}
private void cmsAccountCharge_Opening(object sender, CancelEventArgs e)
{
int itemCount = dgvAccount.SelectedRows.Count;
string deleteChargeText = "&Delete Charge"; //to be localized
if (itemCount > 1)
deleteChargeText = "&Delete Charge"; //to be localized
if (tsmiDeleteCharge.Text != deleteChargeText) // avoid possible flicker
tsmiDeleteCharge.Text = deleteChargeText;
}
private void tsmiVoidPayment_Click(object sender, EventArgs e)
{
int paymentCount = dgvAccount.SelectedRows.Count;
if (paymentCount == 0)
return;
bool voidPayments = false;
string confirmText = "Are you sure you would like to void this payment?"; // to be localized
if (paymentCount > 1)
confirmText = "Are you sure you would like to void these payments?"; // to be localized
voidPayments = (MessageBox.Show(
confirmText,
"Confirm", // to be localized
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2
) == DialogResult.Yes);
if (voidPayments)
{
// SQLTransaction Start
foreach (DataGridViewRow row in dgvAccount.SelectedRows)
{
//do Work
}
}
}
private void tsmiDeleteCharge_Click(object sender, EventArgs e)
{
int chargeCount = dgvAccount.SelectedRows.Count;
if (chargeCount == 0)
return;
bool deleteCharges = false;
string confirmText = "Are you sure you would like to delete this charge?"; // to be localized
if (chargeCount > 1)
confirmText = "Are you sure you would like to delete these charges?"; // to be localized
deleteCharges = (MessageBox.Show(
confirmText,
"Confirm", // to be localized
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2
) == DialogResult.Yes);
if (deleteCharges)
{
// SQLTransaction Start
foreach (DataGridViewRow row in dgvAccount.SelectedRows)
{
//do Work
}
}
}
Put a context menu on your form, name it, set captions etc. using the built-in editor
Link it to your grid using the grid property ContextMenuStrip
For your grid, create an event to handle CellContextMenuStripNeeded
The Event Args e has useful properties e.ColumnIndex, e.RowIndex.
I believe that e.RowIndex is what you are asking for.
Suggestion: when user causes your event CellContextMenuStripNeeded to fire, use e.RowIndex to get data from your grid, such as the ID. Store the ID as the menu event's tag item.
Now, when user actually clicks your menu item, use the Sender property to fetch the tag. Use the tag, containing your ID, to perform the action you need.
Use the CellMouseDown event on the DataGridView. From the event handler arguments you can determine which cell was clicked. Using the PointToClient() method on the DataGridView you can determine the relative position of the pointer to the DataGridView, so you can pop up the menu in the correct location.
(The DataGridViewCellMouseEvent parameter just gives you the X and Y relative to the cell you clicked, which isn't as easy to use to pop up the context menu.)
This is the code I used to get the mouse position, then adjust for the position of the DataGridView:
var relativeMousePosition = DataGridView1.PointToClient(Cursor.Position);
this.ContextMenuStrip1.Show(DataGridView1, relativeMousePosition);
The entire event handler looks like this:
private void DataGridView1_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
// Ignore if a column or row header is clicked
if (e.RowIndex != -1 && e.ColumnIndex != -1)
{
if (e.Button == MouseButtons.Right)
{
DataGridViewCell clickedCell = (sender as DataGridView).Rows[e.RowIndex].Cells[e.ColumnIndex];
// Here you can do whatever you want with the cell
this.DataGridView1.CurrentCell = clickedCell; // Select the clicked cell, for instance
// Get mouse position relative to the vehicles grid
var relativeMousePosition = DataGridView1.PointToClient(Cursor.Position);
// Show the context menu
this.ContextMenuStrip1.Show(DataGridView1, relativeMousePosition);
}
}
}
Follow the steps:
Create a context menu like:
User needs to right click on the row to get this menu. We need to handle the _MouseClick event and _CellMouseDown event.
selectedBiodataid is the variable that contains the selected row information.
Here is the code:
private void dgrdResults_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
contextMenuStrip1.Show(Cursor.Position.X, Cursor.Position.Y);
}
}
private void dgrdResults_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
//handle the row selection on right click
if (e.Button == MouseButtons.Right)
{
try
{
dgrdResults.CurrentCell = dgrdResults.Rows[e.RowIndex].Cells[e.ColumnIndex];
// Can leave these here - doesn't hurt
dgrdResults.Rows[e.RowIndex].Selected = true;
dgrdResults.Focus();
selectedBiodataId = Convert.ToInt32(dgrdResults.Rows[e.RowIndex].Cells[1].Value);
}
catch (Exception)
{
}
}
}
and the output would be:
Simply drag a ContextMenu or ContextMenuStrip component into your form and visually design it, then assign it to the ContextMenu or ContextMenuStrip property of your desired control.
For the position for the context menu, y found the problem that I needed a it to be relative to the DataGridView, and the event I needed to use gives the poistion relative to the cell clicked. I haven't found a better solution so I implemented this function in the commons class, so I call it from wherever I need.
It's quite tested and works well. I Hope you find it useful.
/// <summary>
/// When DataGridView_CellMouseClick ocurs, it gives the position relative to the cell clicked, but for context menus you need the position relative to the DataGridView
/// </summary>
/// <param name="dgv">DataGridView that produces the event</param>
/// <param name="e">Event arguments produced</param>
/// <returns>The Location of the click, relative to the DataGridView</returns>
public static Point PositionRelativeToDataGridViewFromDataGridViewCellMouseEventArgs(DataGridView dgv, DataGridViewCellMouseEventArgs e)
{
int x = e.X;
int y = e.Y;
if (dgv.RowHeadersVisible)
x += dgv.RowHeadersWidth;
if (dgv.ColumnHeadersVisible)
y += dgv.ColumnHeadersHeight;
for (int j = 0; j < e.ColumnIndex; j++)
if (dgv.Columns[j].Visible)
x += dgv.Columns[j].Width;
for (int i = 0; i < e.RowIndex; i++)
if (dgv.Rows[i].Visible)
y += dgv.Rows[i].Height;
return new Point(x, y);
}
There is simple answer to this topic and that is to use CellMouseDown
After designing your ContextMenu
// somewhere in your code
ContextMenu cm = new ContextMenu();
cm.MenuItems.Add(new MenuItem("Option1"));
cm.MenuItems.Add(new MenuItem("Option2"));
Assign it to the DataGridView
myDataView.ContextMenu = cm;
Get Data from the CLICKED cell without changing the Selected one
private void myDataView_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
string myData = myDataView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString();
// Now ADD the captured DATA to the ContextMenu
cm.MenuItems.Add(new MenuItem("myData"));
// OR if you are doing it by the designer and YOU ALREADY have an Item
// you can simply change the name of it in the designer page to your
// desired one and then just change the Text of it
MenuItem_BTN.Text = "$Properties of {myData}";
}
}
We have a DataGridView with data in a form. To enable quick search, we added TextBox to DataGridView.Controls and highlight cells which contain text from TextBox.
However, there is an issue. DataGridView consumes the Left arrow ←, Right arrow →, Home and End (with or without Shift) keys even if the cursor is in TextBox, and the user cannot change the caret position or select text from the keyboard.
TextBox generates a PreviewKeyDown event and nothing more happens.
Simplified code:
public partial class TestForm : Form
{
public TestForm()
{
InitializeComponent();
Width = 400;
Height = 400;
var txt = new TextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };
var dgv = new DataGridView
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 5
};
dgv.Controls.Add(txt);
Controls.Add(dgv);
dgv.PreviewKeyDown += DgvOnPreviewKeyDown;
dgv.KeyDown += DgvOnKeyDown;
txt.PreviewKeyDown += TxtOnPreviewKeyDown;
txt.KeyDown += TxtOnKeyDown;
}
private void DgvOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
Debug.WriteLine(String.Format("Dgv Key Preview {0}", e.KeyCode));
e.IsInputKey = true;
}
private void DgvOnKeyDown(object sender, KeyEventArgs e)
{
Debug.WriteLine(String.Format("Dgv Key {0}", e.KeyCode));
}
private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
Debug.WriteLine(String.Format("Txt Key Preview {0}", e.KeyCode));
}
private void TxtOnKeyDown(object sender, KeyEventArgs e)
{
Debug.WriteLine(String.Format("Txt Key {0}", e.KeyCode));
}
}
Type 123 in TextBox and then try the Right arrow, Left arrow, End, or Home. DataGridView change the selected cell, but the TextBox caret doesn't move.
TextBox works just fine if not inside a DataGridView (no problem at all when using the same method adding it into TreeView for example). TextBox acts similar to the Quick search Panel in the browser and has to be on top of the DataGridView. Adding a TextBox to a Form (or to be more specific, to a DataGridView parent) creates its own set of issues (tracking Location, Size, Visibility, ...) and is not acceptable.
What can be done to make sure that TextBox receive those keys and change the caret position or select text?
TextBox works just fine if not inside DataGridView (no problem at all when using the same method adding it into TreeView for example)
Apparently the problem is in DataGridView. It's because DataGridView overrides the Control.ProcessKeyPreview method:
This method is called by a child control when the child control receives a keyboard message. The child control calls this method before generating any keyboard events for the message. If this method returns true, the child control considers the message processed and does not generate any keyboard events.
The DataGridView implementation does just that - it maintains zero or one child controls internally (EditingControl), and when there is no such control active, it handles many keys (navigation, tab, enter, escape, etc.) by returning true, thus preventing the child TextBox keyboard events generation. The return value is controlled by the ProcessDataGridViewKey method.
Since the method is virtual, you can replace the DataGridView with a custom derived class which overrides the aforementioned method and prevents the undesired behavior when neither the view nor the view active editor (if any) has the keyboard focus.
Something like this:
public class CustomDataGridView : DataGridView
{
bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
(EditingControl == null || !EditingControl.ContainsFocus);
protected override bool ProcessDataGridViewKey(KeyEventArgs e)
{
if (SuppressDataGridViewKeyProcessing) return false;
return base.ProcessDataGridViewKey(e);
}
}
The above is just the half of the story and solves the cursor navigation and selection keys issue. However DataGridView intercepts another key message preprocessing infrastructure method - Control.ProcessDialogKey and handles Tab, Esc, Return, etc. keys there. So in order to prevent that, the method has to be overridden as well and redirected to the parent of the data grid view. The later needs a little reflection trickery to call a protected method, but using one time compiled delegate at least avoids the performance hit.
With that addition, the final custom class would be like this:
public class CustomDataGridView : DataGridView
{
bool SuppressDataGridViewKeyProcessing => ContainsFocus && !Focused &&
(EditingControl == null || !EditingControl.ContainsFocus);
protected override bool ProcessDataGridViewKey(KeyEventArgs e)
{
if (SuppressDataGridViewKeyProcessing) return false;
return base.ProcessDataGridViewKey(e);
}
protected override bool ProcessDialogKey(Keys keyData)
{
if (SuppressDataGridViewKeyProcessing)
{
if (Parent != null) return DefaultProcessDialogKey(Parent, keyData);
return false;
}
return base.ProcessDialogKey(keyData);
}
static readonly Func<Control, Keys, bool> DefaultProcessDialogKey =
(Func<Control, Keys, bool>)Delegate.CreateDelegate(typeof(Func<Control, Keys, bool>),
typeof(Control).GetMethod(nameof(ProcessDialogKey), BindingFlags.NonPublic | BindingFlags.Instance));
}
You can try this.
I created my own textbox and overrode method ProcessKeyMessage.
public class MyTextBox : TextBox
{
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
protected override bool ProcessKeyMessage(ref Message m)
{
if (m.Msg != WM_SYSKEYDOWN && m.Msg != WM_KEYDOWN)
{
return base.ProcessKeyMessage(ref m);
}
Keys keyData = (Keys)((int)m.WParam);
switch (keyData)
{
case Keys.Left:
case Keys.Right:
case Keys.Home:
case Keys.End:
case Keys.ShiftKey:
return base.ProcessKeyEventArgs(ref m);
default:
return base.ProcessKeyMessage(ref m);
}
}
}
And then you can call:
var txt = new MyTextBox { Dock = DockStyle.Bottom, BackColor = Color.Khaki };
Try to just add the TextBox to the main form instead of the DataGridView:
Controls.Add(txt);
Controls.Add(dgv);
txt.PreviewKeyDown += DgvOnPreviewKeyDown;
txt.KeyDown += DgvOnKeyDown;
txt.PreviewKeyDown += TxtOnPreviewKeyDown;
txt.KeyDown += TxtOnKeyDown;
It sounds a bit like an exercise in futility.
It may be easier to encapsulate the behavior of both the TextBox and DataGridView controls by placing them into a UserControl together with a little code to handle events.
Here is a partial solution to the issue. TextBox still doesn't receive navigation keys input natively, but I reproduced a normal caret and selection behavior.
PreviewKeyDownEventArgs contains information about the pressed key and modifiers (Shift). For each key combination I set a new SelectionStart and SelectionLength for the TextBox.
private void TxtOnPreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
TextBox txt = (TextBox)sender;
if (e.KeyCode == Keys.Home)
{
int idx = txt.SelectionStart;
txt.SelectionStart = 0;
txt.SelectionLength = e.Shift ? idx : 0;
}
else if (e.KeyCode == Keys.End)
{
int idx = txt.SelectionStart;
if (e.Shift)
txt.SelectionLength = txt.TextLength - idx;
else
{
txt.SelectionStart = txt.TextLength;
txt.SelectionLength = 0;
}
}
else if (e.KeyCode == Keys.Left)
{
if (e.Shift)
{
if (txt.SelectionStart > 0)
{
txt.SelectionStart--;
txt.SelectionLength++;
}
}
else
{
txt.SelectionStart = Math.Max(0, txt.SelectionStart - 1);
txt.SelectionLength = 0;
}
}
else if (e.KeyCode == Keys.Right)
{
if (e.Shift)
txt.SelectionLength++;
else
{
txt.SelectionStart = Math.Min(txt.TextLength, txt.SelectionStart + 1);
txt.SelectionLength = 0;
}
}
}
I got a windows form with around 140 numericUpDown elements and want all of them to do this:
private void numericUpDown_B1_RS_LS_Leave(object sender, EventArgs e)
{
if (numericUpDown_B1_RS_LS.Value < 1 || numericUpDown_B1_RS_LS.Value > 6)
{
numericUpDown_B1_RS_LS.BackColor = Color.Red;
}
else
{
numericUpDown_B1_RS_LS.BackColor = Color.White;
}
}
Is there a more comfortable way of doing this than just adding 140 functions to the form manually?
Assuming you want the same method for all NUDs on your form this will do it:
public Form1()
{
InitializeComponent();
foreach (Control ctl in this.Controls)
if (ctl.GetType() == typeof(NumericUpDown) ) ctl.Leave += numericUpDown_Leave;
}
private void numericUpDown_Leave(object sender, EventArgs e)
{
NumericUpDown NumericUD = (NumericUpDown) sender ;
if (NumericUD.Value < 1 || NumericUD.Value > 6)
{
NumericUD.BackColor = Color.Red;
}
else
{
NumericUD.BackColor = Color.White;
}
}
You can just as easily iterate over the Controls collection of another container, say a Panel or a GroupBox if the NUDs are not directly sitting on the Form. And if some NUDs should be excepted from that behaviour you could mark them in some way, maybe in their Tag or Name and check for that before adding their handlers ..
You should not write multiple functions for same code. Instead you can create just one function and assign it to the event method of your numericUpDown element i.e. "UpDown" event.
So whenever any of the element's "UpDown" event will fire, your same function will be executed. And as far as you want to write different methods for different elements, then consider that in your method
Method(object sender, EventArguments e)
sender is the actual sender object of your updownElement type, and that code will be executed for that particular object only.
You can take for loop to iterate your all 140 elements and assign this function to the "UpDown" element.
maybe you could write a static method to handle the adding of event handler recursively, something like:
private void frmMain_Load(object sender, EventArgs e)
{
AddHandleNumericUpdownLeave(this);
}
public static void AddHandleNumericUpdownLeave(Control theContrl)
{
if (theContrl.Controls != null && theContrl.Controls.Count > 0)
{
foreach (Control cControl in theContrl.Controls)
{
AddHandleNumericUpdownLeave(cControl);
}
}
else
{
NumericUpDown nudCtrl = theContrl as NumericUpDown;
if (nudCtrl != null)
{
nudCtrl.Leave += (object senderT, EventArgs eT) =>
{
var tmpCtrl = senderT as NumericUpDown;
if (tmpCtrl != null)
{
if (tmpCtrl.Value < 1 || tmpCtrl.Value > 6)
{
tmpCtrl.BackColor = Color.Red;
}
else
{
tmpCtrl.BackColor = Color.White;
}
}
}
}
}
}
but this might be costly if you have too many controls in a form...
with a form like this:
I wrote this piece of code to take care of enable/disable logic for moveup/down buttons when they click on at item ( we don't care about Avaiable list on the left, we just care about Selected list on the right)
private void SelectedLV_SelectedIndexChanged(object sender, EventArgs e)
{
// what to do wth move up button
if (SelectedLV.SelectedIndices.Count == 1 && SelectedLV.SelectedItems[0].Index > 0)
{
MoveUpBtn.Enabled = true;
}
else
{
MoveUpBtn.Enabled = false;
}
//what to do with move down button
if (SelectedLV.SelectedIndices.Count == 1 && SelectedLV.SelectedItems[0].Index < SelectedLV.Items.Count - 1)
{
MoveDownBtn.Enabled = true;
}
else
{
MoveDownBtn.Enabled = false;
}
}
I think it works fine for that scenario but my question is what about when we click off of Selected Listview, What is good logic to handle that and Disable Both Moveup/Down buttons?
I don't want them be enabled when we are not inside SelectedListView...
Also if you notice any issue with the code I pasted please let me know.
Thanks
You are about to shoot your foot with the focus requirement. These kind of UI updates are best done with the Application.Idle event, it only runs when nothing important is happening. And can help to eliminate a lot of event handlers. Like this:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
Application.Idle += Application_Idle;
this.FormClosed += delegate { Application.Idle -= Application_Idle; };
}
void Application_Idle(object sender, EventArgs e) {
bool focusOk = this.ActiveControl == SelectedLV;
bool selectOk = SelectedLV.SelectedIndices.Count == 1;
int index = selectOk ? SelectedLV.SelectedIndices[0] : -1;
MoveUpBtn.Enabled = focusOk && selectOk && index > 0;
MoveDownBtn.Enabled = focusOk && selectOk && index < SelectedLV.Items.Count-1;
}
}
Don't forget to set the focus back in the buttons' Click event handler. And don't forget about the ListView.HideSelection property. Set it to False so that focus doesn't matter anymore.
The problem is once you click on the Move buttons, then you are outside of the SelectedListView control, so the logic should really be based on if you have a correct index value or not:
private void SelectedLV_SelectedIndexChanged(object sender, EventArgs e)
if (SelectedLV.SelectedIndicies.Count == 0) {
MoveUpBtn.Enabled = false;
MoveDownBtn.Enabled = false;
} else {
// normal processing
}