4

I'm trying to change the default (blue) color of selection bar in ListView.
I'm refusing to use ObjectListView since I'll have to change all the code.

I've searched on this subject and found some answers here:
Change background selection color of ListView?
but that points to ObjectListView.

When I was using ListBox before, this worked to set the selection bar color to my likings:

  1. Set DrawMode to OwnerDrawFixed under Properties
  2. Set DrawItem to ListBox1_DrawItem under Events

private void ListBox1_DrawItem(object sender, DrawItemEventArgs e)
{
    if (e.Index < 0) return;
    //if the item state is selected them change the back color 
    if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
        e = new DrawItemEventArgs(e.Graphics,
                                  e.Font,
                                  e.Bounds,
                                  e.Index,
                                  e.State ^ DrawItemState.Selected,
                                  e.ForeColor,
                                  Color.FromArgb(43, 144, 188));//Choose the color

    // Draw the background of the ListBox control for each item.
    e.DrawBackground();
    // Draw the current item text
    e.Graphics.DrawString(lb_result.Items[e.Index].ToString(), e.Font, Brushes.Black, e.Bounds, StringFormat.GenericDefault);
    // If the ListBox has focus, draw a focus rectangle around the selected item.
    e.DrawFocusRectangle();
}

But I'm now using ListView.

  1. I set OwnerDraw to True
  2. I set DrawItem to ListView1_DrawItem

...and use the code from above.

I expected it to show me a different selection color as stated, but instead I get a few errors:

errors

How would I use this code correctly for a ListView?

Jimi
  • 29,621
  • 8
  • 43
  • 61
Bas Curtiz
  • 73
  • 8
  • You just need: `if (e.Item.Selected) =>` draw the background (`e.Graphics.FillRectangle()`) and the text. If not selected, `e.DrawDefault = true;`. You can fill the rectangle, using the `e.Bounds` measure, with whatever color you want. You also need to draw the Bitmaps, should the ListView contain any. – Jimi Apr 03 '19 at 21:46
  • @Jimi Could u show me example of the code please? I'm trying but not getting it right. – Bas Curtiz Apr 04 '19 at 09:42

1 Answers1

6

Owner-drawing a ListView control is more complicated than a ListBox control: many more details to take care of. This is an example that considers four View settings of a ListView:
View.Details, View.List, View.Tile and View.SmallIcon.

Only the Text is drawn here (that's why View.LargeIcon is not included), to contain the code to a decent limit.
An example of drawing the Bitmaps included in a ImageList linked to the ListView is here.

Set up the ListView:
Enable your ListView OwnerDraw mode, then subscribe to its DrawItem, DrawSubItem and DrawColumnHeader events as shown in the sample code (mandatory, if you want the ListView to show anything).

The Headers are painted using the default rendering (setting e.DrawDefault = true).

Description of common operations:
The Item Text is drawn using TextRenderer.DrawText: this is the original method used by the ListView to draw its items. It allows to match exactly the default rendering, so we won't notice some mis-alignment of the text.

The DrawItem event is used to draw a custom background in all View modes and will draw the Items' text in all mode except View.Details, where the DrawSubItems event comes into play: we would draw the first Item's text twice, should the DrawItem event perform the same task.

The DrawSubItems event is not called when the View is set to Tile or List.

Details on the code presented here:
A helper method, GetTextAlignment, takes care of setting the Items' alignment, since each Column can have a specific text alignment.

A field, Color listViewSelectionColor, is used to set/change the Color of the select Items. To modify the selection color, set this filed to any value and Invalidate() the ListView: the new Color will be applied immediately.

Sample of the results:

ListView OwnerDraw

bool lvEditMode = false;
Color listViewSelectionColor = Color.Orange;

protected void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
{
    var lView = sender as ListView;

    if (lvEditMode || lView.View == View.Details) return;
    TextFormatFlags flags = GetTextAlignment(lView, 0);
    Color itemColor = e.Item.ForeColor;

    if (e.Item.Selected) {
        using (var bkBrush = new SolidBrush(listViewSelectionColor)) {
            e.Graphics.FillRectangle(bkBrush, e.Bounds);
        }
        itemColor = e.Item.BackColor;
    }
    else {
        e.DrawBackground();
    }

    TextRenderer.DrawText(e.Graphics, e.Item.Text, e.Item.Font, e.Bounds, itemColor, flags);

    if (lView.View == View.Tile && e.Item.SubItems.Count > 1) {
        var subItem = e.Item.SubItems[1];
        flags = GetTextAlignment(lView, 1);
        TextRenderer.DrawText(e.Graphics, subItem.Text, subItem.Font, e.Bounds, SystemColors.GrayText, flags);
    }
}

private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
    var lView = sender as ListView;
    TextFormatFlags flags = GetTextAlignment(lView, e.ColumnIndex);
    Color itemColor = e.Item.ForeColor;

    if (e.Item.Selected && !lvEditMode) {
        if (e.ColumnIndex == 0 || lView.FullRowSelect) {
            using (var bkgrBrush = new SolidBrush(listViewSelectionColor)) {
                e.Graphics.FillRectangle(bkgrBrush, e.Bounds);
            }
            itemColor = e.Item.BackColor;
        }
    }
    else  {
        e.DrawBackground();
    }
    TextRenderer.DrawText(e.Graphics, e.SubItem.Text, e.SubItem.Font, e.Bounds, itemColor, flags);
}

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

private TextFormatFlags GetTextAlignment(ListView lstView, int colIndex)
{
    TextFormatFlags flags = (lstView.View == View.Tile)
        ? (colIndex == 0) ? TextFormatFlags.Default : TextFormatFlags.Bottom
        : TextFormatFlags.VerticalCenter;

    if (lstView.View == View.Details) flags |= TextFormatFlags.LeftAndRightPadding;

    if (lstView.Columns[colIndex].TextAlign != HorizontalAlignment.Left) {
        flags |= (TextFormatFlags)((int)lstView.Columns[colIndex].TextAlign ^ 3);
    }
    return flags;
}

private void listView1_BeforeLabelEdit(object sender, LabelEditEventArgs e) => lvEditMode = true;

private void listView1_AfterLabelEdit(object sender, LabelEditEventArgs e) => lvEditMode = false;  
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thank you for your indepth feedback. Works like a charm! – Bas Curtiz Apr 05 '19 at 09:59
  • Only thing is, the other columns I'm using (listView Details mode) disappear when I hover my mouse over the 1st result in the listView: https://imgur.com/Fkst2Ux Any idea if this is related to the code or do I need to assign a MouseEnter? – Bas Curtiz Apr 05 '19 at 10:55
  • Since it doesn't happen with the code I posted here, it might be related to other events in your ListView I'm not aware of. Can you update the question with the code you're using now, so I can test it? – Jimi Apr 05 '19 at 11:08
  • I think it's related to this (see note): https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.listview.drawitem?redirectedfrom=MSDN&view=netframework-4.7.2#remarks and this: https://stackoverflow.com/questions/45643212/vb-net-listview-ownerdraw-subitems-disappear-on-hovering – Bas Curtiz Apr 09 '19 at 20:44
  • This is already handled in the code presented here. Read the **Description of common operations:** section. You need to implement your code as described here. Make a test ListView with 3 Columns with some Items/SubItems and use this code exactly how it is. It should behave as you can see in the graphic sample. – Jimi Apr 09 '19 at 21:22
  • It is mainly resolved by invoking the mouse click event. Thanks again! – Bas Curtiz Apr 28 '19 at 13:49
  • 2 things though: 1. When I use the "&" character it changes to a "_" character when selected. 2. When selected the content of ColumnHeader moves a bit to the right, since the space in front gets wider. Example 1 here: https://imgur.com/a/BXfVgek Example 2 here: https://imgur.com/a/e05WfJD Source code here: https://mega.nz/#!dZ5GzSYa!Ks6kjoA-DuHS0cdXM6M1nadBaN_VOQdYugeO7g0kmwE – Bas Curtiz May 09 '19 at 19:02
  • You're right on both details. 1) I added `TextFormatFlags.NoPrefix` to the flags: it will not convert the accelerators (`&`) and ampersand escaping (`&&`). 2) In `View.List` mode, `TextFormatFlags` padding is turned off because the ListView control doesn't use it. It adds a *padding* of its own. Updated the code. Let me know. – Jimi May 09 '19 at 20:59
  • 1) & + && are fixed, thanks to your updated code! 2) It's still jumping a bit due the added space in the front, I think. You can see it clearly in the SubColumns. Example here: https://imgur.com/a/pc65suq Source code here: https://mega.nz/#!BZx03CKY!UxKTc2JnGAy7uco1LVxgFP1SAh90T9DstZxEBmbYl6o Any solution for 2) would be highly appreciated. – Bas Curtiz May 10 '19 at 21:47
  • Note, in the animation you see here, that I don't have that effect in details view. You're using FW `4.5.2`, I'm using FW `4.7.2`. FW `4.7+` has brough in some changes in WinForms: RichTextBox, completely new (RichEdit 50w instead of 20w), DataGridView, ListView, TreeView, MonthCalendar, ContextMenuStrip/Toolstrip have some changes, but all are now supporting the new DPIAwareness logic introduced in Windows 10. Anyway, this kind of procedure should be *compatible*, so I've rewritten it keeping this in mind. The `FullRowSelect` behaviour is changed. Let me know. – Jimi May 11 '19 at 00:32
  • Wow, never knew there was such a big difference between the FW versions since 4.5.2. I've added your latest code and altered the Back/Forecolor to my likings. It's working FLAWLESS now! Thanks again Jimi. – Bas Curtiz May 11 '19 at 13:02