How to get the selected SubItem index in a Listview and highlight it?

Viewed 743

I am trying to get the selected ListViewItem index, so that when the user clicks on a particular row, such as row 2, I want the clicked cell's text to be set to a TextBox's text.

I also want to highlight only this cell, ideally using the regular selection ListView uses, or do I need to create a class that inherits from ListView to do this?

Something like this:

enter image description here enter image description here

1 Answers

You can draw yourself the ListViewItem.ListViewSubItem selected, owner-drawing the Control (set ListView.OwnerDraw = true), then handle the ListView.DrawSubItem event.
The ListView.DrawColumnHeader event can use default values.

▶ I'm using TextRenderer since this is the default renderer. If you use Graphics.DrawText, you'll notice the difference.

TextFormatFlags flags = TextFormatFlags.LeftAndRightPadding |
                        TextFormatFlags.VerticalCenter;

private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
    var lv = sender as ListView;
    var subItem = lv.HitTest(lv.PointToClient(MousePosition)).SubItem;

    if (subItem != null && e.SubItem == subItem) {
        using (var brush = new SolidBrush(SystemColors.Highlight)) {
            e.Graphics.FillRectangle(brush, e.SubItem.Bounds);
        }
        TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.SubItem.Font, 
                              e.Bounds, SystemColors.HighlightText, flags);
    }
    else {
        e.DrawDefault = true;
    }
}

private void listView1_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e) 
    => e.DrawDefault = true;

// Invalidate on a mouse interaction, otherwise the ListView doesn't redraw the SubItem
private void listView1_MouseUp(object sender, MouseEventArgs e)
    => (sender as ListView).Invalidate();

Or, you can change the Colors of a SubItem when a mouse interaction is notified (here, using the MouseDown event) and save the previous state (just the Colors here). It's better to save the state because each SubItem can have it's own settings, so you cannot just revert back to the Parent ListViewItem or the ListView values.

As mentioned, set UseItemStyleForSubItems = false in each parent ListViewItem, otherwise the Colors settings are ignored.
Also, FullRowSelect must be set to false, otherwise it's pointless :)

Here, the state is saved in a nullable named Tuple Field, (ListViewSubItem, Color[]).
A class object is probably better, this is just shorter.

private (ListViewItem.ListViewSubItem Item, Color[] colors)? previousItem = null;

private void listView1_MouseDown(object sender, MouseEventArgs e)
{
    var lv = sender as ListView;
    var subItem = lv.HitTest(e.Location).SubItem;

    if (previousItem.HasValue) {
        // If an Item's Colors have been changed, restore the state
        // It removes the selection if you click in an empty area
        previousItem.Value.Item.BackColor = previousItem.Value.colors[0];
        previousItem.Value.Item.ForeColor = previousItem.Value.colors[1];
        lv.Invalidate(previousItem.Value.Item.Bounds);
    }

    if (subItem != null) {
        // Save the SubItem's colors state
        previousItem = (subItem, new[] { subItem.BackColor, subItem.ForeColor });
        // Set new Colors. Here, using the default highlight colors
        subItem.BackColor = SystemColors.Highlight;
        subItem.ForeColor = SystemColors.HighlightText;
        lv.Invalidate(subItem.Bounds);
    }
}

This is how this thing works:

ListView SubItem Selection


▶ About the Item / SubItem index, as it's mentioned in the question:

When you retrieve the ListViewItem / SubItem clicked with ListView.HitTest

 var hitTest = lv.HitTest(e.Location);

then the ListViewItem index is of course:

var itemIndex = hitTest.Item.Index;

and the SubItem.Index is:

var subItemIndex = hitTest.Item.SubItems.IndexOf(hitTest.SubItem);
Related